mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-03 02:20:19 +08:00
refactor!: remove next.js (#3267)
This commit is contained in:
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
# AFFiNE Hooks
|
||||
|
||||
> This directory will be moved to `@affine/worksapce/affine/hooks` in the future.
|
||||
|
||||
Only put hooks in this directory if they are specific to AFFiNE, for example
|
||||
if they are using the AFFiNE API, or if the `AffineWorkspace` is required.
|
||||
@@ -0,0 +1,126 @@
|
||||
import {
|
||||
useBlockSuitePageMeta,
|
||||
usePageMetaHelper,
|
||||
} from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import type { BlockSuiteWorkspace } from '../../shared';
|
||||
import { useReferenceLinkHelper } from './use-reference-link-helper';
|
||||
|
||||
export function useBlockSuiteMetaHelper(
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace
|
||||
) {
|
||||
const { setPageMeta, getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
|
||||
const { addReferenceLink } = useReferenceLinkHelper(blockSuiteWorkspace);
|
||||
const metas = useBlockSuitePageMeta(blockSuiteWorkspace);
|
||||
|
||||
const addToFavorite = useCallback(
|
||||
(pageId: string) => {
|
||||
setPageMeta(pageId, {
|
||||
favorite: true,
|
||||
});
|
||||
},
|
||||
[setPageMeta]
|
||||
);
|
||||
const removeFromFavorite = useCallback(
|
||||
(pageId: string) => {
|
||||
setPageMeta(pageId, {
|
||||
favorite: false,
|
||||
});
|
||||
},
|
||||
[setPageMeta]
|
||||
);
|
||||
const toggleFavorite = useCallback(
|
||||
(pageId: string) => {
|
||||
const { favorite } = getPageMeta(pageId) ?? {};
|
||||
setPageMeta(pageId, {
|
||||
favorite: !favorite,
|
||||
});
|
||||
},
|
||||
[getPageMeta, setPageMeta]
|
||||
);
|
||||
|
||||
// TODO-Doma
|
||||
// "Remove" may cause ambiguity here. Consider renaming as "moveToTrash".
|
||||
const removeToTrash = useCallback(
|
||||
(pageId: string, isRoot = true) => {
|
||||
const parentMeta = metas.find(m => m.subpageIds?.includes(pageId));
|
||||
const { subpageIds = [] } = getPageMeta(pageId) ?? {};
|
||||
|
||||
subpageIds.forEach(id => {
|
||||
removeToTrash(id, false);
|
||||
});
|
||||
|
||||
setPageMeta(pageId, {
|
||||
trash: true,
|
||||
trashDate: +new Date(),
|
||||
trashRelate: isRoot ? parentMeta?.id : undefined,
|
||||
});
|
||||
},
|
||||
[getPageMeta, metas, setPageMeta]
|
||||
);
|
||||
|
||||
const restoreFromTrash = useCallback(
|
||||
(pageId: string) => {
|
||||
const { subpageIds = [], trashRelate } = getPageMeta(pageId) ?? {};
|
||||
|
||||
if (trashRelate) {
|
||||
addReferenceLink(trashRelate, pageId);
|
||||
}
|
||||
|
||||
setPageMeta(pageId, {
|
||||
trash: false,
|
||||
trashDate: undefined,
|
||||
trashRelate: undefined,
|
||||
});
|
||||
subpageIds.forEach(id => {
|
||||
restoreFromTrash(id);
|
||||
});
|
||||
},
|
||||
[addReferenceLink, getPageMeta, setPageMeta]
|
||||
);
|
||||
|
||||
const permanentlyDeletePage = useCallback(
|
||||
(pageId: string) => {
|
||||
blockSuiteWorkspace.removePage(pageId);
|
||||
},
|
||||
[blockSuiteWorkspace]
|
||||
);
|
||||
|
||||
/**
|
||||
* see {@link useBlockSuiteWorkspacePageIsPublic}
|
||||
*/
|
||||
const publicPage = useCallback(
|
||||
(pageId: string) => {
|
||||
setPageMeta(pageId, {
|
||||
isPublic: true,
|
||||
});
|
||||
},
|
||||
[setPageMeta]
|
||||
);
|
||||
|
||||
/**
|
||||
* see {@link useBlockSuiteWorkspacePageIsPublic}
|
||||
*/
|
||||
const cancelPublicPage = useCallback(
|
||||
(pageId: string) => {
|
||||
setPageMeta(pageId, {
|
||||
isPublic: false,
|
||||
});
|
||||
},
|
||||
[setPageMeta]
|
||||
);
|
||||
|
||||
return {
|
||||
publicPage,
|
||||
cancelPublicPage,
|
||||
|
||||
addToFavorite,
|
||||
removeFromFavorite,
|
||||
toggleFavorite,
|
||||
|
||||
removeToTrash,
|
||||
restoreFromTrash,
|
||||
permanentlyDeletePage,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import type { BlockSuiteWorkspace } from '../../shared';
|
||||
|
||||
export function useReferenceLinkHelper(
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace
|
||||
) {
|
||||
const addReferenceLink = useCallback(
|
||||
(pageId: string, referenceId: string) => {
|
||||
const page = blockSuiteWorkspace?.getPage(pageId);
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
const text = page.Text.fromDelta([
|
||||
{
|
||||
insert: ' ',
|
||||
attributes: {
|
||||
reference: {
|
||||
type: 'Subpage',
|
||||
pageId: referenceId,
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
const [frame] = page.getBlockByFlavour('affine:note');
|
||||
|
||||
frame && page.addBlock('affine:paragraph', { text }, frame.id);
|
||||
},
|
||||
[blockSuiteWorkspace]
|
||||
);
|
||||
|
||||
return {
|
||||
addReferenceLink,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
interface ShortcutTip {
|
||||
[x: string]: string;
|
||||
}
|
||||
|
||||
export const useWinGeneralKeyboardShortcuts = (): ShortcutTip => {
|
||||
const t = useAFFiNEI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[t['Cancel']()]: 'ESC',
|
||||
[t['Quick Search']()]: 'Ctrl + K',
|
||||
[t['New Page']()]: 'Ctrl + N',
|
||||
// not implement yet
|
||||
// [t['Append to Daily Note']()]: 'Ctrl + Alt + A',
|
||||
[t['Expand/Collapse Sidebar']()]: 'Ctrl + /',
|
||||
// not implement yet
|
||||
// [t['Go Back']()]: 'Ctrl + [',
|
||||
// [t['Go Forward']()]: 'Ctrl + ]',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
export const useMacGeneralKeyboardShortcuts = (): ShortcutTip => {
|
||||
const t = useAFFiNEI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[t['Cancel']()]: 'ESC',
|
||||
[t['Quick Search']()]: '⌘ + K',
|
||||
[t['New Page']()]: '⌘ + N',
|
||||
// not implement yet
|
||||
// [t['Append to Daily Note']()]: '⌘ + ⌥ + A',
|
||||
[t['Expand/Collapse Sidebar']()]: '⌘ + /',
|
||||
// not implement yet
|
||||
// [t['Go Back']()]: '⌘ + [',
|
||||
// [t['Go Forward']()]: '⌘ + ]',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
|
||||
export const useMacEdgelessKeyboardShortcuts = (): ShortcutTip => {
|
||||
const t = useAFFiNEI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[t['Select All']()]: '⌘ + A',
|
||||
[t['Undo']()]: '⌘ + Z',
|
||||
[t['Redo']()]: '⌘ + ⇧ + Z',
|
||||
[t['Zoom in']()]: '⌘ + +',
|
||||
[t['Zoom out']()]: '⌘ + -',
|
||||
[t['Zoom to 100%']()]: '⌘ + 0',
|
||||
[t['Zoom to fit']()]: '⌘ + 1',
|
||||
[t['Select']()]: 'V',
|
||||
[t['Text']()]: 'T',
|
||||
[t['Shape']()]: 'S',
|
||||
[t['Image']()]: 'I',
|
||||
[t['Straight Connector']()]: 'L',
|
||||
[t['Elbowed Connector']()]: 'X',
|
||||
// not implement yet
|
||||
// [t['Curve Connector']()]: 'C',
|
||||
[t['Pen']()]: 'P',
|
||||
[t['Hand']()]: 'H',
|
||||
[t['Note']()]: 'N',
|
||||
// not implement yet
|
||||
// [t['Group']()]: '⌘ + G',
|
||||
// [t['Ungroup']()]: '⌘ + ⇧ + G',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
export const useWinEdgelessKeyboardShortcuts = (): ShortcutTip => {
|
||||
const t = useAFFiNEI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[t['Select All']()]: 'Ctrl + A',
|
||||
[t['Undo']()]: 'Ctrl + Z',
|
||||
[t['Redo']()]: 'Ctrl + Y/Ctrl + Shift + Z',
|
||||
[t['Zoom in']()]: 'Ctrl + +',
|
||||
[t['Zoom out']()]: 'Ctrl + -',
|
||||
[t['Zoom to 100%']()]: 'Ctrl + 0',
|
||||
[t['Zoom to fit']()]: 'Ctrl + 1',
|
||||
[t['Select']()]: 'V',
|
||||
[t['Text']()]: 'T',
|
||||
[t['Shape']()]: 'S',
|
||||
[t['Image']()]: 'I',
|
||||
[t['Straight Connector']()]: 'L',
|
||||
[t['Elbowed Connector']()]: 'X',
|
||||
// not implement yet
|
||||
// [t['Curve Connector']()]: 'C',
|
||||
[t['Pen']()]: 'P',
|
||||
[t['Hand']()]: 'H',
|
||||
[t['Note']()]: 'N',
|
||||
[t['Switch']()]: 'Alt + S',
|
||||
// not implement yet
|
||||
// [t['Group']()]: 'Ctrl + G',
|
||||
// [t['Ungroup']()]: 'Ctrl + Shift + G',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
export const useMacPageKeyboardShortcuts = (): ShortcutTip => {
|
||||
const t = useAFFiNEI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[t['Undo']()]: '⌘+Z',
|
||||
[t['Redo']()]: '⌘+⇧+Z',
|
||||
[t['Bold']()]: '⌘+B',
|
||||
[t['Italic']()]: '⌘+I',
|
||||
[t['Underline']()]: '⌘+U',
|
||||
[t['Strikethrough']()]: '⌘+⇧+S',
|
||||
[t['Inline code']()]: ' ⌘+E',
|
||||
[t['Code block']()]: '⌘+⌥+C',
|
||||
[t['Link']()]: '⌘+K',
|
||||
[t['Quick search']()]: '⌘+K',
|
||||
[t['Body text']()]: '⌘+⌥+0',
|
||||
[t['Heading']({ number: '1' })]: '⌘+⌥+1',
|
||||
[t['Heading']({ number: '2' })]: '⌘+⌥+2',
|
||||
[t['Heading']({ number: '3' })]: '⌘+⌥+3',
|
||||
[t['Heading']({ number: '4' })]: '⌘+⌥+4',
|
||||
[t['Heading']({ number: '5' })]: '⌘+⌥+5',
|
||||
[t['Heading']({ number: '6' })]: '⌘+⌥+6',
|
||||
[t['Increase indent']()]: 'Tab',
|
||||
[t['Reduce indent']()]: '⇧+Tab',
|
||||
[t['Group as Database']()]: '⌘ + G',
|
||||
[t['Switch']()]: '⌥ + S',
|
||||
// not implement yet
|
||||
// [t['Move Up']()]: '⌘ + ⌥ + ↑',
|
||||
// [t['Move Down']()]: '⌘ + ⌥ + ↓',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
|
||||
export const useMacMarkdownShortcuts = (): ShortcutTip => {
|
||||
const t = useAFFiNEI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[t['Bold']()]: '**Text** ',
|
||||
[t['Italic']()]: '*Text* ',
|
||||
[t['Underline']()]: '~Text~ ',
|
||||
[t['Strikethrough']()]: '~~Text~~ ',
|
||||
[t['Divider']()]: '***',
|
||||
[t['Inline code']()]: '`Text` ',
|
||||
[t['Code block']()]: '``` Space',
|
||||
[t['Heading']({ number: '1' })]: '# Text',
|
||||
[t['Heading']({ number: '2' })]: '## Text',
|
||||
[t['Heading']({ number: '3' })]: '### Text',
|
||||
[t['Heading']({ number: '4' })]: '#### Text',
|
||||
[t['Heading']({ number: '5' })]: '##### Text',
|
||||
[t['Heading']({ number: '6' })]: '###### Text',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
|
||||
export const useWinPageKeyboardShortcuts = (): ShortcutTip => {
|
||||
const t = useAFFiNEI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[t['Undo']()]: 'Ctrl+Z',
|
||||
[t['Redo']()]: 'Ctrl+Y',
|
||||
[t['Bold']()]: 'Ctrl+B',
|
||||
[t['Italic']()]: 'Ctrl+I',
|
||||
[t['Underline']()]: 'Ctrl+U',
|
||||
[t['Strikethrough']()]: 'Ctrl+Shift+S',
|
||||
[t['Inline code']()]: ' Ctrl+E',
|
||||
[t['Code block']()]: 'Ctrl+Alt+C',
|
||||
[t['Link']()]: 'Ctrl+K',
|
||||
[t['Quick search']()]: 'Ctrl+K',
|
||||
[t['Body text']()]: 'Ctrl+Shift+0',
|
||||
[t['Heading']({ number: '1' })]: 'Ctrl+Shift+1',
|
||||
[t['Heading']({ number: '2' })]: 'Ctrl+Shift+2',
|
||||
[t['Heading']({ number: '3' })]: 'Ctrl+Shift+3',
|
||||
[t['Heading']({ number: '4' })]: 'Ctrl+Shift+4',
|
||||
[t['Heading']({ number: '5' })]: 'Ctrl+Shift+5',
|
||||
[t['Heading']({ number: '6' })]: 'Ctrl+Shift+6',
|
||||
[t['Increase indent']()]: 'Tab',
|
||||
[t['Reduce indent']()]: 'Shift+Tab',
|
||||
[t['Group as Database']()]: 'Ctrl + G',
|
||||
['Switch']: 'Alt + S',
|
||||
// not implement yet
|
||||
// [t['Move Up']()]: 'Ctrl + Alt + ↑',
|
||||
// [t['Move Down']()]: 'Ctrl + Alt + ↓',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
export const useWinMarkdownShortcuts = (): ShortcutTip => {
|
||||
const t = useAFFiNEI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[t['Bold']()]: '**Text** ',
|
||||
[t['Italic']()]: '*Text* ',
|
||||
[t['Underline']()]: '~Text~ ',
|
||||
[t['Strikethrough']()]: '~~Text~~ ',
|
||||
[t['Divider']()]: '***',
|
||||
[t['Inline code']()]: '`Text` ',
|
||||
[t['Code block']()]: '``` Text',
|
||||
[t['Heading']({ number: '1' })]: '# Text',
|
||||
[t['Heading']({ number: '2' })]: '## Text',
|
||||
[t['Heading']({ number: '3' })]: '### Text',
|
||||
[t['Heading']({ number: '4' })]: '#### Text',
|
||||
[t['Heading']({ number: '5' })]: '##### Text',
|
||||
[t['Heading']({ number: '6' })]: '###### Text',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
|
||||
export const useMarkdownShortcuts = (): ShortcutTip => {
|
||||
const macMarkdownShortcuts = useMacMarkdownShortcuts();
|
||||
const winMarkdownShortcuts = useWinMarkdownShortcuts();
|
||||
const isMac = environment.isBrowser && environment.isMacOs;
|
||||
return isMac ? macMarkdownShortcuts : winMarkdownShortcuts;
|
||||
};
|
||||
|
||||
export const usePageShortcuts = (): ShortcutTip => {
|
||||
const macPageShortcuts = useMacPageKeyboardShortcuts();
|
||||
const winPageShortcuts = useWinPageKeyboardShortcuts();
|
||||
const isMac = environment.isBrowser && environment.isMacOs;
|
||||
return isMac ? macPageShortcuts : winPageShortcuts;
|
||||
};
|
||||
|
||||
export const useEdgelessShortcuts = (): ShortcutTip => {
|
||||
const macEdgelessShortcuts = useMacEdgelessKeyboardShortcuts();
|
||||
const winEdgelessShortcuts = useWinEdgelessKeyboardShortcuts();
|
||||
const isMac = environment.isBrowser && environment.isMacOs;
|
||||
|
||||
return isMac ? macEdgelessShortcuts : winEdgelessShortcuts;
|
||||
};
|
||||
|
||||
export const useGeneralShortcuts = (): ShortcutTip => {
|
||||
const macGeneralShortcuts = useMacGeneralKeyboardShortcuts();
|
||||
const winGeneralShortcuts = useWinGeneralKeyboardShortcuts();
|
||||
const isMac = environment.isBrowser && environment.isMacOs;
|
||||
|
||||
return isMac ? macGeneralShortcuts : winGeneralShortcuts;
|
||||
};
|
||||
@@ -0,0 +1,52 @@
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
} from '@toeverything/plugin-infra/manager';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import type { AllWorkspace } from '../../shared';
|
||||
import { useWorkspace } from '../use-workspace';
|
||||
|
||||
declare global {
|
||||
/**
|
||||
* @internal debug only
|
||||
*/
|
||||
// eslint-disable-next-line no-var
|
||||
var currentWorkspace: AllWorkspace | undefined;
|
||||
interface WindowEventMap {
|
||||
'affine:workspace:change': CustomEvent<{ id: string }>;
|
||||
}
|
||||
}
|
||||
|
||||
export function useCurrentWorkspace(): [
|
||||
AllWorkspace,
|
||||
(id: string | null) => void,
|
||||
] {
|
||||
const [id, setId] = useAtom(currentWorkspaceIdAtom);
|
||||
assertExists(id);
|
||||
const currentWorkspace = useWorkspace(id);
|
||||
useEffect(() => {
|
||||
globalThis.currentWorkspace = currentWorkspace;
|
||||
globalThis.dispatchEvent(
|
||||
new CustomEvent('affine:workspace:change', {
|
||||
detail: { id: currentWorkspace.id },
|
||||
})
|
||||
);
|
||||
}, [currentWorkspace]);
|
||||
const setPageId = useSetAtom(currentPageIdAtom);
|
||||
return [
|
||||
currentWorkspace,
|
||||
useCallback(
|
||||
(id: string | null) => {
|
||||
if (environment.isBrowser && id) {
|
||||
localStorage.setItem('last_workspace_id', id);
|
||||
}
|
||||
setPageId(null);
|
||||
setId(id);
|
||||
},
|
||||
[setId, setPageId]
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
import type { WorkspaceRegistry } from '@affine/env/workspace';
|
||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { currentPageIdAtom } from '@toeverything/plugin-infra/manager';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useTransformWorkspace } from '../use-transform-workspace';
|
||||
|
||||
export function useOnTransformWorkspace() {
|
||||
const transformWorkspace = useTransformWorkspace();
|
||||
const setWorkspaceId = useSetAtom(currentPageIdAtom);
|
||||
return useCallback(
|
||||
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
|
||||
from: From,
|
||||
to: To,
|
||||
workspace: WorkspaceRegistry[From]
|
||||
): Promise<void> => {
|
||||
const workspaceId = await transformWorkspace(from, to, workspace);
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('affine-workspace:transform', {
|
||||
detail: {
|
||||
from,
|
||||
to,
|
||||
oldId: workspace.id,
|
||||
newId: workspaceId,
|
||||
},
|
||||
})
|
||||
);
|
||||
setWorkspaceId(workspaceId);
|
||||
},
|
||||
[setWorkspaceId, transformWorkspace]
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
// global Events
|
||||
interface WindowEventMap {
|
||||
'affine-workspace:transform': CustomEvent<{
|
||||
from: WorkspaceFlavour;
|
||||
to: WorkspaceFlavour;
|
||||
oldId: string;
|
||||
newId: string;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import type { GetPageInfoById } from '@affine/env/page-info';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { useBlockSuitePageMeta } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useAtomValue } from 'jotai';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
import { pageSettingsAtom } from '../atoms';
|
||||
|
||||
export const useGetPageInfoById = (workspace: Workspace): GetPageInfoById => {
|
||||
const pageMetas = useBlockSuitePageMeta(workspace);
|
||||
const pageMap = useMemo(
|
||||
() => Object.fromEntries(pageMetas.map(page => [page.id, page])),
|
||||
[pageMetas]
|
||||
);
|
||||
const pageSettings = useAtomValue(pageSettingsAtom);
|
||||
return useCallback(
|
||||
(id: string) => {
|
||||
const page = pageMap[id];
|
||||
if (!page) {
|
||||
return;
|
||||
}
|
||||
return {
|
||||
...page,
|
||||
isEdgeless: pageSettings[id]?.mode === 'edgeless',
|
||||
};
|
||||
},
|
||||
[pageMap, pageSettings]
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,4 @@
|
||||
// todo
|
||||
export function useLocationTitle(): string {
|
||||
return 'AFFiNE';
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
import type { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useCallback } from 'react';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
|
||||
export enum RouteLogic {
|
||||
REPLACE = 'replace',
|
||||
PUSH = 'push',
|
||||
}
|
||||
|
||||
export function useNavigateHelper() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const jumpToPage = useCallback(
|
||||
(
|
||||
workspaceId: string,
|
||||
pageId: string,
|
||||
logic: RouteLogic = RouteLogic.PUSH
|
||||
) => {
|
||||
return navigate(`/workspace/${workspaceId}/${pageId}`, {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
const jumpToPublicWorkspacePage = useCallback(
|
||||
(
|
||||
workspaceId: string,
|
||||
pageId: string,
|
||||
logic: RouteLogic = RouteLogic.PUSH
|
||||
) => {
|
||||
return navigate(`/public-workspace/${workspaceId}/${pageId}`, {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
const jumpToSubPath = useCallback(
|
||||
(
|
||||
workspaceId: string,
|
||||
subPath: WorkspaceSubPath,
|
||||
logic: RouteLogic = RouteLogic.PUSH
|
||||
) => {
|
||||
return navigate(`/workspace/${workspaceId}/${subPath}`, {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
const openPage = useCallback(
|
||||
(workspaceId: string, pageId: string) => {
|
||||
const isPublicWorkspace =
|
||||
location.pathname.indexOf('/public-workspace') === 0;
|
||||
if (isPublicWorkspace) {
|
||||
return jumpToPublicWorkspacePage(workspaceId, pageId);
|
||||
} else {
|
||||
return jumpToPage(workspaceId, pageId);
|
||||
}
|
||||
},
|
||||
[jumpToPage, jumpToPublicWorkspacePage, location.pathname]
|
||||
);
|
||||
|
||||
const jumpToIndex = useCallback(
|
||||
(logic: RouteLogic = RouteLogic.PUSH) => {
|
||||
return navigate('/', {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const jumpTo404 = useCallback(
|
||||
(logic: RouteLogic = RouteLogic.PUSH) => {
|
||||
return navigate('/404', {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
return {
|
||||
jumpToPage,
|
||||
jumpToPublicWorkspacePage,
|
||||
jumpToSubPath,
|
||||
jumpToIndex,
|
||||
jumpTo404,
|
||||
openPage,
|
||||
};
|
||||
}
|
||||
@@ -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, [])
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import type { WorkspaceRegistry } from '@affine/env/workspace';
|
||||
import { WorkspaceVersion } from '@affine/env/workspace';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||
|
||||
/**
|
||||
* Transform workspace from one flavor to another
|
||||
*
|
||||
* The logic here is to delete the old workspace and create a new one.
|
||||
*/
|
||||
export function useTransformWorkspace() {
|
||||
const set = useSetAtom(rootWorkspacesMetadataAtom);
|
||||
return useCallback(
|
||||
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
|
||||
from: From,
|
||||
to: To,
|
||||
workspace: WorkspaceRegistry[From]
|
||||
): Promise<string> => {
|
||||
// create first, then delete, in case of failure
|
||||
const newId = await WorkspaceAdapters[to].CRUD.create(
|
||||
workspace.blockSuiteWorkspace
|
||||
);
|
||||
await WorkspaceAdapters[from].CRUD.delete(workspace as any);
|
||||
await set(workspaces => {
|
||||
const idx = workspaces.findIndex(ws => ws.id === workspace.id);
|
||||
workspaces.splice(idx, 1, {
|
||||
id: newId,
|
||||
flavour: to,
|
||||
version: WorkspaceVersion.SubDoc,
|
||||
});
|
||||
return [...workspaces];
|
||||
});
|
||||
return newId;
|
||||
},
|
||||
[set]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import type { BlobManager } from '@blocksuite/store';
|
||||
import { useEffect, useMemo, useRef, useState } from 'react';
|
||||
|
||||
import type { BlockSuiteWorkspace } from '../shared';
|
||||
|
||||
const logger = new DebugLogger('useWorkspaceBlob');
|
||||
|
||||
export function useWorkspaceBlob(
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace
|
||||
): BlobManager {
|
||||
return useMemo(() => blockSuiteWorkspace.blobs, [blockSuiteWorkspace.blobs]);
|
||||
}
|
||||
|
||||
export function useWorkspaceBlobImage(
|
||||
key: string | null,
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace
|
||||
) {
|
||||
const blobManager = useWorkspaceBlob(blockSuiteWorkspace);
|
||||
const [blob, setBlob] = useState<Blob | null>(null);
|
||||
useEffect(() => {
|
||||
const controller = new AbortController();
|
||||
if (key === null) {
|
||||
setBlob(null);
|
||||
return;
|
||||
}
|
||||
blobManager
|
||||
?.get(key)
|
||||
.then(blob => {
|
||||
if (controller.signal.aborted) {
|
||||
return;
|
||||
}
|
||||
if (blob) {
|
||||
setBlob(blob);
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
logger.error('Failed to get blob', err);
|
||||
});
|
||||
return () => {
|
||||
controller.abort();
|
||||
};
|
||||
}, [blobManager, key]);
|
||||
const [url, setUrl] = useState<string | null>(null);
|
||||
const ref = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (ref.current) {
|
||||
URL.revokeObjectURL(ref.current);
|
||||
}
|
||||
if (blob) {
|
||||
const url = URL.createObjectURL(blob);
|
||||
setUrl(url);
|
||||
ref.current = url;
|
||||
}
|
||||
}, [blob]);
|
||||
return url;
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { useStaticBlockSuiteWorkspace } from '@toeverything/plugin-infra/__internal__/react';
|
||||
import type { Atom } from 'jotai';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
|
||||
import type { AffineOfficialWorkspace } from '../shared';
|
||||
|
||||
const workspaceWeakMap = new WeakMap<
|
||||
Workspace,
|
||||
Atom<Promise<AffineOfficialWorkspace>>
|
||||
>();
|
||||
|
||||
export function useWorkspace(workspaceId: string): AffineOfficialWorkspace {
|
||||
const blockSuiteWorkspace = useStaticBlockSuiteWorkspace(workspaceId);
|
||||
if (!workspaceWeakMap.has(blockSuiteWorkspace)) {
|
||||
const baseAtom = atom(async get => {
|
||||
const metadata = await get(rootWorkspacesMetadataAtom);
|
||||
const flavour = metadata.find(({ id }) => id === workspaceId)?.flavour;
|
||||
assertExists(flavour);
|
||||
return {
|
||||
id: workspaceId,
|
||||
flavour,
|
||||
blockSuiteWorkspace,
|
||||
};
|
||||
});
|
||||
workspaceWeakMap.set(blockSuiteWorkspace, baseAtom);
|
||||
}
|
||||
|
||||
return useAtomValue(
|
||||
workspaceWeakMap.get(blockSuiteWorkspace) as Atom<
|
||||
Promise<AffineOfficialWorkspace>
|
||||
>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { initEmptyPage, initPageWithPreloading } from '@affine/env/blocksuite';
|
||||
import { DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX } from '@affine/env/constant';
|
||||
import { WorkspaceFlavour, WorkspaceVersion } from '@affine/env/workspace';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { saveWorkspaceToLocalStorage } from '@affine/workspace/local/crud';
|
||||
import { createEmptyBlockSuiteWorkspace } from '@affine/workspace/utils';
|
||||
import { assertEquals } from '@blocksuite/global/utils';
|
||||
import { nanoid } from '@blocksuite/store';
|
||||
import { getWorkspace } from '@toeverything/plugin-infra/__internal__/workspace';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { LocalAdapter } from '../adapters/local';
|
||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||
|
||||
const logger = new DebugLogger('use-workspaces');
|
||||
|
||||
/**
|
||||
* This hook has the permission to all workspaces. Be careful when using it.
|
||||
*/
|
||||
export function useAppHelper() {
|
||||
const jotaiWorkspaces = useAtomValue(rootWorkspacesMetadataAtom);
|
||||
const set = useSetAtom(rootWorkspacesMetadataAtom);
|
||||
return {
|
||||
addLocalWorkspace: useCallback(
|
||||
async (workspaceId: string): Promise<string> => {
|
||||
createEmptyBlockSuiteWorkspace(workspaceId, WorkspaceFlavour.LOCAL);
|
||||
saveWorkspaceToLocalStorage(workspaceId);
|
||||
await set(workspaces => [
|
||||
...workspaces,
|
||||
{
|
||||
id: workspaceId,
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
version: WorkspaceVersion.SubDoc,
|
||||
},
|
||||
]);
|
||||
logger.debug('imported local workspace', workspaceId);
|
||||
return workspaceId;
|
||||
},
|
||||
[set]
|
||||
),
|
||||
createLocalWorkspace: useCallback(
|
||||
async (name: string): Promise<string> => {
|
||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||
nanoid(),
|
||||
WorkspaceFlavour.LOCAL
|
||||
);
|
||||
blockSuiteWorkspace.meta.setName(name);
|
||||
const id = await LocalAdapter.CRUD.create(blockSuiteWorkspace);
|
||||
{
|
||||
// this is hack, because CRUD doesn't return the workspace
|
||||
const blockSuiteWorkspace = createEmptyBlockSuiteWorkspace(
|
||||
id,
|
||||
WorkspaceFlavour.LOCAL
|
||||
);
|
||||
const pageId = `${blockSuiteWorkspace.id}-${DEFAULT_HELLO_WORLD_PAGE_ID_SUFFIX}`;
|
||||
const page = blockSuiteWorkspace.createPage({
|
||||
id: pageId,
|
||||
});
|
||||
assertEquals(page.id, pageId);
|
||||
if (runtimeConfig.enablePreloading) {
|
||||
await initPageWithPreloading(page).catch(error => {
|
||||
console.error('import error:', error);
|
||||
});
|
||||
} else {
|
||||
await initEmptyPage(page).catch(error => {
|
||||
console.error('init empty page error', error);
|
||||
});
|
||||
}
|
||||
blockSuiteWorkspace.setPageMeta(page.id, {
|
||||
jumpOnce: true,
|
||||
});
|
||||
}
|
||||
await set(workspaces => [
|
||||
...workspaces,
|
||||
{
|
||||
id,
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
version: WorkspaceVersion.SubDoc,
|
||||
},
|
||||
]);
|
||||
logger.debug('created local workspace', id);
|
||||
return id;
|
||||
},
|
||||
[set]
|
||||
),
|
||||
deleteWorkspace: useCallback(
|
||||
async (workspaceId: string) => {
|
||||
const targetJotaiWorkspace = jotaiWorkspaces.find(
|
||||
ws => ws.id === workspaceId
|
||||
);
|
||||
if (!targetJotaiWorkspace) {
|
||||
throw new Error('page cannot be found');
|
||||
}
|
||||
|
||||
const targetWorkspace = getWorkspace(targetJotaiWorkspace.id);
|
||||
|
||||
// delete workspace from plugin
|
||||
await WorkspaceAdapters[targetJotaiWorkspace.flavour].CRUD.delete(
|
||||
targetWorkspace
|
||||
);
|
||||
// delete workspace from jotai storage
|
||||
await set(workspaces => workspaces.filter(ws => ws.id !== workspaceId));
|
||||
},
|
||||
[jotaiWorkspaces, set]
|
||||
),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user