mirror of
https://github.com/hanzoai/dataroom.git
synced 2026-10-30 18:41:07 +00:00
868 lines
26 KiB
TypeScript
868 lines
26 KiB
TypeScript
"use client";
|
|
|
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
|
|
|
import { useTeam } from "@/context/team-context";
|
|
import { ItemType, ViewerGroupAccessControls } from "@prisma/client";
|
|
import {
|
|
ColumnDef,
|
|
flexRender,
|
|
getCoreRowModel,
|
|
getExpandedRowModel,
|
|
useReactTable,
|
|
} from "@tanstack/react-table";
|
|
import {
|
|
ArrowDownToLineIcon,
|
|
ChevronDown,
|
|
ChevronRight,
|
|
EyeIcon,
|
|
EyeOffIcon,
|
|
File,
|
|
Folder,
|
|
HomeIcon,
|
|
} from "lucide-react";
|
|
import { toast } from "sonner";
|
|
import { useDebounce } from "use-debounce";
|
|
|
|
import { useFeatureFlags } from "@/lib/hooks/use-feature-flags";
|
|
import { useDataroomFoldersTree } from "@/lib/swr/use-dataroom";
|
|
import { cn } from "@/lib/utils";
|
|
import {
|
|
HIERARCHICAL_DISPLAY_STYLE,
|
|
getHierarchicalDisplayName,
|
|
} from "@/lib/utils/hierarchical-display";
|
|
|
|
import CloudDownloadOff from "@/components/shared/icons/cloud-download-off";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import {
|
|
Table,
|
|
TableBody,
|
|
TableCell,
|
|
TableHead,
|
|
TableHeader,
|
|
TableRow,
|
|
} from "@/components/ui/table";
|
|
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
|
|
|
|
const PermissionItemName = ({ item }: { item: FileOrFolder }) => {
|
|
const { isFeatureEnabled } = useFeatureFlags();
|
|
const isDataroomIndexEnabled = isFeatureEnabled("dataroomIndex");
|
|
|
|
const displayName = getHierarchicalDisplayName(
|
|
item.name,
|
|
item.hierarchicalIndex,
|
|
isDataroomIndexEnabled,
|
|
);
|
|
|
|
const isRoot = item.id === "__dataroom_root__";
|
|
|
|
return (
|
|
<div className="flex items-center text-foreground">
|
|
{isRoot ? (
|
|
<HomeIcon className="mr-2 h-5 w-5" />
|
|
) : item.itemType === ItemType.DATAROOM_FOLDER ? (
|
|
<Folder className="mr-2 h-5 w-5" />
|
|
) : (
|
|
<File className="mr-2 h-5 w-5" />
|
|
)}
|
|
<span className="truncate" style={HIERARCHICAL_DISPLAY_STYLE}>
|
|
{displayName}
|
|
</span>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
// Update the FileOrFolder type to include permissions
|
|
type FileOrFolder = {
|
|
id: string;
|
|
name: string;
|
|
hierarchicalIndex?: string | null;
|
|
subItems?: FileOrFolder[];
|
|
permissions: {
|
|
view: boolean;
|
|
download: boolean;
|
|
partialView?: boolean;
|
|
partialDownload?: boolean;
|
|
};
|
|
itemType: ItemType;
|
|
documentId?: string;
|
|
};
|
|
|
|
type ItemPermission = Record<
|
|
string,
|
|
{ view: boolean; download: boolean; itemType: ItemType }
|
|
>;
|
|
|
|
type ColumnExtra = {
|
|
updatePermissions: (id: string, newPermissions: string[]) => void;
|
|
};
|
|
|
|
const createColumns = (extra: ColumnExtra): ColumnDef<FileOrFolder>[] => [
|
|
{
|
|
accessorKey: "name",
|
|
header: "Name",
|
|
cell: ({ row }) => {
|
|
const isRoot = row.original.id === "__dataroom_root__";
|
|
return (
|
|
<div className="flex items-center text-foreground">
|
|
{isRoot ? (
|
|
<div className="h-6 w-6 shrink-0" />
|
|
) : row.getCanExpand() ? (
|
|
<Button
|
|
variant="ghost"
|
|
onClick={row.getToggleExpandedHandler()}
|
|
className="mr-1 h-6 w-6 shrink-0 p-0"
|
|
disabled={isRoot}
|
|
>
|
|
{row.getIsExpanded() ? (
|
|
<ChevronDown className="h-4 w-4" />
|
|
) : (
|
|
<ChevronRight className="h-4 w-4" />
|
|
)}
|
|
</Button>
|
|
) : (
|
|
<div className="mr-1 h-6 w-6 shrink-0" />
|
|
)}
|
|
<PermissionItemName item={row.original} />
|
|
</div>
|
|
);
|
|
},
|
|
},
|
|
{
|
|
id: "actions",
|
|
header: "Actions",
|
|
cell: ({ row }) => {
|
|
const item = row.original;
|
|
|
|
const handleValueChange = (value: string[]) => {
|
|
extra.updatePermissions(item.id, value);
|
|
};
|
|
|
|
return (
|
|
<ToggleGroup
|
|
type="multiple"
|
|
value={Object.entries(item.permissions)
|
|
.filter(([_, value]) => value)
|
|
.map(([key, _]) => key)}
|
|
onValueChange={handleValueChange}
|
|
>
|
|
<ToggleGroupItem
|
|
value="view"
|
|
aria-label="Toggle view"
|
|
size="sm"
|
|
className={cn(
|
|
"px-2 text-muted-foreground hover:ring-1 hover:ring-gray-400 data-[state=on]:bg-foreground data-[state=on]:text-background",
|
|
item.permissions.view
|
|
? item.permissions.partialView
|
|
? "data-[state=on]:bg-gray-400 data-[state=on]:text-background"
|
|
: "data-[state=on]:bg-foreground data-[state=on]:text-background"
|
|
: "",
|
|
)}
|
|
>
|
|
{item.permissions.view ||
|
|
(item.permissions.view && item.permissions.partialView) ? (
|
|
<EyeIcon className="h-5 w-5" />
|
|
) : (
|
|
<EyeOffIcon className="h-5 w-5" />
|
|
)}
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem
|
|
value="download"
|
|
aria-label="Toggle download"
|
|
size="sm"
|
|
className={cn(
|
|
"px-2 text-muted-foreground hover:ring-1 hover:ring-gray-400 data-[state=on]:bg-foreground data-[state=on]:text-background",
|
|
item.permissions.download
|
|
? item.permissions.partialDownload
|
|
? "data-[state=on]:bg-gray-400 data-[state=on]:text-background"
|
|
: "data-[state=on]:bg-foreground data-[state=on]:text-background"
|
|
: "",
|
|
)}
|
|
>
|
|
{item.permissions.download ||
|
|
(item.permissions.download && item.permissions.partialDownload) ? (
|
|
<ArrowDownToLineIcon className="h-5 w-5" />
|
|
) : (
|
|
<CloudDownloadOff className="h-5 w-5" />
|
|
)}
|
|
</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
);
|
|
},
|
|
},
|
|
];
|
|
|
|
// Build tree function to include permissions
|
|
const buildTree = (
|
|
items: any[],
|
|
permissions: ViewerGroupAccessControls[],
|
|
parentId: string | null = null,
|
|
): FileOrFolder[] => {
|
|
const getPermissions = (id: string) => {
|
|
const permission = permissions.find((p) => p.itemId === id);
|
|
|
|
// If we have permission data loaded, use it. Otherwise default to true for consistency.
|
|
const hasPermissionData = permissions.length > 0;
|
|
|
|
return {
|
|
view: permission ? permission.canView : hasPermissionData ? false : true,
|
|
download: permission ? permission.canDownload : false,
|
|
partialView: false,
|
|
partialDownload: false,
|
|
};
|
|
};
|
|
|
|
const result: FileOrFolder[] = [];
|
|
|
|
// Handle folders and their contents
|
|
items
|
|
.filter((item) => item.parentId === parentId && !item.document)
|
|
.forEach((folder) => {
|
|
const subItems = buildTree(items, permissions, folder.id);
|
|
|
|
// Add documents directly in this folder
|
|
const folderDocuments = (folder.documents || []).map((doc: any) => ({
|
|
id: doc.id,
|
|
documentId: doc.document.id,
|
|
name: doc.document.name,
|
|
hierarchicalIndex: doc.hierarchicalIndex,
|
|
permissions: getPermissions(doc.id),
|
|
itemType: ItemType.DATAROOM_DOCUMENT,
|
|
}));
|
|
|
|
const allSubItems = [...subItems, ...folderDocuments];
|
|
|
|
const folderPermissions = getPermissions(folder.id);
|
|
|
|
// Calculate view and partialView for folders
|
|
let viewStatus = folderPermissions.view;
|
|
let partialView = false;
|
|
let downloadStatus = folderPermissions.download;
|
|
let partialDownload = false;
|
|
|
|
if (allSubItems.length > 0) {
|
|
const viewableItems = allSubItems.filter(
|
|
(item) => item.permissions.view,
|
|
);
|
|
const downloadableItems = allSubItems.filter(
|
|
(item) => item.permissions.download,
|
|
);
|
|
|
|
viewStatus = viewableItems.length > 0;
|
|
partialView =
|
|
viewableItems.length > 0 && viewableItems.length < allSubItems.length;
|
|
downloadStatus = downloadableItems.length > 0;
|
|
partialDownload =
|
|
downloadableItems.length > 0 &&
|
|
downloadableItems.length < allSubItems.length;
|
|
}
|
|
|
|
result.push({
|
|
id: folder.id,
|
|
name: folder.name,
|
|
hierarchicalIndex: folder.hierarchicalIndex,
|
|
subItems: allSubItems,
|
|
permissions: {
|
|
view: viewStatus,
|
|
download: downloadStatus,
|
|
partialView,
|
|
partialDownload,
|
|
},
|
|
itemType: ItemType.DATAROOM_FOLDER,
|
|
});
|
|
});
|
|
|
|
// Handle documents at the current level (including root level)
|
|
items
|
|
.filter(
|
|
(item) =>
|
|
(item.parentId === parentId && item.document) ||
|
|
(parentId === null && item.folderId === null && item.document),
|
|
)
|
|
.forEach((doc) => {
|
|
result.push({
|
|
id: doc.id,
|
|
documentId: doc.document.id,
|
|
name: doc.document.name,
|
|
hierarchicalIndex: doc.hierarchicalIndex,
|
|
permissions: getPermissions(doc.id),
|
|
itemType: ItemType.DATAROOM_DOCUMENT,
|
|
});
|
|
});
|
|
|
|
return result;
|
|
};
|
|
|
|
// Build tree with virtual root folder
|
|
const buildTreeWithRoot = (
|
|
items: any[],
|
|
permissions: ViewerGroupAccessControls[],
|
|
dataroomName: string = "Dataroom Home",
|
|
): FileOrFolder[] => {
|
|
// Get all items (folders and root documents)
|
|
const allItems = buildTree(items, permissions, null);
|
|
|
|
// Calculate overall permissions for the virtual root
|
|
const calculateRootPermissions = (items: FileOrFolder[]) => {
|
|
const flattenItems = (items: FileOrFolder[]): FileOrFolder[] => {
|
|
return items.reduce((acc, item) => {
|
|
acc.push(item);
|
|
if (item.subItems) {
|
|
acc.push(...flattenItems(item.subItems));
|
|
}
|
|
return acc;
|
|
}, [] as FileOrFolder[]);
|
|
};
|
|
|
|
const allFlatItems = flattenItems(items);
|
|
const viewableItems = allFlatItems.filter((item) => item.permissions.view);
|
|
const downloadableItems = allFlatItems.filter(
|
|
(item) => item.permissions.download,
|
|
);
|
|
|
|
return {
|
|
view: viewableItems.length > 0,
|
|
download: downloadableItems.length > 0,
|
|
partialView:
|
|
viewableItems.length > 0 && viewableItems.length < allFlatItems.length,
|
|
partialDownload:
|
|
downloadableItems.length > 0 &&
|
|
downloadableItems.length < allFlatItems.length,
|
|
};
|
|
};
|
|
|
|
const rootPermissions = calculateRootPermissions(allItems);
|
|
|
|
return [
|
|
{
|
|
id: "__dataroom_root__",
|
|
name: dataroomName,
|
|
subItems: allItems,
|
|
permissions: rootPermissions,
|
|
itemType: ItemType.DATAROOM_FOLDER,
|
|
},
|
|
];
|
|
};
|
|
|
|
export default function ExpandableTable({
|
|
dataroomId,
|
|
groupId,
|
|
permissions,
|
|
}: {
|
|
dataroomId: string;
|
|
groupId: string;
|
|
permissions: ViewerGroupAccessControls[];
|
|
}) {
|
|
const teamInfo = useTeam();
|
|
const teamId = teamInfo?.currentTeam?.id;
|
|
const { folders, loading } = useDataroomFoldersTree({
|
|
dataroomId,
|
|
include_documents: true,
|
|
});
|
|
const [data, setData] = useState<FileOrFolder[]>([]);
|
|
const [pendingChanges, setPendingChanges] = useState<ItemPermission>({});
|
|
const [debouncedPendingChanges] = useDebounce(pendingChanges, 2000);
|
|
|
|
// Use ref to access current data without dependency
|
|
const dataRef = useRef<FileOrFolder[]>([]);
|
|
|
|
useEffect(() => {
|
|
dataRef.current = data;
|
|
}, [data]);
|
|
|
|
const updatePermissions = useCallback(
|
|
(id: string, newPermissions: string[]) => {
|
|
const isRoot = id === "__dataroom_root__";
|
|
|
|
const findItemAndParents = (
|
|
items: FileOrFolder[],
|
|
targetId: string,
|
|
parents: FileOrFolder[] = [],
|
|
): { item: FileOrFolder; parents: FileOrFolder[] } | null => {
|
|
for (const item of items) {
|
|
if (item.id === targetId) {
|
|
return { item, parents };
|
|
}
|
|
if (item.subItems) {
|
|
const result = findItemAndParents(item.subItems, targetId, [
|
|
...parents,
|
|
item,
|
|
]);
|
|
if (result) return result;
|
|
}
|
|
}
|
|
return null;
|
|
};
|
|
|
|
const result = findItemAndParents(dataRef.current, id);
|
|
if (!result) return;
|
|
|
|
const { item, parents } = result;
|
|
|
|
const updatedPermissions = {
|
|
view: newPermissions.includes("view"),
|
|
download: newPermissions.includes("download"),
|
|
partialView: newPermissions.includes("partialView"),
|
|
partialDownload: newPermissions.includes("partialDownload"),
|
|
};
|
|
|
|
// Special cases
|
|
if (!updatedPermissions.view && item.permissions.download) {
|
|
updatedPermissions.download = false;
|
|
} else if (updatedPermissions.download && !updatedPermissions.view) {
|
|
updatedPermissions.view = true;
|
|
}
|
|
|
|
if (updatedPermissions.partialDownload) {
|
|
updatedPermissions.download = true;
|
|
}
|
|
|
|
if (updatedPermissions.partialView) {
|
|
updatedPermissions.view = true;
|
|
}
|
|
|
|
// Handle root-level permissions (affects all items)
|
|
if (isRoot) {
|
|
setData((prevData) => {
|
|
const updateAllItems = (items: FileOrFolder[]): FileOrFolder[] => {
|
|
return items.map((currentItem) => {
|
|
if (currentItem.id === "__dataroom_root__") {
|
|
return {
|
|
...currentItem,
|
|
permissions: {
|
|
view: updatedPermissions.view,
|
|
download: updatedPermissions.download,
|
|
partialView: false,
|
|
partialDownload: false,
|
|
},
|
|
subItems: currentItem.subItems
|
|
? updateAllItems(currentItem.subItems)
|
|
: undefined,
|
|
};
|
|
}
|
|
|
|
const updatedItem = {
|
|
...currentItem,
|
|
permissions: {
|
|
view: updatedPermissions.view,
|
|
download: updatedPermissions.download,
|
|
partialView: false,
|
|
partialDownload: false,
|
|
},
|
|
subItems: currentItem.subItems
|
|
? updateAllItems(currentItem.subItems)
|
|
: undefined,
|
|
};
|
|
|
|
return updatedItem;
|
|
});
|
|
};
|
|
|
|
return updateAllItems(prevData);
|
|
});
|
|
|
|
// Collect changes for all items
|
|
const collectAllChanges = (items: FileOrFolder[]): ItemPermission => {
|
|
let changes: ItemPermission = {};
|
|
|
|
const processItems = (items: FileOrFolder[]) => {
|
|
items.forEach((item) => {
|
|
// Don't save the virtual __dataroom_root__ item to database
|
|
if (item.id !== "__dataroom_root__") {
|
|
changes[item.id] = {
|
|
view: updatedPermissions.view,
|
|
download: updatedPermissions.download,
|
|
itemType: item.itemType,
|
|
};
|
|
}
|
|
|
|
if (item.subItems) {
|
|
processItems(item.subItems);
|
|
}
|
|
});
|
|
};
|
|
|
|
processItems(items);
|
|
return changes;
|
|
};
|
|
|
|
const rootChanges = collectAllChanges(dataRef.current);
|
|
setPendingChanges((prev) => ({
|
|
...prev,
|
|
...rootChanges,
|
|
}));
|
|
|
|
return;
|
|
}
|
|
|
|
setData((prevData) => {
|
|
const updateItemInTree = (items: FileOrFolder[]): FileOrFolder[] => {
|
|
return items.map((currentItem) => {
|
|
if (currentItem.id === id) {
|
|
const updatedItem = {
|
|
...currentItem,
|
|
permissions: {
|
|
view: updatedPermissions.view,
|
|
download: updatedPermissions.download,
|
|
partialView: false,
|
|
partialDownload: false,
|
|
},
|
|
};
|
|
|
|
// If it's a folder, update all subitems
|
|
if (updatedItem.itemType === ItemType.DATAROOM_FOLDER) {
|
|
updatedItem.subItems = updateSubItems(
|
|
updatedItem.subItems || [],
|
|
updatedPermissions.view,
|
|
updatedPermissions.download,
|
|
);
|
|
}
|
|
|
|
return updatedItem;
|
|
}
|
|
|
|
// if the current item is a parent of the updated item, update the parent's permissions
|
|
if (parents.some((parent) => parent.id === currentItem.id)) {
|
|
const updatedSubItems = currentItem.subItems
|
|
? updateItemInTree(currentItem.subItems)
|
|
: [];
|
|
return updateParentPermissions(currentItem, updatedSubItems);
|
|
}
|
|
|
|
// if the current item has subitems, update the subitems
|
|
if (currentItem.subItems) {
|
|
return {
|
|
...currentItem,
|
|
subItems: updateItemInTree(currentItem.subItems),
|
|
};
|
|
}
|
|
return currentItem;
|
|
});
|
|
};
|
|
|
|
const updateSubItems = (
|
|
items: FileOrFolder[],
|
|
viewState: boolean,
|
|
downloadState: boolean,
|
|
): FileOrFolder[] => {
|
|
return items.map((item) => ({
|
|
...item,
|
|
permissions: {
|
|
...item.permissions,
|
|
view: viewState,
|
|
partialView: false,
|
|
partialDownload: false,
|
|
download: downloadState,
|
|
},
|
|
subItems: item.subItems
|
|
? updateSubItems(item.subItems, viewState, downloadState)
|
|
: undefined,
|
|
}));
|
|
};
|
|
|
|
const updateParentPermissions = (
|
|
parent: FileOrFolder,
|
|
subItems: FileOrFolder[],
|
|
): FileOrFolder => {
|
|
const isParentRoot = parent.id === "__dataroom_root__";
|
|
|
|
// For root folder, calculate based on all descendants
|
|
const calculatePermissions = (items: FileOrFolder[]) => {
|
|
const flattenItems = (items: FileOrFolder[]): FileOrFolder[] => {
|
|
return items.reduce((acc, item) => {
|
|
if (item.id !== "__dataroom_root__") {
|
|
acc.push(item);
|
|
}
|
|
if (item.subItems) {
|
|
acc.push(...flattenItems(item.subItems));
|
|
}
|
|
return acc;
|
|
}, [] as FileOrFolder[]);
|
|
};
|
|
|
|
const allItems = flattenItems(items);
|
|
const viewableItems = allItems.filter(
|
|
(item) => item.permissions.view,
|
|
);
|
|
const downloadableItems = allItems.filter(
|
|
(item) => item.permissions.download,
|
|
);
|
|
|
|
return {
|
|
view: viewableItems.length > 0,
|
|
partialView:
|
|
viewableItems.length > 0 &&
|
|
viewableItems.length < allItems.length,
|
|
download: downloadableItems.length > 0,
|
|
partialDownload:
|
|
downloadableItems.length > 0 &&
|
|
downloadableItems.length < allItems.length,
|
|
};
|
|
};
|
|
|
|
if (isParentRoot) {
|
|
const rootPermissions = calculatePermissions(subItems);
|
|
return {
|
|
...parent,
|
|
permissions: rootPermissions,
|
|
subItems,
|
|
};
|
|
}
|
|
|
|
// For regular folders
|
|
const someSubItemViewable = subItems.some(
|
|
(subItem) => subItem.permissions.view,
|
|
);
|
|
const allSubItemsViewable = subItems.every(
|
|
(subItem) => subItem.permissions.view,
|
|
);
|
|
const someSubItemDownloadable = subItems.some(
|
|
(subItem) => subItem.permissions.download,
|
|
);
|
|
const allSubItemsDownloadable = subItems.every(
|
|
(subItem) => subItem.permissions.download,
|
|
);
|
|
|
|
return {
|
|
...parent,
|
|
permissions: {
|
|
view: someSubItemViewable,
|
|
partialView: someSubItemViewable && !allSubItemsViewable,
|
|
download: someSubItemDownloadable,
|
|
partialDownload:
|
|
someSubItemDownloadable && !allSubItemsDownloadable,
|
|
},
|
|
subItems,
|
|
};
|
|
};
|
|
|
|
return updateItemInTree(prevData);
|
|
});
|
|
|
|
// Collect changes for database update
|
|
const collectChanges = (
|
|
item: FileOrFolder,
|
|
parents: FileOrFolder[],
|
|
): ItemPermission => {
|
|
let changes: ItemPermission = {};
|
|
|
|
// Don't save the virtual __dataroom_root__ item to database
|
|
if (item.id !== "__dataroom_root__") {
|
|
changes[item.id] = {
|
|
view: updatedPermissions.view,
|
|
download: updatedPermissions.download,
|
|
itemType: item.itemType,
|
|
};
|
|
}
|
|
|
|
// Collect changes for all subitems
|
|
const collectSubItemChanges = (
|
|
subItems: FileOrFolder[] | undefined,
|
|
) => {
|
|
if (!subItems) return;
|
|
subItems.forEach((subItem) => {
|
|
// Don't save the virtual __dataroom_root__ item to database
|
|
if (subItem.id !== "__dataroom_root__") {
|
|
changes[subItem.id] = {
|
|
view: updatedPermissions.view,
|
|
download: updatedPermissions.download,
|
|
itemType: subItem.itemType,
|
|
};
|
|
}
|
|
collectSubItemChanges(subItem.subItems);
|
|
});
|
|
};
|
|
|
|
collectSubItemChanges(item.subItems);
|
|
|
|
// Ensure all parent folders are viewable if the item is being set to viewable
|
|
if (updatedPermissions.view || updatedPermissions.download) {
|
|
parents.forEach((parent) => {
|
|
// Don't save the virtual __dataroom_root__ item to database
|
|
if (parent.id !== "__dataroom_root__") {
|
|
changes[parent.id] = {
|
|
view: true,
|
|
download: updatedPermissions.download
|
|
? true
|
|
: parent.permissions.download,
|
|
itemType: parent.itemType,
|
|
};
|
|
}
|
|
});
|
|
} else {
|
|
// If turning off view, recalculate parent permissions
|
|
[...parents].reverse().forEach((parent) => {
|
|
// Don't save the virtual __dataroom_root__ item to database
|
|
if (parent.id !== "__dataroom_root__") {
|
|
const otherChildren =
|
|
parent.subItems?.filter((subItem) => subItem.id !== item.id) ||
|
|
[];
|
|
const someSubItemViewable = otherChildren.some(
|
|
(subItem) => subItem.permissions.view,
|
|
);
|
|
const someSubItemDownloadable = otherChildren.some(
|
|
(subItem) => subItem.permissions.download,
|
|
);
|
|
|
|
changes[parent.id] = {
|
|
view: someSubItemViewable,
|
|
download: someSubItemDownloadable,
|
|
itemType: parent.itemType,
|
|
};
|
|
}
|
|
});
|
|
}
|
|
|
|
return changes;
|
|
};
|
|
|
|
setPendingChanges((prev) => ({
|
|
...prev,
|
|
...collectChanges(item, parents),
|
|
}));
|
|
},
|
|
[],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (folders && !loading) {
|
|
const treeData = buildTreeWithRoot(folders, permissions, "Dataroom Home");
|
|
setData(treeData);
|
|
}
|
|
}, [folders, loading, permissions]);
|
|
|
|
const saveChanges = useCallback(
|
|
async (changes: typeof pendingChanges) => {
|
|
try {
|
|
const response = await fetch(
|
|
`/api/teams/${teamId}/datarooms/${dataroomId}/groups/${groupId}/permissions`,
|
|
{
|
|
method: "POST",
|
|
headers: {
|
|
"Content-Type": "application/json",
|
|
},
|
|
body: JSON.stringify({
|
|
dataroomId,
|
|
groupId,
|
|
permissions: changes,
|
|
}),
|
|
},
|
|
);
|
|
|
|
if (!response.ok) {
|
|
throw new Error("Failed to save permissions");
|
|
}
|
|
|
|
toast.success("Permissions updated successfully.");
|
|
|
|
setPendingChanges({});
|
|
} catch (error) {
|
|
console.error("Error saving permissions:", error);
|
|
toast.error("Failed to update permissions", {
|
|
description: "Please try again.",
|
|
});
|
|
}
|
|
},
|
|
[dataroomId, groupId],
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (Object.keys(debouncedPendingChanges).length > 0) {
|
|
saveChanges(debouncedPendingChanges);
|
|
}
|
|
}, [debouncedPendingChanges, saveChanges]);
|
|
|
|
const columns = useMemo(
|
|
() => createColumns({ updatePermissions }),
|
|
[updatePermissions],
|
|
);
|
|
|
|
const table = useReactTable({
|
|
data,
|
|
columns,
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getExpandedRowModel: getExpandedRowModel(),
|
|
getSubRows: (row) => row.subItems,
|
|
initialState: {
|
|
expanded: {
|
|
"0": true, // Always expand the root folder (first row)
|
|
},
|
|
},
|
|
getRowCanExpand: (row) => {
|
|
// Root folder is always expanded and cannot be collapsed
|
|
if (row.original.id === "__dataroom_root__") {
|
|
return true;
|
|
}
|
|
return (row.subRows?.length ?? 0) > 0;
|
|
},
|
|
});
|
|
|
|
if (loading) return <div>Loading...</div>;
|
|
|
|
return (
|
|
<div className="rounded-md border">
|
|
<Table>
|
|
<TableHeader>
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
<TableRow key={headerGroup.id}>
|
|
{headerGroup.headers.map((header) => (
|
|
<TableHead
|
|
key={header.id}
|
|
className="py-2 first:w-12 last:text-right"
|
|
>
|
|
{header.isPlaceholder
|
|
? null
|
|
: flexRender(
|
|
header.column.columnDef.header,
|
|
header.getContext(),
|
|
)}
|
|
</TableHead>
|
|
))}
|
|
</TableRow>
|
|
))}
|
|
</TableHeader>
|
|
<TableBody>
|
|
{table.getRowModel().rows?.length ? (
|
|
table.getRowModel().rows.map((row) => {
|
|
const isRoot = row.original.id === "__dataroom_root__";
|
|
return (
|
|
<TableRow
|
|
key={row.id}
|
|
data-state={row.getIsSelected() && "selected"}
|
|
className={cn(isRoot && "bg-blue-50/50 dark:bg-blue-950/50")}
|
|
>
|
|
{row.getVisibleCells().map((cell, index) => (
|
|
<TableCell
|
|
key={cell.id}
|
|
style={
|
|
index === 0
|
|
? {
|
|
paddingLeft: `${row.depth * 1.25}rem`,
|
|
}
|
|
: undefined
|
|
}
|
|
className="py-2 last:flex last:justify-end"
|
|
>
|
|
{flexRender(
|
|
cell.column.columnDef.cell,
|
|
cell.getContext(),
|
|
)}
|
|
</TableCell>
|
|
))}
|
|
</TableRow>
|
|
);
|
|
})
|
|
) : (
|
|
<TableRow>
|
|
<TableCell colSpan={columns.length} className="h-24 text-center">
|
|
No results.
|
|
</TableCell>
|
|
</TableRow>
|
|
)}
|
|
</TableBody>
|
|
</Table>
|
|
</div>
|
|
);
|
|
}
|