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
isItemEditableandonItemLabelChangeprops) - 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
getSectionReorderPermissionto 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
getSectionIconfunction - 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} />;
- Background
- Objectives
- Scope
- Analysis
- Implementation
- Conclusion
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
- Analysis
- Implementation
- Conclusion
- End
- Last part
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}
/>;
- Editable subsection
- Editable section
- Not renamable section
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'}
/>;
- Deletable subsection
- Deletable section
- Can not delete me
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"
/>;
- Subsection (will be hidden too)
- Visible section (hover eye icon)
- Hidden section (hover label)
- Évènements principaux
- Principes, règles et méthodes comptables
- Identité de la société mère consolidante
- Rémunération des dirigeants
- 88888
- Immobilisations corporelles et incorporelles
- Principaux mouvements de l'exercice
- Amortissements
- Durées d'amortissement
- Amortissements dérogatoires
- Dépréciations actif immobilisé
- 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
- 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
- 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
- Simulation des amortissements si acquisition en pleine propriété
- Tableaux des engagements de crédit-bail
- bbbb
- Subsection 1.1
- Deep Subsection 1.2.1
- Subsection 2.1
- Deep Subsection 2.2.1
- Subsection 3.1
- Deep Subsection 3.2.1
- Subsection 4.1
- Deep Subsection 4.2.1
- Subsection 5.1
- Deep Subsection 5.2.1
- Subsection 6.1
- Deep Subsection 6.2.1
- Subsection 7.1
- Deep Subsection 7.2.1
- Subsection 8.1
- Deep Subsection 8.2.1
- Subsection 9.1
- Deep Subsection 9.2.1
- Subsection 10.1
- Deep Subsection 10.2.1
- Subsection 11.1
- Deep Subsection 11.2.1
- Subsection 12.1
- Deep Subsection 12.2.1
- Subsection 13.1
- Deep Subsection 13.2.1
- Subsection 14.1
- Deep Subsection 14.2.1
- Subsection 15.1
- Deep Subsection 15.2.1
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'}
/>;
- Existing subsection
- Regular section
- Not section creatable
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
- Background (no children - cannot create)
- Hidden section (cannot rename)
- Analysis (no children - cannot create)
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." />
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 />
créant une section
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
getSectionTitleChangePermissionreturns false) - Supprimer (Delete): Remove the section (disabled if
getSectionDeletePermissionreturns false) - Créer une sous-section (Create subsection): Add a child section (disabled if
getSectionCreatePermissionreturns false)
API Reference
Props
| Name | Type | Required | Default | Description |
|---|---|---|---|---|
sections | DocumentSection[] | No | undefined | Array of document sections (tree structure) |
noSectionsPlaceholder | string | React.ReactNode | No | undefined | Content displayed when there are no sections |
isLoading | boolean | No | false | Shows a loading skeleton when true |
getSectionTitleChangePermission | (section: DocumentSection) => boolean | No | () => true | Returns whether section title can be edited |
getSectionDeletePermission | (section: DocumentSection) => boolean | No | () => true | Returns whether section can be deleted |
getSectionReorderPermission | (section: DocumentSection) => boolean | No | () => true | Returns whether section can be reordered via drag & drop |
getSectionCreatePermission | (section: DocumentSection) => boolean | No | () => true | Returns whether a section can be created under this section |
getSectionVisibilityPermission | (section: DocumentSection) => boolean | No | () => true | Returns whether section visibility can be toggled |
onSectionOrderChange | (params: ItemPositionParams) => void | No | undefined | Callback when section order is changed via drag & drop |
onSectionTitleChange | (sectionId: string, newTitle: string) => void | No | undefined | Callback when section title is changed via inline editing |
onSectionDelete | (sectionId: string) => void | No | undefined | Callback when a section is deleted via context menu |
onSectionVisibilityChange | (sectionId: string, visible: boolean) => void | No | undefined | Callback when section visibility is toggled |
onSectionCreate | (parentId: string | null, title: string) => string | Promise<string> | No | undefined | Callback 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 movedoldPosition: Object withparentId(string | null) andindex(number)newPosition: Object withparentId(string | null) andindex(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: nulland receive special visual styling (bold text). - Drag & Drop: By default, all sections can be reordered freely. Use
getSectionReorderPermissionto 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)