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:
@@ -0,0 +1,12 @@
|
|||||||
|
-- Add published_at to workspace_pages to track when a doc was shared
|
||||||
|
ALTER TABLE "workspace_pages"
|
||||||
|
ADD COLUMN IF NOT EXISTS "published_at" TIMESTAMPTZ(3);
|
||||||
|
|
||||||
|
-- Backfill existing public docs using the snapshot updated_at
|
||||||
|
UPDATE "workspace_pages" wp
|
||||||
|
SET "published_at" = s."updated_at"
|
||||||
|
FROM "snapshots" s
|
||||||
|
WHERE wp."workspace_id" = s."workspace_id"
|
||||||
|
AND wp."page_id" = s."guid"
|
||||||
|
AND wp."public" = TRUE
|
||||||
|
AND wp."published_at" IS NULL;
|
||||||
@@ -158,6 +158,7 @@ model WorkspaceDoc {
|
|||||||
blocked Boolean @default(false)
|
blocked Boolean @default(false)
|
||||||
title String? @db.VarChar
|
title String? @db.VarChar
|
||||||
summary String? @db.VarChar
|
summary String? @db.VarChar
|
||||||
|
publishedAt DateTime? @map("published_at") @db.Timestamptz(3)
|
||||||
|
|
||||||
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
|
||||||
|
|
||||||
|
|||||||
@@ -79,6 +79,18 @@ class AdminWorkspaceMember {
|
|||||||
status!: WorkspaceMemberStatus;
|
status!: WorkspaceMemberStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
class AdminWorkspaceSharedLink {
|
||||||
|
@Field()
|
||||||
|
docId!: string;
|
||||||
|
|
||||||
|
@Field(() => String, { nullable: true })
|
||||||
|
title?: string | null;
|
||||||
|
|
||||||
|
@Field(() => Date, { nullable: true })
|
||||||
|
publishedAt?: Date | null;
|
||||||
|
}
|
||||||
|
|
||||||
@ObjectType()
|
@ObjectType()
|
||||||
export class AdminWorkspace {
|
export class AdminWorkspace {
|
||||||
@Field()
|
@Field()
|
||||||
@@ -128,6 +140,9 @@ export class AdminWorkspace {
|
|||||||
|
|
||||||
@Field(() => SafeIntResolver)
|
@Field(() => SafeIntResolver)
|
||||||
blobSize!: number;
|
blobSize!: number;
|
||||||
|
|
||||||
|
@Field(() => [AdminWorkspaceSharedLink])
|
||||||
|
sharedLinks!: AdminWorkspaceSharedLink[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@InputType()
|
@InputType()
|
||||||
@@ -250,6 +265,18 @@ export class AdminWorkspaceResolver {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ResolveField(() => [AdminWorkspaceSharedLink], {
|
||||||
|
description: 'Shared links of workspace',
|
||||||
|
})
|
||||||
|
async sharedLinks(@Parent() workspace: AdminWorkspace) {
|
||||||
|
const publicDocs = await this.models.doc.findPublics(workspace.id, 'desc');
|
||||||
|
return publicDocs.map(doc => ({
|
||||||
|
docId: doc.docId,
|
||||||
|
title: doc.title,
|
||||||
|
publishedAt: doc.publishedAt ?? null,
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
@Mutation(() => AdminWorkspace, {
|
@Mutation(() => AdminWorkspace, {
|
||||||
description: 'Update workspace flags and features for admin',
|
description: 'Update workspace flags and features for admin',
|
||||||
nullable: true,
|
nullable: true,
|
||||||
|
|||||||
@@ -230,14 +230,6 @@ export class WorkspaceDocResolver {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ResolveField(() => [DocType], {
|
|
||||||
complexity: 2,
|
|
||||||
deprecationReason: 'use [WorkspaceType.publicDocs] instead',
|
|
||||||
})
|
|
||||||
async publicPages(@Parent() workspace: WorkspaceType) {
|
|
||||||
return this.publicDocs(workspace);
|
|
||||||
}
|
|
||||||
|
|
||||||
@ResolveField(() => [DocType], {
|
@ResolveField(() => [DocType], {
|
||||||
description: 'Get public docs of a workspace',
|
description: 'Get public docs of a workspace',
|
||||||
complexity: 2,
|
complexity: 2,
|
||||||
|
|||||||
@@ -484,12 +484,10 @@ export class DocModel extends BaseModel {
|
|||||||
/**
|
/**
|
||||||
* Find the workspace public doc metas.
|
* Find the workspace public doc metas.
|
||||||
*/
|
*/
|
||||||
async findPublics(workspaceId: string) {
|
async findPublics(workspaceId: string, order: 'asc' | 'desc' = 'asc') {
|
||||||
return await this.db.workspaceDoc.findMany({
|
return await this.db.workspaceDoc.findMany({
|
||||||
where: {
|
where: { workspaceId, public: true },
|
||||||
workspaceId,
|
orderBy: { publishedAt: order },
|
||||||
public: true,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -524,6 +522,7 @@ export class DocModel extends BaseModel {
|
|||||||
return await this.upsertMeta(workspaceId, docId, {
|
return await this.upsertMeta(workspaceId, docId, {
|
||||||
public: true,
|
public: true,
|
||||||
mode,
|
mode,
|
||||||
|
publishedAt: new Date(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,6 +535,7 @@ export class DocModel extends BaseModel {
|
|||||||
|
|
||||||
return await this.upsertMeta(workspaceId, docId, {
|
return await this.upsertMeta(workspaceId, docId, {
|
||||||
public: false,
|
public: false,
|
||||||
|
publishedAt: null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ type AdminWorkspace {
|
|||||||
owner: WorkspaceUserType
|
owner: WorkspaceUserType
|
||||||
public: Boolean!
|
public: Boolean!
|
||||||
publicPageCount: Int!
|
publicPageCount: Int!
|
||||||
|
sharedLinks: [AdminWorkspaceSharedLink!]!
|
||||||
snapshotCount: Int!
|
snapshotCount: Int!
|
||||||
snapshotSize: SafeInt!
|
snapshotSize: SafeInt!
|
||||||
}
|
}
|
||||||
@@ -73,6 +74,12 @@ type AdminWorkspaceMember {
|
|||||||
status: WorkspaceMemberStatus!
|
status: WorkspaceMemberStatus!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AdminWorkspaceSharedLink {
|
||||||
|
docId: String!
|
||||||
|
publishedAt: DateTime
|
||||||
|
title: String
|
||||||
|
}
|
||||||
|
|
||||||
enum AdminWorkspaceSort {
|
enum AdminWorkspaceSort {
|
||||||
BlobCount
|
BlobCount
|
||||||
BlobSize
|
BlobSize
|
||||||
@@ -2466,7 +2473,6 @@ type WorkspaceType {
|
|||||||
|
|
||||||
"""Get public page of a workspace by page id."""
|
"""Get public page of a workspace by page id."""
|
||||||
publicPage(pageId: String!): DocType @deprecated(reason: "use [WorkspaceType.doc] instead")
|
publicPage(pageId: String!): DocType @deprecated(reason: "use [WorkspaceType.doc] instead")
|
||||||
publicPages: [DocType!]! @deprecated(reason: "use [WorkspaceType.publicDocs] instead")
|
|
||||||
|
|
||||||
"""quota of workspace"""
|
"""quota of workspace"""
|
||||||
quota: WorkspaceQuotaType!
|
quota: WorkspaceQuotaType!
|
||||||
|
|||||||
@@ -26,6 +26,11 @@ query adminWorkspace(
|
|||||||
snapshotSize
|
snapshotSize
|
||||||
blobCount
|
blobCount
|
||||||
blobSize
|
blobSize
|
||||||
|
sharedLinks {
|
||||||
|
docId
|
||||||
|
title
|
||||||
|
publishedAt
|
||||||
|
}
|
||||||
members(skip: $memberSkip, take: $memberTake, query: $memberQuery) {
|
members(skip: $memberSkip, take: $memberTake, query: $memberQuery) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
|||||||
@@ -190,6 +190,11 @@ export const adminWorkspaceQuery = {
|
|||||||
snapshotSize
|
snapshotSize
|
||||||
blobCount
|
blobCount
|
||||||
blobSize
|
blobSize
|
||||||
|
sharedLinks {
|
||||||
|
docId
|
||||||
|
title
|
||||||
|
publishedAt
|
||||||
|
}
|
||||||
members(skip: $memberSkip, take: $memberTake, query: $memberQuery) {
|
members(skip: $memberSkip, take: $memberTake, query: $memberQuery) {
|
||||||
id
|
id
|
||||||
name
|
name
|
||||||
|
|||||||
@@ -96,6 +96,7 @@ export interface AdminWorkspace {
|
|||||||
owner: Maybe<WorkspaceUserType>;
|
owner: Maybe<WorkspaceUserType>;
|
||||||
public: Scalars['Boolean']['output'];
|
public: Scalars['Boolean']['output'];
|
||||||
publicPageCount: Scalars['Int']['output'];
|
publicPageCount: Scalars['Int']['output'];
|
||||||
|
sharedLinks: Array<AdminWorkspaceSharedLink>;
|
||||||
snapshotCount: Scalars['Int']['output'];
|
snapshotCount: Scalars['Int']['output'];
|
||||||
snapshotSize: Scalars['SafeInt']['output'];
|
snapshotSize: Scalars['SafeInt']['output'];
|
||||||
}
|
}
|
||||||
@@ -116,6 +117,13 @@ export interface AdminWorkspaceMember {
|
|||||||
status: WorkspaceMemberStatus;
|
status: WorkspaceMemberStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface AdminWorkspaceSharedLink {
|
||||||
|
__typename?: 'AdminWorkspaceSharedLink';
|
||||||
|
docId: Scalars['String']['output'];
|
||||||
|
publishedAt: Maybe<Scalars['DateTime']['output']>;
|
||||||
|
title: Maybe<Scalars['String']['output']>;
|
||||||
|
}
|
||||||
|
|
||||||
export enum AdminWorkspaceSort {
|
export enum AdminWorkspaceSort {
|
||||||
BlobCount = 'BlobCount',
|
BlobCount = 'BlobCount',
|
||||||
BlobSize = 'BlobSize',
|
BlobSize = 'BlobSize',
|
||||||
@@ -3142,8 +3150,6 @@ export interface WorkspaceType {
|
|||||||
* @deprecated use [WorkspaceType.doc] instead
|
* @deprecated use [WorkspaceType.doc] instead
|
||||||
*/
|
*/
|
||||||
publicPage: Maybe<DocType>;
|
publicPage: Maybe<DocType>;
|
||||||
/** @deprecated use [WorkspaceType.publicDocs] instead */
|
|
||||||
publicPages: Array<DocType>;
|
|
||||||
/** quota of workspace */
|
/** quota of workspace */
|
||||||
quota: WorkspaceQuotaType;
|
quota: WorkspaceQuotaType;
|
||||||
/** Get recently updated docs of a workspace */
|
/** Get recently updated docs of a workspace */
|
||||||
@@ -3378,6 +3384,12 @@ export type AdminWorkspaceQuery = {
|
|||||||
email: string;
|
email: string;
|
||||||
avatarUrl: string | null;
|
avatarUrl: string | null;
|
||||||
} | null;
|
} | null;
|
||||||
|
sharedLinks: Array<{
|
||||||
|
__typename?: 'AdminWorkspaceSharedLink';
|
||||||
|
docId: string;
|
||||||
|
title: string | null;
|
||||||
|
publishedAt: string | null;
|
||||||
|
}>;
|
||||||
members: Array<{
|
members: Array<{
|
||||||
__typename?: 'AdminWorkspaceMember';
|
__typename?: 'AdminWorkspaceMember';
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ export const useColumns = () => {
|
|||||||
{
|
{
|
||||||
id: 'actions',
|
id: 'actions',
|
||||||
meta: {
|
meta: {
|
||||||
className: 'w-[80px] justify-end',
|
className: 'w-[190px] justify-end',
|
||||||
},
|
},
|
||||||
header: () => (
|
header: () => (
|
||||||
<div className="text-xs font-medium text-right">Actions</div>
|
<div className="text-xs font-medium text-right">Actions</div>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import { Button } from '@affine/admin/components/ui/button';
|
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 { useCallback, useState } from 'react';
|
||||||
|
|
||||||
import { DiscardChanges } from '../../../components/shared/discard-changes';
|
import { DiscardChanges } from '../../../components/shared/discard-changes';
|
||||||
import { useRightPanel } from '../../panel/context';
|
import { useRightPanel } from '../../panel/context';
|
||||||
import type { WorkspaceListItem } from '../schema';
|
import type { WorkspaceListItem } from '../schema';
|
||||||
import { WorkspacePanel } from './workspace-panel';
|
import { WorkspacePanel } from './workspace-panel';
|
||||||
|
import { WorkspaceSharedLinksPanel } from './workspace-shared-links-panel';
|
||||||
|
|
||||||
export function DataTableRowActions({
|
export function DataTableRowActions({
|
||||||
workspace,
|
workspace,
|
||||||
@@ -13,6 +14,9 @@ export function DataTableRowActions({
|
|||||||
workspace: WorkspaceListItem;
|
workspace: WorkspaceListItem;
|
||||||
}) {
|
}) {
|
||||||
const [discardDialogOpen, setDiscardDialogOpen] = useState(false);
|
const [discardDialogOpen, setDiscardDialogOpen] = useState(false);
|
||||||
|
const [pendingAction, setPendingAction] = useState<
|
||||||
|
'edit' | 'sharedLinks' | null
|
||||||
|
>(null);
|
||||||
const {
|
const {
|
||||||
setPanelContent,
|
setPanelContent,
|
||||||
openPanel,
|
openPanel,
|
||||||
@@ -22,7 +26,7 @@ export function DataTableRowActions({
|
|||||||
setHasDirtyChanges,
|
setHasDirtyChanges,
|
||||||
} = useRightPanel();
|
} = useRightPanel();
|
||||||
|
|
||||||
const handleConfirm = useCallback(() => {
|
const openWorkspacePanel = useCallback(() => {
|
||||||
setHasDirtyChanges(false);
|
setHasDirtyChanges(false);
|
||||||
setPanelContent(
|
setPanelContent(
|
||||||
<WorkspacePanel workspaceId={workspace.id} onClose={closePanel} />
|
<WorkspacePanel workspaceId={workspace.id} onClose={closePanel} />
|
||||||
@@ -39,35 +43,89 @@ export function DataTableRowActions({
|
|||||||
workspace.id,
|
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(() => {
|
const handleEdit = useCallback(() => {
|
||||||
if (hasDirtyChanges) {
|
if (hasDirtyChanges) {
|
||||||
|
setPendingAction('edit');
|
||||||
setDiscardDialogOpen(true);
|
setDiscardDialogOpen(true);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleConfirm();
|
openWorkspacePanel();
|
||||||
}, [handleConfirm, hasDirtyChanges]);
|
}, [hasDirtyChanges, openWorkspacePanel]);
|
||||||
|
|
||||||
|
const handleSharedLinks = useCallback(() => {
|
||||||
|
if (hasDirtyChanges) {
|
||||||
|
setPendingAction('sharedLinks');
|
||||||
|
setDiscardDialogOpen(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
openSharedLinksPanel();
|
||||||
|
}, [hasDirtyChanges, openSharedLinksPanel]);
|
||||||
|
|
||||||
const handleDiscardConfirm = useCallback(() => {
|
const handleDiscardConfirm = useCallback(() => {
|
||||||
setDiscardDialogOpen(false);
|
setDiscardDialogOpen(false);
|
||||||
setHasDirtyChanges(false);
|
setHasDirtyChanges(false);
|
||||||
handleConfirm();
|
if (pendingAction === 'sharedLinks') {
|
||||||
}, [handleConfirm, setHasDirtyChanges]);
|
openSharedLinksPanel();
|
||||||
|
} else {
|
||||||
|
openWorkspacePanel();
|
||||||
|
}
|
||||||
|
setPendingAction(null);
|
||||||
|
}, [
|
||||||
|
openSharedLinksPanel,
|
||||||
|
openWorkspacePanel,
|
||||||
|
pendingAction,
|
||||||
|
setHasDirtyChanges,
|
||||||
|
]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Button
|
<div className="flex gap-2">
|
||||||
variant="ghost"
|
<Button
|
||||||
size="sm"
|
variant="ghost"
|
||||||
className="px-2 h-8 flex items-center gap-2"
|
size="sm"
|
||||||
onClick={handleEdit}
|
className="px-2 h-8 flex items-center gap-2"
|
||||||
>
|
onClick={handleEdit}
|
||||||
<EditIcon fontSize={18} />
|
>
|
||||||
<span>Edit</span>
|
<EditIcon fontSize={18} />
|
||||||
</Button>
|
<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
|
<DiscardChanges
|
||||||
open={discardDialogOpen}
|
open={discardDialogOpen}
|
||||||
onOpenChange={setDiscardDialogOpen}
|
onOpenChange={setDiscardDialogOpen}
|
||||||
onClose={() => setDiscardDialogOpen(false)}
|
onClose={() => {
|
||||||
|
setDiscardDialogOpen(false);
|
||||||
|
setPendingAction(null);
|
||||||
|
}}
|
||||||
onConfirm={handleDiscardConfirm}
|
onConfirm={handleDiscardConfirm}
|
||||||
description="Changes to this workspace will not be saved."
|
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']
|
AdminWorkspaceQuery['adminWorkspace']
|
||||||
>;
|
>;
|
||||||
export type WorkspaceMember = WorkspaceDetail['members'][0];
|
export type WorkspaceMember = WorkspaceDetail['members'][0];
|
||||||
|
export type WorkspaceSharedLink = WorkspaceDetail['sharedLinks'][0];
|
||||||
|
|
||||||
export type WorkspaceUpdateInput =
|
export type WorkspaceUpdateInput =
|
||||||
AdminUpdateWorkspaceMutation['adminUpdateWorkspace'];
|
AdminUpdateWorkspaceMutation['adminUpdateWorkspace'];
|
||||||
|
|||||||
Reference in New Issue
Block a user