File Explorer
Compose a local file manager from Files, Toolbar, context menus, upload, breadcrumbs, and details.
This example shows the consumer-owned shape for a full file manager. Cubitt provides Files selection, inline rename, upload input wiring, details display, and primitive menus. The app owns the directory model, navigation, persistence, permissions, action labels, and the behavior behind each menu item.
import { useMemo, useState } from "react";
import {
Files,
type FilesItem,
type FilesMoveEvent,
type FilesUploadEntry,
type FilesViewMode,
useFilesContext,
} from "@tilt-legal/cubitt-components/files";
import { initialExplorerItems, ROOT_DIRECTORY_ID } from "./data";
import { FileExplorerDetailsPanel } from "./details-panel";
import { FileExplorerSurface } from "./file-surface";
import { FileExplorerToolbar } from "./toolbar";
import type {
ExplorerActionHandlers,
ExplorerItem,
ExplorerSource,
} from "./types";
import {
canMoveExplorerItems,
createFolderItem,
createUploadedItems,
downloadFakeFile,
getChildren,
getDirectoryDetailsItem,
getDirectoryPath,
getMoveDestinationDirectoryId,
getSelectedItems,
moveExplorerItems,
openFakeFile,
removeItemsAndDescendants,
} from "./utils";
function FileExplorerInner(props: {
currentDirectoryId: string;
detailsOpen: boolean;
items: ExplorerItem[];
onCurrentDirectoryChange: (directoryId: string) => void;
onDetailsOpenChange: (open: boolean) => void;
onItemsChange: (items: ExplorerItem[]) => void;
onSelectedIdsChange: (ids: string[]) => void;
onViewChange: (view: FilesViewMode) => void;
view: FilesViewMode;
}) {
const filesContext = useFilesContext();
const visibleItems = useMemo(
() => getChildren(props.items, props.currentDirectoryId),
[props.currentDirectoryId, props.items],
);
const selectedItems = useMemo(
() => getSelectedItems(props.items, filesContext.selectedIds),
[filesContext.selectedIds, props.items],
);
const currentPath = useMemo(
() => getDirectoryPath(props.items, props.currentDirectoryId),
[props.currentDirectoryId, props.items],
);
const detailsItems =
selectedItems.length > 0
? selectedItems
: [getDirectoryDetailsItem(props.items, props.currentDirectoryId)];
function navigateToDirectory(directoryId: string) {
props.onCurrentDirectoryChange(directoryId);
props.onSelectedIdsChange([]);
}
function createFolder() {
const folder = createFolderItem(props.currentDirectoryId);
props.onItemsChange([...props.items, folder]);
props.onSelectedIdsChange([folder.id]);
filesContext.startRename(folder);
}
const actionHandlers: ExplorerActionHandlers = {
onArchive: (items) => {
props.onItemsChange(removeItemsAndDescendants(props.items, items));
props.onSelectedIdsChange([]);
},
onCreateFolder: createFolder,
onDownload: (items) => {
items.filter((item) => item.kind === "file").forEach(downloadFakeFile);
},
onOpen: (item) => {
if (item.kind === "folder") {
navigateToDirectory(item.id);
return;
}
openFakeFile(item);
},
onRename: filesContext.startRename,
onViewDetails: () => props.onDetailsOpenChange(true),
};
return (
<>
<div className="sticky top-0 z-20 bg-background pb-4">
<FileExplorerToolbar
actionHandlers={actionHandlers}
currentPath={currentPath}
itemCount={visibleItems.length}
items={props.items}
onFilesAccepted={(entries: FilesUploadEntry[]) => {
const uploadedItems = createUploadedItems(
entries,
props.currentDirectoryId,
);
props.onItemsChange([...props.items, ...uploadedItems]);
props.onSelectedIdsChange(uploadedItems.map((item) => item.id));
}}
onNavigate={navigateToDirectory}
onViewChange={props.onViewChange}
view={props.view}
/>
</div>
<div className="flex min-h-[calc(100%-4rem)]">
<Files.Content
className="min-h-[30rem]"
moveDestinationId={props.currentDirectoryId}
>
<FileExplorerSurface
actionHandlers={actionHandlers}
items={visibleItems}
view={props.view}
/>
</Files.Content>
<FileExplorerDetailsPanel
items={detailsItems}
onClose={() => props.onDetailsOpenChange(false)}
open={props.detailsOpen}
/>
</div>
</>
);
}
export function FilesExplorerConsumerExample() {
const [items, setItems] = useState<ExplorerItem[]>(initialExplorerItems);
const [currentDirectoryId, setCurrentDirectoryId] =
useState(ROOT_DIRECTORY_ID);
const [selectedIds, setSelectedIds] = useState<string[]>([]);
const [view, setView] = useState<FilesViewMode>("grid");
const [detailsOpen, setDetailsOpen] = useState(false);
function renameItem(item: FilesItem<ExplorerSource>, name: string) {
setItems((currentItems) =>
currentItems.map((currentItem) =>
currentItem.id === item.id ? { ...currentItem, name } : currentItem,
),
);
}
const currentItem = useMemo(
() => getDirectoryDetailsItem(items, currentDirectoryId),
[currentDirectoryId, items],
);
function moveItems(event: FilesMoveEvent<ExplorerSource>) {
const destinationId = getMoveDestinationDirectoryId(event);
setItems((currentItems) =>
moveExplorerItems(currentItems, event.items, destinationId),
);
setSelectedIds(event.items.map((item) => item.id));
}
return (
<Files
canMove={(event) => canMoveExplorerItems(items, event)}
canRename={(item) => item.id !== ROOT_DIRECTORY_ID}
className="h-[560px] w-full max-w-6xl overflow-y-auto"
detailsItem={currentItem}
items={items}
onMove={moveItems}
onRename={renameItem}
onSelectedIdsChange={setSelectedIds}
selectedIds={selectedIds}
selectionBehavior="replace"
selectionMode="multiple"
>
<FileExplorerInner
currentDirectoryId={currentDirectoryId}
detailsOpen={detailsOpen}
items={items}
onCurrentDirectoryChange={setCurrentDirectoryId}
onDetailsOpenChange={setDetailsOpen}
onItemsChange={setItems}
onSelectedIdsChange={setSelectedIds}
onViewChange={setView}
view={view}
/>
</Files>
);
}import { Fragment } from "react";
import {
BreadcrumbIcon,
BreadcrumbItem,
BreadcrumbLabel,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@tilt-legal/cubitt-components/breadcrumb";
import { Button } from "@tilt-legal/cubitt-components/button";
import {
Menu,
MenuContent,
MenuTrigger,
} from "@tilt-legal/cubitt-components/menu";
import {
ToggleGroup,
ToggleGroupItem,
} from "@tilt-legal/cubitt-components/toggle-group";
import { Toolbar } from "@tilt-legal/cubitt-components/toolbar";
import {
Dots,
Grid,
Menu as MenuIcon,
} from "@tilt-legal/cubitt-icons/ui/outline";
import {
Files,
type FilesUploadEntry,
type FilesViewMode,
useFilesContext,
} from "@tilt-legal/cubitt-components/files";
import { FileExplorerActionMenuItems } from "./file-actions";
import { getSelectedItems } from "./utils";
import type {
ExplorerActionHandlers,
ExplorerFolder,
ExplorerItem,
} from "./types";
export function FileExplorerToolbar({
actionHandlers,
currentPath,
itemCount,
items,
onFilesAccepted,
onNavigate,
onViewChange,
view,
}: {
actionHandlers: ExplorerActionHandlers;
currentPath: ExplorerFolder[];
itemCount: number;
items: ExplorerItem[];
onFilesAccepted: (entries: FilesUploadEntry[]) => void;
onNavigate: (directoryId: string) => void;
onViewChange: (view: FilesViewMode) => void;
view: FilesViewMode;
}) {
const filesContext = useFilesContext();
const selectedItems = getSelectedItems(items, filesContext.selectedIds);
return (
<Toolbar size="md">
<Toolbar.Heading className="min-w-0">
{currentPath.length === 1 ? (
<>
<Toolbar.Title>Files</Toolbar.Title>
<Toolbar.Count>{itemCount}</Toolbar.Count>
</>
) : (
<Toolbar.Breadcrumb className="@container w-full">
<BreadcrumbList>
<BreadcrumbItem className="shrink-0">
<BreadcrumbLink
onClick={() => onNavigate(currentPath[0]?.id ?? "root")}
render={<button type="button" />}
>
<BreadcrumbLabel className="shrink-0">Files</BreadcrumbLabel>
</BreadcrumbLink>
</BreadcrumbItem>
{currentPath.slice(1).map((directory, index, path) => {
const isCurrent = index === path.length - 1;
return (
<Fragment key={directory.id}>
<BreadcrumbSeparator>/</BreadcrumbSeparator>
<BreadcrumbItem
className={isCurrent ? "min-w-0" : "shrink-0"}
>
{isCurrent ? (
<BreadcrumbPage className="min-w-0">
<BreadcrumbIcon>
<Files.FolderIcon />
</BreadcrumbIcon>
<BreadcrumbLabel>{directory.name}</BreadcrumbLabel>
</BreadcrumbPage>
) : (
<BreadcrumbLink
onClick={() => onNavigate(directory.id)}
render={<button type="button" />}
>
<BreadcrumbIcon>
<Files.FolderIcon />
</BreadcrumbIcon>
<BreadcrumbLabel className="shrink-0">
{directory.name}
</BreadcrumbLabel>
</BreadcrumbLink>
)}
</BreadcrumbItem>
</Fragment>
);
})}
</BreadcrumbList>
</Toolbar.Breadcrumb>
)}
</Toolbar.Heading>
<Toolbar.Actions>
<Menu>
<Button
aria-label="File actions"
mode="icon"
render={<MenuTrigger />}
variant="secondary"
>
<Dots />
</Button>
<MenuContent align="end">
<FileExplorerActionMenuItems
handlers={actionHandlers}
kind="toolbar"
showCreateFolder
targetItems={selectedItems}
/>
</MenuContent>
</Menu>
<ToggleGroup
aria-label="View"
groupVariant="segmented"
multiple={false}
onValueChange={(nextValue) => {
if (nextValue === "grid" || nextValue === "list") {
onViewChange(nextValue);
}
}}
value={view}
>
<ToggleGroupItem aria-label="Grid view" mode="icon" value="grid">
<Grid />
</ToggleGroupItem>
<ToggleGroupItem aria-label="List view" mode="icon" value="list">
<MenuIcon />
</ToggleGroupItem>
</ToggleGroup>
<Toolbar.Separator />
<Files.UploadButton multiple onFilesAccepted={onFilesAccepted} />
</Toolbar.Actions>
</Toolbar>
);
}import {
Files,
type FilesViewMode,
useFilesContextMenu,
} from "@tilt-legal/cubitt-components/files";
import {
ContextMenu,
ContextMenuContent,
ContextMenuTrigger,
} from "@tilt-legal/cubitt-components/context-menu";
import { FileExplorerActionMenuItems } from "./file-actions";
import type { ExplorerActionHandlers, ExplorerItem } from "./types";
export function FileExplorerSurface({
actionHandlers,
items,
view,
}: {
actionHandlers: ExplorerActionHandlers;
items: ExplorerItem[];
view: FilesViewMode;
}) {
const contextMenu = useFilesContextMenu(items);
return (
<ContextMenu {...contextMenu.getRootProps()}>
<ContextMenuTrigger
render={(triggerProps) =>
view === "grid" ? (
<Files.Grid
className="min-h-full"
{...contextMenu.getTriggerProps(triggerProps)}
>
{items.map((item) => (
<Files.Item
item={item}
key={item.id}
onOpen={actionHandlers.onOpen}
{...contextMenu.getItemProps(item)}
/>
))}
</Files.Grid>
) : (
<Files.List
className="min-h-full"
{...contextMenu.getTriggerProps(triggerProps)}
>
{items.map((item) => (
<Files.Item
item={item}
key={item.id}
onOpen={actionHandlers.onOpen}
{...contextMenu.getItemProps(item)}
/>
))}
</Files.List>
)
}
/>
<ContextMenuContent>
<FileExplorerActionMenuItems
handlers={actionHandlers}
kind="context"
showCreateFolder={contextMenu.targetItems.length === 0}
targetItems={contextMenu.targetItems}
/>
</ContextMenuContent>
</ContextMenu>
);
}import type { ReactNode } from "react";
import {
CircleInfo,
Download4,
FolderPlus,
Pen2,
Trash,
} from "@tilt-legal/cubitt-icons/ui/outline";
import {
ContextMenuItem,
ContextMenuSeparator,
} from "@tilt-legal/cubitt-components/context-menu";
import {
MenuItem,
MenuSeparator,
} from "@tilt-legal/cubitt-components/menu";
import { formatCount } from "./utils";
import type {
ExplorerActionHandlers,
ExplorerActionMenuKind,
ExplorerItem,
} from "./types";
function ActionMenuItem({
children,
kind,
onClick,
variant,
}: {
children: ReactNode;
kind: ExplorerActionMenuKind;
onClick?: () => void;
variant?: "destructive";
}) {
if (kind === "context") {
return (
<ContextMenuItem onClick={onClick} variant={variant}>
{children}
</ContextMenuItem>
);
}
return (
<MenuItem onClick={onClick} variant={variant}>
{children}
</MenuItem>
);
}
function ActionMenuSeparator({ kind }: { kind: ExplorerActionMenuKind }) {
return kind === "context" ? <ContextMenuSeparator /> : <MenuSeparator />;
}
function CreateFolderMenuItem({
handlers,
kind,
}: {
handlers: ExplorerActionHandlers;
kind: ExplorerActionMenuKind;
}) {
return (
<ActionMenuItem kind={kind} onClick={handlers.onCreateFolder}>
<FolderPlus />
New folder
</ActionMenuItem>
);
}
function ViewDetailsMenuItem({
handlers,
kind,
}: {
handlers: ExplorerActionHandlers;
kind: ExplorerActionMenuKind;
}) {
return (
<ActionMenuItem kind={kind} onClick={handlers.onViewDetails}>
<CircleInfo />
View details
</ActionMenuItem>
);
}
export function FileExplorerActionMenuItems({
handlers,
kind,
showCreateFolder = false,
targetItems,
}: {
handlers: ExplorerActionHandlers;
kind: ExplorerActionMenuKind;
showCreateFolder?: boolean;
targetItems: ExplorerItem[];
}) {
if (targetItems.length === 0) {
return (
<>
<CreateFolderMenuItem handlers={handlers} kind={kind} />
<ViewDetailsMenuItem handlers={handlers} kind={kind} />
</>
);
}
return (
<>
{showCreateFolder ? (
<>
<CreateFolderMenuItem handlers={handlers} kind={kind} />
<ViewDetailsMenuItem handlers={handlers} kind={kind} />
<ActionMenuSeparator kind={kind} />
</>
) : null}
<TargetActionMenuItems
handlers={handlers}
kind={kind}
showViewDetails={!showCreateFolder}
targetItems={targetItems}
/>
</>
);
}import { Files } from "@tilt-legal/cubitt-components/files";
import { Button } from "@tilt-legal/cubitt-components/button";
import { Separator } from "@tilt-legal/cubitt-components/separator";
import { formatBytes } from "@tilt-legal/cubitt-components/utilities/formatters";
import { Xmark } from "@tilt-legal/cubitt-icons/ui/outline";
import type { ExplorerItem } from "./types";
import { formatCount } from "./utils";
export function FileExplorerDetailsPanel({
items,
onClose,
open,
}: {
items: ExplorerItem[];
onClose: () => void;
open: boolean;
}) {
const singleItem = items.length === 1 ? items[0] : null;
const ownerCount = new Set(
items
.map((item) => item.source?.owner)
.filter((owner): owner is string => Boolean(owner)),
).size;
const statusCount = new Set(
items
.map((item) => item.source?.status)
.filter(
(status): status is NonNullable<ExplorerItem["source"]>["status"] =>
Boolean(status),
),
).size;
const totalSizeLabel = items.every((item) => typeof item.size === "number")
? formatBytes(
items.reduce((totalSize, item) => totalSize + (item.size ?? 0), 0),
)
: null;
return (
<div
aria-hidden={!open}
className={
open
? "sticky top-16 h-[calc(560px-4rem)] w-[400px] shrink-0 overflow-hidden border-l border-border-3 opacity-100"
: "w-0 shrink-0 overflow-hidden opacity-0"
}
>
<Button
aria-label="Close details"
className="absolute top-1 right-1 z-10"
mode="icon"
onClick={onClose}
type="button"
variant="secondary"
>
<Xmark />
</Button>
<Files.Details className="h-full min-w-0 ps-6">
<Files.Details.Icon />
<Files.Details.Header>
<Files.Details.Heading>
<Files.Details.Name />
<Files.Details.Type />
</Files.Details.Heading>
<Files.Details.EditNameButton />
</Files.Details.Header>
<Files.Details.Panels>
<Files.Details.Panel label="Details" value="details">
<Separator />
<Files.Details.Properties>
{singleItem?.source?.owner ? (
<Files.Details.Property>
<Files.Details.Property.Label>
Owner
</Files.Details.Property.Label>
<Files.Details.Property.Value>
{singleItem.source.owner}
</Files.Details.Property.Value>
</Files.Details.Property>
) : null}
{singleItem?.source?.status ? (
<Files.Details.Property>
<Files.Details.Property.Label>
Status
</Files.Details.Property.Label>
<Files.Details.Property.Value>
{singleItem.source.status}
</Files.Details.Property.Value>
</Files.Details.Property>
) : null}
{singleItem?.source?.updatedLabel ? (
<Files.Details.Property>
<Files.Details.Property.Label>
Updated
</Files.Details.Property.Label>
<Files.Details.Property.Value>
{singleItem.source.updatedLabel}
</Files.Details.Property.Value>
</Files.Details.Property>
) : null}
{singleItem?.sizeLabel ? (
<Files.Details.Property>
<Files.Details.Property.Label>
Size
</Files.Details.Property.Label>
<Files.Details.Property.Value>
{singleItem.sizeLabel}
</Files.Details.Property.Value>
</Files.Details.Property>
) : null}
{items.length > 1 ? (
<Files.Details.Property>
<Files.Details.Property.Label>
Files selected
</Files.Details.Property.Label>
<Files.Details.Property.Value>
{items.length}
</Files.Details.Property.Value>
</Files.Details.Property>
) : null}
{items.length > 1 && totalSizeLabel ? (
<Files.Details.Property>
<Files.Details.Property.Label>
Total size
</Files.Details.Property.Label>
<Files.Details.Property.Value>
{totalSizeLabel}
</Files.Details.Property.Value>
</Files.Details.Property>
) : null}
{items.length > 1 ? (
<Files.Details.Property>
<Files.Details.Property.Label>
Owners
</Files.Details.Property.Label>
<Files.Details.Property.Value>
{formatCount(ownerCount, "owner")}
</Files.Details.Property.Value>
</Files.Details.Property>
) : null}
{items.length > 1 ? (
<Files.Details.Property>
<Files.Details.Property.Label>
Statuses
</Files.Details.Property.Label>
<Files.Details.Property.Value>
{formatCount(statusCount, "status")}
</Files.Details.Property.Value>
</Files.Details.Property>
) : null}
</Files.Details.Properties>
</Files.Details.Panel>
</Files.Details.Panels>
</Files.Details>
</div>
);
}import type { ExplorerFolder, ExplorerItem } from "./types";
export const ROOT_DIRECTORY_ID = "root";
export const rootDirectory: ExplorerFolder = {
id: ROOT_DIRECTORY_ID,
kind: "folder",
name: "Files",
description: "Workspace root",
source: {
owner: "Matter team",
parentId: null,
status: "Reviewed",
updatedLabel: "Today",
},
};
export const initialExplorerItems: ExplorerItem[] = [
{
id: "folder-discovery",
kind: "folder",
name: "Discovery",
description: "4 items",
source: {
owner: "Sarah Chen",
parentId: ROOT_DIRECTORY_ID,
status: "Draft",
updatedLabel: "Today",
},
},
{
id: "folder-depositions",
kind: "folder",
name: "Depositions",
description: "3 items",
source: {
owner: "Ava Johnson",
parentId: ROOT_DIRECTORY_ID,
status: "Reviewed",
updatedLabel: "Yesterday",
},
},
{
id: "financial-report",
kind: "file",
name: "Q4 Financial Report.pdf",
mediaType: "application/pdf",
size: 2_458_624,
sizeLabel: "2.4 MB",
description: "Sarah Chen",
source: {
owner: "Sarah Chen",
parentId: ROOT_DIRECTORY_ID,
status: "Reviewed",
updatedLabel: "2 days ago",
},
},
];import type {
FilesMoveEvent,
FilesUploadEntry,
} from "@tilt-legal/cubitt-components/files";
import { formatBytes } from "@tilt-legal/cubitt-components/utilities/formatters";
import { ROOT_DIRECTORY_ID, rootDirectory } from "./data";
import type { ExplorerFolder, ExplorerItem, ExplorerSource } from "./types";
export function getChildren(
items: ExplorerItem[],
directoryId: string,
): ExplorerItem[] {
return items
.filter((item) => item.source?.parentId === directoryId)
.sort((first, second) => {
if (first.kind !== second.kind) {
return first.kind === "folder" ? -1 : 1;
}
return first.name.localeCompare(second.name);
});
}
export function getSelectedItems(
items: ExplorerItem[],
selectedIds: string[],
): ExplorerItem[] {
const itemsById = new Map(items.map((item) => [item.id, item]));
return selectedIds
.map((id) => itemsById.get(id))
.filter((item): item is ExplorerItem => Boolean(item));
}
export function getDirectoryPath(
items: ExplorerItem[],
directoryId: string,
): ExplorerFolder[] {
const foldersById = new Map(
items
.filter((item): item is ExplorerFolder => item.kind === "folder")
.map((folder) => [folder.id, folder]),
);
const path: ExplorerFolder[] = [];
let currentDirectory =
directoryId === ROOT_DIRECTORY_ID
? rootDirectory
: (foldersById.get(directoryId) ?? rootDirectory);
while (currentDirectory.id !== ROOT_DIRECTORY_ID) {
path.unshift(currentDirectory);
const parentId = currentDirectory.source?.parentId ?? ROOT_DIRECTORY_ID;
currentDirectory =
parentId === ROOT_DIRECTORY_ID
? rootDirectory
: (foldersById.get(parentId) ?? rootDirectory);
}
return [rootDirectory, ...path];
}
export function createFolderItem(directoryId: string): ExplorerFolder {
return {
id: `folder-${globalThis.crypto.randomUUID()}`,
kind: "folder",
name: "Untitled folder",
description: "0 items",
source: {
owner: "You",
parentId: directoryId,
status: "Draft",
updatedLabel: "Just now",
},
};
}
export function createUploadedItems(
entries: FilesUploadEntry[],
directoryId: string,
): ExplorerItem[] {
return entries.map((entry) => ({
id: `file-${globalThis.crypto.randomUUID()}`,
kind: "file",
name: entry.name,
mediaType: entry.mediaType,
size: entry.size,
sizeLabel: formatBytes(entry.size),
description: "You",
source: {
owner: "You",
parentId: directoryId,
status: "Draft",
updatedLabel: "Just now",
},
}));
}
export function removeItemsAndDescendants(
items: ExplorerItem[],
removedItems: ExplorerItem[],
): ExplorerItem[] {
const removedIds = new Set(removedItems.map((item) => item.id));
let changed = true;
while (changed) {
changed = false;
for (const item of items) {
if (
!removedIds.has(item.id) &&
item.source?.parentId &&
removedIds.has(item.source.parentId)
) {
removedIds.add(item.id);
changed = true;
}
}
}
return items.filter((item) => !removedIds.has(item.id));
}
export function canMoveExplorerItems(
items: ExplorerItem[],
event: FilesMoveEvent<ExplorerSource>,
): boolean {
const destinationId = getMoveDestinationDirectoryId(event);
const movingIds = new Set(event.items.map((item) => item.id));
if (event.destination.type === "folder" && movingIds.has(destinationId)) {
return false;
}
if (event.items.every((item) => item.source?.parentId === destinationId)) {
return false;
}
return event.items.every((item) => {
if (item.kind !== "folder") {
return true;
}
return !isDirectoryDescendant(items, item.id, destinationId);
});
}
export function moveExplorerItems(
items: ExplorerItem[],
movedItems: ExplorerItem[],
destinationId: string,
): ExplorerItem[] {
const movedIds = new Set(movedItems.map((item) => item.id));
return items.map((item) =>
movedIds.has(item.id)
? {
...item,
source: {
owner: item.source?.owner ?? "You",
parentId: destinationId,
status: item.source?.status ?? "Draft",
updatedLabel: "Just now",
},
}
: item,
);
}
export function getMoveDestinationDirectoryId(
event: FilesMoveEvent<ExplorerSource>,
): string {
return event.destination.type === "folder"
? event.destination.item.id
: (event.destination.id ?? ROOT_DIRECTORY_ID);
}
function isDirectoryDescendant(
items: ExplorerItem[],
ancestorId: string,
directoryId: string,
): boolean {
const foldersById = new Map(
items
.filter((item): item is ExplorerFolder => item.kind === "folder")
.map((folder) => [folder.id, folder]),
);
let currentDirectoryId: string | null = directoryId;
while (currentDirectoryId && currentDirectoryId !== ROOT_DIRECTORY_ID) {
if (currentDirectoryId === ancestorId) {
return true;
}
currentDirectoryId =
foldersById.get(currentDirectoryId)?.source?.parentId ??
ROOT_DIRECTORY_ID;
}
return false;
}import type { FilesItem } from "@tilt-legal/cubitt-components/files";
export type ExplorerSource = {
owner: string;
parentId: string | null;
status: "Draft" | "Reviewed" | "Filed";
updatedLabel: string;
};
export type ExplorerItem = FilesItem<ExplorerSource>;
export type ExplorerFolder = Extract<ExplorerItem, { kind: "folder" }>;
export type ExplorerActionMenuKind = "context" | "toolbar";
export type ExplorerActionHandlers = {
onArchive: (items: ExplorerItem[]) => void;
onCreateFolder: () => void;
onDownload: (items: ExplorerItem[]) => void;
onOpen: (item: ExplorerItem) => void;
onRename: (item: ExplorerItem) => void;
onViewDetails: () => void;
};