mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-08 20:57:08 +08:00
feat(core): guard service (#9816)
This commit is contained in:
@@ -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
|
||||
|
||||
+23
-10
@@ -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']()
|
||||
|
||||
Reference in New Issue
Block a user