Overview

Timeline / Gantt

Archetype business per pianificazione operativa su asse temporale (gantt) metadata-driven: task come barre, milestone, dipendenze, drag/resize con persistenza. Rendering basato su frappe-gantt (SVG, lazy-loaded).

Use cases

  • Pianificazione commesse/attività con date inizio/fine.
  • Roadmap di progetto con milestone e dipendenze tra task.
  • Ripianificazione rapida via drag (sposta) e resize (allunga/accorcia) delle barre.

md_props_bag: archetypes.timeline

L'archetype si attiva con timeline (alias accettati: gantt, gantt-list, timeline-list).

Snippet 1JSON
{
  "archetypes": {
    "timeline": {
      "advancedFilter": false,
      "startDateField": "StartDate",
      "endDateField": "EndDate",
      "titleField": "Title",
  • advancedFilter: quando true mostra la wuic-filter-bar per l'archetype timeline (a livello data-repeater).
  • startDateField (obbligatorio): campo data di inizio del task. Se manca, il componente mostra un warning di configurazione e non crasha.
  • endDateField: campo data di fine. Se il valore sul record è vuoto, il task è reso come milestone (barra di 1 giorno).
  • titleField: campo testo usato come label della barra (fallback: prima colonna testuale con name/title/descri nel nome).
  • progressField: campo numerico 0-100 per l'avanzamento (clampato; opzionale).
  • dependencyField: campo con i predecessori. Sono accettate due forme (vedi "Predecessori: csv o multiselect" più sotto): una colonna testo con csv di id ("3, 7") oppure una colonna multiple_check (il multiselect del framework). v1: le dipendenze sono rese come frecce standard fine→inizio (FS); se un task inizia prima della fine del predecessore viene mostrato un warning (non bloccante).
  • milestoneField: campo booleano che marca il record come milestone (diamante, non ridimensionabile).
  • groupByField: raggruppamento opzionale. v1: ordina i task per gruppo e prefissa il titolo con [gruppo] (frappe-gantt non ha corsie per gruppo). Può puntare a una colonna testo o a un lookupByID — vedi "Raggruppamento: testo o lookup".
  • persistMode (enum):

- immediate (default): ogni drag/resize persiste subito lato server (rollback visivo in caso di errore);

- batch: accumula le modifiche e salva/annulla in blocco dalla toolbar.

  • timeScale (enum): day, week (default), month — zoom iniziale, cambiabile dalla toolbar.
  • showTodayLine: evidenzia il giorno corrente (default true).
  • clickAction (enum): none (default), detail, edit — navigazione al click sulla barra.

Predecessori: csv o multiselect

dependencyField accetta due configurazioni, e il componente riconosce da solo quale è in uso:

1. Colonna testo con csv di id ("3, 7"): configurazione minima, nessuna tabella aggiuntiva. L'utente digita gli id a mano, quindi non c'è validazione né autocompletamento.

2. Colonna `multiple_check` — il multiselect del framework (vedi Widget Many To Many): è la forma consigliata, perché in edit l'utente sceglie i predecessori da un elenco di task invece di scrivere id.

Per la seconda serve il corredo standard del widget many-to-many, con una particolarità: la relazione è auto-referenziale, perché i predecessori di un task sono a loro volta task.

  • una tabella ponte con due FK verso la tabella dei task (es. wuic_timeline_task_deps con FK_Task e FK_Predecessor);
  • sulla route principale una colonna virtuale (mc_real_column_name vuoto, mc_is_computed = 1, voa_class = 4) di tipo multiple_check, con mc_ui_grid_route che punta alla route stessa e mc_ui_grid_manytomany_* che mappano le due FK della ponte;
  • sulla tabella ponte, entrambe le FK configurate come lookupByID verso la route dei task (voa_class = 2).

Quest'ultimo punto è il più facile da sbagliare: lo scaffolding crea le FK intere come number, ma il backend, quando compone i valori many-to-many, converte la colonna correlata a lookup e ne legge l'entità puntata. Se le FK restano number, ogni lettura della route padre fallisce con un NullReferenceException in ParseGridColumns.

In lettura il framework espone la colonna come collezione di record correlati: il componente estrae l'id di ciascun predecessore dal campo dichiarato in mc_ui_grid_related_id_field, con fallback sulla chiave primaria della route.

Lo script scripts/timeline-sample/scaffold-timeline-sample.ps1 dell'app applica esattamente questo schema su mssql, mysql, postgres e oracle ed è un riferimento pratico riutilizzabile.

Raggruppamento: testo o lookup

Come per i predecessori, groupByField accetta due configurazioni e il componente riconosce da sola quale è in uso:

1. Colonna testo: nessuna tabella aggiuntiva, ma il gruppo è digitato a mano — un refuso crea un gruppo nuovo, che compare come riga separata e, se il nome precede alfabeticamente gli altri, finisce in cima al grafico.

2. Colonna `lookupByID` verso un'anagrafica dedicata (es. wuic_timeline_project): è la forma consigliata, perché i gruppi diventano un insieme chiuso scelto da elenco.

Con il lookup il valore grezzo della colonna è l'id, non il nome: mostrarlo darebbe [3] Titolo. Il componente risolve l'etichetta con MetadatiColonna.formatGridViewValue, lo stesso risolutore usato dalla grid, che conosce l'alias denormalizzato emesso dal server (<route>___<textField>__<colonna>) e ricade su __lookup_obj quando il record non ha ancora fatto round-trip — così l'etichetta è corretta anche subito dopo un inserimento.

Ordinamento. Le righe sono ordinate per gruppo e, a parità di gruppo, per data di inizio: l'ordine non è topologico, quindi un task può comparire sopra il proprio predecessore se appartiene a un gruppo che lo precede alfabeticamente.

Regole di rendering dei dati

  • Record senza startDateField valido → escluso dal gantt, conteggiato nel warning "righe escluse".
  • endDateField vuoto → milestone implicita (end = start).
  • end < start (nel dato o dopo un drag) → clamp a start + warning.
  • La persistenza usa il flusso current-record del datasource (syncData con pristine) → il payload __changes contiene solo le date modificate ed è compatibile con concorrenza ottimistica e audit.

Popup della barra e azioni sul record

Cliccando una barra si apre il popup con titolo, intervallo di date e avanzamento. Le azioni disponibili sotto al testo seguono i permessi dichiarati nei metadata della tabella, così una route in sola lettura mostra un popup puramente informativo:

AzioneCondizioneComportamento
Modificamd_editableApre il record nel parametric-dialog, lo stesso form dell'edit da list-grid.
Nuovomd_insertableApre il dialog di inserimento con il task cliccato già preselezionato fra i predecessori del nuovo task.
Eliminamd_deletable e task fogliaChiede conferma ed elimina il record.

Alla chiusura con salvataggio (o dopo l'eliminazione) il datasource ricarica e il gantt si ridisegna.

Perché solo le foglie si eliminano. Un task è foglia quando nessun altro task lo elenca fra i propri predecessori. Eliminare un task da cui altri dipendono lascerebbe dipendenze orfane — frecce verso un id che non esiste più — quindi su quei task il pulsante non compare affatto. Nell'esempio Analisi requisiti → Design architettura, il pulsante appare su Design architettura ma non su Analisi requisiti.

Preselezione del predecessore. Con dependencyField su colonna testo viene scritto direttamente l'id nel csv. Con la multiselect vengono valorizzati sia l'elenco di id sia la collezione di record correlati, marcando la voce come aggiunta: è lo stesso contratto che il lookup-editor produce quando l'utente sceglie a mano, ed è ciò che il backend usa per scrivere nella tabella ponte.

Il dialog viene caricato con un import dinamico al primo click, così il chunk dell'archetype non si porta dietro i field editor.

Eventi e subscriptions (host)

Eventi disponibili su wuic-timeline-list:

  • onTimelineDataBound: emesso quando il gantt viene ricostruito dal datasource ({ metaInfo, tasks, skipped }).
  • onTimelineTaskChange: emesso su drag/resize ({ task, start, end, mode }).
  • onTimelineTaskClick: emesso su click barra ({ task }).
  • onTimelineBatchSave: emesso dopo salvataggio batch ({ savedCount }).
  • onTimelineBatchCancel: emesso dopo annullamento batch ({ revertedCount }).

<!-- import-and-mount -->

Import e mount (componente Angular)

Importa LazyTimelineListComponent da 'wuic-framework-lib' e montalo col DataSource del framework:

Snippet 2ts
import { DataSourceComponent, LazyTimelineListComponent } from 'wuic-framework-lib';

@Component({
  selector: 'app-esempio',
  imports: [DataSourceComponent, LazyTimelineListComponent],
  template: `
    <wuic-data-source #ds [hardcodedRoute]="'<route>'" [autoload]="true"></wuic-data-source>

Nomi ESATTI (barrel wuic-framework-lib): classe LazyTimelineListComponent, selector <wuic-timeline-list-lazy>. Il componente non-lazy TimelineListComponent NON è esportato dal barrel (importa staticamente frappe-gantt): usa sempre il lazy wrapper.

Config metadata-driven: il tag accetta SOLO gli input standard [hardcodedRoute], [hardcodedDatasource], [hideToolbar]. La configurazione specifica (TimelineOptions: campi data, milestone, dipendenze, ecc.) NON è un input HTML → va nei metadata (md_props_bag.archetypes.timeline).