Show a project with a vertical timeline of assignments. Supports inline status changes, duration editing, dependency management, and tracks who completed each task.
List / Timeline / Calendar tabs
The page has three views — the vertical task list, the embedded
ProjectGanttLive Timeline, and the embedded ProjectCalendarLive month
calendar — toggled by tabs under the shared header. Switching is instant (an
assign flip) and each nested LV is lazily mounted on first open, then kept
(so the gantt's zoom/expand and the calendar's month navigation survive
switching back). The tabs render in every mount context, embedded
live_render renders included (only templates stay list-only). Note this
means the Timeline/Calendar tabs are nested live_renders, which are
themselves nested LVs when the show page is embedded — deliberate,
server-rendered, so both views show even before any JS loads.
The page is a set of TOP-LEVEL tabs — Tasks (List / Board / Timeline / Calendar behind its own, subordinate strip), one per enabled extension that contributes a tab (Events, Whiteboards, …) and Comments (the project's own thread, switched by the Discussions extension). Each stands alone in an otherwise empty project, so a project can be only a whiteboard or only a discussion. The header stays: it is the project, not a tab. Templates keep the Tasks tab alone, without the strip.
Keeping the URL in sync (/tasks/board, /whiteboards, /comments) is
optional and off by default: it's only on when @tab_url_sync? is true,
which the router-mounted standalone admin page sets (so its deep-linking
keeps working) and an embed can opt into via session["tab_url_sync"]. When
on, the strip carries the active tab's canonical address in data-url and
core's PkUrlMirror hook REPLACES the browser's URL with it on every
render — deep links / copy / reload land on the right tab, while
back/forward return to the previous page. Deliberately no per-tab history
entries: they'd need this LV to export handle_params/3 (which would block
live_render embedding), and LiveView's popstate handler treats foreign
pushState entries as live navigation — remounting and crashing on the
missing callback. With sync off the tabs still switch fully — they just
never touch the host page's URL.
Summary
Functions
The page's identity controls — the workflow-status picker and the ⋮ menu
(Edit, Members, Files, Activity, Set health, Archive). On the standalone
admin page core renders this in the site header next to the project's
name (page_toolbar: {__MODULE__, :header_toolbar}, the LiveView's own
assigns, events land here as usual); an embed has no site header, so its
body renders the same component under its h1. Never both — the form id
is unique either way.
The canonical address of a tab — what the page hands core's PkUrlMirror
so the browser's URL follows a switch (router mounts only). The task
views live under /tasks (/tasks/board|timeline|calendar), Comments at
/comments, a contributed extension tab at /<its key>; an unknown tab
(unreachable after resolution) falls back to the bare project page.
The TOP-LEVEL tab a view belongs to: the four task views (:list,
:board, :gantt, :calendar) are :tasks; everything else — a
contributed extension tab ("ext:<ext>:<tab>"), :comments — is itself.
Functions
The page's identity controls — the workflow-status picker and the ⋮ menu
(Edit, Members, Files, Activity, Set health, Archive). On the standalone
admin page core renders this in the site header next to the project's
name (page_toolbar: {__MODULE__, :header_toolbar}, the LiveView's own
assigns, events land here as usual); an embed has no site header, so its
body renders the same component under its h1. Never both — the form id
is unique either way.
The canonical address of a tab — what the page hands core's PkUrlMirror
so the browser's URL follows a switch (router mounts only). The task
views live under /tasks (/tasks/board|timeline|calendar), Comments at
/comments, a contributed extension tab at /<its key>; an unknown tab
(unreachable after resolution) falls back to the bare project page.
The TOP-LEVEL tab a view belongs to: the four task views (:list,
:board, :gantt, :calendar) are :tasks; everything else — a
contributed extension tab ("ext:<ext>:<tab>"), :comments — is itself.