feat(core): guard service (#9816)

This commit is contained in:
EYHN
2025-02-10 07:26:38 +08:00
committed by GitHub
parent 879157b938
commit 92f4f0c2d9
89 changed files with 1520 additions and 522 deletions
@@ -17,6 +17,7 @@ import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
import { DocsService } from '@affine/core/modules/doc';
import { DocDatabaseBacklinkInfo } from '@affine/core/modules/doc-info';
import { DocsSearchService } from '@affine/core/modules/docs-search';
import { GuardService } from '@affine/core/modules/permissions';
import { useI18n } from '@affine/i18n';
import { PlusIcon } from '@blocksuite/icons/rc';
import { LiveData, useLiveData, useServices } from '@toeverything/infra';
@@ -30,12 +31,17 @@ export const DocInfoSheet = ({
docId: string;
defaultOpenProperty?: DefaultOpenProperty;
}) => {
const { docsSearchService, docsService } = useServices({
const { docsSearchService, docsService, guardService } = useServices({
DocsSearchService,
DocsService,
GuardService,
});
const t = useI18n();
const canEditPropertyInfo = useLiveData(
guardService.can$('Workspace_Properties_Update')
);
const canEditProperty = useLiveData(guardService.can$('Doc_Update', docId));
const links = useLiveData(
useMemo(
() => LiveData.from(docsSearchService.watchRefsFrom(docId), null),
@@ -102,16 +108,11 @@ export const DocInfoSheet = ({
key={property.id}
propertyInfo={property}
defaultOpenEditMenu={newPropertyId === property.id}
propertyInfoReadonly={!canEditPropertyInfo}
readonly={!canEditProperty}
/>
))}
<Menu
items={<CreatePropertyMenuItems onCreated={onPropertyAdded} />}
contentOptions={{
onClick(e) {
e.stopPropagation();
},
}}
>
{!canEditPropertyInfo ? (
<Button
variant="plain"
prefix={<PlusIcon />}
@@ -119,7 +120,26 @@ export const DocInfoSheet = ({
>
{t['com.affine.page-properties.add-property']()}
</Button>
</Menu>
) : (
<Menu
items={
<CreatePropertyMenuItems onCreated={onPropertyAdded} />
}
contentOptions={{
onClick(e) {
e.stopPropagation();
},
}}
>
<Button
variant="plain"
prefix={<PlusIcon />}
className={styles.addPropertyButton}
>
{t['com.affine.page-properties.add-property']()}
</Button>
</Menu>
)}
</PropertyCollapsibleContent>
</PropertyCollapsibleSection>
<Divider size="thinner" />
@@ -1,4 +1,5 @@
import { Loading } from '@affine/component';
import { DocPermissionGuard } from '@affine/core/components/guard/doc-guard';
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
import { DocsService } from '@affine/core/modules/doc';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
@@ -136,10 +137,16 @@ export const ExplorerDocNode = ({
<ExplorerDocNode key={child.docId} docId={child.docId} isLinked />
))}
<AddItemPlaceholder
label={t['com.affine.rootAppSidebar.explorer.doc-add-tooltip']()}
onClick={handleAddLinkedPage}
/>
<DocPermissionGuard docId={docId} permission="Doc_Update">
{canEdit =>
canEdit ? (
<AddItemPlaceholder
label={t['com.affine.rootAppSidebar.explorer.doc-add-tooltip']()}
onClick={handleAddLinkedPage}
/>
) : null
}
</DocPermissionGuard>
</ExplorerTreeNode>
);
};
@@ -1,5 +1,4 @@
import {
IconButton,
MenuItem,
MenuSeparator,
MenuSub,
@@ -7,6 +6,7 @@ import {
useConfirmModal,
} from '@affine/component';
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
import { DocPermissionGuard } from '@affine/core/components/guard/doc-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';
@@ -24,8 +24,6 @@ import {
InformationIcon,
LinkedPageIcon,
OpenInNewIcon,
PlusIcon,
SplitViewIcon,
} from '@blocksuite/icons/rc';
import { useLiveData, useService, useServices } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
@@ -121,7 +119,7 @@ export const useExplorerDocNodeOperations = (
track.$.navigationPanel.docs.createDoc({ control: 'linkDoc' });
track.$.navigationPanel.docs.linkDoc({ control: 'createDoc' });
options.openNodeCollapsed();
}, [createPage, docsService, docId, options]);
}, [createPage, docId, docsService, options]);
const handleToggleFavoriteDoc = useCallback(() => {
compatibleFavoriteItemsAdapter.toggle(docId, 'doc');
@@ -175,7 +173,6 @@ export const useExplorerDocNodeOperationsMenu = (
handleAddLinkedPage,
handleDuplicate,
handleToggleFavoriteDoc,
handleOpenInSplitView,
handleOpenInNewTab,
handleMoveToTrash,
handleRename,
@@ -187,21 +184,19 @@ export const useExplorerDocNodeOperationsMenu = (
return useMemo(
() => [
{
index: 0,
inline: true,
view: (
<IconButton
size="16"
icon={<PlusIcon />}
tooltip={t['com.affine.rootAppSidebar.explorer.doc-add-tooltip']()}
onClick={handleAddLinkedPage}
/>
),
},
{
index: 10,
view: <DocRenameSubMenu onConfirm={handleRename} initialName={title} />,
view: (
<DocPermissionGuard docId={docId} permission="Doc_Update">
{canEdit => (
<DocRenameSubMenu
onConfirm={handleRename}
initialName={title}
disabled={!canEdit}
/>
)}
</DocPermissionGuard>
),
},
{
index: 11,
@@ -229,12 +224,17 @@ export const useExplorerDocNodeOperationsMenu = (
{
index: 97,
view: (
<MenuItem
prefixIcon={<LinkedPageIcon />}
onClick={handleAddLinkedPage}
>
{t['com.affine.page-operation.add-linked-page']()}
</MenuItem>
<DocPermissionGuard docId={docId} permission="Doc_Update">
{canEdit => (
<MenuItem
prefixIcon={<LinkedPageIcon />}
onClick={handleAddLinkedPage}
disabled={!canEdit}
>
{t['com.affine.page-operation.add-linked-page']()}
</MenuItem>
)}
</DocPermissionGuard>
),
},
{
@@ -253,21 +253,6 @@ export const useExplorerDocNodeOperationsMenu = (
</MenuItem>
),
},
...(BUILD_CONFIG.isElectron
? [
{
index: 100,
view: (
<MenuItem
prefixIcon={<SplitViewIcon />}
onClick={handleOpenInSplitView}
>
{t['com.affine.workbench.split-view.page-menu-open']()}
</MenuItem>
),
},
]
: []),
{
index: 199,
view: (
@@ -288,13 +273,18 @@ export const useExplorerDocNodeOperationsMenu = (
{
index: 10000,
view: (
<MenuItem
type={'danger'}
prefixIcon={<DeleteIcon />}
onClick={handleMoveToTrash}
>
{t['com.affine.moveToTrash.title']()}
</MenuItem>
<DocPermissionGuard docId={docId} permission="Doc_Trash">
{canMoveToTrash => (
<MenuItem
type={'danger'}
prefixIcon={<DeleteIcon />}
onClick={handleMoveToTrash}
disabled={!canMoveToTrash}
>
{t['com.affine.moveToTrash.title']()}
</MenuItem>
)}
</DocPermissionGuard>
),
},
],
@@ -305,7 +295,6 @@ export const useExplorerDocNodeOperationsMenu = (
handleDuplicate,
handleMoveToTrash,
handleOpenInNewTab,
handleOpenInSplitView,
handleRename,
handleToggleFavoriteDoc,
t,
@@ -14,6 +14,7 @@ export const RenameSubMenu = ({
children,
menuProps,
onConfirm,
disabled,
...props
}: RenameSubMenuProps) => {
const t = useI18n();
@@ -33,6 +34,7 @@ export const RenameSubMenu = ({
triggerOptions={{
prefixIcon: icon ?? <EditIcon />,
suffixIcon: null,
disabled,
...triggerOptions,
}}
items={
@@ -28,6 +28,7 @@ export interface RenameSubMenuProps
/** MenuItem.text */
text?: string;
menuProps?: Partial<MenuSubProps>;
disabled?: boolean;
}
export interface RenameDialogProps
@@ -5,6 +5,7 @@ import {
MobileMenuSub,
useConfirmModal,
} from '@affine/component';
import { DocPermissionGuard } from '@affine/core/components/guard/doc-guard';
import { MoveToTrash } from '@affine/core/components/page-list';
import {
type DocRecord,
@@ -58,16 +59,28 @@ export const ResolveConflictOperations = ({
return (
<>
<MobileMenuItem
prefixIcon={<CalendarXmarkIcon />}
onClick={() => {
handleRemoveJournalMark(docRecord.id);
}}
data-testid="journal-conflict-remove-mark"
>
{t['com.affine.page-properties.property.journal-remove']()}
</MobileMenuItem>
<MoveToTrash onSelect={() => handleOpenTrashModal(docRecord)} />
<DocPermissionGuard docId={docRecord.id} permission="Doc_Update">
{canEdit => (
<MobileMenuItem
prefixIcon={<CalendarXmarkIcon />}
onClick={() => {
handleRemoveJournalMark(docRecord.id);
}}
data-testid="journal-conflict-remove-mark"
disabled={!canEdit}
>
{t['com.affine.page-properties.property.journal-remove']()}
</MobileMenuItem>
)}
</DocPermissionGuard>
<DocPermissionGuard docId={docRecord.id} permission="Doc_Trash">
{canTrash => (
<MoveToTrash
onSelect={() => handleOpenTrashModal(docRecord)}
disabled={!canTrash}
/>
)}
</DocPermissionGuard>
</>
);
};
@@ -2,7 +2,6 @@ import { useThemeColorV2 } from '@affine/component';
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
import { AffineErrorBoundary } from '@affine/core/components/affine/affine-error-boundary';
import { useActiveBlocksuiteEditor } from '@affine/core/components/hooks/use-block-suite-editor';
import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-page-meta';
import { usePageDocumentTitle } from '@affine/core/components/hooks/use-global-state';
import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-helper';
import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
@@ -17,6 +16,7 @@ import { EditorService } from '@affine/core/modules/editor';
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
import { GlobalContextService } from '@affine/core/modules/global-context';
import { JournalService } from '@affine/core/modules/journal';
import { GuardService } from '@affine/core/modules/permissions';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { ViewService } from '@affine/core/modules/workbench/services/view';
import { WorkspaceService } from '@affine/core/modules/workspace';
@@ -56,6 +56,7 @@ const DetailPageImpl = () => {
globalContextService,
featureFlagService,
aIButtonService,
guardService,
} = useServices({
WorkbenchService,
ViewService,
@@ -65,6 +66,7 @@ const DetailPageImpl = () => {
GlobalContextService,
FeatureFlagService,
AIButtonService,
GuardService,
});
const editor = editorService.editor;
const workspace = workspaceService.workspace;
@@ -84,7 +86,6 @@ const DetailPageImpl = () => {
featureFlagService.flags.enable_mobile_keyboard_toolbar.value;
const enableEdgelessEditing =
featureFlagService.flags.enable_mobile_edgeless_editing.value;
const { setDocReadonly } = useDocMetaHelper();
// TODO(@eyhn): remove jotai here
const [_, setActiveBlockSuiteEditor] = useActiveBlocksuiteEditor();
@@ -111,19 +112,6 @@ const DetailPageImpl = () => {
};
}, [doc, globalContext, mode]);
useEffect(() => {
setDocReadonly(
doc.id,
!enableKeyboardToolbar || (mode === 'edgeless' && !enableEdgelessEditing)
);
}, [
enableKeyboardToolbar,
doc.id,
setDocReadonly,
mode,
enableEdgelessEditing,
]);
useEffect(() => {
aIButtonService.presentAIButton(true);
@@ -202,6 +190,14 @@ const DetailPageImpl = () => {
[docCollection.id, editor, jumpToPageBlock, openPage, server]
);
const canEdit = useLiveData(guardService.can$('Doc_Update', doc.id));
const readonly =
!canEdit ||
isInTrash ||
!enableKeyboardToolbar ||
(mode === 'edgeless' && !enableEdgelessEditing);
return (
<FrameworkScope scope={editor.scope}>
<div className={styles.mainContainer}>
@@ -216,7 +212,7 @@ const DetailPageImpl = () => {
>
{/* Add a key to force rerender when page changed, to avoid error boundary persisting. */}
<AffineErrorBoundary key={doc.id} className={styles.errorBoundary}>
<PageDetailEditor onLoad={onLoad} />
<PageDetailEditor onLoad={onLoad} readonly={readonly} />
</AffineErrorBoundary>
</div>
</div>
@@ -11,6 +11,7 @@ import { DocInfoSheet } from '@affine/core/mobile/components';
import { MobileTocMenu } from '@affine/core/mobile/components/toc-menu';
import { DocService } from '@affine/core/modules/doc';
import { EditorService } from '@affine/core/modules/editor';
import { GuardService } from '@affine/core/modules/permissions';
import { ViewService } from '@affine/core/modules/workbench/services/view';
import { preventDefault } from '@affine/core/utils';
import { useI18n } from '@affine/i18n';
@@ -34,6 +35,8 @@ export const PageHeaderMenuButton = () => {
const t = useI18n();
const docId = useService(DocService).doc.id;
const guardService = useService(GuardService);
const canEdit = useLiveData(guardService.can$('Doc_Update', docId));
const editorService = useService(EditorService);
const editorContainer = useLiveData(editorService.editor.editorContainer$);
@@ -94,6 +97,7 @@ export const PageHeaderMenuButton = () => {
prefixIcon={primaryMode === 'page' ? <EdgelessIcon /> : <PageIcon />}
data-testid="editor-option-menu-mode-switch"
onSelect={handleSwitchMode}
disabled={!canEdit}
>
{primaryMode === 'page'
? t['com.affine.editorDefaultMode.edgeless']()