From d47bb645976e58187ca4f72af47335ac9841f927 Mon Sep 17 00:00:00 2001 From: JimmFly Date: Thu, 20 Mar 2025 07:31:11 +0000 Subject: [PATCH] feat(core): add no access to share menu (#10927) --- .../bi-directional-link-panel.css.ts | 7 + .../bi-directional-link-panel.tsx | 175 +++++++++++------- .../detail-page/detail-page-wrapper.tsx | 6 +- .../workspace/detail-page/detail-page.tsx | 5 + .../components/explorer/nodes/doc/index.tsx | 18 +- .../workspace/detail/mobile-detail-page.tsx | 4 + .../explorer/views/nodes/doc/empty.tsx | 6 +- .../explorer/views/nodes/doc/index.tsx | 39 ++-- .../src/modules/explorer/views/tree/node.tsx | 3 +- .../view/doc-preview/doc-peek-view.tsx | 7 +- .../general-access/members-permission.tsx | 25 ++- packages/frontend/i18n/src/i18n.gen.ts | 4 + packages/frontend/i18n/src/resources/en.json | 1 + 13 files changed, 209 insertions(+), 91 deletions(-) diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/bi-directional-link-panel.css.ts b/packages/frontend/core/src/blocksuite/block-suite-editor/bi-directional-link-panel.css.ts index d0e11c55d2..40dae9f1ac 100644 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/bi-directional-link-panel.css.ts +++ b/packages/frontend/core/src/blocksuite/block-suite-editor/bi-directional-link-panel.css.ts @@ -110,6 +110,13 @@ globalStyle(`${linkPreview} *`, { cursor: 'default', }); +export const notFound = style({ + color: cssVarV2('text/secondary'), + fontSize: '12px', + lineHeight: '16px', + textAlign: 'center', +}); + export const linkPreviewRenderer = style({ cursor: 'pointer', }); diff --git a/packages/frontend/core/src/blocksuite/block-suite-editor/bi-directional-link-panel.tsx b/packages/frontend/core/src/blocksuite/block-suite-editor/bi-directional-link-panel.tsx index a56eab5d1c..177c8911c9 100644 --- a/packages/frontend/core/src/blocksuite/block-suite-editor/bi-directional-link-panel.tsx +++ b/packages/frontend/core/src/blocksuite/block-suite-editor/bi-directional-link-panel.tsx @@ -7,6 +7,7 @@ import { type Link, } from '@affine/core/modules/doc-link'; import { toURLSearchParams } from '@affine/core/modules/navigation'; +import { GuardService } from '@affine/core/modules/permissions'; import { GlobalSessionStateService } from '@affine/core/modules/storage'; import { WorkbenchLink } from '@affine/core/modules/workbench'; import { @@ -15,13 +16,17 @@ import { } from '@affine/core/modules/workspace'; import { useI18n } from '@affine/i18n'; import track from '@affine/track'; -import type { TransformerMiddleware } from '@blocksuite/affine/store'; +import type { + ExtensionType, + TransformerMiddleware, +} from '@blocksuite/affine/store'; import { ToggleDownIcon } from '@blocksuite/icons/rc'; import * as Collapsible from '@radix-ui/react-collapsible'; import { LiveData, useFramework, useLiveData, + useService, useServices, } from '@toeverything/infra'; import { @@ -46,6 +51,18 @@ import * as styles from './bi-directional-link-panel.css'; const PREFIX = 'bi-directional-link-panel-collapse:'; +type BacklinkGroups = { + docId: string; + title: string; + links: Backlink[]; +}; + +type TextRendererOptions = { + customHeading: boolean; + extensions: ExtensionType[]; + additionalMiddlewares: TransformerMiddleware[]; +}; + const useBiDirectionalLinkPanelCollapseState = ( docId: string, linkDocId?: string @@ -155,7 +172,7 @@ const usePreviewExtensions = () => { return [extensions, portals] as const; }; -const useBacklinkGroups = () => { +const useBacklinkGroups: () => BacklinkGroups[] = () => { const { docLinksService } = useServices({ DocLinksService, }); @@ -226,72 +243,10 @@ export const BacklinkGroups = () => { docId={docService.doc.id} linkDocId={linkGroup.docId} > -
- {linkGroup.links.map(link => { - if (!link.markdownPreview) { - return null; - } - const searchParams = new URLSearchParams(); - const displayMode = link.displayMode || 'page'; - searchParams.set('mode', displayMode); - - let blockId = link.blockId; - if ( - link.parentFlavour === 'affine:database' && - link.parentBlockId - ) { - // if parentBlockFlavour is 'affine:database', - // we will fallback to the database block instead - blockId = link.parentBlockId; - } else if (displayMode === 'edgeless' && link.noteBlockId) { - // if note has displayMode === 'edgeless' && has noteBlockId, - // set noteBlockId as blockId - blockId = link.noteBlockId; - } - - searchParams.set('blockIds', blockId); - - const to = { - pathname: '/' + linkGroup.docId, - search: '?' + searchParams.toString(), - hash: '', - }; - - // if this backlink has no noteBlock && displayMode is edgeless, we will render - // the link as a page link - const edgelessLink = - displayMode === 'edgeless' && !link.noteBlockId; - - return ( - { - track.doc.biDirectionalLinksPanel.backlinkPreview.navigate(); - }} - > - {edgelessLink ? ( - <> - [Edgeless] - - - ) : ( - - )} - - ); - })} -
+ ))} <> @@ -303,6 +258,90 @@ export const BacklinkGroups = () => { ); }; +export const LinkPreview = ({ + linkGroup, + textRendererOptions, +}: { + linkGroup: BacklinkGroups; + textRendererOptions: TextRendererOptions; +}) => { + const guardService = useService(GuardService); + const canAccess = useLiveData(guardService.can$('Doc_Read', linkGroup.docId)); + const t = useI18n(); + + if (!canAccess) { + return ( + + {t['com.affine.share-menu.option.permission.no-access']()} + + ); + } + return ( +
+ {linkGroup.links.map(link => { + if (!link.markdownPreview) { + return null; + } + const searchParams = new URLSearchParams(); + const displayMode = link.displayMode || 'page'; + searchParams.set('mode', displayMode); + + let blockId = link.blockId; + if (link.parentFlavour === 'affine:database' && link.parentBlockId) { + // if parentBlockFlavour is 'affine:database', + // we will fallback to the database block instead + blockId = link.parentBlockId; + } else if (displayMode === 'edgeless' && link.noteBlockId) { + // if note has displayMode === 'edgeless' && has noteBlockId, + // set noteBlockId as blockId + blockId = link.noteBlockId; + } + + searchParams.set('blockIds', blockId); + + const to = { + pathname: '/' + linkGroup.docId, + search: '?' + searchParams.toString(), + hash: '', + }; + + // if this backlink has no noteBlock && displayMode is edgeless, we will render + // the link as a page link + const edgelessLink = displayMode === 'edgeless' && !link.noteBlockId; + + return ( + { + track.doc.biDirectionalLinksPanel.backlinkPreview.navigate(); + }} + > + {edgelessLink ? ( + <> + [Edgeless] + + + ) : ( + + )} + + ); + })} +
+ ); +}; + export const BiDirectionalLinkPanel = () => { const { docLinksService, docService } = useServices({ DocLinksService, diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-wrapper.tsx b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-wrapper.tsx index 5b08c0c22a..6956083caa 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-wrapper.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page-wrapper.tsx @@ -68,10 +68,12 @@ export const DetailPageWrapper = ({ children, skeleton, notFound, + canAccess, }: PropsWithChildren<{ pageId: string; skeleton: ReactNode; notFound: ReactNode; + canAccess?: boolean; }>) => { const { doc, editor, docListReady } = useLoadDoc(pageId); // if sync engine has been synced and the page is null, show 404 page. @@ -79,8 +81,10 @@ export const DetailPageWrapper = ({ return notFound; } - if (!doc || !editor) { + if (canAccess === undefined || !doc || !editor) { return skeleton; + } else if (!canAccess) { + return notFound; } return ( diff --git a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx index 568be593cf..d90735cd46 100644 --- a/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx +++ b/packages/frontend/core/src/desktop/pages/workspace/detail-page/detail-page.tsx @@ -368,6 +368,7 @@ const DetailPageImpl = memo(function DetailPageImpl() { export const Component = () => { const params = useParams(); const recentPages = useService(RecentDocsService); + const guardService = useService(GuardService); useEffect(() => { if (params.pageId) { @@ -379,10 +380,14 @@ export const Component = () => { }, [params, recentPages]); const pageId = params.pageId; + const canAccess = useLiveData( + pageId ? guardService.can$('Doc_Read', pageId) : undefined + ); return pageId ? ( } notFound={} > diff --git a/packages/frontend/core/src/mobile/components/explorer/nodes/doc/index.tsx b/packages/frontend/core/src/mobile/components/explorer/nodes/doc/index.tsx index 1d572d4b0c..1140962fe4 100644 --- a/packages/frontend/core/src/mobile/components/explorer/nodes/doc/index.tsx +++ b/packages/frontend/core/src/mobile/components/explorer/nodes/doc/index.tsx @@ -57,6 +57,7 @@ export const ExplorerDocNode = ({ reference: isLinked, }) ); + const docTitle = useLiveData(docDisplayMetaService.title$(docId)); const isInTrash = useLiveData(docRecord?.trash$); const enableEmojiIcon = useLiveData( @@ -133,10 +134,19 @@ export const ExplorerDocNode = ({ operations={finalOperations} data-testid={`explorer-doc-${docId}`} > - {children?.map(child => ( - - ))} - + + {canRead => + canRead + ? children?.map((child, index) => ( + + )) + : null + } + {canEdit => canEdit ? ( diff --git a/packages/frontend/core/src/mobile/pages/workspace/detail/mobile-detail-page.tsx b/packages/frontend/core/src/mobile/pages/workspace/detail/mobile-detail-page.tsx index ae0389910c..31ac8f76d0 100644 --- a/packages/frontend/core/src/mobile/pages/workspace/detail/mobile-detail-page.tsx +++ b/packages/frontend/core/src/mobile/pages/workspace/detail/mobile-detail-page.tsx @@ -254,6 +254,9 @@ const MobileDetailPage = ({ const [showTitle, setShowTitle] = useState(checkShowTitle); const title = useLiveData(docDisplayMetaService.title$(pageId)); + const guardService = useService(GuardService); + const canAccess = useLiveData(guardService.can$('Doc_Read', pageId)); + const allJournalDates = useLiveData(journalService.allJournalDates$); const location = useLiveData(workbench.location$); @@ -281,6 +284,7 @@ const MobileDetailPage = ({ skeleton={date ? skeleton : skeletonWithBack} notFound={date ? notFound : notFoundWithBack} pageId={pageId} + canAccess={canAccess} > ) => void; + noAccessible?: boolean; }) => { const { dropTargetRef } = useDropTarget( () => ({ @@ -19,7 +21,9 @@ export const Empty = ({ return ( - {t['com.affine.rootAppSidebar.docs.no-subdoc']()} + {noAccessible + ? t['com.affine.share-menu.option.permission.no-access']() + : t['com.affine.rootAppSidebar.docs.no-subdoc']()} ); }; diff --git a/packages/frontend/core/src/modules/explorer/views/nodes/doc/index.tsx b/packages/frontend/core/src/modules/explorer/views/nodes/doc/index.tsx index 9125dd8034..1493403a79 100644 --- a/packages/frontend/core/src/modules/explorer/views/nodes/doc/index.tsx +++ b/packages/frontend/core/src/modules/explorer/views/nodes/doc/index.tsx @@ -5,6 +5,7 @@ import { toast, Tooltip, } from '@affine/component'; +import { DocPermissionGuard } from '@affine/core/components/guard/doc-guard'; import { useAsyncCallback } from '@affine/core/components/hooks/affine-async-hooks'; import { WorkspaceDialogService } from '@affine/core/modules/dialogs'; import { DocsService } from '@affine/core/modules/doc'; @@ -43,6 +44,7 @@ export const ExplorerDocNode = ({ }: { docId: string; isLinked?: boolean; + forwardKey?: string; } & GenericExplorerNode) => { const t = useI18n(); const { @@ -261,24 +263,35 @@ export const ExplorerDocNode = ({ }} onRename={handleRename} childrenPlaceholder={ - searching ? null : + searching ? null : ( + 0} + /> + ) } operations={finalOperations} dropEffect={handleDropEffectOnDoc} data-testid={`explorer-doc-${docId}`} > - {children?.map(child => ( - - ))} + + {canRead => + canRead + ? children?.map((child, index) => ( + + )) + : null + } + ); }; diff --git a/packages/frontend/core/src/modules/explorer/views/tree/node.tsx b/packages/frontend/core/src/modules/explorer/views/tree/node.tsx index 80a7b27971..e3ce2870b8 100644 --- a/packages/frontend/core/src/modules/explorer/views/tree/node.tsx +++ b/packages/frontend/core/src/modules/explorer/views/tree/node.tsx @@ -73,6 +73,7 @@ export interface BaseExplorerTreeNodeProps { operations?: NodeOperation[]; childrenOperations?: NodeOperation[]; childrenPlaceholder?: React.ReactNode; + linkComponent?: React.ComponentType< React.PropsWithChildren<{ to: To; className?: string }> & RefAttributes & { draggable?: boolean } @@ -492,7 +493,7 @@ export const ExplorerTreeNode = ({ {/* For lastInGroup check, the placeholder must be placed above all children in the dom */}
- {childCount === 0 && !collapsed && childrenPlaceholder} + {childCount === 0 && !collapsed ? childrenPlaceholder : null}
{collapsed ? null : children} diff --git a/packages/frontend/core/src/modules/peek-view/view/doc-preview/doc-peek-view.tsx b/packages/frontend/core/src/modules/peek-view/view/doc-preview/doc-peek-view.tsx index 25a86422a7..58f4b99fd6 100644 --- a/packages/frontend/core/src/modules/peek-view/view/doc-preview/doc-peek-view.tsx +++ b/packages/frontend/core/src/modules/peek-view/view/doc-preview/doc-peek-view.tsx @@ -220,9 +220,12 @@ export function DocPeekPreview({ !animating ); + const guardService = useService(GuardService); + const canAccess = useLiveData(guardService.can$('Doc_Read', docId)); + // if sync engine has been synced and the page is null, show 404 page. - if (!doc || !editor) { - return loading ? ( + if (!doc || !editor || !canAccess) { + return loading || canAccess === undefined ? ( ) : ( diff --git a/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/members-permission.tsx b/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/members-permission.tsx index f2e3b2d1cd..8fe34e6427 100644 --- a/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/members-permission.tsx +++ b/packages/frontend/core/src/modules/share-menu/view/share-menu/general-access/members-permission.tsx @@ -28,6 +28,8 @@ const getRoleName = (t: ReturnType, role?: DocRole) => { return t['com.affine.share-menu.option.permission.can-edit'](); case DocRole.Reader: return t['com.affine.share-menu.option.permission.can-read'](); + case DocRole.None: + return t['com.affine.share-menu.option.permission.no-access'](); default: return ''; } @@ -53,7 +55,9 @@ export const MembersPermission = ({ [docDefaultRole, t] ); const showTips = - docDefaultRole === DocRole.Reader || docDefaultRole === DocRole.Editor; + docDefaultRole === DocRole.Reader || + docDefaultRole === DocRole.Editor || + docDefaultRole === DocRole.None; const changePermission = useAsyncCallback( async (docRole: DocRole) => { try { @@ -93,6 +97,14 @@ export const MembersPermission = ({ changePermission(DocRole.Reader); }, [changePermission, hittingPaywall, openPaywallModal]); + const selectNone = useCallback(() => { + if (hittingPaywall) { + openPaywallModal?.(); + return; + } + changePermission(DocRole.None); + }, [changePermission, hittingPaywall, openPaywallModal]); + return (
@@ -141,6 +153,17 @@ export const MembersPermission = ({
+ +
+
+ {t['com.affine.share-menu.option.permission.no-access']()} + {hittingPaywall ? : null} +
+
+
} > diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index 311c2678cb..846f830aa9 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -6004,6 +6004,10 @@ export function useAFFiNEI18N(): { * `Can read` */ ["com.affine.share-menu.option.permission.can-read"](): string; + /** + * `No access` + */ + ["com.affine.share-menu.option.permission.no-access"](): string; /** * `Members in workspace` */ diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index e54775d4c1..f48785451a 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1498,6 +1498,7 @@ "com.affine.share-menu.option.permission.can-manage": "Can manage", "com.affine.share-menu.option.permission.can-edit": "Can edit", "com.affine.share-menu.option.permission.can-read": "Can read", + "com.affine.share-menu.option.permission.no-access": "No access", "com.affine.share-menu.option.permission.label": "Members in workspace", "com.affine.share-menu.option.permission.tips": "Workspace admins and owner automatically have Can manage permissions.", "com.affine.share-menu.publish-to-web": "Publish to web",