feat: add show workspace names and members

This commit is contained in:
MingLiang Wang
2022-12-22 16:36:50 +08:00
parent b0e7362eff
commit 0d23cf31e3
13 changed files with 162 additions and 34 deletions
@@ -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}
/> />
); );
})} })}
@@ -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}`);
@@ -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 />
@@ -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>
); );
@@ -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>
@@ -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;
} }
+1 -1
View File
@@ -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;
+20 -1
View File
@@ -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;
}