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,7 +5,7 @@ import {
toast,
Tooltip,
} from '@affine/component';
import { DocPermissionGuard } from '@affine/core/components/guard/doc-guard';
import { Guard } from '@affine/core/components/guard';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { DocsService } from '@affine/core/modules/doc';
@@ -274,7 +274,7 @@ export const ExplorerDocNode = ({
dropEffect={handleDropEffectOnDoc}
data-testid={`explorer-doc-${docId}`}
>
<DocPermissionGuard docId={docId} permission="Doc_Read">
<Guard docId={docId} permission="Doc_Read">
{canRead =>
canRead
? children?.map((child, index) => (
@@ -291,7 +291,7 @@ export const ExplorerDocNode = ({
))
: null
}
</DocPermissionGuard>
</Guard>
</ExplorerTreeNode>
);
};
@@ -6,7 +6,7 @@ import {
useConfirmModal,
} from '@affine/component';
import { usePageHelper } from '@affine/core/blocksuite/block-suite-page-list/utils';
import { DocPermissionGuard } from '@affine/core/components/guard/doc-guard';
import { Guard } from '@affine/core/components/guard';
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
@@ -175,7 +175,7 @@ export const useExplorerDocNodeOperations = (
{
index: 99,
view: (
<DocPermissionGuard docId={docId} permission="Doc_Update">
<Guard docId={docId} permission="Doc_Update">
{canEdit => (
<MenuItem
prefixIcon={<LinkedPageIcon />}
@@ -185,7 +185,7 @@ export const useExplorerDocNodeOperations = (
{t['com.affine.page-operation.add-linked-page']()}
</MenuItem>
)}
</DocPermissionGuard>
</Guard>
),
},
{
@@ -239,7 +239,7 @@ export const useExplorerDocNodeOperations = (
{
index: 10000,
view: (
<DocPermissionGuard docId={docId} permission="Doc_Trash">
<Guard docId={docId} permission="Doc_Trash">
{canMoveToTrash => (
<MenuItem
type={'danger'}
@@ -250,7 +250,7 @@ export const useExplorerDocNodeOperations = (
{t['com.affine.moveToTrash.title']()}
</MenuItem>
)}
</DocPermissionGuard>
</Guard>
),
},
],
@@ -10,7 +10,7 @@ import {
useDropTarget,
} from '@affine/component';
import { RenameModal } from '@affine/component/rename-modal';
import { DocPermissionGuard } from '@affine/core/components/guard/doc-guard';
import { Guard } from '@affine/core/components/guard';
import { AppSidebarService } from '@affine/core/modules/app-sidebar';
import type { DocPermissionActions } from '@affine/core/modules/permissions';
import { WorkbenchLink } from '@affine/core/modules/workbench';
@@ -285,7 +285,7 @@ export const ExplorerTreeNode = ({
? {
index: 0,
view: renameableGuard ? (
<DocPermissionGuard
<Guard
permission={renameableGuard.action}
docId={renameableGuard.docId}
>
@@ -300,7 +300,7 @@ export const ExplorerTreeNode = ({
{t['com.affine.menu.rename']()}
</MenuItem>
)}
</DocPermissionGuard>
</Guard>
) : (
<MenuItem
key={'explorer-tree-rename'}
@@ -4,9 +4,9 @@ import { AIProvider } from '@affine/core/blocksuite/ai';
import type { AffineEditorContainer } from '@affine/core/blocksuite/block-suite-editor';
import { EditorOutlineViewer } from '@affine/core/blocksuite/outline-viewer';
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
import { useGuard } from '@affine/core/components/guard';
import { PageNotFound } from '@affine/core/desktop/pages/404';
import { EditorService } from '@affine/core/modules/editor';
import { GuardService } from '@affine/core/modules/permissions';
import { DebugLogger } from '@affine/debug';
import { GfxControllerIdentifier } from '@blocksuite/affine/block-std/gfx';
import { DisposableGroup } from '@blocksuite/affine/global/disposable';
@@ -85,7 +85,6 @@ function DocPeekPreviewEditor({
const defaultOpenProperty = useLiveData(editor.defaultOpenProperty$);
const workbench = useService(WorkbenchService).workbench;
const peekView = useService(PeekViewService).peekView;
const guardService = useService(GuardService);
const editorElement = useLiveData(editor.editorContainer$);
const isInTrash = useLiveData(doc.record.trash$);
@@ -151,7 +150,7 @@ function DocPeekPreviewEditor({
peekView.close();
}, [doc, peekView, workbench]);
const canEdit = useLiveData(guardService.can$('Doc_Update', doc.id));
const canEdit = useGuard('Doc_Update', doc.id);
const readonly = !canEdit || isInTrash;
@@ -220,8 +219,7 @@ export function DocPeekPreview({
!animating
);
const guardService = useService(GuardService);
const canAccess = useLiveData(guardService.can$('Doc_Read', docId));
const canAccess = useGuard('Doc_Read', docId);
// if sync engine has been synced and the page is null, show 404 page.
if (!doc || !editor || !canAccess) {
@@ -66,17 +66,6 @@ export class GuardService extends Service {
const docId = args[0];
return LiveData.from(
new Observable(subscriber => {
// revalidate permission
if (docId) {
this.revalidateDocPermission(docId);
} else {
this.revalidateWorkspacePermission();
}
// revalidate workspace permission if it's not initialized
if (this.isAdmin$.value === null) {
this.workspacePermissionService.permission.revalidate();
}
let prev: boolean | undefined = undefined;
const subscription = combineLatest([
@@ -128,6 +117,29 @@ export class GuardService extends Service {
return permissions[action as keyof typeof permissions] ?? false;
}
revalidateCan<T extends WorkspacePermissionActions | DocPermissionActions>(
_action: T,
...args: T extends DocPermissionActions ? [string] : []
) {
// revalidate workspace permission if it's not initialized
if (this.isAdmin$.value === null) {
this.workspacePermissionService.permission.revalidate();
}
if (this.isAdmin$.value === true) {
// if the user is admin, the permission is always true
return;
}
const docId = args[0];
// revalidate permission
if (docId) {
this.revalidateDocPermission(docId);
} else {
this.revalidateWorkspacePermission();
}
}
private readonly revalidateWorkspacePermission = effect(
exhaustMapWithTrailing(() =>
fromPromise(() => this.guardStore.getWorkspacePermissions()).pipe(
@@ -8,12 +8,12 @@ import {
Tooltip,
useConfirmModal,
} from '@affine/component';
import { useGuard } from '@affine/core/components/guard';
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
import { DocService } from '@affine/core/modules/doc';
import {
DocGrantedUsersService,
type GrantedUser,
GuardService,
WorkspacePermissionService,
} from '@affine/core/modules/permissions';
import { UserFriendlyError } from '@affine/error';
@@ -131,7 +131,6 @@ const Options = ({
const t = useI18n();
const docGrantedUsersService = useService(DocGrantedUsersService);
const docService = useService(DocService);
const guardService = useService(GuardService);
const workspacePermissionService = useService(WorkspacePermissionService);
const isWorkspaceOwner = useLiveData(
workspacePermissionService.permission.isOwner$
@@ -140,11 +139,8 @@ const Options = ({
const { openConfirmModal } = useConfirmModal();
const canTransferOwner =
useLiveData(guardService.can$('Doc_TransferOwner', docService.doc.id)) &&
!!isWorkspaceOwner;
const canManageUsers = useLiveData(
guardService.can$('Doc_Users_Manage', docService.doc.id)
);
useGuard('Doc_TransferOwner', docService.doc.id) && !!isWorkspaceOwner;
const canManageUsers = useGuard('Doc_Users_Manage', docService.doc.id);
const updateUserRole = useCallback(
async (userId: string, role: DocRole) => {
@@ -1,9 +1,9 @@
import { Skeleton } from '@affine/component';
import { useGuard } from '@affine/core/components/guard';
import { DocService } from '@affine/core/modules/doc';
import {
DocGrantedUsersService,
type GrantedUser,
GuardService,
} from '@affine/core/modules/permissions';
import { useI18n } from '@affine/i18n';
import { ArrowLeftBigIcon } from '@blocksuite/icons/rc';
@@ -33,11 +33,8 @@ export const MemberManagement = ({
docGrantedUsersService.grantedUserCount$
);
const docService = useService(DocService);
const guardService = useService(GuardService);
const canManageUsers = useLiveData(
guardService.can$('Doc_Users_Manage', docService.doc.id)
);
const canManageUsers = useGuard('Doc_Users_Manage', docService.doc.id);
const t = useI18n();
@@ -1,8 +1,8 @@
import { Divider, Skeleton } from '@affine/component';
import { Button } from '@affine/component/ui/button';
import { useGuard } from '@affine/core/components/guard';
import { ServerService } from '@affine/core/modules/cloud';
import { DocService } from '@affine/core/modules/doc';
import { GuardService } from '@affine/core/modules/permissions';
import { ShareInfoService } from '@affine/core/modules/share-doc';
import { useI18n } from '@affine/i18n';
import { useLiveData, useService } from '@toeverything/infra';
@@ -64,15 +64,10 @@ export const AFFiNESharePage = (
const shareInfoService = useService(ShareInfoService);
const serverService = useService(ServerService);
const docService = useService(DocService);
const guardService = useService(GuardService);
const canManageUsers = useLiveData(
guardService.can$('Doc_Users_Manage', docService.doc.id)
);
const canManageUsers = useGuard('Doc_Users_Manage', docService.doc.id);
const canPublish = useLiveData(
guardService.can$('Doc_Publish', docService.doc.id)
);
const canPublish = useGuard('Doc_Publish', docService.doc.id);
useEffect(() => {
shareInfoService.shareInfo.revalidate();