mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
feat: shared link list (#14200)
#### PR Dependency Tree * **PR #14200** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Release Notes * **New Features** * Added a "Shared Links" panel to workspace management, enabling admins to view all published documents within a workspace * Added publication date tracking for published documents, now displayed alongside shared links * **Chores** * Removed deprecated `publicPages` field; use `publicDocs` instead <sub>✏️ Tip: You can customize this high-level summary in your review settings.</sub> <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -154,7 +154,7 @@ export const useColumns = () => {
|
||||
{
|
||||
id: 'actions',
|
||||
meta: {
|
||||
className: 'w-[80px] justify-end',
|
||||
className: 'w-[190px] justify-end',
|
||||
},
|
||||
header: () => (
|
||||
<div className="text-xs font-medium text-right">Actions</div>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Button } from '@affine/admin/components/ui/button';
|
||||
import { EditIcon } from '@blocksuite/icons/rc';
|
||||
import { EditIcon, LinkIcon } from '@blocksuite/icons/rc';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { DiscardChanges } from '../../../components/shared/discard-changes';
|
||||
import { useRightPanel } from '../../panel/context';
|
||||
import type { WorkspaceListItem } from '../schema';
|
||||
import { WorkspacePanel } from './workspace-panel';
|
||||
import { WorkspaceSharedLinksPanel } from './workspace-shared-links-panel';
|
||||
|
||||
export function DataTableRowActions({
|
||||
workspace,
|
||||
@@ -13,6 +14,9 @@ export function DataTableRowActions({
|
||||
workspace: WorkspaceListItem;
|
||||
}) {
|
||||
const [discardDialogOpen, setDiscardDialogOpen] = useState(false);
|
||||
const [pendingAction, setPendingAction] = useState<
|
||||
'edit' | 'sharedLinks' | null
|
||||
>(null);
|
||||
const {
|
||||
setPanelContent,
|
||||
openPanel,
|
||||
@@ -22,7 +26,7 @@ export function DataTableRowActions({
|
||||
setHasDirtyChanges,
|
||||
} = useRightPanel();
|
||||
|
||||
const handleConfirm = useCallback(() => {
|
||||
const openWorkspacePanel = useCallback(() => {
|
||||
setHasDirtyChanges(false);
|
||||
setPanelContent(
|
||||
<WorkspacePanel workspaceId={workspace.id} onClose={closePanel} />
|
||||
@@ -39,35 +43,89 @@ export function DataTableRowActions({
|
||||
workspace.id,
|
||||
]);
|
||||
|
||||
const openSharedLinksPanel = useCallback(() => {
|
||||
setHasDirtyChanges(false);
|
||||
setPanelContent(
|
||||
<WorkspaceSharedLinksPanel
|
||||
workspaceId={workspace.id}
|
||||
onClose={closePanel}
|
||||
/>
|
||||
);
|
||||
if (!isOpen) {
|
||||
openPanel();
|
||||
}
|
||||
}, [
|
||||
closePanel,
|
||||
isOpen,
|
||||
openPanel,
|
||||
setHasDirtyChanges,
|
||||
setPanelContent,
|
||||
workspace.id,
|
||||
]);
|
||||
|
||||
const handleEdit = useCallback(() => {
|
||||
if (hasDirtyChanges) {
|
||||
setPendingAction('edit');
|
||||
setDiscardDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
handleConfirm();
|
||||
}, [handleConfirm, hasDirtyChanges]);
|
||||
openWorkspacePanel();
|
||||
}, [hasDirtyChanges, openWorkspacePanel]);
|
||||
|
||||
const handleSharedLinks = useCallback(() => {
|
||||
if (hasDirtyChanges) {
|
||||
setPendingAction('sharedLinks');
|
||||
setDiscardDialogOpen(true);
|
||||
return;
|
||||
}
|
||||
openSharedLinksPanel();
|
||||
}, [hasDirtyChanges, openSharedLinksPanel]);
|
||||
|
||||
const handleDiscardConfirm = useCallback(() => {
|
||||
setDiscardDialogOpen(false);
|
||||
setHasDirtyChanges(false);
|
||||
handleConfirm();
|
||||
}, [handleConfirm, setHasDirtyChanges]);
|
||||
if (pendingAction === 'sharedLinks') {
|
||||
openSharedLinksPanel();
|
||||
} else {
|
||||
openWorkspacePanel();
|
||||
}
|
||||
setPendingAction(null);
|
||||
}, [
|
||||
openSharedLinksPanel,
|
||||
openWorkspacePanel,
|
||||
pendingAction,
|
||||
setHasDirtyChanges,
|
||||
]);
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="px-2 h-8 flex items-center gap-2"
|
||||
onClick={handleEdit}
|
||||
>
|
||||
<EditIcon fontSize={18} />
|
||||
<span>Edit</span>
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="px-2 h-8 flex items-center gap-2"
|
||||
onClick={handleEdit}
|
||||
>
|
||||
<EditIcon fontSize={18} />
|
||||
<span>Edit</span>
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
className="px-2 h-8 flex items-center gap-2"
|
||||
onClick={handleSharedLinks}
|
||||
>
|
||||
<LinkIcon fontSize={18} />
|
||||
<span>Shared links</span>
|
||||
</Button>
|
||||
</div>
|
||||
<DiscardChanges
|
||||
open={discardDialogOpen}
|
||||
onOpenChange={setDiscardDialogOpen}
|
||||
onClose={() => setDiscardDialogOpen(false)}
|
||||
onClose={() => {
|
||||
setDiscardDialogOpen(false);
|
||||
setPendingAction(null);
|
||||
}}
|
||||
onConfirm={handleDiscardConfirm}
|
||||
description="Changes to this workspace will not be saved."
|
||||
/>
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
import { Separator } from '@affine/admin/components/ui/separator';
|
||||
import { adminWorkspaceQuery } from '@affine/graphql';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { useMemo } from 'react';
|
||||
|
||||
import { useQuery } from '../../../use-query';
|
||||
import { RightPanelHeader } from '../../header';
|
||||
import type { WorkspaceSharedLink } from '../schema';
|
||||
|
||||
export function WorkspaceSharedLinksPanel({
|
||||
workspaceId,
|
||||
onClose,
|
||||
}: {
|
||||
workspaceId: string;
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const { data } = useQuery({
|
||||
query: adminWorkspaceQuery,
|
||||
variables: {
|
||||
id: workspaceId,
|
||||
memberSkip: 0,
|
||||
memberTake: 0,
|
||||
memberQuery: undefined,
|
||||
},
|
||||
});
|
||||
|
||||
const workspace = data?.adminWorkspace;
|
||||
|
||||
const sharedLinks = useMemo<WorkspaceSharedLink[]>(() => {
|
||||
const links = workspace?.sharedLinks ?? [];
|
||||
return [...links].sort((a, b) => {
|
||||
const aTime = a.publishedAt ? new Date(a.publishedAt).getTime() : 0;
|
||||
const bTime = b.publishedAt ? new Date(b.publishedAt).getTime() : 0;
|
||||
return bTime - aTime;
|
||||
});
|
||||
}, [workspace?.sharedLinks]);
|
||||
|
||||
if (!workspace) {
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<RightPanelHeader
|
||||
title="Shared Links"
|
||||
handleClose={onClose}
|
||||
handleConfirm={onClose}
|
||||
canSave={false}
|
||||
/>
|
||||
<div
|
||||
className="p-6 text-sm"
|
||||
style={{ color: cssVarV2('text/secondary') }}
|
||||
>
|
||||
Workspace not found.
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full">
|
||||
<RightPanelHeader
|
||||
title="Shared Links"
|
||||
handleClose={onClose}
|
||||
handleConfirm={onClose}
|
||||
canSave={false}
|
||||
/>
|
||||
<div className="p-4 flex flex-col gap-3 overflow-y-auto">
|
||||
{sharedLinks.length === 0 ? (
|
||||
<div
|
||||
className="text-sm"
|
||||
style={{ color: cssVarV2('text/secondary') }}
|
||||
>
|
||||
No shared links.
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex flex-col divide-y rounded-md border">
|
||||
{sharedLinks.map(link => (
|
||||
<SharedLinkItem key={link.docId} link={link} />
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SharedLinkItem({ link }: { link: WorkspaceSharedLink }) {
|
||||
const title = link.title || link.docId;
|
||||
const sharedDate = formatSharedDate(link.publishedAt);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-1 px-3 py-3">
|
||||
<div className="text-sm font-medium truncate">{title}</div>
|
||||
<div className="flex items-center gap-2 text-xs">
|
||||
<Separator className="h-3" orientation="vertical" />
|
||||
<span style={{ color: cssVarV2('text/secondary') }}>
|
||||
Shared on {sharedDate}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatSharedDate(publishedAt?: string | null) {
|
||||
if (!publishedAt) {
|
||||
return 'Unknown';
|
||||
}
|
||||
|
||||
const date = new Date(publishedAt);
|
||||
if (Number.isNaN(date.getTime())) {
|
||||
return 'Unknown';
|
||||
}
|
||||
return date.toISOString().slice(0, 10);
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export type WorkspaceDetail = NonNullable<
|
||||
AdminWorkspaceQuery['adminWorkspace']
|
||||
>;
|
||||
export type WorkspaceMember = WorkspaceDetail['members'][0];
|
||||
export type WorkspaceSharedLink = WorkspaceDetail['sharedLinks'][0];
|
||||
|
||||
export type WorkspaceUpdateInput =
|
||||
AdminUpdateWorkspaceMutation['adminUpdateWorkspace'];
|
||||
|
||||
Reference in New Issue
Block a user