feat(core): add use guard hook (#11180)

Previously, we used `useLiveData(guardService.can$())` to get the guard result, but `guardService.can$()` will request the server to revalidate the permission when calling it, will cause additional network requests when re-render.

This pr make a new hook `useGuard` to fix this problem.

And the side effect in `can$` is moved to `revalidateCan()` to make that the subscribe method is pure
This commit is contained in:
EYHN
2025-03-25 14:15:29 +00:00
parent a10acf304b
commit 3df51a217d
28 changed files with 154 additions and 163 deletions
@@ -5,10 +5,7 @@ import { Modal, useConfirmModal } from '@affine/component/ui/modal';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
import { EditorService } from '@affine/core/modules/editor';
import {
GuardService,
WorkspacePermissionService,
} from '@affine/core/modules/permissions';
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
import { WorkspaceQuotaService } from '@affine/core/modules/quota';
import { WorkspaceService } from '@affine/core/modules/workspace';
import { i18nTime, Trans, useI18n } from '@affine/i18n';
@@ -35,6 +32,7 @@ import { encodeStateAsUpdate } from 'yjs';
import { BlockSuiteEditor } from '../../../blocksuite/block-suite-editor';
import { PureEditorModeSwitch } from '../../../blocksuite/block-suite-mode-switch';
import { pageHistoryModalAtom } from '../../atoms/page-history';
import { useGuard } from '../../guard';
import { AffineErrorBoundary } from '../affine-error-boundary';
import {
historyListGroupByDay,
@@ -408,8 +406,6 @@ const PageHistoryManager = ({
const workspaceId = docCollection.id;
const [activeVersion, setActiveVersion] = useState<string>();
const guardService = useService(GuardService);
const pageDocId = useMemo(() => {
return docCollection.getDoc(pageId)?.spaceDoc.guid ?? pageId;
}, [pageId, docCollection]);
@@ -441,7 +437,7 @@ const PageHistoryManager = ({
const i18n = useI18n();
const title = useLiveData(docDisplayMetaService.title$(pageId));
const canEdit = useLiveData(guardService.can$('Doc_Update', pageDocId));
const canEdit = useGuard('Doc_Update', pageDocId);
const onConfirmRestore = useCallback(() => {
openConfirmModal({
@@ -8,7 +8,6 @@ import {
} from '@affine/component';
import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
import { DocsService } from '@affine/core/modules/doc';
import { GuardService } from '@affine/core/modules/permissions';
import { WorkspaceService } from '@affine/core/modules/workspace';
import type { AffineDNDData } from '@affine/core/types/dnd';
import { useI18n } from '@affine/i18n';
@@ -17,6 +16,7 @@ import { useLiveData, useService } from '@toeverything/infra';
import clsx from 'clsx';
import { type HTMLProps, useCallback, useState } from 'react';
import { useGuard } from '../../guard';
import { DocPropertyIcon } from '../icons/doc-property-icon';
import { EditDocPropertyMenuItems } from '../menu/edit-doc-property';
import {
@@ -38,13 +38,10 @@ const PropertyItem = ({
) => void;
}) => {
const t = useI18n();
const guardService = useService(GuardService);
const workspaceService = useService(WorkspaceService);
const docsService = useService(DocsService);
const [moreMenuOpen, setMoreMenuOpen] = useState(defaultOpenEditMenu);
const canEditPropertyInfo = useLiveData(
guardService.can$('Workspace_Properties_Update')
);
const canEditPropertyInfo = useGuard('Workspace_Properties_Update');
const typeInfo = isSupportedDocPropertyType(propertyInfo.type)
? DocPropertyTypes[propertyInfo.type]
@@ -1,7 +1,6 @@
import { Divider, IconButton, Tooltip } from '@affine/component';
import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
import { DocsService } from '@affine/core/modules/doc';
import { GuardService } from '@affine/core/modules/permissions';
import { generateUniqueNameInSequence } from '@affine/core/utils/unique-name';
import { useI18n } from '@affine/i18n';
import track from '@affine/track';
@@ -13,6 +12,7 @@ import {
import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useState } from 'react';
import { useGuard } from '../../guard';
import { DocPropertyManager } from '../manager';
import {
DocPropertyTypes,
@@ -29,12 +29,9 @@ export const DocPropertySidebar = () => {
const [newPropertyId, setNewPropertyId] = useState<string>();
const docsService = useService(DocsService);
const guardService = useService(GuardService);
const propertyList = docsService.propertyList;
const properties = useLiveData(propertyList.properties$);
const canEditPropertyInfo = useLiveData(
guardService.can$('Workspace_Properties_Update')
);
const canEditPropertyInfo = useGuard('Workspace_Properties_Update');
const onAddProperty = useCallback(
(option: { type: string; name: string }) => {
if (!isSupportedDocPropertyType(option.type)) {
@@ -16,7 +16,6 @@ import type {
DatabaseValueCell,
} from '@affine/core/modules/doc-info/types';
import { DocIntegrationPropertiesTable } from '@affine/core/modules/integration';
import { GuardService } from '@affine/core/modules/permissions';
import { ViewService, WorkbenchService } from '@affine/core/modules/workbench';
import type { AffineDNDData } from '@affine/core/types/dnd';
import { useI18n } from '@affine/i18n';
@@ -32,6 +31,7 @@ import clsx from 'clsx';
import type React from 'react';
import { forwardRef, useCallback, useMemo, useState } from 'react';
import { useGuard } from '../guard';
import { DocPropertyIcon } from './icons/doc-property-icon';
import { CreatePropertyMenuItems } from './menu/create-doc-property';
import { EditDocPropertyMenuItems } from './menu/edit-doc-property';
@@ -290,18 +290,13 @@ const DocWorkspacePropertiesTableBody = forwardRef<
const workbenchService = useService(WorkbenchService);
const viewService = useServiceOptional(ViewService);
const docService = useService(DocService);
const guardService = useService(GuardService);
const properties = useLiveData(docsService.propertyList.sortedProperties$);
const [addMoreCollapsed, setAddMoreCollapsed] = useState(true);
const [newPropertyId, setNewPropertyId] = useState<string | null>(null);
const canEditProperty = useLiveData(
guardService.can$('Doc_Update', docService.doc.id)
);
const canEditPropertyInfo = useLiveData(
guardService.can$('Workspace_Properties_Update')
);
const canEditProperty = useGuard('Doc_Update', docService.doc.id);
const canEditPropertyInfo = useGuard('Workspace_Properties_Update');
const handlePropertyAdded = useCallback(
(property: DocCustomPropertyInfo) => {
@@ -1,24 +0,0 @@
import {
type DocPermissionActions,
GuardService,
} from '@affine/core/modules/permissions';
import { useLiveData, useService } from '@toeverything/infra';
import type React from 'react';
export const DocPermissionGuard = ({
docId,
children,
permission,
}: {
docId: string;
permission: DocPermissionActions;
children: (can: boolean | undefined) => React.ReactNode;
}) => {
const guardService = useService(GuardService);
const can = useLiveData(guardService.can$(permission, docId));
if (typeof children === 'function') {
return children(can);
}
throw new Error('children must be a function');
};
@@ -0,0 +1,25 @@
import {
type DocPermissionActions,
type WorkspacePermissionActions,
} from '@affine/core/modules/permissions';
import type React from 'react';
import { useGuard } from './use-guard';
export const Guard = <
T extends WorkspacePermissionActions | DocPermissionActions,
>(
props: {
permission: T;
children: (can: boolean | undefined) => React.ReactNode;
} & (T extends DocPermissionActions ? { docId: string } : {})
) => {
const { permission, children, ...rest } = props;
const docId = 'docId' in rest ? [rest.docId] : [];
const can = useGuard(permission, ...(docId as any));
if (typeof children === 'function') {
return children(can);
}
throw new Error('children must be a function');
};
@@ -0,0 +1,2 @@
export * from './guard';
export * from './use-guard';
@@ -0,0 +1,29 @@
import {
type DocPermissionActions,
GuardService,
type WorkspacePermissionActions,
} from '@affine/core/modules/permissions';
import { useLiveData, useService } from '@toeverything/infra';
import { useEffect, useMemo } from 'react';
export const useGuard = <
T extends WorkspacePermissionActions | DocPermissionActions,
>(
action: T,
...args: T extends DocPermissionActions ? [string] : []
) => {
const guardService = useService(GuardService);
useEffect(() => {
guardService.revalidateCan(action, ...args);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [action, guardService, ...args]);
const livedata$ = useMemo(
() => guardService.can$(action, ...args),
// eslint-disable-next-line react-hooks/exhaustive-deps
[action, guardService, ...args]
);
const can = useLiveData(livedata$);
return can;
};
@@ -14,7 +14,6 @@ import {
CompatibleFavoriteItemsAdapter,
FavoriteService,
} from '@affine/core/modules/favorite';
import { GuardService } from '@affine/core/modules/permissions';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { WorkspaceService } from '@affine/core/modules/workspace';
import type { Collection, DeleteCollectionInfo } from '@affine/env/filter';
@@ -41,6 +40,7 @@ import { useCallback, useState } from 'react';
import { usePageHelper } from '../../blocksuite/block-suite-page-list/utils';
import type { CollectionService } from '../../modules/collection';
import { useGuard } from '../guard';
import { IsFavoriteIcon } from '../pure/icons';
import { FavoriteTag } from './components/favorite-tag';
import * as styles from './list.css';
@@ -68,15 +68,13 @@ const PageOperationCellMenuItem = ({
workspaceService,
compatibleFavoriteItemsAdapter: favAdapter,
workbenchService,
guardService,
} = useServices({
WorkspaceService,
CompatibleFavoriteItemsAdapter,
WorkbenchService,
GuardService,
});
const canMoveToTrash = useLiveData(guardService.can$('Doc_Trash', page.id));
const canMoveToTrash = useGuard('Doc_Trash', page.id);
const currentWorkspace = workspaceService.workspace;
const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc'));
const workbench = workbenchService.workbench;