Skip to content

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

typescript
import { FolderTree } from '../../components/workspace/FolderTree';

Props

PropTypeDefaultDescription
onDocumentClick(doc: DocumentUI) => voidrequiredCallback when a document is clicked
onNewDocument() => voidoptionalCallback when "New Document" is clicked
onNewFolder() => voidoptionalCallback when "New Folder" is clicked
enableDragDropbooleantrueEnable drag-drop reordering
classNamestring''Additional CSS classes

Usage

tsx
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

The FolderTree reads from these nanostores atoms:

typescript
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

typescript
import { FolderTreeItem } from '../../components/workspace/FolderTreeItem';

Props

PropTypeDefaultDescription
documentDocumentUIrequiredDocument/folder to display
depthnumberrequiredNesting depth (0 = root)
onDocumentClick(doc: DocumentUI) => voidrequiredCallback when clicked
onFolderClick(folder: DocumentUI) => voidoptionalCallback when folder clicked
enableDragDropbooleantrueEnable drag-drop

Features

  • Indentation: depth * 16px padding for visual hierarchy
  • Draggable: Uses useDraggable from @dnd-kit
  • Droppable: Folders use useDroppable from @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

html
<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

typescript
import { FolderCreateModal } from '../../components/workspace/FolderCreateModal';

Props

PropTypeDefaultDescription
onCreated(folderId: bigint) => voidoptionalCallback after folder created

Usage

tsx
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:

typescript
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

typescript
import { DocumentContextMenu } from '../../components/workspace/DocumentContextMenu';

Props

PropTypeDescription
documentDocumentUIDocument/folder for context actions
onRename(documentId: bigint) => voidOptional callback when rename triggered
onDelete(document: DocumentUI) => voidOptional callback when delete triggered
onNewFolder(parentId: bigint) => voidOptional callback when new folder triggered
onNewDocument(parentId: bigint) => voidOptional callback when new document triggered
onSaveAsTemplate(document: DocumentUI) => voidOptional callback when save as template triggered
ItemConditionAction
RenameAlwaysOpens inline rename mode
New FolderIf is_folderOpens create folder modal with parent
New DocumentIf is_folderOpens create document modal with parent
Save as TemplateIf not is_folderOpens save as template modal
DeleteAlwaysSoft-deletes to trash

TemplateSelector

Grid component for selecting a document template during document creation.

Import

typescript
import { TemplateSelector } from '../../components/workspace/TemplateSelector';

Props

PropTypeDefaultDescription
onSelect(template: TemplateUI) => voidrequiredCallback when a template is selected
selectedIdbigintoptionalCurrently selected template ID for highlighting
classNamestring''Additional CSS classes

Usage

tsx
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:

typescript
import {
  $allTemplates,       // Combined system + user templates
  $isLoadingTemplates, // Loading state
  $templateError,      // Error state
} from '../../stores/documentStore';

Data Attributes

html
<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

typescript
import { SaveAsTemplateModal } from '../../components/workspace/SaveAsTemplateModal';

Props

PropTypeDefaultDescription
onSaved(templateId: bigint) => voidoptionalCallback after template is saved

Usage

tsx
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

typescript
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

html
<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

typescript
import { TemplateManagement } from '../../components/workspace/TemplateManagement';

Props

PropTypeDefaultDescription
onDeleted(templateId: bigint) => voidoptionalCallback after template is deleted

Usage

tsx
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

typescript
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

html
<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:

typescript
// 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 deletion

Key atoms and actions for folder functionality:

typescript
// 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): boolean

documentService.ts

Service functions for template operations:

typescript
// 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:

typescript
// 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

FileCoverage
__tests__/FolderTree.test.tsxTree rendering, expand/collapse, drag-drop
__tests__/FolderCreateModal.test.tsxModal visibility, validation, creation
__tests__/TemplateSelector.test.tsxTemplate grid, search/filter, selection (21 tests)
__tests__/SaveAsTemplateModal.test.tsxModal visibility, form, save action (21 tests)
__tests__/TemplateManagement.test.tsxTemplate list, preview, delete (23 tests)
__tests__/DocumentContextMenu.test.tsxMenu items including Save as Template (12 tests)
__tests__/DocumentCreateModal.template.test.tsxTemplate mode creation flow (15 tests)
stores/__tests__/documentStore.test.tsStore atoms, tree computation, template atoms
services/__tests__/documentService.test.tscreateFolder, moveDocument, template CRUD

Running Tests

bash
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 message

Acceptance Criteria Reference

Folder Features (AC-2.3.4)

ACDescriptionImplementation
AC-2.3.4.1Create folder with nestingcreateFolder(), FolderCreateModal
AC-2.3.4.2Drag-drop into foldersuseDraggable, useDroppable, moveDocument()
AC-2.3.4.3Expand/collapse folderstoggleFolderExpanded(), chevron UI
AC-2.3.4.4Hierarchical tree view$documentTree computed, indentation CSS
AC-2.3.4.5Move persistencemoveDocument() updates parent_id

Template Features (AC-2.3.5)

ACDescriptionImplementation
AC-2.3.5.1Templates appear on selectionTemplateSelector, mode toggle in DocumentCreateModal
AC-2.3.5.2Create document from templatecreateDocumentFromTemplate(), content pre-population
AC-2.3.5.3Save document as templateSaveAsTemplateModal, saveAsTemplate()
AC-2.3.5.4View saved templatesTemplateManagement, $documentTemplates
AC-2.3.5.5Delete templatedeleteTemplate(), confirmation dialog

Hello World Co-Op DAO