Skip to main content

DocumentComposer

A structured document editor component with an interactive tree view for composing and organizing document sections. The component provides drag-and-drop reordering, inline editing, contextual menus, and hierarchical section management.

Features

Features from MUI X Tree View Pro

The component leverages the following features from the underlying @mui/x-tree-view-pro library: https://mui.com/x/react-tree-view/quickstart/

  • Hierarchical Tree View: Display and navigate nested document sections
  • Drag & Drop Reordering: Core drag-and-drop functionality
  • Inline Editing: Double-click to edit section titles (via isItemEditable and onItemLabelChange props)
  • Expansion Control: Expand/collapse sections (triggered by icon container click)

DocumentComposer-Specific Features

The following features and business rules are specific to the DocumentComposer component:

  • Drag & Drop: Sections can be reordered via drag & drop. Use getSectionReorderPermission to implement custom restrictions if needed.
  • Contextual Menus: Menu button (three dots) with document-specific actions (rename, delete, create subsection)
  • Subsection Creation: Create new subsections via context menu with inline editing (temporary section pattern)
  • Section Highlighting: Visual distinction for root sections (sections with parentId: null)
  • Hover Controls: Custom interactive controls (drag handle, menu button) that appear on section hover
  • Custom Icons: Support for custom icons per section via getSectionIcon function
  • Two-Column Layout: DocumentComposer-specific layout with tree view (left) and content area (right)
  • Capability Callbacks: Fine-grained control over which actions are available per section

Basic Usage

See DocumentSection for the type definition.

const sectionsData: DocumentSection[] = [
{
id: "section-1",
title: "Introduction",
visible: true,
parentId: null,
sections: [
{
id: "section-1-1",
title: "Overview",
visible: true,
parentId: "section-1",
sections: [],
},
],
},
];

<DocumentComposer sections={sectionsData} />;
  • Introduction with long long long long long longlong long longlong long longlong long longlong long longlong long long label
    • Overview
      • Background
      • Objectives
    • Scope
  • Main Content
    • Analysis
    • Implementation
  • Conclusion
content

handleSectionOrderChange

const [sections, setSections] = useState(sectionsData);

const handleSectionOrderChange = (params: ItemPositionParams) => {
console.log("Section moved:", params);
if (params.itemId === "section-1") {
// specific re order
}
};

<DocumentComposer
sections={sections}
onSectionOrderChange={handleSectionOrderChange}
/>;
  • DRAG ME TO SPECIFIC RE ORDER
  • Main Content
    • Analysis
    • Implementation
  • Conclusion
  • End
  • Last part
content

onSectionTitleChange & getSectionTitleChangePermission

Double-click on a section or use the context menu (menu button → "Rename") to edit the title.

const [sections, setSections] = useState(sectionsData);

const handleSectionTitleChange = (sectionId: string, newTitle: string) => {
console.log(`Section ${sectionId} renamed to "${newTitle}"`);
if (sectionId === "section-1") {
// specific rename
}
};

<DocumentComposer
sections={sections}
onSectionTitleChange={handleSectionTitleChange}
/>;
  • SPECIFIC RENAME (custom behavior)
    • Editable subsection
  • Editable section
  • Not renamable section
content

onSectionDelete & getSectionDeletePermission

Click on the menu button (three dots) and select "Supprimer" from the context menu to remove it.

Use getSectionDeletePermission to control whether a section can be deleted. When it returns false, the "Supprimer" menu item is disabled.

const [sections, setSections] = useState(sectionsData);

const handleSectionDelete = (sectionId: string) => {
console.log(`Section ${sectionId} deleted`);
if (sectionId === "section-1") {
// specific delete
}
};

<DocumentComposer
sections={sections}
onSectionDelete={handleSectionDelete}
// Prevent deletion for section-3
getSectionDeletePermission={(section) => section.id !== 'section-3'}
/>;
  • CUSTOM DELETE (deletes all except last)
    • Deletable subsection
  • Deletable section
  • Can not delete me
content

Section visibility

