mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-10 21:48:48 +08:00
feat: add show workspace names and members
This commit is contained in:
+62
-6
@@ -11,20 +11,72 @@ import {
|
|||||||
LoginItem,
|
LoginItem,
|
||||||
} from './WorkspaceItem';
|
} from './WorkspaceItem';
|
||||||
import { WorkspaceSetting } from '@/components/workspace-setting';
|
import { WorkspaceSetting } from '@/components/workspace-setting';
|
||||||
import { useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { WorkspaceType } from '@pathfinder/data-services';
|
import { getWorkspaceDetail, WorkspaceType } from '@pathfinder/data-services';
|
||||||
|
|
||||||
export const SelectorPopperContent = () => {
|
type WorkspaceDetails = Record<
|
||||||
const { user, workspacesMeta } = useAppState();
|
string,
|
||||||
|
{ memberCount: number; owner: { id: string; name: string } }
|
||||||
|
>;
|
||||||
|
|
||||||
|
type SelectorPopperContentProps = {
|
||||||
|
isShow: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const SelectorPopperContent = ({
|
||||||
|
isShow,
|
||||||
|
}: SelectorPopperContentProps) => {
|
||||||
|
const { user, workspacesMeta, workspaces } = useAppState();
|
||||||
const [settingWorkspaceId, setSettingWorkspaceId] = useState<string | null>(
|
const [settingWorkspaceId, setSettingWorkspaceId] = useState<string | null>(
|
||||||
null
|
null
|
||||||
);
|
);
|
||||||
|
const [workSpaceDetails, setWorkSpaceDetails] = useState<WorkspaceDetails>(
|
||||||
|
{}
|
||||||
|
);
|
||||||
const handleClickSettingWorkspace = (workspaceId: string) => {
|
const handleClickSettingWorkspace = (workspaceId: string) => {
|
||||||
setSettingWorkspaceId(workspaceId);
|
setSettingWorkspaceId(workspaceId);
|
||||||
};
|
};
|
||||||
const handleCloseWorkSpace = () => {
|
const handleCloseWorkSpace = () => {
|
||||||
setSettingWorkspaceId(null);
|
setSettingWorkspaceId(null);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const refreshDetails = useCallback(async () => {
|
||||||
|
const workspaceDetailList = await Promise.all(
|
||||||
|
workspacesMeta?.map(async ({ id, type }) => {
|
||||||
|
if (user) {
|
||||||
|
if (type === WorkspaceType.Private) {
|
||||||
|
return { id, member_count: 1, owner: user };
|
||||||
|
} else {
|
||||||
|
const data = await getWorkspaceDetail({ id });
|
||||||
|
return { id, ...data } || { id, member_count: 0, owner: user };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
let workSpaceDetails: WorkspaceDetails = {};
|
||||||
|
workspaceDetailList.forEach(details => {
|
||||||
|
if (details) {
|
||||||
|
const { id, member_count, owner } = details;
|
||||||
|
if (!owner) return;
|
||||||
|
workSpaceDetails[id] = {
|
||||||
|
memberCount: member_count || 1,
|
||||||
|
owner: {
|
||||||
|
id: owner.id,
|
||||||
|
name: owner.name,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
setWorkSpaceDetails(workSpaceDetails);
|
||||||
|
}, [user, workspacesMeta]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isShow) {
|
||||||
|
setSettingWorkspaceId(null);
|
||||||
|
refreshDetails();
|
||||||
|
}
|
||||||
|
}, [isShow, refreshDetails]);
|
||||||
|
|
||||||
return !user ? (
|
return !user ? (
|
||||||
<SelectorPopperContainer placement="bottom-start">
|
<SelectorPopperContainer placement="bottom-start">
|
||||||
<LoginItem />
|
<LoginItem />
|
||||||
@@ -47,9 +99,13 @@ export const SelectorPopperContent = () => {
|
|||||||
type={workspace.type}
|
type={workspace.type}
|
||||||
key={workspace.id}
|
key={workspace.id}
|
||||||
id={workspace.id}
|
id={workspace.id}
|
||||||
name={`workspace-${workspace.id}`}
|
icon={workspaces[workspace.id]?.meta.avatar || ''}
|
||||||
icon={''}
|
|
||||||
onClick={handleClickSettingWorkspace}
|
onClick={handleClickSettingWorkspace}
|
||||||
|
name={
|
||||||
|
workspaces[workspace.id]?.meta.name ||
|
||||||
|
`workspace-${workspace.id}`
|
||||||
|
}
|
||||||
|
memberCount={workSpaceDetails[workspace.id]?.memberCount || 1}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|||||||
+39
-15
@@ -1,4 +1,8 @@
|
|||||||
import { createWorkspace } from '@pathfinder/data-services';
|
import {
|
||||||
|
createWorkspace,
|
||||||
|
getBlob,
|
||||||
|
uploadBlob,
|
||||||
|
} from '@pathfinder/data-services';
|
||||||
import Modal from '@/ui/modal';
|
import Modal from '@/ui/modal';
|
||||||
import Input from '@/ui/input';
|
import Input from '@/ui/input';
|
||||||
import { Button } from '@/ui/button';
|
import { Button } from '@/ui/button';
|
||||||
@@ -19,6 +23,14 @@ interface WorkspaceCreateProps {
|
|||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DefaultHeadImgColors = [
|
||||||
|
['#C6F2F3', '#0C6066'],
|
||||||
|
['#FFF5AB', '#896406'],
|
||||||
|
['#FFCCA7', '#8F4500'],
|
||||||
|
['#FFCECE', '#AF1212'],
|
||||||
|
['#E3DEFF', '#511AAB'],
|
||||||
|
];
|
||||||
|
|
||||||
export const WorkspaceCreate = ({ open, onClose }: WorkspaceCreateProps) => {
|
export const WorkspaceCreate = ({ open, onClose }: WorkspaceCreateProps) => {
|
||||||
const [workspaceName, setWorkspaceId] = useState<string>('');
|
const [workspaceName, setWorkspaceId] = useState<string>('');
|
||||||
const [canCreate, setCanCreate] = useState<boolean>(false);
|
const [canCreate, setCanCreate] = useState<boolean>(false);
|
||||||
@@ -30,22 +42,34 @@ export const WorkspaceCreate = ({ open, onClose }: WorkspaceCreateProps) => {
|
|||||||
canvas.height = 100;
|
canvas.height = 100;
|
||||||
canvas.width = 100;
|
canvas.width = 100;
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
if (ctx) {
|
return new Promise<string>((resolve, reject) => {
|
||||||
// TODO: add a image list
|
if (ctx) {
|
||||||
ctx.fillStyle = '#FFDE03';
|
const randomNumber = Math.floor(Math.random() * 5);
|
||||||
ctx.fillRect(0, 0, 100, 100);
|
const randomColor = DefaultHeadImgColors[randomNumber];
|
||||||
ctx.font = "600 50px 'PingFang SC', 'Microsoft Yahei'";
|
ctx.fillStyle = randomColor[0];
|
||||||
ctx.fillStyle = '#FFF';
|
ctx.fillRect(0, 0, 100, 100);
|
||||||
ctx.textAlign = 'center';
|
ctx.font = "600 50px 'PingFang SC', 'Microsoft Yahei'";
|
||||||
ctx.textBaseline = 'middle';
|
ctx.fillStyle = randomColor[1];
|
||||||
ctx.fillText(workspaceName[0], 50, 50);
|
ctx.textAlign = 'center';
|
||||||
const blob = canvas.toBlob(blob => {}, 'image/png');
|
ctx.textBaseline = 'middle';
|
||||||
}
|
ctx.fillText(workspaceName[0], 50, 50);
|
||||||
|
canvas.toBlob(blob => {
|
||||||
|
if (blob) {
|
||||||
|
const blobId = uploadBlob({ blob });
|
||||||
|
resolve(blobId);
|
||||||
|
} else {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
}, 'image/png');
|
||||||
|
} else {
|
||||||
|
reject();
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
const handleCreateWorkspace = () => {
|
const handleCreateWorkspace = async () => {
|
||||||
setCanCreate(true);
|
setCanCreate(true);
|
||||||
createDefaultHeadImg(workspaceName);
|
const blobId = await createDefaultHeadImg(workspaceName);
|
||||||
createWorkspace({ name: workspaceName, avatar: '' })
|
createWorkspace({ name: workspaceName, avatar: `/api/blob/${blobId}` })
|
||||||
.then(data => {
|
.then(data => {
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
router.push(`/workspace/${data.created_at}`);
|
router.push(`/workspace/${data.created_at}`);
|
||||||
|
|||||||
+4
-2
@@ -1,13 +1,15 @@
|
|||||||
import { SettingsIcon } from '@blocksuite/icons';
|
import { SettingsIcon } from '@blocksuite/icons';
|
||||||
import { styled } from '@/styles';
|
import { styled } from '@/styles';
|
||||||
import { IconButton } from '@/ui/button';
|
import { IconButton } from '@/ui/button';
|
||||||
|
import { MouseEventHandler } from 'react';
|
||||||
|
|
||||||
type SettingProps = {
|
type SettingProps = {
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const FooterSetting = ({ onClick }: SettingProps) => {
|
export const FooterSetting = ({ onClick }: SettingProps) => {
|
||||||
const handleClick = () => {
|
const handleClick: MouseEventHandler<HTMLButtonElement> = e => {
|
||||||
|
e.stopPropagation();
|
||||||
onClick && onClick();
|
onClick && onClick();
|
||||||
};
|
};
|
||||||
return (
|
return (
|
||||||
@@ -15,7 +17,7 @@ export const FooterSetting = ({ onClick }: SettingProps) => {
|
|||||||
className="footer-setting"
|
className="footer-setting"
|
||||||
onClick={e => {
|
onClick={e => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
handleClick();
|
handleClick(e);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<SettingsIcon />
|
<SettingsIcon />
|
||||||
|
|||||||
+6
-2
@@ -2,12 +2,16 @@ import { UsersIcon } from '@blocksuite/icons';
|
|||||||
import { styled } from '@/styles';
|
import { styled } from '@/styles';
|
||||||
import { IconButton } from '@/ui/button';
|
import { IconButton } from '@/ui/button';
|
||||||
|
|
||||||
export const FooterUsers = () => {
|
type FooterUsersProps = {
|
||||||
|
memberCount: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FooterUsers = ({ memberCount = 1 }: FooterUsersProps) => {
|
||||||
return (
|
return (
|
||||||
<Wrapper className="footer-users">
|
<Wrapper className="footer-users">
|
||||||
<>
|
<>
|
||||||
<UsersIcon />
|
<UsersIcon />
|
||||||
<Tip>99+</Tip>
|
<Tip>{memberCount > 99 ? '99+' : memberCount}</Tip>
|
||||||
</>
|
</>
|
||||||
</Wrapper>
|
</Wrapper>
|
||||||
);
|
);
|
||||||
|
|||||||
+3
-1
@@ -14,6 +14,7 @@ interface WorkspaceItemProps {
|
|||||||
name: string;
|
name: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
type: WorkspaceType;
|
type: WorkspaceType;
|
||||||
|
memberCount: number;
|
||||||
onClick?: (workspaceId: string) => void;
|
onClick?: (workspaceId: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,6 +24,7 @@ export const WorkspaceItem = ({
|
|||||||
icon,
|
icon,
|
||||||
type,
|
type,
|
||||||
onClick,
|
onClick,
|
||||||
|
memberCount,
|
||||||
}: WorkspaceItemProps) => {
|
}: WorkspaceItemProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
@@ -44,7 +46,7 @@ export const WorkspaceItem = ({
|
|||||||
<Name title={name}>{name}</Name>
|
<Name title={name}>{name}</Name>
|
||||||
</WorkspaceItemContent>
|
</WorkspaceItemContent>
|
||||||
<Footer>
|
<Footer>
|
||||||
<FooterUsers />
|
<FooterUsers memberCount={memberCount} />
|
||||||
<FooterSetting onClick={handleClickSetting} />
|
<FooterSetting onClick={handleClickSetting} />
|
||||||
</Footer>
|
</Footer>
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
|
|||||||
+5
-1
@@ -1,14 +1,18 @@
|
|||||||
import { Popper } from '@/ui/popper';
|
import { Popper } from '@/ui/popper';
|
||||||
import { Avatar, WorkspaceName, SelectorWrapper } from './styles';
|
import { Avatar, WorkspaceName, SelectorWrapper } from './styles';
|
||||||
import { SelectorPopperContent } from './SelectorPopperContent';
|
import { SelectorPopperContent } from './SelectorPopperContent';
|
||||||
|
import { useState } from 'react';
|
||||||
|
|
||||||
export const WorkspaceSelector = () => {
|
export const WorkspaceSelector = () => {
|
||||||
|
const [isShow, setIsShow] = useState(false);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popper
|
<Popper
|
||||||
content={<SelectorPopperContent />}
|
content={<SelectorPopperContent isShow={isShow} />}
|
||||||
zIndex={1000}
|
zIndex={1000}
|
||||||
placement="bottom-start"
|
placement="bottom-start"
|
||||||
trigger="click"
|
trigger="click"
|
||||||
|
onVisibleChange={setIsShow}
|
||||||
>
|
>
|
||||||
<SelectorWrapper>
|
<SelectorWrapper>
|
||||||
<Avatar alt="Affine" />
|
<Avatar alt="Affine" />
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ export type LoadWorkspaceHandler = (
|
|||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
websocket?: boolean
|
websocket?: boolean
|
||||||
) => Promise<StoreWorkspace | null> | null;
|
) => Promise<StoreWorkspace | null> | null;
|
||||||
|
|
||||||
export type CreateEditorHandler = (page: StorePage) => EditorContainer | null;
|
export type CreateEditorHandler = (page: StorePage) => EditorContainer | null;
|
||||||
|
|
||||||
export interface AppStateValue {
|
export interface AppStateValue {
|
||||||
user: AccessTokenMessage | null;
|
user: AccessTokenMessage | null;
|
||||||
workspacesMeta: Workspace[];
|
workspacesMeta: Workspace[];
|
||||||
|
workspaces: Record<string, StoreWorkspace | null>;
|
||||||
|
|
||||||
currentWorkspaceId: string;
|
currentWorkspaceId: string;
|
||||||
currentWorkspace: StoreWorkspace | null;
|
currentWorkspace: StoreWorkspace | null;
|
||||||
@@ -59,6 +59,7 @@ export const AppState = createContext<AppStateContext>({
|
|||||||
loadWorkspace: undefined,
|
loadWorkspace: undefined,
|
||||||
loadPage: undefined,
|
loadPage: undefined,
|
||||||
createPage: undefined,
|
createPage: undefined,
|
||||||
|
workspaces: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const useAppState = () => {
|
export const useAppState = () => {
|
||||||
|
|||||||
@@ -101,6 +101,11 @@ const DynamicBlocksuite = ({
|
|||||||
resolve(workspace);
|
resolve(workspace);
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
const updates = await downloadWorkspace({ workspaceId });
|
||||||
|
updates &&
|
||||||
|
Workspace.Y.applyUpdate(workspace.doc, new Uint8Array(updates));
|
||||||
|
// if after update, the space:meta is empty, then we need to get map with doc
|
||||||
|
workspace.doc.getMap('space:meta');
|
||||||
resolve(workspace);
|
resolve(workspace);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ import {
|
|||||||
authorizationEvent,
|
authorizationEvent,
|
||||||
AccessTokenMessage,
|
AccessTokenMessage,
|
||||||
getWorkspaces,
|
getWorkspaces,
|
||||||
|
WorkspaceType,
|
||||||
|
getWorkspaceDetail,
|
||||||
} from '@pathfinder/data-services';
|
} from '@pathfinder/data-services';
|
||||||
import { AppState } from './context';
|
import { AppState } from './context';
|
||||||
import type {
|
import type {
|
||||||
@@ -26,6 +28,7 @@ export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
|
|||||||
currentWorkspace: null,
|
currentWorkspace: null,
|
||||||
currentPage: null,
|
currentPage: null,
|
||||||
editor: null,
|
editor: null,
|
||||||
|
workspaces: {},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [loadWorkspaceHandler, _setLoadWorkspaceHandler] =
|
const [loadWorkspaceHandler, _setLoadWorkspaceHandler] =
|
||||||
@@ -129,14 +132,21 @@ export const AppStateProvider = ({ children }: { children?: ReactNode }) => {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const callback = async (user: AccessTokenMessage | null) => {
|
const callback = async (user: AccessTokenMessage | null) => {
|
||||||
const workspacesMeta = user ? await getWorkspaces() : [];
|
const workspacesMeta = user ? await getWorkspaces() : [];
|
||||||
const workspaces = await Promise.all(
|
const workspacesList = await Promise.all(
|
||||||
workspacesMeta?.map(async ({ id }) => {
|
workspacesMeta?.map(async ({ id }) => {
|
||||||
const workspace = (await loadWorkspaceHandler?.(id)) || null;
|
const workspace = (await loadWorkspaceHandler?.(id)) || null;
|
||||||
return workspace;
|
return { id, workspace };
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let workspaces: Record<string, Workspace | null> = {};
|
||||||
|
|
||||||
|
workspacesList.forEach(({ id, workspace }) => {
|
||||||
|
workspaces[id] = workspace;
|
||||||
|
});
|
||||||
|
|
||||||
// TODO: add meta info to workspace meta
|
// TODO: add meta info to workspace meta
|
||||||
setState(state => ({ ...state, user: user, workspacesMeta }));
|
setState(state => ({ ...state, user: user, workspacesMeta, workspaces }));
|
||||||
};
|
};
|
||||||
authorizationEvent.onChange(callback);
|
authorizationEvent.onChange(callback);
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ function parseAccessToken(token: string): AccessTokenMessage | null {
|
|||||||
const message: AccessTokenMessage = JSON.parse(
|
const message: AccessTokenMessage = JSON.parse(
|
||||||
b64DecodeUnicode(token.split('.')[1])
|
b64DecodeUnicode(token.split('.')[1])
|
||||||
);
|
);
|
||||||
|
message.id = message.id.toString();
|
||||||
return message;
|
return message;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export interface AccessTokenMessage {
|
|||||||
create_at: number;
|
create_at: number;
|
||||||
exp: number;
|
exp: number;
|
||||||
email: string;
|
email: string;
|
||||||
id: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
avatar_url: string;
|
avatar_url: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export interface GetUserByEmailParams {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: number;
|
id: string;
|
||||||
name: string;
|
name: string;
|
||||||
email: string;
|
email: string;
|
||||||
avatar_url: string;
|
avatar_url: string;
|
||||||
|
|||||||
@@ -46,7 +46,9 @@ export async function getWorkspaceDetail(
|
|||||||
url: `/api/workspace/${params.id}`,
|
url: `/api/workspace/${params.id}`,
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
|
if (data.data?.owner.id) {
|
||||||
|
data.data.owner.id = String(data.data?.owner.id);
|
||||||
|
}
|
||||||
return data.data;
|
return data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,3 +201,20 @@ export async function downloadWorkspace(
|
|||||||
|
|
||||||
return data.data;
|
return data.data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function uploadBlob(params: { blob: Blob }): Promise<string> {
|
||||||
|
const data = await request({
|
||||||
|
url: '/api/blob',
|
||||||
|
method: 'PUT',
|
||||||
|
data: params.blob,
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getBlob(params: { blobId: string }): Promise<string> {
|
||||||
|
const data = await request({
|
||||||
|
url: `/api/blob/${params.blobId}`,
|
||||||
|
method: 'GET',
|
||||||
|
});
|
||||||
|
return data.data;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user