mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-08 12:45:55 +08:00
feat(core): guard service (#9816)
This commit is contained in:
@@ -47,10 +47,7 @@ export const DocInfoDialog = ({
|
||||
className={styles.titleContainer}
|
||||
data-testid="info-modal-title"
|
||||
>
|
||||
<BlocksuiteHeaderTitle
|
||||
docId={docId}
|
||||
className={styles.titleStyle}
|
||||
/>
|
||||
<BlocksuiteHeaderTitle className={styles.titleStyle} />
|
||||
</div>
|
||||
<InfoTable docId={docId} onClose={() => close()} />
|
||||
</Scrollable.Viewport>
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
DatabaseValueCell,
|
||||
} from '@affine/core/modules/doc-info/types';
|
||||
import { DocsSearchService } from '@affine/core/modules/docs-search';
|
||||
import { GuardService } from '@affine/core/modules/permissions';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import track from '@affine/track';
|
||||
import { PlusIcon } from '@blocksuite/icons/rc';
|
||||
@@ -33,10 +34,15 @@ export const InfoTable = ({
|
||||
onClose: () => void;
|
||||
}) => {
|
||||
const t = useI18n();
|
||||
const { docsSearchService, docsService } = useServices({
|
||||
const { docsSearchService, docsService, guardService } = useServices({
|
||||
DocsSearchService,
|
||||
DocsService,
|
||||
GuardService,
|
||||
});
|
||||
const canEditPropertyInfo = useLiveData(
|
||||
guardService.can$('Workspace_Properties_Update')
|
||||
);
|
||||
const canEditProperty = useLiveData(guardService.can$('Doc_Update', docId));
|
||||
const [newPropertyId, setNewPropertyId] = useState<string | null>(null);
|
||||
const properties = useLiveData(docsService.propertyList.sortedProperties$);
|
||||
const links = useLiveData(
|
||||
@@ -133,6 +139,8 @@ export const InfoTable = ({
|
||||
<DocPropertyRow
|
||||
key={property.id}
|
||||
propertyInfo={property}
|
||||
readonly={!canEditProperty}
|
||||
propertyInfoReadonly={!canEditPropertyInfo}
|
||||
defaultOpenEditMenu={newPropertyId === property.id}
|
||||
onChange={value => onPropertyChange(property, value)}
|
||||
onPropertyInfoChange={(...args) =>
|
||||
@@ -140,22 +148,33 @@ export const InfoTable = ({
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<Menu
|
||||
items={<CreatePropertyMenuItems onCreated={onPropertyAdded} />}
|
||||
contentOptions={{
|
||||
onClick(e) {
|
||||
e.stopPropagation();
|
||||
},
|
||||
}}
|
||||
>
|
||||
{!canEditPropertyInfo ? (
|
||||
<Button
|
||||
disabled
|
||||
variant="plain"
|
||||
prefix={<PlusIcon />}
|
||||
className={styles.addPropertyButton}
|
||||
>
|
||||
{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" />
|
||||
|
||||
@@ -5,10 +5,11 @@ import {
|
||||
VirtualizedPageList,
|
||||
} from '@affine/core/components/page-list';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import type { Filter } from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import {
|
||||
@@ -26,8 +27,11 @@ import { AllPageHeader } from './all-page-header';
|
||||
export const AllPage = () => {
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const globalContext = useService(GlobalContextService).globalContext;
|
||||
const permissionService = useService(WorkspacePermissionService);
|
||||
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
||||
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
|
||||
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
|
||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||
|
||||
const [filters, setFilters] = useState<Filter[]>([]);
|
||||
const filteredPageMetas = useFilteredPageMetas(pageMetas, {
|
||||
@@ -65,6 +69,7 @@ export const AllPage = () => {
|
||||
<FilterContainer filters={filters} onChangeFilters={setFilters} />
|
||||
{filteredPageMetas.length > 0 ? (
|
||||
<VirtualizedPageList
|
||||
disableMultiDelete={!isAdmin && !isOwner}
|
||||
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
||||
filters={filters}
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { VirtualizedPageList } from '@affine/core/components/page-list';
|
||||
import { CollectionService } from '@affine/core/modules/collection';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import type { Collection } from '@affine/env/filter';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
@@ -30,6 +31,9 @@ export const CollectionDetail = ({
|
||||
const { workspaceDialogService } = useServices({
|
||||
WorkspaceDialogService,
|
||||
});
|
||||
const permissionService = useService(WorkspacePermissionService);
|
||||
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
|
||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||
const [hideHeaderCreateNew, setHideHeaderCreateNew] = useState(true);
|
||||
|
||||
const handleEditCollection = useCallback(() => {
|
||||
@@ -50,6 +54,7 @@ export const CollectionDetail = ({
|
||||
<VirtualizedPageList
|
||||
collection={collection}
|
||||
setHideHeaderCreateNewPage={setHideHeaderCreateNew}
|
||||
disableMultiDelete={!isAdmin && !isOwner}
|
||||
/>
|
||||
</ViewBody>
|
||||
</>
|
||||
|
||||
+1
-4
@@ -158,10 +158,7 @@ export function NormalPageHeader({ page, workspace }: PageHeaderProps) {
|
||||
<ViewTitle title={title} />
|
||||
<ViewIcon icon={currentMode ?? 'page'} />
|
||||
<EditorModeSwitch />
|
||||
<BlocksuiteHeaderTitle
|
||||
docId={page.id}
|
||||
inputHandleRef={titleInputHandleRef}
|
||||
/>
|
||||
<BlocksuiteHeaderTitle inputHandleRef={titleInputHandleRef} />
|
||||
<TemplateMark />
|
||||
<div className={styles.iconButtonContainer}>
|
||||
{hideCollect ? null : (
|
||||
|
||||
@@ -6,12 +6,12 @@ import { PageAIOnboarding } from '@affine/core/components/affine/ai-onboarding';
|
||||
import { EditorOutlineViewer } from '@affine/core/components/blocksuite/outline-viewer';
|
||||
import { DocPropertySidebar } from '@affine/core/components/doc-properties/sidebar';
|
||||
import { useAppSettingHelper } from '@affine/core/components/hooks/affine/use-app-setting-helper';
|
||||
import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||
import { DocService } from '@affine/core/modules/doc';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { PeekViewService } from '@affine/core/modules/peek-view';
|
||||
import { GuardService } from '@affine/core/modules/permissions';
|
||||
import { RecentDocsService } from '@affine/core/modules/quicksearch';
|
||||
import { ViewService } from '@affine/core/modules/workbench';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
@@ -73,6 +73,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
workspaceService,
|
||||
globalContextService,
|
||||
featureFlagService,
|
||||
guardService,
|
||||
} = useServices({
|
||||
WorkbenchService,
|
||||
ViewService,
|
||||
@@ -81,6 +82,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
WorkspaceService,
|
||||
GlobalContextService,
|
||||
FeatureFlagService,
|
||||
GuardService,
|
||||
});
|
||||
const workbench = workbenchService.workbench;
|
||||
const editor = editorService.editor;
|
||||
@@ -98,7 +100,6 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
const isSideBarOpen = useLiveData(workbench.sidebarOpen$);
|
||||
const { appSettings } = useAppSettingHelper();
|
||||
const chatPanelRef = useRef<ChatPanel | null>(null);
|
||||
const { setDocReadonly } = useDocMetaHelper();
|
||||
|
||||
const peekView = useService(PeekViewService).peekView;
|
||||
|
||||
@@ -149,12 +150,6 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
return;
|
||||
}, [doc, globalContext, isActiveView, mode]);
|
||||
|
||||
useEffect(() => {
|
||||
if ('isMobile' in environment && environment.isMobile) {
|
||||
setDocReadonly(doc.id, true);
|
||||
}
|
||||
}, [doc.id, setDocReadonly]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActiveView) {
|
||||
globalContext.isTrashDoc.set(!!isInTrash);
|
||||
@@ -273,6 +268,10 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
|
||||
const [dragging, setDragging] = useState(false);
|
||||
|
||||
const canEdit = useLiveData(guardService.can$('Doc_Update', doc.id));
|
||||
|
||||
const readonly = !canEdit || isInTrash;
|
||||
|
||||
return (
|
||||
<FrameworkScope scope={editor.scope}>
|
||||
<ViewHeader>
|
||||
@@ -302,7 +301,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
styles.editorContainer
|
||||
)}
|
||||
>
|
||||
<PageDetailEditor onLoad={onLoad} />
|
||||
<PageDetailEditor onLoad={onLoad} readonly={readonly} />
|
||||
</Scrollable.Viewport>
|
||||
<Scrollable.Scrollbar
|
||||
className={clsx({
|
||||
|
||||
+31
-14
@@ -8,6 +8,7 @@ import {
|
||||
Scrollable,
|
||||
useConfirmModal,
|
||||
} from '@affine/component';
|
||||
import { DocPermissionGuard } from '@affine/core/components/guard/doc-guard';
|
||||
import { useJournalRouteHelper } from '@affine/core/components/hooks/use-journal';
|
||||
import { MoveToTrash } from '@affine/core/components/page-list';
|
||||
import {
|
||||
@@ -362,22 +363,38 @@ const ConflictList = ({
|
||||
}}
|
||||
items={
|
||||
<>
|
||||
<MenuItem
|
||||
prefixIcon={<CalendarXmarkIcon />}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
handleRemoveJournalMark(docRecord.id);
|
||||
}}
|
||||
data-testid="journal-conflict-remove-mark"
|
||||
<DocPermissionGuard
|
||||
docId={docRecord.id}
|
||||
permission="Doc_Update"
|
||||
>
|
||||
{t[
|
||||
'com.affine.page-properties.property.journal-remove'
|
||||
]()}
|
||||
</MenuItem>
|
||||
{canEdit => (
|
||||
<MenuItem
|
||||
prefixIcon={<CalendarXmarkIcon />}
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
handleRemoveJournalMark(docRecord.id);
|
||||
}}
|
||||
data-testid="journal-conflict-remove-mark"
|
||||
disabled={!canEdit}
|
||||
>
|
||||
{t[
|
||||
'com.affine.page-properties.property.journal-remove'
|
||||
]()}
|
||||
</MenuItem>
|
||||
)}
|
||||
</DocPermissionGuard>
|
||||
<MenuSeparator />
|
||||
<MoveToTrash
|
||||
onSelect={() => handleOpenTrashModal(docRecord)}
|
||||
/>
|
||||
<DocPermissionGuard
|
||||
docId={docRecord.id}
|
||||
permission="Doc_Trash"
|
||||
>
|
||||
{canTrash => (
|
||||
<MoveToTrash
|
||||
onSelect={() => handleOpenTrashModal(docRecord)}
|
||||
disabled={!canTrash}
|
||||
/>
|
||||
)}
|
||||
</DocPermissionGuard>
|
||||
</>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -6,7 +6,6 @@ import type { DocMode } from '@blocksuite/affine/blocks';
|
||||
import * as styles from './share-header.css';
|
||||
|
||||
export function ShareHeader({
|
||||
pageId,
|
||||
publishMode,
|
||||
isTemplate,
|
||||
templateName,
|
||||
@@ -21,7 +20,7 @@ export function ShareHeader({
|
||||
return (
|
||||
<div className={styles.header}>
|
||||
<EditorModeSwitch />
|
||||
<BlocksuiteHeaderTitle docId={pageId} />
|
||||
<BlocksuiteHeaderTitle />
|
||||
<div className={styles.spacer} />
|
||||
<ShareHeaderRightItem
|
||||
publishMode={publishMode}
|
||||
|
||||
@@ -260,7 +260,7 @@ const SharePageInner = ({
|
||||
styles.editorContainer
|
||||
)}
|
||||
>
|
||||
<PageDetailEditor onLoad={onEditorLoad} />
|
||||
<PageDetailEditor onLoad={onEditorLoad} readonly />
|
||||
{publishMode === 'page' && !BUILD_CONFIG.isElectron ? (
|
||||
<ShareFooter />
|
||||
) : null}
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
VirtualizedPageList,
|
||||
} from '@affine/core/components/page-list';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import {
|
||||
useIsActiveView,
|
||||
@@ -26,6 +27,9 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
||||
const globalContext = useService(GlobalContextService).globalContext;
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const pageMetas = useBlockSuiteDocMeta(currentWorkspace.docCollection);
|
||||
const permissionService = useService(WorkspacePermissionService);
|
||||
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
|
||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||
|
||||
const tagList = useService(TagService).tagList;
|
||||
const currentTag = useLiveData(tagList.tagByTagId$(tagId));
|
||||
@@ -72,6 +76,7 @@ export const TagDetail = ({ tagId }: { tagId?: string }) => {
|
||||
<VirtualizedPageList
|
||||
tag={currentTag}
|
||||
listItem={filteredPageMetas}
|
||||
disableMultiDelete={!isAdmin && !isOwner}
|
||||
/>
|
||||
) : (
|
||||
<EmptyPageList
|
||||
|
||||
@@ -5,11 +5,12 @@ import {
|
||||
} from '@affine/core/components/page-list';
|
||||
import { Header } from '@affine/core/components/pure/header';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { assertExists } from '@blocksuite/affine/global/utils';
|
||||
import { DeleteIcon } from '@blocksuite/icons/rc';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import {
|
||||
@@ -39,6 +40,9 @@ const TrashHeader = () => {
|
||||
export const TrashPage = () => {
|
||||
const globalContextService = useService(GlobalContextService);
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const permissionService = useService(WorkspacePermissionService);
|
||||
const isAdmin = useLiveData(permissionService.permission.isAdmin$);
|
||||
const isOwner = useLiveData(permissionService.permission.isOwner$);
|
||||
const docCollection = currentWorkspace.docCollection;
|
||||
assertExists(docCollection);
|
||||
|
||||
@@ -71,7 +75,10 @@ export const TrashPage = () => {
|
||||
<ViewBody>
|
||||
<div className={styles.body}>
|
||||
{filteredPageMetas.length > 0 ? (
|
||||
<VirtualizedTrashList />
|
||||
<VirtualizedTrashList
|
||||
disableMultiDelete={!isAdmin && !isOwner}
|
||||
disableMultiRestore={!isAdmin && !isOwner}
|
||||
/>
|
||||
) : (
|
||||
<EmptyPageList type="trash" />
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user