Click on the eye icon to toggle the visibility of a section.

  • onSectionVisibilityChange: Callback when the visibility of a section is toggled.
  • getSectionVisibilityPermission: Controls whether the visibility toggle is enabled for a given section. Defaults to () => true.
  • hiddenSectionTooltip: Tooltip text displayed when hovering a hidden section label (visible: false).
  • visibleIconTooltip: Tooltip text displayed when hovering the visibility icon of a visible section.
  • hiddenIconTooltip: Tooltip text displayed when hovering the visibility icon of a hidden section.
const handleSectionVisibilityChange = (sectionId: string, visible: boolean) => {
console.log(`Section ${sectionId} visibility changed to ${visible}`);

// Specific behavior for section-1: hide all sections
if (sectionId === 'section-1') {
setSections((prev) => hideAllSections(prev));
return;
}

// Default behavior: update only the target section
setSections((prev) => updateVisibility(prev, sectionId, visible));
};

<DocumentComposer
sections={sections}
onSectionVisibilityChange={handleSectionVisibilityChange}
hiddenSectionTooltip="This section will not appear in the annex"
visibleIconTooltip="Click to hide this section"
hiddenIconTooltip="Click to show this section"
/>;
  • Hide all sections (custom behavior)
    • Subsection (will be hidden too)
  • Visible section (hover eye icon)
  • Hidden section (hover label)
  • EXEMPLE AVEC 130 sections TOTAL
    • Évènements principaux
    • Principes, règles et méthodes comptables
    • Identité de la société mère consolidante
    • Rémunération des dirigeants
    • 88888
  • Informations relatives au bilan et au compte de resultat
    • Actif
      • Immobilisations
        • Immobilisations corporelles et incorporelles
        • Principaux mouvements de l'exercice
        • Amortissements
        • Durées d'amortissement
        • Amortissements dérogatoires
        • Dépréciations actif immobilisé
        • Précisions sur éléments actifs immobilisés
          • Frais d'établissement
          • Fonds commercial
          • Frais de recherche et de développement
      • Immobilisations financières
      • Filiales et participations
      • Autres participations
      • Créances immobilisées
      • Stocks
      • Contrats à long terme
      • Dépréciations actif
      • Produits à recevoir
      • État des créances
      • Valeur mobilières de placement et trésorerie
      • Charges constatées d'avance
      • Écarts de conversion actif
    • Passif
      • Capitaux propres
      • Subventions investissement
      • Provisions pour risques et charges
      • Engagement retraite
      • État des dettes
      • Charges à payer
      • Produits constatés d'avance
      • Écarts de conversion passifs
      • Évaluation des créances et dettes en devises
    • Compte de résultat
      • Chiffre d'affaires
      • Transferts de charges
      • Résultat financier
      • Résultat exceptionnel
      • Impôts sur les sociétés
      • Honoraires du commissaire aux comptes
  • Informations sur les parties liées
  • Effectif moyen
  • Autres informations spécifiques
  • Engagements financiers donnés et reçus
  • Engagements de dettes assorties de sûretés réelles
  • aaaa
  • Engagement de crédit-bail
    • Simulation des amortissements si acquisition en pleine propriété
    • Tableaux des engagements de crédit-bail
  • bbbb
  • Performance Test Section 1
    • Subsection 1.1
    • Subsection 1.2
      • Deep Subsection 1.2.1
  • Performance Test Section 2
    • Subsection 2.1
    • Subsection 2.2
      • Deep Subsection 2.2.1
  • Performance Test Section 3
    • Subsection 3.1
    • Subsection 3.2
      • Deep Subsection 3.2.1
  • Performance Test Section 4
    • Subsection 4.1
    • Subsection 4.2
      • Deep Subsection 4.2.1
  • Performance Test Section 5
    • Subsection 5.1
    • Subsection 5.2
      • Deep Subsection 5.2.1
  • Performance Test Section 6
    • Subsection 6.1
    • Subsection 6.2
      • Deep Subsection 6.2.1
  • Performance Test Section 7
    • Subsection 7.1
    • Subsection 7.2
      • Deep Subsection 7.2.1
  • Performance Test Section 8
    • Subsection 8.1
    • Subsection 8.2
      • Deep Subsection 8.2.1
  • Performance Test Section 9
    • Subsection 9.1
    • Subsection 9.2
      • Deep Subsection 9.2.1
  • Performance Test Section 10
    • Subsection 10.1
    • Subsection 10.2
      • Deep Subsection 10.2.1
  • Performance Test Section 11
    • Subsection 11.1
    • Subsection 11.2
      • Deep Subsection 11.2.1
  • Performance Test Section 12
    • Subsection 12.1
    • Subsection 12.2
      • Deep Subsection 12.2.1
  • Performance Test Section 13
    • Subsection 13.1
    • Subsection 13.2
      • Deep Subsection 13.2.1
  • Performance Test Section 14
    • Subsection 14.1
    • Subsection 14.2
      • Deep Subsection 14.2.1
  • Performance Test Section 15
    • Subsection 15.1
    • Subsection 15.2
      • Deep Subsection 15.2.1
