Skip to content

feat(tracker): Gantt chart for the issue tracker - #10992

Open
MichaelUray wants to merge 3 commits into
hcengineering:developfrom
MichaelUray:feat/gantt-consolidated
Open

feat(tracker): Gantt chart for the issue tracker#10992
MichaelUray wants to merge 3 commits into
hcengineering:developfrom
MichaelUray:feat/gantt-consolidated

Conversation

@MichaelUray

@MichaelUray MichaelUray commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

The search/filter rework this built on has merged (#10998), so this is now a standalone Gantt PR on current develop.

What it adds

  • Read-only viewlet: time-scale canvas with day/week/month/quarter zoom, sidebar columns, today marker and jump-to-start/end/date navigation.
  • Editable bars: drag to move, resize handles for start/due, drag an unscheduled row to schedule it, with optional confirm prompts and permission/conflict handling on commit.
  • Dependencies: FS/SS/FF/SF relations with lag, a dependency editor and arrows, cascade shifting with cycle detection, and critical-path plus slack.
  • Visual polish & coloring: bar labels, quick-info popover, milestone and deadline markers, non-working-day shading, shortcut help; bar coloring by status/priority/assignee/component/milestone with overdue and blocked overlays and sub-issue progress fill.
  • Tiers: undo/redo, saved views, bulk select and bulk drag, auto-scheduling with manual pinning; row virtualization for large projects; a read-only mobile layout, tree view with hierarchy breadcrumbs and a predecessor column.
  • Export & fullscreen: PNG and PDF export plus a fullscreen toggle, confined to the Gantt toolbar.
  • Dependency-shift notification: after a dependency cascade commits, a server-side trigger notifies affected issue collaborators, falling back to the assignee when no Collaborator records exist. It derives the sender, issue metadata, recipient spaces, and same-project issue set server-side rather than trusting client-supplied notification data.

The engine package

Introduces @hcengineering/gantt, a runtime-dependency-free package for the domain-neutral part of the Gantt library — time-scale and working-day calculations, zoom and viewport primitives, drag/pointer state, responsive toolbar helpers, and the neutral GanttItem, GanttDependency, and WorkingCalendar contracts. Tracker-specific scheduling, layout, critical-path and dependency-routing logic remains in the Tracker adapter; the Svelte UI, model schema, Huly actions, notifications, persistence and sanity coverage remain in their corresponding Tracker packages.

Data model and migrations

Adds optional scheduling fields to Tracker documents, including issue deadlines and scheduling mode, project working-day configuration, and component/milestone colors. It also registers the dependency-shift request and notification documents and includes idempotent upgrades for Gantt view options and legacy relation-activity records. Existing issues and projects retain their previous behaviour because the new scheduling fields are optional.

New runtime dependencies

In plugins/tracker-resources, both MIT: @tanstack/svelte-virtual (row virtualization) and jspdf (PDF export). Export re-renders the chart from the data model into an SVG rather than capturing the DOM; the PDF is paginated into bounded pages so large charts stay within the browser canvas and jsPDF size limits.

Locales

en, de and ru carry the full set of new strings, and a unit test enforces ICU placeholder parity across all locale files. Other locales fall back to English at runtime.

Testing

Full CI is green on a fork mirror of this exact head (02933ee0f0) — build, svelte-check, unit test, formatting, docker-build, uitest, uitest-pg, uitest-workspaces, uitest-qms. Fork-CI run: https://github.com/MichaelUray/huly-platform/actions/runs/31542585071

One small cross-cutting change: GanttView is a third writer to the shared result-count store from the search rework, so it uses the owner-token gate (claim/release like List and Kanban); the gate now resets to the pending sentinel on claim, so switching viewlets never shows a stale count.

@ArtyomSavchenko

Copy link
Copy Markdown
Member

Hi @MichaelUray
Thank you for your contribution.
I have the following suggestions:
1. Split out the search/filter rework into its own PR
The one thing that stands out as not Gantt infrastructure is the search/filter overhaul:

packages/ui: SearchInputAdvanced (field-prefixed field:value search), HighlightedText
foundations/server/packages/elastic: the new query_string branch
plugins/view-resources: inline filter chips + overflow popover, query-builder, empty state

2. A separate package for Gantt engine
I think that Gantt engine(gantt/lib) is a good candidate to live as its own reusable package, with the tracker integration staying in tracker-resources. I think we could then reuse it elsewhere — for cards, for example.

@MichaelUray

Copy link
Copy Markdown
Contributor Author

Understood. I'll split the shared search/filter rework into a separate PR.

For the Gantt code, I'll extract a tracker-agnostic engine package with generic item, dependency, scheduling, layout and viewport interfaces. Tracker-specific adapters, Huly actions, notifications and the Svelte integration will remain in tracker-resources.

I'll move this PR back to Draft while restructuring it and update it once both boundaries build and test independently.

Builds on the merged search/filter rework (hcengineering#10998). Introduces @hcengineering/gantt, a dependency-free scheduling/zoom/drag/viewport engine package (with a neutral GanttDependency contract), and wires the tracker-specific adapter, Svelte UI, model schema, dependency-shift notifications and sanity coverage in tracker-resources.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
@MichaelUray
MichaelUray force-pushed the feat/gantt-consolidated branch from 42c196d to 02933ee Compare August 11, 2026 23:14
@MichaelUray
MichaelUray marked this pull request as ready for review August 11, 2026 23:16
Comment thread packages/gantt/src/confirm-gate.ts Outdated
Comment thread plugins/tracker/src/index.ts Outdated
Comment thread plugins/tracker-resources/src/components/gantt/lib/sidebar-sort.ts Outdated
MichaelUray added a commit to MichaelUray/huly-platform that referenced this pull request Aug 12, 2026
…r view

Follow-up to review feedback on hcengineering#10992:

- Confirm-gate: the module-global flag is replaced by a createConfirmGate()
  factory - one instance per mounted GanttView, so two Gantt views mounted at
  once in the same JS context no longer block each other's pointer input. A
  two-instance independence test guards the regression.

- Sidebar sorting: the stale deadline cast is dropped and issue.deadline is
  read directly (the field was added to the Issue model in this PR).

- HR holidays: the per-project WorkingDaysConfig.holidays field duplicated the
  workspace-wide hr.class.PublicHoliday concept with no link and could drift
  from the real HR calendar. The field is removed; the Gantt adapter now
  sources holidays from HR via a LiveQuery (union across all departments,
  deduplicated, resolved to UTC midnight) and merges them with the per-project
  weekday mask into the engine's neutral WorkingCalendar. The soft dependency
  targets only the hr declaration package and is guarded by hierarchy.hasClass
  - deployments without the HR model fall back to "no holidays". The gantt
  engine package stays domain-neutral and functionally unchanged.

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
MichaelUray added a commit to MichaelUray/huly-platform that referenced this pull request Aug 12, 2026
…r view

Follow-up to review feedback on hcengineering#10992:

- Confirm-gate: the module-global flag is replaced by a createConfirmGate()
  factory - one instance per mounted GanttView, so two Gantt views mounted at
  once in the same JS context no longer block each other's pointer input. A
  two-instance independence test guards the regression.

- Sidebar sorting: the stale deadline cast is dropped and issue.deadline is
  read directly (the field was added to the Issue model in this PR).

- HR holidays: the per-project WorkingDaysConfig.holidays field duplicated the
  workspace-wide hr.class.PublicHoliday concept and could drift from the real
  HR calendar. It is removed. The Gantt now sources holidays from HR with the
  same semantics HR itself uses (hr-resources ScheduleView): a project's
  holidays are those of a chosen department plus all of its ancestors up to the
  company root (hr.ids.Head). WorkingDaysConfig gains an optional
  holidayDepartment; with none selected the calendar falls back to the root
  (company-wide) holidays, a deleted/invalid reference fails safe to the root,
  and never to a union across departments. The ancestor walk has cycle and
  missing-parent protection.

  The calendar wiring is extracted into a CalendarStateMachine
  (lib/calendar-state.ts) that carries a monotonic generation counter, bumped
  synchronously on every project switch. Calendar-dependent mutations (drag,
  resize, cascade, auto-schedule) stay disabled until both the project config
  and the holidays have loaded, and every ops.commit() re-checks the captured
  mutation ticket against the current generation and target space, so a
  mutation started under one project can never persist after a switch to
  another. Plain display keeps working throughout. The manual date-picker /
  SetDueDate path does no calendar arithmetic and is intentionally not gated.

  A small standalone WorkingDaysEditor (hosted in the project create/edit
  dialog, behind the existing project-edit permission) lets an owner enable the
  mode, pick weekdays, and choose the HR department (default: Company-wide),
  persisting via TxOperations.updateDoc / $unset. At least one working day must
  stay active.

  This is a model-optional runtime integration: @hcengineering/hr is a static
  package import for the declaration types only; when a deployment ships without
  the HR model the class is absent from the hierarchy and the calendar resolves
  to an empty holiday set. The gantt engine package stays domain-neutral (no
  @hcengineering imports, no runtime deps).

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
MichaelUray added a commit to MichaelUray/huly-platform that referenced this pull request Aug 12, 2026
…r view

Follow-up to review feedback on hcengineering#10992:

- Confirm-gate: the module-global flag is replaced by a createConfirmGate()
  factory - one instance per mounted GanttView, so two Gantt views mounted at
  once in the same JS context no longer block each other's pointer input. A
  two-instance independence test guards the regression.

- Sidebar sorting: the stale deadline cast is dropped and issue.deadline is
  read directly (the field was added to the Issue model in this PR).

- HR holidays: the per-project WorkingDaysConfig.holidays field duplicated the
  workspace-wide hr.class.PublicHoliday concept and could drift from the real
  HR calendar. It is removed. The Gantt now sources holidays from HR with the
  same semantics HR itself uses (hr-resources ScheduleView): a project's
  holidays are those of a chosen department plus all of its ancestors up to the
  company root (hr.ids.Head). WorkingDaysConfig gains an optional
  holidayDepartment; with none selected the calendar falls back to the root
  (company-wide) holidays, a deleted/invalid reference fails safe to the root,
  and never to a union across departments. The ancestor walk has cycle and
  missing-parent protection.

  The calendar wiring is extracted into a CalendarStateMachine
  (lib/calendar-state.ts) that carries a monotonic generation counter, bumped
  synchronously on every project switch. Calendar-dependent mutations (drag,
  resize, cascade, auto-schedule) stay disabled until both the project config
  and the holidays have loaded, and every ops.commit() re-checks the captured
  mutation ticket against the current generation and target space, so a
  mutation started under one project can never persist after a switch to
  another. Plain display keeps working throughout. The manual date-picker /
  SetDueDate path does no calendar arithmetic and is intentionally not gated.

  A small standalone WorkingDaysEditor (hosted in the project create/edit
  dialog, behind the existing project-edit permission) lets an owner enable the
  mode, pick weekdays, and choose the HR department (default: Company-wide),
  persisting via TxOperations.updateDoc / $unset. At least one working day must
  stay active.

  This is a model-optional runtime integration: @hcengineering/hr is a static
  package import for the declaration types only; when a deployment ships without
  the HR model the class is absent from the hierarchy and the calendar resolves
  to an empty holiday set. The gantt engine package stays domain-neutral (no
  @hcengineering imports, no runtime deps).

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
MichaelUray added a commit to MichaelUray/huly-platform that referenced this pull request Aug 12, 2026
…r view

Follow-up to review feedback on hcengineering#10992:

- Confirm-gate: the module-global flag is replaced by a createConfirmGate()
  factory - one instance per mounted GanttView, so two Gantt views mounted at
  once in the same JS context no longer block each other's pointer input. A
  two-instance independence test guards the regression.

- Sidebar sorting: the stale deadline cast is dropped and issue.deadline is
  read directly (the field was added to the Issue model in this PR).

- HR holidays: the per-project WorkingDaysConfig.holidays field duplicated the
  workspace-wide hr.class.PublicHoliday concept and could drift from the real
  HR calendar. It is removed. The Gantt now sources holidays from HR with the
  same semantics HR itself uses (hr-resources ScheduleView): a project's
  holidays are those of a chosen department plus all of its ancestors up to the
  company root (hr.ids.Head). WorkingDaysConfig gains an optional
  holidayDepartment; with none selected the calendar falls back to the root
  (company-wide) holidays, a deleted/invalid reference fails safe to the root,
  and never to a union across departments. The ancestor walk has cycle and
  missing-parent protection.

  The calendar wiring is extracted into a CalendarStateMachine
  (lib/calendar-state.ts) that carries a monotonic generation counter, bumped
  synchronously on every project switch. Calendar-dependent mutations (drag,
  resize, cascade, auto-schedule) stay disabled until both the project config
  and the holidays have loaded, and every ops.commit() re-checks the captured
  mutation ticket against the current generation and target space, so a
  mutation started under one project can never persist after a switch to
  another. The all-projects view has no single project calendar, so
  calendar-dependent mutations are read-only there; display and the manual
  date-picker / SetDueDate path (which does no calendar arithmetic and is
  intentionally not gated) stay available.

  A small standalone WorkingDaysEditor (hosted in the project create/edit
  dialog, behind the existing project-edit permission) lets an owner enable the
  mode, pick weekdays, and choose the HR department (default: Company-wide),
  persisting via TxOperations.updateDoc / $unset. At least one working day must
  stay active.

  This is a model-optional runtime integration: @hcengineering/hr is a static
  package import for the declaration types only; when a deployment ships without
  the HR model the class is absent from the hierarchy and the calendar resolves
  to an empty holiday set. The gantt engine package stays domain-neutral (no
  @hcengineering imports, no runtime deps).

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
MichaelUray added a commit to MichaelUray/huly-platform that referenced this pull request Aug 12, 2026
…r view

Follow-up to review feedback on hcengineering#10992:

- Confirm-gate: the module-global flag is replaced by a createConfirmGate()
  factory - one instance per mounted GanttView, so two Gantt views mounted at
  once in the same JS context no longer block each other's pointer input. A
  two-instance independence test guards the regression.

- Sidebar sorting: the stale deadline cast is dropped and issue.deadline is
  read directly (the field was added to the Issue model in this PR).

- HR holidays: the per-project WorkingDaysConfig.holidays field duplicated the
  workspace-wide hr.class.PublicHoliday concept and could drift from the real
  HR calendar. It is removed. WorkingDaysConfig now carries only the weekday
  mask and an optional holidayDepartment. The tracker adapter resolves holidays
  for the selected department plus all ancestors up to hr.ids.Head, matching
  HR's existing inheritance semantics (hr-resources ScheduleView). A missing or
  invalid reference falls back to company-wide (root) holidays; unrelated
  departments are never included. The ancestor walk is cycle/missing-parent
  safe.

  A small WorkingDaysEditor in the existing project create/edit dialog lets
  users with the existing project permission enable the mode, choose weekdays,
  and select an HR department (default: Company-wide). It persists through the
  existing project create/update transaction paths (createDoc on create,
  client.update on edit) and uses $unset when the mode is disabled. At least
  one working day must stay active.

  Calendar loading is explicit via a CalendarStateMachine: project-scoped drag,
  resize, cascade, keyboard-shift and auto-schedule mutations stay disabled
  until both the project config and the holiday data have loaded. A monotonic
  generation ticket (bumped synchronously on project switch) is re-checked
  immediately before every calendar-dependent date commit, so work started in
  one project can never persist after a switch to another. The all-projects
  view has no single project calendar, so calendar-dependent edits are
  read-only there; display, selection, the context menu and the manual
  date-picker (which does no calendar arithmetic and takes no mutation ticket)
  stay available.

  This depends on the lightweight @hcengineering/hr model package, but not on
  hr-resources; the tracker adapter uses hr.class.* and hr.ids.Head at runtime,
  and actual HR model presence is checked through the hierarchy - without the
  HR model, holidays resolve to an empty set. The @hcengineering/gantt engine
  package stays domain-neutral (no @hcengineering imports, no runtime deps).

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
MichaelUray added a commit to MichaelUray/huly-platform that referenced this pull request Aug 12, 2026
…r view

Follow-up to review feedback on hcengineering#10992:

- Confirm-gate: the module-global flag is replaced by a createConfirmGate()
  factory - one instance per mounted GanttView, so two Gantt views mounted at
  once in the same JS context no longer block each other's pointer input. A
  two-instance independence test guards the regression.

- Sidebar sorting: the stale deadline cast is dropped and issue.deadline is
  read directly (the field was added to the Issue model in this PR).

- HR holidays: the per-project WorkingDaysConfig.holidays field duplicated the
  workspace-wide hr.class.PublicHoliday concept and could drift from the real
  HR calendar. It is removed. WorkingDaysConfig now carries only the weekday
  mask and an optional holidayDepartment. The tracker adapter resolves holidays
  for the selected department plus all ancestors up to hr.ids.Head, matching
  HR's existing inheritance semantics (hr-resources ScheduleView). A missing or
  invalid reference falls back to company-wide (root) holidays; unrelated
  departments are never included. The ancestor walk is cycle/missing-parent
  safe.

  A small WorkingDaysEditor in the existing project create/edit dialog lets
  users with the existing project permission enable the mode, choose weekdays,
  and select an HR department (default: Company-wide). It persists through the
  existing project create/update transaction paths (createDoc on create,
  client.update on edit) and uses $unset when the mode is disabled. At least
  one working day must stay active.

  Calendar loading is explicit via a CalendarStateMachine: project-scoped drag,
  resize, cascade, keyboard-shift and auto-schedule mutations stay disabled
  until both the project config and the holiday data have loaded. A monotonic
  generation ticket (bumped synchronously on project switch) is re-checked
  immediately before every calendar-dependent date commit, so work started in
  one project can never persist after a switch to another. The all-projects
  view has no single project calendar, so calendar-dependent edits are
  read-only there; display, selection, the context menu and the manual
  date-picker (which does no calendar arithmetic and takes no mutation ticket)
  stay available.

  This depends on the lightweight @hcengineering/hr model package, but not on
  hr-resources; the tracker adapter uses hr.class.* and hr.ids.Head at runtime,
  and actual HR model presence is checked through the hierarchy - without the
  HR model, holidays resolve to an empty set. The @hcengineering/gantt engine
  package stays domain-neutral (no @hcengineering imports, no runtime deps).

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
…r view

Follow-up to review feedback on hcengineering#10992:

- Confirm-gate: the module-global flag is replaced by a createConfirmGate()
  factory - one instance per mounted GanttView, so two Gantt views mounted at
  once in the same JS context no longer block each other's pointer input. A
  two-instance independence test guards the regression.

- Sidebar sorting: the stale deadline cast is dropped and issue.deadline is
  read directly (the field was added to the Issue model in this PR).

- HR holidays: the per-project WorkingDaysConfig.holidays field duplicated the
  workspace-wide hr.class.PublicHoliday concept and could drift from the real
  HR calendar. It is removed. WorkingDaysConfig now carries only the weekday
  mask and an optional holidayDepartment. The tracker adapter resolves holidays
  for the selected department plus all ancestors up to hr.ids.Head, matching
  HR's existing inheritance semantics (hr-resources ScheduleView). A missing or
  invalid reference falls back to company-wide (root) holidays; unrelated
  departments are never included. The ancestor walk is cycle/missing-parent
  safe.

  A small WorkingDaysEditor in the existing project create/edit dialog lets
  users with the existing project permission enable the mode, choose weekdays,
  and select an HR department (default: Company-wide). It persists through the
  existing project create/update transaction paths (createDoc on create,
  client.update on edit) and uses $unset when the mode is disabled. At least
  one working day must stay active.

  Calendar loading is explicit via a CalendarStateMachine: project-scoped drag,
  resize, cascade, keyboard-shift and auto-schedule mutations stay disabled
  until both the project config and the holiday data have loaded. A monotonic
  generation ticket (bumped synchronously on project switch) is re-checked
  immediately before every calendar-dependent date commit, so work started in
  one project can never persist after a switch to another. The all-projects
  view has no single project calendar, so calendar-dependent edits are
  read-only there; display, selection, the context menu and the manual
  date-picker (which does no calendar arithmetic and takes no mutation ticket)
  stay available.

  This depends on the lightweight @hcengineering/hr model package, but not on
  hr-resources; the tracker adapter uses hr.class.* and hr.ids.Head at runtime,
  and actual HR model presence is checked through the hierarchy - without the
  HR model, holidays resolve to an empty set. The @hcengineering/gantt engine
  package stays domain-neutral (no @hcengineering imports, no runtime deps).

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
Comment thread plugins/tracker-resources/src/components/gantt/lib/scheduler.ts Outdated
The finish-to-start gap-preservation floor in the cascade scheduler mixed
two time measures: the FS snap anchor is working-calendar aware, but the
floor added a raw calendar-millisecond start-side delta. With an active
working calendar the two diverge, so dragging a predecessor over a weekend
over-shifted the successor across non-working days (a Fri->Mon body drag is
3 calendar days but only 1 working day, and could even land a successor on
a Saturday).

Measure the floor on the DUE side in working days instead: translate the
predecessor's shift via the new workingDayDelta helper and advance the
floor base with addWorkingDays over the calendar. A day-granular resize
gate (utcMidnight comparison, not a raw === 0 test) arms the floor only on
a start-DAY move, so a pure due-resize stays a pure snap even when the
stored issue carries a time-of-day on the cascade-commit path. The floor
base is normalized via utcMidnight(targetAnchor). The legacy path
(cfg === undefined) stays byte-identical to today's raw-ms floor. A
left-resize in working-days mode now correctly no-ops the floor (the due,
and thus the FS gap, did not move) instead of shifting on the raw start
delta.

Engine: add workingDayDelta (signed, half-open from-exclusive/to-inclusive
working-day step count) and export the previously-private utcMidnight from
packages/gantt/src/working-days.ts. The engine stays domain-neutral (no
@hcengineering imports, empty dependencies).

Adds engine and adapter test coverage: workingDayDelta cases and the full
FS floor matrix (right-resize no-push, weekend-spanner body drag, floor
wins over snap, holiday in the floor path, floor-dominant multi-hop chain,
negative delta, legacy no-cfg, SS pure-snap, raw time-of-day right-resize,
direct left-resize).

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants