Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
0e6d0df
feat(tracker): add Gantt scheduling schema (startDate + IssueRelation)
MichaelUray May 15, 2026
5c9faf7
test(model-tracker): add migrateAddStartDate jest tests
MichaelUray May 15, 2026
8641423
feat(model-tracker): add migrateAddStartDate + wire into trackerOpera…
MichaelUray May 15, 2026
8d932e8
feat(tracker): expose Issue.startDate / Milestone.startDate in UI; ti…
MichaelUray May 15, 2026
c94a5cc
fix(tracker): set explicit @Prop ranks for Milestone date fields
MichaelUray May 15, 2026
f84e497
fix(tracker-resources): EditMilestone renders Status/Start/Target in …
MichaelUray May 15, 2026
7d745f9
fix(fulltext): bump model version to 0.7.423 to match deployed worksp…
MichaelUray May 17, 2026
32cef1e
feat(tracker): register Gantt view tab (placeholder UI)
MichaelUray May 15, 2026
cd7a58a
feat(tracker-resources): add Gantt lib types
MichaelUray May 15, 2026
e9dd999
test(tracker-resources): add failing Gantt time-scale tests
MichaelUray May 15, 2026
7d5b84f
feat(tracker-resources): implement Gantt time-scale lib
MichaelUray May 15, 2026
a8e88a3
test(tracker-resources): add failing Gantt layout tests
MichaelUray May 15, 2026
1c63a84
feat(tracker-resources): implement Gantt layout lib
MichaelUray May 15, 2026
bd9a07c
feat(tracker-resources): add GanttHeader.svelte
MichaelUray May 15, 2026
dcfd750
feat(tracker-resources): add GanttBar.svelte (regular + summary claws)
MichaelUray May 15, 2026
e48b940
feat(tracker-resources): add GanttTodayMarker.svelte
MichaelUray May 15, 2026
291e524
feat(tracker-resources): add GanttMilestoneFlag.svelte
MichaelUray May 15, 2026
51c81c7
feat(tracker-resources): add GanttCanvas.svelte
MichaelUray May 15, 2026
c74ea95
feat(tracker-resources): add GanttSidebar.svelte
MichaelUray May 15, 2026
e5f4e0d
feat(tracker-resources): add GanttToolbar.svelte (zoom buttons)
MichaelUray May 15, 2026
a91cf34
feat(tracker-resources): GanttView wires Sidebar+Canvas+Toolbar with …
MichaelUray May 15, 2026
d0f6744
- layout.ts: orphan child issues (parent filtered out) now emit as roots
MichaelUray May 15, 2026
fa3b39c
Toolbar (replaces previous floating zoom):
MichaelUray May 15, 2026
83feba5
- SVG horizontal-scroll bug: GanttHeader and GanttCanvas now render at
MichaelUray May 15, 2026
7916e68
feat(tracker-resources): wire Gantt sidebar toggles into Customize-View
MichaelUray May 15, 2026
2996127
fix(tracker-resources): clip GanttSidebar inside its host
MichaelUray May 15, 2026
6131bbc
fix(tracker-resources): unified Gantt scroll container (Plane-style s…
MichaelUray May 15, 2026
5105650
feat(tracker-resources): + New issue button in Gantt toolbar
MichaelUray May 15, 2026
e67d341
chore(tracker): localize Gantt UI strings + minimize locale diffs
MichaelUray May 15, 2026
2e2da19
feat(tracker-resources): blue plus-only NewIssueHeader
MichaelUray May 15, 2026
1ddb3c5
fix(tracker-resources): resize handle no longer fights canvas pan
MichaelUray May 15, 2026
efc1cf6
feat(tracker-resources): sticky-bottom horizontal scrollbar (Plane-st…
MichaelUray May 15, 2026
ae74b0e
feat(tracker-resources): custom DOM scrollbar thumb (visible everywhere)
MichaelUray May 15, 2026
6f21093
feat(tracker-resources): hardened scrollbars + status colors + tight …
MichaelUray May 15, 2026
ad9c23d
feat(tracker-resources): status badge column + ganttShowStatus toggle
MichaelUray May 15, 2026
9be5b4e
feat(tracker-resources): Tracker UI redesign polish v27
MichaelUray May 15, 2026
1ef25b4
feat(tracker-resources): inline-add row in Gantt sidebar (PR2.1)
MichaelUray May 15, 2026
f5368f8
feat(model-tracker): register startDate + dueDate as Issue filters
MichaelUray May 15, 2026
f8f7daf
fix(tracker-resources): hide Gantt h-scrollbar when there's no overflow
MichaelUray May 15, 2026
6d74120
chore(tracker-resources): remove internal review markers from inline …
MichaelUray May 15, 2026
a0b3095
feat(tracker-resources): Gantt drag/resize state types
MichaelUray May 15, 2026
930801d
feat(tracker-resources): drag-controller reducer skeleton + idle tran…
MichaelUray May 15, 2026
cf44863
feat(tracker-resources): drag-controller — body drag transitions
MichaelUray May 15, 2026
aafc648
feat(tracker-resources): drag-controller — resize-left/right with cla…
MichaelUray May 15, 2026
9f87af1
feat(tracker-resources): scheduler.descendantsWithDates (cycle-safe BFS)
MichaelUray May 15, 2026
f2b2836
feat(tracker): i18n keys for Gantt edit-mode (set-start-date + 5 toas…
MichaelUray May 15, 2026
c1b3a24
feat(tracker-resources): local Set-start-date action for Gantt menu
MichaelUray May 15, 2026
b923569
feat(tracker-resources): GanttView activeDrag store + per-issue edit …
MichaelUray May 15, 2026
1b7a732
feat(tracker-resources): GanttBar edge-detect + resize-handle hit areas
MichaelUray May 15, 2026
4dfcf38
feat(tracker-resources): GanttCanvas forwards bar events + mounts res…
MichaelUray May 15, 2026
35f411e
feat(tracker-resources): wire drag reducer + atomic commitDrag in Gan…
MichaelUray May 15, 2026
f54ab27
feat(tracker-resources): active-drag preview geometry + tint on GanttBar
MichaelUray May 15, 2026
c01a244
feat(tracker-resources): right-click context menu on bar + sidebar row
MichaelUray May 15, 2026
76f5689
feat(tracker-resources): keyboard Tab cycle + arrow-shift for Gantt bars
MichaelUray May 15, 2026
31a4ef6
feat(tracker-resources): dim non-active rows during drag (spotlight f…
MichaelUray May 15, 2026
946b819
feat(tracker-resources): drag-from-unscheduled — drop on canvas sets …
MichaelUray May 15, 2026
3b4bd54
fix(tracker-resources): allow direct idle → drag in reducer
MichaelUray May 15, 2026
cf7ba25
feat(tracker-resources): slim Gantt context menu + fix pan-grip colli…
MichaelUray May 15, 2026
e544048
fix(tracker-resources): Gantt menu — deny-list + restore Open/Parent/…
MichaelUray May 15, 2026
e8b31b4
feat(tracker): Gantt Hierarchy submenu + EditIssue parent + header su…
MichaelUray May 15, 2026
7d22e29
feat(tracker-resources): Gantt select-first drag + confirm popup + su…
MichaelUray May 15, 2026
5db9d53
fix(tracker-resources): Gantt parent-drag truth source + canvasX offs…
MichaelUray May 15, 2026
abc9a92
chore(tracker-resources): Gantt polish — drop stale CSS, i18n ARIA la…
MichaelUray May 15, 2026
38ab080
chore(tracker-resources): remove internal review markers from inline …
MichaelUray May 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion common/scripts/version.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
"0.7.422"
"0.7.423"
49 changes: 49 additions & 0 deletions models/tracker/src/__tests__/migration.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
//
// Copyright © 2026 Hardcore Engineering Inc.
//
// Licensed under the Eclipse Public License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
//

import { DOMAIN_TASK } from '@hcengineering/model-task'
import tracker from '@hcengineering/tracker'

import { DOMAIN_TRACKER } from '../types'
import { migrateAddStartDate } from '../migration'

describe('migrateAddStartDate', () => {
it('sets startDate=null on every Issue lacking the field (DOMAIN_TASK)', async () => {
const update = jest.fn().mockResolvedValue(undefined)
const client: any = { update }

await migrateAddStartDate(client)

expect(update).toHaveBeenCalledWith(
DOMAIN_TASK,
{ _class: tracker.class.Issue, startDate: { $exists: false } },
{ startDate: null }
)
})

it('sets startDate=null on every Milestone lacking the field (DOMAIN_TRACKER)', async () => {
const update = jest.fn().mockResolvedValue(undefined)
const client: any = { update }

await migrateAddStartDate(client)

expect(update).toHaveBeenCalledWith(
DOMAIN_TRACKER,
{ _class: tracker.class.Milestone, startDate: { $exists: false } },
{ startDate: null }
)
})

it('issues exactly two update calls (one per class)', async () => {
const update = jest.fn().mockResolvedValue(undefined)
const client: any = { update }

await migrateAddStartDate(client)

expect(update).toHaveBeenCalledTimes(2)
})
})
6 changes: 5 additions & 1 deletion models/tracker/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
TClassicProjectTypeData,
TComponent,
TIssue,
TIssueRelation,
TIssueStatus,
TIssueTemplate,
TIssueTypeData,
Expand Down Expand Up @@ -195,7 +196,9 @@ function defineFilters (builder: Builder): void {
key: 'milestone',
component: view.component.ObjectFilter,
showNested: false
}
},
'startDate',
'dueDate'
],
ignoreKeys: ['number', 'estimation', 'attachedTo'],
getVisibleFilters: tracker.function.GetVisibleFilters
Expand Down Expand Up @@ -441,6 +444,7 @@ export function createModel (builder: Builder): void {
TProject,
TComponent,
TIssue,
TIssueRelation,
TIssueTemplate,
TIssueStatus,
TTypeIssuePriority,
Expand Down
20 changes: 20 additions & 0 deletions models/tracker/src/migration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ import tracker, {
} from '@hcengineering/tracker'

import { classicIssueTaskStatuses } from '.'
import { DOMAIN_TRACKER } from './types'

async function createDefaultProject (tx: TxOperations): Promise<void> {
const current = await tx.findOne(tracker.class.Project, {
Expand Down Expand Up @@ -170,6 +171,20 @@ async function migrateIdentifiers (client: MigrationClient): Promise<void> {
}
}

export async function migrateAddStartDate (client: MigrationClient): Promise<void> {
// Issues live in DOMAIN_TASK; Milestones live in DOMAIN_TRACKER.
await client.update(
DOMAIN_TASK,
{ _class: tracker.class.Issue, startDate: { $exists: false } },
{ startDate: null }
)
await client.update(
DOMAIN_TRACKER,
{ _class: tracker.class.Milestone, startDate: { $exists: false } },
{ startDate: null }
)
}

async function migrateDefaultStatuses (client: MigrationClient, logger: ModelLogger): Promise<void> {
const defaultTypeId = tracker.ids.ClassingProjectType
const typeDescriptor = tracker.descriptors.ProjectType
Expand Down Expand Up @@ -398,6 +413,11 @@ export const trackerOperation: MigrateOperation = {
state: 'migrateDefaultTypeMixins',
mode: 'upgrade',
func: migrateDefaultTypeMixins
},
{
state: 'gantt-add-startdate',
mode: 'upgrade',
func: migrateAddStartDate
}
])
},
Expand Down
31 changes: 31 additions & 0 deletions models/tracker/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,10 +58,12 @@ import time, { type ToDo } from '@hcengineering/time'
import {
type ProjectTargetPreference,
type Component,
type DependencyKind,
type Issue,
type IssueChildInfo,
type IssueParentInfo,
type IssuePriority,
type IssueRelation,
type IssueStatus,
type IssueTemplate,
type IssueTemplateChild,
Expand Down Expand Up @@ -233,6 +235,10 @@ export class TIssue extends TTask implements Issue {
@ReadOnly()
declare space: Ref<Project>

@Prop(TypeDate(DateRangeMode.DATETIME), tracker.string.IssueStartDate)
@Index(IndexKind.Indexed)
declare startDate: Timestamp | null

@Prop(TypeDate(DateRangeMode.DATETIME), tracker.string.DueDate)
declare dueDate: Timestamp | null

Expand Down Expand Up @@ -340,6 +346,28 @@ export class TTimeSpendReport extends TAttachedDoc implements TimeSpendReport {
@Prop(TypeString(), tracker.string.TimeSpendReportDescription)
description!: string
}

/**
* @public
*/
@Model(tracker.class.IssueRelation, core.class.AttachedDoc, DOMAIN_TRACKER)
@UX(tracker.string.GanttDependency, tracker.icon.Issue)
export class TIssueRelation extends TAttachedDoc implements IssueRelation {
@Prop(TypeRef(tracker.class.Issue), tracker.string.Issue)
declare attachedTo: Ref<Issue>

declare collection: 'relations'

@Prop(TypeRef(tracker.class.Issue), tracker.string.Issue)
@Index(IndexKind.Indexed)
target!: Ref<Issue>

@Prop(TypeString(), tracker.string.GanttDependency)
kind!: DependencyKind

@Prop(TypeNumber(), tracker.string.GanttLag)
lag!: number
}
/**
* @public
*/
Expand Down Expand Up @@ -389,6 +417,9 @@ export class TMilestone extends TDoc implements Milestone {
@Prop(Collection(attachment.class.Attachment), attachment.string.Attachments, { shortLabel: attachment.string.Files })
attachments?: number

@Prop(TypeDate(), tracker.string.StartDate)
startDate!: Timestamp | null

@Prop(TypeDate(), tracker.string.TargetDate)
targetDate!: Timestamp

Expand Down
100 changes: 99 additions & 1 deletion models/tracker/src/viewlets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,70 @@ export function issueConfig (
]
}

export function ganttViewOptions (): ViewOptionsModel {
// PR 2 ships a minimal read-only Gantt. Group-by + Show-colors are
// intentionally NOT advertised — the canvas does not honour them yet.
// The two sidebar-column toggles below ARE wired up to GanttSidebar.
return {
groupBy: [],
orderBy: [
['startDate', SortingOrder.Ascending],
['rank', SortingOrder.Ascending],
['dueDate', SortingOrder.Ascending]
],
other: [
{
key: 'ganttShowIssueCode',
type: 'toggle',
defaultValue: false,
actionTarget: 'display',
label: tracker.string.GanttShowIssueCode
},
{
key: 'ganttShowTitle',
type: 'toggle',
defaultValue: true,
actionTarget: 'display',
label: tracker.string.GanttShowTitle
},
{
key: 'ganttShowStatus',
type: 'toggle',
defaultValue: true,
actionTarget: 'display',
label: tracker.string.GanttShowStatus
},
{
// Default-on safety prompt: when set, dragging an issue's bar to a
// new date range shows a confirm dialog before writing the change.
// User feedback 2026-05-11: easy to misclick a bar while panning,
// and a one-click confirm prevents accidental schedule edits.
key: 'ganttConfirmMove',
type: 'toggle',
defaultValue: true,
actionTarget: 'display',
label: tracker.string.GanttConfirmMove
},
{
// Same idea but for left/right resize handles.
key: 'ganttConfirmResize',
type: 'toggle',
defaultValue: true,
actionTarget: 'display',
label: tracker.string.GanttConfirmResize
}
]
}
}

export function ganttConfig (): BuildModelKey[] {
// Minimal config — Gantt drives its own column layout.
return [
{ key: '', presenter: tracker.component.PriorityEditor, label: tracker.string.Priority, props: { kind: 'list', size: 'small' } },
{ key: '', presenter: tracker.component.IssuePresenter, label: tracker.string.Issue }
]
}

export function defineViewlets (builder: Builder): void {
builder.createDoc(
view.class.ViewletDescriptor,
Expand All @@ -224,6 +288,17 @@ export function defineViewlets (builder: Builder): void {
tracker.viewlet.Kanban
)

builder.createDoc(
view.class.ViewletDescriptor,
core.space.Model,
{
label: tracker.string.Gantt,
icon: tracker.icon.Gantt,
component: tracker.component.GanttView
},
tracker.viewlet.Gantt
)

builder.createDoc(
view.class.Viewlet,
core.space.Model,
Expand Down Expand Up @@ -501,6 +576,23 @@ export function defineViewlets (builder: Builder): void {
tracker.viewlet.IssueKanban
)

// Gantt is registered AFTER List + Kanban so List remains the default
// viewlet (ViewletSelector falls back to viewlets[0] when no preference
// is saved). Putting Gantt last avoids surprising users with an empty
// canvas on first visit.
builder.createDoc(
view.class.Viewlet,
core.space.Model,
{
attachTo: tracker.class.Issue,
descriptor: tracker.viewlet.Gantt,
viewOptions: ganttViewOptions(),
configOptions: { strict: true, hiddenKeys: ['title'] },
config: ganttConfig()
},
tracker.viewlet.IssueGantt
)

const componentListViewOptions: ViewOptionsModel = {
groupBy: ['lead', 'createdBy', 'modifiedBy'],
orderBy: [
Expand Down Expand Up @@ -663,7 +755,7 @@ export function defineViewlets (builder: Builder): void {
viewOptions: milestoneOptions,
configOptions: {
strict: true,
hiddenKeys: ['targetDate', 'label', 'description']
hiddenKeys: ['startDate', 'targetDate', 'label', 'description']
},
config: [
{
Expand All @@ -672,6 +764,12 @@ export function defineViewlets (builder: Builder): void {
},
{ key: '', presenter: tracker.component.MilestonePresenter, props: { shouldUseMargin: true } },
{ key: '', displayProps: { grow: true } },
{
key: '',
label: tracker.string.StartDate,
presenter: tracker.component.MilestoneDatePresenter,
props: { field: 'startDate' }
},
{
key: '',
label: tracker.string.TargetDate,
Expand Down
1 change: 1 addition & 0 deletions packages/importer/src/importer/importer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -591,6 +591,7 @@ export class WorkspaceImporter {
rank,
comments: issue.comments?.length ?? 0,
subIssues: issue.subdocs.length,
startDate: null,
dueDate: null,
parents: parentsInfo,
remainingTime,
Expand Down
24 changes: 23 additions & 1 deletion plugins/tracker-assets/lang/cs.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@
"NoAssignee": "Bez přiřazení",
"LastUpdated": "Poslední aktualizace",
"DueDate": "Datum splnění",
"IssueStartDate": "Datum zahájení",
"GanttDependency": "Dependency",
"GanttLag": "Lag",
"Manual": "Manuální",
"All": "Vše",
"PastWeek": "Minulý týden",
Expand Down Expand Up @@ -280,7 +283,26 @@
"UnsetParentIssue": "Odebrat nadřazený úkol",
"ForbidCreateProjectPermission": "Zakázat vytvoření projektu",
"ForbidCreateProjectPermissionDescription": "Zakazuje uživatelům vytvářet nové projekty",
"AllowCreatingIssues": "Povolit vytváření úkolů"
"AllowCreatingIssues": "Povolit vytváření úkolů",
"Day": "Day",
"Week": "Week",
"Month": "Month",
"Quarter": "Quarter",
"Gantt": "Gantt",
"GanttShowIssueCode": "Show issue code",
"GanttShowTitle": "Show title",
"GanttShowStatus": "Show status",
"GanttToday": "Today",
"GanttJumpToStart": "Jump to start",
"GanttJumpToEnd": "Jump to end",
"GanttJumpToDate": "Jump to date",
"GanttPreviousPeriod": "Previous period",
"GanttNextPeriod": "Next period",
"GanttScrollLeftToBar": "Scroll left to bar",
"GanttScrollRightToBar": "Scroll right to bar",
"GanttExpand": "Expand",
"GanttCollapse": "Collapse"

},
"status": {}
}
Loading