diff --git a/packages/app/src/components/404/index.tsx b/packages/app/src/components/404/index.tsx index 8ed423280..36f1b90a3 100644 --- a/packages/app/src/components/404/index.tsx +++ b/packages/app/src/components/404/index.tsx @@ -1,10 +1,24 @@ import { NotFoundTitle, PageContainer } from './styles'; import { useTranslation } from '@affine/i18n'; +import { Button } from '@/ui/button'; +import { useRouter } from 'next/router'; export const NotfoundPage = () => { const { t } = useTranslation(); + const router = useRouter(); return ( - {t('404 - Page Not Found')} + + {t('404 - Page Not Found')} + + { + router.push('/workspace'); + }} + > + Back Home + + + ); }; diff --git a/packages/app/src/components/workspace-avatar/index.tsx b/packages/app/src/components/workspace-avatar/Avatar.tsx similarity index 88% rename from packages/app/src/components/workspace-avatar/index.tsx rename to packages/app/src/components/workspace-avatar/Avatar.tsx index 22f5fdd04..c01808e68 100644 --- a/packages/app/src/components/workspace-avatar/index.tsx +++ b/packages/app/src/components/workspace-avatar/Avatar.tsx @@ -23,7 +23,11 @@ export const WorkspaceAvatar = (props: IWorkspaceAvatar) => { }} > - + ) : ( diff --git a/packages/app/src/components/workspace-avatar/WorkspaceUnitAvatar.tsx b/packages/app/src/components/workspace-avatar/WorkspaceUnitAvatar.tsx new file mode 100644 index 000000000..8a390bddd --- /dev/null +++ b/packages/app/src/components/workspace-avatar/WorkspaceUnitAvatar.tsx @@ -0,0 +1,39 @@ +import { useState, useEffect } from 'react'; +import type { WorkspaceUnit } from '@affine/datacenter'; +import { WorkspaceAvatar as Avatar } from './Avatar'; + +const useAvatar = (workspaceUnit?: WorkspaceUnit) => { + const [avatarUrl, setAvatarUrl] = useState(''); + const avatarId = + workspaceUnit?.avatar || workspaceUnit?.blocksuiteWorkspace?.meta.avatar; + useEffect(() => { + if (avatarId && workspaceUnit?.blocksuiteWorkspace?.blobs) { + workspaceUnit.blocksuiteWorkspace.blobs.then(blobs => { + blobs?.get(avatarId).then(url => setAvatarUrl(url || '')); + }); + } else { + setAvatarUrl(''); + } + }, [avatarId]); + + return avatarUrl; +}; + +export const WorkspaceUnitAvatar = ({ + size = 20, + name, + workspaceUnit, +}: { + size?: number; + name?: string; + workspaceUnit?: WorkspaceUnit | null; +}) => { + const avatarUrl = useAvatar(workspaceUnit || undefined); + return ( + + ); +}; diff --git a/packages/app/src/components/workspace-avatar/index.ts b/packages/app/src/components/workspace-avatar/index.ts new file mode 100644 index 000000000..8d6dfe17b --- /dev/null +++ b/packages/app/src/components/workspace-avatar/index.ts @@ -0,0 +1,2 @@ +export { WorkspaceAvatar } from './Avatar'; +export { WorkspaceUnitAvatar } from './WorkspaceUnitAvatar'; diff --git a/packages/app/src/components/workspace-modal/index.tsx b/packages/app/src/components/workspace-modal/index.tsx index a5f2369c4..edb47b3cb 100644 --- a/packages/app/src/components/workspace-modal/index.tsx +++ b/packages/app/src/components/workspace-modal/index.tsx @@ -10,7 +10,7 @@ import { AddIcon, } from '@blocksuite/icons'; import { toast } from '@/ui/toast'; -import { WorkspaceAvatar } from '@/components/workspace-avatar'; +import { WorkspaceUnitAvatar } from '@/components/workspace-avatar'; import { useAppState } from '@/providers/app-state-provider'; import { useRouter } from 'next/router'; import { useConfirm } from '@/providers/ConfirmProvider'; @@ -67,11 +67,7 @@ export const WorkspaceModal = ({ open, onClose }: WorkspaceModalProps) => { marginRight: '10px', }} > - + { const [showLeave, setShowLeave] = useState(false); const [uploading, setUploading] = useState(false); const [workspaceName, setWorkspaceName] = useState(workspace.name); - const { currentWorkspace } = useAppState(); + const { currentWorkspace, isOwner } = useAppState(); const { updateWorkspace } = useWorkspaceHelper(); const { t } = useTranslation(); - const isOwner = true; const handleChangeWorkSpaceName = (newName: string) => { setWorkspaceName(newName); }; - const handleClickDelete = () => { - setShowDelete(true); - }; - const handleCloseDelete = () => { - setShowDelete(false); - }; - - const handleClickLeave = () => { - setShowLeave(true); - }; - const handleCloseLeave = () => { - setShowLeave(false); - }; const handleUpdateWorkspaceName = () => { currentWorkspace && updateWorkspace({ name: workspaceName }, currentWorkspace); }; const fileChange = async (file: File) => { - // console.log('file: ', file); setUploading(true); const blob = new Blob([file], { type: file.type }); currentWorkspace && @@ -67,10 +52,10 @@ export const GeneralPage = ({ workspace }: { workspace: WorkspaceUnit }) => { marginRight: '20px', }} > - { placeholder={t('Workspace Name')} maxLength={14} minLength={1} + disabled={!isOwner} onChange={handleChangeWorkSpaceName} > - { - handleUpdateWorkspaceName(); - }} - style={{ marginLeft: '0px' }} - > - ✔️ - + {isOwner ? ( + { + handleUpdateWorkspaceName(); + }} + style={{ marginLeft: '0px' }} + > + ✔️ + + ) : null} {t('Workspace Type')} @@ -107,25 +95,39 @@ export const GeneralPage = ({ workspace }: { workspace: WorkspaceUnit }) => { {isOwner ? ( <> - + { + setShowDelete(true); + }} + > {t('Delete Workspace')} { + setShowDelete(false); + }} workspace={workspace} /> > ) : ( <> - + { + setShowLeave(true); + }} + > {t('Leave Workspace')} { + setShowLeave(false); + }} /> > )} diff --git a/packages/app/src/components/workspace-setting/general/delete/Delete.tsx b/packages/app/src/components/workspace-setting/general/delete/Delete.tsx index 0c4254880..c6db311e2 100644 --- a/packages/app/src/components/workspace-setting/general/delete/Delete.tsx +++ b/packages/app/src/components/workspace-setting/general/delete/Delete.tsx @@ -12,13 +12,11 @@ import { useState } from 'react'; import { ModalCloseButton } from '@/ui/modal'; import { Button } from '@/ui/button'; import { useRouter } from 'next/router'; -import { - deleteWorkspace, - getWorkspaces, - // Workspace, -} from '@/hooks/mock-data/mock'; + import { WorkspaceUnit } from '@affine/datacenter'; import { Trans, useTranslation } from '@affine/i18n'; +import { useWorkspaceHelper } from '@/hooks/use-workspace-helper'; + interface WorkspaceDeleteProps { open: boolean; onClose: () => void; @@ -33,23 +31,15 @@ export const WorkspaceDelete = ({ const [deleteStr, setDeleteStr] = useState(''); const { t } = useTranslation(); const router = useRouter(); - + const { deleteWorkSpace } = useWorkspaceHelper(); const handlerInputChange = (workspaceName: string) => { setDeleteStr(workspaceName); }; const handleDelete = async () => { - // const dc = await getDataCenter(); - // await dc.apis.deleteWorkspace({ id: workspaceId }); - // router.push(`/workspace/${nextWorkSpaceId}`); - deleteWorkspace(workspace.id); - const workspaceList = getWorkspaces(); - if (workspaceList.length) { - router.push(`/workspace/${workspaceList[0].id}`); - } else { - router.push(`/workspace`); - } + await deleteWorkSpace(); onClose(); + router.push(`/workspace`); }; return ( diff --git a/packages/app/src/components/workspace-setting/general/leave/Leave.tsx b/packages/app/src/components/workspace-setting/general/leave/Leave.tsx index a9689cbfd..3c91b026b 100644 --- a/packages/app/src/components/workspace-setting/general/leave/Leave.tsx +++ b/packages/app/src/components/workspace-setting/general/leave/Leave.tsx @@ -8,30 +8,20 @@ import { import { ModalCloseButton } from '@/ui/modal'; import { Button } from '@/ui/button'; import { useTranslation } from '@affine/i18n'; +import { useWorkspaceHelper } from '@/hooks/use-workspace-helper'; // import { getDataCenter } from '@affine/datacenter'; // import { useAppState } from '@/providers/app-state-provider'; interface WorkspaceDeleteProps { open: boolean; onClose: () => void; - workspaceName: string; - workspaceId: string; } -export const WorkspaceLeave = ({ - open, - onClose, - workspaceId, -}: WorkspaceDeleteProps) => { +export const WorkspaceLeave = ({ open, onClose }: WorkspaceDeleteProps) => { + const { leaveWorkSpace } = useWorkspaceHelper(); const { t } = useTranslation(); - console.log('workspaceId: ', workspaceId); - // const router = useRouter(); - // const { refreshWorkspacesMeta } = useAppState(); const handleLeave = async () => { - // const dc = await getDataCenter(); - // await dc.apis.leaveWorkspace({ id: workspaceId }); - // // router.push(`/workspace/${nextWorkSpaceId}`); - // refreshWorkspacesMeta(); + await leaveWorkSpace(); onClose(); }; diff --git a/packages/app/src/components/workspace-setting/member/MembersPage.tsx b/packages/app/src/components/workspace-setting/member/MembersPage.tsx index e93e65b6e..4f96371ba 100644 --- a/packages/app/src/components/workspace-setting/member/MembersPage.tsx +++ b/packages/app/src/components/workspace-setting/member/MembersPage.tsx @@ -20,20 +20,20 @@ import { InviteMemberModal } from './InviteMemberModal'; import { Menu, MenuItem } from '@/ui/menu'; import { Empty } from '@/ui/empty'; import { WorkspaceUnit } from '@affine/datacenter'; -import { useTemporaryHelper } from '@/providers/temporary-helper-provider'; import { useConfirm } from '@/providers/ConfirmProvider'; import { toast } from '@/ui/toast'; import useMembers from '@/hooks/use-members'; import Loading from '@/components/loading'; import { Wrapper } from '@/ui/layout'; import { useTranslation } from '@affine/i18n'; +import { useWorkspaceHelper } from '@/hooks/use-workspace-helper'; export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => { const [isInviteModalShow, setIsInviteModalShow] = useState(false); const { members, removeMember, loaded } = useMembers(); const { t } = useTranslation(); // FIXME: DELETE THIS - const { user, login, updateWorkspaceMeta } = useTemporaryHelper(); + const { enableWorkspace } = useWorkspaceHelper(); const { confirm } = useConfirm(); if (workspace.provider === 'affine') { @@ -164,22 +164,8 @@ export const MembersPage = ({ workspace }: { workspace: WorkspaceUnit }) => { { - confirm({ - title: `${t('Enable AFFiNE Cloud')}?`, - content: t('Enable AFFiNE Cloud Description'), - confirmText: user ? t('Enable') : t('Sign in and Enable'), - cancelText: t('Skip'), - }).then(confirm => { - if (confirm) { - if (user) { - updateWorkspaceMeta(workspace.id, { isPublish: true }); - } else { - login(); - updateWorkspaceMeta(workspace.id, { isPublish: true }); - } - } - }); + onClick={async () => { + await enableWorkspace(); }} > {t('Enable AFFiNE Cloud')} diff --git a/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceSelector.tsx b/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceSelector.tsx index 5997106ee..36484ca46 100644 --- a/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceSelector.tsx +++ b/packages/app/src/components/workspace-slider-bar/WorkspaceSelector/WorkspaceSelector.tsx @@ -1,7 +1,7 @@ import { Avatar, WorkspaceName, SelectorWrapper } from './styles'; import { useEffect, useState } from 'react'; import { WorkspaceModal } from '@/components/workspace-modal'; -import { WorkspaceAvatar } from '@/components/workspace-avatar'; +import { WorkspaceUnitAvatar } from '@/components/workspace-avatar'; import { useAppState } from '@/providers/app-state-provider'; export const WorkspaceSelector = () => { const [workspaceListShow, setWorkspaceListShow] = useState(false); @@ -23,17 +23,17 @@ export const WorkspaceSelector = () => { - diff --git a/packages/app/src/hooks/mock-data/mock.ts b/packages/app/src/hooks/mock-data/mock.ts deleted file mode 100644 index 9a091e202..000000000 --- a/packages/app/src/hooks/mock-data/mock.ts +++ /dev/null @@ -1,160 +0,0 @@ -export interface Workspace { - name: string; // 名称 - id: string; //唯一标识 - isPublish?: boolean; // 是否公开 - isLocal?: boolean; // 是否全部数据都在本地 - avatar?: string; // 封面 - type: 'local' | 'cloud' | 'join'; // cloud: 云端(本次暂不支持),local: 本地,join : 加入别人的 - workspaceOwner?: User; // 本地工作空间的拥有者 -} - -export interface User { - name: string; - id: string; - email: string; - avatar: string; -} - -export function updateWorkspaceMeta( - workspaceId: string, - workspaceData: { - name?: string; - avatar?: string; - type?: 'local' | 'cloud' | 'join'; - } -) { - const workspacesMeta = getWorkspaces(); - const newWorkspacesMeta = workspacesMeta.map((workspace: Workspace) => { - if (workspace.id === workspaceId) { - workspaceData.name && (workspace.name = workspaceData.name); - workspaceData.avatar && (workspace.avatar = workspaceData.avatar); - workspaceData.type && (workspace.type = workspaceData.type); - return workspaceData; - } - return workspace; - }); - localStorage.setItem('affine-workspace', JSON.stringify(newWorkspacesMeta)); - const activeWorkspace = getActiveWorkspace(); - workspaceData.name && (activeWorkspace.name = workspaceData.name); - workspaceData.avatar && (activeWorkspace.avatar = workspaceData.avatar); - - workspaceData.type && (activeWorkspace.type = workspaceData.type); - console.log(workspaceData); - setActiveWorkspace(activeWorkspace); -} -export function createWorkspace(workspaceName: string) { - const workspaceId = 'workspace-' + Date.now(); - const workspaceData = { - name: workspaceName, - id: workspaceId, - isPublish: false, - isLocal: true, - avatar: '', - type: 'local', - } as Workspace; - const workspacesMeta = getWorkspaces(); - workspacesMeta.push(workspaceData); - localStorage.setItem('affine-workspace', JSON.stringify(workspacesMeta)); - setActiveWorkspace(workspaceData); - return { workspaceId }; -} - -export function getWorkspaces(): Workspace[] { - const workspacesMeta = JSON.parse( - localStorage.getItem('affine-workspace') ?? '[]' - ); - return workspacesMeta; -} - -export function deleteWorkspace(workspaceId: string) { - const workspacesMeta = getWorkspaces(); - const newWorkspacesMeta = workspacesMeta.filter(() => { - return workspaceId !== workspaceId; - }); - localStorage.setItem('affine-workspace', JSON.stringify(newWorkspacesMeta)); -} - -export function getMembers(id: string): User[] { - const memberMap = JSON.parse(localStorage.getItem('affine-member') ?? '{}'); - return memberMap[id] || []; -} - -export function setMember(workspaceId: string, member: User) { - const memberMap = JSON.parse(localStorage.getItem('affine-member') ?? '{}'); - memberMap[workspaceId] = memberMap[workspaceId] || []; - memberMap[workspaceId].push(member); - localStorage.setItem('affine-member', JSON.stringify(memberMap)); -} - -export function deleteMember(workspaceId: string, index: number) { - const memberMap = JSON.parse(localStorage.getItem('affine-member') ?? '{}'); - const memberList = memberMap[workspaceId]; - memberList.splice(index, 1); - memberMap[workspaceId] = memberList; - localStorage.setItem('affine-member', JSON.stringify(memberMap)); -} -export function leaveWorkspace() { - return true; -} - -export function setWorkspacePublish(id: string, isPublish: boolean): boolean { - const workspacesMeta = getWorkspaces(); - const newWorkspacesMeta = workspacesMeta.map((workspace: Workspace) => { - if (workspace.id === id) { - workspace.isPublish = isPublish; - } - return workspace; - }); - localStorage.setItem('affine-workspace', JSON.stringify(newWorkspacesMeta)); - return isPublish; -} - -export function getWorkspaceById(id: string) { - const workspacesMeta = getWorkspaces(); - return workspacesMeta.find((workspace: Workspace) => { - return workspace.id === id; - }); -} - -export function getPagesByWorkspaceId(workspaceId: string) { - if (!workspaceId) return []; - const workspacesMeta = []; - for (let i = 0; i < 10; i++) { - workspacesMeta.push({ - id: 'page-' + i, - name: 'page ' + i, - }); - } -} - -export function getActiveWorkspace(): Workspace { - return JSON.parse(localStorage.getItem('affine-active-workspace') ?? '{}'); -} - -export function setActiveWorkspace(workspaceData: Workspace) { - console.log('workspaceData: ', workspaceData); - localStorage.setItem( - 'affine-active-workspace', - JSON.stringify(workspaceData) - ); -} - -export function getUserInfo(): User { - return JSON.parse(localStorage.getItem('affine-user') ?? 'null'); -} - -export function Login(): void { - localStorage.setItem( - 'affine-user', - JSON.stringify({ - name: 'Diamond', - id: 'ttt', - email: 'diamond.shx@gmail.com', - avatar: 'string', - }) - ); -} - -export function SignOut(): void { - localStorage.removeItem('affine-user'); -} diff --git a/packages/app/src/hooks/use-workspace-helper.ts b/packages/app/src/hooks/use-workspace-helper.ts index 397371e52..480ba409c 100644 --- a/packages/app/src/hooks/use-workspace-helper.ts +++ b/packages/app/src/hooks/use-workspace-helper.ts @@ -44,22 +44,41 @@ export const useWorkspaceHelper = () => { cancelText: 'Skip', }).then(async confirm => { if (confirm && currentWorkspace) { - if (user) { + if (!user) { await login(); } - const newWorkspaceId = await dataCenter.enableWorkspaceCloud( + const workspace = await dataCenter.enableWorkspaceCloud( currentWorkspace ); - router.push(`/workspace/${newWorkspaceId}/setting`); + workspace && router.push(`/workspace/${workspace.id}/setting`); toast('Enabled success'); } }); }; + const deleteWorkSpace = async () => { + currentWorkspace && + (await dataCenter.deleteWorkspace(currentWorkspace?.id)); + }; + const leaveWorkSpace = async () => { + currentWorkspace && (await dataCenter.leaveWorkspace(currentWorkspace?.id)); + }; + + const acceptInvite = async (inviteCode: string) => { + let inviteInfo; + if (inviteCode) { + inviteInfo = await dataCenter.acceptInvitation(inviteCode); + } + return inviteInfo; + }; + return { createWorkspace, publishWorkspace, updateWorkspace, enableWorkspace, + deleteWorkSpace, + leaveWorkSpace, + acceptInvite, }; }; diff --git a/packages/app/src/pages/404.tsx b/packages/app/src/pages/404.tsx index 74f25f07e..0eef5261e 100644 --- a/packages/app/src/pages/404.tsx +++ b/packages/app/src/pages/404.tsx @@ -1,9 +1,5 @@ import NotfoundPage from '@/components/404'; -import { useAppState } from '@/providers/app-state-provider'; export default function Custom404() { - const { workspaceList } = useAppState(); - console.log('workspaceList: ', workspaceList); - return ; } diff --git a/packages/app/src/pages/_app.tsx b/packages/app/src/pages/_app.tsx index c6cb2f6e6..bc06e51a4 100644 --- a/packages/app/src/pages/_app.tsx +++ b/packages/app/src/pages/_app.tsx @@ -21,7 +21,6 @@ import { useAppState } from '@/providers/app-state-provider'; import { PageLoading } from '@/components/loading'; import Head from 'next/head'; import '@affine/i18n'; -import TemporaryHelperProvider from '@/providers/temporary-helper-provider'; const ThemeProvider = dynamic(() => import('@/providers/ThemeProvider'), { ssr: false, @@ -55,10 +54,8 @@ const App = ({ Component, pageProps }: AppPropsWithLayout) => { , , , - // , , , ]} diff --git a/packages/app/src/pages/invite/[invite_code].tsx b/packages/app/src/pages/invite/[invite_code].tsx index e007a6cd2..342fa7c48 100644 --- a/packages/app/src/pages/invite/[invite_code].tsx +++ b/packages/app/src/pages/invite/[invite_code].tsx @@ -1,44 +1,37 @@ +import { useWorkspaceHelper } from '@/hooks/use-workspace-helper'; +import { useAppState } from '@/providers/app-state-provider'; import { styled } from '@/styles'; import { Empty } from '@/ui/empty'; -import { Avatar } from '@mui/material'; -// import { getDataCenter } from '@affine/datacenter'; +// import { Avatar } from '@mui/material'; import { useRouter } from 'next/router'; import { useEffect, useState } from 'react'; -const User = ({ name, avatar }: { name: string; avatar?: string }) => { - return ( - - {avatar ? ( - - ) : ( - {name.slice(0, 1)} - )} - {name} - - ); -}; +// const User = ({ name, avatar }: { name: string; avatar?: string }) => { +// return ( +// +// {avatar ? ( +// +// ) : ( +// {name.slice(0, 1)} +// )} +// {name} +// +// ); +// }; export default function DevPage() { const router = useRouter(); const [successInvited, setSuccessInvited] = useState(false); - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const [inviteData, setInviteData] = useState(null); + const { acceptInvite } = useWorkspaceHelper(); + const { user } = useAppState(); useEffect(() => { - // getDataCenter() - // .then(dc => - // dc.apis.acceptInviting({ - // invitingCode: router.query.invite_code as string, - // }) - // ) - // .then(data => { - // setSuccessInvited(true); - // setInviteData(data); - // }) - // .catch(err => { - // console.log('err: ', err); - // }); - setSuccessInvited(true); - setInviteData(null); + router.query.invite_code && + acceptInvite(router.query.invite_code as string).then(data => { + if (data && data.accepted) { + setSuccessInvited(true); + user && router.push(`/workspace/${data.workspace_id}`); + } + }); }, [router.query.invite_code]); return ( @@ -47,11 +40,12 @@ export default function DevPage() { - invited - you to join - invited */} + {/* you to join */} + {/* + > */} {successInvited ? ( { - const [open, setOpen] = useState(false); - - useEffect(() => { - const data = getWorkspaces(); - if (!data.length) { - setOpen(true); - } - }, []); - return ( - - workspace - - { - setOpen(true); - }} - > - View Workspace List - - - { - setOpen(false); - }} - > - - ); -}; -export default Page; - -const Workspace = styled.div(({ theme }) => { - return { - height: '100vh', - background: theme.colors.pageBackground, - color: '#FFFFFF', - fontSize: '18px', - fontWeight: 500, - }; -}); diff --git a/packages/app/src/pages/workspace/[workspaceId]/setting.tsx b/packages/app/src/pages/workspace/[workspaceId]/setting.tsx index 55426cc60..2a78641d6 100644 --- a/packages/app/src/pages/workspace/[workspaceId]/setting.tsx +++ b/packages/app/src/pages/workspace/[workspaceId]/setting.tsx @@ -61,8 +61,9 @@ const tabMap: { ]; const WorkspaceSetting = () => { - const { currentWorkspace } = useAppState(); const { t } = useTranslation(); + const { currentWorkspace, isOwner } = useAppState(); + const [activeTab, setActiveTab] = useState(tabMap[0].name); const handleTabChange = (tab: TabNames) => { setActiveTab(tab); @@ -71,7 +72,20 @@ const WorkspaceSetting = () => { const activeTabPanelRender = tabMap.find( tab => tab.name === activeTab )?.panelRender; - + let tableArr: { + name: TabNames; + icon: ReactNode; + panelRender: (workspace: WorkspaceUnit) => ReactNode; + }[] = tabMap; + if (!isOwner) { + tableArr = [ + { + name: 'general', + icon: , + panelRender: workspace => , + }, + ]; + } return ( @@ -79,7 +93,7 @@ const WorkspaceSetting = () => { {t('Workspace Settings')} - {tabMap.map(({ icon, name }) => { + {tableArr.map(({ icon, name }) => { return ( (); loadWorkspace.current = async (workspaceId: string) => { - const { dataCenter, workspaceList, currentWorkspace } = appState; + const { dataCenter, workspaceList, currentWorkspace, user } = appState; if (!workspaceList.find(v => v.id.toString() === workspaceId)) { return null; } @@ -99,6 +100,13 @@ export const AppStateProvider = ({ return currentWorkspace; } const workspace = (await dataCenter.loadWorkspace(workspaceId)) ?? null; + let isOwner; + if (workspace.provider === 'local') { + // isOwner is useful only in the cloud + isOwner = true; + } else { + isOwner = workspace?.owner && user?.id === workspace?.owner?.id; + } const pageList = (workspace?.blocksuiteWorkspace?.meta.pageMetas as PageMeta[]) ?? []; setAppState({ @@ -107,6 +115,7 @@ export const AppStateProvider = ({ pageList: pageList, currentPage: null, editor: null, + isOwner, }); return workspace; diff --git a/packages/app/src/providers/app-state-provider/interface.ts b/packages/app/src/providers/app-state-provider/interface.ts index 6cb5f5551..c76b2993c 100644 --- a/packages/app/src/providers/app-state-provider/interface.ts +++ b/packages/app/src/providers/app-state-provider/interface.ts @@ -23,6 +23,7 @@ export type AppStateValue = { currentPage: StorePage | null; editor?: EditorContainer | null; synced: boolean; + isOwner?: boolean; }; export type AppStateFunction = { diff --git a/packages/app/src/providers/temporary-helper-provider.tsx b/packages/app/src/providers/temporary-helper-provider.tsx deleted file mode 100644 index 5731646a1..000000000 --- a/packages/app/src/providers/temporary-helper-provider.tsx +++ /dev/null @@ -1,183 +0,0 @@ -import { createContext, useContext, useEffect, useState } from 'react'; -import type { PropsWithChildren } from 'react'; -import { getWorkspaces, User, Workspace } from '@/hooks/mock-data/mock'; - -type TemporaryContextValue = { - workspaceMetaList: Workspace[]; - currentWorkspace: Workspace | null; - user: User | null; - - updateWorkspaceMeta: ( - workspaceId: string, - workspaceData: { - name?: string; - avatar?: string; - isPublish?: boolean; - type?: 'local' | 'cloud' | 'join'; - } - ) => void; - - createWorkspace: (workspaceName: string) => Workspace; - - deleteWorkspace: (workspaceId: string) => void; - - setWorkspacePublish: (id: string, isPublish: boolean) => void; - - setActiveWorkspace: (workspaceData: Workspace) => void; - - login: () => void; - - signOut: () => void; -}; -type TemporaryContextProps = PropsWithChildren>; - -export const TemporaryContext = createContext({ - workspaceMetaList: [], - currentWorkspace: null, - user: null, - - // eslint-disable-next-line @typescript-eslint/no-empty-function - updateWorkspaceMeta: () => {}, - - createWorkspace: () => { - return {} as Workspace; - }, - // eslint-disable-next-line @typescript-eslint/no-empty-function - deleteWorkspace: () => {}, - // eslint-disable-next-line @typescript-eslint/no-empty-function - setWorkspacePublish: () => {}, - // eslint-disable-next-line @typescript-eslint/no-empty-function - setActiveWorkspace: () => {}, - // eslint-disable-next-line @typescript-eslint/no-empty-function - login: () => {}, - // eslint-disable-next-line @typescript-eslint/no-empty-function - signOut: () => {}, -}); - -export const useTemporaryHelper = () => useContext(TemporaryContext); - -export const TemporaryHelperProvider = ({ - children, -}: PropsWithChildren) => { - const [workspaceMetaList, setWorkspaceMetaList] = useState([]); - const [currentWorkspace, setCurrentWorkspace] = useState( - null - ); - const [user, setUser] = useState(null); - - useEffect(() => { - setWorkspaceMetaList(getWorkspaces()); - setCurrentWorkspace( - JSON.parse(localStorage.getItem('affine-active-workspace') ?? '{}') - ); - }, []); - - return ( - { - const workspacesMeta = getWorkspaces(); - const newWorkspacesMeta = workspacesMeta.map( - (workspace: Workspace) => { - if (workspace.id === workspaceId) { - const workspaceObj = Object.assign(workspace, workspaceData); - return workspaceObj; - } - return workspace; - } - ); - localStorage.setItem( - 'affine-workspace', - JSON.stringify(newWorkspacesMeta) - ); - setWorkspaceMetaList(newWorkspacesMeta); - if (workspaceId === currentWorkspace?.id) { - setCurrentWorkspace( - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - newWorkspacesMeta.find(v => v.id === currentWorkspace.id) - ); - } - }, - createWorkspace: workspaceName => { - const workspaceData = { - name: workspaceName, - id: 'workspace-' + Date.now(), - isPublish: false, - isLocal: true, - avatar: '', - type: 'local', - } as Workspace; - const workspacesMeta = getWorkspaces(); - workspacesMeta.push(workspaceData); - localStorage.setItem( - 'affine-workspace', - JSON.stringify(workspacesMeta) - ); - - setWorkspaceMetaList([...workspacesMeta]); - return workspaceData; - }, - deleteWorkspace: workspaceId => { - const workspacesMeta = getWorkspaces(); - const newWorkspacesMeta = workspacesMeta.filter(() => { - return workspaceId !== workspaceId; - }); - localStorage.setItem( - 'affine-workspace', - JSON.stringify(newWorkspacesMeta) - ); - setWorkspaceMetaList(workspacesMeta); - }, - - setWorkspacePublish: (id, isPublish) => { - const workspacesMeta = getWorkspaces(); - const newWorkspacesMeta = workspacesMeta.map( - (workspace: Workspace) => { - if (workspace.id === id) { - workspace.isPublish = isPublish; - } - return workspace; - } - ); - localStorage.setItem( - 'affine-workspace', - JSON.stringify(newWorkspacesMeta) - ); - setWorkspaceMetaList(workspacesMeta); - }, - setActiveWorkspace(workspaceData) { - localStorage.setItem( - 'affine-active-workspace', - JSON.stringify(workspaceData) - ); - setCurrentWorkspace(workspaceData); - }, - - login: () => { - const userInfo = { - name: 'Diamond', - id: 'ttt', - email: 'diamond.shx@gmail.com', - avatar: 'string', - }; - localStorage.setItem('affine-user', JSON.stringify(userInfo)); - setUser(userInfo); - }, - - signOut(): void { - localStorage.removeItem('affine-user'); - setUser(null); - }, - }} - > - {children} - - ); -}; - -export default TemporaryHelperProvider; diff --git a/packages/data-center/src/datacenter.ts b/packages/data-center/src/datacenter.ts index add13ec0e..e8876ac2d 100644 --- a/packages/data-center/src/datacenter.ts +++ b/packages/data-center/src/datacenter.ts @@ -23,7 +23,7 @@ export class DataCenter { private readonly _workspaceUnitCollection = new WorkspaceUnitCollection(); private readonly _logger = getLogger('dc'); private _workspaceInstances: Map = new Map(); - private _messageCenter = new MessageCenter(); + private _messageCenter = MessageCenter.getInstance(); /** * A mainProvider must exist as the only data trustworthy source. */ @@ -96,12 +96,7 @@ export class DataCenter { 'There is no provider. You should add provider first.' ); - const workspaceMeta = await this._mainProvider.createWorkspaceInfo(params); - - const workspace = createBlocksuiteWorkspace(workspaceMeta.id); - - await this._mainProvider.createWorkspace(workspace, workspaceMeta); - const workspaceUnit = this._workspaceUnitCollection.find(workspaceMeta.id); + const workspaceUnit = await this._mainProvider.createWorkspace(params); return workspaceUnit; } @@ -169,7 +164,9 @@ export class DataCenter { this._workspaceInstances.set(workspaceId, workspace); await provider.warpWorkspace(workspace); this._workspaceUnitCollection.workspaces.forEach(workspaceUnit => { - workspaceUnit.setBlocksuiteWorkspace(null); + const provider = this.providerMap.get(workspaceUnit.provider); + assert(provider); + provider.closeWorkspace(workspaceUnit.id); }); workspaceUnit.setBlocksuiteWorkspace(workspace); return workspaceUnit; @@ -322,55 +319,33 @@ export class DataCenter { } } - private async _transWorkspaceProvider( - workspace: BlocksuiteWorkspace, - providerId: string + public async enableProvider( + workspaceUnit: WorkspaceUnit, + providerId = 'affine' ) { - assert(workspace.room, 'No workspace id'); - const workspaceInfo = this._workspaceUnitCollection.find(workspace.room); - assert(workspaceInfo, 'Workspace not found'); - if (workspaceInfo.provider === providerId) { + if (workspaceUnit.provider === providerId) { this._logger('Workspace provider is same'); return; } - const currentProvider = this.providerMap.get(workspaceInfo.provider); - assert(currentProvider, 'Provider not found'); - const newProvider = this.providerMap.get(providerId); - assert(newProvider, `provide '${providerId}' is not registered`); - this._logger(`create ${providerId} workspace: `, workspaceInfo.name); - const newWorkspaceInfo = await newProvider.createWorkspaceInfo({ - name: workspaceInfo.name, - // avatar: workspaceInfo.avatar, - }); - const newWorkspace = createBlocksuiteWorkspace(newWorkspaceInfo.id); - // TODO optimize this function - await newProvider.createWorkspace(newWorkspace, { - ...newWorkspaceInfo, - name: workspaceInfo.name, - avatar: workspaceInfo.avatar, - }); + const provider = this.providerMap.get(providerId); + assert(provider); + const newWorkspaceUnit = await provider.extendWorkspace(workspaceUnit); - assert(newWorkspace, 'Create workspace failed'); - this._logger( - `update workspace data from ${workspaceInfo.provider} to ${providerId}` - ); - await newProvider.assign(newWorkspace, workspace); - assert(newWorkspace, 'Create workspace failed'); - await currentProvider.deleteWorkspace(workspace.room); - return newWorkspace.room; + // Currently we only allow enable one provider, so after enable new provider, + // delete the old workspace from its provider. + const oldProvider = this.providerMap.get(workspaceUnit.provider); + assert(oldProvider); + await oldProvider.deleteWorkspace(workspaceUnit.id); + + return newWorkspaceUnit; } /** * Enable workspace cloud * @param {string} id ID of workspace. */ - public async enableWorkspaceCloud(workspace: WorkspaceUnit) { - assert(workspace?.id, 'No workspace to enable cloud'); - assert(workspace.blocksuiteWorkspace); - return await this._transWorkspaceProvider( - workspace.blocksuiteWorkspace, - 'affine' - ); + public async enableWorkspaceCloud(workspaceUnit: WorkspaceUnit) { + return this.enableProvider(workspaceUnit); } /** @@ -441,13 +416,14 @@ export class DataCenter { /** * accept invitation * @param {string} inviteCode + * @returns {Promise} permission */ async acceptInvitation(inviteCode: string, providerStr = 'affine') { const provider = this.providerMap.get(providerStr); if (provider) { return await provider.acceptInvitation(inviteCode); } - return []; + return null; } onMessage(cb: (message: Message) => void) { diff --git a/packages/data-center/src/message/code.ts b/packages/data-center/src/message/code.ts index 271ffc8d3..d7df92696 100644 --- a/packages/data-center/src/message/code.ts +++ b/packages/data-center/src/message/code.ts @@ -1,3 +1,61 @@ export enum MessageCode { loginError, + noPermission, + loadListFailed, + getDetailFailed, + createWorkspaceFailed, + getMembersFailed, + updateWorkspaceFailed, + deleteWorkspaceFailed, + inviteMemberFailed, + removeMemberFailed, + acceptInvitingFailed, + getBlobFailed, + leaveWorkspaceFailed, + downloadWorkspaceFailed, } + +export const messages = { + [MessageCode.loginError]: { + message: 'Login failed', + }, + [MessageCode.noPermission]: { + message: 'No permission', + }, + [MessageCode.loadListFailed]: { + message: 'Load list failed', + }, + [MessageCode.getDetailFailed]: { + message: 'Get detail failed', + }, + [MessageCode.createWorkspaceFailed]: { + message: 'Create workspace failed', + }, + [MessageCode.getMembersFailed]: { + message: 'Get members failed', + }, + [MessageCode.updateWorkspaceFailed]: { + message: 'Update workspace failed', + }, + [MessageCode.deleteWorkspaceFailed]: { + message: 'Delete workspace failed', + }, + [MessageCode.inviteMemberFailed]: { + message: 'Invite member failed', + }, + [MessageCode.removeMemberFailed]: { + message: 'Remove member failed', + }, + [MessageCode.acceptInvitingFailed]: { + message: 'Accept inviting failed', + }, + [MessageCode.getBlobFailed]: { + message: 'Get blob failed', + }, + [MessageCode.leaveWorkspaceFailed]: { + message: 'Leave workspace failed', + }, + [MessageCode.downloadWorkspaceFailed]: { + message: 'Download workspace failed', + }, +}; diff --git a/packages/data-center/src/message/message.ts b/packages/data-center/src/message/message.ts index 6de9c7579..4becebc2f 100644 --- a/packages/data-center/src/message/message.ts +++ b/packages/data-center/src/message/message.ts @@ -1,24 +1,36 @@ import { Observable } from 'lib0/observable'; import { Message } from '../types'; -import { MessageCode } from './code.js'; +import { MessageCode, messages } from './code.js'; export class MessageCenter extends Observable { + private _messages: Record> = + messages; constructor() { super(); } - public send(message: MessageCode) { - this.emit('message', [message]); + static instance: MessageCenter; + + static getInstance() { + if (!MessageCenter.instance) { + MessageCenter.instance = new MessageCenter(); + } + return MessageCenter.instance; + } + + static messageCode = MessageCode; + + public getMessageSender(provider: string) { + return this._send.bind(this, provider); + } + + private _send(provider: string, messageCode: MessageCode) { + this.emit('message', [ + { ...this._messages[messageCode], provider, code: messageCode }, + ]); } public onMessage(callback: (message: Message) => void) { this.on('message', callback); } - - private messages: Record = { - [MessageCode.loginError]: { - code: MessageCode.loginError, - message: 'Login failed', - }, - }; } diff --git a/packages/data-center/src/provider/affine/affine.ts b/packages/data-center/src/provider/affine/affine.ts index 5bb3c11fd..c8078c4cd 100644 --- a/packages/data-center/src/provider/affine/affine.ts +++ b/packages/data-center/src/provider/affine/affine.ts @@ -2,22 +2,26 @@ import { BaseProvider } from '../base.js'; import type { ProviderConstructorParams, CreateWorkspaceInfoParams, - WorkspaceMeta0, } from '../base'; import type { User } from '../../types'; import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store'; -import { BlockSchema } from '@blocksuite/blocks/models'; import { storage } from './storage.js'; import assert from 'assert'; import { WebsocketProvider } from './sync.js'; // import { IndexedDBProvider } from '../local/indexeddb'; import { getApis, Workspace } from './apis/index.js'; import type { Apis, WorkspaceDetail, Callback } from './apis'; -import { setDefaultAvatar } from '../utils.js'; -import { MessageCode } from '../../message'; import { token } from './apis/token.js'; import { WebsocketClient } from './channel'; -import { SyncMode } from '../../workspace-unit'; +import { + loadWorkspaceUnit, + createWorkspaceUnit, + syncToCloud, +} from './utils.js'; +import { WorkspaceUnit } from '../../workspace-unit.js'; +import { createBlocksuiteWorkspace, applyUpdate } from '../../utils/index.js'; +import type { SyncMode } from '../../workspace-unit'; +import { MessageCenter } from '../../message/index.js'; type ChannelMessage = { ws_list: Workspace[]; @@ -31,12 +35,11 @@ export interface AffineProviderConstructorParams } const { - Y: { applyUpdate, encodeStateAsUpdate }, + Y: { encodeStateAsUpdate }, } = BlocksuiteWorkspace; export class AffineProvider extends BaseProvider { public id = 'affine'; - private _workspacesCache: Map = new Map(); private _onTokenRefresh?: Callback = undefined; private _wsMap: Map = new Map(); private _apis: Apis; @@ -95,14 +98,23 @@ export class AffineProvider extends BaseProvider { ); } this._channel.on('message', (msg: ChannelMessage) => { - this.handlerAffineListMessage(msg); + this._handlerAffineListMessage(msg); }); } - private handlerAffineListMessage({ ws_details, metadata }: ChannelMessage) { + private async _handlerAffineListMessage({ + ws_details, + metadata, + }: ChannelMessage) { this._logger('receive server message'); - Object.entries(ws_details).forEach(([id, detail]) => { + const addedWorkspaces: WorkspaceUnit[] = []; + const removeWorkspaceList = this._workspaces.list().map(w => w.id); + for (const [id, detail] of Object.entries(ws_details)) { const { name, avatar } = metadata[id]; + const index = removeWorkspaceList.indexOf(id); + if (index !== -1) { + removeWorkspaceList.splice(index, 1); + } assert(name); const workspace = { name: name, @@ -115,15 +127,24 @@ export class AffineProvider extends BaseProvider { }, published: detail.public, memberCount: detail.member_count, - provider: 'affine', + provider: this.id, syncMode: 'core' as SyncMode, }; if (this._workspaces.get(id)) { + // update workspaces this._workspaces.update(id, workspace); } else { - this._workspaces.add({ id, ...workspace }); + const workspaceUnit = await loadWorkspaceUnit( + { id, ...workspace }, + this._apis + ); + addedWorkspaces.push(workspaceUnit); } - }); + } + // add workspaces + this._workspaces.add(addedWorkspaces); + // remove workspaces + this._workspaces.remove(removeWorkspaceList); } private _getWebsocketProvider(workspace: BlocksuiteWorkspace) { @@ -147,17 +168,10 @@ export class AffineProvider extends BaseProvider { blocksuiteWorkspace: BlocksuiteWorkspace, published = false ) { - const { doc, room: workspaceId } = blocksuiteWorkspace; + const { room: workspaceId } = blocksuiteWorkspace; assert(workspaceId, 'Blocksuite Workspace without room(workspaceId).'); const updates = await this._apis.downloadWorkspace(workspaceId, published); - if (updates && updates.byteLength) { - await new Promise(resolve => { - doc.once('update', () => { - setTimeout(resolve, 100); - }); - BlocksuiteWorkspace.Y.applyUpdate(doc, new Uint8Array(updates)); - }); - } + await applyUpdate(blocksuiteWorkspace, new Uint8Array(updates)); } override async loadPublicWorkspace(blocksuiteWorkspace: BlocksuiteWorkspace) { @@ -194,79 +208,25 @@ export class AffineProvider extends BaseProvider { return []; } const workspacesList = await this._apis.getWorkspaces(); - const workspaces: WorkspaceMeta0[] = workspacesList.map(w => { - return { - ...w, - published: w.public, - memberCount: 0, - name: '', - provider: 'affine', - syncMode: 'core', - }; - }); - const workspaceInstances = workspaces.map(({ id }) => { - const workspace = - this._workspacesCache.get(id) || - new BlocksuiteWorkspace({ - room: id, - }).register(BlockSchema); - this._workspacesCache.set(id, workspace); - if (workspace) { - return new Promise(resolve => { - this._apis.downloadWorkspace(id).then(data => { - applyUpdate(workspace.doc, new Uint8Array(data)); - resolve(workspace); - }); - }); - } else { - return Promise.resolve(null); - } - }); - - (await Promise.all(workspaceInstances)).forEach((workspace, i) => { - if (workspace) { - workspaces[i] = { - ...workspaces[i], - name: workspace.meta.name, - avatar: workspace.meta.avatar, - }; - } - }); - const getDetailList = workspacesList.map(w => { - const { id } = w; - return new Promise<{ id: string; detail: WorkspaceDetail | null }>( - resolve => { - this._apis.getWorkspaceDetail({ id }).then(data => { - resolve({ id, detail: data || null }); - }); - } - ); - }); - const ownerList = await Promise.all(getDetailList); - (await Promise.all(ownerList)).forEach(detail => { - if (detail) { - const { id, detail: workspaceDetail } = detail; - if (workspaceDetail) { - const { owner, member_count } = workspaceDetail; - const currentWorkspace = workspaces.find(w => w.id === id); - if (currentWorkspace) { - currentWorkspace.owner = { - id: owner.id, - name: owner.name, - avatar: owner.avatar_url, - email: owner.email, - }; - currentWorkspace.memberCount = member_count; - } - } - } - }); - - workspaces.forEach(workspace => { - this._workspaces.add(workspace); - }); - - return workspaces; + const workspaceUnits = await Promise.all( + workspacesList.map(w => { + return loadWorkspaceUnit( + { + id: w.id, + name: '', + avatar: undefined, + owner: undefined, + published: w.public, + memberCount: 1, + provider: this.id, + syncMode: 'core', + }, + this._apis + ); + }) + ); + this._workspaces.add(workspaceUnits); + return workspaceUnits; } override async auth() { @@ -283,7 +243,7 @@ export class AffineProvider extends BaseProvider { this._connectChannel(); } if (!user) { - this._messageCenter.send(MessageCode.loginError); + this._sendMessage(MessageCenter.messageCode.loginError); } } @@ -309,17 +269,17 @@ export class AffineProvider extends BaseProvider { } public override async clear(): Promise { - for (const w of this._workspacesCache.values()) { - if (w.room) { + for (const w of this._workspaces.list()) { + if (w.id) { try { - await this.deleteWorkspace(w.room); - this._workspaces.remove(w.room); + await this.deleteWorkspace(w.id); + this._workspaces.remove(w.id); } catch (e) { this._logger('has a problem of delete workspace ', e); } } } - this._workspacesCache.clear(); + this._workspaces.clear(); } public override async closeWorkspace(id: string) { @@ -353,52 +313,29 @@ export class AffineProvider extends BaseProvider { // return workspace; } - public override async createWorkspaceInfo( + public override async createWorkspace( meta: CreateWorkspaceInfoParams - ): Promise { + ): Promise { const { id } = await this._apis.createWorkspace(meta); - const workspaceInfo: WorkspaceMeta0 = { + const workspaceUnit = await createWorkspaceUnit({ + id, name: meta.name, - id: id, - published: false, - avatar: '', + avatar: undefined, owner: await this.getUserInfo(), - syncMode: 'core', - memberCount: 1, - provider: 'affine', - }; - return workspaceInfo; - } - - public override async createWorkspace( - blocksuiteWorkspace: BlocksuiteWorkspace, - meta: WorkspaceMeta0 - ): Promise { - const workspaceId = blocksuiteWorkspace.room; - assert(workspaceId, 'Blocksuite Workspace without room(workspaceId).'); - this._logger('Creating affine workspace'); - - this._applyCloudUpdates(blocksuiteWorkspace); - this.linkLocal(blocksuiteWorkspace); - - const workspaceInfo: WorkspaceMeta0 = { - name: meta.name, - id: workspaceId, published: false, - avatar: '', - owner: undefined, - syncMode: 'core', memberCount: 1, - provider: 'affine', - }; + provider: this.id, + syncMode: 'core', + }); - if (!blocksuiteWorkspace.meta.avatar) { - await setDefaultAvatar(blocksuiteWorkspace); - workspaceInfo.avatar = blocksuiteWorkspace.meta.avatar; - } - this._workspaces.add(workspaceInfo); - return blocksuiteWorkspace; + await syncToCloud( + workspaceUnit.blocksuiteWorkspace!, + this._apis.token.refresh + ); + this._workspaces.add(workspaceUnit); + + return workspaceUnit; } public override async publish(id: string, isPublish: boolean): Promise { @@ -420,22 +357,36 @@ export class AffineProvider extends BaseProvider { : null; } - public override async assign( - to: BlocksuiteWorkspace, - from: BlocksuiteWorkspace - ): Promise { - assert(to.room, 'Blocksuite Workspace without room(workspaceId).'); - const ws = this._getWebsocketProvider(to); - applyUpdate(to.doc, encodeStateAsUpdate(from.doc)); - // TODO: upload blobs and make sure doc is synced - await new Promise((resolve, reject) => { - ws.once('synced', () => { - setTimeout(() => resolve(), 1000); - }); - ws.once('lost-connection', () => reject()); - ws.once('connection-error', () => reject()); + public override async extendWorkspace( + workspaceUnit: WorkspaceUnit + ): Promise { + const { id } = await this._apis.createWorkspace({ + name: workspaceUnit.name, }); - return to; + const newWorkspaceUnit = new WorkspaceUnit({ + id, + name: workspaceUnit.name, + avatar: undefined, + owner: await this.getUserInfo(), + published: false, + memberCount: 1, + provider: this.id, + syncMode: 'core', + }); + + const blocksuiteWorkspace = createBlocksuiteWorkspace(id); + assert(workspaceUnit.blocksuiteWorkspace); + await applyUpdate( + blocksuiteWorkspace, + encodeStateAsUpdate(workspaceUnit.blocksuiteWorkspace.doc) + ); + + await syncToCloud(blocksuiteWorkspace, this._apis.token.refresh); + + newWorkspaceUnit.setBlocksuiteWorkspace(blocksuiteWorkspace); + + this._workspaces.add(newWorkspaceUnit); + return newWorkspaceUnit; } public override async logout(): Promise { @@ -449,7 +400,7 @@ export class AffineProvider extends BaseProvider { return this._apis.getWorkspaceMembers({ id }); } - public override async acceptInvitation(invitingCode: string): Promise { - await this._apis.acceptInviting({ invitingCode }); + public override async acceptInvitation(invitingCode: string) { + return await this._apis.acceptInviting({ invitingCode }); } } diff --git a/packages/data-center/src/provider/affine/apis/request.ts b/packages/data-center/src/provider/affine/apis/request.ts index ce5b64eeb..2520b1d53 100644 --- a/packages/data-center/src/provider/affine/apis/request.ts +++ b/packages/data-center/src/provider/affine/apis/request.ts @@ -1,7 +1,11 @@ -import kyOrigin from 'ky'; import ky from 'ky-universal'; +import { MessageCenter } from '../../../message/index.js'; import { token } from './token.js'; +const messageCenter = MessageCenter.getInstance(); + +const _sendMessage = messageCenter.getMessageSender('affine'); + export const bareClient = ky.extend({ prefixUrl: 'http://localhost:8080', retry: 1, @@ -41,5 +45,15 @@ export const client = bareClient.extend({ request.headers.set('Authorization', token.token); }, ], + + beforeError: [ + error => { + const { response } = error; + if (response.status === 401) { + _sendMessage(MessageCenter.messageCode.noPermission); + } + return error; + }, + ], }, }); diff --git a/packages/data-center/src/provider/affine/apis/workspace.ts b/packages/data-center/src/provider/affine/apis/workspace.ts index c8eebd01e..106dc72b0 100644 --- a/packages/data-center/src/provider/affine/apis/workspace.ts +++ b/packages/data-center/src/provider/affine/apis/workspace.ts @@ -1,6 +1,20 @@ +import { MessageCenter } from '../../../message/index.js'; import { bareClient, client } from './request.js'; import type { User } from './user'; +const messageCenter = MessageCenter.getInstance(); + +const sendMessage = messageCenter.getMessageSender('affine'); + +const { messageCode } = MessageCenter; + +class RequestError extends Error { + constructor(message: string, cause: unknown | null = null) { + super(message); + this.name = 'RequestError'; + this.cause = cause; + } +} export interface GetWorkspaceDetailParams { id: string; } @@ -26,13 +40,18 @@ export interface Workspace { } export async function getWorkspaces(): Promise { - return client - .get('api/workspace', { - headers: { - 'Cache-Control': 'no-cache', - }, - }) - .json(); + try { + return client + .get('api/workspace', { + headers: { + 'Cache-Control': 'no-cache', + }, + }) + .json(); + } catch (error) { + sendMessage(messageCode.loadListFailed); + throw new RequestError('load list failed', error); + } } export interface WorkspaceDetail extends Workspace { @@ -43,7 +62,13 @@ export interface WorkspaceDetail extends Workspace { export async function getWorkspaceDetail( params: GetWorkspaceDetailParams ): Promise { - return client.get(`api/workspace/${params.id}`).json(); + try { + const response = client.get(`api/workspace/${params.id}`); + return response.json(); + } catch (error) { + sendMessage(messageCode.getDetailFailed); + throw new RequestError('get detail failed', error); + } } export interface Permission { @@ -74,7 +99,12 @@ export interface GetWorkspaceMembersParams { export async function getWorkspaceMembers( params: GetWorkspaceDetailParams ): Promise { - return client.get(`api/workspace/${params.id}/permission`).json(); + try { + return client.get(`api/workspace/${params.id}/permission`).json(); + } catch (error) { + sendMessage(messageCode.getMembersFailed); + throw new RequestError('get members failed', error); + } } export interface CreateWorkspaceParams { @@ -84,10 +114,12 @@ export interface CreateWorkspaceParams { export async function createWorkspace( params: CreateWorkspaceParams ): Promise<{ id: string }> { - // FIXME: avatar should be optional - return client - .post('api/workspace', { json: { ...params, avatar: '123' } }) - .json(); + try { + return client.post('api/workspace', { json: params }).json(); + } catch (error) { + sendMessage(messageCode.createWorkspaceFailed); + throw new RequestError('create workspace failed', error); + } } export interface UpdateWorkspaceParams { @@ -98,13 +130,18 @@ export interface UpdateWorkspaceParams { export async function updateWorkspace( params: UpdateWorkspaceParams ): Promise<{ public: boolean | null }> { - return client - .post(`api/workspace/${params.id}`, { - json: { - public: params.public, - }, - }) - .json(); + try { + return client + .post(`api/workspace/${params.id}`, { + json: { + public: params.public, + }, + }) + .json(); + } catch (error) { + sendMessage(messageCode.updateWorkspaceFailed); + throw new RequestError('update workspace failed', error); + } } export interface DeleteWorkspaceParams { @@ -114,7 +151,10 @@ export interface DeleteWorkspaceParams { export async function deleteWorkspace( params: DeleteWorkspaceParams ): Promise { - await client.delete(`api/workspace/${params.id}`); + await client.delete(`api/workspace/${params.id}`).catch(error => { + sendMessage(messageCode.deleteWorkspaceFailed); + throw new RequestError('delete workspace failed', error); + }); } export interface InviteMemberParams { @@ -126,13 +166,18 @@ export interface InviteMemberParams { * Notice: Only support normal(contrast to private) workspace. */ export async function inviteMember(params: InviteMemberParams): Promise { - return client - .post(`api/workspace/${params.id}/permission`, { - json: { - email: params.email, - }, - }) - .json(); + try { + return client + .post(`api/workspace/${params.id}/permission`, { + json: { + email: params.email, + }, + }) + .json(); + } catch (error) { + sendMessage(messageCode.inviteMemberFailed); + throw new RequestError('invite member failed', error); + } } export interface RemoveMemberParams { @@ -140,7 +185,10 @@ export interface RemoveMemberParams { } export async function removeMember(params: RemoveMemberParams): Promise { - await client.delete(`api/permission/${params.permissionId}`); + await client.delete(`api/permission/${params.permissionId}`).catch(error => { + sendMessage(messageCode.removeMemberFailed); + throw new RequestError('remove member failed', error); + }); } export interface AcceptInvitingParams { @@ -149,8 +197,13 @@ export interface AcceptInvitingParams { export async function acceptInviting( params: AcceptInvitingParams -): Promise { - await bareClient.post(`api/invitation/${params.invitingCode}`); +): Promise { + try { + return bareClient.post(`api/invitation/${params.invitingCode}`).json(); + } catch (error) { + sendMessage(messageCode.acceptInvitingFailed); + throw new RequestError('accept inviting failed', error); + } } export async function uploadBlob(params: { blob: Blob }): Promise { @@ -160,7 +213,12 @@ export async function uploadBlob(params: { blob: Blob }): Promise { export async function getBlob(params: { blobId: string; }): Promise { - return client.get(`api/blob/${params.blobId}`).arrayBuffer(); + try { + return client.get(`api/blob/${params.blobId}`).arrayBuffer(); + } catch (error) { + sendMessage(messageCode.getBlobFailed); + throw new RequestError('get blob failed', error); + } } export interface LeaveWorkspaceParams { @@ -168,15 +226,26 @@ export interface LeaveWorkspaceParams { } export async function leaveWorkspace({ id }: LeaveWorkspaceParams) { - await client.delete(`api/workspace/${id}/permission`).json(); + await client + .delete(`api/workspace/${id}/permission`) + .json() + .catch(error => { + sendMessage(messageCode.leaveWorkspaceFailed); + throw new RequestError('leave workspace failed', error); + }); } export async function downloadWorkspace( workspaceId: string, published = false ): Promise { - if (published) { - return bareClient.get(`api/public/doc/${workspaceId}`).arrayBuffer(); + try { + if (published) { + return bareClient.get(`api/public/doc/${workspaceId}`).arrayBuffer(); + } + return client.get(`api/workspace/${workspaceId}/doc`).arrayBuffer(); + } catch (error) { + sendMessage(messageCode.downloadWorkspaceFailed); + throw new RequestError('download workspace failed', error); } - return client.get(`api/workspace/${workspaceId}/doc`).arrayBuffer(); } diff --git a/packages/data-center/src/provider/affine/utils.ts b/packages/data-center/src/provider/affine/utils.ts new file mode 100644 index 000000000..93c2f5600 --- /dev/null +++ b/packages/data-center/src/provider/affine/utils.ts @@ -0,0 +1,90 @@ +import assert from 'assert'; +import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store'; +import { WorkspaceUnit } from '../../workspace-unit.js'; +import type { WorkspaceUnitCtorParams } from '../../workspace-unit'; +import { createBlocksuiteWorkspace } from '../../utils/index.js'; +import type { Apis } from './apis'; +import { WebsocketProvider } from './sync.js'; +import { setDefaultAvatar } from '../utils.js'; +import { applyUpdate } from '../../utils/index.js'; + +export const loadWorkspaceUnit = async ( + params: WorkspaceUnitCtorParams, + apis: Apis +) => { + const workspaceUnit = new WorkspaceUnit(params); + const blocksuiteWorkspace = createBlocksuiteWorkspace(workspaceUnit.id); + + const updates = await apis.downloadWorkspace( + workspaceUnit.id, + params.published + ); + applyUpdate(blocksuiteWorkspace, new Uint8Array(updates)); + + const details = await apis.getWorkspaceDetail({ id: workspaceUnit.id }); + const owner = details?.owner; + + workspaceUnit.setBlocksuiteWorkspace(blocksuiteWorkspace); + workspaceUnit.update({ + name: blocksuiteWorkspace.meta.name, + avatar: blocksuiteWorkspace.meta.avatar, + memberCount: details?.member_count || 1, + owner: owner + ? { + id: owner.id, + name: owner.name, + avatar: owner.avatar_url, + email: owner.email, + } + : undefined, + }); + + return workspaceUnit; +}; + +export const syncToCloud = async ( + blocksuiteWorkspace: BlocksuiteWorkspace, + refreshToken: string +) => { + const workspaceId = blocksuiteWorkspace.room; + assert(workspaceId, 'Blocksuite workspace without room(workspaceId).'); + + const wsUrl = `${window.location.protocol === 'https:' ? 'wss' : 'ws'}://${ + window.location.host + }/api/sync/`; + + const ws = new WebsocketProvider( + wsUrl, + workspaceId, + blocksuiteWorkspace.doc, + { + params: { token: refreshToken }, + } + ); + + await new Promise((resolve, reject) => { + ws.once('synced', () => { + // FIXME: we don't when send local data to cloud successfully, so hack to wait 1s. + // Server will support this by add a new api. + setTimeout(resolve, 1000); + }); + ws.once('lost-connection', () => reject()); + ws.once('connection-error', () => reject()); + }); +}; + +export const createWorkspaceUnit = async (params: WorkspaceUnitCtorParams) => { + const workspaceUnit = new WorkspaceUnit(params); + + const blocksuiteWorkspace = createBlocksuiteWorkspace(workspaceUnit.id); + + blocksuiteWorkspace.meta.setName(workspaceUnit.name); + if (!workspaceUnit.avatar) { + await setDefaultAvatar(blocksuiteWorkspace); + workspaceUnit.update({ avatar: blocksuiteWorkspace.meta.avatar }); + } + + workspaceUnit.setBlocksuiteWorkspace(blocksuiteWorkspace); + + return workspaceUnit; +}; diff --git a/packages/data-center/src/provider/base.ts b/packages/data-center/src/provider/base.ts index 11547c6a6..eb9a68d74 100644 --- a/packages/data-center/src/provider/base.ts +++ b/packages/data-center/src/provider/base.ts @@ -1,9 +1,10 @@ -import { Workspace as BlocksuiteWorkspace, uuidv4 } from '@blocksuite/store'; +import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store'; import { MessageCenter } from '../message'; import { Logger, User } from '../types'; import type { WorkspaceUnitCollectionScope } from '../workspace-unit-collection'; -import type { WorkspaceUnitCtorParams } from '../workspace-unit'; +import type { WorkspaceUnitCtorParams, WorkspaceUnit } from '../workspace-unit'; import { Member } from './affine/apis'; +import { Permission } from './affine/apis/workspace.js'; const defaultLogger = () => { return; @@ -22,10 +23,15 @@ export type UpdateWorkspaceMetaParams = Partial< >; export class BaseProvider { + /** provider id */ public readonly id: string = 'base'; + /** workspace unit collection */ protected _workspaces!: WorkspaceUnitCollectionScope; protected _logger!: Logger; - protected _messageCenter!: MessageCenter; + /** send message with message center */ + protected _sendMessage!: ReturnType< + InstanceType['getMessageSender'] + >; public constructor({ logger, @@ -34,7 +40,7 @@ export class BaseProvider { }: ProviderConstructorParams) { this._logger = (logger || defaultLogger) as Logger; this._workspaces = workspaces; - this._messageCenter = messageCenter; + this._sendMessage = messageCenter.getMessageSender(this.id); } /** @@ -44,12 +50,6 @@ export class BaseProvider { return; } - public async createWorkspaceInfo( - params: CreateWorkspaceInfoParams - ): Promise { - throw new Error(`provider: ${this.id} createWorkspaceInfo Not implemented`); - } - /** * auth provider */ @@ -87,7 +87,7 @@ export class BaseProvider { /** * load workspaces **/ - public async loadWorkspaces(): Promise { + public async loadWorkspaces(): Promise { throw new Error(`provider: ${this.id} loadWorkSpace Not implemented`); } @@ -183,13 +183,18 @@ export class BaseProvider { /** * create workspace by workspace meta - * @param {WorkspaceMeta} meta + * @param {CreateWorkspaceInfoParams} meta */ public async createWorkspace( - blocksuiteWorkspace: BlocksuiteWorkspace, - meta: WorkspaceMeta0 - ): Promise { - return blocksuiteWorkspace; + meta: CreateWorkspaceInfoParams + ): Promise { + throw new Error(`provider: ${this.id} createWorkspace not implemented`); + } + + public async extendWorkspace( + workspaceUnit: WorkspaceUnit + ): Promise { + throw new Error(`provider: ${this.id} extendWorkspace not implemented`); } /** @@ -214,16 +219,6 @@ export class BaseProvider { return workspace; } - /** - * merge one workspaces to another - * @param workspace - * @returns - */ - public async assign(to: BlocksuiteWorkspace, from: BlocksuiteWorkspace) { - from; - return to; - } - /** * get workspace members * @param {string} workspaceId @@ -239,8 +234,10 @@ export class BaseProvider { * @param {string} inviteCode * @returns */ - public async acceptInvitation(inviteCode: string): Promise { + public async acceptInvitation( + inviteCode: string + ): Promise { inviteCode; - return; + return null; } } diff --git a/packages/data-center/src/provider/local/indexeddb/utils.ts b/packages/data-center/src/provider/local/indexeddb/utils.ts index 8ca5c9b46..68a4172eb 100644 --- a/packages/data-center/src/provider/local/indexeddb/utils.ts +++ b/packages/data-center/src/provider/local/indexeddb/utils.ts @@ -1,10 +1,13 @@ import assert from 'assert'; import * as idb from 'lib0/indexeddb.js'; import { Workspace as BlocksuiteWorkspace } from '@blocksuite/store'; +import { applyUpdate } from '../../../utils/index.js'; -const { encodeStateAsUpdate } = BlocksuiteWorkspace.Y; +const { encodeStateAsUpdate, mergeUpdates } = BlocksuiteWorkspace.Y; -export const initStore = async (blocksuiteWorkspace: BlocksuiteWorkspace) => { +export const writeUpdatesToLocal = async ( + blocksuiteWorkspace: BlocksuiteWorkspace +) => { const workspaceId = blocksuiteWorkspace.room; assert(workspaceId); await idb.deleteDB(workspaceId); @@ -17,4 +20,23 @@ export const initStore = async (blocksuiteWorkspace: BlocksuiteWorkspace) => { if (updatesStore) { await idb.addAutoKey(updatesStore, currState); } + db.close(); +}; + +export const applyLocalUpdates = async ( + blocksuiteWorkspace: BlocksuiteWorkspace +) => { + const workspaceId = blocksuiteWorkspace.room; + assert(workspaceId, 'Blocksuite workspace without room(workspaceId).'); + const db = await idb.openDB(workspaceId, db => + idb.createStores(db, [['updates', { autoIncrement: true }], ['custom']]) + ); + + const [updatesStore] = idb.transact(db, ['updates']); // , 'readonly') + if (updatesStore) { + const updates = await idb.getAll(updatesStore); + const mergedUpdates = mergeUpdates(updates); + await applyUpdate(blocksuiteWorkspace, mergedUpdates); + } + return blocksuiteWorkspace; }; diff --git a/packages/data-center/src/provider/local/local.spec.ts b/packages/data-center/src/provider/local/local.spec.ts index e60ce31aa..ba9cb8b66 100644 --- a/packages/data-center/src/provider/local/local.spec.ts +++ b/packages/data-center/src/provider/local/local.spec.ts @@ -16,12 +16,10 @@ test.describe.serial('local provider', () => { let workspaceId: string | undefined; test('create workspace', async () => { - const workspaceInfo = await provider.createWorkspaceInfo({ + const workspaceUnit = await provider.createWorkspace({ name: workspaceName, }); - workspaceId = workspaceInfo.id; - const blocksuiteWorkspace = createBlocksuiteWorkspace(workspaceId); - await provider.createWorkspace(blocksuiteWorkspace, workspaceInfo); + workspaceId = workspaceUnit?.id; expect(workspaceMetaCollection.workspaces.length).toEqual(1); expect(workspaceMetaCollection.workspaces[0].name).toEqual(workspaceName); diff --git a/packages/data-center/src/provider/local/local.ts b/packages/data-center/src/provider/local/local.ts index 12ed43011..f9b819ea4 100644 --- a/packages/data-center/src/provider/local/local.ts +++ b/packages/data-center/src/provider/local/local.ts @@ -8,9 +8,10 @@ import type { import { varStorage as storage } from 'lib0/storage'; import { Workspace as BlocksuiteWorkspace, uuidv4 } from '@blocksuite/store'; import { IndexedDBProvider } from './indexeddb/indexeddb.js'; -import { initStore } from './indexeddb/utils.js'; +import { applyLocalUpdates } from './indexeddb/utils.js'; import assert from 'assert'; -import { setDefaultAvatar } from '../utils.js'; +import { loadWorkspaceUnit, createWorkspaceUnit } from './utils.js'; +import type { WorkspaceUnit } from '../../workspace-unit'; const WORKSPACE_KEY = 'workspaces'; @@ -22,21 +23,12 @@ export class LocalProvider extends BaseProvider { super(params); } - private _storeWorkspaces(workspaces: WorkspaceMeta0[]) { + private _storeWorkspaces(workspaceUnits: WorkspaceUnit[]) { storage.setItem( WORKSPACE_KEY, JSON.stringify( - workspaces.map(w => { - return { - id: w.id, - name: w.name, - avatar: w.avatar, - owner: w.owner, - published: w.published, - memberCount: w.memberCount, - provider: w.provider, - syncMode: w.syncMode, - }; + workspaceUnits.map(w => { + return w.toJSON(); }) ) ); @@ -57,24 +49,28 @@ export class LocalProvider extends BaseProvider { workspace: BlocksuiteWorkspace ): Promise { assert(workspace.room); + await applyLocalUpdates(workspace); await this.linkLocal(workspace); return workspace; } - override loadWorkspaces(): Promise { + override async loadWorkspaces(): Promise { const workspaceStr = storage.getItem(WORKSPACE_KEY); - let workspaces: WorkspaceMeta0[] = []; if (workspaceStr) { try { - workspaces = JSON.parse(workspaceStr) as WorkspaceMeta0[]; - workspaces.forEach(workspace => { - this._workspaces.add(workspace); - }); + const workspaceMetas = JSON.parse(workspaceStr) as WorkspaceMeta0[]; + const workspaceUnits = await Promise.all( + workspaceMetas.map(meta => { + return loadWorkspaceUnit(meta); + }) + ); + this._workspaces.add(workspaceUnits); + return workspaceUnits; } catch (error) { this._logger(`Failed to parse workspaces from storage`); } } - return Promise.resolve(workspaces); + return []; } public override async deleteWorkspace(id: string): Promise { @@ -96,10 +92,10 @@ export class LocalProvider extends BaseProvider { this._storeWorkspaces(this._workspaces.list()); } - public override async createWorkspaceInfo( + public override async createWorkspace( meta: CreateWorkspaceInfoParams - ): Promise { - const workspaceInfo: WorkspaceMeta0 = { + ): Promise { + const workspaceUnit = await createWorkspaceUnit({ name: meta.name, id: uuidv4(), published: false, @@ -107,36 +103,11 @@ export class LocalProvider extends BaseProvider { owner: undefined, syncMode: 'core', memberCount: 1, - provider: 'local', - }; - return Promise.resolve(workspaceInfo); - } - - public override async createWorkspace( - blocksuiteWorkspace: BlocksuiteWorkspace, - meta: WorkspaceMeta0 - ): Promise { - const workspaceId = blocksuiteWorkspace.room; - assert(workspaceId, 'Blocksuite Workspace without room(workspaceId).'); - this._logger('Creating affine workspace'); - - const workspaceInfo: WorkspaceMeta0 = { - ...meta, - }; - - blocksuiteWorkspace.meta.setName(meta.name); - - if (!meta.avatar) { - await setDefaultAvatar(blocksuiteWorkspace); - workspaceInfo.avatar = blocksuiteWorkspace.meta.avatar; - } - - await initStore(blocksuiteWorkspace); - - this._workspaces.add(workspaceInfo); + provider: this.id, + }); + this._workspaces.add(workspaceUnit); this._storeWorkspaces(this._workspaces.list()); - - return blocksuiteWorkspace; + return workspaceUnit; } public override async clear(): Promise { diff --git a/packages/data-center/src/provider/local/utils.ts b/packages/data-center/src/provider/local/utils.ts new file mode 100644 index 000000000..bd5654c51 --- /dev/null +++ b/packages/data-center/src/provider/local/utils.ts @@ -0,0 +1,34 @@ +import { WorkspaceUnit } from '../../workspace-unit.js'; +import type { WorkspaceUnitCtorParams } from '../../workspace-unit'; +import { createBlocksuiteWorkspace } from '../../utils/index.js'; +import { applyLocalUpdates, writeUpdatesToLocal } from './indexeddb/utils.js'; +import { setDefaultAvatar } from '../utils.js'; + +export const loadWorkspaceUnit = async (params: WorkspaceUnitCtorParams) => { + const workspaceUnit = new WorkspaceUnit(params); + + const blocksuiteWorkspace = createBlocksuiteWorkspace(workspaceUnit.id); + + await applyLocalUpdates(blocksuiteWorkspace); + + workspaceUnit.setBlocksuiteWorkspace(blocksuiteWorkspace); + + return workspaceUnit; +}; + +export const createWorkspaceUnit = async (params: WorkspaceUnitCtorParams) => { + const workspaceUnit = new WorkspaceUnit(params); + + const blocksuiteWorkspace = createBlocksuiteWorkspace(workspaceUnit.id); + blocksuiteWorkspace.meta.setName(workspaceUnit.name); + if (!workspaceUnit.avatar) { + await setDefaultAvatar(blocksuiteWorkspace); + workspaceUnit.update({ avatar: blocksuiteWorkspace.meta.avatar }); + } + + await writeUpdatesToLocal(blocksuiteWorkspace); + + workspaceUnit.setBlocksuiteWorkspace(blocksuiteWorkspace); + + return workspaceUnit; +}; diff --git a/packages/data-center/src/types/index.ts b/packages/data-center/src/types/index.ts index ea85ded82..33343f939 100644 --- a/packages/data-center/src/types/index.ts +++ b/packages/data-center/src/types/index.ts @@ -25,4 +25,5 @@ export type Logger = ReturnType; export type Message = { code: number; message: string; + provider: string; }; diff --git a/packages/data-center/src/utils/index.ts b/packages/data-center/src/utils/index.ts index 2f2c516d8..9b3489bea 100644 --- a/packages/data-center/src/utils/index.ts +++ b/packages/data-center/src/utils/index.ts @@ -45,3 +45,25 @@ export async function getDefaultHeadImgBlob( } }); } + +export const applyUpdate = async ( + blocksuiteWorkspace: BlocksuiteWorkspace, + updates: Uint8Array +) => { + if (updates && updates.byteLength) { + await new Promise(resolve => { + // FIXME: if we merge two empty doc, there will no update event. + // So we set a timer to cancel update listener. + const doc = blocksuiteWorkspace.doc; + const timer = setTimeout(() => { + doc.off('update', resolve); + resolve(undefined); + }, 1000); + doc.once('update', () => { + clearTimeout(timer); + setTimeout(resolve, 100); + }); + BlocksuiteWorkspace.Y.applyUpdate(doc, new Uint8Array(updates)); + }); + } +}; diff --git a/packages/data-center/src/workspace-unit-collection.spec.ts b/packages/data-center/src/workspace-unit-collection.spec.ts index f010c31d1..405fc8237 100644 --- a/packages/data-center/src/workspace-unit-collection.spec.ts +++ b/packages/data-center/src/workspace-unit-collection.spec.ts @@ -1,6 +1,7 @@ import { test, expect } from '@playwright/test'; import { WorkspaceUnitCollection } from './workspace-unit-collection.js'; import type { WorkspaceUnitCollectionChangeEvent } from './workspace-unit-collection'; +import { WorkspaceUnit } from './workspace-unit.js'; test.describe.serial('workspace meta collection observable', () => { const workspaceUnitCollection = new WorkspaceUnitCollection(); @@ -14,13 +15,15 @@ test.describe.serial('workspace meta collection observable', () => { expect(event.added?.[0]?.id).toEqual('123'); } ); - scope.add({ + const workspaceUnit = new WorkspaceUnit({ id: '123', name: 'test', + avatar: undefined, memberCount: 1, provider: '', syncMode: 'core', }); + scope.add(workspaceUnit); }); test('list workspace', () => { diff --git a/packages/data-center/src/workspace-unit-collection.ts b/packages/data-center/src/workspace-unit-collection.ts index fced72064..b51f5d160 100644 --- a/packages/data-center/src/workspace-unit-collection.ts +++ b/packages/data-center/src/workspace-unit-collection.ts @@ -1,25 +1,24 @@ import { Observable } from 'lib0/observable'; -import { WorkspaceUnit } from './workspace-unit.js'; import type { - WorkspaceUnitCtorParams, + WorkspaceUnit, UpdateWorkspaceUnitParams, } from './workspace-unit'; export interface WorkspaceUnitCollectionScope { get: (workspaceId: string) => WorkspaceUnit | undefined; list: () => WorkspaceUnit[]; - add: (workspace: WorkspaceUnitCtorParams) => void; - remove: (workspaceId: string) => boolean; + add: (workspace: WorkspaceUnit | WorkspaceUnit[]) => void; + remove: (workspaceId: string | string[]) => boolean; clear: () => void; update: ( workspaceId: string, - workspaceMeta: UpdateWorkspaceUnitParams + workspaceUnit: UpdateWorkspaceUnitParams ) => void; } export interface WorkspaceUnitCollectionChangeEvent { added?: WorkspaceUnit[]; - deleted?: WorkspaceUnit; + deleted?: WorkspaceUnit[]; updated?: WorkspaceUnit; } @@ -59,45 +58,66 @@ export class WorkspaceUnitCollection { return this._workspaceUnitMap.get(workspaceId); }; - const add = (workspace: WorkspaceUnitCtorParams) => { - if (this._workspaceUnitMap.has(workspace.id)) { - // FIXME: multiple add same workspace + const add = (workspaceUnit: WorkspaceUnit | WorkspaceUnit[]) => { + const workspaceUnits = Array.isArray(workspaceUnit) + ? workspaceUnit + : [workspaceUnit]; + let added = false; + + workspaceUnits.forEach(workspaceUnit => { + if (this._workspaceUnitMap.has(workspaceUnit.id)) { + // FIXME: multiple add same workspace + return; + } + added = true; + this._workspaceUnitMap.set(workspaceUnit.id, workspaceUnit); + scopedWorkspaceIds.add(workspaceUnit.id); + }); + + if (!added) { return; } - const workspaceUnit = new WorkspaceUnit(workspace); - this._workspaceUnitMap.set(workspace.id.toString(), workspaceUnit); - - scopedWorkspaceIds.add(workspace.id); - this._events.emit('change', [ { - added: [workspaceUnit], + added: workspaceUnits, } as WorkspaceUnitCollectionChangeEvent, ]); }; - const remove = (workspaceId: string) => { - if (!scopedWorkspaceIds.has(workspaceId)) { - return true; - } + const remove = (workspaceId: string | string[]) => { + const workspaceIds = Array.isArray(workspaceId) + ? workspaceId + : [workspaceId]; + const workspaceUnits: WorkspaceUnit[] = []; - const workspaceUnit = this._workspaceUnitMap.get(workspaceId); - if (workspaceUnit) { - const ret = this._workspaceUnitMap.delete(workspaceId); - // If deletion failed, return. - if (!ret) { - return ret; + workspaceIds.forEach(workspaceId => { + if (!scopedWorkspaceIds.has(workspaceId)) { + return; } + const workspaceUnit = this._workspaceUnitMap.get(workspaceId); + if (workspaceUnit) { + const ret = this._workspaceUnitMap.delete(workspaceId); + // If deletion failed, return. + if (!ret) { + return; + } - scopedWorkspaceIds.delete(workspaceId); + workspaceUnits.push(workspaceUnit); + scopedWorkspaceIds.delete(workspaceId); + } + }); - this._events.emit('change', [ - { - deleted: workspaceUnit, - } as WorkspaceUnitCollectionChangeEvent, - ]); + if (!workspaceUnits.length) { + return false; } + + this._events.emit('change', [ + { + deleted: workspaceUnits, + } as WorkspaceUnitCollectionChangeEvent, + ]); + return true; }; @@ -107,10 +127,7 @@ export class WorkspaceUnitCollection { }); }; - const update = ( - workspaceId: string, - workspaceMeta: UpdateWorkspaceUnitParams - ) => { + const update = (workspaceId: string, meta: UpdateWorkspaceUnitParams) => { if (!scopedWorkspaceIds.has(workspaceId)) { return true; } @@ -120,7 +137,7 @@ export class WorkspaceUnitCollection { return true; } - workspaceUnit.update(workspaceMeta); + workspaceUnit.update(meta); this._events.emit('change', [ { diff --git a/packages/data-center/src/workspace-unit.ts b/packages/data-center/src/workspace-unit.ts index b7244c0b5..5bc4bf8ae 100644 --- a/packages/data-center/src/workspace-unit.ts +++ b/packages/data-center/src/workspace-unit.ts @@ -37,6 +37,16 @@ export class WorkspaceUnit { this.update(params); } + get isPublish() { + console.error('Suggest changing to published'); + return this.published; + } + + get isLocal() { + console.error('Suggest changing to syncMode'); + return this.syncMode === 'all'; + } + get blocksuiteWorkspace() { return this._blocksuiteWorkspace; } @@ -52,13 +62,16 @@ export class WorkspaceUnit { Object.assign(this, params); } - get isPublish() { - console.error('Suggest changing to published'); - return this.published; - } - - get isLocal() { - console.error('Suggest changing to syncMode'); - return this.syncMode === 'all'; + toJSON(): Omit { + return { + id: this.id, + name: this.name, + avatar: this.avatar, + owner: this.owner, + published: this.published, + memberCount: this.memberCount, + provider: this.provider, + syncMode: this.syncMode, + }; } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4844bc6a2..f608cb2fa 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3011,7 +3011,6 @@ packages: /@next/env/13.1.0: resolution: {integrity: sha512-6iNixFzCndH+Bl4FetQzOMjxCJqg8fs0LAlZviig1K6mIjOWH2m2oPcHcOg1Ta5VCe7Bx5KG1Hs+NrWDUkBt9A==} - dev: false /@next/eslint-plugin-next/12.3.1: resolution: {integrity: sha512-sw+lTf6r6P0j+g/n9y4qdWWI2syPqZx+uc0+B/fRENqfR3KpSid6MIKqc9gNwGhJASazEQ5b3w8h4cAET213jw==} @@ -3034,7 +3033,6 @@ packages: cpu: [arm] os: [android] requiresBuild: true - dev: false optional: true /@next/swc-android-arm64/12.3.1: @@ -3052,7 +3050,6 @@ packages: cpu: [arm64] os: [android] requiresBuild: true - dev: false optional: true /@next/swc-darwin-arm64/12.3.1: @@ -3070,7 +3067,6 @@ packages: cpu: [arm64] os: [darwin] requiresBuild: true - dev: false optional: true /@next/swc-darwin-x64/12.3.1: @@ -3088,7 +3084,6 @@ packages: cpu: [x64] os: [darwin] requiresBuild: true - dev: false optional: true /@next/swc-freebsd-x64/12.3.1: @@ -3106,7 +3101,6 @@ packages: cpu: [x64] os: [freebsd] requiresBuild: true - dev: false optional: true /@next/swc-linux-arm-gnueabihf/12.3.1: @@ -3124,7 +3118,6 @@ packages: cpu: [arm] os: [linux] requiresBuild: true - dev: false optional: true /@next/swc-linux-arm64-gnu/12.3.1: @@ -3142,7 +3135,6 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: false optional: true /@next/swc-linux-arm64-musl/12.3.1: @@ -3160,7 +3152,6 @@ packages: cpu: [arm64] os: [linux] requiresBuild: true - dev: false optional: true /@next/swc-linux-x64-gnu/12.3.1: @@ -3178,7 +3169,6 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: false optional: true /@next/swc-linux-x64-musl/12.3.1: @@ -3196,7 +3186,6 @@ packages: cpu: [x64] os: [linux] requiresBuild: true - dev: false optional: true /@next/swc-win32-arm64-msvc/12.3.1: @@ -3214,7 +3203,6 @@ packages: cpu: [arm64] os: [win32] requiresBuild: true - dev: false optional: true /@next/swc-win32-ia32-msvc/12.3.1: @@ -3232,7 +3220,6 @@ packages: cpu: [ia32] os: [win32] requiresBuild: true - dev: false optional: true /@next/swc-win32-x64-msvc/12.3.1: @@ -3250,7 +3237,6 @@ packages: cpu: [x64] os: [win32] requiresBuild: true - dev: false optional: true /@nodelib/fs.scandir/2.1.5: @@ -3613,7 +3599,6 @@ packages: resolution: {integrity: sha512-4C7nX/dvpzB7za4Ql9K81xK3HPxCpHMgwTZVyf+9JQ6VUbn9jjZVN7/Nkdz/Ugzs2CSjqnL/UPXroiVBVHUWUw==} dependencies: tslib: 2.4.0 - dev: false /@szmarczak/http-timer/5.0.1: resolution: {integrity: sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==} @@ -4580,7 +4565,6 @@ packages: /client-only/0.0.1: resolution: {integrity: sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==} - dev: false /cliui/6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} @@ -7512,7 +7496,6 @@ packages: resolution: {integrity: sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true - dev: false /natural-compare-lite/1.4.0: resolution: {integrity: sha512-Tj+HTDSJJKaZnfiuw+iaF9skdPpTo2GtEly5JHnWV/hfv2Qj/9RKsGISQtLh2ox3l5EAGw487hnBee0sIJ6v2g==} @@ -7635,7 +7618,6 @@ packages: transitivePeerDependencies: - '@babel/core' - babel-plugin-macros - dev: false /node-domexception/1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} @@ -7986,7 +7968,6 @@ packages: nanoid: 3.3.4 picocolors: 1.0.0 source-map-js: 1.0.2 - dev: false /preferred-pm/3.0.3: resolution: {integrity: sha512-+wZgbxNES/KlJs9q40F/1sfOd/j7f1O9JaHcW5Dsn3aUUOZg3L2bjpVUcKV2jvtElYfoTuQiNeMfQJ4kwUAhCQ==} @@ -8156,7 +8137,6 @@ packages: loose-envify: 1.4.0 react: 18.2.0 scheduler: 0.23.0 - dev: false /react-i18next/11.18.6_i18next@21.10.0: resolution: {integrity: sha512-yHb2F9BiT0lqoQDt8loZ5gWP331GwctHz9tYQ8A2EIEUu+CcEdjBLQWli1USG3RdWQt3W+jqQLg/d4rrQR96LA==} @@ -8253,7 +8233,6 @@ packages: engines: {node: '>=0.10.0'} dependencies: loose-envify: 1.4.0 - dev: false /read-pkg-up/7.0.1: resolution: {integrity: sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==} @@ -8531,7 +8510,6 @@ packages: resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} dependencies: loose-envify: 1.4.0 - dev: false /schema-utils/2.7.1: resolution: {integrity: sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==} @@ -8690,7 +8668,6 @@ packages: /source-map-js/1.0.2: resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} engines: {node: '>=0.10.0'} - dev: false /source-map-support/0.5.13: resolution: {integrity: sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==} @@ -8939,7 +8916,6 @@ packages: dependencies: client-only: 0.0.1 react: 18.2.0 - dev: false /stylis/4.0.13: resolution: {integrity: sha512-xGPXiFVl4YED9Jh7Euv2V220mriG9u4B2TA6Ybjc1catrstKD2PpIdU3U0RKpkVBC2EhmL/F0sPCr9vrFTNRag==}
+ { + router.push('/workspace'); + }} + > + Back Home + +