content

onSectionCreate & getSectionCreatePermission

Click on the menu button (three dots) and select "Créer une sous-section" to create a new subsection. You can also use the "Créer une section" button at the bottom of the tree view to create a root-level section.

Use getSectionCreatePermission to control whether the "Créer une sous-section" menu item is enabled for a given section. Defaults to () => true.

const [sections, setSections] = useState(sectionsData);

const handleSectionCreate = (
parentId: string | null,
title: string
): string => {
console.log(`onSectionCreate called with parentId: ${parentId}, title: ${title}`);

const newId = `section-${Date.now()}`;
setSections((prev) => addSectionToParent(prev, parentId, newSection));
return newId;
};

<DocumentComposer
sections={sections}
onSectionCreate={handleSectionCreate}
// Disable section creation for section with id 'section-3'
getSectionCreatePermission={(section) => section.id !== 'section-3'}
/>;
  • CUSTOM ADD SECTION
    • Existing subsection
  • Regular section
  • Not section creatable
content

Loading state

<DocumentComposer sections={sections} isLoading={true} />

Controlling section capabilities

You can control which actions are available for each section using capability callbacks. All callbacks receive the section as parameter and return a boolean. They default to () => true (all actions allowed).

<DocumentComposer
sections={sections}
// Protect root sections from deletion
getSectionDeletePermission={(section) => section.parentId !== null}
// Disable section creation for sections without children
getSectionCreatePermission={(section) => section.sections?.length > 0}
// Prevent renaming hidden sections
getSectionTitleChangePermission={(section) => section.visible}
/>

Try it:

  • Click the menu button on "Introduction (root - cannot delete)" - notice "Supprimer" is disabled
  • Click the menu button on "Analysis (no children - cannot create)" - notice "Créer une sous-section" is disabled
  • Click the menu button on "Hidden section (cannot rename)" - notice "Renommer" is disabled
  • Introduction (root - cannot delete)
    • Overview (has children - can create)
      • Background (no children - cannot create)
    • Hidden section (cannot rename)
  • Main Content (root - cannot delete)
    • Analysis (no children - cannot create)
content

Component Structure

The DocumentComposer component uses a two-column layout:

  • Left Column (430px): Interactive tree view for navigating and managing sections
  • Right Column (1fr): Content area (currently displays "content" placeholder)

Customization

No Sections Placeholder

You can customize the content displayed when there are no sections. This prop accepts either a string or a React component:

With a simple string:

<DocumentComposer noSectionsPlaceholder="No sections available." />
No sections available.
content

With a React component:

<DocumentComposer
noSectionsPlaceholder={
<div style={{ textAlign: "center", padding: "20px" }}>
<strong>No sections yet</strong>
<p>Click "Add from library" to get started</p>
</div>
}
/>

Empty state

<DocumentComposer />
Commencez la création de votre annexe en
créant une section
content

Interactions

Drag & Drop

Sections can be reordered by dragging them. By default, all moves are allowed. Use the getSectionReorderPermission callback to implement custom restrictions if needed (e.g., limit moves to same parent). The underlying drag-and-drop functionality is provided by MUI X Tree View Pro.

Inline Editing

  • Double-click on any section title to enter edit mode (provided by MUI X Tree View Pro)
  • Or use the context menu "Renommer" (Rename) option (DocumentComposer-specific menu action)
  • Press Enter to confirm or Escape to cancel

Context Menu

