mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 05:37:32 +00:00
refactor!: next generation AFFiNE code structure (#1176)
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
// Vitest Snapshot v1
|
||||
|
||||
exports[`usePageMetas > basic 1`] = `
|
||||
<DocumentFragment>
|
||||
<div>
|
||||
<div>
|
||||
page0
|
||||
</div>
|
||||
<div>
|
||||
page1
|
||||
</div>
|
||||
<div>
|
||||
page2
|
||||
</div>
|
||||
</div>
|
||||
</DocumentFragment>
|
||||
`;
|
||||
180
apps/web/src/hooks/__tests__/index.spec.tsx
Normal file
180
apps/web/src/hooks/__tests__/index.spec.tsx
Normal file
@@ -0,0 +1,180 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
import assert from 'node:assert';
|
||||
|
||||
import { __unstableSchemas, builtInSchemas } from '@blocksuite/blocks/models';
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import { render, renderHook } from '@testing-library/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import routerMock from 'next-router-mock';
|
||||
import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes';
|
||||
import { beforeAll, beforeEach, describe, expect, test } from 'vitest';
|
||||
|
||||
import { BlockSuiteWorkspace, RemWorkspaceFlavour } from '../../shared';
|
||||
import { useCurrentWorkspace } from '../current/use-current-workspace';
|
||||
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 {
|
||||
useWorkspaces,
|
||||
useWorkspacesHelper,
|
||||
vitestRefreshWorkspaces,
|
||||
} from '../use-workspaces';
|
||||
|
||||
let blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
beforeAll(() => {
|
||||
routerMock.useParser(
|
||||
createDynamicRouteParser(['/workspace/[workspaceId]/[pageId]'])
|
||||
);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
vitestRefreshWorkspaces();
|
||||
dataCenter.isLoaded = true;
|
||||
return new Promise<void>(resolve => {
|
||||
blockSuiteWorkspace = new BlockSuiteWorkspace({
|
||||
room: 'test',
|
||||
})
|
||||
.register(builtInSchemas)
|
||||
.register(__unstableSchemas);
|
||||
blockSuiteWorkspace.signals.pageAdded.on(pageId => {
|
||||
setTimeout(() => {
|
||||
const page = blockSuiteWorkspace.getPage(pageId);
|
||||
expect(page).not.toBeNull();
|
||||
assertExists(page);
|
||||
const pageBlockId = page.addBlockByFlavour('affine:page', {
|
||||
title: '',
|
||||
});
|
||||
const frameId = page.addBlockByFlavour('affine:frame', {}, pageBlockId);
|
||||
page.addBlockByFlavour('affine:paragraph', {}, frameId);
|
||||
if (pageId === 'page2') {
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
blockSuiteWorkspace.createPage('page0');
|
||||
blockSuiteWorkspace.createPage('page1');
|
||||
blockSuiteWorkspace.createPage('page2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('usePageMetas', async () => {
|
||||
test('basic', async () => {
|
||||
const Component = () => {
|
||||
const pageMetas = usePageMeta(blockSuiteWorkspace);
|
||||
return (
|
||||
<div>
|
||||
{pageMetas.map(meta => (
|
||||
<div key={meta.id}>{meta.id}</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
const result = render(<Component />);
|
||||
await result.findByText('page0');
|
||||
await result.findByText('page1');
|
||||
await result.findByText('page2');
|
||||
expect(result.asFragment()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('mutation', () => {
|
||||
const { result, rerender } = renderHook(() =>
|
||||
usePageMeta(blockSuiteWorkspace)
|
||||
);
|
||||
expect(result.current.length).toBe(3);
|
||||
expect(result.current[0].mode).not.exist;
|
||||
const { result: result2 } = renderHook(() =>
|
||||
usePageMetaHelper(blockSuiteWorkspace)
|
||||
);
|
||||
result2.current.setPageMeta('page0', {
|
||||
mode: 'edgeless',
|
||||
});
|
||||
rerender();
|
||||
expect(result.current[0].mode).exist;
|
||||
expect(result.current[0].mode).toBe('edgeless');
|
||||
result2.current.setPageMeta('page0', {
|
||||
mode: 'page',
|
||||
});
|
||||
rerender();
|
||||
expect(result.current[0].mode).toBe('page');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useWorkspaces', () => {
|
||||
test('basic', () => {
|
||||
const { result } = renderHook(() => useWorkspaces());
|
||||
expect(result.current).toEqual([]);
|
||||
});
|
||||
|
||||
test('mutation', () => {
|
||||
const { result } = renderHook(() => useWorkspacesHelper());
|
||||
result.current.createRemLocalWorkspace('test');
|
||||
const { result: result2 } = renderHook(() => useWorkspaces());
|
||||
expect(result2.current.length).toEqual(1);
|
||||
const firstWorkspace = result2.current[0];
|
||||
expect(firstWorkspace.flavour).toBe('local');
|
||||
assert(firstWorkspace.flavour === RemWorkspaceFlavour.LOCAL);
|
||||
expect(firstWorkspace.blockSuiteWorkspace.meta.name).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useSyncRouterWithCurrentWorkspaceAndPage', () => {
|
||||
test('from "/"', async () => {
|
||||
const mutationHook = renderHook(() => useWorkspacesHelper());
|
||||
const id = mutationHook.result.current.createRemLocalWorkspace('test0');
|
||||
mutationHook.result.current.createWorkspacePage(id, 'page0');
|
||||
const routerHook = renderHook(() => useRouter());
|
||||
await routerHook.result.current.push('/');
|
||||
routerHook.rerender();
|
||||
expect(routerHook.result.current.asPath).toBe('/');
|
||||
renderHook(
|
||||
({ router }) => useSyncRouterWithCurrentWorkspaceAndPage(router),
|
||||
{
|
||||
initialProps: {
|
||||
router: routerHook.result.current,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/page0`);
|
||||
});
|
||||
|
||||
test('from incorrect "/workspace/[workspaceId]/[pageId]"', async () => {
|
||||
const mutationHook = renderHook(() => useWorkspacesHelper());
|
||||
const id = mutationHook.result.current.createRemLocalWorkspace('test0');
|
||||
mutationHook.result.current.createWorkspacePage(id, 'page0');
|
||||
const routerHook = renderHook(() => useRouter());
|
||||
await routerHook.result.current.push(`/workspace/${id}/not_exist`);
|
||||
routerHook.rerender();
|
||||
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/not_exist`);
|
||||
renderHook(
|
||||
({ router }) => useSyncRouterWithCurrentWorkspaceAndPage(router),
|
||||
{
|
||||
initialProps: {
|
||||
router: routerHook.result.current,
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
expect(routerHook.result.current.asPath).toBe(`/workspace/${id}/page0`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('useLastOpenedWorkspace', () => {
|
||||
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
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import 'fake-indexeddb/auto';
|
||||
|
||||
import { __unstableSchemas, builtInSchemas } from '@blocksuite/blocks/models';
|
||||
import { Page } from '@blocksuite/store';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { BlockSuiteWorkspace } from '../../shared';
|
||||
import { useBlockSuiteWorkspaceHelper } from '../use-blocksuite-workspace-helper';
|
||||
import { usePageMeta } from '../use-page-meta';
|
||||
import { vitestRefreshWorkspaces } from '../use-workspaces';
|
||||
|
||||
let blockSuiteWorkspace: BlockSuiteWorkspace;
|
||||
|
||||
beforeEach(() => {
|
||||
vitestRefreshWorkspaces();
|
||||
blockSuiteWorkspace = new BlockSuiteWorkspace({
|
||||
room: 'test',
|
||||
})
|
||||
.register(builtInSchemas)
|
||||
.register(__unstableSchemas);
|
||||
blockSuiteWorkspace.signals.pageAdded.on(pageId => {
|
||||
const page = blockSuiteWorkspace.getPage(pageId) as Page;
|
||||
const pageBlockId = page.addBlockByFlavour('affine:page', { title: '' });
|
||||
const frameId = page.addBlockByFlavour('affine:frame', {}, pageBlockId);
|
||||
page.addBlockByFlavour('affine:paragraph', {}, frameId);
|
||||
});
|
||||
blockSuiteWorkspace.createPage('page0');
|
||||
blockSuiteWorkspace.createPage('page1');
|
||||
blockSuiteWorkspace.createPage('page2');
|
||||
});
|
||||
|
||||
describe('useBlockSuiteWorkspaceHelper', () => {
|
||||
test('should create page', () => {
|
||||
expect(blockSuiteWorkspace.meta.pageMetas.length).toBe(3);
|
||||
const helperHook = renderHook(() =>
|
||||
useBlockSuiteWorkspaceHelper(blockSuiteWorkspace)
|
||||
);
|
||||
const pageMetaHook = renderHook(() => usePageMeta(blockSuiteWorkspace));
|
||||
expect(pageMetaHook.result.current.length).toBe(3);
|
||||
expect(blockSuiteWorkspace.meta.pageMetas.length).toBe(3);
|
||||
const callback = vi.fn(id => {
|
||||
expect(id).toBe('page4');
|
||||
});
|
||||
helperHook.result.current.createPage('page4').then(callback);
|
||||
expect(blockSuiteWorkspace.meta.pageMetas.length).toBe(4);
|
||||
pageMetaHook.rerender();
|
||||
expect(pageMetaHook.result.current.length).toBe(4);
|
||||
});
|
||||
});
|
||||
25
apps/web/src/hooks/__tests__/use-feature-flag.spec.ts
Normal file
25
apps/web/src/hooks/__tests__/use-feature-flag.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { beforeEach, describe, expect, test } from 'vitest';
|
||||
|
||||
import { defaultRecord, useFeatureFlag } from '../use-feature-flag';
|
||||
|
||||
beforeEach(() => {
|
||||
globalThis.featureFlag.record = defaultRecord;
|
||||
globalThis.featureFlag.callback.clear();
|
||||
});
|
||||
|
||||
describe('useFeatureFlag', () => {
|
||||
test('basic', () => {
|
||||
const flagHook = renderHook(() =>
|
||||
useFeatureFlag('enableIndexedDBProvider')
|
||||
);
|
||||
expect(flagHook.result.current).toBe(defaultRecord.enableIndexedDBProvider);
|
||||
globalThis.featureFlag.record.enableIndexedDBProvider = false;
|
||||
globalThis.featureFlag.callback.forEach(cb => cb());
|
||||
flagHook.rerender();
|
||||
expect(flagHook.result.current).toBe(false);
|
||||
});
|
||||
});
|
||||
101
apps/web/src/hooks/__tests__/use-router-helper.spec.ts
Normal file
101
apps/web/src/hooks/__tests__/use-router-helper.spec.ts
Normal file
@@ -0,0 +1,101 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useRouter } from 'next/router';
|
||||
import routerMock from 'next-router-mock';
|
||||
import { createDynamicRouteParser } from 'next-router-mock/dynamic-routes';
|
||||
import { beforeAll, describe, expect, test } from 'vitest';
|
||||
|
||||
import { WorkspaceSubPath } from '../../shared';
|
||||
import { RouteLogic, useRouterHelper } from '../use-router-helper';
|
||||
|
||||
beforeAll(() => {
|
||||
routerMock.useParser(
|
||||
createDynamicRouteParser([
|
||||
'/workspace/[workspaceId]/[pageId]',
|
||||
'/workspace/[workspaceId]/all',
|
||||
'/workspace/[workspaceId]/favorite',
|
||||
'/workspace/[workspaceId]/trash',
|
||||
'/workspace/[workspaceId]/setting',
|
||||
])
|
||||
);
|
||||
});
|
||||
|
||||
describe('useRouterHelper', () => {
|
||||
test('should return a expected router helper', async () => {
|
||||
const routerHook = renderHook(() => useRouter());
|
||||
const helperHook = renderHook(router => useRouterHelper(router), {
|
||||
initialProps: routerHook.result.current,
|
||||
});
|
||||
const hook = helperHook.result.current;
|
||||
expect(hook).toBeTypeOf('object');
|
||||
Object.values(hook).forEach(value => {
|
||||
expect(value).toBeTypeOf('function');
|
||||
});
|
||||
});
|
||||
|
||||
test('should jump to the expected sub path', async () => {
|
||||
const routerHook = renderHook(() => useRouter());
|
||||
// set current path to '/'
|
||||
await routerHook.result.current.replace('/');
|
||||
const helperHook = renderHook(router => useRouterHelper(router), {
|
||||
initialProps: routerHook.result.current,
|
||||
});
|
||||
const hook = helperHook.result.current;
|
||||
await hook.jumpToSubPath('workspace0', WorkspaceSubPath.ALL);
|
||||
routerHook.rerender();
|
||||
expect(routerHook.result.current.pathname).toBe(
|
||||
'/workspace/[workspaceId]/all'
|
||||
);
|
||||
expect(routerHook.result.current.asPath).toBe('/workspace/workspace0/all');
|
||||
// `router.back` is not working in `next-router-mock`
|
||||
// routerHook.result.current.back()
|
||||
// routerHook.rerender()
|
||||
// expect(routerHook.result.current.pathname).toBe('/')
|
||||
|
||||
await hook.jumpToSubPath(
|
||||
'workspace1',
|
||||
WorkspaceSubPath.FAVORITE,
|
||||
RouteLogic.REPLACE
|
||||
);
|
||||
routerHook.rerender();
|
||||
expect(routerHook.result.current.pathname).toBe(
|
||||
'/workspace/[workspaceId]/favorite'
|
||||
);
|
||||
expect(routerHook.result.current.asPath).toBe(
|
||||
'/workspace/workspace1/favorite'
|
||||
);
|
||||
});
|
||||
|
||||
test('should jump to the expected page', async () => {
|
||||
const routerHook = renderHook(() => useRouter());
|
||||
// set current path to '/'
|
||||
await routerHook.result.current.replace('/');
|
||||
const helperHook = renderHook(router => useRouterHelper(router), {
|
||||
initialProps: routerHook.result.current,
|
||||
});
|
||||
const hook = helperHook.result.current;
|
||||
await hook.jumpToPage('workspace0', 'page0');
|
||||
routerHook.rerender();
|
||||
expect(routerHook.result.current.pathname).toBe(
|
||||
'/workspace/[workspaceId]/[pageId]'
|
||||
);
|
||||
expect(routerHook.result.current.asPath).toBe(
|
||||
'/workspace/workspace0/page0'
|
||||
);
|
||||
// `router.back` is not working in `next-router-mock`
|
||||
// routerHook.result.current.back()
|
||||
// routerHook.rerender()
|
||||
// expect(routerHook.result.current.pathname).toBe('/')
|
||||
|
||||
await hook.jumpToPage('workspace1', 'page1', RouteLogic.REPLACE);
|
||||
routerHook.rerender();
|
||||
expect(routerHook.result.current.pathname).toBe(
|
||||
'/workspace/[workspaceId]/[pageId]'
|
||||
);
|
||||
expect(routerHook.result.current.asPath).toBe(
|
||||
'/workspace/workspace1/page1'
|
||||
);
|
||||
});
|
||||
});
|
||||
23
apps/web/src/hooks/__tests__/use-system-online.spec.ts
Normal file
23
apps/web/src/hooks/__tests__/use-system-online.spec.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
/**
|
||||
* @vitest-environment happy-dom
|
||||
*/
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { describe, expect, test, vi } from 'vitest';
|
||||
|
||||
import { useSystemOnline } from '../use-system-online';
|
||||
|
||||
describe('useSystemOnline', () => {
|
||||
test('should be online', () => {
|
||||
const systemOnlineHook = renderHook(() => useSystemOnline());
|
||||
expect(systemOnlineHook.result.current).toBe(true);
|
||||
});
|
||||
|
||||
test('should be offline', () => {
|
||||
const systemOnlineHook = renderHook(() => useSystemOnline());
|
||||
vi.spyOn(navigator, 'onLine', 'get').mockReturnValue(false);
|
||||
expect(systemOnlineHook.result.current).toBe(true);
|
||||
window.dispatchEvent(new Event('offline'));
|
||||
systemOnlineHook.rerender();
|
||||
expect(systemOnlineHook.result.current).toBe(false);
|
||||
});
|
||||
});
|
||||
8
apps/web/src/hooks/affine/use-is-workspace-owner.ts
Normal file
8
apps/web/src/hooks/affine/use-is-workspace-owner.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { PermissionType } from '@affine/datacenter';
|
||||
|
||||
import { AffineOfficialWorkspace } from '../../shared';
|
||||
|
||||
export function useIsWorkspaceOwner(workspace: AffineOfficialWorkspace) {
|
||||
if (workspace.flavour === 'local') return true;
|
||||
return workspace.permission === PermissionType.Owner;
|
||||
}
|
||||
43
apps/web/src/hooks/affine/use-members.ts
Normal file
43
apps/web/src/hooks/affine/use-members.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { Member } from '@affine/datacenter';
|
||||
import { useCallback } from 'react';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { QueryKey } from '../../plugins/affine/fetcher';
|
||||
import { apis } from '../../shared/apis';
|
||||
|
||||
export function useMembers(workspaceId: string) {
|
||||
const { data, mutate } = useSWR<Member[]>(
|
||||
[QueryKey.getMembers, workspaceId],
|
||||
{
|
||||
fallbackData: [],
|
||||
}
|
||||
);
|
||||
|
||||
const inviteMember = useCallback(
|
||||
async (email: string) => {
|
||||
await apis.inviteMember({
|
||||
id: workspaceId,
|
||||
email,
|
||||
});
|
||||
return mutate();
|
||||
},
|
||||
[mutate, workspaceId]
|
||||
);
|
||||
|
||||
const removeMember = useCallback(
|
||||
async (permissionId: number) => {
|
||||
// fixme: what about the workspaceId?
|
||||
await apis.removeMember({
|
||||
permissionId,
|
||||
});
|
||||
return mutate();
|
||||
},
|
||||
[mutate]
|
||||
);
|
||||
|
||||
return {
|
||||
members: data ?? [],
|
||||
inviteMember,
|
||||
removeMember,
|
||||
};
|
||||
}
|
||||
21
apps/web/src/hooks/affine/use-toggle-workspace-publish.ts
Normal file
21
apps/web/src/hooks/affine/use-toggle-workspace-publish.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useCallback } from 'react';
|
||||
import { mutate } from 'swr';
|
||||
|
||||
import { QueryKey } from '../../plugins/affine/fetcher';
|
||||
import { AffineRemoteWorkspace } from '../../shared';
|
||||
import { apis } from '../../shared/apis';
|
||||
import { refreshDataCenter } from '../use-workspaces';
|
||||
|
||||
export function useToggleWorkspacePublish(workspace: AffineRemoteWorkspace) {
|
||||
return useCallback(
|
||||
async (isPublish: boolean) => {
|
||||
await apis.updateWorkspace({
|
||||
id: workspace.id,
|
||||
public: isPublish,
|
||||
});
|
||||
await mutate(QueryKey.getWorkspaces);
|
||||
await refreshDataCenter();
|
||||
},
|
||||
[workspace]
|
||||
);
|
||||
}
|
||||
24
apps/web/src/hooks/affine/use-users-by-email.ts
Normal file
24
apps/web/src/hooks/affine/use-users-by-email.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { QueryKey } from '../../plugins/affine/fetcher';
|
||||
|
||||
export interface QueryEmailMember {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
avatar_url: string;
|
||||
create_at: string;
|
||||
}
|
||||
|
||||
export function useUsersByEmail(
|
||||
workspaceId: string,
|
||||
email: string
|
||||
): QueryEmailMember[] | null {
|
||||
const { data } = useSWR<QueryEmailMember[] | null>(
|
||||
[QueryKey.getUserByEmail, workspaceId, email],
|
||||
{
|
||||
fallbackData: null,
|
||||
}
|
||||
);
|
||||
return data ?? null;
|
||||
}
|
||||
10
apps/web/src/hooks/current/use-current-page-id.ts
Normal file
10
apps/web/src/hooks/current/use-current-page-id.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import { useAtom } from 'jotai';
|
||||
|
||||
import { currentPageIdAtom } from '../../atoms';
|
||||
|
||||
export function useCurrentPageId(): [
|
||||
string | null,
|
||||
(newId: string | null) => void
|
||||
] {
|
||||
return useAtom(currentPageIdAtom);
|
||||
}
|
||||
11
apps/web/src/hooks/current/use-current-user.ts
Normal file
11
apps/web/src/hooks/current/use-current-user.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { AccessTokenMessage } from '@affine/datacenter';
|
||||
import useSWR from 'swr';
|
||||
|
||||
import { QueryKey } from '../../plugins/affine/fetcher';
|
||||
|
||||
export function useCurrentUser(): AccessTokenMessage | null {
|
||||
const { data } = useSWR<AccessTokenMessage | null>(QueryKey.getUser, {
|
||||
fallbackData: null,
|
||||
});
|
||||
return data ?? null;
|
||||
}
|
||||
24
apps/web/src/hooks/current/use-current-workspace.ts
Normal file
24
apps/web/src/hooks/current/use-current-workspace.ts
Normal file
@@ -0,0 +1,24 @@
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { currentPageIdAtom, currentWorkspaceIdAtom } from '../../atoms';
|
||||
import { RemWorkspace } from '../../shared';
|
||||
import { useWorkspace } from '../use-workspace';
|
||||
|
||||
export function useCurrentWorkspace(): [
|
||||
RemWorkspace | null,
|
||||
(id: string | null) => void
|
||||
] {
|
||||
const [id, setId] = useAtom(currentWorkspaceIdAtom);
|
||||
const [, setPageId] = useAtom(currentPageIdAtom);
|
||||
return [
|
||||
useWorkspace(id),
|
||||
useCallback(
|
||||
(id: string | null) => {
|
||||
setPageId(null);
|
||||
setId(id);
|
||||
},
|
||||
[setId, setPageId]
|
||||
),
|
||||
];
|
||||
}
|
||||
29
apps/web/src/hooks/use-blocksuite-workspace-helper.ts
Normal file
29
apps/web/src/hooks/use-blocksuite-workspace-helper.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { BlockSuiteWorkspace } from '../shared';
|
||||
|
||||
export function useBlockSuiteWorkspaceHelper(
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace | null
|
||||
) {
|
||||
return useMemo(
|
||||
() => ({
|
||||
createPage: (pageId: string, title?: string): Promise<string> => {
|
||||
return new Promise(resolve => {
|
||||
assertExists(blockSuiteWorkspace);
|
||||
const dispose = blockSuiteWorkspace.signals.pageAdded.on(id => {
|
||||
if (id === pageId) {
|
||||
dispose.dispose();
|
||||
// Fixme: https://github.com/toeverything/blocksuite/issues/1350
|
||||
setTimeout(() => {
|
||||
resolve(pageId);
|
||||
}, 0);
|
||||
}
|
||||
});
|
||||
blockSuiteWorkspace.createPage(pageId);
|
||||
});
|
||||
},
|
||||
}),
|
||||
[blockSuiteWorkspace]
|
||||
);
|
||||
}
|
||||
26
apps/web/src/hooks/use-blocksuite-workspace-page-title.ts
Normal file
26
apps/web/src/hooks/use-blocksuite-workspace-page-title.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { assertExists } from '@blocksuite/store';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { BlockSuiteWorkspace } from '../shared';
|
||||
|
||||
export function useBlockSuiteWorkspacePageTitle(
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace,
|
||||
pageId: string
|
||||
) {
|
||||
const page = blockSuiteWorkspace.getPage(pageId);
|
||||
const [title, setTitle] = useState(() => page?.meta.title || 'AFFiNE');
|
||||
useEffect(() => {
|
||||
const page = blockSuiteWorkspace.getPage(pageId);
|
||||
setTitle(page?.meta.title || 'AFFiNE');
|
||||
const dispose = blockSuiteWorkspace.meta.pagesUpdated.on(() => {
|
||||
const page = blockSuiteWorkspace.getPage(pageId);
|
||||
assertExists(page);
|
||||
setTitle(page?.meta.title || 'AFFiNE');
|
||||
});
|
||||
return () => {
|
||||
dispose.dispose();
|
||||
};
|
||||
}, [blockSuiteWorkspace, pageId]);
|
||||
|
||||
return title;
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { PageMeta } from '@/providers/app-state-provider';
|
||||
import { useGlobalState } from '@/store/app';
|
||||
|
||||
export type ChangePageMeta = (
|
||||
pageId: string,
|
||||
pageMeta: Partial<PageMeta>
|
||||
) => void;
|
||||
|
||||
export const useChangePageMeta = () => {
|
||||
const currentWorkspace = useGlobalState(
|
||||
useCallback(store => store.currentDataCenterWorkspace, [])
|
||||
);
|
||||
|
||||
return useCallback<ChangePageMeta>(
|
||||
(pageId, pageMeta) => {
|
||||
currentWorkspace?.blocksuiteWorkspace?.setPageMeta(pageId, {
|
||||
...pageMeta,
|
||||
});
|
||||
},
|
||||
[currentWorkspace]
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangePageMeta;
|
||||
@@ -1,42 +0,0 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { PageMeta } from '@/providers/app-state-provider';
|
||||
import { useGlobalState } from '@/store/app';
|
||||
|
||||
export const useCurrentPageMeta = (): PageMeta | null => {
|
||||
const currentPage = useGlobalState(store => store.currentPage);
|
||||
const currentBlockSuiteWorkspace = useGlobalState(
|
||||
store => store.currentWorkspace
|
||||
);
|
||||
|
||||
const pageMetaHandler = useCallback((): PageMeta | null => {
|
||||
if (!currentPage || !currentBlockSuiteWorkspace) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
(currentBlockSuiteWorkspace.meta.pageMetas.find(
|
||||
p => p.id === currentPage.id
|
||||
) as PageMeta) ?? null
|
||||
);
|
||||
}, [currentPage, currentBlockSuiteWorkspace]);
|
||||
|
||||
const [currentPageMeta, setCurrentPageMeta] = useState<PageMeta | null>(
|
||||
pageMetaHandler
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setCurrentPageMeta(pageMetaHandler);
|
||||
|
||||
const dispose = currentBlockSuiteWorkspace?.meta.pagesUpdated.on(() => {
|
||||
setCurrentPageMeta(pageMetaHandler);
|
||||
}).dispose;
|
||||
|
||||
return () => {
|
||||
dispose?.();
|
||||
};
|
||||
}, [currentPage, currentBlockSuiteWorkspace, pageMetaHandler]);
|
||||
|
||||
return currentPageMeta;
|
||||
};
|
||||
|
||||
export default useCurrentPageMeta;
|
||||
@@ -1,73 +0,0 @@
|
||||
import { assertEquals } from '@blocksuite/global/utils';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useGlobalState } from '@/store/app';
|
||||
|
||||
// todo: refactor with suspense mode
|
||||
// It is a fully effective hook
|
||||
// Cause it not just ensure workspace loaded, but also have router change.
|
||||
export const useEnsureWorkspace = () => {
|
||||
const dataCenter = useGlobalState(useCallback(store => store.dataCenter, []));
|
||||
const currentWorkspace = useGlobalState(
|
||||
useCallback(store => store.currentDataCenterWorkspace, [])
|
||||
);
|
||||
const loadWorkspace = useGlobalState(
|
||||
useCallback(store => store.loadWorkspace, [])
|
||||
);
|
||||
const router = useRouter();
|
||||
const [currentWorkspaceId, setCurrentWorkspaceId] = useState<string | null>(
|
||||
typeof router.query.workspaceId === 'string'
|
||||
? router.query.workspaceId
|
||||
: null
|
||||
);
|
||||
|
||||
// const defaultOutLineWorkspaceId = '99ce7eb7';
|
||||
// console.log(defaultOutLineWorkspaceId);
|
||||
useEffect(() => {
|
||||
const abortController = new AbortController();
|
||||
|
||||
const workspaceId =
|
||||
(router.query.workspaceId as string) || dataCenter.workspaces[0]?.id;
|
||||
|
||||
// If router.query.workspaceId is not in workspace list, jump to 404 page
|
||||
// If workspaceList is empty, we need to create a default workspace but not jump to 404
|
||||
if (
|
||||
workspaceId &&
|
||||
dataCenter.workspaces.length &&
|
||||
dataCenter.workspaces.findIndex(
|
||||
meta => meta.id.toString() === workspaceId
|
||||
) === -1
|
||||
) {
|
||||
router.push('/404');
|
||||
return;
|
||||
}
|
||||
// If user is not login and input a custom workspaceId, jump to 404 page
|
||||
// if (
|
||||
// !user &&
|
||||
// router.query.workspaceId &&
|
||||
// router.query.workspaceId !== defaultOutLineWorkspaceId
|
||||
// ) {
|
||||
// router.push('/404');
|
||||
// return;
|
||||
// }
|
||||
|
||||
loadWorkspace(workspaceId, abortController.signal).then(unit => {
|
||||
if (!abortController.signal.aborted && unit) {
|
||||
setCurrentWorkspaceId(unit.id);
|
||||
assertEquals(unit.id, workspaceId);
|
||||
}
|
||||
});
|
||||
|
||||
return () => {
|
||||
abortController.abort();
|
||||
};
|
||||
}, [dataCenter, loadWorkspace, router]);
|
||||
|
||||
return {
|
||||
workspaceLoaded: currentWorkspace?.id === currentWorkspaceId,
|
||||
activeWorkspaceId: currentWorkspace?.id ?? router.query.workspaceId,
|
||||
};
|
||||
};
|
||||
|
||||
export default useEnsureWorkspace;
|
||||
46
apps/web/src/hooks/use-feature-flag.ts
Normal file
46
apps/web/src/hooks/use-feature-flag.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { useCallback, useSyncExternalStore } from 'react';
|
||||
|
||||
interface FeatureFlag {
|
||||
enableIndexedDBProvider: boolean;
|
||||
}
|
||||
|
||||
export const defaultRecord: FeatureFlag = {
|
||||
enableIndexedDBProvider: true,
|
||||
} as const;
|
||||
|
||||
const featureFlag = {
|
||||
record: {
|
||||
enableIndexedDBProvider: true,
|
||||
} as FeatureFlag,
|
||||
callback: new Set<() => void>(),
|
||||
};
|
||||
|
||||
declare global {
|
||||
// eslint-disable-next-line no-var
|
||||
var featureFlag: {
|
||||
record: FeatureFlag;
|
||||
callback: Set<() => void>;
|
||||
};
|
||||
}
|
||||
|
||||
if (!globalThis.featureFlag) {
|
||||
globalThis.featureFlag = featureFlag;
|
||||
}
|
||||
|
||||
export const getFeatureFlag = <Key extends keyof FeatureFlag>(key: Key) =>
|
||||
featureFlag.record[key];
|
||||
|
||||
export function useFeatureFlag<Key extends keyof FeatureFlag>(
|
||||
key: Key
|
||||
): FeatureFlag[Key] {
|
||||
return useSyncExternalStore(
|
||||
useCallback(onStoreChange => {
|
||||
featureFlag.callback.add(onStoreChange);
|
||||
return () => {
|
||||
featureFlag.callback.delete(onStoreChange);
|
||||
};
|
||||
}, []),
|
||||
useCallback(() => featureFlag.record[key], [key]),
|
||||
useCallback(() => defaultRecord[key], [key])
|
||||
);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Page } from '@blocksuite/store';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { useGlobalState } from '@/store/app';
|
||||
|
||||
export type EventCallBack<T> = (callback: (props: T) => void) => void;
|
||||
export type UseHistoryUpdated = (page?: Page) => EventCallBack<Page>;
|
||||
|
||||
export const useHistoryUpdate: UseHistoryUpdated = () => {
|
||||
const currentPage = useGlobalState(store => store.currentPage);
|
||||
const callbackQueue = useRef<((page: Page) => void)[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!currentPage) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
currentPage.signals.historyUpdated.on(() => {
|
||||
callbackQueue.current.forEach(callback => {
|
||||
callback(currentPage);
|
||||
});
|
||||
});
|
||||
}, 300);
|
||||
|
||||
return () => {
|
||||
callbackQueue.current = [];
|
||||
currentPage.signals.historyUpdated.dispose();
|
||||
};
|
||||
}, [currentPage]);
|
||||
|
||||
return callback => {
|
||||
callbackQueue.current.push(callback);
|
||||
};
|
||||
};
|
||||
|
||||
export default useHistoryUpdate;
|
||||
41
apps/web/src/hooks/use-last-opened-workspace.ts
Normal file
41
apps/web/src/hooks/use-last-opened-workspace.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
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,35 +0,0 @@
|
||||
import { getDataCenter, WorkspaceUnit } from '@affine/datacenter';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
export function useLoadPublicWorkspace(workspaceId: string) {
|
||||
const router = useRouter();
|
||||
const [workspace, setWorkspace] = useState<WorkspaceUnit | null>();
|
||||
const [status, setStatus] = useState<'loading' | 'error' | 'success'>(
|
||||
'loading'
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
setStatus('loading');
|
||||
|
||||
const init = async () => {
|
||||
const dataCenter = await getDataCenter();
|
||||
|
||||
dataCenter
|
||||
.loadPublicWorkspace(workspaceId)
|
||||
.then(data => {
|
||||
setWorkspace(data);
|
||||
setStatus('success');
|
||||
})
|
||||
.catch(() => {
|
||||
// if (!cancel) {
|
||||
// router.push('/404');
|
||||
// }
|
||||
setStatus('error');
|
||||
});
|
||||
};
|
||||
init();
|
||||
}, [router, workspaceId]);
|
||||
|
||||
return { status, workspace };
|
||||
}
|
||||
13
apps/web/src/hooks/use-load-workspace.ts
Normal file
13
apps/web/src/hooks/use-load-workspace.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { RemWorkspace, RemWorkspaceFlavour } from '../shared';
|
||||
|
||||
export function useLoadWorkspace(workspace: RemWorkspace | null | undefined) {
|
||||
useEffect(() => {
|
||||
if (workspace?.flavour === RemWorkspaceFlavour.AFFINE) {
|
||||
if (!workspace.firstBinarySynced) {
|
||||
workspace.syncBinary();
|
||||
}
|
||||
}
|
||||
}, [workspace]);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useState } from 'react';
|
||||
|
||||
export type UseLocalStorage = <S>(
|
||||
key: string,
|
||||
initialState: S | (() => S),
|
||||
// If initialState is different from the initial local data, set it
|
||||
initialValue?: S
|
||||
) => [S, Dispatch<SetStateAction<S>>];
|
||||
|
||||
export const useLocalStorage: UseLocalStorage = (
|
||||
key,
|
||||
defaultValue,
|
||||
initialValue
|
||||
) => {
|
||||
const [item, setItem] = useState(defaultValue);
|
||||
|
||||
useEffect(() => {
|
||||
const saved = localStorage.getItem(key);
|
||||
if (saved) {
|
||||
setItem(JSON.parse(saved));
|
||||
} else if (initialValue) {
|
||||
setItem(initialValue);
|
||||
}
|
||||
}, [initialValue, key, setItem]);
|
||||
|
||||
useEffect(() => {
|
||||
localStorage.setItem(key, JSON.stringify(item));
|
||||
}, [item, key]);
|
||||
|
||||
return [item, setItem];
|
||||
};
|
||||
|
||||
export default useLocalStorage;
|
||||
@@ -1,55 +0,0 @@
|
||||
import { Member } from '@affine/datacenter';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
|
||||
import { useGlobalState } from '@/store/app';
|
||||
export const useMembers = () => {
|
||||
const dataCenter = useGlobalState(store => store.dataCenter);
|
||||
const currentWorkspace = useGlobalState(
|
||||
useCallback(store => store.currentDataCenterWorkspace, [])
|
||||
);
|
||||
const [members, setMembers] = useState<Member[]>([]);
|
||||
const [loaded, setLoaded] = useState(false);
|
||||
const refreshMembers = useCallback(async () => {
|
||||
if (!currentWorkspace || !dataCenter) return;
|
||||
const members = await dataCenter.getMembers(currentWorkspace.id);
|
||||
setMembers(members ?? []);
|
||||
}, [dataCenter, currentWorkspace]);
|
||||
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
await refreshMembers();
|
||||
setLoaded(true);
|
||||
};
|
||||
init();
|
||||
}, [refreshMembers]);
|
||||
|
||||
const inviteMember = async (email: string) => {
|
||||
currentWorkspace &&
|
||||
dataCenter &&
|
||||
(await dataCenter.inviteMember(currentWorkspace.id, email));
|
||||
};
|
||||
|
||||
const removeMember = async (permissionId: number) => {
|
||||
if (!currentWorkspace || !dataCenter) {
|
||||
return;
|
||||
}
|
||||
setLoaded(false);
|
||||
await dataCenter.removeMember(currentWorkspace.id, permissionId);
|
||||
await refreshMembers();
|
||||
setLoaded(true);
|
||||
};
|
||||
|
||||
const getUserByEmail = async (email: string) => {
|
||||
if (!currentWorkspace) return null;
|
||||
return dataCenter?.getUserByEmail(currentWorkspace.id, email);
|
||||
};
|
||||
return {
|
||||
members,
|
||||
removeMember,
|
||||
inviteMember,
|
||||
getUserByEmail,
|
||||
loaded,
|
||||
};
|
||||
};
|
||||
|
||||
export default useMembers;
|
||||
@@ -1,152 +0,0 @@
|
||||
import { WorkspaceUnit } from '@affine/datacenter';
|
||||
import { EditorContainer } from '@blocksuite/editor';
|
||||
import { uuidv4, Workspace } from '@blocksuite/store';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import type { QueryContent } from '@blocksuite/store/dist/workspace/search';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useChangePageMeta } from '@/hooks/use-change-page-meta';
|
||||
import { PageMeta } from '@/providers/app-state-provider';
|
||||
import { useGlobalState } from '@/store/app';
|
||||
|
||||
export type EditorHandlers = {
|
||||
createPage: (params?: {
|
||||
pageId?: string;
|
||||
title?: string;
|
||||
}) => Promise<string | null>;
|
||||
openPage: (
|
||||
pageId: string,
|
||||
query?: { [key: string]: string },
|
||||
newTab?: boolean
|
||||
) => Promise<boolean>;
|
||||
getPageMeta: (pageId: string) => PageMeta | null;
|
||||
toggleDeletePage: (pageId: string) => Promise<boolean>;
|
||||
toggleFavoritePage: (pageId: string) => Promise<boolean>;
|
||||
permanentlyDeletePage: (pageId: string) => void;
|
||||
search: (
|
||||
query: QueryContent,
|
||||
workspace?: Workspace
|
||||
) => Map<string, string | undefined>;
|
||||
// changeEditorMode: (pageId: string) => void;
|
||||
changePageMode: (
|
||||
pageId: string,
|
||||
mode: EditorContainer['mode']
|
||||
) => Promise<EditorContainer['mode']>;
|
||||
};
|
||||
|
||||
const getPageMeta = (workspace: WorkspaceUnit | null, pageId: string) => {
|
||||
return workspace?.blocksuiteWorkspace?.meta.pageMetas.find(
|
||||
p => p.id === pageId
|
||||
);
|
||||
};
|
||||
export const usePageHelper = (): EditorHandlers => {
|
||||
const router = useRouter();
|
||||
const changePageMeta = useChangePageMeta();
|
||||
const editor = useGlobalState(store => store.editor);
|
||||
const currentWorkspace = useGlobalState(
|
||||
useCallback(store => store.currentDataCenterWorkspace, [])
|
||||
);
|
||||
|
||||
return {
|
||||
createPage: ({
|
||||
pageId = uuidv4().replaceAll('-', ''),
|
||||
title = '',
|
||||
} = {}) => {
|
||||
return new Promise(resolve => {
|
||||
if (!currentWorkspace) {
|
||||
return resolve(null);
|
||||
}
|
||||
currentWorkspace.blocksuiteWorkspace?.createPage(pageId);
|
||||
currentWorkspace.blocksuiteWorkspace?.signals.pageAdded.once(
|
||||
addedPageId => {
|
||||
currentWorkspace.blocksuiteWorkspace?.setPageMeta(addedPageId, {
|
||||
title,
|
||||
mode: 'page',
|
||||
});
|
||||
resolve(addedPageId);
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
toggleFavoritePage: async pageId => {
|
||||
const pageMeta = getPageMeta(currentWorkspace, pageId);
|
||||
if (!pageMeta) {
|
||||
return Promise.reject('No page');
|
||||
}
|
||||
const favorite = !pageMeta.favorite;
|
||||
changePageMeta(pageMeta.id, {
|
||||
favorite,
|
||||
});
|
||||
return favorite;
|
||||
},
|
||||
toggleDeletePage: async pageId => {
|
||||
const pageMeta = getPageMeta(currentWorkspace, pageId);
|
||||
|
||||
if (!pageMeta) {
|
||||
return Promise.reject('No page');
|
||||
}
|
||||
const trash = !pageMeta.trash;
|
||||
|
||||
changePageMeta(pageMeta.id, {
|
||||
trash,
|
||||
trashDate: +new Date(),
|
||||
});
|
||||
return trash;
|
||||
},
|
||||
search: (query: QueryContent, workspace?: Workspace) => {
|
||||
if (workspace) {
|
||||
return workspace.search(query);
|
||||
}
|
||||
if (currentWorkspace) {
|
||||
if (currentWorkspace.blocksuiteWorkspace) {
|
||||
return currentWorkspace.blocksuiteWorkspace.search(query);
|
||||
}
|
||||
}
|
||||
return new Map();
|
||||
},
|
||||
changePageMode: async (pageId, mode) => {
|
||||
const pageMeta = getPageMeta(currentWorkspace, pageId);
|
||||
if (!pageMeta) {
|
||||
return Promise.reject('No page');
|
||||
}
|
||||
|
||||
editor?.setAttribute('mode', mode as string);
|
||||
|
||||
changePageMeta(pageMeta.id, {
|
||||
mode,
|
||||
});
|
||||
return mode;
|
||||
},
|
||||
permanentlyDeletePage: pageId => {
|
||||
// TODO: workspace.meta.removePage or workspace.removePage?
|
||||
|
||||
currentWorkspace!.blocksuiteWorkspace?.meta.removePage(pageId);
|
||||
},
|
||||
openPage: (pageId, query = {}, newTab = false) => {
|
||||
pageId = pageId.replace('space:', '');
|
||||
|
||||
if (newTab) {
|
||||
window.open(`/workspace/${currentWorkspace?.id}/${pageId}`, '_blank');
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
return router.push({
|
||||
pathname: `/workspace/${currentWorkspace?.id}/${pageId}`,
|
||||
query,
|
||||
});
|
||||
},
|
||||
getPageMeta: pageId => {
|
||||
if (!currentWorkspace) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
(currentWorkspace.blocksuiteWorkspace?.meta.pageMetas.find(
|
||||
page => page.id === pageId
|
||||
) as PageMeta) || null
|
||||
);
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default usePageHelper;
|
||||
49
apps/web/src/hooks/use-page-meta.ts
Normal file
49
apps/web/src/hooks/use-page-meta.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { PageMeta } from '@blocksuite/store';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { BlockSuiteWorkspace } from '../shared';
|
||||
|
||||
declare module '@blocksuite/store' {
|
||||
interface PageMeta {
|
||||
mode?: 'page' | 'edgeless';
|
||||
favorite?: boolean;
|
||||
trash?: boolean;
|
||||
}
|
||||
}
|
||||
|
||||
export function usePageMeta(
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace | null
|
||||
): PageMeta[] {
|
||||
const [pageMeta, setPageMeta] = useState<PageMeta[]>(
|
||||
() => blockSuiteWorkspace?.meta.pageMetas ?? []
|
||||
);
|
||||
const [prev, setPrev] = useState(() => blockSuiteWorkspace);
|
||||
if (prev !== blockSuiteWorkspace) {
|
||||
setPrev(blockSuiteWorkspace);
|
||||
if (blockSuiteWorkspace) {
|
||||
setPageMeta(blockSuiteWorkspace.meta.pageMetas);
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (blockSuiteWorkspace) {
|
||||
const dispose = blockSuiteWorkspace.meta.pagesUpdated.on(() => {
|
||||
setPageMeta(blockSuiteWorkspace.meta.pageMetas);
|
||||
});
|
||||
return () => {
|
||||
dispose.dispose();
|
||||
};
|
||||
}
|
||||
}, [blockSuiteWorkspace]);
|
||||
return pageMeta;
|
||||
}
|
||||
|
||||
export function usePageMetaHelper(blockSuiteWorkspace: BlockSuiteWorkspace) {
|
||||
return useMemo(
|
||||
() => ({
|
||||
setPageMeta: (pageId: string, pageMeta: Partial<PageMeta>) => {
|
||||
blockSuiteWorkspace.meta.setPageMeta(pageId, pageMeta);
|
||||
},
|
||||
}),
|
||||
[blockSuiteWorkspace]
|
||||
);
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { EditorContainer } from '@blocksuite/editor';
|
||||
import { useEffect, useRef } from 'react';
|
||||
|
||||
import { useGlobalState } from '@/store/app';
|
||||
export type EventCallBack<T> = (callback: (props: T) => void) => void;
|
||||
|
||||
export type UsePropsUpdated = (
|
||||
editor?: EditorContainer
|
||||
) => EventCallBack<EditorContainer>;
|
||||
|
||||
export const usePropsUpdated: UsePropsUpdated = () => {
|
||||
const editor = useGlobalState(store => store.editor);
|
||||
|
||||
const callbackQueue = useRef<((editor: EditorContainer) => void)[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor) {
|
||||
return;
|
||||
}
|
||||
setTimeout(() => {
|
||||
editor.pageBlockModel?.propsUpdated.on(() => {
|
||||
callbackQueue.current.forEach(callback => {
|
||||
callback(editor);
|
||||
});
|
||||
});
|
||||
}, 300);
|
||||
|
||||
return () => {
|
||||
callbackQueue.current = [];
|
||||
editor?.pageBlockModel?.propsUpdated?.dispose();
|
||||
};
|
||||
}, [editor]);
|
||||
|
||||
return callback => {
|
||||
callbackQueue.current.push(callback);
|
||||
};
|
||||
};
|
||||
|
||||
export default usePropsUpdated;
|
||||
42
apps/web/src/hooks/use-router-helper.ts
Normal file
42
apps/web/src/hooks/use-router-helper.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import { NextRouter } from 'next/router';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { WorkspaceSubPath } from '../shared';
|
||||
|
||||
export const enum RouteLogic {
|
||||
REPLACE = 'replace',
|
||||
PUSH = 'push',
|
||||
}
|
||||
|
||||
export function useRouterHelper(router: NextRouter) {
|
||||
return useMemo(
|
||||
() => ({
|
||||
jumpToPage: (
|
||||
workspaceId: string,
|
||||
pageId: string,
|
||||
logic: RouteLogic = RouteLogic.PUSH
|
||||
) => {
|
||||
return router[logic]({
|
||||
pathname: `/workspace/[workspaceId]/[pageId]`,
|
||||
query: {
|
||||
workspaceId,
|
||||
pageId,
|
||||
},
|
||||
});
|
||||
},
|
||||
jumpToSubPath: (
|
||||
workspaceId: string,
|
||||
subPath: WorkspaceSubPath,
|
||||
logic: RouteLogic = RouteLogic.PUSH
|
||||
): Promise<boolean> => {
|
||||
return router[logic]({
|
||||
pathname: `/workspace/[workspaceId]/${subPath}`,
|
||||
query: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
},
|
||||
}),
|
||||
[router]
|
||||
);
|
||||
}
|
||||
26
apps/web/src/hooks/use-router-title.ts
Normal file
26
apps/web/src/hooks/use-router-title.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { NextRouter } from 'next/router';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { WorkspaceSubPathName } from '../shared';
|
||||
|
||||
export function useRouterTitle(router: NextRouter) {
|
||||
return useMemo(() => {
|
||||
if (!router.isReady) {
|
||||
return 'Loading...';
|
||||
} else {
|
||||
if (
|
||||
!router.query.pageId &&
|
||||
router.pathname.startsWith('/workspace/[workspaceId]/')
|
||||
) {
|
||||
const subPath = router.pathname.split('/').at(-1);
|
||||
if (subPath && subPath in WorkspaceSubPathName) {
|
||||
return (
|
||||
WorkspaceSubPathName[subPath as keyof typeof WorkspaceSubPathName] +
|
||||
' - AFFiNE'
|
||||
);
|
||||
}
|
||||
}
|
||||
return 'AFFiNE';
|
||||
}
|
||||
}, [router]);
|
||||
}
|
||||
@@ -0,0 +1,166 @@
|
||||
import { NextRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { RemWorkspace, RemWorkspaceFlavour } from '../shared';
|
||||
import { useCurrentPageId } from './current/use-current-page-id';
|
||||
import { useCurrentWorkspace } from './current/use-current-workspace';
|
||||
import { useWorkspaces, useWorkspacesIsLoaded } from './use-workspaces';
|
||||
|
||||
export function findSuitablePageId(
|
||||
workspace: RemWorkspace,
|
||||
targetId: string
|
||||
): string | null {
|
||||
switch (workspace.flavour) {
|
||||
case RemWorkspaceFlavour.AFFINE: {
|
||||
if (workspace.firstBinarySynced) {
|
||||
return (
|
||||
workspace.blockSuiteWorkspace.meta.pageMetas.find(
|
||||
page => page.id === targetId
|
||||
)?.id ??
|
||||
workspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id ??
|
||||
null
|
||||
);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
case RemWorkspaceFlavour.LOCAL: {
|
||||
return (
|
||||
workspace.blockSuiteWorkspace.meta.pageMetas.find(
|
||||
page => page.id === targetId
|
||||
)?.id ??
|
||||
workspace.blockSuiteWorkspace.meta.pageMetas.at(0)?.id ??
|
||||
null
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function useSyncRouterWithCurrentWorkspaceAndPage(router: NextRouter) {
|
||||
const [currentWorkspace, setCurrentWorkspaceId] = useCurrentWorkspace();
|
||||
const [currentPageId, setCurrentPageId] = useCurrentPageId();
|
||||
const workspaces = useWorkspaces();
|
||||
const isLoaded = useWorkspacesIsLoaded();
|
||||
useEffect(() => {
|
||||
const listener: Parameters<typeof router.events.on>[1] = (url: string) => {
|
||||
if (url.startsWith('/')) {
|
||||
const path = url.split('/');
|
||||
if (path.length === 4 && path[1] === 'workspace') {
|
||||
if (
|
||||
path[3] === 'all' ||
|
||||
path[3] === 'setting' ||
|
||||
path[3] === 'trash' ||
|
||||
path[3] === 'favorite'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
setCurrentWorkspaceId(path[2]);
|
||||
if (currentWorkspace && 'blockSuiteWorkspace' in currentWorkspace) {
|
||||
if (currentWorkspace.blockSuiteWorkspace.getPage(path[3])) {
|
||||
setCurrentPageId(path[3]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
router.events.on('routeChangeStart', listener);
|
||||
return () => {
|
||||
router.events.off('routeChangeStart', listener);
|
||||
};
|
||||
}, [currentWorkspace, router, setCurrentPageId, setCurrentWorkspaceId]);
|
||||
useEffect(() => {
|
||||
if (!router.isReady || !isLoaded) {
|
||||
return;
|
||||
}
|
||||
if (
|
||||
router.pathname === '/workspace/[workspaceId]/[pageId]' ||
|
||||
router.pathname === '/'
|
||||
) {
|
||||
const targetPageId = router.query.pageId;
|
||||
const targetWorkspaceId = router.query.workspaceId;
|
||||
if (currentWorkspace && currentPageId) {
|
||||
if (
|
||||
currentWorkspace.id === targetWorkspaceId &&
|
||||
currentPageId === targetPageId
|
||||
) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (
|
||||
typeof targetPageId !== 'string' ||
|
||||
typeof targetWorkspaceId !== 'string'
|
||||
) {
|
||||
if (router.asPath === '/') {
|
||||
const first = workspaces.at(0);
|
||||
if (first && 'blockSuiteWorkspace' in first) {
|
||||
const targetWorkspaceId = first.id;
|
||||
const targetPageId =
|
||||
first.blockSuiteWorkspace.meta.pageMetas.at(0)?.id;
|
||||
if (targetPageId) {
|
||||
setCurrentWorkspaceId(targetWorkspaceId);
|
||||
setCurrentPageId(targetPageId);
|
||||
router.push(`/workspace/${targetWorkspaceId}/${targetPageId}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!currentWorkspace) {
|
||||
const targetWorkspace = workspaces.find(
|
||||
workspace => workspace.id === targetPageId
|
||||
);
|
||||
if (targetWorkspace) {
|
||||
setCurrentWorkspaceId(targetWorkspace.id);
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
workspaceId: targetWorkspace.id,
|
||||
},
|
||||
});
|
||||
return;
|
||||
} else {
|
||||
const first = workspaces.at(0);
|
||||
if (first) {
|
||||
setCurrentWorkspaceId(first.id);
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
workspaceId: first.id,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (!currentPageId && currentWorkspace) {
|
||||
if ('blockSuiteWorkspace' in currentWorkspace) {
|
||||
const targetId = findSuitablePageId(currentWorkspace, targetPageId);
|
||||
if (targetId) {
|
||||
setCurrentPageId(targetId);
|
||||
router.push({
|
||||
query: {
|
||||
...router.query,
|
||||
workspaceId: currentWorkspace.id,
|
||||
pageId: targetId,
|
||||
},
|
||||
});
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [
|
||||
currentPageId,
|
||||
currentWorkspace,
|
||||
router.query.workspaceId,
|
||||
router.query.pageId,
|
||||
setCurrentPageId,
|
||||
setCurrentWorkspaceId,
|
||||
workspaces,
|
||||
router,
|
||||
isLoaded,
|
||||
]);
|
||||
}
|
||||
55
apps/web/src/hooks/use-sync-router-with-current-workspace.ts
Normal file
55
apps/web/src/hooks/use-sync-router-with-current-workspace.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { NextRouter } from 'next/router';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useCurrentWorkspace } from './current/use-current-workspace';
|
||||
import { useLoadWorkspace } from './use-load-workspace';
|
||||
import { useWorkspaces } from './use-workspaces';
|
||||
|
||||
export function useSyncRouterWithCurrentWorkspace(router: NextRouter) {
|
||||
const [currentWorkspace, setCurrentWorkspaceId] = useCurrentWorkspace();
|
||||
useLoadWorkspace(currentWorkspace);
|
||||
const workspaces = useWorkspaces();
|
||||
useEffect(() => {
|
||||
const listener: Parameters<typeof router.events.on>[1] = (url: string) => {
|
||||
if (url.startsWith('/')) {
|
||||
const path = url.split('/');
|
||||
if (path.length === 4 && path[1] === 'workspace') {
|
||||
setCurrentWorkspaceId(path[2]);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
router.events.on('routeChangeStart', listener);
|
||||
return () => {
|
||||
router.events.off('routeChangeStart', listener);
|
||||
};
|
||||
}, [currentWorkspace, router, setCurrentWorkspaceId]);
|
||||
useEffect(() => {
|
||||
if (!router.isReady) {
|
||||
return;
|
||||
}
|
||||
const workspaceId = router.query.workspaceId;
|
||||
if (typeof workspaceId !== 'string') {
|
||||
return;
|
||||
}
|
||||
if (!currentWorkspace) {
|
||||
const targetWorkspace = workspaces.find(
|
||||
workspace => workspace.id === workspaceId
|
||||
);
|
||||
if (targetWorkspace) {
|
||||
setCurrentWorkspaceId(targetWorkspace.id);
|
||||
} else {
|
||||
const targetWorkspace = workspaces.at(0);
|
||||
if (targetWorkspace) {
|
||||
setCurrentWorkspaceId(targetWorkspace.id);
|
||||
router.push({
|
||||
pathname: '/workspace/[workspaceId]/all',
|
||||
query: {
|
||||
workspaceId: targetWorkspace.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}, [currentWorkspace, router, setCurrentWorkspaceId, workspaces]);
|
||||
}
|
||||
21
apps/web/src/hooks/use-system-online.ts
Normal file
21
apps/web/src/hooks/use-system-online.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { useCallback, useSyncExternalStore } from 'react';
|
||||
|
||||
const getOnLineStatus = () =>
|
||||
typeof navigator !== 'undefined' && typeof navigator.onLine === 'boolean'
|
||||
? navigator.onLine
|
||||
: true;
|
||||
|
||||
export function useSystemOnline(): boolean {
|
||||
return useSyncExternalStore(
|
||||
useCallback(onStoreChange => {
|
||||
window.addEventListener('online', onStoreChange);
|
||||
window.addEventListener('offline', onStoreChange);
|
||||
return () => {
|
||||
window.removeEventListener('online', onStoreChange);
|
||||
window.removeEventListener('offline', onStoreChange);
|
||||
};
|
||||
}, []),
|
||||
useCallback(() => getOnLineStatus(), []),
|
||||
useCallback(() => true, [])
|
||||
);
|
||||
}
|
||||
35
apps/web/src/hooks/use-system-theme.ts
Normal file
35
apps/web/src/hooks/use-system-theme.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Theme } from '@affine/component';
|
||||
import { useCallback, useSyncExternalStore } from 'react';
|
||||
|
||||
const themeRef = {
|
||||
current: 'light',
|
||||
media: null,
|
||||
} as {
|
||||
current: Theme;
|
||||
media: MediaQueryList | null;
|
||||
};
|
||||
|
||||
if (typeof window !== 'undefined') {
|
||||
themeRef.media = window.matchMedia('(prefers-color-scheme: light)');
|
||||
}
|
||||
|
||||
export function useSystemTheme() {
|
||||
return useSyncExternalStore<Theme>(
|
||||
useCallback(onStoreChange => {
|
||||
if (themeRef.media) {
|
||||
const media = themeRef.media;
|
||||
media.addEventListener('change', onStoreChange);
|
||||
return () => {
|
||||
media.addEventListener('change', onStoreChange);
|
||||
};
|
||||
}
|
||||
return () => {};
|
||||
}, []),
|
||||
useCallback(
|
||||
() =>
|
||||
themeRef.media ? (themeRef.media.matches ? 'light' : 'dark') : 'light',
|
||||
[]
|
||||
),
|
||||
useCallback(() => 'light', [])
|
||||
);
|
||||
}
|
||||
30
apps/web/src/hooks/use-workspace-blob.ts
Normal file
30
apps/web/src/hooks/use-workspace-blob.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { BlobStorage } from '@blocksuite/store';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { BlockSuiteWorkspace } from '../shared';
|
||||
|
||||
export function useWorkspaceBlob(
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace
|
||||
): BlobStorage | null {
|
||||
const [blobStorage, setBlobStorage] = useState<BlobStorage | null>(null);
|
||||
useEffect(() => {
|
||||
blockSuiteWorkspace.blobs.then(blobStorage => {
|
||||
setBlobStorage(blobStorage);
|
||||
});
|
||||
}, [blockSuiteWorkspace]);
|
||||
return blobStorage;
|
||||
}
|
||||
|
||||
export function useWorkspaceBlobImage(
|
||||
key: string,
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace
|
||||
) {
|
||||
const blobStorage = useWorkspaceBlob(blockSuiteWorkspace);
|
||||
const [imageURL, setImageURL] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
blobStorage?.get(key).then(blob => {
|
||||
setImageURL(blob);
|
||||
});
|
||||
}, [blobStorage, key]);
|
||||
return imageURL;
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
import { WorkspaceUnit } from '@affine/datacenter';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useGlobalState } from '@/store/app';
|
||||
|
||||
export const useWorkspaceHelper = () => {
|
||||
const dataCenter = useGlobalState(store => store.dataCenter);
|
||||
const currentWorkspace = useGlobalState(
|
||||
useCallback(store => store.currentDataCenterWorkspace, [])
|
||||
);
|
||||
const loadWorkspace = useGlobalState(store => store.loadWorkspace);
|
||||
const createWorkspace = async (name: string) => {
|
||||
const workspaceInfo = await dataCenter.createWorkspace({
|
||||
name: name,
|
||||
});
|
||||
if (workspaceInfo && workspaceInfo.id) {
|
||||
return loadWorkspace(workspaceInfo.id);
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
// const updateWorkspace = async (workspace: Workspace) => {};
|
||||
|
||||
const publishWorkspace = async (workspaceId: string, publish: boolean) => {
|
||||
await dataCenter.setWorkspacePublish(workspaceId, publish);
|
||||
};
|
||||
|
||||
const updateWorkspace = async (
|
||||
{ name, avatarBlob }: { name?: string; avatarBlob?: Blob },
|
||||
workspace: WorkspaceUnit
|
||||
) => {
|
||||
if (name) {
|
||||
await dataCenter.updateWorkspaceMeta({ name }, workspace);
|
||||
}
|
||||
if (avatarBlob) {
|
||||
const blobId = await dataCenter.setBlob(workspace, avatarBlob);
|
||||
await dataCenter.updateWorkspaceMeta({ avatar: blobId }, workspace);
|
||||
}
|
||||
};
|
||||
|
||||
const deleteWorkSpace = async () => {
|
||||
currentWorkspace && (await dataCenter.deleteWorkspace(currentWorkspace.id));
|
||||
};
|
||||
const leaveWorkSpace = async () => {
|
||||
currentWorkspace && (await dataCenter.leaveWorkspace(currentWorkspace.id));
|
||||
};
|
||||
|
||||
const acceptInvite = async (inviteCode: string) => {
|
||||
return dataCenter.acceptInvitation(inviteCode);
|
||||
};
|
||||
|
||||
return {
|
||||
createWorkspace,
|
||||
publishWorkspace,
|
||||
updateWorkspace,
|
||||
deleteWorkSpace,
|
||||
leaveWorkSpace,
|
||||
acceptInvite,
|
||||
};
|
||||
};
|
||||
12
apps/web/src/hooks/use-workspace.ts
Normal file
12
apps/web/src/hooks/use-workspace.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { RemWorkspace } from '../shared';
|
||||
import { useWorkspaces } from './use-workspaces';
|
||||
|
||||
export function useWorkspace(workspaceId: string | null): RemWorkspace | null {
|
||||
const workspaces = useWorkspaces();
|
||||
return useMemo(
|
||||
() => workspaces.find(ws => ws.id === workspaceId) ?? null,
|
||||
[workspaces, workspaceId]
|
||||
);
|
||||
}
|
||||
195
apps/web/src/hooks/use-workspaces.ts
Normal file
195
apps/web/src/hooks/use-workspaces.ts
Normal file
@@ -0,0 +1,195 @@
|
||||
import { Workspace } from '@affine/datacenter';
|
||||
import { uuidv4 } from '@blocksuite/store';
|
||||
import { useCallback, useMemo, useSyncExternalStore } from 'react';
|
||||
import useSWR from 'swr';
|
||||
import { IndexeddbPersistence } from 'y-indexeddb';
|
||||
|
||||
import { lockMutex } from '../atoms';
|
||||
import { createLocalProviders } from '../blocksuite';
|
||||
import { WorkspacePlugins } from '../plugins';
|
||||
import { QueryKey } from '../plugins/affine/fetcher';
|
||||
import { kStoreKey } from '../plugins/local';
|
||||
import { LocalWorkspace, RemWorkspace, RemWorkspaceFlavour } from '../shared';
|
||||
import { config } from '../shared/env';
|
||||
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 = uuidv4();
|
||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(id);
|
||||
blockSuiteWorkspace.meta.setName(name);
|
||||
const workspace: LocalWorkspace = {
|
||||
flavour: RemWorkspaceFlavour.LOCAL,
|
||||
blockSuiteWorkspace: blockSuiteWorkspace,
|
||||
providers: [...createLocalProviders(blockSuiteWorkspace)],
|
||||
syncBinary: async () => {
|
||||
if (!config.enableIndexedDBProvider) {
|
||||
return {
|
||||
...workspace,
|
||||
};
|
||||
}
|
||||
const persistence = new IndexeddbPersistence(
|
||||
blockSuiteWorkspace.room as string,
|
||||
blockSuiteWorkspace.doc
|
||||
);
|
||||
return persistence.whenSynced.then(() => {
|
||||
persistence.destroy();
|
||||
return {
|
||||
...workspace,
|
||||
};
|
||||
});
|
||||
},
|
||||
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());
|
||||
// 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[] {
|
||||
return useSyncExternalStore(
|
||||
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() {
|
||||
return useMemo(
|
||||
() => ({
|
||||
createWorkspacePage: (workspaceId: string, pageId: string) => {
|
||||
const workspace = dataCenter.workspaces.find(
|
||||
ws => ws.id === workspaceId
|
||||
) as LocalWorkspace;
|
||||
if (workspace && 'blockSuiteWorkspace' in workspace) {
|
||||
workspace.blockSuiteWorkspace.createPage(pageId);
|
||||
} else {
|
||||
throw new Error('cannot create page. blockSuiteWorkspace not found');
|
||||
}
|
||||
},
|
||||
createRemLocalWorkspace,
|
||||
deleteWorkspace,
|
||||
}),
|
||||
[]
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user