Workspace Components
React components for document and folder management in FounderyOS Suite.
Location: foundery-os-suite/src/components/workspace/
FolderTree
Hierarchical document tree with folders, expand/collapse, and drag-drop organization.
Import
import { FolderTree } from '../../components/workspace/FolderTree';Props
| Prop | Type | Default | Description |
|---|---|---|---|
onDocumentClick | (doc: DocumentUI) => void | required | Callback when a document is clicked |
onNewDocument | () => void | optional | Callback when "New Document" is clicked |
onNewFolder | () => void | optional | Callback when "New Folder" is clicked |
enableDragDrop | boolean | true | Enable drag-drop reordering |
className | string | '' | Additional CSS classes |
Usage
import { FolderTree } from '../../components/workspace/FolderTree';
import { openCreateDocumentModal, openCreateFolderModal } from '../../stores/documentStore';
function WorkspaceDocuments() {
const handleDocumentClick = (doc: DocumentUI) => {
// Open document in editor
selectDocument(doc.id, doc.content);
};
return (
<FolderTree
onDocumentClick={handleDocumentClick}
onNewDocument={() => openCreateDocumentModal()}
onNewFolder={() => openCreateFolderModal()}
enableDragDrop={true}
/>
);
}Features
- Hierarchical Display: Renders nested folder structure with indentation
- Expand/Collapse: Click folders or chevrons to show/hide children
- Drag-Drop: Drag documents between folders using @dnd-kit
- Context Menu: Right-click for rename, delete, create subfolder
- Circular Reference Prevention: Cannot drop a folder into its own descendants
Related Store
The FolderTree reads from these nanostores atoms:
import {
$documentTree, // Computed hierarchical tree
$folderExpandedState, // Map of folder ID → expanded boolean
} from '../../stores/documentStore';FolderTreeItem
Individual item in the folder tree (document or folder).
Import
import { FolderTreeItem } from '../../components/workspace/FolderTreeItem';Props
| Prop | Type | Default | Description |
|---|---|---|---|
document | DocumentUI | required | Document/folder to display |
depth | number | required | Nesting depth (0 = root) |
onDocumentClick | (doc: DocumentUI) => void | required | Callback when clicked |
onFolderClick | (folder: DocumentUI) => void | optional | Callback when folder clicked |
enableDragDrop | boolean | true | Enable drag-drop |
Features
- Indentation:
depth * 16pxpadding for visual hierarchy - Draggable: Uses
useDraggablefrom @dnd-kit - Droppable: Folders use
useDroppablefrom @dnd-kit - Inline Rename: Press F2 or use context menu to rename
- Visual Feedback: Highlights on drag-over, shows drag handle on hover
Data Attributes
<li data-testid="tree-item-{id}"
data-folder="true|false"
data-depth="{depth}"
data-dragging="true|false"
data-drop-target="true|false">FolderCreateModal
Modal dialog for creating new folders.
Import
import { FolderCreateModal } from '../../components/workspace/FolderCreateModal';Props
| Prop | Type | Default | Description |
|---|---|---|---|
onCreated | (folderId: bigint) => void | optional | Callback after folder created |
Usage
import { FolderCreateModal } from '../../components/workspace/FolderCreateModal';
import { openCreateFolderModal } from '../../stores/documentStore';
function WorkspacePage() {
return (
<>
<button onClick={() => openCreateFolderModal()}>New Folder</button>
<FolderCreateModal onCreated={(id) => console.log('Created:', id)} />
</>
);
}Store Integration
The modal visibility is controlled by store atoms:
import {
$isCreatingFolder, // Modal open state
$isCreatingFolderLoading, // Loading spinner state
$newFolderParentId, // Parent folder for nesting
openCreateFolderModal, // Action to open modal
closeCreateFolderModal, // Action to close modal
} from '../../stores/documentStore';
// Open modal for root-level folder
openCreateFolderModal();
// Open modal for nested folder
openCreateFolderModal(parentFolderId);DocumentContextMenu
Context menu for document/folder actions.
Import
import { DocumentContextMenu } from '../../components/workspace/DocumentContextMenu';Props
| Prop | Type | Description |
|---|---|---|
document | DocumentUI | Document/folder for context actions |
onRename | (documentId: bigint) => void | Optional callback when rename triggered |
onDelete | (document: DocumentUI) => void | Optional callback when delete triggered |
onNewFolder | (parentId: bigint) => void | Optional callback when new folder triggered |
onNewDocument | (parentId: bigint) => void | Optional callback when new document triggered |
onSaveAsTemplate | (document: DocumentUI) => void | Optional callback when save as template triggered |
Menu Items
| Item | Condition | Action |
|---|---|---|
| Rename | Always | Opens inline rename mode |
| New Folder | If is_folder | Opens create folder modal with parent |
| New Document | If is_folder | Opens create document modal with parent |
| Save as Template | If not is_folder | Opens save as template modal |
| Delete | Always | Soft-deletes to trash |
TemplateSelector
Grid component for selecting a document template during document creation.
Import
import { TemplateSelector } from '../../components/workspace/TemplateSelector';Props
| Prop | Type | Default | Description |
|---|---|---|---|
onSelect | (template: TemplateUI) => void | required | Callback when a template is selected |
selectedId | bigint | optional | Currently selected template ID for highlighting |
className | string | '' | Additional CSS classes |
Usage
import { TemplateSelector } from '../../components/workspace/TemplateSelector';
import { type TemplateUI } from '../../stores/documentStore';
function CreateDocumentWithTemplate() {
const [selectedTemplate, setSelectedTemplate] = useState<TemplateUI | null>(null);
return (
<TemplateSelector
onSelect={(template) => setSelectedTemplate(template)}
selectedId={selectedTemplate?.id}
/>
);
}Features
- Template Grid: Displays templates in a responsive 2-column grid
- Search/Filter: Filter templates by name, description, or content
- System vs User: Shows system templates with "System" badge
- Content Preview: Shows first 150 characters of template content
- Loading State: Spinner while templates are being fetched
- Error State: Error message with retry button on fetch failure
Store Integration
TemplateSelector reads from these nanostores atoms:
import {
$allTemplates, // Combined system + user templates
$isLoadingTemplates, // Loading state
$templateError, // Error state
} from '../../stores/documentStore';Data Attributes
<div data-testid="template-selector"> <!-- Container -->
<input data-testid="template-search-input"> <!-- Search input -->
<div data-testid="template-grid"> <!-- Template grid -->
<button data-testid="template-card-{id}"> <!-- Individual template card -->
<div data-testid="template-selector-loading"> <!-- Loading state -->
<div data-testid="template-selector-error"> <!-- Error state -->
<div data-testid="template-selector-empty"> <!-- Empty state -->SaveAsTemplateModal
Modal dialog for saving a document as a reusable template.
Import
import { SaveAsTemplateModal } from '../../components/workspace/SaveAsTemplateModal';Props
| Prop | Type | Default | Description |
|---|---|---|---|
onSaved | (templateId: bigint) => void | optional | Callback after template is saved |
Usage
import { SaveAsTemplateModal } from '../../components/workspace/SaveAsTemplateModal';
import { openSaveAsTemplateModal } from '../../stores/documentStore';
function WorkspacePage() {
return (
<>
{/* Trigger from context menu or button */}
<button onClick={() => openSaveAsTemplateModal(document)}>
Save as Template
</button>
<SaveAsTemplateModal onSaved={(id) => console.log('Template created:', id)} />
</>
);
}Features
- Pre-filled Name: Defaults to document title
- Optional Description: Add context for the template
- Content Preview: Toggle to see document content being saved
- Validation: Requires non-empty template name
- Loading State: Spinner during save operation
- Error Handling: Displays error messages on failure
Store Integration
import {
$isSavingAsTemplate, // Modal open state
$isSavingAsTemplateLoading, // Loading spinner state
$documentToSaveAsTemplate, // Document being saved
openSaveAsTemplateModal, // Action to open modal
closeSaveAsTemplateModal, // Action to close modal
setSavingAsTemplateLoading, // Set loading state
} from '../../stores/documentStore';
// Open modal with a document
openSaveAsTemplateModal(document);Data Attributes
<div data-testid="save-as-template-modal"> <!-- Modal container -->
<input data-testid="template-name-input"> <!-- Name input -->
<textarea data-testid="template-description-input"> <!-- Description input -->
<button data-testid="toggle-preview-button"> <!-- Toggle content preview -->
<div data-testid="content-preview"> <!-- Content preview (when shown) -->
<button data-testid="save-as-template-submit"> <!-- Save button -->
<button data-testid="save-as-template-cancel"> <!-- Cancel button -->
<p data-testid="save-as-template-error"> <!-- Error message -->TemplateManagement
Full-screen view for managing user templates with list and delete functionality.
Import
import { TemplateManagement } from '../../components/workspace/TemplateManagement';Props
| Prop | Type | Default | Description |
|---|---|---|---|
onDeleted | (templateId: bigint) => void | optional | Callback after template is deleted |
Usage
import { TemplateManagement } from '../../components/workspace/TemplateManagement';
import { setShowTemplateManagement } from '../../stores/documentStore';
function WorkspacePage() {
return (
<>
<button onClick={() => setShowTemplateManagement(true)}>
Manage Templates
</button>
<TemplateManagement onDeleted={(id) => console.log('Deleted:', id)} />
</>
);
}Features
- Sections: Separate sections for System Templates and My Templates
- Template Details: Name, description, usage count
- Content Preview: Toggle eye icon to show/hide content
- Delete with Confirmation: Delete button opens confirmation dialog
- System Protection: System templates cannot be deleted (no delete button)
- Template Counts: Header shows count of user and system templates
Store Integration
import {
$documentTemplates, // User templates list
$systemTemplates, // System templates list
$showTemplateManagement, // Panel visibility
$isDeleteTemplateConfirmOpen, // Confirmation dialog state
$templateToDelete, // Template pending deletion
setShowTemplateManagement, // Open/close panel
confirmDeleteTemplate, // Open confirmation dialog
cancelDeleteTemplate, // Close confirmation dialog
executeDeleteTemplate, // Execute deletion
} from '../../stores/documentStore';Data Attributes
<div data-testid="template-management-panel"> <!-- Panel container -->
<button data-testid="template-management-close"> <!-- Close button -->
<div data-testid="template-management-empty"> <!-- Empty state -->
<div data-testid="system-templates-section"> <!-- System templates section -->
<div data-testid="user-templates-section"> <!-- User templates section -->
<div data-testid="template-card-{id}"> <!-- Individual template card -->
<button data-testid="template-preview-toggle-{id}"> <!-- Preview toggle button -->
<div data-testid="template-content-preview-{id}"> <!-- Content preview -->
<button data-testid="template-delete-{id}"> <!-- Delete button -->
<p data-testid="template-usage-count"> <!-- Usage count text -->
<div data-testid="delete-template-confirm-dialog"> <!-- Confirmation dialog -->
<button data-testid="delete-template-confirm"> <!-- Confirm delete button -->
<button data-testid="delete-template-cancel"> <!-- Cancel delete button -->Store Reference
documentStore.ts
Key atoms and actions for template functionality:
// Template Type Definitions
type TemplateType = 'Document' | 'Capture';
interface Template {
id: bigint;
owner: Uint8Array;
name: string;
description?: string;
template_type: TemplateType;
content: string;
is_system: boolean;
created_at: bigint;
updated_at: bigint;
}
interface TemplateUI extends Template {
lastModified: Date;
usageCount?: number;
}
// Template Atoms
$documentTemplates: atom<TemplateUI[]> // User templates list
$systemTemplates: atom<TemplateUI[]> // System templates list
$allTemplates: computed // Combined templates (computed)
$isLoadingTemplates: atom<boolean> // Loading state
$templateError: atom<string | null> // Error state
$isSavingAsTemplate: atom<boolean> // Save modal visibility
$isSavingAsTemplateLoading: atom<boolean> // Save loading state
$documentToSaveAsTemplate: atom<DocumentUI | null> // Document being saved
$showTemplateManagement: atom<boolean> // Management panel visibility
$templateToDelete: atom<TemplateUI | null> // Template pending deletion
$isDeleteTemplateConfirmOpen: atom<boolean>// Confirmation dialog state
// Template Actions
setTemplates(user: TemplateUI[], system: TemplateUI[]) // Set both template lists
setUserTemplates(templates: TemplateUI[]) // Set user templates
setSystemTemplates(templates: TemplateUI[]) // Set system templates
setTemplatesLoading(loading: boolean) // Set loading state
setTemplateError(error: string | null) // Set error state
addTemplateToStore(template: TemplateUI) // Add new template
removeTemplateFromStore(templateId: bigint) // Remove template
updateTemplateUsageCount(templateId: bigint, count?: number) // Increment usage
openSaveAsTemplateModal(document: DocumentUI) // Open save modal
closeSaveAsTemplateModal() // Close save modal
setSavingAsTemplateLoading(loading: boolean) // Set save loading
setShowTemplateManagement(show: boolean) // Open/close management
confirmDeleteTemplate(template: TemplateUI) // Open delete confirmation
cancelDeleteTemplate() // Cancel deletion
executeDeleteTemplate() // Execute deletionKey atoms and actions for folder functionality:
// Atoms
$documents: atom<DocumentUI[]> // Flat document list
$documentTree: computed // Hierarchical tree (computed)
$folderExpandedState: atom<Map> // Folder expand states
$isCreatingFolder: atom<boolean> // Modal visibility
$isCreatingFolderLoading: atom<boolean> // Loading state
$newFolderParentId: atom<bigint | null> // Parent for new folder
// Actions
toggleFolderExpanded(folderId: bigint) // Toggle expand/collapse
expandAllFolders() // Expand all folders
collapseAllFolders() // Collapse all folders
openCreateFolderModal(parentId?: bigint) // Open create modal
closeCreateFolderModal() // Close create modal
moveDocumentInStore(docId, newParentId) // Update parent locally
// Helpers
hasChildren(documentId: bigint): boolean
getDescendantIds(documentId: bigint): Set<string>
wouldCreateCircularReference(docId, targetParentId): booleandocumentService.ts
Service functions for template operations:
// Fetch all templates (user and system)
fetchTemplates(): Promise<{ user: TemplateUI[]; system: TemplateUI[] }>
// Create a document from a template
createDocumentFromTemplate(
workspaceId: bigint,
templateId: bigint,
title: string,
parentId?: bigint
): Promise<DocumentUI>
// Save a document as a template
saveAsTemplate(
documentId: bigint,
name: string,
description?: string
): Promise<TemplateUI>
// Delete a user template
deleteTemplate(templateId: bigint): Promise<boolean>Service functions for folder operations:
// Create a new folder
createFolder(
workspaceId: bigint,
title: string,
parentId?: bigint | null
): Promise<DocumentUI>
// Move a document to a new parent
moveDocument(
documentId: bigint,
newParentId: bigint | null
): Promise<boolean>Testing
Test Files
| File | Coverage |
|---|---|
__tests__/FolderTree.test.tsx | Tree rendering, expand/collapse, drag-drop |
__tests__/FolderCreateModal.test.tsx | Modal visibility, validation, creation |
__tests__/TemplateSelector.test.tsx | Template grid, search/filter, selection (21 tests) |
__tests__/SaveAsTemplateModal.test.tsx | Modal visibility, form, save action (21 tests) |
__tests__/TemplateManagement.test.tsx | Template list, preview, delete (23 tests) |
__tests__/DocumentContextMenu.test.tsx | Menu items including Save as Template (12 tests) |
__tests__/DocumentCreateModal.template.test.tsx | Template mode creation flow (15 tests) |
stores/__tests__/documentStore.test.ts | Store atoms, tree computation, template atoms |
services/__tests__/documentService.test.ts | createFolder, moveDocument, template CRUD |
Running Tests
cd foundery-os-suite
npm test -- --grep "FolderTree"
npm test -- --grep "FolderCreateModal"
npm test -- --grep "documentStore"
npm test -- --grep "documentService"Test IDs
Components expose data-testid attributes for testing:
tree-item-{id} # Individual tree item
drag-handle-{id} # Drag handle button
chevron-{id} # Expand/collapse chevron
tree-rename-input-{id} # Inline rename input
create-folder-modal # Create folder modal
folder-title-input # Title input in modal
create-folder-submit # Submit button
create-folder-cancel # Cancel button
create-folder-error # Error messageAcceptance Criteria Reference
Folder Features (AC-2.3.4)
| AC | Description | Implementation |
|---|---|---|
| AC-2.3.4.1 | Create folder with nesting | createFolder(), FolderCreateModal |
| AC-2.3.4.2 | Drag-drop into folders | useDraggable, useDroppable, moveDocument() |
| AC-2.3.4.3 | Expand/collapse folders | toggleFolderExpanded(), chevron UI |
| AC-2.3.4.4 | Hierarchical tree view | $documentTree computed, indentation CSS |
| AC-2.3.4.5 | Move persistence | moveDocument() updates parent_id |
Template Features (AC-2.3.5)
| AC | Description | Implementation |
|---|---|---|
| AC-2.3.5.1 | Templates appear on selection | TemplateSelector, mode toggle in DocumentCreateModal |
| AC-2.3.5.2 | Create document from template | createDocumentFromTemplate(), content pre-population |
| AC-2.3.5.3 | Save document as template | SaveAsTemplateModal, saveAsTemplate() |
| AC-2.3.5.4 | View saved templates | TemplateManagement, $documentTemplates |
| AC-2.3.5.5 | Delete template | deleteTemplate(), confirmation dialog |