DocumentComposer-specific feature: Click the menu button (three dots) on any section to access:

  • Renommer (Rename): Enter edit mode for the section (disabled if getSectionTitleChangePermission returns false)
  • Supprimer (Delete): Remove the section (disabled if getSectionDeletePermission returns false)
  • Créer une sous-section (Create subsection): Add a child section (disabled if getSectionCreatePermission returns false)

API Reference

Props

NameTypeRequiredDefaultDescription
sectionsDocumentSection[]NoundefinedArray of document sections (tree structure)
noSectionsPlaceholderstring | React.ReactNodeNoundefinedContent displayed when there are no sections
isLoadingbooleanNofalseShows a loading skeleton when true
getSectionTitleChangePermission(section: DocumentSection) => booleanNo() => trueReturns whether section title can be edited
getSectionDeletePermission(section: DocumentSection) => booleanNo() => trueReturns whether section can be deleted
getSectionReorderPermission(section: DocumentSection) => booleanNo() => trueReturns whether section can be reordered via drag & drop
getSectionCreatePermission(section: DocumentSection) => booleanNo() => trueReturns whether a section can be created under this section
getSectionVisibilityPermission(section: DocumentSection) => booleanNo() => trueReturns whether section visibility can be toggled
onSectionOrderChange(params: ItemPositionParams) => voidNoundefinedCallback when section order is changed via drag & drop
onSectionTitleChange(sectionId: string, newTitle: string) => voidNoundefinedCallback when section title is changed via inline editing
onSectionDelete(sectionId: string) => voidNoundefinedCallback when a section is deleted via context menu
onSectionVisibilityChange(sectionId: string, visible: boolean) => voidNoundefinedCallback when section visibility is toggled
onSectionCreate(parentId: string | null, title: string) => string | Promise<string>NoundefinedCallback when a new section is created. Must return the new section's ID.

Types

DocumentSection

type DocumentSection = {
id: string;
title: string;
visible: boolean;
parentId: string | null;
sections?: DocumentSection[] | null;
};

Note: The sections property allows for recursive nesting, creating a hierarchical structure. Root sections have parentId: null. The title property is used as the displayed label in the tree view.

ItemDisplayOrder

type ItemDisplayOrder = {
id: string;
displayOrder: number;
};

ItemPositionParams

type ItemPositionParams = {
itemId: string;
oldPosition: { parentId: string | null; index: number };
newPosition: { parentId: string | null; index: number };
};

Callback Props

onSectionOrderChange

Called when the user reorders sections via drag & drop. Provides:

  • params: Object containing details about the section being moved:
    • itemId: ID of the section being moved
    • oldPosition: Object with parentId (string | null) and index (number)
    • newPosition: Object with parentId (string | null) and index (number)

Example:

const handleSectionOrderChange = (params: ItemPositionParams) => {
console.log("Section moved:", params.itemId);
console.log("From:", params.oldPosition);
console.log("To:", params.newPosition);
// Implement your own logic to update state/backend with the new order
};

<DocumentComposer
sections={sections}
onSectionOrderChange={handleSectionOrderChange}
/>;

onSectionDelete

Called when the user deletes a section via the context menu. Provides:

  • sectionId: ID of the section being deleted

Example:

const handleSectionDelete = (sectionId: string) => {
console.log(`Section ${sectionId} deleted`);
// Update your state/backend to remove the section
// Remember to handle nested children if applicable
};

<DocumentComposer sections={sections} onSectionDelete={handleSectionDelete} />;

Business Rules & Limitations

The following business rules and limitations are enforced by the DocumentComposer component:

  • Root Section Identification: Root sections are identified by parentId: null and receive special visual styling (bold text).
  • Drag & Drop: By default, all sections can be reordered freely. Use getSectionReorderPermission to implement custom restrictions (e.g., limiting moves to same parent, depth-based rules, etc.).

Planned Features

The following features are planned for future implementation:

  • Menu Item Labels: Pass menu item labels through props for internationalization and customization
  • WYSIWYG Editor: Rich text editor for textual elements and tables in the right column (currently displays a placeholder)
  • Template Selection Modal / Library Browser: A complementary modal dialog with a multi-select tree view (using checkbox selection) providing a complete view of all available sections and subsections from a pre-defined template/library
  • Menu Action Implementation: Full functionality for remaining context menu action (add from library)