mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
feat(core): guard service (#9816)
This commit is contained in:
@@ -5,7 +5,10 @@ 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 { WorkspacePermissionService } from '@affine/core/modules/permissions';
|
||||
import {
|
||||
GuardService,
|
||||
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';
|
||||
@@ -409,6 +412,8 @@ 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]);
|
||||
@@ -440,6 +445,7 @@ const PageHistoryManager = ({
|
||||
const i18n = useI18n();
|
||||
|
||||
const title = useLiveData(docDisplayMetaService.title$(pageId));
|
||||
const canEdit = useLiveData(guardService.can$('Doc_Update', pageDocId));
|
||||
|
||||
const onConfirmRestore = useCallback(() => {
|
||||
openConfirmModal({
|
||||
@@ -499,7 +505,7 @@ const PageHistoryManager = ({
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={onConfirmRestore}
|
||||
disabled={isMutating || !activeVersion}
|
||||
disabled={isMutating || !activeVersion || !canEdit}
|
||||
>
|
||||
{t['com.affine.history.restore-current-version']()}
|
||||
</Button>
|
||||
|
||||
@@ -31,6 +31,7 @@ interface BlocksuiteEditorContainerProps {
|
||||
page: Store;
|
||||
mode: DocMode;
|
||||
shared?: boolean;
|
||||
readonly?: boolean;
|
||||
className?: string;
|
||||
defaultOpenProperty?: DefaultOpenProperty;
|
||||
style?: React.CSSProperties;
|
||||
@@ -40,7 +41,7 @@ export const BlocksuiteEditorContainer = forwardRef<
|
||||
AffineEditorContainer,
|
||||
BlocksuiteEditorContainerProps
|
||||
>(function AffineEditorContainer(
|
||||
{ page, mode, className, style, shared, defaultOpenProperty },
|
||||
{ page, mode, className, style, shared, readonly, defaultOpenProperty },
|
||||
ref
|
||||
) {
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
@@ -119,7 +120,7 @@ export const BlocksuiteEditorContainer = forwardRef<
|
||||
]);
|
||||
|
||||
const handleClickPageModeBlank = useCallback(() => {
|
||||
if (shared || page.readonly) return;
|
||||
if (shared || readonly || page.readonly) return;
|
||||
const std = affineEditorContainerProxy.host?.std;
|
||||
if (!std) {
|
||||
return;
|
||||
@@ -141,7 +142,7 @@ export const BlocksuiteEditorContainer = forwardRef<
|
||||
}
|
||||
|
||||
std.command.exec(appendParagraphCommand);
|
||||
}, [affineEditorContainerProxy, page, shared]);
|
||||
}, [affineEditorContainerProxy.host?.std, page, readonly, shared]);
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -161,6 +162,7 @@ export const BlocksuiteEditorContainer = forwardRef<
|
||||
shared={shared}
|
||||
page={page}
|
||||
ref={docRef}
|
||||
readonly={readonly}
|
||||
titleRef={docTitleRef}
|
||||
onClickBlank={handleClickPageModeBlank}
|
||||
defaultOpenProperty={defaultOpenProperty}
|
||||
|
||||
@@ -22,6 +22,7 @@ export type EditorProps = {
|
||||
page: Store;
|
||||
mode: DocMode;
|
||||
shared?: boolean;
|
||||
readonly?: boolean;
|
||||
defaultOpenProperty?: DefaultOpenProperty;
|
||||
// on Editor ready
|
||||
onEditorReady?: (editor: AffineEditorContainer) => (() => void) | void;
|
||||
@@ -34,6 +35,7 @@ const BlockSuiteEditorImpl = ({
|
||||
page,
|
||||
className,
|
||||
shared,
|
||||
readonly,
|
||||
style,
|
||||
onEditorReady,
|
||||
defaultOpenProperty,
|
||||
@@ -111,6 +113,7 @@ const BlockSuiteEditorImpl = ({
|
||||
mode={mode}
|
||||
page={page}
|
||||
shared={shared}
|
||||
readonly={readonly}
|
||||
defaultOpenProperty={defaultOpenProperty}
|
||||
ref={editorRef}
|
||||
className={className}
|
||||
|
||||
@@ -89,6 +89,7 @@ const adapted = {
|
||||
|
||||
interface BlocksuiteEditorProps {
|
||||
page: Store;
|
||||
readonly?: boolean;
|
||||
shared?: boolean;
|
||||
defaultOpenProperty?: DefaultOpenProperty;
|
||||
}
|
||||
@@ -220,6 +221,7 @@ export const BlocksuiteDocEditor = forwardRef<
|
||||
onClickBlank,
|
||||
titleRef: externalTitleRef,
|
||||
defaultOpenProperty,
|
||||
readonly,
|
||||
},
|
||||
ref
|
||||
) {
|
||||
@@ -334,7 +336,7 @@ export const BlocksuiteDocEditor = forwardRef<
|
||||
data-testid="page-editor-blank"
|
||||
onClick={onClickBlank}
|
||||
></div>
|
||||
<StarterBar doc={page} />
|
||||
{!readonly && <StarterBar doc={page} />}
|
||||
{!shared && displayBiDirectionalLink ? (
|
||||
<BiDirectionalLinkPanel />
|
||||
) : null}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { notify, useConfirmModal } from '@affine/component';
|
||||
import { notify, toast, useConfirmModal } from '@affine/component';
|
||||
import {
|
||||
Menu,
|
||||
MenuItem,
|
||||
@@ -9,13 +9,13 @@ import { PageHistoryModal } from '@affine/core/components/affine/page-history-mo
|
||||
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
|
||||
import { useEnableCloud } from '@affine/core/components/hooks/affine/use-enable-cloud';
|
||||
import { useExportPage } from '@affine/core/components/hooks/affine/use-export-page';
|
||||
import { useDocMetaHelper } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||
import { Export, MoveToTrash } from '@affine/core/components/page-list';
|
||||
import { IsFavoriteIcon } from '@affine/core/components/pure/icons';
|
||||
import { useDetailPageHeaderResponsive } from '@affine/core/desktop/pages/workspace/detail-page/use-header-responsive';
|
||||
import { WorkspaceDialogService } from '@affine/core/modules/dialogs';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { OpenInAppService } from '@affine/core/modules/open-in-app/services';
|
||||
import { GuardService } from '@affine/core/modules/permissions';
|
||||
import { ShareMenuContent } from '@affine/core/modules/share-menu';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||
@@ -34,7 +34,6 @@ import {
|
||||
LocalWorkspaceIcon,
|
||||
OpenInNewIcon,
|
||||
PageIcon,
|
||||
SaveIcon,
|
||||
ShareIcon,
|
||||
SplitViewIcon,
|
||||
TocIcon,
|
||||
@@ -56,24 +55,95 @@ type PageMenuProps = {
|
||||
isJournal?: boolean;
|
||||
containerWidth: number;
|
||||
};
|
||||
// fixme: refactor this file
|
||||
|
||||
export const PageHeaderMenuButton = ({
|
||||
rename,
|
||||
page,
|
||||
isJournal,
|
||||
containerWidth,
|
||||
}: PageMenuProps) => {
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const editorService = useService(EditorService);
|
||||
const isInTrash = useLiveData(
|
||||
editorService.editor.doc.meta$.map(meta => meta.trash)
|
||||
);
|
||||
|
||||
const [historyModalOpen, setHistoryModalOpen] = useState(false);
|
||||
const [openHistoryTipsModal, setOpenHistoryTipsModal] = useState(false);
|
||||
|
||||
const handleMenuOpenChange = useCallback((open: boolean) => {
|
||||
if (open) {
|
||||
track.$.header.docOptions.open();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const openHistoryModal = useCallback(() => {
|
||||
track.$.header.history.open();
|
||||
if (workspace.flavour === 'affine-cloud') {
|
||||
return setHistoryModalOpen(true);
|
||||
}
|
||||
return setOpenHistoryTipsModal(true);
|
||||
}, [setOpenHistoryTipsModal, workspace.flavour]);
|
||||
|
||||
if (isInTrash) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Menu
|
||||
items={
|
||||
<PageHeaderMenuItem
|
||||
page={page}
|
||||
containerWidth={containerWidth}
|
||||
rename={rename}
|
||||
isJournal={isJournal}
|
||||
openHistoryModal={openHistoryModal}
|
||||
/>
|
||||
}
|
||||
contentOptions={{
|
||||
align: 'center',
|
||||
}}
|
||||
rootOptions={{
|
||||
onOpenChange: handleMenuOpenChange,
|
||||
}}
|
||||
>
|
||||
<HeaderDropDownButton />
|
||||
</Menu>
|
||||
{workspace.flavour !== 'local' ? (
|
||||
<PageHistoryModal
|
||||
docCollection={workspace.docCollection}
|
||||
open={historyModalOpen}
|
||||
pageId={page.id}
|
||||
onOpenChange={setHistoryModalOpen}
|
||||
/>
|
||||
) : null}
|
||||
<HistoryTipsModal
|
||||
open={openHistoryTipsModal}
|
||||
setOpen={setOpenHistoryTipsModal}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// fixme: refactor this file
|
||||
const PageHeaderMenuItem = ({
|
||||
rename,
|
||||
page,
|
||||
isJournal,
|
||||
containerWidth,
|
||||
openHistoryModal,
|
||||
}: PageMenuProps & {
|
||||
openHistoryModal: () => void;
|
||||
}) => {
|
||||
const pageId = page?.id;
|
||||
const t = useI18n();
|
||||
const { hideShare } = useDetailPageHeaderResponsive(containerWidth);
|
||||
const confirmEnableCloud = useEnableCloud();
|
||||
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
|
||||
const guardService = useService(GuardService);
|
||||
const editorService = useService(EditorService);
|
||||
const isInTrash = useLiveData(
|
||||
editorService.editor.doc.meta$.map(meta => meta.trash)
|
||||
);
|
||||
const currentMode = useLiveData(editorService.editor.mode$);
|
||||
const primaryMode = useLiveData(editorService.editor.doc.primaryMode$);
|
||||
|
||||
@@ -84,9 +154,6 @@ export const PageHeaderMenuButton = ({
|
||||
|
||||
const { duplicate } = useBlockSuiteMetaHelper();
|
||||
|
||||
const [isEditing, setEditing] = useState(!page.readonly);
|
||||
const { setDocReadonly } = useDocMetaHelper();
|
||||
|
||||
const view = useService(ViewService).view;
|
||||
|
||||
const openSidePanel = useCallback(
|
||||
@@ -105,17 +172,6 @@ export const PageHeaderMenuButton = ({
|
||||
openSidePanel('outline');
|
||||
}, [openSidePanel]);
|
||||
|
||||
const [historyModalOpen, setHistoryModalOpen] = useState(false);
|
||||
const [openHistoryTipsModal, setOpenHistoryTipsModal] = useState(false);
|
||||
|
||||
const openHistoryModal = useCallback(() => {
|
||||
track.$.header.history.open();
|
||||
if (workspace.flavour === 'affine-cloud') {
|
||||
return setHistoryModalOpen(true);
|
||||
}
|
||||
return setOpenHistoryTipsModal(true);
|
||||
}, [setOpenHistoryTipsModal, workspace.flavour]);
|
||||
|
||||
const workspaceDialogService = useService(WorkspaceDialogService);
|
||||
const openInfoModal = useCallback(() => {
|
||||
track.$.header.pageInfo.open();
|
||||
@@ -148,11 +204,16 @@ export const PageHeaderMenuButton = ({
|
||||
confirmButtonOptions: {
|
||||
variant: 'error',
|
||||
},
|
||||
onConfirm: () => {
|
||||
onConfirm: async () => {
|
||||
const canTrash = await guardService.can('Doc_Trash', pageId);
|
||||
if (!canTrash) {
|
||||
toast(t['com.affine.no-permission']());
|
||||
return;
|
||||
}
|
||||
editorService.editor.doc.moveToTrash();
|
||||
},
|
||||
});
|
||||
}, [editorService.editor.doc, openConfirmModal, t]);
|
||||
}, [editorService.editor.doc, guardService, openConfirmModal, pageId, t]);
|
||||
|
||||
const handleRename = useCallback(() => {
|
||||
rename?.();
|
||||
@@ -178,12 +239,6 @@ export const PageHeaderMenuButton = ({
|
||||
});
|
||||
}, [primaryMode, editorService, t]);
|
||||
|
||||
const handleMenuOpenChange = useCallback((open: boolean) => {
|
||||
if (open) {
|
||||
track.$.header.docOptions.open();
|
||||
}
|
||||
}, []);
|
||||
|
||||
const exportHandler = useExportPage();
|
||||
|
||||
const handleDuplicate = useCallback(() => {
|
||||
@@ -238,21 +293,6 @@ export const PageHeaderMenuButton = ({
|
||||
toggleFavorite();
|
||||
}, [toggleFavorite]);
|
||||
|
||||
const handleToggleEdit = useCallback(() => {
|
||||
setDocReadonly(page.id, !page.readonly);
|
||||
setEditing(!isEditing);
|
||||
}, [isEditing, page.id, page.readonly, setDocReadonly]);
|
||||
|
||||
const isMobile = environment.isMobile;
|
||||
const mobileEditMenuItem = (
|
||||
<MenuItem
|
||||
prefixIcon={isEditing ? <SaveIcon /> : <EditIcon />}
|
||||
onSelect={handleToggleEdit}
|
||||
>
|
||||
{t[isEditing ? 'Save' : 'Edit']()}
|
||||
</MenuItem>
|
||||
);
|
||||
|
||||
const showResponsiveMenu = hideShare;
|
||||
const ResponsiveMenuItems = (
|
||||
<>
|
||||
@@ -293,15 +333,18 @@ export const PageHeaderMenuButton = ({
|
||||
openInAppService?.showOpenInAppPage();
|
||||
}, [openInAppService]);
|
||||
|
||||
const EditMenu = (
|
||||
const canEdit = useLiveData(guardService.can$('Doc_Update', pageId));
|
||||
const canMoveToTrash = useLiveData(guardService.can$('Doc_Trash', pageId));
|
||||
|
||||
return (
|
||||
<>
|
||||
{showResponsiveMenu ? ResponsiveMenuItems : null}
|
||||
{isMobile && mobileEditMenuItem}
|
||||
{!isJournal && (
|
||||
<MenuItem
|
||||
prefixIcon={<EditIcon />}
|
||||
data-testid="editor-option-menu-rename"
|
||||
onSelect={handleRename}
|
||||
disabled={!canEdit}
|
||||
>
|
||||
{t['Rename']()}
|
||||
</MenuItem>
|
||||
@@ -310,6 +353,7 @@ export const PageHeaderMenuButton = ({
|
||||
prefixIcon={primaryMode === 'page' ? <EdgelessIcon /> : <PageIcon />}
|
||||
data-testid="editor-option-menu-edgeless"
|
||||
onSelect={handleSwitchMode}
|
||||
disabled={!canEdit}
|
||||
>
|
||||
{primaryMode === 'page'
|
||||
? t['com.affine.editorDefaultMode.edgeless']()
|
||||
@@ -396,6 +440,7 @@ export const PageHeaderMenuButton = ({
|
||||
<MoveToTrash
|
||||
data-testid="editor-option-menu-delete"
|
||||
onSelect={handleOpenTrashModal}
|
||||
disabled={!canMoveToTrash}
|
||||
/>
|
||||
{BUILD_CONFIG.isWeb && workspace.flavour === 'affine-cloud' ? (
|
||||
<MenuItem
|
||||
@@ -408,34 +453,4 @@ export const PageHeaderMenuButton = ({
|
||||
) : null}
|
||||
</>
|
||||
);
|
||||
if (isInTrash) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<Menu
|
||||
items={EditMenu}
|
||||
contentOptions={{
|
||||
align: 'center',
|
||||
}}
|
||||
rootOptions={{
|
||||
onOpenChange: handleMenuOpenChange,
|
||||
}}
|
||||
>
|
||||
<HeaderDropDownButton />
|
||||
</Menu>
|
||||
{workspace.flavour !== 'local' ? (
|
||||
<PageHistoryModal
|
||||
docCollection={workspace.docCollection}
|
||||
open={historyModalOpen}
|
||||
pageId={pageId}
|
||||
onOpenChange={setHistoryModalOpen}
|
||||
/>
|
||||
) : null}
|
||||
<HistoryTipsModal
|
||||
open={openHistoryTipsModal}
|
||||
setOpen={setOpenHistoryTipsModal}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { InlineEditProps } from '@affine/component';
|
||||
import { InlineEdit } from '@affine/component';
|
||||
import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { DocService, DocsService } from '@affine/core/modules/doc';
|
||||
import { GuardService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { track } from '@affine/track';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
@@ -11,7 +12,6 @@ import type { HTMLAttributes } from 'react';
|
||||
import * as styles from './style.css';
|
||||
|
||||
export interface BlockSuiteHeaderTitleProps {
|
||||
docId: string;
|
||||
/** if set, title cannot be edited */
|
||||
inputHandleRef?: InlineEditProps['handleRef'];
|
||||
className?: string;
|
||||
@@ -21,20 +21,24 @@ const inputAttrs = {
|
||||
'data-testid': 'title-content',
|
||||
} as HTMLAttributes<HTMLInputElement>;
|
||||
export const BlocksuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => {
|
||||
const { inputHandleRef, docId } = props;
|
||||
const { inputHandleRef } = props;
|
||||
const workspaceService = useService(WorkspaceService);
|
||||
const isSharedMode = workspaceService.workspace.openOptions.isSharedMode;
|
||||
const docsService = useService(DocsService);
|
||||
|
||||
const docRecord = useLiveData(docsService.list.doc$(docId));
|
||||
const docTitle = useLiveData(docRecord?.title$);
|
||||
const guardService = useService(GuardService);
|
||||
const docService = useService(DocService);
|
||||
const docTitle = useLiveData(docService.doc.record.title$);
|
||||
|
||||
const onChange = useAsyncCallback(
|
||||
async (v: string) => {
|
||||
await docsService.changeDocTitle(docId, v);
|
||||
await docsService.changeDocTitle(docService.doc.id, v);
|
||||
track.$.header.actions.renameDoc();
|
||||
},
|
||||
[docId, docsService]
|
||||
[docService.doc.id, docsService]
|
||||
);
|
||||
|
||||
const canEdit = useLiveData(
|
||||
guardService.can$('Doc_Update', docService.doc.id)
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -42,7 +46,7 @@ export const BlocksuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => {
|
||||
className={clsx(styles.title, props.className)}
|
||||
value={docTitle}
|
||||
onChange={onChange}
|
||||
editable={!isSharedMode}
|
||||
editable={!isSharedMode && canEdit}
|
||||
exitible={true}
|
||||
placeholder="Untitled"
|
||||
data-testid="title-edit-button"
|
||||
|
||||
@@ -51,8 +51,14 @@ export const iconSelectorButton = style({
|
||||
border: `1px solid ${cssVar('borderColor')}`,
|
||||
background: cssVar('backgroundSecondaryColor'),
|
||||
cursor: 'pointer',
|
||||
':hover': {
|
||||
backgroundColor: cssVar('backgroundTertiaryColor'),
|
||||
|
||||
selectors: {
|
||||
'&:hover:not([data-readonly=true])': {
|
||||
backgroundColor: cssVar('backgroundTertiaryColor'),
|
||||
},
|
||||
'&[data-readonly=true]': {
|
||||
cursor: 'default',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -55,11 +55,20 @@ const IconsSelectorPanel = ({
|
||||
|
||||
export const DocPropertyIconSelector = ({
|
||||
propertyInfo,
|
||||
readonly,
|
||||
onSelectedChange,
|
||||
}: {
|
||||
propertyInfo: DocCustomPropertyInfo;
|
||||
readonly?: boolean;
|
||||
onSelectedChange: (icon: DocPropertyIconName) => void;
|
||||
}) => {
|
||||
if (readonly) {
|
||||
return (
|
||||
<div className={styles.iconSelectorButton} data-readonly={readonly}>
|
||||
<DocPropertyIcon propertyInfo={propertyInfo} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Menu
|
||||
items={
|
||||
|
||||
@@ -8,6 +8,7 @@ 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';
|
||||
@@ -37,9 +38,13 @@ 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 typeInfo = isSupportedDocPropertyType(propertyInfo.type)
|
||||
? DocPropertyTypes[propertyInfo.type]
|
||||
@@ -51,6 +56,7 @@ const PropertyItem = ({
|
||||
|
||||
const { dragRef } = useDraggable<AffineDNDData>(
|
||||
() => ({
|
||||
canDrag: canEditPropertyInfo,
|
||||
data: {
|
||||
entity: {
|
||||
type: 'custom-property',
|
||||
@@ -62,13 +68,14 @@ const PropertyItem = ({
|
||||
},
|
||||
},
|
||||
}),
|
||||
[propertyInfo, workspaceService]
|
||||
[propertyInfo, workspaceService, canEditPropertyInfo]
|
||||
);
|
||||
|
||||
const { dropTargetRef, closestEdge } = useDropTarget<AffineDNDData>(
|
||||
() => ({
|
||||
canDrop(data) {
|
||||
return (
|
||||
canEditPropertyInfo &&
|
||||
data.source.data.entity?.type === 'custom-property' &&
|
||||
data.source.data.from?.at === 'doc-property:manager' &&
|
||||
data.source.data.from?.workspaceId ===
|
||||
@@ -97,7 +104,7 @@ const PropertyItem = ({
|
||||
});
|
||||
},
|
||||
}),
|
||||
[docsService, propertyInfo, workspaceService]
|
||||
[docsService, propertyInfo, workspaceService, canEditPropertyInfo]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -139,6 +146,7 @@ const PropertyItem = ({
|
||||
<EditDocPropertyMenuItems
|
||||
propertyId={propertyInfo.id}
|
||||
onPropertyInfoChange={onPropertyInfoChange}
|
||||
readonly={!canEditPropertyInfo}
|
||||
/>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -28,8 +28,10 @@ import * as styles from './edit-doc-property.css';
|
||||
export const EditDocPropertyMenuItems = ({
|
||||
propertyId,
|
||||
onPropertyInfoChange,
|
||||
readonly,
|
||||
}: {
|
||||
propertyId: string;
|
||||
readonly?: boolean;
|
||||
onPropertyInfoChange?: (
|
||||
field: keyof DocCustomPropertyInfo,
|
||||
value: string
|
||||
@@ -142,9 +144,10 @@ export const EditDocPropertyMenuItems = ({
|
||||
>
|
||||
<DocPropertyIconSelector
|
||||
propertyInfo={propertyInfo}
|
||||
readonly={readonly}
|
||||
onSelectedChange={handleIconChange}
|
||||
/>
|
||||
{typeInfo?.renameable === false ? (
|
||||
{typeInfo?.renameable === false || readonly ? (
|
||||
<span className={styles.propertyName}>{name}</span>
|
||||
) : (
|
||||
<Input
|
||||
@@ -180,6 +183,7 @@ export const EditDocPropertyMenuItems = ({
|
||||
propertyInfo.show !== 'always-hide'
|
||||
}
|
||||
data-property-visibility="always-show"
|
||||
disabled={readonly}
|
||||
>
|
||||
{t['com.affine.page-properties.property.always-show']()}
|
||||
</MenuItem>
|
||||
@@ -188,6 +192,7 @@ export const EditDocPropertyMenuItems = ({
|
||||
onClick={handleClickHideWhenEmpty}
|
||||
selected={propertyInfo.show === 'hide-when-empty'}
|
||||
data-property-visibility="hide-when-empty"
|
||||
disabled={readonly}
|
||||
>
|
||||
{t['com.affine.page-properties.property.hide-when-empty']()}
|
||||
</MenuItem>
|
||||
@@ -196,6 +201,7 @@ export const EditDocPropertyMenuItems = ({
|
||||
onClick={handleClickAlwaysHide}
|
||||
selected={propertyInfo.show === 'always-hide'}
|
||||
data-property-visibility="always-hide"
|
||||
disabled={readonly}
|
||||
>
|
||||
{t['com.affine.page-properties.property.always-hide']()}
|
||||
</MenuItem>
|
||||
@@ -203,6 +209,7 @@ export const EditDocPropertyMenuItems = ({
|
||||
<MenuItem
|
||||
prefixIcon={<DeleteIcon />}
|
||||
type="danger"
|
||||
disabled={readonly}
|
||||
onClick={() => {
|
||||
confirmModal.openConfirmModal({
|
||||
title:
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
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';
|
||||
@@ -28,9 +29,12 @@ 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 onAddProperty = useCallback(
|
||||
(option: { type: string; name: string }) => {
|
||||
if (!isSupportedDocPropertyType(option.type)) {
|
||||
@@ -104,12 +108,15 @@ export const DocPropertySidebar = () => {
|
||||
<div
|
||||
className={styles.itemContainer}
|
||||
onClick={() => {
|
||||
if (!canEditPropertyInfo) {
|
||||
return;
|
||||
}
|
||||
onAddProperty({
|
||||
type: key,
|
||||
name,
|
||||
});
|
||||
}}
|
||||
data-disabled={isUniqueExist}
|
||||
data-disabled={isUniqueExist || !canEditPropertyInfo}
|
||||
>
|
||||
<Icon className={styles.itemIcon} />
|
||||
<span className={styles.itemName}>{t.t(value.name)}</span>
|
||||
|
||||
@@ -16,6 +16,7 @@ import type {
|
||||
DatabaseValueCell,
|
||||
} from '@affine/core/modules/doc-info/types';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
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';
|
||||
@@ -111,6 +112,8 @@ interface DocPropertyRowProps {
|
||||
propertyInfo: DocCustomPropertyInfo;
|
||||
showAll?: boolean;
|
||||
defaultOpenEditMenu?: boolean;
|
||||
propertyInfoReadonly?: boolean;
|
||||
readonly?: boolean;
|
||||
onChange?: (value: unknown) => void;
|
||||
onPropertyInfoChange?: (
|
||||
field: keyof DocCustomPropertyInfo,
|
||||
@@ -122,6 +125,8 @@ export const DocPropertyRow = ({
|
||||
propertyInfo,
|
||||
defaultOpenEditMenu,
|
||||
onChange,
|
||||
propertyInfoReadonly,
|
||||
readonly,
|
||||
onPropertyInfoChange,
|
||||
}: DocPropertyRowProps) => {
|
||||
const t = useI18n();
|
||||
@@ -160,6 +165,7 @@ export const DocPropertyRow = ({
|
||||
const docId = docService.doc.id;
|
||||
const { dragRef } = useDraggable<AffineDNDData>(
|
||||
() => ({
|
||||
canDrag: !propertyInfoReadonly,
|
||||
data: {
|
||||
entity: {
|
||||
type: 'custom-property',
|
||||
@@ -171,7 +177,7 @@ export const DocPropertyRow = ({
|
||||
},
|
||||
},
|
||||
}),
|
||||
[docId, propertyInfo.id]
|
||||
[docId, propertyInfo.id, propertyInfoReadonly]
|
||||
);
|
||||
const { dropTargetRef, closestEdge } = useDropTarget<AffineDNDData>(
|
||||
() => ({
|
||||
@@ -180,6 +186,7 @@ export const DocPropertyRow = ({
|
||||
},
|
||||
canDrop: data => {
|
||||
return (
|
||||
!propertyInfoReadonly &&
|
||||
data.source.data.entity?.type === 'custom-property' &&
|
||||
data.source.data.entity.id !== propertyInfo.id &&
|
||||
data.source.data.from?.at === 'doc-property:table' &&
|
||||
@@ -204,7 +211,7 @@ export const DocPropertyRow = ({
|
||||
});
|
||||
},
|
||||
}),
|
||||
[docId, docsService.propertyList, propertyInfo.id]
|
||||
[docId, docsService.propertyList, propertyInfo.id, propertyInfoReadonly]
|
||||
);
|
||||
|
||||
if (!ValueRenderer || typeof ValueRenderer !== 'function') return null;
|
||||
@@ -221,6 +228,8 @@ export const DocPropertyRow = ({
|
||||
dropIndicatorEdge={closestEdge}
|
||||
hideEmpty={hideEmpty}
|
||||
hide={hide}
|
||||
data-property-info-readonly={propertyInfoReadonly}
|
||||
data-readonly={readonly}
|
||||
data-testid="doc-property-row"
|
||||
data-info-id={propertyInfo.id}
|
||||
>
|
||||
@@ -235,6 +244,7 @@ export const DocPropertyRow = ({
|
||||
<EditDocPropertyMenuItems
|
||||
propertyId={propertyInfo.id}
|
||||
onPropertyInfoChange={onPropertyInfoChange}
|
||||
readonly={propertyInfoReadonly}
|
||||
/>
|
||||
}
|
||||
data-testid="doc-property-name"
|
||||
@@ -243,6 +253,7 @@ export const DocPropertyRow = ({
|
||||
propertyInfo={propertyInfo}
|
||||
onChange={handleChange}
|
||||
value={customPropertyValue}
|
||||
readonly={readonly}
|
||||
/>
|
||||
</PropertyRoot>
|
||||
);
|
||||
@@ -284,11 +295,20 @@ const DocWorkspacePropertiesTableBody = forwardRef<
|
||||
const docsService = useService(DocsService);
|
||||
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 handlePropertyAdded = useCallback(
|
||||
(property: DocCustomPropertyInfo) => {
|
||||
setNewPropertyId(property.id);
|
||||
@@ -340,34 +360,48 @@ const DocWorkspacePropertiesTableBody = forwardRef<
|
||||
propertyInfo={property}
|
||||
defaultOpenEditMenu={newPropertyId === property.id}
|
||||
onChange={value => onChange?.(property, value)}
|
||||
readonly={!canEditProperty}
|
||||
propertyInfoReadonly={!canEditPropertyInfo}
|
||||
onPropertyInfoChange={(...args) =>
|
||||
onPropertyInfoChange?.(property, ...args)
|
||||
}
|
||||
/>
|
||||
))}
|
||||
<div className={styles.actionContainer}>
|
||||
<Menu
|
||||
items={
|
||||
<CreatePropertyMenuItems
|
||||
at="after"
|
||||
onCreated={handlePropertyAdded}
|
||||
/>
|
||||
}
|
||||
contentOptions={{
|
||||
onClick(e) {
|
||||
e.stopPropagation();
|
||||
},
|
||||
}}
|
||||
>
|
||||
{!canEditPropertyInfo ? (
|
||||
<Button
|
||||
variant="plain"
|
||||
prefix={<PlusIcon />}
|
||||
className={styles.propertyActionButton}
|
||||
data-testid="add-property-button"
|
||||
disabled={!canEditPropertyInfo}
|
||||
>
|
||||
{t['com.affine.page-properties.add-property']()}
|
||||
</Button>
|
||||
</Menu>
|
||||
) : (
|
||||
<Menu
|
||||
items={
|
||||
<CreatePropertyMenuItems
|
||||
at="after"
|
||||
onCreated={handlePropertyAdded}
|
||||
/>
|
||||
}
|
||||
contentOptions={{
|
||||
onClick(e) {
|
||||
e.stopPropagation();
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="plain"
|
||||
prefix={<PlusIcon />}
|
||||
className={styles.propertyActionButton}
|
||||
data-testid="add-property-button"
|
||||
>
|
||||
{t['com.affine.page-properties.add-property']()}
|
||||
</Button>
|
||||
</Menu>
|
||||
)}
|
||||
{viewService ? (
|
||||
<Button
|
||||
variant="plain"
|
||||
|
||||
@@ -4,14 +4,21 @@ import { useCallback } from 'react';
|
||||
import * as styles from './checkbox.css';
|
||||
import type { PropertyValueProps } from './types';
|
||||
|
||||
export const CheckboxValue = ({ value, onChange }: PropertyValueProps) => {
|
||||
export const CheckboxValue = ({
|
||||
value,
|
||||
onChange,
|
||||
readonly,
|
||||
}: PropertyValueProps) => {
|
||||
const parsedValue = value === 'true' ? true : false;
|
||||
const handleClick = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
if (readonly) {
|
||||
return;
|
||||
}
|
||||
onChange(parsedValue ? 'false' : 'true');
|
||||
},
|
||||
[onChange, parsedValue]
|
||||
[onChange, parsedValue, readonly]
|
||||
);
|
||||
return (
|
||||
<PropertyValue onClick={handleClick} className={styles.container}>
|
||||
@@ -19,6 +26,7 @@ export const CheckboxValue = ({ value, onChange }: PropertyValueProps) => {
|
||||
className={styles.checkboxProperty}
|
||||
checked={parsedValue}
|
||||
onChange={() => {}}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</PropertyValue>
|
||||
);
|
||||
|
||||
@@ -77,14 +77,14 @@ export const CreatedByValue = () => {
|
||||
|
||||
if (!isCloud) {
|
||||
return (
|
||||
<PropertyValue>
|
||||
<PropertyValue readonly>
|
||||
<LocalUserValue />
|
||||
</PropertyValue>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PropertyValue>
|
||||
<PropertyValue readonly>
|
||||
<CloudUserAvatar type="CreatedBy" />
|
||||
</PropertyValue>
|
||||
);
|
||||
@@ -96,14 +96,14 @@ export const UpdatedByValue = () => {
|
||||
|
||||
if (!isCloud) {
|
||||
return (
|
||||
<PropertyValue>
|
||||
<PropertyValue readonly>
|
||||
<LocalUserValue />
|
||||
</PropertyValue>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PropertyValue>
|
||||
<PropertyValue readonly>
|
||||
<CloudUserAvatar type="UpdatedBy" />
|
||||
</PropertyValue>
|
||||
);
|
||||
|
||||
@@ -23,9 +23,25 @@ const useParsedDate = (value: string) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const DateValue = ({ value, onChange }: PropertyValueProps) => {
|
||||
export const DateValue = ({
|
||||
value,
|
||||
onChange,
|
||||
readonly,
|
||||
}: PropertyValueProps) => {
|
||||
const { parsedValue, displayValue } = useParsedDate(value);
|
||||
|
||||
if (readonly) {
|
||||
return (
|
||||
<PropertyValue
|
||||
className={parsedValue ? '' : styles.empty}
|
||||
isEmpty={!parsedValue}
|
||||
readonly
|
||||
>
|
||||
{displayValue}
|
||||
</PropertyValue>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Menu
|
||||
contentOptions={{
|
||||
|
||||
@@ -13,7 +13,10 @@ import { useCallback, useMemo } from 'react';
|
||||
import * as styles from './doc-primary-mode.css';
|
||||
import type { PropertyValueProps } from './types';
|
||||
|
||||
export const DocPrimaryModeValue = ({ onChange }: PropertyValueProps) => {
|
||||
export const DocPrimaryModeValue = ({
|
||||
onChange,
|
||||
readonly,
|
||||
}: PropertyValueProps) => {
|
||||
const t = useI18n();
|
||||
const doc = useService(DocService).doc;
|
||||
|
||||
@@ -51,13 +54,18 @@ export const DocPrimaryModeValue = ({ onChange }: PropertyValueProps) => {
|
||||
[doc, t, onChange]
|
||||
);
|
||||
return (
|
||||
<PropertyValue className={styles.container} hoverable={false}>
|
||||
<PropertyValue
|
||||
className={styles.container}
|
||||
hoverable={false}
|
||||
readonly={readonly}
|
||||
>
|
||||
<RadioGroup
|
||||
width={BUILD_CONFIG.isMobileEdition ? '100%' : 194}
|
||||
itemHeight={24}
|
||||
value={primaryMode}
|
||||
onChange={handleChange}
|
||||
items={DocModeItems}
|
||||
disabled={readonly}
|
||||
className={styles.radioGroup}
|
||||
/>
|
||||
</PropertyValue>
|
||||
|
||||
@@ -23,7 +23,10 @@ const getThemeOptions = (t: ReturnType<typeof useI18n>) =>
|
||||
},
|
||||
] satisfies RadioItem[];
|
||||
|
||||
export const EdgelessThemeValue = ({ onChange }: PropertyValueProps) => {
|
||||
export const EdgelessThemeValue = ({
|
||||
onChange,
|
||||
readonly,
|
||||
}: PropertyValueProps) => {
|
||||
const t = useI18n();
|
||||
const doc = useService(DocService).doc;
|
||||
const edgelessTheme = useLiveData(doc.properties$).edgelessColorTheme;
|
||||
@@ -38,13 +41,18 @@ export const EdgelessThemeValue = ({ onChange }: PropertyValueProps) => {
|
||||
const themeItems = useMemo<RadioItem[]>(() => getThemeOptions(t), [t]);
|
||||
|
||||
return (
|
||||
<PropertyValue className={styles.container} hoverable={false}>
|
||||
<PropertyValue
|
||||
className={styles.container}
|
||||
hoverable={false}
|
||||
readonly={readonly}
|
||||
>
|
||||
<RadioGroup
|
||||
width={BUILD_CONFIG.isMobileEdition ? '100%' : 194}
|
||||
itemHeight={24}
|
||||
value={edgelessTheme || 'system'}
|
||||
onChange={handleChange}
|
||||
items={themeItems}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</PropertyValue>
|
||||
);
|
||||
|
||||
@@ -25,8 +25,11 @@ export const date = style({
|
||||
padding: '0 4px',
|
||||
borderRadius: 4,
|
||||
whiteSpace: 'nowrap',
|
||||
':hover': {
|
||||
background: cssVarV2('layer/background/hoverOverlay'),
|
||||
|
||||
selectors: {
|
||||
'&:hover:not([data-disabled])': {
|
||||
background: cssVarV2('layer/background/hoverOverlay'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import * as styles from './journal.css';
|
||||
import type { PropertyValueProps } from './types';
|
||||
|
||||
const stopPropagation = (e: React.MouseEvent) => e.stopPropagation();
|
||||
export const JournalValue = ({ onChange }: PropertyValueProps) => {
|
||||
export const JournalValue = ({ readonly }: PropertyValueProps) => {
|
||||
const t = useI18n();
|
||||
|
||||
const journalService = useService(JournalService);
|
||||
@@ -53,9 +53,8 @@ export const JournalValue = ({ onChange }: PropertyValueProps) => {
|
||||
const date = dayjs(day).format('YYYY-MM-DD');
|
||||
setSelectedDate(date);
|
||||
journalService.setJournalDate(doc.id, date);
|
||||
onChange?.(date, true);
|
||||
},
|
||||
[journalService, doc.id, onChange]
|
||||
[journalService, doc.id]
|
||||
);
|
||||
|
||||
const handleCheck = useCallback(
|
||||
@@ -63,12 +62,11 @@ export const JournalValue = ({ onChange }: PropertyValueProps) => {
|
||||
if (!v) {
|
||||
journalService.removeJournalDate(doc.id);
|
||||
setShowDatePicker(false);
|
||||
onChange?.(null, true);
|
||||
} else {
|
||||
handleDateSelect(selectedDate);
|
||||
}
|
||||
},
|
||||
[onChange, journalService, doc.id, handleDateSelect, selectedDate]
|
||||
[journalService, doc.id, handleDateSelect, selectedDate]
|
||||
);
|
||||
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
@@ -88,11 +86,12 @@ export const JournalValue = ({ onChange }: PropertyValueProps) => {
|
||||
|
||||
const toggle = useCallback(
|
||||
(e: React.MouseEvent) => {
|
||||
if (readonly) return;
|
||||
if (propertyRef.current?.contains(e.target as Node)) {
|
||||
handleCheck(null, !checked);
|
||||
}
|
||||
},
|
||||
[checked, handleCheck]
|
||||
[checked, handleCheck, readonly]
|
||||
);
|
||||
|
||||
return (
|
||||
@@ -100,9 +99,14 @@ export const JournalValue = ({ onChange }: PropertyValueProps) => {
|
||||
ref={propertyRef}
|
||||
className={styles.property}
|
||||
onClick={toggle}
|
||||
readonly={readonly}
|
||||
>
|
||||
<div className={styles.root}>
|
||||
<Checkbox className={styles.checkbox} checked={checked} />
|
||||
<Checkbox
|
||||
className={styles.checkbox}
|
||||
checked={checked}
|
||||
disabled={readonly}
|
||||
/>
|
||||
{checked ? (
|
||||
<Menu
|
||||
contentOptions={{
|
||||
@@ -113,7 +117,7 @@ export const JournalValue = ({ onChange }: PropertyValueProps) => {
|
||||
}}
|
||||
rootOptions={{
|
||||
modal: true,
|
||||
open: showDatePicker,
|
||||
open: !readonly && showDatePicker,
|
||||
onOpenChange: setShowDatePicker,
|
||||
}}
|
||||
items={
|
||||
@@ -132,6 +136,7 @@ export const JournalValue = ({ onChange }: PropertyValueProps) => {
|
||||
onClick={e => {
|
||||
e.stopPropagation();
|
||||
}}
|
||||
data-disabled={readonly ? 'true' : undefined}
|
||||
>
|
||||
{displayDate}
|
||||
</div>
|
||||
|
||||
@@ -10,7 +10,11 @@ import {
|
||||
import * as styles from './number.css';
|
||||
import type { PropertyValueProps } from './types';
|
||||
|
||||
export const NumberValue = ({ value, onChange }: PropertyValueProps) => {
|
||||
export const NumberValue = ({
|
||||
value,
|
||||
onChange,
|
||||
readonly,
|
||||
}: PropertyValueProps) => {
|
||||
const parsedValue = isNaN(Number(value)) ? null : value;
|
||||
const [tempValue, setTempValue] = useState(parsedValue);
|
||||
const handleBlur = useCallback(
|
||||
@@ -33,6 +37,7 @@ export const NumberValue = ({ value, onChange }: PropertyValueProps) => {
|
||||
<PropertyValue
|
||||
className={styles.numberPropertyValueContainer}
|
||||
isEmpty={!parsedValue}
|
||||
readonly={readonly}
|
||||
>
|
||||
<input
|
||||
className={styles.numberPropertyValueInput}
|
||||
@@ -45,6 +50,7 @@ export const NumberValue = ({ value, onChange }: PropertyValueProps) => {
|
||||
placeholder={t[
|
||||
'com.affine.page-properties.property-value-placeholder'
|
||||
]()}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</PropertyValue>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useCallback, useMemo } from 'react';
|
||||
import { container } from './page-width.css';
|
||||
import type { PageLayoutMode, PropertyValueProps } from './types';
|
||||
|
||||
export const PageWidthValue = ({ onChange }: PropertyValueProps) => {
|
||||
export const PageWidthValue = ({ readonly }: PropertyValueProps) => {
|
||||
const t = useI18n();
|
||||
const editorSetting = useService(EditorSettingService).editorSetting;
|
||||
const defaultPageWidth = useLiveData(editorSetting.settings$).fullWidthLayout;
|
||||
@@ -45,18 +45,18 @@ export const PageWidthValue = ({ onChange }: PropertyValueProps) => {
|
||||
const handleChange = useCallback(
|
||||
(value: PageLayoutMode) => {
|
||||
doc.record.setProperty('pageWidth', value);
|
||||
onChange?.(value, true);
|
||||
},
|
||||
[doc, onChange]
|
||||
[doc]
|
||||
);
|
||||
return (
|
||||
<PropertyValue className={container} hoverable={false}>
|
||||
<PropertyValue className={container} hoverable={false} readonly={readonly}>
|
||||
<RadioGroup
|
||||
width={BUILD_CONFIG.isMobileEdition ? '100%' : 194}
|
||||
itemHeight={24}
|
||||
value={radioValue}
|
||||
onChange={handleChange}
|
||||
items={radioItems}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</PropertyValue>
|
||||
);
|
||||
|
||||
@@ -8,7 +8,7 @@ import { TagsInlineEditor } from '../tags-inline-editor';
|
||||
import * as styles from './tags.css';
|
||||
import type { PropertyValueProps } from './types';
|
||||
|
||||
export const TagsValue = ({ onChange }: PropertyValueProps) => {
|
||||
export const TagsValue = ({ readonly }: PropertyValueProps) => {
|
||||
const t = useI18n();
|
||||
|
||||
const doc = useService(DocService).doc;
|
||||
@@ -22,6 +22,7 @@ export const TagsValue = ({ onChange }: PropertyValueProps) => {
|
||||
className={styles.container}
|
||||
isEmpty={empty}
|
||||
data-testid="property-tags-value"
|
||||
readonly={readonly}
|
||||
>
|
||||
<TagsInlineEditor
|
||||
className={styles.tagInlineEditor}
|
||||
@@ -29,7 +30,8 @@ export const TagsValue = ({ onChange }: PropertyValueProps) => {
|
||||
'com.affine.page-properties.property-value-placeholder'
|
||||
]()}
|
||||
pageId={doc.id}
|
||||
onChange={value => onChange(value, true)}
|
||||
onChange={() => {}}
|
||||
readonly={readonly}
|
||||
/>
|
||||
</PropertyValue>
|
||||
);
|
||||
|
||||
@@ -6,9 +6,7 @@ import { type ChangeEvent, useCallback } from 'react';
|
||||
import * as styles from './template.css';
|
||||
import type { PropertyValueProps } from './types';
|
||||
|
||||
export const TemplateValue = ({
|
||||
onChange: propOnChange,
|
||||
}: PropertyValueProps) => {
|
||||
export const TemplateValue = ({ readonly }: PropertyValueProps) => {
|
||||
const docService = useService(DocService);
|
||||
|
||||
const isTemplate = useLiveData(
|
||||
@@ -17,24 +15,26 @@ export const TemplateValue = ({
|
||||
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (readonly) return;
|
||||
const value = e.target.checked;
|
||||
docService.doc.record.setProperty('isTemplate', value);
|
||||
propOnChange?.(value, true);
|
||||
},
|
||||
[docService.doc.record, propOnChange]
|
||||
[docService.doc.record, readonly]
|
||||
);
|
||||
|
||||
const toggle = useCallback(() => {
|
||||
if (readonly) return;
|
||||
docService.doc.record.setProperty('isTemplate', !isTemplate);
|
||||
}, [docService.doc.record, isTemplate]);
|
||||
}, [docService.doc.record, isTemplate, readonly]);
|
||||
|
||||
return (
|
||||
<PropertyValue className={styles.property} onClick={toggle}>
|
||||
<PropertyValue className={styles.property} onClick={toggle} readonly>
|
||||
<Checkbox
|
||||
data-testid="toggle-template-checkbox"
|
||||
checked={!!isTemplate}
|
||||
onChange={onChange}
|
||||
className={styles.checkbox}
|
||||
disabled={readonly}
|
||||
/>
|
||||
</PropertyValue>
|
||||
);
|
||||
|
||||
@@ -13,7 +13,11 @@ import { ConfigModal } from '../../mobile';
|
||||
import * as styles from './text.css';
|
||||
import type { PropertyValueProps } from './types';
|
||||
|
||||
const DesktopTextValue = ({ value, onChange }: PropertyValueProps) => {
|
||||
const DesktopTextValue = ({
|
||||
value,
|
||||
onChange,
|
||||
readonly,
|
||||
}: PropertyValueProps) => {
|
||||
const [tempValue, setTempValue] = useState<string>(value);
|
||||
const handleClick = useCallback((e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
@@ -50,6 +54,7 @@ const DesktopTextValue = ({ value, onChange }: PropertyValueProps) => {
|
||||
className={styles.textPropertyValueContainer}
|
||||
onClick={handleClick}
|
||||
isEmpty={!value}
|
||||
readonly={readonly}
|
||||
>
|
||||
<textarea
|
||||
ref={ref}
|
||||
@@ -62,6 +67,7 @@ const DesktopTextValue = ({ value, onChange }: PropertyValueProps) => {
|
||||
placeholder={t[
|
||||
'com.affine.page-properties.property-value-placeholder'
|
||||
]()}
|
||||
disabled={readonly}
|
||||
/>
|
||||
<div className={styles.textInvisible}>
|
||||
{tempValue}
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { DocCustomPropertyInfo } from '@affine/core/modules/db';
|
||||
export interface PropertyValueProps {
|
||||
propertyInfo?: DocCustomPropertyInfo;
|
||||
value: any;
|
||||
readonly?: boolean;
|
||||
onChange: (value: any, skipCommit?: boolean) => void; // if skipCommit is true, the change will be handled in the component itself
|
||||
}
|
||||
|
||||
|
||||
24
packages/frontend/core/src/components/guard/doc-guard.tsx
Normal file
24
packages/frontend/core/src/components/guard/doc-guard.tsx
Normal file
@@ -0,0 +1,24 @@
|
||||
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) => 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');
|
||||
};
|
||||
@@ -12,8 +12,7 @@ import { useNavigateHelper } from '../use-navigate-helper';
|
||||
|
||||
export function useBlockSuiteMetaHelper() {
|
||||
const workspace = useService(WorkspaceService).workspace;
|
||||
const { setDocMeta, getDocMeta, setDocTitle, setDocReadonly } =
|
||||
useDocMetaHelper();
|
||||
const { setDocMeta, getDocMeta, setDocTitle } = useDocMetaHelper();
|
||||
const { createDoc } = useDocCollectionHelper(workspace.docCollection);
|
||||
const { openPage } = useNavigateHelper();
|
||||
const docRecordList = useService(DocsService).list;
|
||||
@@ -25,10 +24,9 @@ export function useBlockSuiteMetaHelper() {
|
||||
const docRecord = docRecordList.doc$(docId).value;
|
||||
if (docRecord) {
|
||||
docRecord.moveToTrash();
|
||||
setDocReadonly(docId, true);
|
||||
}
|
||||
},
|
||||
[docRecordList, setDocReadonly]
|
||||
[docRecordList]
|
||||
);
|
||||
|
||||
const restoreFromTrash = useCallback(
|
||||
@@ -36,10 +34,9 @@ export function useBlockSuiteMetaHelper() {
|
||||
const docRecord = docRecordList.doc$(docId).value;
|
||||
if (docRecord) {
|
||||
docRecord.restoreFromTrash();
|
||||
setDocReadonly(docId, false);
|
||||
}
|
||||
},
|
||||
[docRecordList, setDocReadonly]
|
||||
[docRecordList]
|
||||
);
|
||||
|
||||
const permanentlyDeletePage = useCallback(
|
||||
|
||||
@@ -9,7 +9,9 @@ import type { Editor } from '@affine/core/modules/editor';
|
||||
import { EditorSettingService } from '@affine/core/modules/editor-setting';
|
||||
import { CompatibleFavoriteItemsAdapter } from '@affine/core/modules/favorite';
|
||||
import { OpenInAppService } from '@affine/core/modules/open-in-app';
|
||||
import { GuardService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { UserFriendlyError } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { track } from '@affine/track';
|
||||
import {
|
||||
@@ -35,6 +37,7 @@ export function useRegisterBlocksuiteEditorCommands(
|
||||
active: boolean
|
||||
) {
|
||||
const doc = useService(DocService).doc;
|
||||
const guardService = useService(GuardService);
|
||||
const docId = doc.id;
|
||||
const mode = useLiveData(editor.mode$);
|
||||
const t = useI18n();
|
||||
@@ -74,11 +77,22 @@ export function useRegisterBlocksuiteEditorCommands(
|
||||
}),
|
||||
cancelText: t['com.affine.confirmModal.button.cancel'](),
|
||||
confirmText: t.Delete(),
|
||||
onConfirm: () => {
|
||||
doc.moveToTrash();
|
||||
onConfirm: async () => {
|
||||
try {
|
||||
const canTrash = await guardService.can('Doc_Trash', docId);
|
||||
if (!canTrash) {
|
||||
toast(t['com.affine.no-permission']());
|
||||
return;
|
||||
}
|
||||
doc.moveToTrash();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const userFriendlyError = UserFriendlyError.fromAnyError(error);
|
||||
toast(t[`error.${userFriendlyError.name}`](userFriendlyError.data));
|
||||
}
|
||||
},
|
||||
});
|
||||
}, [doc, openConfirmModal, t]);
|
||||
}, [doc, docId, guardService, openConfirmModal, t]);
|
||||
|
||||
const isCloudWorkspace = workspace.flavour !== 'local';
|
||||
|
||||
@@ -190,6 +204,11 @@ export function useRegisterBlocksuiteEditorCommands(
|
||||
? t['com.affine.cmdk.affine.current-page-width-layout.standard']()
|
||||
: t['com.affine.cmdk.affine.current-page-width-layout.full-width'](),
|
||||
async run() {
|
||||
const canEdit = await guardService.can('Doc_Update', docId);
|
||||
if (!canEdit) {
|
||||
toast(t['com.affine.no-permission']());
|
||||
return;
|
||||
}
|
||||
doc.record.setProperty(
|
||||
'pageWidth',
|
||||
checked ? 'standard' : 'fullWidth'
|
||||
@@ -306,7 +325,12 @@ export function useRegisterBlocksuiteEditorCommands(
|
||||
category: `editor:${mode}`,
|
||||
icon: mode === 'page' ? <PageIcon /> : <EdgelessIcon />,
|
||||
label: t['com.affine.cmdk.affine.editor.restore-from-trash'](),
|
||||
run() {
|
||||
async run() {
|
||||
const canRestore = await guardService.can('Doc_Restore', docId);
|
||||
if (!canRestore) {
|
||||
toast(t['com.affine.no-permission']());
|
||||
return;
|
||||
}
|
||||
track.$.cmdk.editor.restoreDoc();
|
||||
|
||||
doc.restoreFromTrash();
|
||||
@@ -383,5 +407,6 @@ export function useRegisterBlocksuiteEditorCommands(
|
||||
checked,
|
||||
openInAppService,
|
||||
active,
|
||||
guardService,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import type { DocMeta, Workspace } from '@blocksuite/affine/store';
|
||||
import { useService } from '@toeverything/infra';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
@@ -26,7 +25,6 @@ export function useBlockSuiteDocMeta(docCollection: Workspace) {
|
||||
}
|
||||
|
||||
export function useDocMetaHelper() {
|
||||
const workspaceService = useService(WorkspaceService);
|
||||
const docsService = useService(DocsService);
|
||||
|
||||
const setDocTitle = useAsyncCallback(
|
||||
@@ -53,23 +51,13 @@ export function useDocMetaHelper() {
|
||||
},
|
||||
[docsService]
|
||||
);
|
||||
const setDocReadonly = useCallback(
|
||||
(docId: string, readonly: boolean) => {
|
||||
const doc = workspaceService.workspace.docCollection.getDoc(docId);
|
||||
if (doc) {
|
||||
doc.readonly = readonly;
|
||||
}
|
||||
},
|
||||
[workspaceService]
|
||||
);
|
||||
|
||||
return useMemo(
|
||||
() => ({
|
||||
setDocTitle,
|
||||
setDocMeta,
|
||||
getDocMeta,
|
||||
setDocReadonly,
|
||||
}),
|
||||
[getDocMeta, setDocMeta, setDocReadonly, setDocTitle]
|
||||
[getDocMeta, setDocMeta, setDocTitle]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import './page-detail-editor.css';
|
||||
import type { AffineEditorContainer } from '@blocksuite/affine/presets';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import clsx from 'clsx';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { DocService } from '../modules/doc';
|
||||
import { EditorService } from '../modules/editor';
|
||||
@@ -24,9 +25,13 @@ export type OnLoadEditor = (
|
||||
|
||||
export interface PageDetailEditorProps {
|
||||
onLoad?: OnLoadEditor;
|
||||
readonly?: boolean;
|
||||
}
|
||||
|
||||
export const PageDetailEditor = ({ onLoad }: PageDetailEditorProps) => {
|
||||
export const PageDetailEditor = ({
|
||||
onLoad,
|
||||
readonly,
|
||||
}: PageDetailEditorProps) => {
|
||||
const editor = useService(EditorService).editor;
|
||||
const mode = useLiveData(editor.mode$);
|
||||
const defaultOpenProperty = useLiveData(editor.defaultOpenProperty$);
|
||||
@@ -47,6 +52,10 @@ export const PageDetailEditor = ({ onLoad }: PageDetailEditorProps) => {
|
||||
? pageWidth === 'fullWidth'
|
||||
: settings.fullWidthLayout;
|
||||
|
||||
useEffect(() => {
|
||||
editor.doc.blockSuiteDoc.readonly = readonly ?? false;
|
||||
}, [editor, readonly]);
|
||||
|
||||
return (
|
||||
<CustomEditorWrapper>
|
||||
<Editor
|
||||
@@ -58,6 +67,7 @@ export const PageDetailEditor = ({ onLoad }: PageDetailEditorProps) => {
|
||||
defaultOpenProperty={defaultOpenProperty}
|
||||
page={editor.doc.blockSuiteDoc}
|
||||
shared={isSharedMode}
|
||||
readonly={readonly}
|
||||
onEditorReady={onLoad}
|
||||
/>
|
||||
</CustomEditorWrapper>
|
||||
|
||||
@@ -26,7 +26,7 @@ export const ListFloatingToolbar = ({
|
||||
<FloatingToolbar className={styles.floatingToolbar} open={open}>
|
||||
<FloatingToolbar.Item>{content}</FloatingToolbar.Item>
|
||||
<FloatingToolbar.Button onClick={onClose} icon={<CloseIcon />} />
|
||||
<FloatingToolbar.Separator />
|
||||
{(!!onRestore || !!onDelete) && <FloatingToolbar.Separator />}
|
||||
{!!onRestore && (
|
||||
<FloatingToolbar.Button
|
||||
onClick={onRestore}
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import { Checkbox, Tooltip, useDraggable } from '@affine/component';
|
||||
import { type Doc, DocsService } from '@affine/core/modules/doc';
|
||||
import { TagService } from '@affine/core/modules/tag';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { stopPropagation } from '@affine/core/utils';
|
||||
import { i18nTime } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { FrameworkScope, useLiveData, useService } from '@toeverything/infra';
|
||||
import type { ForwardedRef, PropsWithChildren } from 'react';
|
||||
import { forwardRef, useCallback, useEffect, useMemo } from 'react';
|
||||
import {
|
||||
forwardRef,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { WorkbenchLink } from '../../../modules/workbench/view/workbench-link';
|
||||
import {
|
||||
@@ -142,7 +150,7 @@ const PageListOperationsCell = ({
|
||||
) : null;
|
||||
};
|
||||
|
||||
export const PageListItem = (props: PageListItemProps) => {
|
||||
const PagelistItemInner = (props: PageListItemProps) => {
|
||||
const [displayProperties] = useAllDocDisplayProperties();
|
||||
const pageTitleElement = useMemo(() => {
|
||||
return (
|
||||
@@ -249,6 +257,29 @@ export const PageListItem = (props: PageListItemProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const PageListItem = (props: PageListItemProps) => {
|
||||
const docsService = useService(DocsService);
|
||||
const [doc, setDoc] = useState<Doc | null>(null);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const { doc, release } = docsService.open(props.pageId);
|
||||
setDoc(doc);
|
||||
return () => {
|
||||
release();
|
||||
};
|
||||
}, [props.pageId, docsService.list, docsService]);
|
||||
|
||||
if (!doc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<FrameworkScope scope={doc.scope}>
|
||||
<PagelistItemInner {...props} />
|
||||
</FrameworkScope>
|
||||
);
|
||||
};
|
||||
|
||||
type PageListWrapperProps = PropsWithChildren<
|
||||
Pick<PageListItemProps, 'to' | 'pageId' | 'onClick' | 'draggable'> & {
|
||||
isDragging: boolean;
|
||||
|
||||
@@ -56,12 +56,14 @@ export const VirtualizedPageList = memo(function VirtualizedPageList({
|
||||
filters,
|
||||
listItem,
|
||||
setHideHeaderCreateNewPage,
|
||||
disableMultiDelete,
|
||||
}: {
|
||||
tag?: Tag;
|
||||
collection?: Collection;
|
||||
filters?: Filter[];
|
||||
listItem?: DocMeta[];
|
||||
setHideHeaderCreateNewPage?: (hide: boolean) => void;
|
||||
disableMultiDelete?: boolean;
|
||||
}) {
|
||||
const t = useI18n();
|
||||
const listRef = useRef<ItemListHandle>(null);
|
||||
@@ -186,7 +188,7 @@ export const VirtualizedPageList = memo(function VirtualizedPageList({
|
||||
/>
|
||||
<ListFloatingToolbar
|
||||
open={showFloatingToolbar}
|
||||
onDelete={handleMultiDelete}
|
||||
onDelete={disableMultiDelete ? undefined : handleMultiDelete}
|
||||
onClose={hideFloatingToolbar}
|
||||
content={
|
||||
<Trans
|
||||
|
||||
@@ -14,6 +14,7 @@ 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';
|
||||
@@ -57,7 +58,7 @@ export interface PageOperationCellProps {
|
||||
onRemoveFromAllowList?: () => void;
|
||||
}
|
||||
|
||||
export const PageOperationCell = ({
|
||||
const PageOperationCellMenuItem = ({
|
||||
isInAllowList,
|
||||
page,
|
||||
onRemoveFromAllowList,
|
||||
@@ -67,12 +68,15 @@ export const PageOperationCell = ({
|
||||
workspaceService,
|
||||
compatibleFavoriteItemsAdapter: favAdapter,
|
||||
workbenchService,
|
||||
guardService,
|
||||
} = useServices({
|
||||
WorkspaceService,
|
||||
CompatibleFavoriteItemsAdapter,
|
||||
WorkbenchService,
|
||||
GuardService,
|
||||
});
|
||||
|
||||
const canMoveToTrash = useLiveData(guardService.can$('Doc_Trash', page.id));
|
||||
const currentWorkspace = workspaceService.workspace;
|
||||
const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc'));
|
||||
const workbench = workbenchService.workbench;
|
||||
@@ -159,7 +163,7 @@ export const PageOperationCell = ({
|
||||
}
|
||||
}, [onRemoveFromAllowList]);
|
||||
|
||||
const OperationMenu = (
|
||||
return (
|
||||
<>
|
||||
{page.isPublic && (
|
||||
<DisablePublicSharing
|
||||
@@ -199,9 +203,36 @@ export const PageOperationCell = ({
|
||||
{t['com.affine.header.option.duplicate']()}
|
||||
</MenuItem>
|
||||
|
||||
<MoveToTrash data-testid="move-to-trash" onSelect={onRemoveToTrash} />
|
||||
<MoveToTrash
|
||||
data-testid="move-to-trash"
|
||||
onSelect={onRemoveToTrash}
|
||||
disabled={!canMoveToTrash}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const PageOperationCell = ({
|
||||
isInAllowList,
|
||||
page,
|
||||
onRemoveFromAllowList,
|
||||
}: PageOperationCellProps) => {
|
||||
const t = useI18n();
|
||||
const { compatibleFavoriteItemsAdapter: favAdapter } = useServices({
|
||||
CompatibleFavoriteItemsAdapter,
|
||||
});
|
||||
|
||||
const favourite = useLiveData(favAdapter.isFavorite$(page.id, 'doc'));
|
||||
|
||||
const onToggleFavoritePage = useCallback(() => {
|
||||
const status = favAdapter.isFavorite(page.id, 'doc');
|
||||
favAdapter.toggle(page.id, 'doc');
|
||||
toast(
|
||||
status
|
||||
? t['com.affine.toastMessage.removedFavorites']()
|
||||
: t['com.affine.toastMessage.addedFavorites']()
|
||||
);
|
||||
}, [page.id, favAdapter, t]);
|
||||
return (
|
||||
<>
|
||||
<ColWrapper
|
||||
@@ -214,7 +245,13 @@ export const PageOperationCell = ({
|
||||
</ColWrapper>
|
||||
<ColWrapper alignment="start">
|
||||
<Menu
|
||||
items={OperationMenu}
|
||||
items={
|
||||
<PageOperationCellMenuItem
|
||||
page={page}
|
||||
isInAllowList={isInAllowList}
|
||||
onRemoveFromAllowList={onRemoveFromAllowList}
|
||||
/>
|
||||
}
|
||||
contentOptions={{
|
||||
align: 'end',
|
||||
}}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { toast, useConfirmModal } from '@affine/component';
|
||||
import { useBlockSuiteMetaHelper } from '@affine/core/components/hooks/affine/use-block-suite-meta-helper';
|
||||
import { useBlockSuiteDocMeta } from '@affine/core/components/hooks/use-block-suite-page-meta';
|
||||
import { GuardService } from '@affine/core/modules/permissions';
|
||||
import { WorkspaceService } from '@affine/core/modules/workspace';
|
||||
import { Trans, useI18n } from '@affine/i18n';
|
||||
import type { DocMeta } from '@blocksuite/affine/store';
|
||||
@@ -16,8 +17,15 @@ import type { ItemListHandle, ListItem } from './types';
|
||||
import { useFilteredPageMetas } from './use-filtered-page-metas';
|
||||
import { VirtualizedList } from './virtualized-list';
|
||||
|
||||
export const VirtualizedTrashList = () => {
|
||||
export const VirtualizedTrashList = ({
|
||||
disableMultiDelete,
|
||||
disableMultiRestore,
|
||||
}: {
|
||||
disableMultiDelete?: boolean;
|
||||
disableMultiRestore?: boolean;
|
||||
}) => {
|
||||
const currentWorkspace = useService(WorkspaceService).workspace;
|
||||
const guardService = useService(GuardService);
|
||||
const docCollection = currentWorkspace.docCollection;
|
||||
const { restoreFromTrash, permanentlyDeletePage } = useBlockSuiteMetaHelper();
|
||||
const pageMetas = useBlockSuiteDocMeta(docCollection);
|
||||
@@ -82,16 +90,38 @@ export const VirtualizedTrashList = () => {
|
||||
(item: ListItem) => {
|
||||
const page = item as DocMeta;
|
||||
const onRestorePage = () => {
|
||||
restoreFromTrash(page.id);
|
||||
toast(
|
||||
t['com.affine.toastMessage.restored']({
|
||||
title: page.title || 'Untitled',
|
||||
guardService
|
||||
.can('Doc_Delete', page.id)
|
||||
.then(can => {
|
||||
if (can) {
|
||||
restoreFromTrash(page.id);
|
||||
toast(
|
||||
t['com.affine.toastMessage.restored']({
|
||||
title: page.title || 'Untitled',
|
||||
})
|
||||
);
|
||||
} else {
|
||||
toast(t['com.affine.no-permission']());
|
||||
}
|
||||
})
|
||||
);
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
const onPermanentlyDeletePage = () => {
|
||||
permanentlyDeletePage(page.id);
|
||||
toast(t['com.affine.toastMessage.permanentlyDeleted']());
|
||||
guardService
|
||||
.can('Doc_Delete', page.id)
|
||||
.then(can => {
|
||||
if (can) {
|
||||
permanentlyDeletePage(page.id);
|
||||
toast(t['com.affine.toastMessage.permanentlyDeleted']());
|
||||
} else {
|
||||
toast(t['com.affine.no-permission']());
|
||||
}
|
||||
})
|
||||
.catch(e => {
|
||||
console.error(e);
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -102,7 +132,7 @@ export const VirtualizedTrashList = () => {
|
||||
);
|
||||
},
|
||||
|
||||
[permanentlyDeletePage, restoreFromTrash, t]
|
||||
[guardService, permanentlyDeletePage, restoreFromTrash, t]
|
||||
);
|
||||
const pageItemRenderer = useCallback((item: ListItem) => {
|
||||
return <PageListItemRenderer {...item} />;
|
||||
@@ -128,9 +158,9 @@ export const VirtualizedTrashList = () => {
|
||||
/>
|
||||
<ListFloatingToolbar
|
||||
open={showFloatingToolbar}
|
||||
onDelete={onConfirmPermanentlyDelete}
|
||||
onDelete={disableMultiDelete ? undefined : onConfirmPermanentlyDelete}
|
||||
onClose={hideFloatingToolbar}
|
||||
onRestore={handleMultiRestore}
|
||||
onRestore={disableMultiRestore ? undefined : handleMultiRestore}
|
||||
content={
|
||||
<Trans
|
||||
i18nKey="com.affine.page.toolbar.selected"
|
||||
|
||||
@@ -1,12 +1,15 @@
|
||||
import {
|
||||
AnimatedDeleteIcon,
|
||||
toast,
|
||||
useConfirmModal,
|
||||
useDropTarget,
|
||||
} from '@affine/component';
|
||||
import { MenuLinkItem } from '@affine/core/modules/app-sidebar/views';
|
||||
import { DocsService } from '@affine/core/modules/doc';
|
||||
import { GlobalContextService } from '@affine/core/modules/global-context';
|
||||
import { GuardService } from '@affine/core/modules/permissions';
|
||||
import type { AffineDNDData } from '@affine/core/types/dnd';
|
||||
import { UserFriendlyError } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
|
||||
@@ -16,6 +19,7 @@ export const TrashButton = () => {
|
||||
const { openConfirmModal } = useConfirmModal();
|
||||
const globalContextService = useService(GlobalContextService);
|
||||
const trashActive = useLiveData(globalContextService.globalContext.isTrash.$);
|
||||
const guardService = useService(GuardService);
|
||||
|
||||
const { dropTargetRef, draggedOver } = useDropTarget<AffineDNDData>(
|
||||
() => ({
|
||||
@@ -41,15 +45,32 @@ export const TrashButton = () => {
|
||||
confirmButtonOptions: {
|
||||
variant: 'error',
|
||||
},
|
||||
onConfirm() {
|
||||
docRecord.moveToTrash();
|
||||
async onConfirm() {
|
||||
try {
|
||||
const canTrash = await guardService.can(
|
||||
'Doc_Trash',
|
||||
docRecord.id
|
||||
);
|
||||
if (!canTrash) {
|
||||
toast(t['com.affine.no-permission']());
|
||||
return;
|
||||
}
|
||||
docRecord.moveToTrash();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const userFriendlyError =
|
||||
UserFriendlyError.fromAnyError(error);
|
||||
toast(
|
||||
t[`error.${userFriendlyError.name}`](userFriendlyError.data)
|
||||
);
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
}),
|
||||
[docsService.list, openConfirmModal, t]
|
||||
[docsService.list, guardService, openConfirmModal, t]
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -7,6 +7,9 @@ export const tagsInlineEditor = style({
|
||||
'&[data-empty=true]': {
|
||||
color: cssVar('placeholderColor'),
|
||||
},
|
||||
'&[data-readonly="true"]': {
|
||||
pointerEvents: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user