feat: workspace level share settings (#14201)

fix #13698
This commit is contained in:
DarkSky
2026-01-03 01:13:27 +08:00
committed by GitHub
parent 60de882a30
commit 9a7f8e7d4d
36 changed files with 560 additions and 34 deletions
@@ -18,6 +18,7 @@ import {
} from '../../../components/ui/popover';
import { useDebouncedValue } from '../../../hooks/use-debounced-value';
import { useServerConfig } from '../../common';
import type { WorkspaceFlagFilter } from '../schema';
interface DataTableToolbarProps<TData> {
table?: Table<TData>;
@@ -25,19 +26,21 @@ interface DataTableToolbarProps<TData> {
onKeywordChange: (keyword: string) => void;
selectedFeatures: FeatureType[];
onFeaturesChange: (features: FeatureType[]) => void;
flags: WorkspaceFlagFilter;
onFlagsChange: (flags: WorkspaceFlagFilter) => void;
sort: AdminWorkspaceSort | undefined;
onSortChange: (sort: AdminWorkspaceSort | undefined) => void;
disabled?: boolean;
}
const sortOptions: { value: AdminWorkspaceSort; label: string }[] = [
{ value: AdminWorkspaceSort.SnapshotSize, label: 'Snapshot size' },
{ value: AdminWorkspaceSort.CreatedAt, label: 'Created time' },
{ value: AdminWorkspaceSort.BlobCount, label: 'Blob count' },
{ value: AdminWorkspaceSort.BlobSize, label: 'Blob size' },
{ value: AdminWorkspaceSort.SnapshotCount, label: 'Snapshot count' },
{ value: AdminWorkspaceSort.SnapshotSize, label: 'Snapshot size' },
{ value: AdminWorkspaceSort.MemberCount, label: 'Member count' },
{ value: AdminWorkspaceSort.PublicPageCount, label: 'Public pages' },
{ value: AdminWorkspaceSort.CreatedAt, label: 'Created time' },
];
export function DataTableToolbar<TData>({
@@ -45,6 +48,8 @@ export function DataTableToolbar<TData>({
onKeywordChange,
selectedFeatures,
onFeaturesChange,
flags,
onFlagsChange,
sort,
onSortChange,
disabled = false,
@@ -80,6 +85,35 @@ export function DataTableToolbar<TData>({
[sort]
);
const flagOptions: { key: keyof WorkspaceFlagFilter; label: string }[] = [
{ key: 'public', label: 'Public' },
{ key: 'enableSharing', label: 'Enable sharing' },
{ key: 'enableAi', label: 'Enable AI' },
{ key: 'enableUrlPreview', label: 'Enable URL preview' },
{ key: 'enableDocEmbedding', label: 'Enable doc embedding' },
];
const flagLabel = (value: boolean | undefined) => {
if (value === true) return 'On';
if (value === false) return 'Off';
return 'Any';
};
const handleFlagToggle = useCallback(
(key: keyof WorkspaceFlagFilter) => {
const current = flags[key];
const next =
current === undefined ? true : current === true ? false : undefined;
onFlagsChange({ ...flags, [key]: next });
},
[flags, onFlagsChange]
);
const hasFlagFilter = useMemo(
() => Object.values(flags).some(v => v !== undefined),
[flags]
);
return (
<div className="flex items-center justify-between gap-y-2 gap-x-4 flex-wrap">
<FeatureFilterPopover
@@ -119,6 +153,37 @@ export function DataTableToolbar<TData>({
</div>
</PopoverContent>
</Popover>
<Popover open={disabled ? false : undefined}>
<PopoverTrigger asChild>
<Button
variant={hasFlagFilter ? 'secondary' : 'outline'}
size="sm"
className="h-8 px-2 lg:px-3"
disabled={disabled}
>
Flags
</Button>
</PopoverTrigger>
<PopoverContent className="w-[260px] p-2">
<div className="flex flex-col gap-1">
{flagOptions.map(option => (
<Button
key={option.key}
variant="ghost"
className="justify-between"
size="sm"
disabled={disabled}
onClick={() => handleFlagToggle(option.key)}
>
<span>{option.label}</span>
<span className="text-xs text-muted-foreground">
{flagLabel(flags[option.key])}
</span>
</Button>
))}
</div>
</PopoverContent>
</Popover>
<div className="flex">
<Input
placeholder="Search Workspace / Owner"
@@ -3,6 +3,7 @@ import type { ColumnDef, PaginationState } from '@tanstack/react-table';
import type { Dispatch, SetStateAction } from 'react';
import { SharedDataTable } from '../../../components/shared/data-table';
import type { WorkspaceFlagFilter } from '../schema';
import { DataTableToolbar } from './data-table-toolbar';
interface DataTableProps<TData, TValue> {
@@ -14,6 +15,8 @@ interface DataTableProps<TData, TValue> {
onKeywordChange: (value: string) => void;
selectedFeatures: FeatureType[];
onFeaturesChange: (features: FeatureType[]) => void;
flags: WorkspaceFlagFilter;
onFlagsChange: Dispatch<SetStateAction<WorkspaceFlagFilter>>;
sort: AdminWorkspaceSort | undefined;
onSortChange: (sort: AdminWorkspaceSort | undefined) => void;
loading?: boolean;
@@ -34,6 +37,8 @@ export function DataTable<TData extends { id: string }, TValue>({
onKeywordChange,
selectedFeatures,
onFeaturesChange,
flags,
onFlagsChange,
sort,
onSortChange,
onPaginationChange,
@@ -46,7 +51,7 @@ export function DataTable<TData extends { id: string }, TValue>({
totalCount={workspacesCount}
pagination={pagination}
onPaginationChange={onPaginationChange}
resetFiltersDeps={[keyword, selectedFeatures, sort]}
resetFiltersDeps={[keyword, selectedFeatures, sort, flags]}
renderToolbar={table => (
<DataTableToolbar
table={table}
@@ -54,6 +59,8 @@ export function DataTable<TData extends { id: string }, TValue>({
onKeywordChange={onKeywordChange}
selectedFeatures={selectedFeatures}
onFeaturesChange={onFeaturesChange}
flags={flags}
onFlagsChange={onFlagsChange}
sort={sort}
onSortChange={onSortChange}
disabled={loading}
@@ -86,6 +86,7 @@ function WorkspacePanelContent({
flags: {
public: workspace.public,
enableAi: workspace.enableAi,
enableSharing: workspace.enableSharing,
enableUrlPreview: workspace.enableUrlPreview,
enableDocEmbedding: workspace.enableDocEmbedding,
name: workspace.name ?? '',
@@ -110,6 +111,7 @@ function WorkspacePanelContent({
return (
flags.public !== baseline.flags.public ||
flags.enableAi !== baseline.flags.enableAi ||
flags.enableSharing !== baseline.flags.enableSharing ||
flags.enableUrlPreview !== baseline.flags.enableUrlPreview ||
flags.enableDocEmbedding !== baseline.flags.enableDocEmbedding ||
flags.name !== baseline.flags.name ||
@@ -134,6 +136,7 @@ function WorkspacePanelContent({
id: workspace.id,
public: flags.public,
enableAi: flags.enableAi,
enableSharing: flags.enableSharing,
enableUrlPreview: flags.enableUrlPreview,
enableDocEmbedding: flags.enableDocEmbedding,
name: flags.name || null,
@@ -231,6 +234,15 @@ function WorkspacePanelContent({
}
/>
<Separator />
<FlagItem
label="Allow Workspace Sharing"
description="Allow pages in this workspace to be shared publicly"
checked={flags.enableSharing}
onCheckedChange={value =>
setFlags(prev => ({ ...prev, enableSharing: value }))
}
/>
<Separator />
<FlagItem
label="Enable Doc Embedding"
description="Allow document embedding for search"
@@ -4,11 +4,13 @@ import { useState } from 'react';
import { Header } from '../header';
import { useColumns } from './components/columns';
import { DataTable } from './components/data-table';
import type { WorkspaceFlagFilter } from './schema';
import { useWorkspaceList } from './use-workspace-list';
export function WorkspacePage() {
const [keyword, setKeyword] = useState('');
const [featureFilters, setFeatureFilters] = useState<FeatureType[]>([]);
const [flagFilters, setFlagFilters] = useState<WorkspaceFlagFilter>({});
const [sort, setSort] = useState<AdminWorkspaceSort | undefined>(
AdminWorkspaceSort.CreatedAt
);
@@ -18,6 +20,7 @@ export function WorkspacePage() {
keyword,
features: featureFilters,
orderBy: sort,
flags: flagFilters,
});
const columns = useColumns();
@@ -36,6 +39,8 @@ export function WorkspacePage() {
onKeywordChange={setKeyword}
selectedFeatures={featureFilters}
onFeaturesChange={setFeatureFilters}
flags={flagFilters}
onFlagsChange={setFlagFilters}
sort={sort}
onSortChange={setSort}
loading={loading}
@@ -16,3 +16,11 @@ export type WorkspaceUpdateInput =
AdminUpdateWorkspaceMutation['adminUpdateWorkspace'];
export type WorkspaceFeatureFilter = FeatureType[];
export type WorkspaceFlagFilter = {
public?: boolean;
enableAi?: boolean;
enableSharing?: boolean;
enableUrlPreview?: boolean;
enableDocEmbedding?: boolean;
};
@@ -7,10 +7,13 @@ import {
} from '@affine/graphql';
import { useEffect, useMemo, useState } from 'react';
import type { WorkspaceFlagFilter } from './schema';
export const useWorkspaceList = (filter?: {
keyword?: string;
features?: FeatureType[];
orderBy?: AdminWorkspaceSort;
flags?: WorkspaceFlagFilter;
}) => {
const [pagination, setPagination] = useState({
pageIndex: 0,
@@ -21,8 +24,10 @@ export const useWorkspaceList = (filter?: {
() =>
`${filter?.keyword ?? ''}-${[...(filter?.features ?? [])]
.sort()
.join(',')}-${filter?.orderBy ?? ''}`,
[filter?.features, filter?.keyword, filter?.orderBy]
.join(',')}-${filter?.orderBy ?? ''}-${JSON.stringify(
filter?.flags ?? {}
)}`,
[filter?.features, filter?.flags, filter?.keyword, filter?.orderBy]
);
useEffect(() => {
@@ -40,10 +45,20 @@ export const useWorkspaceList = (filter?: {
? filter.features
: undefined,
orderBy: filter?.orderBy,
public: filter?.flags?.public,
enableAi: filter?.flags?.enableAi,
enableSharing: filter?.flags?.enableSharing,
enableUrlPreview: filter?.flags?.enableUrlPreview,
enableDocEmbedding: filter?.flags?.enableDocEmbedding,
},
}),
[
filter?.features,
filter?.flags?.enableAi,
filter?.flags?.enableDocEmbedding,
filter?.flags?.enableSharing,
filter?.flags?.enableUrlPreview,
filter?.flags?.public,
filter?.keyword,
filter?.orderBy,
pagination.pageIndex,