# Collections Support for TrustGraph UI ## Overview This document specifies the implementation of collections support in the TrustGraph UI. Collections provide a way to organize and manage groups of documents with metadata including name, description, and tags. The feature adds collection management capabilities to the Library page through a tabbed interface. ## API Integration ### Backend API Summary The TrustGraph API provides three collection operations via the `collection-management` endpoint: #### 1. List Collections - **Operation**: `list-collections` - **Request**: ```json { "operation": "list-collections", "user": "username", "tag_filter": ["tag1", "tag2"] // optional } ``` - **Response**: Array of collection metadata objects - **Fields**: user, collection, name, description, tags, created_at, updated_at #### 2. Update Collection (also creates) - **Operation**: `update-collection` - **Request**: ```json { "operation": "update-collection", "user": "username", "collection": "collection-id", "name": "Display Name", // optional "description": "Description", // optional "tags": ["tag1", "tag2"] // optional } ``` - **Response**: Single collection metadata object in `collections` array - **Note**: Creates collection if it doesn't exist; updates if it does #### 3. Delete Collection - **Operation**: `delete-collection` - **Request**: ```json { "operation": "delete-collection", "user": "username", "collection": "collection-id" } ``` - **Response**: Empty object `{}` ### Collection Metadata Structure ```typescript interface CollectionMetadata { user: string; collection: string; // Collection ID (unique identifier) name: string; // Display name description: string; // Description text tags: string[]; // Array of tags created_at: string; // ISO timestamp updated_at: string; // ISO timestamp } ``` ## Implementation Plan ### Phase 1: Socket Layer Integration **File**: `src/api/trustgraph/trustgraph-socket.ts` Add collection management methods to the socket interface: ```typescript // Add to Socket interface export interface Socket { // ... existing methods ... // Collection management collectionManagement: () => CollectionManagement; } // New CollectionManagement interface export interface CollectionManagement { listCollections: ( user: string, tagFilter?: string[] ) => Promise; updateCollection: ( user: string, collection: string, name?: string, description?: string, tags?: string[] ) => Promise; deleteCollection: ( user: string, collection: string ) => Promise; } // Implementation in BaseApi class class BaseApi { // ... existing methods ... collectionManagement(): CollectionManagement { return { listCollections: async (user, tagFilter) => { const request = { operation: "list-collections", user, ...(tagFilter && { tag_filter: tagFilter }), }; const response = await this.request("collection-management", request); return response.collections || []; }, updateCollection: async (user, collection, name, description, tags) => { const request = { operation: "update-collection", user, collection, ...(name !== undefined && { name }), ...(description !== undefined && { description }), ...(tags !== undefined && { tags }), }; const response = await this.request("collection-management", request); return response.collections[0]; }, deleteCollection: async (user, collection) => { const request = { operation: "delete-collection", user, collection, }; await this.request("collection-management", request); }, }; } } ``` ### Phase 2: State Management Hook **File**: `src/state/collections.ts` (new file) Create a React Query-based state management hook following the library.ts pattern: ```typescript import { useQueryClient, useQuery, useMutation } from "@tanstack/react-query"; import { useSocket, useConnectionState } from "../api/trustgraph/socket"; import { useNotification } from "./notify"; import { useActivity } from "./activity"; import { useSettings } from "./settings"; export interface CollectionMetadata { user: string; collection: string; name: string; description: string; tags: string[]; created_at: string; updated_at: string; } export const useCollections = () => { const socket = useSocket(); const connectionState = useConnectionState(); const queryClient = useQueryClient(); const notify = useNotification(); const { settings } = useSettings(); const isSocketReady = connectionState?.status === "authenticated" || connectionState?.status === "unauthenticated"; // Query for fetching all collections const collectionsQuery = useQuery({ queryKey: ["collections", settings.user], enabled: isSocketReady && !!settings.user, queryFn: () => { return socket.collectionManagement().listCollections(settings.user); }, }); // Mutation for creating/updating a collection const updateCollectionMutation = useMutation({ mutationFn: ({ collection, name, description, tags, onSuccess }) => { return socket .collectionManagement() .updateCollection( settings.user, collection, name, description, tags ) .then(() => { if (onSuccess) onSuccess(); }); }, onError: (err) => { console.log("Error:", err); notify.error(err.toString()); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["collections"] }); notify.success("Collection saved successfully"); }, }); // Mutation for deleting collections const deleteCollectionsMutation = useMutation({ mutationFn: ({ collections, onSuccess }) => { return Promise.all( collections.map((collection) => socket .collectionManagement() .deleteCollection(settings.user, collection) ) ).then(() => { if (onSuccess) onSuccess(); }); }, onError: (err) => { console.log("Error:", err); notify.error(err.toString()); }, onSuccess: () => { queryClient.invalidateQueries({ queryKey: ["collections"] }); notify.success("Collections deleted successfully"); }, }); // Activity indicators useActivity(collectionsQuery.isLoading, "Loading collections"); useActivity(updateCollectionMutation.isPending, "Saving collection"); useActivity(deleteCollectionsMutation.isPending, "Deleting collections"); return { // Collection data and query state collections: collectionsQuery.data || [], isLoading: collectionsQuery.isLoading, isError: collectionsQuery.isError, error: collectionsQuery.error, // Update/create collection operations updateCollection: updateCollectionMutation.mutate, isUpdating: updateCollectionMutation.isPending, updateError: updateCollectionMutation.error, // Delete collection operations deleteCollections: deleteCollectionsMutation.mutate, isDeleting: deleteCollectionsMutation.isPending, deleteError: deleteCollectionsMutation.error, // Manual refetch refetch: collectionsQuery.refetch, }; }; ``` ### Phase 3: Data Model **File**: `src/model/collection-table.tsx` (new file) Define table columns for collections display: ```typescript import { createColumnHelper } from "@tanstack/react-table"; import { Tag } from "@chakra-ui/react"; import { Checkbox } from "../components/ui/checkbox"; import { selectionState } from "../components/common/SelectableTable"; import { CollectionMetadata } from "../state/collections"; export const columnHelper = createColumnHelper(); export const columns = [ // Selection column columnHelper.display({ id: "select", header: ({ table }) => ( ), cell: ({ row }) => ( ), }), // Collection ID column columnHelper.accessor("collection", { header: "Collection ID", cell: (info) => info.getValue(), }), // Name column columnHelper.accessor("name", { header: "Name", cell: (info) => info.getValue(), }), // Description column columnHelper.accessor("description", { header: "Description", cell: (info) => info.getValue(), }), // Tags column columnHelper.accessor("tags", { header: "Tags", cell: (info) => info.getValue()?.map((tag) => ( {tag} )), }), // Created column columnHelper.accessor("created_at", { header: "Created", cell: (info) => new Date(info.getValue()).toLocaleString(), }), // Updated column columnHelper.accessor("updated_at", { header: "Updated", cell: (info) => new Date(info.getValue()).toLocaleString(), }), ]; ``` ### Phase 4: UI Components #### 4.1 Collections Component **File**: `src/components/library/Collections.tsx` (new file) Main collections management component following the Documents.tsx pattern: ```typescript import React, { useState } from "react"; import { getCoreRowModel, useReactTable } from "@tanstack/react-table"; import { columns } from "../../model/collection-table"; import { useCollections } from "../../state/collections"; import { useNotification } from "../../state/notify"; import CollectionActions from "./CollectionActions"; import CollectionDialog from "./CollectionDialog"; import SelectableTable from "../common/SelectableTable"; import CollectionControls from "./CollectionControls"; const Collections = () => { const [dialogOpen, setDialogOpen] = useState(false); const [editingCollection, setEditingCollection] = useState(null); const notify = useNotification(); const collectionsState = useCollections(); const collections = collectionsState.collections || []; const table = useReactTable({ data: collections, columns: columns, getCoreRowModel: getCoreRowModel(), }); const selected = table.getSelectedRowModel().rows.map((x) => x.original.collection); const onCreateNew = () => { setEditingCollection(null); setDialogOpen(true); }; const onEdit = () => { if (selected.length !== 1) { notify.info("Please select exactly one collection to edit"); return; } const collection = collections.find(c => c.collection === selected[0]); setEditingCollection(collection); setDialogOpen(true); }; const onDelete = () => { collectionsState.deleteCollections({ collections: selected, onSuccess: () => { table.setRowSelection({}); }, }); }; const onSaveCollection = (collection, name, description, tags) => { collectionsState.updateCollection({ collection, name, description, tags, onSuccess: () => { setDialogOpen(false); table.setRowSelection({}); }, }); }; return ( <> ); }; export default Collections; ``` #### 4.2 Collection Actions Bar **File**: `src/components/library/CollectionActions.tsx` (new file) Action buttons for bulk operations on selected collections: ```typescript import React from "react"; import { HStack, Button, Text } from "@chakra-ui/react"; import { Edit, Trash2 } from "lucide-react"; interface CollectionActionsProps { selectedCount: number; onEdit: () => void; onDelete: () => void; } const CollectionActions = ({ selectedCount, onEdit, onDelete }: CollectionActionsProps) => { if (selectedCount === 0) return null; return ( {selectedCount} collection{selectedCount !== 1 ? "s" : ""} selected ); }; export default CollectionActions; ``` #### 4.3 Collection Dialog **File**: `src/components/library/CollectionDialog.tsx` (new file) Dialog for creating/editing collections: ```typescript import React, { useState, useEffect } from "react"; import { Dialog } from "@chakra-ui/react"; import { Portal } from "@chakra-ui/react"; import TextField from "../common/TextField"; import TextAreaField from "../common/TextAreaField"; import ChipInputField from "../common/ChipInputField"; import ProgressSubmitButton from "../common/ProgressSubmitButton"; interface CollectionDialogProps { open: boolean; onOpenChange: (open: boolean) => void; onSave: (collection: string, name: string, description: string, tags: string[]) => void; editingCollection?: any; } const CollectionDialog = ({ open, onOpenChange, onSave, editingCollection, }: CollectionDialogProps) => { const [collection, setCollection] = useState(""); const [name, setName] = useState(""); const [description, setDescription] = useState(""); const [tags, setTags] = useState([]); useEffect(() => { if (editingCollection) { setCollection(editingCollection.collection); setName(editingCollection.name); setDescription(editingCollection.description); setTags(editingCollection.tags || []); } else { setCollection(""); setName(""); setDescription(""); setTags([]); } }, [editingCollection, open]); const handleSubmit = () => { onSave(collection, name, description, tags); }; const isValid = collection.trim() !== "" && name.trim() !== ""; return ( onOpenChange(e.open)}> {editingCollection ? "Edit Collection" : "Create Collection"} {editingCollection ? "Update" : "Create"} ); }; export default CollectionDialog; ``` #### 4.4 Collection Controls **File**: `src/components/library/CollectionControls.tsx` (new file) Control buttons for collection operations: ```typescript import React from "react"; import { HStack, Button } from "@chakra-ui/react"; import { Plus } from "lucide-react"; interface CollectionControlsProps { onCreate: () => void; } const CollectionControls = ({ onCreate }: CollectionControlsProps) => { return ( ); }; export default CollectionControls; ``` ### Phase 5: Update Library Page with Tabs **File**: `src/pages/LibraryPage.tsx` Update the library page to use tabs for Documents and Collections: ```typescript import React from "react"; import { LibraryBig } from "lucide-react"; import { Tabs } from "@chakra-ui/react"; import PageHeader from "../components/common/PageHeader"; import Documents from "../components/library/Documents"; import Collections from "../components/library/Collections"; const LibraryPage = () => { return ( <> } title="Library" description="Managing documents and collections" /> Documents Collections ); }; export default LibraryPage; ``` ## Type Definitions **File**: `src/api/trustgraph/messages.ts` Add collection-related message types: ```typescript // Collection management request export interface CollectionRequest extends RequestMessage { operation: "list-collections" | "update-collection" | "delete-collection"; user: string; collection?: string; name?: string; description?: string; tags?: string[]; tag_filter?: string[]; } // Collection management response export interface CollectionResponse { collections?: Array<{ user: string; collection: string; name: string; description: string; tags: string[]; created_at: string; updated_at: string; }>; } ``` ## Testing Checklist - [ ] List collections displays all collections for the user - [ ] Tag filter works when listing collections - [ ] Create new collection with ID, name, description, and tags - [ ] Edit existing collection (ID is disabled, other fields editable) - [ ] Delete single collection - [ ] Delete multiple collections - [ ] Selection state persists correctly - [ ] Loading indicators show during operations - [ ] Error notifications display for failures - [ ] Success notifications display for completed operations - [ ] Tab switching preserves state - [ ] Collections table sorts correctly - [ ] Empty state displays when no collections exist - [ ] Validation prevents creating collections with empty ID or name ## Future Enhancements 1. **Collection Assignment**: Allow assigning documents to collections from the Documents tab 2. **Collection Filtering**: Filter documents by collection 3. **Bulk Collection Operations**: Move multiple documents between collections 4. **Collection Statistics**: Show document count per collection 5. **Collection Search**: Search collections by name, description, or tags 6. **Collection Export**: Export collection metadata ## Migration Notes - No breaking changes to existing functionality - Documents tab functionality remains unchanged - New collections functionality is additive - Follows established patterns from library.ts and Documents.tsx - Uses consistent Chakra v3 components and patterns