Overview
Timeline / Gantt
Business archetype for metadata-driven operational planning on a time axis (gantt): tasks as bars, milestones, dependencies, drag/resize with persistence. Rendering based on frappe-gantt (SVG, lazy-loaded).
Use cases
- Job/activity planning with start/end dates.
- Project roadmaps with milestones and task dependencies.
- Quick rescheduling via drag (move) and resize (extend/shorten) of bars.
md_props_bag: archetypes.timeline
The archetype is activated with timeline (accepted aliases: gantt, gantt-list, timeline-list).
{
"archetypes": {
"timeline": {
"advancedFilter": false,
"startDateField": "StartDate",
"endDateField": "EndDate",
"titleField": "Title",
advancedFilter: whentrueshows thewuic-filter-barfor thetimelinearchetype (at data-repeater level).startDateField(required): task start date field. If missing, the component shows a configuration warning and does not crash.endDateField: end date field. If the value on the record is empty, the task is rendered as a milestone (1-day bar).titleField: text field used as the bar label (fallback: first text column withname/title/descriin its name).progressField: numeric 0-100 field for progress (clamped; optional).dependencyField: field holding the predecessors. Two forms are accepted (see "Predecessors: csv or multiselect" below): a text column with a csv of ids ("3, 7"), or amultiple_checkcolumn (the framework multiselect). v1: dependencies are rendered as standard finish→start arrows (FS); if a task starts before its predecessor ends a (non-blocking) warning is shown.milestoneField: boolean field that marks the record as a milestone (diamond, not resizable).groupByField: optional grouping. v1: sorts tasks by group and prefixes the title with[group](frappe-gantt has no group lanes). It can point to a text column or to alookupByID— see "Grouping: text or lookup".persistMode(enum):
- immediate (default): every drag/resize persists immediately server-side (visual rollback on error);
- batch: accumulates changes and saves/discards them in bulk from the toolbar.
timeScale(enum):day,week(default),month— initial zoom, changeable from the toolbar.showTodayLine: highlights the current day (defaulttrue).clickAction(enum):none(default),detail,edit— navigation on bar click.
Predecessors: csv or multiselect
dependencyField accepts two configurations, and the component detects which one is in use:
1. Text column with a csv of ids ("3, 7"): minimal setup, no extra table. Users type ids by hand, so there is no validation or autocompletion.
2. `multiple_check` column — the framework multiselect (see Many To Many Widget): the recommended form, because in edit users pick predecessors from a list of tasks instead of typing ids.
The second one needs the standard many-to-many widget setup, with one twist: the relation is self-referencing, since the predecessors of a task are tasks themselves.
- a bridge table with two FKs to the tasks table (e.g.
wuic_timeline_task_depswithFK_TaskandFK_Predecessor); - on the main route, a virtual column (empty
mc_real_column_name,mc_is_computed = 1,voa_class = 4) of typemultiple_check, whosemc_ui_grid_routepoints back to the route itself and whosemc_ui_grid_manytomany_*fields map the two bridge FKs; - on the bridge table, both FKs configured as
lookupByIDtowards the tasks route (voa_class = 2).
That last point is the easiest to get wrong: scaffolding creates the integer FKs as number, but when the backend composes many-to-many values it casts the related column to a lookup and reads the entity it points to. If the FKs stay number, every read of the parent route fails with a NullReferenceException in ParseGridColumns.
On read, the framework exposes the column as a collection of related records: the component extracts each predecessor id from the field declared in mc_ui_grid_related_id_field, falling back to the route primary key.
The app script scripts/timeline-sample/scaffold-timeline-sample.ps1 applies exactly this schema on mssql, mysql, postgres and oracle and is a practical, reusable reference.
Grouping: text or lookup
Like predecessors, groupByField accepts two configurations and the component detects which one is in use:
1. Text column: no extra table, but the group is typed by hand — a typo creates a brand new group, which shows up as a separate row and, if its name sorts before the others, ends up at the top of the chart.
2. `lookupByID` column pointing at a dedicated lookup table (e.g. wuic_timeline_project): the recommended form, because groups become a closed set picked from a list.
With a lookup the raw column value is the id, not the name: rendering it would give [3] Title. The component resolves the label through MetadatiColonna.formatGridViewValue, the same resolver the grid uses, which knows the denormalized alias emitted by the server (<route>___<textField>__<column>) and falls back to __lookup_obj when the record hasn't round-tripped yet — so the label is correct even right after an insert.
Ordering. Rows are sorted by group and, within a group, by start date: the order is not topological, so a task can appear above its own predecessor when it belongs to a group that sorts before it.
Data rendering rules
- Record without a valid
startDateField→ excluded from the gantt, counted in the "excluded rows" warning. - Empty
endDateField→ implicit milestone (end = start). end < start(in the data or after a drag) → clamped tostart+ warning.- Persistence uses the datasource current-record flow (
syncDatawith pristine) → the__changespayload contains only the modified dates and is compatible with optimistic concurrency and audit.
Bar popup and record actions
Clicking a bar opens the popup with title, date range and progress. The actions below the text follow the permissions declared in the table metadata, so a read-only route shows a purely informational popup:
| Action | Condition | Behaviour |
|---|---|---|
| Edit | md_editable | Opens the record in the parametric-dialog, the same form as the list-grid edit. |
| New | md_insertable | Opens the insert dialog with the clicked task already preselected among the new task's predecessors. |
| Delete | md_deletable and leaf task | Asks for confirmation and deletes the record. |
On save-and-close (or after a deletion) the datasource reloads and the gantt redraws.
Why only leaves can be deleted. A task is a leaf when no other task lists it among its predecessors. Deleting a task others depend on would leave orphan dependencies — arrows pointing at an id that no longer exists — so on those tasks the button doesn't appear at all. In the Requirements analysis → Architecture design example, the button shows on Architecture design but not on Requirements analysis.
Predecessor preselection. With dependencyField on a text column the id is written straight into the csv. With the multiselect both the id list and the related-records collection are filled in, marking the entry as added: that's the same contract the lookup-editor produces when the user picks by hand, and it's what the backend uses to write into the bridge table.
The dialog is loaded with a dynamic import on first click, so the archetype chunk doesn't carry the field editors.
Events and subscriptions (host)
Events available on wuic-timeline-list:
onTimelineDataBound: emitted when the gantt is rebuilt from the datasource ({ metaInfo, tasks, skipped }).onTimelineTaskChange: emitted on drag/resize ({ task, start, end, mode }).onTimelineTaskClick: emitted on bar click ({ task }).onTimelineBatchSave: emitted after batch save ({ savedCount }).onTimelineBatchCancel: emitted after batch discard ({ revertedCount }).
<!-- import-and-mount -->
Import and mount (Angular component)
Import LazyTimelineListComponent from 'wuic-framework-lib' and mount it with the framework DataSource:
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>
EXACT names (wuic-framework-lib barrel): class LazyTimelineListComponent, selector <wuic-timeline-list-lazy>. The non-lazy TimelineListComponent is NOT exported from the barrel (it statically imports frappe-gantt): always use the lazy wrapper.
Metadata-driven config: the tag accepts ONLY the standard inputs [hardcodedRoute], [hardcodedDatasource], [hideToolbar]. The specific configuration (TimelineOptions: date fields, milestone, dependencies, etc.) is NOT an HTML input → it goes in the metadata (md_props_bag.archetypes.timeline).