refactor: support suspense mode in workspaces (#1304)
This commit is contained in:
@@ -1,7 +1,11 @@
|
|||||||
import { atom } from 'jotai';
|
import { assertExists } from '@blocksuite/store';
|
||||||
import { createStore } from 'jotai';
|
import { atom, createStore } from 'jotai';
|
||||||
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
import { unstable_batchedUpdates } from 'react-dom';
|
import { unstable_batchedUpdates } from 'react-dom';
|
||||||
|
|
||||||
|
import { WorkspacePlugins } from '../plugins';
|
||||||
|
import { RemWorkspace, RemWorkspaceFlavour } from '../shared';
|
||||||
|
|
||||||
// workspace necessary atoms
|
// workspace necessary atoms
|
||||||
export const currentWorkspaceIdAtom = atom<string | null>(null);
|
export const currentWorkspaceIdAtom = atom<string | null>(null);
|
||||||
export const currentPageIdAtom = atom<string | null>(null);
|
export const currentPageIdAtom = atom<string | null>(null);
|
||||||
@@ -27,3 +31,32 @@ export const openCreateWorkspaceModalAtom = atom(false);
|
|||||||
export const openQuickSearchModalAtom = atom(false);
|
export const openQuickSearchModalAtom = atom(false);
|
||||||
|
|
||||||
export const jotaiStore = createStore();
|
export const jotaiStore = createStore();
|
||||||
|
|
||||||
|
type JotaiWorkspace = {
|
||||||
|
id: string;
|
||||||
|
flavour: RemWorkspaceFlavour;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const jotaiWorkspacesAtom = atomWithStorage<JotaiWorkspace[]>(
|
||||||
|
'jotai-workspaces',
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
export const workspacesAtom = atom<Promise<RemWorkspace[]>>(async get => {
|
||||||
|
const flavours: string[] = Object.values(WorkspacePlugins).map(
|
||||||
|
plugin => plugin.flavour
|
||||||
|
);
|
||||||
|
const jotaiWorkspaces = get(jotaiWorkspacesAtom).filter(workspace =>
|
||||||
|
flavours.includes(workspace.flavour)
|
||||||
|
);
|
||||||
|
const workspaces = await Promise.all(
|
||||||
|
jotaiWorkspaces.map(workspace => {
|
||||||
|
const plugin =
|
||||||
|
WorkspacePlugins[workspace.flavour as keyof typeof WorkspacePlugins];
|
||||||
|
assertExists(plugin);
|
||||||
|
const { CRUD } = plugin;
|
||||||
|
return CRUD.get(workspace.id);
|
||||||
|
})
|
||||||
|
);
|
||||||
|
return workspaces.filter(workspace => workspace !== null) as RemWorkspace[];
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
import { atom } from 'jotai/index';
|
import { atom } from 'jotai/index';
|
||||||
|
|
||||||
import {
|
import { BlockSuiteWorkspace } from '../../shared';
|
||||||
BlockSuiteWorkspace,
|
|
||||||
LocalWorkspace,
|
|
||||||
RemWorkspaceFlavour,
|
|
||||||
} from '../../shared';
|
|
||||||
import { apis } from '../../shared/apis';
|
import { apis } from '../../shared/apis';
|
||||||
import { createEmptyBlockSuiteWorkspace } from '../../utils';
|
import { createEmptyBlockSuiteWorkspace } from '../../utils';
|
||||||
|
|
||||||
@@ -35,14 +31,15 @@ export const publicBlockSuiteAtom = atom<Promise<BlockSuiteWorkspace>>(
|
|||||||
blockSuiteWorkspace.awarenessStore.setFlag('enable_drag_handle', false);
|
blockSuiteWorkspace.awarenessStore.setFlag('enable_drag_handle', false);
|
||||||
return new Promise(resolve => {
|
return new Promise(resolve => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const workspace: LocalWorkspace = {
|
// const workspace: LocalWorkspace = {
|
||||||
id: workspaceId,
|
// id: workspaceId,
|
||||||
blockSuiteWorkspace,
|
// blockSuiteWorkspace,
|
||||||
flavour: RemWorkspaceFlavour.LOCAL,
|
// flavour: RemWorkspaceFlavour.LOCAL,
|
||||||
providers: [],
|
// providers: [],
|
||||||
};
|
// };
|
||||||
dataCenter.workspaces.push(workspace);
|
// fixme: quick search won't work, ASAP
|
||||||
dataCenter.callbacks.forEach(cb => cb());
|
// dataCenter.workspaces.push(workspace);
|
||||||
|
// dataCenter.callbacks.forEach(cb => cb());
|
||||||
resolve(blockSuiteWorkspace);
|
resolve(blockSuiteWorkspace);
|
||||||
}, 0);
|
}, 0);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -63,6 +63,7 @@ const createIndexedDBProvider = (
|
|||||||
cleanup: () => {
|
cleanup: () => {
|
||||||
assertExists(indexeddbProvider);
|
assertExists(indexeddbProvider);
|
||||||
indexeddbProvider.clearData();
|
indexeddbProvider.clearData();
|
||||||
|
indexeddbProvider = null;
|
||||||
},
|
},
|
||||||
connect: () => {
|
connect: () => {
|
||||||
providerLogger.info(
|
providerLogger.info(
|
||||||
|
|||||||
@@ -5,22 +5,36 @@ import 'fake-indexeddb/auto';
|
|||||||
|
|
||||||
import { assertExists } from '@blocksuite/store';
|
import { assertExists } from '@blocksuite/store';
|
||||||
import { render, renderHook } from '@testing-library/react';
|
import { render, renderHook } from '@testing-library/react';
|
||||||
|
import { createStore, getDefaultStore, Provider } from 'jotai';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { useCallback, useState } from 'react';
|
import React, { useCallback, useState } from 'react';
|
||||||
import { describe, expect, test, vi } from 'vitest';
|
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||||
import createFetchMock from 'vitest-fetch-mock';
|
import createFetchMock from 'vitest-fetch-mock';
|
||||||
|
|
||||||
|
import { workspacesAtom } from '../../atoms';
|
||||||
import { useCurrentPageId } from '../../hooks/current/use-current-page-id';
|
import { useCurrentPageId } from '../../hooks/current/use-current-page-id';
|
||||||
import { useCurrentWorkspace } from '../../hooks/current/use-current-workspace';
|
import {
|
||||||
|
currentWorkspaceAtom,
|
||||||
|
useCurrentWorkspace,
|
||||||
|
} from '../../hooks/current/use-current-workspace';
|
||||||
import { useBlockSuiteWorkspaceHelper } from '../../hooks/use-blocksuite-workspace-helper';
|
import { useBlockSuiteWorkspaceHelper } from '../../hooks/use-blocksuite-workspace-helper';
|
||||||
import { useWorkspacesHelper } from '../../hooks/use-workspaces';
|
import { useWorkspacesHelper } from '../../hooks/use-workspaces';
|
||||||
import { ThemeProvider } from '../../providers/ThemeProvider';
|
import { ThemeProvider } from '../../providers/ThemeProvider';
|
||||||
import { pathGenerator } from '../../shared';
|
import { pathGenerator, RemWorkspaceFlavour } from '../../shared';
|
||||||
import { WorkSpaceSliderBar } from '../pure/workspace-slider-bar';
|
import { WorkSpaceSliderBar } from '../pure/workspace-slider-bar';
|
||||||
|
|
||||||
const fetchMocker = createFetchMock(vi);
|
const fetchMocker = createFetchMock(vi);
|
||||||
|
|
||||||
// fetchMocker.enableMocks();
|
// fetchMocker.enableMocks();
|
||||||
|
let store = getDefaultStore();
|
||||||
|
beforeEach(async () => {
|
||||||
|
store = createStore();
|
||||||
|
await store.get(workspacesAtom);
|
||||||
|
});
|
||||||
|
|
||||||
|
const ProviderWrapper: React.FC<React.PropsWithChildren> = ({ children }) => {
|
||||||
|
return <Provider store={store}>{children}</Provider>;
|
||||||
|
};
|
||||||
|
|
||||||
describe('WorkSpaceSliderBar', () => {
|
describe('WorkSpaceSliderBar', () => {
|
||||||
test('basic', async () => {
|
test('basic', async () => {
|
||||||
@@ -28,10 +42,17 @@ describe('WorkSpaceSliderBar', () => {
|
|||||||
|
|
||||||
const onOpenWorkspaceListModalFn = vi.fn();
|
const onOpenWorkspaceListModalFn = vi.fn();
|
||||||
const onOpenQuickSearchModalFn = vi.fn();
|
const onOpenQuickSearchModalFn = vi.fn();
|
||||||
const mutationHook = renderHook(() => useWorkspacesHelper());
|
const mutationHook = renderHook(() => useWorkspacesHelper(), {
|
||||||
const id = mutationHook.result.current.createRemLocalWorkspace('test0');
|
wrapper: ProviderWrapper,
|
||||||
|
});
|
||||||
|
const id = await mutationHook.result.current.createLocalWorkspace('test0');
|
||||||
|
await store.get(workspacesAtom);
|
||||||
|
mutationHook.rerender();
|
||||||
mutationHook.result.current.createWorkspacePage(id, 'test1');
|
mutationHook.result.current.createWorkspacePage(id, 'test1');
|
||||||
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace());
|
await store.get(currentWorkspaceAtom);
|
||||||
|
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), {
|
||||||
|
wrapper: ProviderWrapper,
|
||||||
|
});
|
||||||
let i = 0;
|
let i = 0;
|
||||||
const Component = () => {
|
const Component = () => {
|
||||||
const [show, setShow] = useState(false);
|
const [show, setShow] = useState(false);
|
||||||
@@ -63,11 +84,17 @@ describe('WorkSpaceSliderBar', () => {
|
|||||||
const App = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
<ThemeProvider>
|
<ThemeProvider>
|
||||||
<Component />
|
<ProviderWrapper>
|
||||||
|
<Component />
|
||||||
|
</ProviderWrapper>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
currentWorkspaceHook.result.current[1](id);
|
currentWorkspaceHook.result.current[1](id);
|
||||||
|
const currentWorkspace = await store.get(currentWorkspaceAtom);
|
||||||
|
expect(currentWorkspace).toBeDefined();
|
||||||
|
expect(currentWorkspace?.flavour).toBe(RemWorkspaceFlavour.LOCAL);
|
||||||
|
expect(currentWorkspace?.id).toBe(id);
|
||||||
const app = render(<App />);
|
const app = render(<App />);
|
||||||
const card = await app.findByTestId('current-workspace');
|
const card = await app.findByTestId('current-workspace');
|
||||||
expect(onOpenWorkspaceListModalFn).toBeCalledTimes(0);
|
expect(onOpenWorkspaceListModalFn).toBeCalledTimes(0);
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ const isMac = () => {
|
|||||||
|
|
||||||
export type QuickSearchModalProps = {
|
export type QuickSearchModalProps = {
|
||||||
blockSuiteWorkspace: BlockSuiteWorkspace;
|
blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||||
enableShortCut: boolean;
|
|
||||||
open: boolean;
|
open: boolean;
|
||||||
setOpen: (value: boolean) => void;
|
setOpen: (value: boolean) => void;
|
||||||
router: NextRouter;
|
router: NextRouter;
|
||||||
@@ -40,7 +39,6 @@ export const QuickSearchModal: React.FC<QuickSearchModalProps> = ({
|
|||||||
open,
|
open,
|
||||||
setOpen,
|
setOpen,
|
||||||
router,
|
router,
|
||||||
enableShortCut,
|
|
||||||
blockSuiteWorkspace,
|
blockSuiteWorkspace,
|
||||||
}) => {
|
}) => {
|
||||||
const [loading, startTransition] = useTransition();
|
const [loading, startTransition] = useTransition();
|
||||||
@@ -65,9 +63,6 @@ export const QuickSearchModal: React.FC<QuickSearchModalProps> = ({
|
|||||||
}, [setOpen, setQuery]);
|
}, [setOpen, setQuery]);
|
||||||
// Add ‘⌘+K’ shortcut keys as switches
|
// Add ‘⌘+K’ shortcut keys as switches
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!enableShortCut) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const keydown = (e: KeyboardEvent) => {
|
const keydown = (e: KeyboardEvent) => {
|
||||||
if ((e.key === 'k' && e.metaKey) || (e.key === 'k' && e.ctrlKey)) {
|
if ((e.key === 'k' && e.metaKey) || (e.key === 'k' && e.ctrlKey)) {
|
||||||
const selection = window.getSelection();
|
const selection = window.getSelection();
|
||||||
@@ -86,7 +81,7 @@ export const QuickSearchModal: React.FC<QuickSearchModalProps> = ({
|
|||||||
document.addEventListener('keydown', keydown, { capture: true });
|
document.addEventListener('keydown', keydown, { capture: true });
|
||||||
return () =>
|
return () =>
|
||||||
document.removeEventListener('keydown', keydown, { capture: true });
|
document.removeEventListener('keydown', keydown, { capture: true });
|
||||||
}, [enableShortCut, open, router, setOpen, setQuery]);
|
}, [open, router, setOpen, setQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
|||||||
@@ -217,6 +217,7 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
|
|||||||
currentPath ===
|
currentPath ===
|
||||||
(currentWorkspaceId && paths.setting(currentWorkspaceId))
|
(currentWorkspaceId && paths.setting(currentWorkspaceId))
|
||||||
}
|
}
|
||||||
|
data-testid="slider-bar-workspace-setting-button"
|
||||||
>
|
>
|
||||||
<StyledLink
|
<StyledLink
|
||||||
href={{
|
href={{
|
||||||
|
|||||||
@@ -8,22 +8,26 @@ import assert from 'node:assert';
|
|||||||
import { __unstableSchemas, builtInSchemas } from '@blocksuite/blocks/models';
|
import { __unstableSchemas, builtInSchemas } from '@blocksuite/blocks/models';
|
||||||
import { assertExists } from '@blocksuite/store';
|
import { assertExists } from '@blocksuite/store';
|
||||||
import { render, renderHook } from '@testing-library/react';
|
import { render, renderHook } from '@testing-library/react';
|
||||||
|
import { createStore, Provider } from 'jotai';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import routerMock from 'next-router-mock';
|
import routerMock from 'next-router-mock';
|
||||||
import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes';
|
import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes';
|
||||||
|
import React from 'react';
|
||||||
import { beforeAll, beforeEach, describe, expect, test } from 'vitest';
|
import { beforeAll, beforeEach, describe, expect, test } from 'vitest';
|
||||||
|
|
||||||
|
import { workspacesAtom } from '../../atoms';
|
||||||
import { BlockSuiteWorkspace, RemWorkspaceFlavour } from '../../shared';
|
import { BlockSuiteWorkspace, RemWorkspaceFlavour } from '../../shared';
|
||||||
import { useCurrentWorkspace } from '../current/use-current-workspace';
|
|
||||||
import { useBlockSuiteWorkspaceName } from '../use-blocksuite-workspace-name';
|
|
||||||
import { useLastOpenedWorkspace } from '../use-last-opened-workspace';
|
|
||||||
import { usePageMeta, usePageMetaHelper } from '../use-page-meta';
|
|
||||||
import { useSyncRouterWithCurrentWorkspaceAndPage } from '../use-sync-router-with-current-workspace-and-page';
|
|
||||||
import {
|
import {
|
||||||
useWorkspaces,
|
currentWorkspaceAtom,
|
||||||
useWorkspacesHelper,
|
useCurrentWorkspace,
|
||||||
vitestRefreshWorkspaces,
|
} from '../current/use-current-workspace';
|
||||||
} from '../use-workspaces';
|
import { useBlockSuiteWorkspaceName } from '../use-blocksuite-workspace-name';
|
||||||
|
import { usePageMeta, usePageMetaHelper } from '../use-page-meta';
|
||||||
|
import {
|
||||||
|
REDIRECT_TIMEOUT,
|
||||||
|
useSyncRouterWithCurrentWorkspaceAndPage,
|
||||||
|
} from '../use-sync-router-with-current-workspace-and-page';
|
||||||
|
import { useWorkspaces, useWorkspacesHelper } from '../use-workspaces';
|
||||||
|
|
||||||
let blockSuiteWorkspace: BlockSuiteWorkspace;
|
let blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@@ -32,9 +36,26 @@ beforeAll(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
localStorage.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
async function getJotaiContext() {
|
||||||
|
const store = createStore();
|
||||||
|
const ProviderWrapper: React.FC<React.PropsWithChildren> =
|
||||||
|
function ProviderWrapper({ children }) {
|
||||||
|
return <Provider store={store}>{children}</Provider>;
|
||||||
|
};
|
||||||
|
const workspaces = await store.get(workspacesAtom);
|
||||||
|
expect(workspaces.length).toBe(0);
|
||||||
|
return {
|
||||||
|
store,
|
||||||
|
ProviderWrapper,
|
||||||
|
initialWorkspaces: workspaces,
|
||||||
|
} as const;
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
vitestRefreshWorkspaces();
|
|
||||||
dataCenter.isLoaded = true;
|
|
||||||
return new Promise<void>(resolve => {
|
return new Promise<void>(resolve => {
|
||||||
blockSuiteWorkspace = new BlockSuiteWorkspace({
|
blockSuiteWorkspace = new BlockSuiteWorkspace({
|
||||||
room: 'test',
|
room: 'test',
|
||||||
@@ -104,16 +125,50 @@ describe('usePageMetas', async () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('useWorkspacesHelper', () => {
|
||||||
|
test('basic', async () => {
|
||||||
|
const { ProviderWrapper, store } = await getJotaiContext();
|
||||||
|
const workspaceHelperHook = renderHook(() => useWorkspacesHelper(), {
|
||||||
|
wrapper: ProviderWrapper,
|
||||||
|
});
|
||||||
|
const id = await workspaceHelperHook.result.current.createLocalWorkspace(
|
||||||
|
'test'
|
||||||
|
);
|
||||||
|
const workspaces = await store.get(workspacesAtom);
|
||||||
|
expect(workspaces.length).toBe(1);
|
||||||
|
expect(workspaces[0].id).toBe(id);
|
||||||
|
const workspacesHook = renderHook(() => useWorkspaces(), {
|
||||||
|
wrapper: ProviderWrapper,
|
||||||
|
});
|
||||||
|
await store.get(currentWorkspaceAtom);
|
||||||
|
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace(), {
|
||||||
|
wrapper: ProviderWrapper,
|
||||||
|
});
|
||||||
|
currentWorkspaceHook.result.current[1](workspacesHook.result.current[0].id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('useWorkspaces', () => {
|
describe('useWorkspaces', () => {
|
||||||
test('basic', () => {
|
test('basic', async () => {
|
||||||
const { result } = renderHook(() => useWorkspaces());
|
const { ProviderWrapper } = await getJotaiContext();
|
||||||
|
const { result } = renderHook(() => useWorkspaces(), {
|
||||||
|
wrapper: ProviderWrapper,
|
||||||
|
});
|
||||||
expect(result.current).toEqual([]);
|
expect(result.current).toEqual([]);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('mutation', () => {
|
test('mutation', async () => {
|
||||||
const { result } = renderHook(() => useWorkspacesHelper());
|
const { ProviderWrapper, store } = await getJotaiContext();
|
||||||
result.current.createRemLocalWorkspace('test');
|
const { result } = renderHook(() => useWorkspacesHelper(), {
|
||||||
const { result: result2 } = renderHook(() => useWorkspaces());
|
wrapper: ProviderWrapper,
|
||||||
|
});
|
||||||
|
await result.current.createLocalWorkspace('test');
|
||||||
|
const workspaces = await store.get(workspacesAtom);
|
||||||
|
console.log(workspaces);
|
||||||
|
expect(workspaces.length).toEqual(1);
|
||||||
|
const { result: result2 } = renderHook(() => useWorkspaces(), {
|
||||||
|
wrapper: ProviderWrapper,
|
||||||
|
});
|
||||||
expect(result2.current.length).toEqual(1);
|
expect(result2.current.length).toEqual(1);
|
||||||
const firstWorkspace = result2.current[0];
|
const firstWorkspace = result2.current[0];
|
||||||
expect(firstWorkspace.flavour).toBe('local');
|
expect(firstWorkspace.flavour).toBe('local');
|
||||||
@@ -124,8 +179,13 @@ describe('useWorkspaces', () => {
|
|||||||
|
|
||||||
describe('useSyncRouterWithCurrentWorkspaceAndPage', () => {
|
describe('useSyncRouterWithCurrentWorkspaceAndPage', () => {
|
||||||
test('from "/"', async () => {
|
test('from "/"', async () => {
|
||||||
const mutationHook = renderHook(() => useWorkspacesHelper());
|
const { ProviderWrapper, store } = await getJotaiContext();
|
||||||
const id = mutationHook.result.current.createRemLocalWorkspace('test0');
|
const mutationHook = renderHook(() => useWorkspacesHelper(), {
|
||||||
|
wrapper: ProviderWrapper,
|
||||||
|
});
|
||||||
|
const id = await mutationHook.result.current.createLocalWorkspace('test0');
|
||||||
|
await store.get(currentWorkspaceAtom);
|
||||||
|
mutationHook.rerender();
|
||||||
mutationHook.result.current.createWorkspacePage(id, 'page0');
|
mutationHook.result.current.createWorkspacePage(id, 'page0');
|
||||||
const routerHook = renderHook(() => useRouter());
|
const routerHook = renderHook(() => useRouter());
|
||||||
await routerHook.result.current.push('/');
|
await routerHook.result.current.push('/');
|
||||||
@@ -134,6 +194,7 @@ describe('useSyncRouterWithCurrentWorkspaceAndPage', () => {
|
|||||||
renderHook(
|
renderHook(
|
||||||
({ router }) => useSyncRouterWithCurrentWorkspaceAndPage(router),
|
({ router }) => useSyncRouterWithCurrentWorkspaceAndPage(router),
|
||||||
{
|
{
|
||||||
|
wrapper: ProviderWrapper,
|
||||||
initialProps: {
|
initialProps: {
|
||||||
router: routerHook.result.current,
|
router: routerHook.result.current,
|
||||||
},
|
},
|
||||||
@@ -144,8 +205,14 @@ describe('useSyncRouterWithCurrentWorkspaceAndPage', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test('from incorrect "/workspace/[workspaceId]/[pageId]"', async () => {
|
test('from incorrect "/workspace/[workspaceId]/[pageId]"', async () => {
|
||||||
const mutationHook = renderHook(() => useWorkspacesHelper());
|
const { ProviderWrapper, store } = await getJotaiContext();
|
||||||
const id = mutationHook.result.current.createRemLocalWorkspace('test0');
|
const mutationHook = renderHook(() => useWorkspacesHelper(), {
|
||||||
|
wrapper: ProviderWrapper,
|
||||||
|
});
|
||||||
|
const id = await mutationHook.result.current.createLocalWorkspace('test0');
|
||||||
|
const workspaces = await store.get(workspacesAtom);
|
||||||
|
expect(workspaces.length).toEqual(1);
|
||||||
|
mutationHook.rerender();
|
||||||
mutationHook.result.current.createWorkspacePage(id, 'page0');
|
mutationHook.result.current.createWorkspacePage(id, 'page0');
|
||||||
const routerHook = renderHook(() => useRouter());
|
const routerHook = renderHook(() => useRouter());
|
||||||
await routerHook.result.current.push(`/workspace/${id}/not_exist`);
|
await routerHook.result.current.push(`/workspace/${id}/not_exist`);
|
||||||
@@ -154,29 +221,16 @@ describe('useSyncRouterWithCurrentWorkspaceAndPage', () => {
|
|||||||
renderHook(
|
renderHook(
|
||||||
({ router }) => useSyncRouterWithCurrentWorkspaceAndPage(router),
|
({ router }) => useSyncRouterWithCurrentWorkspaceAndPage(router),
|
||||||
{
|
{
|
||||||
|
wrapper: ProviderWrapper,
|
||||||
initialProps: {
|
initialProps: {
|
||||||
router: routerHook.result.current,
|
router: routerHook.result.current,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/page0`);
|
await new Promise(resolve => setTimeout(resolve, REDIRECT_TIMEOUT));
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('useLastOpenedWorkspace', () => {
|
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/page0`);
|
||||||
test('basic', async () => {
|
|
||||||
const workspaceHelperHook = renderHook(() => useWorkspacesHelper());
|
|
||||||
workspaceHelperHook.result.current.createRemLocalWorkspace('test');
|
|
||||||
const workspacesHook = renderHook(() => useWorkspaces());
|
|
||||||
const currentWorkspaceHook = renderHook(() => useCurrentWorkspace());
|
|
||||||
currentWorkspaceHook.result.current[1](workspacesHook.result.current[0].id);
|
|
||||||
const lastOpenedWorkspace = renderHook(() => useLastOpenedWorkspace());
|
|
||||||
expect(lastOpenedWorkspace.result.current[0]).toBe(null);
|
|
||||||
const lastOpenedWorkspace2 = renderHook(() => useLastOpenedWorkspace());
|
|
||||||
expect(lastOpenedWorkspace2.result.current[0]).toBe(
|
|
||||||
workspacesHook.result.current[0].id
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,10 @@ import { beforeEach, describe, expect, test, vi } from 'vitest';
|
|||||||
import { BlockSuiteWorkspace } from '../../shared';
|
import { BlockSuiteWorkspace } from '../../shared';
|
||||||
import { useBlockSuiteWorkspaceHelper } from '../use-blocksuite-workspace-helper';
|
import { useBlockSuiteWorkspaceHelper } from '../use-blocksuite-workspace-helper';
|
||||||
import { usePageMeta } from '../use-page-meta';
|
import { usePageMeta } from '../use-page-meta';
|
||||||
import { vitestRefreshWorkspaces } from '../use-workspaces';
|
|
||||||
|
|
||||||
let blockSuiteWorkspace: BlockSuiteWorkspace;
|
let blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vitestRefreshWorkspaces();
|
|
||||||
blockSuiteWorkspace = new BlockSuiteWorkspace({
|
blockSuiteWorkspace = new BlockSuiteWorkspace({
|
||||||
room: 'test',
|
room: 'test',
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { mutate } from 'swr';
|
import { mutate } from 'swr';
|
||||||
|
|
||||||
|
import { jotaiStore, jotaiWorkspacesAtom } from '../../atoms';
|
||||||
import { QueryKey } from '../../plugins/affine/fetcher';
|
import { QueryKey } from '../../plugins/affine/fetcher';
|
||||||
import { AffineWorkspace } from '../../shared';
|
import { AffineWorkspace } from '../../shared';
|
||||||
import { apis } from '../../shared/apis';
|
import { apis } from '../../shared/apis';
|
||||||
import { refreshDataCenter } from '../use-workspaces';
|
|
||||||
|
|
||||||
export function useToggleWorkspacePublish(workspace: AffineWorkspace) {
|
export function useToggleWorkspacePublish(workspace: AffineWorkspace) {
|
||||||
return useCallback(
|
return useCallback(
|
||||||
@@ -14,7 +14,10 @@ export function useToggleWorkspacePublish(workspace: AffineWorkspace) {
|
|||||||
public: isPublish,
|
public: isPublish,
|
||||||
});
|
});
|
||||||
await mutate(QueryKey.getWorkspaces);
|
await mutate(QueryKey.getWorkspaces);
|
||||||
await refreshDataCenter();
|
// force update
|
||||||
|
jotaiStore.set(jotaiWorkspacesAtom, [
|
||||||
|
...jotaiStore.get(jotaiWorkspacesAtom),
|
||||||
|
]);
|
||||||
},
|
},
|
||||||
[workspace]
|
[workspace]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,7 +1,40 @@
|
|||||||
import { useAtom } from 'jotai';
|
import { Page } from '@blocksuite/store';
|
||||||
|
import { atom, useAtom, useAtomValue } from 'jotai';
|
||||||
|
|
||||||
import { currentPageIdAtom } from '../../atoms';
|
import { currentPageIdAtom } from '../../atoms';
|
||||||
|
import { currentWorkspaceAtom } from './use-current-workspace';
|
||||||
|
|
||||||
|
export const currentPageAtom = atom<Promise<Page | null>>(async get => {
|
||||||
|
const id = get(currentPageIdAtom);
|
||||||
|
const workspace = await get(currentWorkspaceAtom);
|
||||||
|
if (!workspace || !id) {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
const page = workspace.blockSuiteWorkspace.getPage(id);
|
||||||
|
if (page) {
|
||||||
|
return page;
|
||||||
|
} else {
|
||||||
|
return new Promise(resolve => {
|
||||||
|
const dispose = workspace.blockSuiteWorkspace.slots.pageAdded.on(
|
||||||
|
pageId => {
|
||||||
|
if (pageId === id) {
|
||||||
|
resolve(page);
|
||||||
|
dispose.dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export function useCurrentPage(): Page | null {
|
||||||
|
return useAtomValue(currentPageAtom);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated
|
||||||
|
*/
|
||||||
export function useCurrentPageId(): [
|
export function useCurrentPageId(): [
|
||||||
string | null,
|
string | null,
|
||||||
(newId: string | null) => void
|
(newId: string | null) => void
|
||||||
|
|||||||
@@ -1,18 +1,30 @@
|
|||||||
import { useAtom } from 'jotai';
|
import { atom, useAtom, useAtomValue } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { currentPageIdAtom, currentWorkspaceIdAtom } from '../../atoms';
|
import {
|
||||||
|
currentPageIdAtom,
|
||||||
|
currentWorkspaceIdAtom,
|
||||||
|
workspacesAtom,
|
||||||
|
} from '../../atoms';
|
||||||
import { RemWorkspace } from '../../shared';
|
import { RemWorkspace } from '../../shared';
|
||||||
import { useWorkspace } from '../use-workspace';
|
|
||||||
|
export const currentWorkspaceAtom = atom<Promise<RemWorkspace | null>>(
|
||||||
|
async get => {
|
||||||
|
const id = get(currentWorkspaceIdAtom);
|
||||||
|
const workspaces = await get(workspacesAtom);
|
||||||
|
return workspaces.find(workspace => workspace.id === id) ?? null;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
export function useCurrentWorkspace(): [
|
export function useCurrentWorkspace(): [
|
||||||
RemWorkspace | null,
|
RemWorkspace | null,
|
||||||
(id: string | null) => void
|
(id: string | null) => void
|
||||||
] {
|
] {
|
||||||
const [id, setId] = useAtom(currentWorkspaceIdAtom);
|
const currentWorkspace = useAtomValue(currentWorkspaceAtom);
|
||||||
|
const [, setId] = useAtom(currentWorkspaceIdAtom);
|
||||||
const [, setPageId] = useAtom(currentPageIdAtom);
|
const [, setPageId] = useAtom(currentPageIdAtom);
|
||||||
return [
|
return [
|
||||||
useWorkspace(id),
|
currentWorkspace,
|
||||||
useCallback(
|
useCallback(
|
||||||
(id: string | null) => {
|
(id: string | null) => {
|
||||||
setPageId(null);
|
setPageId(null);
|
||||||
|
|||||||
53
apps/web/src/hooks/use-create-first-workspace.ts
Normal file
53
apps/web/src/hooks/use-create-first-workspace.ts
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { DEFAULT_WORKSPACE_NAME } from '@affine/env';
|
||||||
|
import { assertEquals, assertExists, nanoid } from '@blocksuite/store';
|
||||||
|
import { useAtom } from 'jotai/index';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { jotaiWorkspacesAtom } from '../atoms';
|
||||||
|
import { LocalPlugin } from '../plugins/local';
|
||||||
|
import { RemWorkspaceFlavour } from '../shared';
|
||||||
|
import { createEmptyBlockSuiteWorkspace } from '../utils';
|
||||||
|
|
||||||
|
export function useCreateFirstWorkspace() {
|
||||||
|
const [jotaiWorkspaces, set] = useAtom(jotaiWorkspacesAtom);
|
||||||
|
useEffect(() => {
|
||||||
|
const controller = new AbortController();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a first workspace, only just once for a browser
|
||||||
|
*/
|
||||||
|
async function createFirst() {
|
||||||
|
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||||
|
nanoid(),
|
||||||
|
(_: string) => undefined
|
||||||
|
);
|
||||||
|
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
|
||||||
|
const id = await LocalPlugin.CRUD.create(blockSuiteWorkspace);
|
||||||
|
const workspace = await LocalPlugin.CRUD.get(id);
|
||||||
|
assertExists(workspace);
|
||||||
|
assertEquals(workspace.id, id);
|
||||||
|
const newPageId = nanoid();
|
||||||
|
workspace.blockSuiteWorkspace.slots.pageAdded.once(pageId => {
|
||||||
|
assertEquals(pageId, newPageId);
|
||||||
|
set(workspaces => [
|
||||||
|
...workspaces,
|
||||||
|
{
|
||||||
|
id: workspace.id,
|
||||||
|
flavour: RemWorkspaceFlavour.LOCAL,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
workspace.blockSuiteWorkspace.createPage(newPageId);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
jotaiWorkspaces.length === 0 &&
|
||||||
|
localStorage.getItem('first') !== 'true'
|
||||||
|
) {
|
||||||
|
localStorage.setItem('first', 'true');
|
||||||
|
createFirst();
|
||||||
|
}
|
||||||
|
return () => {
|
||||||
|
controller.abort();
|
||||||
|
};
|
||||||
|
}, [jotaiWorkspaces.length, set]);
|
||||||
|
}
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
import { useCallback, useEffect, useState } from 'react';
|
|
||||||
|
|
||||||
import { useCurrentPageId } from './current/use-current-page-id';
|
|
||||||
import { useCurrentWorkspace } from './current/use-current-workspace';
|
|
||||||
|
|
||||||
const kLastOpenedWorkspaceKey = 'affine-last-opened-workspace';
|
|
||||||
const kLastOpenedPageKey = 'affine-last-opened-page';
|
|
||||||
|
|
||||||
export function useLastOpenedWorkspace(): [
|
|
||||||
string | null,
|
|
||||||
string | null,
|
|
||||||
() => void
|
|
||||||
] {
|
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
|
||||||
const [currentPageId] = useCurrentPageId();
|
|
||||||
const [lastWorkspaceId, setLastWorkspaceId] = useState<string | null>(null);
|
|
||||||
const [lastPageId, setLastPageId] = useState<string | null>(null);
|
|
||||||
useEffect(() => {
|
|
||||||
const lastWorkspaceId = localStorage.getItem(kLastOpenedWorkspaceKey);
|
|
||||||
if (lastWorkspaceId) {
|
|
||||||
setLastWorkspaceId(lastWorkspaceId);
|
|
||||||
}
|
|
||||||
const lastPageId = localStorage.getItem(kLastOpenedPageKey);
|
|
||||||
if (lastPageId) {
|
|
||||||
setLastPageId(lastPageId);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
useEffect(() => {
|
|
||||||
if (currentWorkspace) {
|
|
||||||
localStorage.setItem(kLastOpenedWorkspaceKey, currentWorkspace.id);
|
|
||||||
}
|
|
||||||
if (currentPageId) {
|
|
||||||
localStorage.setItem(kLastOpenedPageKey, currentPageId);
|
|
||||||
}
|
|
||||||
}, [currentPageId, currentWorkspace]);
|
|
||||||
const refresh = useCallback(() => {
|
|
||||||
localStorage.removeItem(kLastOpenedWorkspaceKey);
|
|
||||||
localStorage.removeItem(kLastOpenedPageKey);
|
|
||||||
}, []);
|
|
||||||
return [lastWorkspaceId, lastPageId, refresh];
|
|
||||||
}
|
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { NextRouter } from 'next/router';
|
import { NextRouter } from 'next/router';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
import { currentPageIdAtom, jotaiStore } from '../atoms';
|
||||||
import { RemWorkspace, RemWorkspaceFlavour } from '../shared';
|
import { RemWorkspace, RemWorkspaceFlavour } from '../shared';
|
||||||
import { useCurrentPageId } from './current/use-current-page-id';
|
import { useCurrentPageId } from './current/use-current-page-id';
|
||||||
import { useCurrentWorkspace } from './current/use-current-workspace';
|
import { useCurrentWorkspace } from './current/use-current-workspace';
|
||||||
import { useWorkspaces, useWorkspacesIsLoaded } from './use-workspaces';
|
import { useWorkspaces } from './use-workspaces';
|
||||||
|
|
||||||
export function findSuitablePageId(
|
export function findSuitablePageId(
|
||||||
workspace: RemWorkspace,
|
workspace: RemWorkspace,
|
||||||
@@ -32,11 +33,11 @@ export function findSuitablePageId(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const REDIRECT_TIMEOUT = 1000;
|
||||||
export function useSyncRouterWithCurrentWorkspaceAndPage(router: NextRouter) {
|
export function useSyncRouterWithCurrentWorkspaceAndPage(router: NextRouter) {
|
||||||
const [currentWorkspace, setCurrentWorkspaceId] = useCurrentWorkspace();
|
const [currentWorkspace, setCurrentWorkspaceId] = useCurrentWorkspace();
|
||||||
const [currentPageId, setCurrentPageId] = useCurrentPageId();
|
const [currentPageId, setCurrentPageId] = useCurrentPageId();
|
||||||
const workspaces = useWorkspaces();
|
const workspaces = useWorkspaces();
|
||||||
const isLoaded = useWorkspacesIsLoaded();
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const listener: Parameters<typeof router.events.on>[1] = (url: string) => {
|
const listener: Parameters<typeof router.events.on>[1] = (url: string) => {
|
||||||
if (url.startsWith('/')) {
|
if (url.startsWith('/')) {
|
||||||
@@ -66,7 +67,7 @@ export function useSyncRouterWithCurrentWorkspaceAndPage(router: NextRouter) {
|
|||||||
};
|
};
|
||||||
}, [currentWorkspace, router, setCurrentPageId, setCurrentWorkspaceId]);
|
}, [currentWorkspace, router, setCurrentPageId, setCurrentWorkspaceId]);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!router.isReady || !isLoaded) {
|
if (!router.isReady) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (
|
if (
|
||||||
@@ -130,19 +131,55 @@ export function useSyncRouterWithCurrentWorkspaceAndPage(router: NextRouter) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!currentPageId && currentWorkspace) {
|
if (!currentPageId && currentWorkspace) {
|
||||||
if ('blockSuiteWorkspace' in currentWorkspace) {
|
const targetId = findSuitablePageId(currentWorkspace, targetPageId);
|
||||||
const targetId = findSuitablePageId(currentWorkspace, targetPageId);
|
if (targetId) {
|
||||||
if (targetId) {
|
setCurrentPageId(targetId);
|
||||||
setCurrentPageId(targetId);
|
router.push({
|
||||||
router.push({
|
query: {
|
||||||
query: {
|
...router.query,
|
||||||
...router.query,
|
workspaceId: currentWorkspace.id,
|
||||||
workspaceId: currentWorkspace.id,
|
pageId: targetId,
|
||||||
pageId: targetId,
|
},
|
||||||
},
|
});
|
||||||
});
|
return;
|
||||||
return;
|
} else {
|
||||||
}
|
const dispose =
|
||||||
|
currentWorkspace.blockSuiteWorkspace.slots.pageAdded.on(
|
||||||
|
pageId => {
|
||||||
|
if (pageId === targetPageId) {
|
||||||
|
dispose.dispose();
|
||||||
|
setCurrentPageId(pageId);
|
||||||
|
router.push({
|
||||||
|
query: {
|
||||||
|
...router.query,
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
pageId: targetId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
const clearId = setTimeout(() => {
|
||||||
|
if (jotaiStore.get(currentPageIdAtom) === null) {
|
||||||
|
const id =
|
||||||
|
currentWorkspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id;
|
||||||
|
if (id) {
|
||||||
|
router.push({
|
||||||
|
query: {
|
||||||
|
...router.query,
|
||||||
|
workspaceId: currentWorkspace.id,
|
||||||
|
pageId: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
setCurrentPageId(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dispose.dispose();
|
||||||
|
}, REDIRECT_TIMEOUT);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(clearId);
|
||||||
|
dispose.dispose();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -156,6 +193,5 @@ export function useSyncRouterWithCurrentWorkspaceAndPage(router: NextRouter) {
|
|||||||
setCurrentWorkspaceId,
|
setCurrentWorkspaceId,
|
||||||
workspaces,
|
workspaces,
|
||||||
router,
|
router,
|
||||||
isLoaded,
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,172 +1,25 @@
|
|||||||
import { Workspace } from '@affine/datacenter';
|
|
||||||
import { config, getEnvironment } from '@affine/env';
|
|
||||||
import { nanoid } from '@blocksuite/store';
|
import { nanoid } from '@blocksuite/store';
|
||||||
import { useCallback, useMemo, useSyncExternalStore } from 'react';
|
import { useAtomValue, useSetAtom } from 'jotai';
|
||||||
import useSWR from 'swr';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
import { lockMutex } from '../atoms';
|
import { jotaiWorkspacesAtom, workspacesAtom } from '../atoms';
|
||||||
import { createLocalProviders } from '../blocksuite';
|
|
||||||
import { WorkspacePlugins } from '../plugins';
|
import { WorkspacePlugins } from '../plugins';
|
||||||
import { QueryKey } from '../plugins/affine/fetcher';
|
import { LocalPlugin } from '../plugins/local';
|
||||||
import { kStoreKey } from '../plugins/local';
|
|
||||||
import { LocalWorkspace, RemWorkspace, RemWorkspaceFlavour } from '../shared';
|
import { LocalWorkspace, RemWorkspace, RemWorkspaceFlavour } from '../shared';
|
||||||
import { createEmptyBlockSuiteWorkspace } from '../utils';
|
import { createEmptyBlockSuiteWorkspace } from '../utils';
|
||||||
|
|
||||||
// fixme(himself65): refactor with jotai atom using async
|
|
||||||
export const dataCenter = {
|
|
||||||
workspaces: [] as RemWorkspace[],
|
|
||||||
isLoaded: false,
|
|
||||||
callbacks: new Set<() => void>(),
|
|
||||||
};
|
|
||||||
|
|
||||||
export function vitestRefreshWorkspaces() {
|
|
||||||
dataCenter.workspaces = [];
|
|
||||||
dataCenter.callbacks.clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
// eslint-disable-next-line no-var
|
|
||||||
var dataCenter: {
|
|
||||||
workspaces: RemWorkspace[];
|
|
||||||
isLoaded: boolean;
|
|
||||||
callbacks: Set<() => void>;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
globalThis.dataCenter = dataCenter;
|
|
||||||
|
|
||||||
function createRemLocalWorkspace(name: string) {
|
|
||||||
const id = nanoid();
|
|
||||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
|
||||||
id,
|
|
||||||
(_: string) => undefined
|
|
||||||
);
|
|
||||||
blockSuiteWorkspace.meta.setName(name);
|
|
||||||
const workspace: LocalWorkspace = {
|
|
||||||
flavour: RemWorkspaceFlavour.LOCAL,
|
|
||||||
blockSuiteWorkspace: blockSuiteWorkspace,
|
|
||||||
providers: [...createLocalProviders(blockSuiteWorkspace)],
|
|
||||||
id,
|
|
||||||
};
|
|
||||||
if (config.enableIndexedDBProvider) {
|
|
||||||
let ids: string[];
|
|
||||||
try {
|
|
||||||
ids = JSON.parse(localStorage.getItem(kStoreKey) ?? '[]');
|
|
||||||
if (!Array.isArray(ids)) {
|
|
||||||
localStorage.setItem(kStoreKey, '[]');
|
|
||||||
ids = [];
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
localStorage.setItem(kStoreKey, '[]');
|
|
||||||
ids = [];
|
|
||||||
}
|
|
||||||
ids.push(id);
|
|
||||||
localStorage.setItem(kStoreKey, JSON.stringify(ids));
|
|
||||||
}
|
|
||||||
dataCenter.workspaces = [...dataCenter.workspaces, workspace];
|
|
||||||
dataCenter.callbacks.forEach(cb => cb());
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
|
|
||||||
const emptyWorkspaces: RemWorkspace[] = [];
|
|
||||||
|
|
||||||
export async function refreshDataCenter(signal?: AbortSignal) {
|
|
||||||
dataCenter.isLoaded = false;
|
|
||||||
dataCenter.callbacks.forEach(cb => cb());
|
|
||||||
if (getEnvironment().isServer) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// fixme(himself65): `prefetchWorkspace` is not used
|
|
||||||
// use `config.enablePlugin = ['affine', 'local']` instead
|
|
||||||
// if (!config.prefetchWorkspace) {
|
|
||||||
// console.info('prefetchNecessaryData: skip prefetching');
|
|
||||||
// return;
|
|
||||||
// }
|
|
||||||
const plugins = Object.values(WorkspacePlugins).sort(
|
|
||||||
(a, b) => a.loadPriority - b.loadPriority
|
|
||||||
);
|
|
||||||
// prefetch data in order
|
|
||||||
for (const plugin of plugins) {
|
|
||||||
console.info('prefetchNecessaryData: plugin', plugin.flavour);
|
|
||||||
try {
|
|
||||||
if (signal?.aborted) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
const oldData = dataCenter.workspaces;
|
|
||||||
await plugin.prefetchData(dataCenter, signal);
|
|
||||||
const newData = dataCenter.workspaces;
|
|
||||||
if (!Object.is(oldData, newData)) {
|
|
||||||
console.info('prefetchNecessaryData: data changed');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('error prefetch data', plugin.flavour, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dataCenter.isLoaded = true;
|
|
||||||
dataCenter.callbacks.forEach(cb => cb());
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useWorkspaces(): RemWorkspace[] {
|
export function useWorkspaces(): RemWorkspace[] {
|
||||||
return useSyncExternalStore(
|
return useAtomValue(workspacesAtom);
|
||||||
useCallback(onStoreChange => {
|
|
||||||
dataCenter.callbacks.add(onStoreChange);
|
|
||||||
return () => {
|
|
||||||
dataCenter.callbacks.delete(onStoreChange);
|
|
||||||
};
|
|
||||||
}, []),
|
|
||||||
useCallback(() => dataCenter.workspaces, []),
|
|
||||||
useCallback(() => emptyWorkspaces, [])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useWorkspacesIsLoaded(): boolean {
|
|
||||||
return useSyncExternalStore(
|
|
||||||
useCallback(onStoreChange => {
|
|
||||||
dataCenter.callbacks.add(onStoreChange);
|
|
||||||
return () => {
|
|
||||||
dataCenter.callbacks.delete(onStoreChange);
|
|
||||||
};
|
|
||||||
}, []),
|
|
||||||
useCallback(() => dataCenter.isLoaded, []),
|
|
||||||
useCallback(() => true, [])
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function useSyncWorkspaces() {
|
|
||||||
return useSWR<Workspace[]>(QueryKey.getWorkspaces, {
|
|
||||||
fallbackData: [],
|
|
||||||
revalidateOnReconnect: true,
|
|
||||||
revalidateOnFocus: false,
|
|
||||||
revalidateOnMount: true,
|
|
||||||
revalidateIfStale: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteWorkspace(workspaceId: string) {
|
|
||||||
return lockMutex(async () => {
|
|
||||||
console.warn('deleting workspace');
|
|
||||||
const idx = dataCenter.workspaces.findIndex(
|
|
||||||
workspace => workspace.id === workspaceId
|
|
||||||
);
|
|
||||||
if (idx === -1) {
|
|
||||||
throw new Error('workspace not found');
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const [workspace] = dataCenter.workspaces.splice(idx, 1);
|
|
||||||
// @ts-expect-error
|
|
||||||
await WorkspacePlugins[workspace.flavour].deleteWorkspace(workspace);
|
|
||||||
dataCenter.callbacks.forEach(cb => cb());
|
|
||||||
} catch (e) {
|
|
||||||
console.error('error deleting workspace', e);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useWorkspacesHelper() {
|
export function useWorkspacesHelper() {
|
||||||
return useMemo(
|
const workspaces = useWorkspaces();
|
||||||
() => ({
|
const jotaiWorkspaces = useAtomValue(jotaiWorkspacesAtom);
|
||||||
createWorkspacePage: (workspaceId: string, pageId: string) => {
|
const set = useSetAtom(jotaiWorkspacesAtom);
|
||||||
const workspace = dataCenter.workspaces.find(
|
return {
|
||||||
|
createWorkspacePage: useCallback(
|
||||||
|
(workspaceId: string, pageId: string) => {
|
||||||
|
const workspace = workspaces.find(
|
||||||
ws => ws.id === workspaceId
|
ws => ws.id === workspaceId
|
||||||
) as LocalWorkspace;
|
) as LocalWorkspace;
|
||||||
if (workspace && 'blockSuiteWorkspace' in workspace) {
|
if (workspace && 'blockSuiteWorkspace' in workspace) {
|
||||||
@@ -175,9 +28,46 @@ export function useWorkspacesHelper() {
|
|||||||
throw new Error('cannot create page. blockSuiteWorkspace not found');
|
throw new Error('cannot create page. blockSuiteWorkspace not found');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
createRemLocalWorkspace,
|
[workspaces]
|
||||||
deleteWorkspace,
|
),
|
||||||
}),
|
createLocalWorkspace: useCallback(
|
||||||
[]
|
async (name: string): Promise<string> => {
|
||||||
);
|
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||||
|
nanoid(),
|
||||||
|
_ => undefined
|
||||||
|
);
|
||||||
|
blockSuiteWorkspace.meta.setName(name);
|
||||||
|
const id = await LocalPlugin.CRUD.create(blockSuiteWorkspace);
|
||||||
|
set(workspaces => [
|
||||||
|
...workspaces,
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
flavour: RemWorkspaceFlavour.LOCAL,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
[set]
|
||||||
|
),
|
||||||
|
deleteWorkspace: useCallback(
|
||||||
|
async (workspaceId: string) => {
|
||||||
|
const targetJotaiWorkspace = jotaiWorkspaces.find(
|
||||||
|
ws => ws.id === workspaceId
|
||||||
|
);
|
||||||
|
const targetWorkspace = workspaces.find(ws => ws.id === workspaceId);
|
||||||
|
if (!targetJotaiWorkspace || !targetWorkspace) {
|
||||||
|
throw new Error('page cannot be found');
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete workspace from plugin
|
||||||
|
await WorkspacePlugins[targetWorkspace.flavour].CRUD.delete(
|
||||||
|
// fixme: type casting
|
||||||
|
targetWorkspace as any
|
||||||
|
);
|
||||||
|
// delete workspace from jotai storage
|
||||||
|
set(workspaces => workspaces.filter(ws => ws.id !== workspaceId));
|
||||||
|
},
|
||||||
|
[jotaiWorkspaces, set, workspaces]
|
||||||
|
),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,57 +1,100 @@
|
|||||||
|
import { DebugLogger } from '@affine/debug';
|
||||||
import { setUpLanguage, useTranslation } from '@affine/i18n';
|
import { setUpLanguage, useTranslation } from '@affine/i18n';
|
||||||
import { assertExists, nanoid } from '@blocksuite/store';
|
import { assertExists, nanoid } from '@blocksuite/store';
|
||||||
import { useAtom, useAtomValue } from 'jotai';
|
import { NoSsr } from '@mui/material';
|
||||||
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||||
import { atomWithStorage } from 'jotai/utils';
|
import { atomWithStorage } from 'jotai/utils';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, { useCallback, useEffect } from 'react';
|
import React, { Suspense, useCallback, useEffect } from 'react';
|
||||||
import { Helmet } from 'react-helmet-async';
|
import { Helmet } from 'react-helmet-async';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
currentWorkspaceIdAtom,
|
||||||
|
jotaiWorkspacesAtom,
|
||||||
openQuickSearchModalAtom,
|
openQuickSearchModalAtom,
|
||||||
openWorkspacesModalAtom,
|
openWorkspacesModalAtom,
|
||||||
workspaceLockAtom,
|
workspaceLockAtom,
|
||||||
} from '../atoms';
|
} from '../atoms';
|
||||||
import { HelpIsland } from '../components/pure/help-island';
|
import { HelpIsland } from '../components/pure/help-island';
|
||||||
import { PageLoading } from '../components/pure/loading';
|
import { PageLoading } from '../components/pure/loading';
|
||||||
|
import QuickSearchModal from '../components/pure/quick-search-modal';
|
||||||
import WorkSpaceSliderBar from '../components/pure/workspace-slider-bar';
|
import WorkSpaceSliderBar from '../components/pure/workspace-slider-bar';
|
||||||
import { useCurrentPageId } from '../hooks/current/use-current-page-id';
|
import { useCurrentPageId } from '../hooks/current/use-current-page-id';
|
||||||
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
|
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
|
||||||
import { useBlockSuiteWorkspaceHelper } from '../hooks/use-blocksuite-workspace-helper';
|
import { useBlockSuiteWorkspaceHelper } from '../hooks/use-blocksuite-workspace-helper';
|
||||||
|
import { useCreateFirstWorkspace } from '../hooks/use-create-first-workspace';
|
||||||
import { useRouterTitle } from '../hooks/use-router-title';
|
import { useRouterTitle } from '../hooks/use-router-title';
|
||||||
import {
|
import { useWorkspaces } from '../hooks/use-workspaces';
|
||||||
refreshDataCenter,
|
import { WorkspacePlugins } from '../plugins';
|
||||||
useSyncWorkspaces,
|
import { ModalProvider } from '../providers/ModalProvider';
|
||||||
useWorkspaces,
|
|
||||||
} from '../hooks/use-workspaces';
|
|
||||||
import { pathGenerator, publicPathGenerator } from '../shared';
|
import { pathGenerator, publicPathGenerator } from '../shared';
|
||||||
import { StyledPage, StyledToolWrapper, StyledWrapper } from './styles';
|
import { StyledPage, StyledToolWrapper, StyledWrapper } from './styles';
|
||||||
|
|
||||||
const sideBarOpenAtom = atomWithStorage('sideBarOpen', true);
|
const sideBarOpenAtom = atomWithStorage('sideBarOpen', true);
|
||||||
|
|
||||||
refreshDataCenter();
|
const logger = new DebugLogger('workspace-layout');
|
||||||
|
export const WorkspaceLayout: React.FC<React.PropsWithChildren> =
|
||||||
|
function WorkspacesSuspense({ children }) {
|
||||||
|
const { i18n } = useTranslation();
|
||||||
|
useEffect(() => {
|
||||||
|
document.documentElement.lang = i18n.language;
|
||||||
|
// todo(himself65): this is a hack, we should use a better way to set the language
|
||||||
|
setUpLanguage(i18n);
|
||||||
|
}, [i18n]);
|
||||||
|
useCreateFirstWorkspace();
|
||||||
|
const set = useSetAtom(jotaiWorkspacesAtom);
|
||||||
|
useEffect(() => {
|
||||||
|
logger.info('mount');
|
||||||
|
const controller = new AbortController();
|
||||||
|
const lists = Object.values(WorkspacePlugins)
|
||||||
|
.sort((a, b) => a.loadPriority - b.loadPriority)
|
||||||
|
.map(({ CRUD }) => CRUD.list);
|
||||||
|
async function fetch() {
|
||||||
|
const items = [];
|
||||||
|
for (const list of lists) {
|
||||||
|
try {
|
||||||
|
const item = await list();
|
||||||
|
items.push(...item.map(x => ({ id: x.id, flavour: x.flavour })));
|
||||||
|
} catch (e) {
|
||||||
|
logger.error('list data error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (controller.signal.aborted) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
set([...items]);
|
||||||
|
logger.info('mount first data:', items);
|
||||||
|
}
|
||||||
|
fetch();
|
||||||
|
return () => {
|
||||||
|
controller.abort();
|
||||||
|
logger.info('unmount');
|
||||||
|
};
|
||||||
|
}, [set]);
|
||||||
|
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
|
||||||
|
return (
|
||||||
|
<NoSsr>
|
||||||
|
{/* fixme(himself65): don't re-render whole modals */}
|
||||||
|
<ModalProvider key={currentWorkspaceId} />
|
||||||
|
<Suspense fallback={<PageLoading />}>
|
||||||
|
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
|
||||||
|
</Suspense>
|
||||||
|
</NoSsr>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export const WorkspaceLayout: React.FC<React.PropsWithChildren> = ({
|
export const WorkspaceLayoutInner: React.FC<React.PropsWithChildren> = ({
|
||||||
children,
|
children,
|
||||||
}) => {
|
}) => {
|
||||||
const { i18n } = useTranslation();
|
|
||||||
useEffect(() => {
|
|
||||||
document.documentElement.lang = i18n.language;
|
|
||||||
// todo(himself65): this is a hack, we should use a better way to set the language
|
|
||||||
setUpLanguage(i18n);
|
|
||||||
}, [i18n]);
|
|
||||||
useEffect(() => {
|
|
||||||
const controller = new AbortController();
|
|
||||||
refreshDataCenter(controller.signal);
|
|
||||||
return () => {
|
|
||||||
controller.abort();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [show, setShow] = useAtom(sideBarOpenAtom);
|
const [show, setShow] = useAtom(sideBarOpenAtom);
|
||||||
useSyncWorkspaces();
|
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
const [currentWorkspace] = useCurrentWorkspace();
|
||||||
const [currentPageId] = useCurrentPageId();
|
const [currentPageId] = useCurrentPageId();
|
||||||
const workspaces = useWorkspaces();
|
const workspaces = useWorkspaces();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
console.log(workspaces);
|
||||||
|
}, [workspaces]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const providers = workspaces.flatMap(workspace =>
|
const providers = workspaces.flatMap(workspace =>
|
||||||
workspace.providers.filter(provider => provider.background)
|
workspace.providers.filter(provider => provider.background)
|
||||||
@@ -91,7 +134,6 @@ export const WorkspaceLayout: React.FC<React.PropsWithChildren> = ({
|
|||||||
const isPublicWorkspace =
|
const isPublicWorkspace =
|
||||||
router.pathname.split('/')[1] === 'public-workspace';
|
router.pathname.split('/')[1] === 'public-workspace';
|
||||||
const title = useRouterTitle(router);
|
const title = useRouterTitle(router);
|
||||||
const [, setOpenQuickSearchModalAtom] = useAtom(openQuickSearchModalAtom);
|
|
||||||
const handleOpenPage = useCallback(
|
const handleOpenPage = useCallback(
|
||||||
(pageId: string) => {
|
(pageId: string) => {
|
||||||
assertExists(currentWorkspace);
|
assertExists(currentWorkspace);
|
||||||
@@ -113,6 +155,10 @@ export const WorkspaceLayout: React.FC<React.PropsWithChildren> = ({
|
|||||||
const handleOpenWorkspaceListModal = useCallback(() => {
|
const handleOpenWorkspaceListModal = useCallback(() => {
|
||||||
setOpenWorkspacesModal(true);
|
setOpenWorkspacesModal(true);
|
||||||
}, [setOpenWorkspacesModal]);
|
}, [setOpenWorkspacesModal]);
|
||||||
|
|
||||||
|
const [openQuickSearchModal, setOpenQuickSearchModalAtom] = useAtom(
|
||||||
|
openQuickSearchModalAtom
|
||||||
|
);
|
||||||
const handleOpenQuickSearchModal = useCallback(() => {
|
const handleOpenQuickSearchModal = useCallback(() => {
|
||||||
setOpenQuickSearchModalAtom(true);
|
setOpenQuickSearchModalAtom(true);
|
||||||
}, [setOpenQuickSearchModalAtom]);
|
}, [setOpenQuickSearchModalAtom]);
|
||||||
@@ -153,6 +199,14 @@ export const WorkspaceLayout: React.FC<React.PropsWithChildren> = ({
|
|||||||
</StyledToolWrapper>
|
</StyledToolWrapper>
|
||||||
</StyledWrapper>
|
</StyledWrapper>
|
||||||
</StyledPage>
|
</StyledPage>
|
||||||
|
{currentWorkspace?.blockSuiteWorkspace && (
|
||||||
|
<QuickSearchModal
|
||||||
|
blockSuiteWorkspace={currentWorkspace?.blockSuiteWorkspace}
|
||||||
|
open={openQuickSearchModal}
|
||||||
|
setOpen={setOpenQuickSearchModalAtom}
|
||||||
|
router={router}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { AffineErrorBoundary } from '../components/affine/affine-error-eoundary'
|
|||||||
import { ProviderComposer } from '../components/provider-composer';
|
import { ProviderComposer } from '../components/provider-composer';
|
||||||
import { PageLoading } from '../components/pure/loading';
|
import { PageLoading } from '../components/pure/loading';
|
||||||
import { AffineSWRConfigProvider } from '../providers/AffineSWRConfigProvider';
|
import { AffineSWRConfigProvider } from '../providers/AffineSWRConfigProvider';
|
||||||
import { ModalProvider } from '../providers/ModalProvider';
|
|
||||||
import { ThemeProvider } from '../providers/ThemeProvider';
|
import { ThemeProvider } from '../providers/ThemeProvider';
|
||||||
import { NextPageWithLayout } from '../shared';
|
import { NextPageWithLayout } from '../shared';
|
||||||
import createEmotionCache from '../utils/create-emotion-cache';
|
import createEmotionCache from '../utils/create-emotion-cache';
|
||||||
@@ -81,7 +80,6 @@ const App = function App({
|
|||||||
<AffineSWRConfigProvider key="AffineSWRConfigProvider" />,
|
<AffineSWRConfigProvider key="AffineSWRConfigProvider" />,
|
||||||
<Provider key="JotaiProvider" store={jotaiStore} />,
|
<Provider key="JotaiProvider" store={jotaiStore} />,
|
||||||
<ThemeProvider key="ThemeProvider" />,
|
<ThemeProvider key="ThemeProvider" />,
|
||||||
<ModalProvider key="ModalProvider" />,
|
|
||||||
],
|
],
|
||||||
[]
|
[]
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,50 +1,18 @@
|
|||||||
import { useAtom } from 'jotai';
|
|
||||||
import { NextPage } from 'next';
|
import { NextPage } from 'next';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, { useEffect } from 'react';
|
import React, { Suspense, useEffect } from 'react';
|
||||||
|
|
||||||
import { currentWorkspaceIdAtom } from '../atoms';
|
|
||||||
import { PageLoading } from '../components/pure/loading';
|
import { PageLoading } from '../components/pure/loading';
|
||||||
import { refreshDataCenter, useWorkspaces } from '../hooks/use-workspaces';
|
import { useCreateFirstWorkspace } from '../hooks/use-create-first-workspace';
|
||||||
|
import { useWorkspaces } from '../hooks/use-workspaces';
|
||||||
|
|
||||||
const IndexPage: NextPage = () => {
|
const IndexPageInner = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
useEffect(() => {
|
|
||||||
const controller = new AbortController();
|
|
||||||
refreshDataCenter(controller.signal);
|
|
||||||
return () => {
|
|
||||||
controller.abort();
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
const [workspaceId] = useAtom(currentWorkspaceIdAtom);
|
|
||||||
const workspaces = useWorkspaces();
|
const workspaces = useWorkspaces();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!router.isReady) {
|
if (!router.isReady) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const targetWorkspace = workspaces.find(w => w.id === workspaceId);
|
|
||||||
if (workspaceId && targetWorkspace) {
|
|
||||||
const pageId =
|
|
||||||
targetWorkspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id;
|
|
||||||
if (pageId) {
|
|
||||||
router.replace({
|
|
||||||
pathname: '/workspace/[workspaceId]/[pageId]',
|
|
||||||
query: {
|
|
||||||
workspaceId,
|
|
||||||
pageId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
router.replace({
|
|
||||||
pathname: '/workspace/[workspaceId]/all',
|
|
||||||
query: {
|
|
||||||
workspaceId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const firstWorkspace = workspaces.at(0);
|
const firstWorkspace = workspaces.at(0);
|
||||||
if (firstWorkspace) {
|
if (firstWorkspace) {
|
||||||
const pageId =
|
const pageId =
|
||||||
@@ -59,17 +27,44 @@ const IndexPage: NextPage = () => {
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
router.replace({
|
const clearId = setTimeout(() => {
|
||||||
pathname: '/workspace/[workspaceId]/all',
|
dispose.dispose();
|
||||||
query: {
|
router.replace({
|
||||||
workspaceId: firstWorkspace.id,
|
pathname: '/workspace/[workspaceId]/all',
|
||||||
},
|
query: {
|
||||||
});
|
workspaceId: firstWorkspace.id,
|
||||||
return;
|
},
|
||||||
|
});
|
||||||
|
}, 1000);
|
||||||
|
const dispose = firstWorkspace.blockSuiteWorkspace.slots.pageAdded.once(
|
||||||
|
pageId => {
|
||||||
|
clearTimeout(clearId);
|
||||||
|
router.replace({
|
||||||
|
pathname: '/workspace/[workspaceId]/[pageId]',
|
||||||
|
query: {
|
||||||
|
workspaceId: firstWorkspace.id,
|
||||||
|
pageId,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
return () => {
|
||||||
|
clearTimeout(clearId);
|
||||||
|
dispose.dispose();
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [router, workspaceId, workspaces]);
|
}, [router, workspaces]);
|
||||||
return <PageLoading />;
|
return <PageLoading />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const IndexPage: NextPage = () => {
|
||||||
|
useCreateFirstWorkspace();
|
||||||
|
return (
|
||||||
|
<Suspense fallback={<PageLoading />}>
|
||||||
|
<IndexPageInner />
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default IndexPage;
|
export default IndexPage;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, { useEffect, useState } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
|
|
||||||
import { Unreachable } from '../../../components/affine/affine-error-eoundary';
|
import { Unreachable } from '../../../components/affine/affine-error-eoundary';
|
||||||
import { PageLoading } from '../../../components/pure/loading';
|
import { PageLoading } from '../../../components/pure/loading';
|
||||||
@@ -27,20 +27,6 @@ function enableFullFlags(blockSuiteWorkspace: BlockSuiteWorkspace) {
|
|||||||
const WorkspaceDetail: React.FC = () => {
|
const WorkspaceDetail: React.FC = () => {
|
||||||
const [pageId] = useCurrentPageId();
|
const [pageId] = useCurrentPageId();
|
||||||
const [currentWorkspace] = useCurrentWorkspace();
|
const [currentWorkspace] = useCurrentWorkspace();
|
||||||
const [, rerender] = useState(false);
|
|
||||||
// fixme(himself65): this is a hack
|
|
||||||
useEffect(() => {
|
|
||||||
const dispose = currentWorkspace?.blockSuiteWorkspace.slots.pageAdded.on(
|
|
||||||
id => {
|
|
||||||
if (pageId === id) {
|
|
||||||
rerender(prev => !prev);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return () => {
|
|
||||||
dispose?.dispose();
|
|
||||||
};
|
|
||||||
}, [currentWorkspace?.blockSuiteWorkspace.slots.pageAdded, pageId]);
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (currentWorkspace) {
|
if (currentWorkspace) {
|
||||||
enableFullFlags(currentWorkspace.blockSuiteWorkspace);
|
enableFullFlags(currentWorkspace.blockSuiteWorkspace);
|
||||||
@@ -52,9 +38,6 @@ const WorkspaceDetail: React.FC = () => {
|
|||||||
if (!pageId) {
|
if (!pageId) {
|
||||||
return <PageLoading />;
|
return <PageLoading />;
|
||||||
}
|
}
|
||||||
if (!currentWorkspace.blockSuiteWorkspace.getPage(pageId)) {
|
|
||||||
return <PageLoading />;
|
|
||||||
}
|
|
||||||
if (currentWorkspace.flavour === RemWorkspaceFlavour.AFFINE) {
|
if (currentWorkspace.flavour === RemWorkspaceFlavour.AFFINE) {
|
||||||
const PageDetail = WorkspacePlugins[currentWorkspace.flavour].PageDetail;
|
const PageDetail = WorkspacePlugins[currentWorkspace.flavour].PageDetail;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { assertEquals } from '@blocksuite/store';
|
import { createJSONStorage } from 'jotai/utils';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { preload } from 'swr';
|
import { preload } from 'swr';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
@@ -19,6 +19,7 @@ import { createEmptyBlockSuiteWorkspace } from '../../utils';
|
|||||||
import { WorkspacePlugin } from '..';
|
import { WorkspacePlugin } from '..';
|
||||||
import { fetcher, QueryKey } from './fetcher';
|
import { fetcher, QueryKey } from './fetcher';
|
||||||
|
|
||||||
|
const storage = createJSONStorage(() => localStorage);
|
||||||
const kAffineLocal = 'affine-local-storage-v2';
|
const kAffineLocal = 'affine-local-storage-v2';
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
id: z.string(),
|
id: z.string(),
|
||||||
@@ -31,33 +32,48 @@ const schema = z.object({
|
|||||||
export const AffinePlugin: WorkspacePlugin<RemWorkspaceFlavour.AFFINE> = {
|
export const AffinePlugin: WorkspacePlugin<RemWorkspaceFlavour.AFFINE> = {
|
||||||
flavour: RemWorkspaceFlavour.AFFINE,
|
flavour: RemWorkspaceFlavour.AFFINE,
|
||||||
loadPriority: LoadPriority.HIGH,
|
loadPriority: LoadPriority.HIGH,
|
||||||
createWorkspace: async (blockSuiteWorkspace: BlockSuiteWorkspace) => {
|
CRUD: {
|
||||||
const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdate(
|
create: async blockSuiteWorkspace => {
|
||||||
blockSuiteWorkspace.doc
|
const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdate(
|
||||||
);
|
blockSuiteWorkspace.doc
|
||||||
const { id } = await apis.createWorkspace(new Blob([binary.buffer]));
|
);
|
||||||
return id;
|
const { id } = await apis.createWorkspace(new Blob([binary.buffer]));
|
||||||
},
|
return id;
|
||||||
deleteWorkspace: async workspace => {
|
},
|
||||||
await apis.deleteWorkspace({
|
delete: async workspace => {
|
||||||
id: workspace.id,
|
await apis.deleteWorkspace({
|
||||||
});
|
id: workspace.id,
|
||||||
workspace.providers.forEach(p => p.cleanup());
|
});
|
||||||
},
|
},
|
||||||
prefetchData: async dataCenter => {
|
get: async workspaceId => {
|
||||||
if (localStorage.getItem(kAffineLocal)) {
|
const workspaces: AffineWorkspace[] = await preload(
|
||||||
const localData = JSON.parse(localStorage.getItem(kAffineLocal) || '[]');
|
QueryKey.getWorkspaces,
|
||||||
if (Array.isArray(localData)) {
|
fetcher
|
||||||
const workspacesDump = localData
|
);
|
||||||
.map((item: any) => {
|
|
||||||
const result = schema.safeParse(item);
|
const workspace = workspaces.find(
|
||||||
if (result.success) {
|
workspace => workspace.id === workspaceId
|
||||||
return result.data;
|
);
|
||||||
}
|
const dump = workspaces.map(workspace => {
|
||||||
return null;
|
return {
|
||||||
})
|
id: workspace.id,
|
||||||
.filter(Boolean) as z.infer<typeof schema>[];
|
type: workspace.type,
|
||||||
const workspaces = workspacesDump.map(workspace => {
|
public: workspace.public,
|
||||||
|
permission: workspace.permission,
|
||||||
|
create_at: workspace.create_at,
|
||||||
|
} satisfies z.infer<typeof schema>;
|
||||||
|
});
|
||||||
|
storage.setItem(kAffineLocal, dump);
|
||||||
|
if (!workspace) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return workspace;
|
||||||
|
},
|
||||||
|
list: async () => {
|
||||||
|
// fixme: refactor auth check
|
||||||
|
if (!apis.auth.isLogin) return [];
|
||||||
|
return await apis.getWorkspaces().then(workspaces => {
|
||||||
|
return workspaces.map(workspace => {
|
||||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||||
workspace.id,
|
workspace.id,
|
||||||
(k: string) =>
|
(k: string) =>
|
||||||
@@ -66,75 +82,14 @@ export const AffinePlugin: WorkspacePlugin<RemWorkspaceFlavour.AFFINE> = {
|
|||||||
);
|
);
|
||||||
const affineWorkspace: AffineWorkspace = {
|
const affineWorkspace: AffineWorkspace = {
|
||||||
...workspace,
|
...workspace,
|
||||||
|
flavour: RemWorkspaceFlavour.AFFINE,
|
||||||
blockSuiteWorkspace,
|
blockSuiteWorkspace,
|
||||||
providers: [...createAffineProviders(blockSuiteWorkspace)],
|
providers: [...createAffineProviders(blockSuiteWorkspace)],
|
||||||
flavour: RemWorkspaceFlavour.AFFINE,
|
|
||||||
};
|
};
|
||||||
return affineWorkspace;
|
return affineWorkspace;
|
||||||
});
|
});
|
||||||
|
|
||||||
// fixme: refactor to a function
|
|
||||||
workspaces.forEach(workspace => {
|
|
||||||
const exist = dataCenter.workspaces.findIndex(
|
|
||||||
ws => ws.id === workspace.id
|
|
||||||
);
|
|
||||||
if (exist !== -1) {
|
|
||||||
dataCenter.workspaces.splice(exist, 1, workspace);
|
|
||||||
dataCenter.workspaces = [...dataCenter.workspaces];
|
|
||||||
} else {
|
|
||||||
dataCenter.workspaces = [...dataCenter.workspaces, workspace];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
dataCenter.callbacks.forEach(cb => cb());
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem(kAffineLocal);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const promise: Promise<AffineWorkspace[]> = preload(
|
|
||||||
QueryKey.getWorkspaces,
|
|
||||||
fetcher
|
|
||||||
);
|
|
||||||
return promise
|
|
||||||
.then(async workspaces => {
|
|
||||||
const promises = workspaces.map(workspace => {
|
|
||||||
assertEquals(workspace.flavour, RemWorkspaceFlavour.AFFINE);
|
|
||||||
return workspace;
|
|
||||||
});
|
|
||||||
return Promise.all(promises)
|
|
||||||
.then(workspaces => {
|
|
||||||
workspaces.forEach(workspace => {
|
|
||||||
if (workspace === null) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const exist = dataCenter.workspaces.findIndex(
|
|
||||||
ws => ws.id === workspace.id
|
|
||||||
);
|
|
||||||
if (exist !== -1) {
|
|
||||||
dataCenter.workspaces.splice(exist, 1, workspace);
|
|
||||||
dataCenter.workspaces = [...dataCenter.workspaces];
|
|
||||||
} else {
|
|
||||||
dataCenter.workspaces = [...dataCenter.workspaces, workspace];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return workspaces;
|
|
||||||
})
|
|
||||||
.then(ws => {
|
|
||||||
const workspaces = ws.filter(Boolean) as AffineWorkspace[];
|
|
||||||
const dump = workspaces.map(workspace => {
|
|
||||||
return {
|
|
||||||
id: workspace.id,
|
|
||||||
type: workspace.type,
|
|
||||||
public: workspace.public,
|
|
||||||
permission: workspace.permission,
|
|
||||||
create_at: workspace.create_at,
|
|
||||||
} satisfies z.infer<typeof schema>;
|
|
||||||
});
|
|
||||||
localStorage.setItem(kAffineLocal, JSON.stringify(dump));
|
|
||||||
});
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
});
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
PageDetail: ({ currentWorkspace, currentPageId }) => {
|
PageDetail: ({ currentWorkspace, currentPageId }) => {
|
||||||
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
|
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
import { assertExists } from '@blocksuite/store';
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
import { refreshDataCenter } from '../hooks/use-workspaces';
|
import { jotaiStore, jotaiWorkspacesAtom } from '../atoms';
|
||||||
import {
|
import {
|
||||||
BlockSuiteWorkspace,
|
BlockSuiteWorkspace,
|
||||||
FlavourToWorkspace,
|
FlavourToWorkspace,
|
||||||
LoadPriority,
|
LoadPriority,
|
||||||
RemWorkspace,
|
|
||||||
RemWorkspaceFlavour,
|
RemWorkspaceFlavour,
|
||||||
SettingPanel,
|
SettingPanel,
|
||||||
} from '../shared';
|
} from '../shared';
|
||||||
@@ -45,19 +43,14 @@ export interface WorkspacePlugin<Flavour extends RemWorkspaceFlavour> {
|
|||||||
// Plugin will be loaded according to the priority
|
// Plugin will be loaded according to the priority
|
||||||
loadPriority: LoadPriority;
|
loadPriority: LoadPriority;
|
||||||
// Fetch necessary data for the first render
|
// Fetch necessary data for the first render
|
||||||
prefetchData: (
|
CRUD: {
|
||||||
dataCenter: {
|
create: (blockSuiteWorkspace: BlockSuiteWorkspace) => Promise<string>;
|
||||||
workspaces: RemWorkspace[];
|
delete: (workspace: FlavourToWorkspace[Flavour]) => Promise<void>;
|
||||||
callbacks: Set<() => void>;
|
get: (workspaceId: string) => Promise<FlavourToWorkspace[Flavour] | null>;
|
||||||
},
|
// not supported yet
|
||||||
signal?: AbortSignal
|
// update: (workspace: FlavourToWorkspace[Flavour]) => Promise<void>;
|
||||||
) => Promise<void>;
|
list: () => Promise<FlavourToWorkspace[Flavour][]>;
|
||||||
|
};
|
||||||
createWorkspace: (
|
|
||||||
blockSuiteWorkspace: BlockSuiteWorkspace
|
|
||||||
) => Promise<string>;
|
|
||||||
|
|
||||||
deleteWorkspace: (workspace: FlavourToWorkspace[Flavour]) => Promise<void>;
|
|
||||||
|
|
||||||
//#region UI
|
//#region UI
|
||||||
PageDetail: React.FC<PageDetailProps<Flavour>>;
|
PageDetail: React.FC<PageDetailProps<Flavour>>;
|
||||||
@@ -73,18 +66,26 @@ export const WorkspacePlugins = {
|
|||||||
[Key in RemWorkspaceFlavour]: WorkspacePlugin<Key>;
|
[Key in RemWorkspaceFlavour]: WorkspacePlugin<Key>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transform workspace from one flavour to another
|
||||||
|
*
|
||||||
|
* The logic here is to delete the old workspace and create a new one.
|
||||||
|
*/
|
||||||
export async function transformWorkspace<
|
export async function transformWorkspace<
|
||||||
From extends RemWorkspaceFlavour,
|
From extends RemWorkspaceFlavour,
|
||||||
To extends RemWorkspaceFlavour
|
To extends RemWorkspaceFlavour
|
||||||
>(from: From, to: To, workspace: FlavourToWorkspace[From]): Promise<string> {
|
>(from: From, to: To, workspace: FlavourToWorkspace[From]): Promise<string> {
|
||||||
// fixme: type cast
|
// fixme: type cast
|
||||||
await WorkspacePlugins[from].deleteWorkspace(workspace as any);
|
await WorkspacePlugins[from].CRUD.delete(workspace as any);
|
||||||
const newId = await WorkspacePlugins[to].createWorkspace(
|
const newId = await WorkspacePlugins[to].CRUD.create(
|
||||||
workspace.blockSuiteWorkspace
|
workspace.blockSuiteWorkspace
|
||||||
);
|
);
|
||||||
// refresh the data center
|
const workspaces = jotaiStore.get(jotaiWorkspacesAtom);
|
||||||
dataCenter.workspaces = [];
|
const idx = workspaces.findIndex(ws => ws.id === workspace.id);
|
||||||
await refreshDataCenter();
|
workspaces.splice(idx, 1, {
|
||||||
assertExists(dataCenter.workspaces.some(w => w.id === newId));
|
id: newId,
|
||||||
|
flavour: to,
|
||||||
|
});
|
||||||
|
jotaiStore.set(jotaiWorkspacesAtom, [...workspaces]);
|
||||||
return newId;
|
return newId;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { DebugLogger } from '@affine/debug';
|
import { DEFAULT_WORKSPACE_NAME } from '@affine/env';
|
||||||
import { config, DEFAULT_WORKSPACE_NAME } from '@affine/env';
|
import { nanoid } from '@blocksuite/store';
|
||||||
import { assertEquals, nanoid } from '@blocksuite/store';
|
import { createJSONStorage } from 'jotai/utils';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { IndexeddbPersistence } from 'y-indexeddb';
|
import { IndexeddbPersistence } from 'y-indexeddb';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { createLocalProviders } from '../../blocksuite';
|
import { createLocalProviders } from '../../blocksuite';
|
||||||
import { PageNotFoundError } from '../../components/affine/affine-error-eoundary';
|
import { PageNotFoundError } from '../../components/affine/affine-error-eoundary';
|
||||||
@@ -18,139 +19,90 @@ import {
|
|||||||
import { createEmptyBlockSuiteWorkspace } from '../../utils';
|
import { createEmptyBlockSuiteWorkspace } from '../../utils';
|
||||||
import { WorkspacePlugin } from '..';
|
import { WorkspacePlugin } from '..';
|
||||||
|
|
||||||
const logger = new DebugLogger('local-plugin');
|
const getStorage = () => createJSONStorage(() => localStorage);
|
||||||
|
|
||||||
export const kStoreKey = 'affine-local-workspace';
|
export const kStoreKey = 'affine-local-workspace';
|
||||||
// fixme(himself65): this is a hacking that first workspace will disappear somehow
|
const schema = z.array(z.string());
|
||||||
const hashMap = new Map<string, BlockSuiteWorkspace>();
|
|
||||||
|
|
||||||
export const LocalPlugin: WorkspacePlugin<RemWorkspaceFlavour.LOCAL> = {
|
export const LocalPlugin: WorkspacePlugin<RemWorkspaceFlavour.LOCAL> = {
|
||||||
flavour: RemWorkspaceFlavour.LOCAL,
|
flavour: RemWorkspaceFlavour.LOCAL,
|
||||||
loadPriority: LoadPriority.LOW,
|
loadPriority: LoadPriority.LOW,
|
||||||
createWorkspace: async blockSuiteWorkspace => {
|
CRUD: {
|
||||||
let ids: string[] = [];
|
get: async workspaceId => {
|
||||||
try {
|
const storage = getStorage();
|
||||||
ids = JSON.parse(localStorage.getItem(kStoreKey) ?? '[]');
|
!Array.isArray(storage.getItem(kStoreKey)) &&
|
||||||
if (!Array.isArray(ids)) {
|
storage.setItem(kStoreKey, []);
|
||||||
localStorage.setItem(kStoreKey, '[]');
|
const data = storage.getItem(kStoreKey) as z.infer<typeof schema>;
|
||||||
ids = [];
|
const id = data.find(id => id === workspaceId);
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
} catch (e) {
|
|
||||||
localStorage.setItem(kStoreKey, '[]');
|
|
||||||
ids = [];
|
|
||||||
}
|
|
||||||
const id = nanoid();
|
|
||||||
const persistence = new IndexeddbPersistence(id, blockSuiteWorkspace.doc);
|
|
||||||
await persistence.whenSynced.then(() => {
|
|
||||||
persistence.destroy();
|
|
||||||
});
|
|
||||||
ids.push(id);
|
|
||||||
localStorage.setItem(kStoreKey, JSON.stringify(ids));
|
|
||||||
return id;
|
|
||||||
},
|
|
||||||
deleteWorkspace: async workspace => {
|
|
||||||
const id = workspace.id;
|
|
||||||
let ids: string[];
|
|
||||||
try {
|
|
||||||
ids = JSON.parse(localStorage.getItem(kStoreKey) ?? '[]');
|
|
||||||
if (!Array.isArray(ids)) {
|
|
||||||
localStorage.setItem(kStoreKey, '[]');
|
|
||||||
ids = [];
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
localStorage.setItem(kStoreKey, '[]');
|
|
||||||
ids = [];
|
|
||||||
}
|
|
||||||
const idx = ids.findIndex(x => x === id);
|
|
||||||
if (idx === -1) {
|
|
||||||
throw new Error('cannot find local workspace from localStorage');
|
|
||||||
}
|
|
||||||
workspace.providers.forEach(p => p.cleanup());
|
|
||||||
ids.splice(idx, 1);
|
|
||||||
assertEquals(
|
|
||||||
ids.every(id => typeof id === 'string'),
|
|
||||||
true
|
|
||||||
);
|
|
||||||
localStorage.setItem(kStoreKey, JSON.stringify(ids));
|
|
||||||
},
|
|
||||||
prefetchData: async (dataCenter, signal) => {
|
|
||||||
if (typeof window === 'undefined') {
|
|
||||||
// SSR mode, no local data
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (signal?.aborted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let ids: string[];
|
|
||||||
try {
|
|
||||||
ids = JSON.parse(localStorage.getItem(kStoreKey) ?? '[]');
|
|
||||||
if (!Array.isArray(ids)) {
|
|
||||||
localStorage.setItem(kStoreKey, '[]');
|
|
||||||
ids = [];
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
localStorage.setItem(kStoreKey, '[]');
|
|
||||||
ids = [];
|
|
||||||
}
|
|
||||||
if (config.enableIndexedDBProvider) {
|
|
||||||
const workspaces = await Promise.all(
|
|
||||||
ids.map(id => {
|
|
||||||
const blockSuiteWorkspace = hashMap.has(id)
|
|
||||||
? (hashMap.get(id) as BlockSuiteWorkspace)
|
|
||||||
: createEmptyBlockSuiteWorkspace(id, (_: string) => undefined);
|
|
||||||
hashMap.set(id, blockSuiteWorkspace);
|
|
||||||
const workspace: LocalWorkspace = {
|
|
||||||
id,
|
|
||||||
flavour: RemWorkspaceFlavour.LOCAL,
|
|
||||||
blockSuiteWorkspace: blockSuiteWorkspace,
|
|
||||||
providers: [...createLocalProviders(blockSuiteWorkspace)],
|
|
||||||
};
|
|
||||||
return workspace;
|
|
||||||
})
|
|
||||||
);
|
|
||||||
workspaces.forEach(workspace => {
|
|
||||||
if (workspace) {
|
|
||||||
const exist = dataCenter.workspaces.findIndex(
|
|
||||||
w => w.id === workspace.id
|
|
||||||
);
|
|
||||||
if (exist === -1) {
|
|
||||||
dataCenter.workspaces = [...dataCenter.workspaces, workspace];
|
|
||||||
} else {
|
|
||||||
dataCenter.workspaces[exist] = workspace;
|
|
||||||
dataCenter.workspaces = [...dataCenter.workspaces];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
if (dataCenter.workspaces.length === 0) {
|
|
||||||
if (signal?.aborted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
logger.info('no local workspace found, create a new one');
|
|
||||||
const workspaceId = nanoid();
|
|
||||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||||
workspaceId,
|
id,
|
||||||
(_: string) => undefined
|
(_: string) => undefined
|
||||||
);
|
);
|
||||||
hashMap.set(workspaceId, blockSuiteWorkspace);
|
|
||||||
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
|
|
||||||
localStorage.setItem(kStoreKey, JSON.stringify([workspaceId]));
|
|
||||||
blockSuiteWorkspace.createPage(nanoid());
|
|
||||||
const workspace: LocalWorkspace = {
|
const workspace: LocalWorkspace = {
|
||||||
id: workspaceId,
|
id,
|
||||||
flavour: RemWorkspaceFlavour.LOCAL,
|
flavour: RemWorkspaceFlavour.LOCAL,
|
||||||
blockSuiteWorkspace: blockSuiteWorkspace,
|
blockSuiteWorkspace: blockSuiteWorkspace,
|
||||||
providers: [...createLocalProviders(blockSuiteWorkspace)],
|
providers: [...createLocalProviders(blockSuiteWorkspace)],
|
||||||
};
|
};
|
||||||
const persistence = new IndexeddbPersistence(
|
return workspace;
|
||||||
blockSuiteWorkspace.room as string,
|
},
|
||||||
blockSuiteWorkspace.doc
|
create: async ({ doc }) => {
|
||||||
|
const storage = getStorage();
|
||||||
|
!Array.isArray(storage.getItem(kStoreKey)) &&
|
||||||
|
storage.setItem(kStoreKey, []);
|
||||||
|
const data = storage.getItem(kStoreKey) as z.infer<typeof schema>;
|
||||||
|
const binary = BlockSuiteWorkspace.Y.encodeStateAsUpdateV2(doc);
|
||||||
|
const id = nanoid();
|
||||||
|
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||||
|
id,
|
||||||
|
(_: string) => undefined
|
||||||
);
|
);
|
||||||
|
BlockSuiteWorkspace.Y.applyUpdateV2(blockSuiteWorkspace.doc, binary);
|
||||||
|
const persistence = new IndexeddbPersistence(id, blockSuiteWorkspace.doc);
|
||||||
await persistence.whenSynced.then(() => {
|
await persistence.whenSynced.then(() => {
|
||||||
persistence.destroy();
|
persistence.destroy();
|
||||||
});
|
});
|
||||||
dataCenter.workspaces = [workspace];
|
storage.setItem(kStoreKey, [...data, id]);
|
||||||
}
|
console.log('create', id, storage.getItem(kStoreKey));
|
||||||
|
return id;
|
||||||
|
},
|
||||||
|
delete: async workspace => {
|
||||||
|
const storage = getStorage();
|
||||||
|
!Array.isArray(storage.getItem(kStoreKey)) &&
|
||||||
|
storage.setItem(kStoreKey, []);
|
||||||
|
const data = storage.getItem(kStoreKey) as z.infer<typeof schema>;
|
||||||
|
const idx = data.findIndex(id => id === workspace.id);
|
||||||
|
if (idx === -1) {
|
||||||
|
throw new Error('workspace not found');
|
||||||
|
}
|
||||||
|
data.splice(idx, 1);
|
||||||
|
storage.setItem(kStoreKey, [...data]);
|
||||||
|
},
|
||||||
|
list: async () => {
|
||||||
|
const storage = getStorage();
|
||||||
|
!Array.isArray(storage.getItem(kStoreKey)) &&
|
||||||
|
storage.setItem(kStoreKey, []);
|
||||||
|
const data = (
|
||||||
|
await Promise.all(
|
||||||
|
(storage.getItem(kStoreKey) as z.infer<typeof schema>).map(id =>
|
||||||
|
LocalPlugin.CRUD.get(id)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
).filter(item => item !== null) as LocalWorkspace[];
|
||||||
|
if (data.length === 0) {
|
||||||
|
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||||
|
nanoid(),
|
||||||
|
(_: string) => undefined
|
||||||
|
);
|
||||||
|
blockSuiteWorkspace.meta.setName(DEFAULT_WORKSPACE_NAME);
|
||||||
|
await LocalPlugin.CRUD.create(blockSuiteWorkspace);
|
||||||
|
return LocalPlugin.CRUD.list();
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
PageDetail: ({ currentWorkspace, currentPageId }) => {
|
PageDetail: ({ currentWorkspace, currentPageId }) => {
|
||||||
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
|
const page = currentWorkspace.blockSuiteWorkspace.getPage(currentPageId);
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import { useAtom } from 'jotai';
|
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import React, { useCallback } from 'react';
|
import React, { useCallback } from 'react';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
currentWorkspaceIdAtom,
|
||||||
openCreateWorkspaceModalAtom,
|
openCreateWorkspaceModalAtom,
|
||||||
openQuickSearchModalAtom,
|
|
||||||
openWorkspacesModalAtom,
|
openWorkspacesModalAtom,
|
||||||
} from '../atoms';
|
} from '../atoms';
|
||||||
import { CreateWorkspaceModal } from '../components/pure/create-workspace-modal';
|
import { CreateWorkspaceModal } from '../components/pure/create-workspace-modal';
|
||||||
import QuickSearchModal from '../components/pure/quick-search-modal';
|
|
||||||
import { WorkspaceListModal } from '../components/pure/workspace-list-modal';
|
import { WorkspaceListModal } from '../components/pure/workspace-list-modal';
|
||||||
import { useCurrentUser } from '../hooks/current/use-current-user';
|
import { useCurrentUser } from '../hooks/current/use-current-user';
|
||||||
import { useCurrentWorkspace } from '../hooks/current/use-current-workspace';
|
|
||||||
import { useWorkspaces, useWorkspacesHelper } from '../hooks/use-workspaces';
|
import { useWorkspaces, useWorkspacesHelper } from '../hooks/use-workspaces';
|
||||||
import { apis } from '../shared/apis';
|
import { apis } from '../shared/apis';
|
||||||
|
|
||||||
@@ -22,28 +20,27 @@ export function Modals() {
|
|||||||
const [openCreateWorkspaceModal, setOpenCreateWorkspaceModal] = useAtom(
|
const [openCreateWorkspaceModal, setOpenCreateWorkspaceModal] = useAtom(
|
||||||
openCreateWorkspaceModalAtom
|
openCreateWorkspaceModalAtom
|
||||||
);
|
);
|
||||||
const [openQuickSearchModal, setOpenQuickSearchModalAtom] = useAtom(
|
|
||||||
openQuickSearchModalAtom
|
|
||||||
);
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const user = useCurrentUser();
|
const user = useCurrentUser();
|
||||||
const workspaces = useWorkspaces();
|
const workspaces = useWorkspaces();
|
||||||
const [currentWorkspace, setCurrentWorkspace] = useCurrentWorkspace();
|
const currentWorkspaceId = useAtomValue(currentWorkspaceIdAtom);
|
||||||
const { createRemLocalWorkspace } = useWorkspacesHelper();
|
const setCurrentWorkspace = useSetAtom(currentWorkspaceIdAtom);
|
||||||
|
const { createLocalWorkspace } = useWorkspacesHelper();
|
||||||
|
|
||||||
const disableShortCut = router.pathname.startsWith('/404');
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<WorkspaceListModal
|
<WorkspaceListModal
|
||||||
user={user}
|
user={user}
|
||||||
workspaces={workspaces}
|
workspaces={workspaces}
|
||||||
currentWorkspaceId={currentWorkspace?.id ?? null}
|
currentWorkspaceId={currentWorkspaceId}
|
||||||
open={openWorkspacesModal}
|
open={openWorkspacesModal}
|
||||||
onClose={useCallback(() => {
|
onClose={useCallback(() => {
|
||||||
setOpenWorkspacesModal(false);
|
setOpenWorkspacesModal(false);
|
||||||
}, [setOpenWorkspacesModal])}
|
}, [setOpenWorkspacesModal])}
|
||||||
onClickWorkspace={useCallback(
|
onClickWorkspace={useCallback(
|
||||||
workspace => {
|
workspace => {
|
||||||
|
setOpenWorkspacesModal(false);
|
||||||
setCurrentWorkspace(workspace.id);
|
setCurrentWorkspace(workspace.id);
|
||||||
router.push({
|
router.push({
|
||||||
pathname: `/workspace/[workspaceId]/all`,
|
pathname: `/workspace/[workspaceId]/all`,
|
||||||
@@ -51,7 +48,6 @@ export function Modals() {
|
|||||||
workspaceId: workspace.id,
|
workspaceId: workspace.id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
setOpenWorkspacesModal(false);
|
|
||||||
},
|
},
|
||||||
[router, setCurrentWorkspace, setOpenWorkspacesModal]
|
[router, setCurrentWorkspace, setOpenWorkspacesModal]
|
||||||
)}
|
)}
|
||||||
@@ -74,11 +70,11 @@ export function Modals() {
|
|||||||
setOpenCreateWorkspaceModal(false);
|
setOpenCreateWorkspaceModal(false);
|
||||||
}, [setOpenCreateWorkspaceModal])}
|
}, [setOpenCreateWorkspaceModal])}
|
||||||
onCreate={useCallback(
|
onCreate={useCallback(
|
||||||
name => {
|
async name => {
|
||||||
const id = createRemLocalWorkspace(name);
|
const id = await createLocalWorkspace(name);
|
||||||
setOpenCreateWorkspaceModal(false);
|
setOpenCreateWorkspaceModal(false);
|
||||||
setOpenWorkspacesModal(false);
|
setOpenWorkspacesModal(false);
|
||||||
router.push({
|
return router.push({
|
||||||
pathname: '/workspace/[workspaceId]/all',
|
pathname: '/workspace/[workspaceId]/all',
|
||||||
query: {
|
query: {
|
||||||
workspaceId: id,
|
workspaceId: id,
|
||||||
@@ -86,22 +82,13 @@ export function Modals() {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
createRemLocalWorkspace,
|
createLocalWorkspace,
|
||||||
router,
|
router,
|
||||||
setOpenCreateWorkspaceModal,
|
setOpenCreateWorkspaceModal,
|
||||||
setOpenWorkspacesModal,
|
setOpenWorkspacesModal,
|
||||||
]
|
]
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{currentWorkspace?.blockSuiteWorkspace && (
|
|
||||||
<QuickSearchModal
|
|
||||||
enableShortCut={!disableShortCut}
|
|
||||||
blockSuiteWorkspace={currentWorkspace?.blockSuiteWorkspace}
|
|
||||||
open={openQuickSearchModal}
|
|
||||||
setOpen={setOpenQuickSearchModalAtom}
|
|
||||||
router={router}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,15 +24,21 @@ export function stringToColour(str: string) {
|
|||||||
return colour;
|
return colour;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const hashMap = new Map<string, BlockSuiteWorkspace>();
|
||||||
export const createEmptyBlockSuiteWorkspace = (
|
export const createEmptyBlockSuiteWorkspace = (
|
||||||
room: string,
|
room: string,
|
||||||
blobOptionsGetter?: BlobOptionsGetter
|
blobOptionsGetter?: BlobOptionsGetter
|
||||||
) => {
|
): BlockSuiteWorkspace => {
|
||||||
return new BlockSuiteWorkspace({
|
if (hashMap.has(room)) {
|
||||||
|
return hashMap.get(room) as BlockSuiteWorkspace;
|
||||||
|
}
|
||||||
|
const workspace = new BlockSuiteWorkspace({
|
||||||
room,
|
room,
|
||||||
isSSR: typeof window === 'undefined',
|
isSSR: typeof window === 'undefined',
|
||||||
blobOptionsGetter,
|
blobOptionsGetter,
|
||||||
})
|
})
|
||||||
.register(builtInSchemas)
|
.register(builtInSchemas)
|
||||||
.register(__unstableSchemas);
|
.register(__unstableSchemas);
|
||||||
|
hashMap.set(room, workspace);
|
||||||
|
return workspace;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"test": "playwright test",
|
"test": "playwright test",
|
||||||
"test:coverage": "cross-env COVERAGE=true pnpm test -- --forbid-only",
|
"test:coverage": "cross-env COVERAGE=true pnpm test -- --forbid-only",
|
||||||
"test:unit": "vitest --run",
|
"test:unit": "vitest --run",
|
||||||
|
"test:unit:ui": "vitest --ui",
|
||||||
"test:unit:coverage": "vitest run --coverage",
|
"test:unit:coverage": "vitest run --coverage",
|
||||||
"postinstall": "husky install",
|
"postinstall": "husky install",
|
||||||
"notify": "node scripts/notify.mjs",
|
"notify": "node scripts/notify.mjs",
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
"@typescript-eslint/parser": "^5.54.0",
|
"@typescript-eslint/parser": "^5.54.0",
|
||||||
"@vitejs/plugin-react": "^3.1.0",
|
"@vitejs/plugin-react": "^3.1.0",
|
||||||
"@vitest/coverage-istanbul": "^0.28.5",
|
"@vitest/coverage-istanbul": "^0.28.5",
|
||||||
|
"@vitest/ui": "^0.29.2",
|
||||||
"concurrently": "^7.6.0",
|
"concurrently": "^7.6.0",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"eslint": "^8.35.0",
|
"eslint": "^8.35.0",
|
||||||
|
|||||||
50
pnpm-lock.yaml
generated
50
pnpm-lock.yaml
generated
@@ -18,6 +18,7 @@ importers:
|
|||||||
'@typescript-eslint/parser': ^5.54.0
|
'@typescript-eslint/parser': ^5.54.0
|
||||||
'@vitejs/plugin-react': ^3.1.0
|
'@vitejs/plugin-react': ^3.1.0
|
||||||
'@vitest/coverage-istanbul': ^0.28.5
|
'@vitest/coverage-istanbul': ^0.28.5
|
||||||
|
'@vitest/ui': ^0.29.2
|
||||||
concurrently: ^7.6.0
|
concurrently: ^7.6.0
|
||||||
cross-env: ^7.0.3
|
cross-env: ^7.0.3
|
||||||
eslint: ^8.35.0
|
eslint: ^8.35.0
|
||||||
@@ -49,7 +50,8 @@ importers:
|
|||||||
'@typescript-eslint/eslint-plugin': 5.54.0_6mj2wypvdnknez7kws2nfdgupi
|
'@typescript-eslint/eslint-plugin': 5.54.0_6mj2wypvdnknez7kws2nfdgupi
|
||||||
'@typescript-eslint/parser': 5.54.0_ycpbpc6yetojsgtrx3mwntkhsu
|
'@typescript-eslint/parser': 5.54.0_ycpbpc6yetojsgtrx3mwntkhsu
|
||||||
'@vitejs/plugin-react': 3.1.0_vite@4.1.4
|
'@vitejs/plugin-react': 3.1.0_vite@4.1.4
|
||||||
'@vitest/coverage-istanbul': 0.28.5_happy-dom@8.9.0
|
'@vitest/coverage-istanbul': 0.28.5_oscpa7jhv6f2pwrg7t4qxe6ngi
|
||||||
|
'@vitest/ui': 0.29.2
|
||||||
concurrently: 7.6.0
|
concurrently: 7.6.0
|
||||||
cross-env: 7.0.3
|
cross-env: 7.0.3
|
||||||
eslint: 8.35.0
|
eslint: 8.35.0
|
||||||
@@ -70,7 +72,7 @@ importers:
|
|||||||
react-dom: 18.2.0_react@18.2.0
|
react-dom: 18.2.0_react@18.2.0
|
||||||
typescript: 4.9.5
|
typescript: 4.9.5
|
||||||
vite: 4.1.4_@types+node@18.14.4
|
vite: 4.1.4_@types+node@18.14.4
|
||||||
vitest: 0.28.5_happy-dom@8.9.0
|
vitest: 0.28.5_oscpa7jhv6f2pwrg7t4qxe6ngi
|
||||||
vitest-fetch-mock: 0.2.2_vitest@0.28.5
|
vitest-fetch-mock: 0.2.2_vitest@0.28.5
|
||||||
|
|
||||||
apps/desktop:
|
apps/desktop:
|
||||||
@@ -3908,6 +3910,10 @@ packages:
|
|||||||
fsevents: 2.3.2
|
fsevents: 2.3.2
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@polka/url/1.0.0-next.21:
|
||||||
|
resolution: {integrity: sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@popperjs/core/2.11.6:
|
/@popperjs/core/2.11.6:
|
||||||
resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==}
|
resolution: {integrity: sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw==}
|
||||||
dev: false
|
dev: false
|
||||||
@@ -5894,12 +5900,12 @@ packages:
|
|||||||
'@babel/plugin-transform-react-jsx-source': 7.19.6_@babel+core@7.20.12
|
'@babel/plugin-transform-react-jsx-source': 7.19.6_@babel+core@7.20.12
|
||||||
magic-string: 0.27.0
|
magic-string: 0.27.0
|
||||||
react-refresh: 0.14.0
|
react-refresh: 0.14.0
|
||||||
vite: 4.1.4_@types+node@18.14.4
|
vite: 4.1.4
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/@vitest/coverage-istanbul/0.28.5_happy-dom@8.9.0:
|
/@vitest/coverage-istanbul/0.28.5_oscpa7jhv6f2pwrg7t4qxe6ngi:
|
||||||
resolution: {integrity: sha512-na1pkr3AVrdFflzuBXsBh1MvBfhSMrv4nfd4N8rm0HEJlvlbQc+GiqNwtwzfO8TPsXxcjNphSIMp5wvCy+0xrQ==}
|
resolution: {integrity: sha512-na1pkr3AVrdFflzuBXsBh1MvBfhSMrv4nfd4N8rm0HEJlvlbQc+GiqNwtwzfO8TPsXxcjNphSIMp5wvCy+0xrQ==}
|
||||||
dependencies:
|
dependencies:
|
||||||
istanbul-lib-coverage: 3.2.0
|
istanbul-lib-coverage: 3.2.0
|
||||||
@@ -5908,7 +5914,7 @@ packages:
|
|||||||
istanbul-lib-source-maps: 4.0.1
|
istanbul-lib-source-maps: 4.0.1
|
||||||
istanbul-reports: 3.1.5
|
istanbul-reports: 3.1.5
|
||||||
test-exclude: 6.0.0
|
test-exclude: 6.0.0
|
||||||
vitest: 0.28.5_happy-dom@8.9.0
|
vitest: 0.28.5_oscpa7jhv6f2pwrg7t4qxe6ngi
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- '@edge-runtime/vm'
|
- '@edge-runtime/vm'
|
||||||
- '@vitest/browser'
|
- '@vitest/browser'
|
||||||
@@ -5945,6 +5951,16 @@ packages:
|
|||||||
tinyspy: 1.1.1
|
tinyspy: 1.1.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@vitest/ui/0.29.2:
|
||||||
|
resolution: {integrity: sha512-GpCExCMptrS1z3Xf6kz35Xdvjc2eTBy9OIIwW3HjePVxw9Q++ZoEaIBVimRTTGzSe40XiAI/ZyR0H0Ya9brqLA==}
|
||||||
|
dependencies:
|
||||||
|
fast-glob: 3.2.12
|
||||||
|
flatted: 3.2.7
|
||||||
|
pathe: 1.1.0
|
||||||
|
picocolors: 1.0.0
|
||||||
|
sirv: 2.0.2
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@vitest/utils/0.28.5:
|
/@vitest/utils/0.28.5:
|
||||||
resolution: {integrity: sha512-UyZdYwdULlOa4LTUSwZ+Paz7nBHGTT72jKwdFSV4IjHF1xsokp+CabMdhjvVhYwkLfO88ylJT46YMilnkSARZA==}
|
resolution: {integrity: sha512-UyZdYwdULlOa4LTUSwZ+Paz7nBHGTT72jKwdFSV4IjHF1xsokp+CabMdhjvVhYwkLfO88ylJT46YMilnkSARZA==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -10740,6 +10756,11 @@ packages:
|
|||||||
engines: {node: '>=4'}
|
engines: {node: '>=4'}
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/mrmime/1.0.1:
|
||||||
|
resolution: {integrity: sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==}
|
||||||
|
engines: {node: '>=10'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/ms/2.0.0:
|
/ms/2.0.0:
|
||||||
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
resolution: {integrity: sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -12596,6 +12617,15 @@ packages:
|
|||||||
semver: 7.0.0
|
semver: 7.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/sirv/2.0.2:
|
||||||
|
resolution: {integrity: sha512-4Qog6aE29nIjAOKe/wowFTxOdmbEZKb+3tsLljaBRzJwtqto0BChD2zzH0LhgCSXiI+V7X+Y45v14wBZQ1TK3w==}
|
||||||
|
engines: {node: '>= 10'}
|
||||||
|
dependencies:
|
||||||
|
'@polka/url': 1.0.0-next.21
|
||||||
|
mrmime: 1.0.1
|
||||||
|
totalist: 3.0.0
|
||||||
|
dev: true
|
||||||
|
|
||||||
/sisteransi/1.0.5:
|
/sisteransi/1.0.5:
|
||||||
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==}
|
||||||
dev: true
|
dev: true
|
||||||
@@ -13277,6 +13307,11 @@ packages:
|
|||||||
ieee754: 1.2.1
|
ieee754: 1.2.1
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/totalist/3.0.0:
|
||||||
|
resolution: {integrity: sha512-eM+pCBxXO/njtF7vdFsHuqb+ElbxqtI4r5EAvk6grfAFyJ6IvWlSkfZ5T9ozC6xWw3Fj1fGoSmrl0gUs46JVIw==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dev: true
|
||||||
|
|
||||||
/tr46/0.0.3:
|
/tr46/0.0.3:
|
||||||
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==}
|
||||||
|
|
||||||
@@ -13835,12 +13870,12 @@ packages:
|
|||||||
vitest: '>=0.16.0'
|
vitest: '>=0.16.0'
|
||||||
dependencies:
|
dependencies:
|
||||||
cross-fetch: 3.1.5
|
cross-fetch: 3.1.5
|
||||||
vitest: 0.28.5_happy-dom@8.9.0
|
vitest: 0.28.5_oscpa7jhv6f2pwrg7t4qxe6ngi
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- encoding
|
- encoding
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/vitest/0.28.5_happy-dom@8.9.0:
|
/vitest/0.28.5_oscpa7jhv6f2pwrg7t4qxe6ngi:
|
||||||
resolution: {integrity: sha512-pyCQ+wcAOX7mKMcBNkzDwEHRGqQvHUl0XnoHR+3Pb1hytAHISgSxv9h0gUiSiYtISXUU3rMrKiKzFYDrI6ZIHA==}
|
resolution: {integrity: sha512-pyCQ+wcAOX7mKMcBNkzDwEHRGqQvHUl0XnoHR+3Pb1hytAHISgSxv9h0gUiSiYtISXUU3rMrKiKzFYDrI6ZIHA==}
|
||||||
engines: {node: '>=v14.16.0'}
|
engines: {node: '>=v14.16.0'}
|
||||||
hasBin: true
|
hasBin: true
|
||||||
@@ -13868,6 +13903,7 @@ packages:
|
|||||||
'@vitest/expect': 0.28.5
|
'@vitest/expect': 0.28.5
|
||||||
'@vitest/runner': 0.28.5
|
'@vitest/runner': 0.28.5
|
||||||
'@vitest/spy': 0.28.5
|
'@vitest/spy': 0.28.5
|
||||||
|
'@vitest/ui': 0.29.2
|
||||||
'@vitest/utils': 0.28.5
|
'@vitest/utils': 0.28.5
|
||||||
acorn: 8.8.2
|
acorn: 8.8.2
|
||||||
acorn-walk: 8.2.0
|
acorn-walk: 8.2.0
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ test.describe('Local first create page', () => {
|
|||||||
await page.getByTestId('create-workspace-button').click();
|
await page.getByTestId('create-workspace-button').click();
|
||||||
await page.getByTestId('workspace-name').click();
|
await page.getByTestId('workspace-name').click();
|
||||||
await page.getByTestId('workspace-card').nth(1).click();
|
await page.getByTestId('workspace-card').nth(1).click();
|
||||||
await page.getByText('Workspace Setting').click();
|
await page.getByTestId('slider-bar-workspace-setting-button').click();
|
||||||
await page
|
await page
|
||||||
.getByTestId('upload-avatar')
|
.getByTestId('upload-avatar')
|
||||||
.setInputFiles('./tests/fixtures/smile.png');
|
.setInputFiles('./tests/fixtures/smile.png');
|
||||||
|
|||||||
Reference in New Issue
Block a user