mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-15 21:41:52 +08:00
refactor(infra): directory structure (#4615)
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);
|
||||
});
|
||||
});
|
||||
6
packages/frontend/core/src/hooks/affine/README.md
Normal file
6
packages/frontend/core/src/hooks/affine/README.md
Normal file
@@ -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,157 @@
|
||||
import {
|
||||
useBlockSuitePageMeta,
|
||||
usePageMetaHelper,
|
||||
} from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { setPageModeAtom } from '../../atoms';
|
||||
import { currentModeAtom } from '../../atoms/mode';
|
||||
import type { BlockSuiteWorkspace } from '../../shared';
|
||||
import { useReferenceLinkHelper } from './use-reference-link-helper';
|
||||
|
||||
export function useBlockSuiteMetaHelper(
|
||||
blockSuiteWorkspace: BlockSuiteWorkspace
|
||||
) {
|
||||
const { setPageMeta, getPageMeta, setPageReadonly } =
|
||||
usePageMetaHelper(blockSuiteWorkspace);
|
||||
const { addReferenceLink } = useReferenceLinkHelper(blockSuiteWorkspace);
|
||||
const metas = useBlockSuitePageMeta(blockSuiteWorkspace);
|
||||
const setPageMode = useSetAtom(setPageModeAtom);
|
||||
const currentMode = useAtomValue(currentModeAtom);
|
||||
|
||||
const switchToPageMode = useCallback(
|
||||
(pageId: string) => {
|
||||
setPageMode(pageId, 'page');
|
||||
},
|
||||
[setPageMode]
|
||||
);
|
||||
const switchToEdgelessMode = useCallback(
|
||||
(pageId: string) => {
|
||||
setPageMode(pageId, 'edgeless');
|
||||
},
|
||||
[setPageMode]
|
||||
);
|
||||
const togglePageMode = useCallback(
|
||||
(pageId: string) => {
|
||||
setPageMode(pageId, currentMode === 'edgeless' ? 'page' : 'edgeless');
|
||||
},
|
||||
[currentMode, setPageMode]
|
||||
);
|
||||
|
||||
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,
|
||||
});
|
||||
setPageReadonly(pageId, true);
|
||||
},
|
||||
[getPageMeta, metas, setPageMeta, setPageReadonly]
|
||||
);
|
||||
|
||||
const restoreFromTrash = useCallback(
|
||||
(pageId: string) => {
|
||||
const { subpageIds = [], trashRelate } = getPageMeta(pageId) ?? {};
|
||||
|
||||
if (trashRelate) {
|
||||
addReferenceLink(trashRelate, pageId);
|
||||
}
|
||||
|
||||
setPageMeta(pageId, {
|
||||
trash: false,
|
||||
trashDate: undefined,
|
||||
trashRelate: undefined,
|
||||
});
|
||||
setPageReadonly(pageId, false);
|
||||
subpageIds.forEach(id => {
|
||||
restoreFromTrash(id);
|
||||
});
|
||||
},
|
||||
[addReferenceLink, getPageMeta, setPageMeta, setPageReadonly]
|
||||
);
|
||||
|
||||
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 {
|
||||
switchToPageMode,
|
||||
switchToEdgelessMode,
|
||||
togglePageMode,
|
||||
|
||||
publicPage,
|
||||
cancelPublicPage,
|
||||
|
||||
addToFavorite,
|
||||
removeFromFavorite,
|
||||
toggleFavorite,
|
||||
|
||||
removeToTrash,
|
||||
restoreFromTrash,
|
||||
permanentlyDeletePage,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
|
||||
export function useCurrentLoginStatus():
|
||||
| 'authenticated'
|
||||
| 'unauthenticated'
|
||||
| 'loading' {
|
||||
const session = useSession();
|
||||
return session.status;
|
||||
}
|
||||
45
packages/frontend/core/src/hooks/affine/use-current-user.ts
Normal file
45
packages/frontend/core/src/hooks/affine/use-current-user.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import type { DefaultSession } from 'next-auth';
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
import { useSession } from 'next-auth/react';
|
||||
export type CheckedUser = {
|
||||
id: string;
|
||||
name: string;
|
||||
email: string;
|
||||
image: string;
|
||||
hasPassword: boolean;
|
||||
update: ReturnType<typeof useSession>['update'];
|
||||
};
|
||||
|
||||
// FIXME: Should this namespace be here?
|
||||
declare module 'next-auth' {
|
||||
interface Session {
|
||||
user: {
|
||||
id: string;
|
||||
hasPassword: boolean;
|
||||
} & DefaultSession['user'];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook checks if the user is logged in.
|
||||
* If not, it will throw an error.
|
||||
*/
|
||||
export function useCurrentUser(): CheckedUser {
|
||||
const { data: session, status, update } = useSession();
|
||||
// If you are seeing this error, it means that you are not logged in.
|
||||
// This should be prohibited in the development environment, please re-write your component logic.
|
||||
if (status === 'unauthenticated') {
|
||||
throw new Error('session.status should be authenticated');
|
||||
}
|
||||
|
||||
const user = session?.user;
|
||||
|
||||
return {
|
||||
id: user?.id ?? 'REPLACE_ME_DEFAULT_ID',
|
||||
name: user?.name ?? 'REPLACE_ME_DEFAULT_NAME',
|
||||
email: user?.email ?? 'REPLACE_ME_DEFAULT_EMAIL',
|
||||
image: user?.image ?? 'REPLACE_ME_DEFAULT_URL',
|
||||
hasPassword: user?.hasPassword ?? false,
|
||||
update,
|
||||
};
|
||||
}
|
||||
79
packages/frontend/core/src/hooks/affine/use-export-page.ts
Normal file
79
packages/frontend/core/src/hooks/affine/use-export-page.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { PageBlockModel } from '@blocksuite/blocks';
|
||||
import { ContentParser } from '@blocksuite/blocks/content-parser';
|
||||
import type { Page } from '@blocksuite/store';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
type ExportType = 'pdf' | 'html' | 'png' | 'markdown';
|
||||
const typeToContentParserMethodMap = {
|
||||
pdf: 'exportPdf',
|
||||
html: 'exportHtml',
|
||||
png: 'exportPng',
|
||||
markdown: 'exportMarkdown',
|
||||
} satisfies Record<ExportType, keyof ContentParser>;
|
||||
|
||||
const contentParserWeakMap = new WeakMap<Page, ContentParser>();
|
||||
|
||||
const getContentParser = (page: Page) => {
|
||||
if (!contentParserWeakMap.has(page)) {
|
||||
contentParserWeakMap.set(
|
||||
page,
|
||||
new ContentParser(page, {
|
||||
imageProxyEndpoint: !environment.isDesktop
|
||||
? runtimeConfig.imageProxyUrl
|
||||
: undefined,
|
||||
})
|
||||
);
|
||||
}
|
||||
return contentParserWeakMap.get(page) as ContentParser;
|
||||
};
|
||||
|
||||
interface ExportHandlerOptions {
|
||||
page: Page;
|
||||
type: ExportType;
|
||||
}
|
||||
|
||||
async function exportHandler({ page, type }: ExportHandlerOptions) {
|
||||
if (type === 'pdf' && environment.isDesktop && page.meta.mode === 'page') {
|
||||
window.apis?.export.savePDFFileAs(
|
||||
(page.root as PageBlockModel).title.toString()
|
||||
);
|
||||
} else {
|
||||
const contentParser = getContentParser(page);
|
||||
const method = typeToContentParserMethodMap[type];
|
||||
await contentParser[method]();
|
||||
}
|
||||
}
|
||||
|
||||
export const useExportPage = (page: Page) => {
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const onClickHandler = useCallback(
|
||||
async (type: ExportType) => {
|
||||
try {
|
||||
await exportHandler({
|
||||
page,
|
||||
type,
|
||||
});
|
||||
pushNotification({
|
||||
title: t['com.affine.export.success.title'](),
|
||||
message: t['com.affine.export.success.message'](),
|
||||
type: 'success',
|
||||
});
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
pushNotification({
|
||||
title: t['com.affine.export.error.title'](),
|
||||
message: t['com.affine.export.error.message'](),
|
||||
type: 'error',
|
||||
});
|
||||
}
|
||||
},
|
||||
[page, pushNotification, t]
|
||||
);
|
||||
|
||||
return onClickHandler;
|
||||
};
|
||||
30
packages/frontend/core/src/hooks/affine/use-invite-member.ts
Normal file
30
packages/frontend/core/src/hooks/affine/use-invite-member.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import type { Permission } from '@affine/graphql';
|
||||
import { inviteByEmailMutation } from '@affine/graphql';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useMutateCloud } from './use-mutate-cloud';
|
||||
|
||||
export function useInviteMember(workspaceId: string) {
|
||||
const { trigger, isMutating } = useMutation({
|
||||
mutation: inviteByEmailMutation,
|
||||
});
|
||||
const mutate = useMutateCloud();
|
||||
return {
|
||||
invite: useCallback(
|
||||
async (email: string, permission: Permission, sendInviteMail = false) => {
|
||||
const res = await trigger({
|
||||
workspaceId,
|
||||
email,
|
||||
permission,
|
||||
sendInviteMail,
|
||||
});
|
||||
await mutate();
|
||||
// return is successful
|
||||
return res?.invite;
|
||||
},
|
||||
[mutate, trigger, workspaceId]
|
||||
),
|
||||
isMutating,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import {
|
||||
getWorkspaceSharedPagesQuery,
|
||||
revokePageMutation,
|
||||
sharePageMutation,
|
||||
} from '@affine/graphql';
|
||||
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export function useIsSharedPage(
|
||||
workspaceId: string,
|
||||
pageId: string
|
||||
): [isSharedPage: boolean, setSharedPage: (enable: boolean) => void] {
|
||||
const { data, mutate } = useQuery({
|
||||
query: getWorkspaceSharedPagesQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
const { trigger: enableSharePage } = useMutation({
|
||||
mutation: sharePageMutation,
|
||||
});
|
||||
const { trigger: disableSharePage } = useMutation({
|
||||
mutation: revokePageMutation,
|
||||
});
|
||||
return [
|
||||
useMemo(
|
||||
() => data.workspace.sharedPages.some(id => id === pageId),
|
||||
[data.workspace.sharedPages, pageId]
|
||||
),
|
||||
useCallback(
|
||||
(enable: boolean) => {
|
||||
// todo: push notification
|
||||
if (enable) {
|
||||
enableSharePage({
|
||||
workspaceId,
|
||||
pageId,
|
||||
})
|
||||
.then(() => {
|
||||
return mutate();
|
||||
})
|
||||
.catch(console.error);
|
||||
} else {
|
||||
disableSharePage({
|
||||
workspaceId,
|
||||
pageId,
|
||||
})
|
||||
.then(() => {
|
||||
return mutate();
|
||||
})
|
||||
.catch(console.error);
|
||||
}
|
||||
mutate().catch(console.error);
|
||||
},
|
||||
[disableSharePage, enableSharePage, mutate, pageId, workspaceId]
|
||||
),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { getIsOwnerQuery } from '@affine/graphql';
|
||||
import { useQuery } from '@affine/workspace/affine/gql';
|
||||
|
||||
export function useIsWorkspaceOwner(workspaceId: string) {
|
||||
const { data } = useQuery({
|
||||
query: getIsOwnerQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
return data.isOwner;
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { LOCALES, useI18N } from '@affine/i18n';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
export function useLanguageHelper() {
|
||||
const i18n = useI18N();
|
||||
const currentLanguage = useMemo(
|
||||
() => LOCALES.find(item => item.tag === i18n.language),
|
||||
[i18n.language]
|
||||
);
|
||||
const languagesList = useMemo(
|
||||
() =>
|
||||
LOCALES.map(item => ({
|
||||
tag: item.tag,
|
||||
originalName: item.originalName,
|
||||
name: item.name,
|
||||
})),
|
||||
[]
|
||||
);
|
||||
const onSelect = useCallback(
|
||||
(event: string) => {
|
||||
i18n.changeLanguage(event);
|
||||
},
|
||||
[i18n]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
currentLanguage,
|
||||
languagesList,
|
||||
onSelect,
|
||||
}),
|
||||
[currentLanguage, languagesList, onSelect]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
import { leaveWorkspaceMutation } from '@affine/graphql';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useAppHelper } from '../use-workspaces';
|
||||
|
||||
export function useLeaveWorkspace() {
|
||||
const { deleteWorkspaceMeta } = useAppHelper();
|
||||
|
||||
const { trigger: leaveWorkspace } = useMutation({
|
||||
mutation: leaveWorkspaceMutation,
|
||||
});
|
||||
|
||||
return useCallback(
|
||||
async (workspaceId: string, workspaceName: string) => {
|
||||
deleteWorkspaceMeta(workspaceId);
|
||||
|
||||
await leaveWorkspace({
|
||||
workspaceId,
|
||||
workspaceName,
|
||||
sendLeaveMail: true,
|
||||
});
|
||||
},
|
||||
[deleteWorkspaceMeta, leaveWorkspace]
|
||||
);
|
||||
}
|
||||
13
packages/frontend/core/src/hooks/affine/use-member-count.ts
Normal file
13
packages/frontend/core/src/hooks/affine/use-member-count.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { getMemberCountByWorkspaceIdQuery } from '@affine/graphql';
|
||||
import { useQuery } from '@affine/workspace/affine/gql';
|
||||
|
||||
export function useMemberCount(workspaceId: string) {
|
||||
const { data } = useQuery({
|
||||
query: getMemberCountByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
},
|
||||
});
|
||||
|
||||
return data.workspace.memberCount;
|
||||
}
|
||||
25
packages/frontend/core/src/hooks/affine/use-members.ts
Normal file
25
packages/frontend/core/src/hooks/affine/use-members.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import {
|
||||
type GetMembersByWorkspaceIdQuery,
|
||||
getMembersByWorkspaceIdQuery,
|
||||
} from '@affine/graphql';
|
||||
import { useQuery } from '@affine/workspace/affine/gql';
|
||||
|
||||
export type Member = Omit<
|
||||
GetMembersByWorkspaceIdQuery['workspace']['members'][number],
|
||||
'__typename'
|
||||
>;
|
||||
export function useMembers(
|
||||
workspaceId: string,
|
||||
skip: number,
|
||||
take: number = 8
|
||||
) {
|
||||
const { data } = useQuery({
|
||||
query: getMembersByWorkspaceIdQuery,
|
||||
variables: {
|
||||
workspaceId,
|
||||
skip,
|
||||
take,
|
||||
},
|
||||
});
|
||||
return data.workspace.members;
|
||||
}
|
||||
14
packages/frontend/core/src/hooks/affine/use-mutate-cloud.ts
Normal file
14
packages/frontend/core/src/hooks/affine/use-mutate-cloud.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useSWRConfig } from 'swr';
|
||||
|
||||
export function useMutateCloud() {
|
||||
const { mutate } = useSWRConfig();
|
||||
return useCallback(async () => {
|
||||
return mutate(key => {
|
||||
if (Array.isArray(key)) {
|
||||
return key[0] === 'cloud';
|
||||
}
|
||||
return false;
|
||||
});
|
||||
}, [mutate]);
|
||||
}
|
||||
@@ -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,208 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import { EdgelessIcon, PageIcon } from '@blocksuite/icons';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { usePageMetaHelper } from '@toeverything/hooks/use-block-suite-page-meta';
|
||||
import {
|
||||
PreconditionStrategy,
|
||||
registerAffineCommand,
|
||||
} from '@toeverything/infra/command';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
|
||||
import { useExportPage } from './use-export-page';
|
||||
import { useTrashModalHelper } from './use-trash-modal-helper';
|
||||
|
||||
export function useRegisterBlocksuiteEditorCommands(
|
||||
blockSuiteWorkspace: Workspace,
|
||||
pageId: string,
|
||||
mode: 'page' | 'edgeless'
|
||||
) {
|
||||
const t = useAFFiNEI18N();
|
||||
const { getPageMeta } = usePageMetaHelper(blockSuiteWorkspace);
|
||||
const currentPage = blockSuiteWorkspace.getPage(pageId);
|
||||
assertExists(currentPage);
|
||||
const pageMeta = getPageMeta(pageId);
|
||||
assertExists(pageMeta);
|
||||
const favorite = pageMeta.favorite ?? false;
|
||||
const trash = pageMeta.trash ?? false;
|
||||
|
||||
const { togglePageMode, toggleFavorite, restoreFromTrash } =
|
||||
useBlockSuiteMetaHelper(blockSuiteWorkspace);
|
||||
const exportHandler = useExportPage(currentPage);
|
||||
const { setTrashModal } = useTrashModalHelper(blockSuiteWorkspace);
|
||||
const onClickDelete = useCallback(() => {
|
||||
setTrashModal({
|
||||
open: true,
|
||||
pageId: pageId,
|
||||
pageTitle: pageMeta.title,
|
||||
});
|
||||
}, [pageId, pageMeta.title, setTrashModal]);
|
||||
|
||||
useEffect(() => {
|
||||
const unsubs: Array<() => void> = [];
|
||||
const preconditionStrategy = () =>
|
||||
PreconditionStrategy.InPaperOrEdgeless && !trash;
|
||||
|
||||
//TODO: add back when edgeless presentation is ready
|
||||
|
||||
// this is pretty hack and easy to break. need a better way to communicate with blocksuite editor
|
||||
// unsubs.push(
|
||||
// registerAffineCommand({
|
||||
// id: 'editor:edgeless-presentation-start',
|
||||
// preconditionStrategy: () => PreconditionStrategy.InEdgeless && !trash,
|
||||
// category: 'editor:edgeless',
|
||||
// icon: <EdgelessIcon />,
|
||||
// label: t['com.affine.cmdk.affine.editor.edgeless.presentation-start'](),
|
||||
// run() {
|
||||
// document
|
||||
// .querySelector<HTMLElement>('edgeless-toolbar')
|
||||
// ?.shadowRoot?.querySelector<HTMLElement>(
|
||||
// '.edgeless-toolbar-left-part > edgeless-tool-icon-button:last-child'
|
||||
// )
|
||||
// ?.click();
|
||||
// },
|
||||
// })
|
||||
// );
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-${favorite ? 'remove-from' : 'add-to'}-favourites`,
|
||||
preconditionStrategy,
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: favorite
|
||||
? t['com.affine.favoritePageOperation.remove']()
|
||||
: t['com.affine.favoritePageOperation.add'](),
|
||||
run() {
|
||||
toggleFavorite(pageId);
|
||||
toast(
|
||||
favorite
|
||||
? t['com.affine.cmdk.affine.editor.remove-from-favourites']()
|
||||
: t['com.affine.cmdk.affine.editor.add-to-favourites']()
|
||||
);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-convert-to-${
|
||||
mode === 'page' ? 'edgeless' : 'page'
|
||||
}`,
|
||||
preconditionStrategy,
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: `${t['Convert to ']()}${
|
||||
mode === 'page'
|
||||
? t['com.affine.pageMode.edgeless']()
|
||||
: t['com.affine.pageMode.page']()
|
||||
}`,
|
||||
run() {
|
||||
togglePageMode(pageId);
|
||||
toast(
|
||||
mode === 'page'
|
||||
? t['com.affine.toastMessage.edgelessMode']()
|
||||
: t['com.affine.toastMessage.pageMode']()
|
||||
);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-export-to-pdf`,
|
||||
preconditionStrategy,
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['Export to PDF'](),
|
||||
run() {
|
||||
exportHandler('pdf');
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-export-to-html`,
|
||||
preconditionStrategy,
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['Export to HTML'](),
|
||||
run() {
|
||||
exportHandler('html');
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-export-to-png`,
|
||||
preconditionStrategy,
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['Export to PNG'](),
|
||||
run() {
|
||||
exportHandler('png');
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-export-to-markdown`,
|
||||
preconditionStrategy,
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['Export to Markdown'](),
|
||||
run() {
|
||||
exportHandler('markdown');
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-move-to-trash`,
|
||||
preconditionStrategy,
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['com.affine.moveToTrash.title'](),
|
||||
run() {
|
||||
onClickDelete();
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: `editor:${mode}-restore-from-trash`,
|
||||
preconditionStrategy: () =>
|
||||
PreconditionStrategy.InPaperOrEdgeless && trash,
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['com.affine.cmdk.affine.editor.restore-from-trash'](),
|
||||
run() {
|
||||
restoreFromTrash(pageId);
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubs.forEach(unsub => unsub());
|
||||
};
|
||||
}, [
|
||||
favorite,
|
||||
mode,
|
||||
onClickDelete,
|
||||
exportHandler,
|
||||
pageId,
|
||||
pageMeta.title,
|
||||
restoreFromTrash,
|
||||
t,
|
||||
toggleFavorite,
|
||||
togglePageMode,
|
||||
trash,
|
||||
]);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { revokeMemberPermissionMutation } from '@affine/graphql';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useMutateCloud } from './use-mutate-cloud';
|
||||
|
||||
export function useRevokeMemberPermission(workspaceId: string) {
|
||||
const mutate = useMutateCloud();
|
||||
const { trigger } = useMutation({
|
||||
mutation: revokeMemberPermissionMutation,
|
||||
});
|
||||
|
||||
return useCallback(
|
||||
async (userId: string) => {
|
||||
const res = await trigger({
|
||||
workspaceId,
|
||||
userId,
|
||||
});
|
||||
await mutate();
|
||||
return res;
|
||||
},
|
||||
[mutate, trigger, workspaceId]
|
||||
);
|
||||
}
|
||||
15
packages/frontend/core/src/hooks/affine/use-share-link.ts
Normal file
15
packages/frontend/core/src/hooks/affine/use-share-link.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
'use client';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
export function useShareLink(workspaceId: string): string {
|
||||
return useMemo(() => {
|
||||
if (environment.isServer) {
|
||||
throw new Error('useShareLink is not available on server side');
|
||||
}
|
||||
if (environment.isDesktop) {
|
||||
return '???';
|
||||
} else {
|
||||
return origin + '/share/' + workspaceId;
|
||||
}
|
||||
}, [workspaceId]);
|
||||
}
|
||||
325
packages/frontend/core/src/hooks/affine/use-shortcuts.ts
Normal file
325
packages/frontend/core/src/hooks/affine/use-shortcuts.ts
Normal file
@@ -0,0 +1,325 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
|
||||
type KeyboardShortcutsI18NKeys =
|
||||
| 'cancel'
|
||||
| 'quickSearch'
|
||||
| 'newPage'
|
||||
| 'appendDailyNote'
|
||||
| 'expandOrCollapseSidebar'
|
||||
| 'goBack'
|
||||
| 'goForward'
|
||||
| 'selectAll'
|
||||
| 'undo'
|
||||
| 'redo'
|
||||
| 'zoomIn'
|
||||
| 'zoomOut'
|
||||
| 'zoomTo100'
|
||||
| 'zoomToFit'
|
||||
| 'select'
|
||||
| 'text'
|
||||
| 'shape'
|
||||
| 'image'
|
||||
| 'straightConnector'
|
||||
| 'elbowedConnector'
|
||||
| 'curveConnector'
|
||||
| 'pen'
|
||||
| 'hand'
|
||||
| 'note'
|
||||
| 'group'
|
||||
| 'unGroup'
|
||||
| 'switch'
|
||||
| 'bold'
|
||||
| 'italic'
|
||||
| 'underline'
|
||||
| 'strikethrough'
|
||||
| 'inlineCode'
|
||||
| 'codeBlock'
|
||||
| 'link'
|
||||
| 'bodyText'
|
||||
| 'increaseIndent'
|
||||
| 'reduceIndent'
|
||||
| 'groupDatabase'
|
||||
| 'moveUp'
|
||||
| 'moveDown'
|
||||
| 'divider';
|
||||
|
||||
// TODO(550): remove this hook after 'useAFFiNEI18N' support scoped i18n
|
||||
const useKeyboardShortcutsI18N = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
return useCallback(
|
||||
(key: KeyboardShortcutsI18NKeys) =>
|
||||
t[`com.affine.keyboardShortcuts.${key}`](),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
|
||||
// TODO(550): remove this hook after 'useAFFiNEI18N' support scoped i18n
|
||||
const useHeadingKeyboardShortcutsI18N = () => {
|
||||
const t = useAFFiNEI18N();
|
||||
return useCallback(
|
||||
(number: string) => t['com.affine.keyboardShortcuts.heading']({ number }),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
|
||||
interface ShortcutMap {
|
||||
[x: string]: string[];
|
||||
}
|
||||
export interface ShortcutsInfo {
|
||||
title: string;
|
||||
shortcuts: ShortcutMap;
|
||||
}
|
||||
|
||||
export const useWinGeneralKeyboardShortcuts = (): ShortcutMap => {
|
||||
const t = useKeyboardShortcutsI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[t('cancel')]: ['ESC'],
|
||||
[t('quickSearch')]: ['Ctrl', 'K'],
|
||||
[t('newPage')]: ['Ctrl', 'N'],
|
||||
// not implement yet
|
||||
// [t('appendDailyNote')]: 'Ctrl + Alt + A',
|
||||
[t('expandOrCollapseSidebar')]: ['Ctrl', '/'],
|
||||
[t('goBack')]: ['Ctrl + ['],
|
||||
[t('goForward')]: ['Ctrl + ]'],
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
export const useMacGeneralKeyboardShortcuts = (): ShortcutMap => {
|
||||
const t = useKeyboardShortcutsI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[t('cancel')]: ['ESC'],
|
||||
[t('quickSearch')]: ['⌘', 'K'],
|
||||
[t('newPage')]: ['⌘', 'N'],
|
||||
// not implement yet
|
||||
// [t('appendDailyNote')]: '⌘ + ⌥ + A',
|
||||
[t('expandOrCollapseSidebar')]: ['⌘', '/'],
|
||||
[t('goBack')]: ['⌘ + ['],
|
||||
[t('goForward')]: ['⌘ + ]'],
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
|
||||
export const useMacEdgelessKeyboardShortcuts = (): ShortcutMap => {
|
||||
const t = useKeyboardShortcutsI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[t('selectAll')]: ['⌘', 'A'],
|
||||
[t('undo')]: ['⌘', 'Z'],
|
||||
[t('redo')]: ['⌘', '⇧', 'Z'],
|
||||
[t('zoomIn')]: ['⌘', '+'],
|
||||
[t('zoomOut')]: ['⌘', '-'],
|
||||
[t('zoomTo100')]: ['⌘', '0'],
|
||||
[t('zoomToFit')]: ['⌘', '1'],
|
||||
[t('select')]: ['V'],
|
||||
[t('text')]: ['T'],
|
||||
[t('shape')]: ['S'],
|
||||
[t('image')]: ['I'],
|
||||
[t('straightConnector')]: ['L'],
|
||||
[t('elbowedConnector')]: ['X'],
|
||||
// not implement yet
|
||||
// [t('curveConnector')]: 'C',
|
||||
[t('pen')]: ['P'],
|
||||
[t('hand')]: ['H'],
|
||||
[t('note')]: ['N'],
|
||||
// not implement yet
|
||||
// [t('group')]: '⌘ + G',
|
||||
// [t('unGroup')]: '⌘ + ⇧ + G',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
export const useWinEdgelessKeyboardShortcuts = (): ShortcutMap => {
|
||||
const t = useKeyboardShortcutsI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[t('selectAll')]: ['Ctrl', 'A'],
|
||||
[t('undo')]: ['Ctrl', 'Z'],
|
||||
[t('redo')]: ['Ctrl', 'Y/Ctrl', 'Shift', 'Z'],
|
||||
[t('zoomIn')]: ['Ctrl', '+'],
|
||||
[t('zoomOut')]: ['Ctrl', '-'],
|
||||
[t('zoomTo100')]: ['Ctrl', '0'],
|
||||
[t('zoomToFit')]: ['Ctrl', '1'],
|
||||
[t('select')]: ['V'],
|
||||
[t('text')]: ['T'],
|
||||
[t('shape')]: ['S'],
|
||||
[t('image')]: ['I'],
|
||||
[t('straightConnector')]: ['L'],
|
||||
[t('elbowedConnector')]: ['X'],
|
||||
// not implement yet
|
||||
// [t('curveConnector')]: 'C',
|
||||
[t('pen')]: ['P'],
|
||||
[t('hand')]: ['H'],
|
||||
[t('note')]: ['N'],
|
||||
[t('switch')]: ['Alt ', ''],
|
||||
// not implement yet
|
||||
// [t('group')]: 'Ctrl + G',
|
||||
// [t('unGroup')]: 'Ctrl + Shift + G',
|
||||
}),
|
||||
[t]
|
||||
);
|
||||
};
|
||||
export const useMacPageKeyboardShortcuts = (): ShortcutMap => {
|
||||
const t = useKeyboardShortcutsI18N();
|
||||
const tH = useHeadingKeyboardShortcutsI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[t('undo')]: ['⌘', 'Z'],
|
||||
[t('redo')]: ['⌘', '⇧', 'Z'],
|
||||
[t('bold')]: ['⌘', 'B'],
|
||||
[t('italic')]: ['⌘', 'I'],
|
||||
[t('underline')]: ['⌘', 'U'],
|
||||
[t('strikethrough')]: ['⌘', '⇧', 'S'],
|
||||
[t('inlineCode')]: ['⌘', 'E'],
|
||||
[t('codeBlock')]: ['⌘', '⌥', 'C'],
|
||||
[t('link')]: ['⌘', 'K'],
|
||||
[t('quickSearch')]: ['⌘', 'K'],
|
||||
[t('bodyText')]: ['⌘', '⌥', '0'],
|
||||
[tH('1')]: ['⌘', '⌥', '1'],
|
||||
[tH('2')]: ['⌘', '⌥', '2'],
|
||||
[tH('3')]: ['⌘', '⌥', '3'],
|
||||
[tH('4')]: ['⌘', '⌥', '4'],
|
||||
[tH('5')]: ['⌘', '⌥', '5'],
|
||||
[tH('6')]: ['⌘', '⌥', '6'],
|
||||
[t('increaseIndent')]: ['Tab'],
|
||||
[t('reduceIndent')]: ['⇧', 'Tab'],
|
||||
[t('groupDatabase')]: ['⌘', 'G'],
|
||||
[t('switch')]: ['⌥', 'S'],
|
||||
// not implement yet
|
||||
// [t('moveUp')]: '⌘ + ⌥ + ↑',
|
||||
// [t('moveDown')]: '⌘ + ⌥ + ↓',
|
||||
}),
|
||||
[t, tH]
|
||||
);
|
||||
};
|
||||
|
||||
export const useMacMarkdownShortcuts = (): ShortcutMap => {
|
||||
const t = useKeyboardShortcutsI18N();
|
||||
const tH = useHeadingKeyboardShortcutsI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[t('bold')]: ['**Text**'],
|
||||
[t('italic')]: ['*Text*'],
|
||||
[t('underline')]: ['~Text~'],
|
||||
[t('strikethrough')]: ['~~Text~~'],
|
||||
[t('divider')]: ['***'],
|
||||
[t('inlineCode')]: ['`Text` '],
|
||||
[t('codeBlock')]: ['``` Space'],
|
||||
[tH('1')]: ['# Text'],
|
||||
[tH('2')]: ['## Text'],
|
||||
[tH('3')]: ['### Text'],
|
||||
[tH('4')]: ['#### Text'],
|
||||
[tH('5')]: ['##### Text'],
|
||||
[tH('6')]: ['###### Text'],
|
||||
}),
|
||||
[t, tH]
|
||||
);
|
||||
};
|
||||
|
||||
export const useWinPageKeyboardShortcuts = (): ShortcutMap => {
|
||||
const t = useKeyboardShortcutsI18N();
|
||||
const tH = useHeadingKeyboardShortcutsI18N();
|
||||
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('inlineCode')]: [' Ctrl', 'E'],
|
||||
[t('codeBlock')]: ['Ctrl', 'Alt', 'C'],
|
||||
[t('link')]: ['Ctr', 'K'],
|
||||
[t('quickSearch')]: ['Ctrl', 'K'],
|
||||
[t('bodyText')]: ['Ctrl', 'Shift', '0'],
|
||||
[tH('1')]: ['Ctrl', 'Shift', '1'],
|
||||
[tH('2')]: ['Ctrl', 'Shift', '2'],
|
||||
[tH('3')]: ['Ctrl', 'Shift', '3'],
|
||||
[tH('4')]: ['Ctrl', 'Shift', '4'],
|
||||
[tH('5')]: ['Ctrl', 'Shift', '5'],
|
||||
[tH('6')]: ['Ctrl', 'Shift', '6'],
|
||||
[t('increaseIndent')]: ['Tab'],
|
||||
[t('reduceIndent')]: ['Shift+Tab'],
|
||||
[t('groupDatabase')]: ['Ctrl + G'],
|
||||
['Switch']: ['Alt + S'],
|
||||
// not implement yet
|
||||
// [t('moveUp')]: 'Ctrl + Alt + ↑',
|
||||
// [t('moveDown')]: 'Ctrl + Alt + ↓',
|
||||
}),
|
||||
[t, tH]
|
||||
);
|
||||
};
|
||||
export const useWinMarkdownShortcuts = (): ShortcutMap => {
|
||||
const t = useKeyboardShortcutsI18N();
|
||||
const tH = useHeadingKeyboardShortcutsI18N();
|
||||
return useMemo(
|
||||
() => ({
|
||||
[t('bold')]: ['**Text** '],
|
||||
[t('italic')]: ['*Text* '],
|
||||
[t('underline')]: ['~Text~ '],
|
||||
[t('strikethrough')]: ['~~Text~~ '],
|
||||
[t('divider')]: ['***'],
|
||||
[t('inlineCode')]: ['`Text` '],
|
||||
[t('codeBlock')]: ['``` Text'],
|
||||
[tH('1')]: ['# Text'],
|
||||
[tH('2')]: ['## Text'],
|
||||
[tH('3')]: ['### Text'],
|
||||
[tH('4')]: ['#### Text'],
|
||||
[tH('5')]: ['##### Text'],
|
||||
[tH('6')]: ['###### Text'],
|
||||
}),
|
||||
[t, tH]
|
||||
);
|
||||
};
|
||||
|
||||
export const useMarkdownShortcuts = (): ShortcutsInfo => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const macMarkdownShortcuts = useMacMarkdownShortcuts();
|
||||
const winMarkdownShortcuts = useWinMarkdownShortcuts();
|
||||
const isMac = environment.isBrowser && environment.isMacOs;
|
||||
return {
|
||||
title: t['com.affine.shortcutsTitle.markdownSyntax'](),
|
||||
shortcuts: isMac ? macMarkdownShortcuts : winMarkdownShortcuts,
|
||||
};
|
||||
};
|
||||
|
||||
export const usePageShortcuts = (): ShortcutsInfo => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const macPageShortcuts = useMacPageKeyboardShortcuts();
|
||||
const winPageShortcuts = useWinPageKeyboardShortcuts();
|
||||
const isMac = environment.isBrowser && environment.isMacOs;
|
||||
return {
|
||||
title: t['com.affine.shortcutsTitle.page'](),
|
||||
shortcuts: isMac ? macPageShortcuts : winPageShortcuts,
|
||||
};
|
||||
};
|
||||
|
||||
export const useEdgelessShortcuts = (): ShortcutsInfo => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const macEdgelessShortcuts = useMacEdgelessKeyboardShortcuts();
|
||||
const winEdgelessShortcuts = useWinEdgelessKeyboardShortcuts();
|
||||
const isMac = environment.isBrowser && environment.isMacOs;
|
||||
return {
|
||||
title: t['com.affine.shortcutsTitle.edgeless'](),
|
||||
shortcuts: isMac ? macEdgelessShortcuts : winEdgelessShortcuts,
|
||||
};
|
||||
};
|
||||
|
||||
export const useGeneralShortcuts = (): ShortcutsInfo => {
|
||||
const t = useAFFiNEI18N();
|
||||
|
||||
const macGeneralShortcuts = useMacGeneralKeyboardShortcuts();
|
||||
const winGeneralShortcuts = useWinGeneralKeyboardShortcuts();
|
||||
const isMac = environment.isBrowser && environment.isMacOs;
|
||||
return {
|
||||
title: t['com.affine.shortcutsTitle.general'](),
|
||||
shortcuts: isMac ? macGeneralShortcuts : winGeneralShortcuts,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,22 @@
|
||||
import { setWorkspacePublicByIdMutation } from '@affine/graphql';
|
||||
import { useMutation } from '@affine/workspace/affine/gql';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { useMutateCloud } from './use-mutate-cloud';
|
||||
|
||||
export function useToggleCloudPublic(workspaceId: string) {
|
||||
const mutate = useMutateCloud();
|
||||
const { trigger } = useMutation({
|
||||
mutation: setWorkspacePublicByIdMutation,
|
||||
});
|
||||
return useCallback(
|
||||
async (isPublic: boolean) => {
|
||||
await trigger({
|
||||
id: workspaceId,
|
||||
public: isPublic,
|
||||
});
|
||||
await mutate();
|
||||
},
|
||||
[mutate, trigger, workspaceId]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { toast } from '@affine/component';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { useAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { trashModalAtom } from '../../atoms/trash-modal';
|
||||
import { useBlockSuiteMetaHelper } from './use-block-suite-meta-helper';
|
||||
|
||||
export function useTrashModalHelper(blocksuiteWorkspace: Workspace) {
|
||||
const t = useAFFiNEI18N();
|
||||
const [trashModal, setTrashModal] = useAtom(trashModalAtom);
|
||||
const { pageId } = trashModal;
|
||||
const { removeToTrash } = useBlockSuiteMetaHelper(blocksuiteWorkspace);
|
||||
|
||||
const handleOnConfirm = useCallback(() => {
|
||||
removeToTrash(pageId);
|
||||
toast(t['com.affine.toastMessage.movedTrash']());
|
||||
setTrashModal({ ...trashModal, open: false });
|
||||
}, [pageId, removeToTrash, setTrashModal, t, trashModal]);
|
||||
|
||||
return {
|
||||
trashModal,
|
||||
setTrashModal,
|
||||
handleOnConfirm,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import {
|
||||
currentPageIdAtom,
|
||||
currentWorkspaceIdAtom,
|
||||
} from '@toeverything/infra/atom';
|
||||
import { useAtom, useSetAtom } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import type { AllWorkspace } from '../../shared';
|
||||
import { useWorkspace, useWorkspaceEffect } 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);
|
||||
// when you call current workspace, effect is always called
|
||||
useWorkspaceEffect(currentWorkspace.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,90 @@
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import type { WorkspaceRegistry } from '@affine/env/workspace';
|
||||
import type { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import {
|
||||
rootWorkspacesMetadataAtom,
|
||||
workspaceAdaptersAtom,
|
||||
} from '@affine/workspace/atom';
|
||||
import { currentPageIdAtom } from '@toeverything/infra/atom';
|
||||
import { WorkspaceVersion } from '@toeverything/infra/blocksuite';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { openSettingModalAtom } from '../../atoms';
|
||||
import { useNavigateHelper } from '../use-navigate-helper';
|
||||
|
||||
export function useOnTransformWorkspace() {
|
||||
const t = useAFFiNEI18N();
|
||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||
const WorkspaceAdapters = useAtomValue(workspaceAdaptersAtom);
|
||||
const setMetadata = useSetAtom(rootWorkspacesMetadataAtom);
|
||||
const { openPage } = useNavigateHelper();
|
||||
const currentPageId = useAtomValue(currentPageIdAtom);
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
|
||||
return useCallback(
|
||||
async <From extends WorkspaceFlavour, To extends WorkspaceFlavour>(
|
||||
from: From,
|
||||
to: To,
|
||||
workspace: WorkspaceRegistry[From]
|
||||
): Promise<void> => {
|
||||
// create first, then delete, in case of failure
|
||||
const newId = await WorkspaceAdapters[to].CRUD.create(
|
||||
workspace.blockSuiteWorkspace
|
||||
);
|
||||
await WorkspaceAdapters[from].CRUD.delete(workspace.blockSuiteWorkspace);
|
||||
setMetadata(workspaces => {
|
||||
const idx = workspaces.findIndex(ws => ws.id === workspace.id);
|
||||
workspaces.splice(idx, 1, {
|
||||
id: newId,
|
||||
flavour: to,
|
||||
version: WorkspaceVersion.SubDoc,
|
||||
});
|
||||
return [...workspaces];
|
||||
}, newId);
|
||||
// fixme(himself65): setting modal could still open and open the non-exist workspace
|
||||
setSettingModal(settings => ({
|
||||
...settings,
|
||||
open: false,
|
||||
}));
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('affine-workspace:transform', {
|
||||
detail: {
|
||||
from,
|
||||
to,
|
||||
oldId: workspace.id,
|
||||
newId: newId,
|
||||
},
|
||||
})
|
||||
);
|
||||
openPage(newId, currentPageId ?? WorkspaceSubPath.ALL);
|
||||
pushNotification({
|
||||
title: t['Successfully enabled AFFiNE Cloud'](),
|
||||
type: 'success',
|
||||
});
|
||||
},
|
||||
[
|
||||
WorkspaceAdapters,
|
||||
setMetadata,
|
||||
setSettingModal,
|
||||
openPage,
|
||||
currentPageId,
|
||||
pushNotification,
|
||||
t,
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
declare global {
|
||||
// global Events
|
||||
interface WindowEventMap {
|
||||
'affine-workspace:transform': CustomEvent<{
|
||||
from: WorkspaceFlavour;
|
||||
to: WorkspaceFlavour;
|
||||
oldId: string;
|
||||
newId: string;
|
||||
}>;
|
||||
}
|
||||
}
|
||||
91
packages/frontend/core/src/hooks/use-datasource-sync.ts
Normal file
91
packages/frontend/core/src/hooks/use-datasource-sync.ts
Normal file
@@ -0,0 +1,91 @@
|
||||
import { pushNotificationAtom } from '@affine/component/notification-center';
|
||||
import type {
|
||||
AffineSocketIOProvider,
|
||||
LocalIndexedDBBackgroundProvider,
|
||||
SQLiteProvider,
|
||||
} from '@affine/env/workspace';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { startTransition, useCallback, useMemo, useState } from 'react';
|
||||
import { type Status, syncDataSource } from 'y-provider';
|
||||
|
||||
export function useDatasourceSync(workspace: Workspace) {
|
||||
const [status, setStatus] = useState<Status>({
|
||||
type: 'idle',
|
||||
});
|
||||
const pushNotification = useSetAtom(pushNotificationAtom);
|
||||
const providers = workspace.providers;
|
||||
const remoteProvider: AffineSocketIOProvider | undefined = useMemo(() => {
|
||||
return providers.find(
|
||||
(provider): provider is AffineSocketIOProvider =>
|
||||
provider.flavour === 'affine-socket-io'
|
||||
);
|
||||
}, [providers]);
|
||||
const localProvider = useMemo(() => {
|
||||
const sqliteProvider = providers.find(
|
||||
(provider): provider is SQLiteProvider => provider.flavour === 'sqlite'
|
||||
);
|
||||
const indexedDbProvider = providers.find(
|
||||
(provider): provider is LocalIndexedDBBackgroundProvider =>
|
||||
provider.flavour === 'local-indexeddb-background'
|
||||
);
|
||||
const provider = sqliteProvider || indexedDbProvider;
|
||||
assertExists(provider, 'no local provider');
|
||||
return provider;
|
||||
}, [providers]);
|
||||
return [
|
||||
status,
|
||||
useCallback(() => {
|
||||
if (!remoteProvider) {
|
||||
return;
|
||||
}
|
||||
startTransition(() => {
|
||||
setStatus({
|
||||
type: 'syncing',
|
||||
});
|
||||
});
|
||||
syncDataSource(
|
||||
() => [
|
||||
workspace.doc.guid,
|
||||
...[...workspace.doc.subdocs].map(doc => doc.guid),
|
||||
],
|
||||
remoteProvider.datasource,
|
||||
localProvider.datasource
|
||||
)
|
||||
.then(async () => {
|
||||
// by default, the syncing status will show for 2.4s
|
||||
setTimeout(() => {
|
||||
startTransition(() => {
|
||||
setStatus({
|
||||
type: 'synced',
|
||||
});
|
||||
pushNotification({
|
||||
title: 'Synced successfully',
|
||||
type: 'success',
|
||||
});
|
||||
});
|
||||
}, 2400);
|
||||
})
|
||||
.catch(error => {
|
||||
startTransition(() => {
|
||||
setStatus({
|
||||
type: 'error',
|
||||
error,
|
||||
});
|
||||
pushNotification({
|
||||
title: 'Unable to Sync',
|
||||
message: 'Server error, please try again later.',
|
||||
type: 'error',
|
||||
});
|
||||
});
|
||||
});
|
||||
}, [
|
||||
remoteProvider,
|
||||
localProvider.datasource,
|
||||
workspace.doc.guid,
|
||||
workspace.doc.subdocs,
|
||||
pushNotification,
|
||||
]),
|
||||
] as const;
|
||||
}
|
||||
29
packages/frontend/core/src/hooks/use-get-page-info.ts
Normal file
29
packages/frontend/core/src/hooks/use-get-page-info.ts
Normal file
@@ -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]
|
||||
);
|
||||
};
|
||||
131
packages/frontend/core/src/hooks/use-navigate-helper.ts
Normal file
131
packages/frontend/core/src/hooks/use-navigate-helper.ts
Normal file
@@ -0,0 +1,131 @@
|
||||
import type { WorkspaceSubPath } from '@affine/env/workspace';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import {
|
||||
type NavigateOptions,
|
||||
useLocation,
|
||||
// eslint-disable-next-line @typescript-eslint/no-restricted-imports
|
||||
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 isPublicWorkspace = useMemo(() => {
|
||||
return location.pathname.indexOf('/public-workspace') === 0;
|
||||
}, [location.pathname]);
|
||||
|
||||
const openPage = useCallback(
|
||||
(workspaceId: string, pageId: string) => {
|
||||
if (isPublicWorkspace) {
|
||||
return jumpToPublicWorkspacePage(workspaceId, pageId);
|
||||
} else {
|
||||
return jumpToPage(workspaceId, pageId);
|
||||
}
|
||||
},
|
||||
[jumpToPage, jumpToPublicWorkspacePage, isPublicWorkspace]
|
||||
);
|
||||
|
||||
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]
|
||||
);
|
||||
const jumpToExpired = useCallback(
|
||||
(logic: RouteLogic = RouteLogic.PUSH) => {
|
||||
return navigate('/expired', {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
const jumpToSignIn = useCallback(
|
||||
(
|
||||
logic: RouteLogic = RouteLogic.PUSH,
|
||||
otherOptions?: Omit<NavigateOptions, 'replace'>
|
||||
) => {
|
||||
return navigate('/signIn', {
|
||||
replace: logic === RouteLogic.REPLACE,
|
||||
...otherOptions,
|
||||
});
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
jumpToPage,
|
||||
jumpToPublicWorkspacePage,
|
||||
jumpToSubPath,
|
||||
jumpToIndex,
|
||||
jumpTo404,
|
||||
openPage,
|
||||
jumpToExpired,
|
||||
jumpToSignIn,
|
||||
}),
|
||||
[
|
||||
jumpTo404,
|
||||
jumpToExpired,
|
||||
jumpToIndex,
|
||||
jumpToPage,
|
||||
jumpToPublicWorkspacePage,
|
||||
jumpToSignIn,
|
||||
jumpToSubPath,
|
||||
openPage,
|
||||
]
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||
import { useAtom, useStore } from 'jotai';
|
||||
import { useTheme } from 'next-themes';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { allPageModeSelectAtom } from '../atoms';
|
||||
import {
|
||||
registerAffineCreationCommands,
|
||||
registerAffineHelpCommands,
|
||||
registerAffineLayoutCommands,
|
||||
registerAffineNavigationCommands,
|
||||
registerAffineSettingsCommands,
|
||||
registerAffineUpdatesCommands,
|
||||
} from '../commands';
|
||||
import { usePageHelper } from '../components/blocksuite/block-suite-page-list/utils';
|
||||
import { useLanguageHelper } from './affine/use-language-helper';
|
||||
import { useCurrentWorkspace } from './current/use-current-workspace';
|
||||
import { useNavigateHelper } from './use-navigate-helper';
|
||||
|
||||
export function useRegisterWorkspaceCommands() {
|
||||
const store = useStore();
|
||||
const t = useAFFiNEI18N();
|
||||
const theme = useTheme();
|
||||
const [currentWorkspace] = useCurrentWorkspace();
|
||||
const languageHelper = useLanguageHelper();
|
||||
const pageHelper = usePageHelper(currentWorkspace.blockSuiteWorkspace);
|
||||
const navigationHelper = useNavigateHelper();
|
||||
const [pageListMode, setPageListMode] = useAtom(allPageModeSelectAtom);
|
||||
useEffect(() => {
|
||||
const unsubs: Array<() => void> = [];
|
||||
unsubs.push(
|
||||
registerAffineUpdatesCommands({
|
||||
store,
|
||||
t,
|
||||
})
|
||||
);
|
||||
unsubs.push(
|
||||
registerAffineNavigationCommands({
|
||||
store,
|
||||
t,
|
||||
workspace: currentWorkspace.blockSuiteWorkspace,
|
||||
navigationHelper,
|
||||
pageListMode,
|
||||
setPageListMode,
|
||||
})
|
||||
);
|
||||
unsubs.push(
|
||||
registerAffineSettingsCommands({
|
||||
store,
|
||||
t,
|
||||
theme,
|
||||
languageHelper,
|
||||
})
|
||||
);
|
||||
unsubs.push(registerAffineLayoutCommands({ store, t }));
|
||||
unsubs.push(
|
||||
registerAffineCreationCommands({
|
||||
store,
|
||||
pageHelper: pageHelper,
|
||||
t,
|
||||
})
|
||||
);
|
||||
unsubs.push(
|
||||
registerAffineHelpCommands({
|
||||
store,
|
||||
t,
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubs.forEach(unsub => unsub());
|
||||
};
|
||||
}, [
|
||||
store,
|
||||
pageHelper,
|
||||
t,
|
||||
theme,
|
||||
currentWorkspace.blockSuiteWorkspace,
|
||||
navigationHelper,
|
||||
pageListMode,
|
||||
setPageListMode,
|
||||
languageHelper,
|
||||
]);
|
||||
}
|
||||
49
packages/frontend/core/src/hooks/use-shortcut-commands.ts
Normal file
49
packages/frontend/core/src/hooks/use-shortcut-commands.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import {
|
||||
PreconditionStrategy,
|
||||
registerAffineCommand,
|
||||
} from '@toeverything/infra/command';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
export function useRegisterBlocksuiteEditorCommands(
|
||||
back: () => unknown,
|
||||
forward: () => unknown
|
||||
) {
|
||||
useEffect(() => {
|
||||
const unsubs: Array<() => void> = [];
|
||||
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'affine:shortcut-history-go-back',
|
||||
category: 'affine:general',
|
||||
preconditionStrategy: PreconditionStrategy.Never,
|
||||
icon: 'none',
|
||||
label: 'go back',
|
||||
keyBinding: {
|
||||
binding: '$mod+[',
|
||||
},
|
||||
run() {
|
||||
back();
|
||||
},
|
||||
})
|
||||
);
|
||||
unsubs.push(
|
||||
registerAffineCommand({
|
||||
id: 'affine:shortcut-history-go-forward',
|
||||
category: 'affine:general',
|
||||
preconditionStrategy: PreconditionStrategy.Never,
|
||||
icon: 'none',
|
||||
label: 'go forward',
|
||||
keyBinding: {
|
||||
binding: '$mod+]',
|
||||
},
|
||||
run() {
|
||||
forward();
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
return () => {
|
||||
unsubs.forEach(unsub => unsub());
|
||||
};
|
||||
}, [back, forward]);
|
||||
}
|
||||
21
packages/frontend/core/src/hooks/use-system-online.ts
Normal file
21
packages/frontend/core/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, [])
|
||||
);
|
||||
}
|
||||
58
packages/frontend/core/src/hooks/use-workspace-blob.ts
Normal file
58
packages/frontend/core/src/hooks/use-workspace-blob.ts
Normal file
@@ -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;
|
||||
}
|
||||
42
packages/frontend/core/src/hooks/use-workspace.ts
Normal file
42
packages/frontend/core/src/hooks/use-workspace.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { assertExists } from '@blocksuite/global/utils';
|
||||
import type { Workspace } from '@blocksuite/store';
|
||||
import { getBlockSuiteWorkspaceAtom } from '@toeverything/infra/__internal__/workspace';
|
||||
import type { Atom } from 'jotai';
|
||||
import { atom, useAtomValue } from 'jotai';
|
||||
|
||||
const workspaceWeakMap = new WeakMap<
|
||||
Workspace,
|
||||
Atom<Promise<AffineOfficialWorkspace>>
|
||||
>();
|
||||
|
||||
// workspace effect is the side effect like connect to the server/indexeddb,
|
||||
// this will save the workspace updates permanently.
|
||||
export function useWorkspaceEffect(workspaceId: string): void {
|
||||
const [, effectAtom] = getBlockSuiteWorkspaceAtom(workspaceId);
|
||||
useAtomValue(effectAtom);
|
||||
}
|
||||
|
||||
// todo(himself65): remove this hook
|
||||
export function useWorkspace(workspaceId: string): AffineOfficialWorkspace {
|
||||
const [workspaceAtom] = getBlockSuiteWorkspaceAtom(workspaceId);
|
||||
const workspace = useAtomValue(workspaceAtom);
|
||||
if (!workspaceWeakMap.has(workspace)) {
|
||||
const baseAtom = atom(async get => {
|
||||
const metadata = await get(rootWorkspacesMetadataAtom);
|
||||
const flavour = metadata.find(({ id }) => id === workspaceId)?.flavour;
|
||||
assertExists(flavour, 'workspace flavour not found');
|
||||
return {
|
||||
id: workspaceId,
|
||||
flavour,
|
||||
blockSuiteWorkspace: workspace,
|
||||
};
|
||||
});
|
||||
workspaceWeakMap.set(workspace, baseAtom);
|
||||
}
|
||||
|
||||
return useAtomValue(
|
||||
workspaceWeakMap.get(workspace) as Atom<Promise<AffineOfficialWorkspace>>
|
||||
);
|
||||
}
|
||||
126
packages/frontend/core/src/hooks/use-workspaces.ts
Normal file
126
packages/frontend/core/src/hooks/use-workspaces.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import { DebugLogger } from '@affine/debug';
|
||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||
import { rootWorkspacesMetadataAtom } from '@affine/workspace/atom';
|
||||
import { saveWorkspaceToLocalStorage } from '@affine/workspace/local/crud';
|
||||
import {
|
||||
getOrCreateWorkspace,
|
||||
globalBlockSuiteSchema,
|
||||
} from '@affine/workspace/manager';
|
||||
import { getWorkspace } from '@toeverything/infra/__internal__/workspace';
|
||||
import { getCurrentStore } from '@toeverything/infra/atom';
|
||||
import {
|
||||
buildShowcaseWorkspace,
|
||||
WorkspaceVersion,
|
||||
} from '@toeverything/infra/blocksuite';
|
||||
import { useAtomValue, useSetAtom } from 'jotai';
|
||||
import { nanoid } from 'nanoid';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { LocalAdapter } from '../adapters/local';
|
||||
import { WorkspaceAdapters } from '../adapters/workspace';
|
||||
import { setPageModeAtom } from '../atoms';
|
||||
|
||||
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> => {
|
||||
getOrCreateWorkspace(workspaceId, WorkspaceFlavour.LOCAL);
|
||||
saveWorkspaceToLocalStorage(workspaceId);
|
||||
set(workspaces => [
|
||||
...workspaces,
|
||||
{
|
||||
id: workspaceId,
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
version: WorkspaceVersion.DatabaseV3,
|
||||
},
|
||||
]);
|
||||
logger.debug('imported local workspace', workspaceId);
|
||||
return workspaceId;
|
||||
},
|
||||
[set]
|
||||
),
|
||||
addCloudWorkspace: useCallback(
|
||||
(workspaceId: string) => {
|
||||
getOrCreateWorkspace(workspaceId, WorkspaceFlavour.AFFINE_CLOUD);
|
||||
set(workspaces => [
|
||||
...workspaces,
|
||||
{
|
||||
id: workspaceId,
|
||||
flavour: WorkspaceFlavour.AFFINE_CLOUD,
|
||||
version: WorkspaceVersion.DatabaseV3,
|
||||
},
|
||||
]);
|
||||
logger.debug('imported cloud workspace', workspaceId);
|
||||
},
|
||||
[set]
|
||||
),
|
||||
createLocalWorkspace: useCallback(
|
||||
async (name: string): Promise<string> => {
|
||||
const blockSuiteWorkspace = getOrCreateWorkspace(
|
||||
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 = getOrCreateWorkspace(
|
||||
id,
|
||||
WorkspaceFlavour.LOCAL
|
||||
);
|
||||
await buildShowcaseWorkspace(blockSuiteWorkspace, {
|
||||
schema: globalBlockSuiteSchema,
|
||||
store: getCurrentStore(),
|
||||
atoms: {
|
||||
pageMode: setPageModeAtom,
|
||||
},
|
||||
});
|
||||
}
|
||||
set(workspaces => [
|
||||
...workspaces,
|
||||
{
|
||||
id,
|
||||
flavour: WorkspaceFlavour.LOCAL,
|
||||
version: WorkspaceVersion.DatabaseV3,
|
||||
},
|
||||
]);
|
||||
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
|
||||
set(workspaces => workspaces.filter(ws => ws.id !== workspaceId));
|
||||
},
|
||||
[jotaiWorkspaces, set]
|
||||
),
|
||||
deleteWorkspaceMeta: useCallback(
|
||||
(workspaceId: string) => {
|
||||
set(workspaces => workspaces.filter(ws => ws.id !== workspaceId));
|
||||
},
|
||||
[set]
|
||||
),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user