Planning Gantt et dépendances
Configurer les calendriers, la hiérarchie et les dépendances entre tables avec un aperçu atomique en React et Vue.
Gantt est un mode d’affichage facultatif commun aux registres React et Vue. Mappez deux colonnes de dates et la table déduit le planning des lignes qu’elle liste déjà, comme Kanban et Galerie n’ont besoin que de leurs propres mappages de colonnes. Fournissez plutôt un adaptateur transactionnel lorsque vous avez besoin de commits atomiques, de relations ou d’une pagination serveur. Dans les deux cas, la table fournit la chronologie, l’éditeur de relations et l’aperçu des conséquences.
Réglages de la vue
Ouvrez Vue → Réglages du Gantt pour le zoom, le premier jour de la semaine et la visibilité des dépendances. Ces contrôles utilisent le même panneau de réglages que les autres modes ; précédent/suivant/aujourd’hui et le rechargement restent près de la chronologie. Sur mobile, les choix restent dans un seul tiroir en bas de l’écran. Les dialogues de tâches et de dépendances s’ouvrent aussi en bas avec une hauteur limitée. Seul le contenu qui déborde défile ; le déplacement dans la chronologie reste disponible. Les champs des vues enregistrées et des URL sont inchangés. Chaque libellé de la chronologie, des dialogues et des réglages lit views.gantt.* dans les traductions de la table ; toute clé omise conserve son libellé anglais ou français intégré.
Essayer le Gantt
Choisissez React ou Vue ci-dessous : chaque onglet exécute son framework avec les mêmes tâches, calendriers et dépendances entre tables. Aperçu et Code fonctionnent comme dans les autres exemples. Agrandir ouvre un exemple en plein écran ; Réinitialiser restaure uniquement cet aperçu.
Déplacez Build the experience d’un jour vers la droite, ou placez le focus sur sa barre et appuyez sur la flèche droite.
Consultez les changements du parent, de la validation et de la publication dans l’autre table. Annulez pour conserver les dates initiales, ou appliquez l’aperçu complet.
Ouvrez Default view et passez à Table, Kanban ou Galerie pour consulter les mêmes éléments. Cliquez sur une tâche pour modifier ses dates, son parent et ses dépendances.
Les deux exemples utilisent le même graphe de démonstration, les mêmes calendriers, action de liste et vues enregistrées. L’adaptateur mémoire illustre le contrat de transaction ; l’application fournit un stockage durable en production.
Données de démonstration. Les modifications restent dans cet aperçu.
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { NuqsAdapter } from "nuqs/adapters/react";
import { useMemo, useState } from "react";
import { Toaster } from "sonner";
import { DataTable } from "@/components/ui/yayaw-table/components/data-table";
import { defineTableConfig } from "@/components/ui/yayaw-table/config/helpers";
import { createMemoryPlanningAdapter } from "@/components/ui/yayaw-table/planning/adapter";
import {
createGanttDemoViews,
demoGanttConfig,
demoPlanningConfig,
demoPlanningSnapshot,
ganttDemoColumns,
ganttDemoForm,
listPlanningDemo,
} from "../shared/gantt-data";
import type { ExamplePresentation } from "./presentation";
const config = defineTableConfig({
id: "gantt-demo",
columns: {
definitions: ganttDemoColumns,
visible: ["name", "start", "end", "status"],
order: ["name", "start", "end", "status"],
mandatory: ["name"],
},
table: {
enableViews: true,
displayModes: ["table", "gantt", "kanban", "gallery"],
defaultDisplayMode: "gantt",
planning: demoPlanningConfig,
gantt: demoGanttConfig,
kanban: { groupBy: "status", titleColumn: "name" },
gallery: { titleColumn: "name" },
allowCreate: false,
allowDelete: false,
allowDuplicate: false,
allowEdit: true,
allowInlineEdit: true,
inlineEdit: { enabled: true },
rowClickMode: "default",
defaultPageSize: 20,
},
translations: {
namespace: "gantt-demo",
keys: {
title: "Autumn launch",
description: "Tasks and releases share one planning graph.",
},
},
});
export default function GanttExample(presentation: ExamplePresentation = {}) {
const [queryClient] = useState(() => new QueryClient());
const integration = useMemo(() => {
const adapter = createMemoryPlanningAdapter({
snapshot: demoPlanningSnapshot(),
config: demoPlanningConfig,
});
const actions = {
planning: adapter.actions,
views: createGanttDemoViews(),
list: (params: Record<string, unknown>) =>
Promise.resolve(listPlanningDemo(adapter.getSnapshot(), params)),
};
return { getTableConfig: () => config, getTableActions: () => actions };
}, []);
return (
<QueryClientProvider client={queryClient}>
<NuqsAdapter>
<DataTable
{...presentation}
getFormConfig={() => ganttDemoForm}
tableType="gantt-demo"
{...integration}
details={{ presentation: "drawer", title: (row) => String(row.name) }}
/>
<Toaster />
</NuqsAdapter>
</QueryClientProvider>
);
}Données de démonstration. Les modifications restent dans cet aperçu.
<script setup lang="ts">
import type { ExamplePresentation } from "./presentation";
defineProps<ExamplePresentation>();
import { DataTable, defineTableConfig } from "@/components/ui/yayaw-table-vue";
import { createMemoryPlanningAdapter } from "@/components/ui/yayaw-table-vue/planning/adapter";
import type { TableListParams } from "@/components/ui/yayaw-table-vue/types";
import {
createGanttDemoViews,
demoGanttConfig,
demoPlanningConfig,
demoPlanningSnapshot,
ganttDemoColumns,
ganttDemoForm,
listPlanningDemo,
} from "../shared/gantt-data";
const config = defineTableConfig({
id: "gantt-demo",
columns: {
definitions: ganttDemoColumns,
visible: ["name", "start", "end", "status"],
order: ["name", "start", "end", "status"],
mandatory: ["name"],
},
table: {
enableViews: true,
displayModes: ["table", "gantt", "kanban", "gallery"],
defaultDisplayMode: "gantt",
planning: demoPlanningConfig,
gantt: demoGanttConfig,
kanban: { groupBy: "status", titleColumn: "name" },
gallery: { titleColumn: "name" },
allowCreate: false,
allowDelete: false,
allowDuplicate: false,
allowEdit: true,
allowInlineEdit: true,
inlineEdit: { enabled: true },
rowClickMode: "default",
defaultPageSize: 20,
},
translations: {
namespace: "gantt-demo",
keys: {
title: "Autumn launch",
description: "Tasks and releases share one planning graph.",
},
},
});
const adapter = createMemoryPlanningAdapter({
snapshot: demoPlanningSnapshot(),
config: demoPlanningConfig,
});
const actions = {
views: createGanttDemoViews(),
planning: adapter.actions,
list: async (params: TableListParams) =>
listPlanningDemo(adapter.getSnapshot(), { ...params }),
};
</script>
<template>
<DataTable v-bind="$props"
table-type="gantt-demo"
:config="config"
:get-form-config="() => ganttDemoForm"
:get-table-actions="() => actions"
:details="{ presentation: 'drawer', title: row => String(row.name) }"
/>
</template>Partir des lignes de la table
Une table qui mappe gantt.startColumn et gantt.endColumn n’a besoin d’aucun adaptateur de
planning : le graphe est déduit de son action list existante, et déplacer une barre enregistre via
son action update existante. L’exemple ci-dessous reprend le catalogue du démarrage
rapide avec deux colonnes de dates mappées.
"use client";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { NuqsAdapter } from "nuqs/adapters/react";
import { useMemo, useRef, useState } from "react";
import { DataTable, defineTableConfig } from "@/components/ui/yayaw-table";
import { products } from "../shared/products";
import { listRecords } from "./list-records";
import type { ExamplePresentation } from "./presentation";
/**
* A Gantt over the catalog from the quick start: no planning adapter, no graph.
* Mapping two date columns is the whole configuration.
*/
const config = defineTableConfig({
id: "restock-plan",
columns: {
definitions: [
{ id: "name", header: "Product", type: "text" },
{ id: "restockFrom", header: "From", type: "date" },
{ id: "restockTo", header: "To", type: "date" },
{
id: "status",
header: "Status",
type: "select",
options: [
{ label: "Draft", value: "draft" },
{ label: "Active", value: "active" },
{ label: "Archived", value: "archived" },
],
},
],
order: ["select", "name", "restockFrom", "restockTo", "status"],
visible: ["name", "restockFrom", "restockTo", "status"],
mandatory: ["name"],
},
table: {
displayModes: ["table", "gantt", "kanban", "gallery"],
defaultDisplayMode: "gantt",
kanban: { groupBy: "status", titleColumn: "name" },
gallery: { titleColumn: "name" },
allowEdit: true,
enableRowSelection: true,
planning: {
enabled: true,
scopeId: "docs/restock-plan",
sourceId: "products",
},
gantt: {
titleColumn: "name",
startColumn: "restockFrom",
endColumn: "restockTo",
},
defaultPageSize: 10,
},
translations: {
namespace: "restock-plan",
keys: {
title: "Restock plan",
description: "The catalog table with two date columns mapped.",
},
},
});
export default function GanttRowsExample(
presentation: ExamplePresentation = {}
) {
const [queryClient] = useState(() => new QueryClient());
// A stable store stands in for the application's own records.
const store = useRef(products.map((product) => ({ ...product })));
const integration = useMemo(
() => ({
getTableConfig: () => config,
getTableActions: () => ({
list: (params: Record<string, unknown>) =>
listRecords(store.current, params),
update: (id: string, patch: Record<string, unknown>) => {
store.current = store.current.map((row) =>
row.id === id ? { ...row, ...patch } : row
);
return Promise.resolve({ success: true });
},
}),
}),
[]
);
return (
<QueryClientProvider client={queryClient}>
<NuqsAdapter>
<DataTable
{...presentation}
getRowId={(row) => String(row.id)}
tableType="restock-plan"
{...integration}
/>
</NuqsAdapter>
</QueryClientProvider>
);
}<script setup lang="ts">
import { ref } from "vue";
import { DataTable, defineTableConfig } from "@/components/ui/yayaw-table-vue";
import type {
TableListParams,
TableRecord,
} from "@/components/ui/yayaw-table-vue/types";
import { products } from "../shared/products";
import { listRecords } from "./list-records";
import type { ExamplePresentation } from "./presentation";
defineProps<ExamplePresentation>();
/**
* A Gantt over the catalog from the quick start: no planning adapter, no graph.
* Mapping two date columns is the whole configuration.
*/
const config = defineTableConfig({
id: "restock-plan",
columns: {
definitions: [
{ id: "name", header: "Product", type: "text" },
{ id: "restockFrom", header: "From", type: "date" },
{ id: "restockTo", header: "To", type: "date" },
{
id: "status",
header: "Status",
type: "select",
options: [
{ label: "Draft", value: "draft" },
{ label: "Active", value: "active" },
{ label: "Archived", value: "archived" },
],
},
],
order: ["select", "name", "restockFrom", "restockTo", "status"],
visible: ["name", "restockFrom", "restockTo", "status"],
mandatory: ["name"],
},
table: {
displayModes: ["table", "gantt", "kanban", "gallery"],
defaultDisplayMode: "gantt",
kanban: { groupBy: "status", titleColumn: "name" },
gallery: { titleColumn: "name" },
allowEdit: true,
enableRowSelection: true,
planning: {
enabled: true,
scopeId: "docs/restock-plan",
sourceId: "products",
},
gantt: {
titleColumn: "name",
startColumn: "restockFrom",
endColumn: "restockTo",
},
defaultPageSize: 10,
},
translations: {
namespace: "restock-plan",
keys: {
title: "Restock plan",
description: "The catalog table with two date columns mapped.",
},
},
});
// A stable store stands in for the application's own records.
const store = ref<TableRecord[]>(products.map((product) => ({ ...product })));
const actions = {
list: (params: TableListParams) => listRecords(store.value, { ...params }),
update: (id: string, patch: TableRecord) => {
store.value = store.value.map((row) =>
row.id === id ? { ...row, ...patch } : row
);
return Promise.resolve({ success: true });
},
};
</script>
<template>
<DataTable v-bind="$props"
table-type="restock-plan"
:config="config"
:get-row-id="(row: TableRecord) => String(row.id)"
:get-table-actions="() => actions"
/>
</template>Un planning déduit n’est pas transactionnel : chaque enregistrement concerné est modifié via update
plutôt que validé d’un bloc, donc un échec partiel est signalé et le graphe rechargé pour refléter ce
qui a été stocké. L’édition des dépendances reste désactivée, car les lignes portent des dates et une
hiérarchie mais aucune relation. Ajoutez gantt.parentColumn pour une hiérarchie issue d’une colonne
parent scalaire, et gantt.calendarColumn pour choisir un calendrier par ligne.
createRowsPlanningAdapter construit explicitement le même adaptateur lorsque vous voulez fournir des
calendriers, des dépendances ou une autre taille de page.
La chronologie affiche tout le graphe alors que la table contient les lignes renvoyées par son list :
un enregistrement hors de la page courante apparaît avec son libellé de planning, sans cellules de
table ni case de sélection.
Activer le planning
Ajoutez "gantt" à table.displayModes et activez explicitement table.planning. Une instance visuelle ne constitue pas une identité métier : utilisez une source stable et un périmètre comprenant l’organisation et le projet.
const gantt = {
titleColumn: "name",
startColumn: "start",
endColumn: "end",
zoom: "week" as const,
weekStartsOn: 1,
showDependencies: true,
};
const planning = {
enabled: true,
scopeId: "organization/project",
sourceId: "tasks",
scheduling: "preview" as const,
};
// Add these properties to the table configuration from the quick start.
const table = {
displayModes: ["table", "gantt", "kanban", "gallery"],
defaultDisplayMode: "gantt",
allowEdit: true,
planning,
gantt,
};Utilisez l’installation React ou Vue. Les deux éditions exposent les mêmes types de planning, calculatePlanning, resolvedPlanningSnapshot, planningTasksFromRows et createMemoryPlanningAdapter. Le moteur ne dépend d’aucun framework.
Normaliser les éléments
Une tâche possède une référence composée ref: {source, id}, un libellé, des dates civiles start/end, un parent facultatif et un calendrier facultatif. La hiérarchie et les dépendances de planification sont deux relations distinctes. Des identifiants identiques dans des sources différentes désignent des tâches différentes.
planningTasksFromRows({source, rows, getId, gantt}) lit les colonnes configurées et normalise les subRows. Il retourne {source, tasks} : incluez cette source dans le snapshot, car ses correspondances fields servent également aux modifications des formulaires et cellules. Les adaptateurs getChildren, getParent et toDate prennent en charge d’autres schémas. Transmettez les mêmes correspondances gantt au normaliseur et à la table.
L’application peut aussi construire directement les tâches normalisées. source.fields associe titre, début, fin, identifiant scalaire du parent et calendrier sans imposer de noms de colonnes. Stockez séparément les parents entre sources sous forme de références composées.
Les dates civiles suivent YYYY-MM-DD, avec une fin inclusive. Une tâche non planifiée a ses deux dates à null et reste accessible dans l’arbre. Une dépendance nécessitant des dates indisponibles bloque son recalcul. Convertissez explicitement les horodatages selon le fuseau de l’application.
Règles configurables
| Paramètre | Valeur par défaut |
|---|---|
planning.scheduling | preview |
planning.parentDates | rollup |
planning.hierarchy | true |
planning.allowDateEdit | true |
planning.allowDependencyEdit | true |
planning.allowHierarchyEdit | true |
planning.allowCrossTableDependencies | true |
planning.allowSummaryMove | true |
planning.dependencyTypes | FS, SS, FF, SF |
planning.maxCalendarSearchDays | 36600 |
gantt.zoom | week |
gantt.weekStartsOn | 1 (lundi) |
gantt.showDependencies | true |
gantt.parentColumn | non défini ; aucune hiérarchie issue des lignes |
gantt.calendarColumn | non défini ; le calendrier de la source ou du planning s’applique |
gantt.height | 480 pixels |
Les trois flags d’édition restreignent les permissions existantes. Ils ne peuvent pas accorder un droit refusé par allowEdit, canEditRow, les permissions d’une tâche ou le serveur.
preview demande une validation. manual signale les contraintes non respectées en conservant les dates des successeurs. automatic utilise le même contrat de validation et de sauvegarde atomique, puis applique sans clic de confirmation. parentDates: "independent" conserve les dates propres des parents.
Calendriers et types de liens
Les calendriers définissent les jours travaillés (0 représente dimanche) et des exceptions ouvrant ou fermant une date. L’ordre de priorité est tâche, source, puis calendrier par défaut du planning. Modifier le premier jour affiché ne change aucune date.
FS relie la fin du prédécesseur au début du successeur ; SS relie les débuts ; FF relie les fins ; SF relie le début du prédécesseur à la fin du successeur. Les fins sont inclusives dans les données et exclusives aux limites des contraintes. Par exemple, FS avec une fin vendredi et un décalage nul commence lundi dans un calendrier du lundi au vendredi. Un décalage d’un jour ouvré commence mardi.
Les décalages peuvent être positifs ou négatifs. Ils comptent par défaut les jours travaillés du successeur ; lagUnit: "calendarDays" compte les jours civils. Le moteur déplace les successeurs lorsque nécessaire et conserve leurs marges existantes. Déplacer un parent récapitulatif déplace ses descendants en conservant leurs durées travaillées ; des calendriers différents peuvent modifier les écarts calendaires. Modifiez les enfants pour changer la durée du récapitulatif.
Les liens vers soi-même, les cycles, les dépendances entre un récapitulatif et ses descendants et les cycles indirects entre groupes ou sources sont rejetés.
Charger, prévisualiser et appliquer
Fournissez actions.planning avec les actions de table :
load({scopeId, sourceId, cursor?, signal?})charge les tâches, ancêtres, liens, sources et calendriers nécessaires au-delà des filtres et de la pagination. Les pages du graphe partagent une révision. Seul un graphe complet peut être recalculé.preview({scopeId, sourceId, revision, mutations})vérifie les droits et calcule toutes les modifications sans écrire. Retournez{success: true, data: {id, revision, changes, dependencies, warnings}}et conservez la proposition derrière son identifiant opaque.apply({scopeId, sourceId, previewId, revision, idempotencyKey})revérifie la proposition, les droits et la révision, puis sauvegarde tous les éléments et relations dans une transaction. Retournez le snapshot complet actualisé uniquement après réussite de l’ensemble.
Une proposition périmée retourne code: "stale-preview" et impose un nouveau chargement et un nouvel aperçu. Les autres échecs permettent une nouvelle tentative. Une réponse réseau incertaine réutilise la même clé d’idempotence. Le serveur lie les propositions et résultats durables au contexte authentifié. N’acceptez pas de modifications arbitraires du client et ne présentez jamais un lot partiel comme une réussite.
L’adaptateur mémoire implémente ce contrat pour les exemples, avec validation à la sauvegarde et contrôle des révisions concurrentes. En production, l’application fournit les transactions durables, les droits faisant autorité et ses validations métier. Les créations, suppressions et mutations spécifiques restent à sa charge et doivent préserver le graphe et actualiser sa révision.
Édition et vues enregistrées
La chronologie utilise une barre de navigation compacte, une liste d’éléments adaptative, une grille discrète, et des barres de tâches et récapitulatives tirées des jetons de thème de l’application : les couleurs ne codent ni le statut ni les règles de planification et suivent le mode clair ou sombre. La date du jour possède un repère dans l’en-tête et une ligne verticale. Les poignées de redimensionnement apparaissent au survol ou au focus clavier et restent visibles sur écran tactile.
Le Gantt affiche un arbre dépliable, les jours non travaillés, les barres et les liens. Sa colonne de gauche porte les cellules de sélection et de titre de la table : y sélectionner alimente les actions groupées et cliquer un titre ouvre la fiche, exactement comme dans Table, Kanban et Galerie. Cliquez sur une barre pour modifier ses dates, son parent et ses dépendances. Choisissez la source, le prédécesseur, le type et le décalage dans des contrôles étiquetés accessibles au clavier. La fiche commune expose aussi Planning depuis chaque mode d’affichage.
Déplacez une barre ou ses poignées. Les flèches gauche/droite sur une barre ou poignée déplacent d’un jour ; Maj+gauche/droite de sept jours. Les éditions inline, formulaires et modifications en masse utilisent les mêmes actions de planning.
L’aperçu présente les anciennes et nouvelles dates, les changements de hiérarchie, de champs et de liens, y compris hors de la vue courante. Annuler n’appelle pas apply. Une sauvegarde invalide les instances montées dans le même périmètre ; leurs aperçus déjà ouverts conservent leur contrôle de révision.
Les vues et URL enregistrent zoom, weekStartsOn, showDependencies et anchorDate. Les calendriers, correspondances de champs et règles restent dans les données et la configuration de l’application. Les filtres modifient seulement l’affichage. Le tri conserve la hiérarchie et ordonne les éléments frères.
La chronologie virtualise les lignes et colonnes de jours dans une fenêtre navigable de 180 jours. Les contrôles précédent/suivant/aujourd’hui et le zoom permettent de parcourir les plannings longs. Les tracés apparaissent lorsque les deux extrémités sont rendues ; l’éditeur conserve les liens vers les éléments masqués.
Exemples exécutables
Dans le dépôt Table, lancez bun run gantt:dev pour React ou bun run vue:dev avec ?example=gantt pour Vue. Le ?example=rows de React exécute la variante sans adaptateur présentée plus haut. Les exemples partagent tâches, jour férié, élément non planifié et publication dans une seconde source portant un identifiant identique. Ils comprennent les vues enregistrées et un formulaire. Leurs données sont réinitialisées au rechargement.
Ouvrez directement les exemples intégrés React ou Vue. Les modifications restent dans votre session navigateur.
Consultez le contrat du moteur et le contrat de persistance du dépôt pour les détails techniques et la couverture de tests commune.