feat(tracker): add Gantt scheduling schema (startDate + IssueRelation)

Schema-only foundation for the upcoming Gantt-chart view in tracker.
No UI in this PR.

Changes:
- Issue.startDate: Timestamp | null (interface + IssueDraft + @Prop with @Index)
- Milestone.startDate: Timestamp | null (interface + @Prop, reusing the
  existing tracker.string.StartDate IntlString)
- New DependencyKind type ('finish-to-start' | 'start-to-start' |
  'finish-to-finish' | 'start-to-finish')
- New IssueRelation AttachedDoc class with kind: DependencyKind, signed
  lag: number — registered in models/tracker via TIssueRelation
- 7 new IntlString keys: IssueStartDate, GanttDependency,
  GanttDependency{FinishToStart,StartToStart,FinishToFinish,StartToFinish},
  GanttLag — all 13 locales updated
- Cross-plugin literal updates in importer + github sync to satisfy the new
  required Issue.startDate / Milestone.startDate fields:
  - packages/importer/src/importer/importer.ts: AttachedData<Issue> literal
  - services/github/pod-github/src/sync/issueBase.ts: 'startDate' added to
    GithubIssueData Omit list (github sync does not own scheduling)
  - services/github/pod-github/src/sync/issues.ts + pullrequests.ts:
    AttachedData<Issue|GithubPullRequest> literals

Out of scope (deferred to follow-up PRs):
- UI for Gantt view, drag/resize, dependency editor, critical path
- blockedBy → IssueRelation migration (ships atomically with the writer
  redirect in the dependency-UI PR)
- LinkIssues permission (tracker uses forbid-style permissions; needs
  maintainer discussion)
- Activity-feed wiring for IssueRelation (needs a producer to test against)
- IssueTemplate.startDate (template propagation semantics undecided)

Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
This commit is contained in:
Michael Uray
2026-05-18 19:37:45 +00:00
parent 18ef71bc04
commit 0e6d0dfc7f
24 changed files with 176 additions and 0 deletions
+2
View File
@@ -39,6 +39,7 @@ import {
TClassicProjectTypeData,
TComponent,
TIssue,
TIssueRelation,
TIssueStatus,
TIssueTemplate,
TIssueTypeData,
@@ -441,6 +442,7 @@ export function createModel (builder: Builder): void {
TProject,
TComponent,
TIssue,
TIssueRelation,
TIssueTemplate,
TIssueStatus,
TTypeIssuePriority,
+29
View File
@@ -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,
@@ -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
@@ -340,6 +346,26 @@ 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>
@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
*/
@@ -389,6 +415,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
@@ -591,6 +591,7 @@ export class WorkspaceImporter {
rank,
comments: issue.comments?.length ?? 0,
subIssues: issue.subdocs.length,
startDate: null,
dueDate: null,
parents: parentsInfo,
remainingTime,
+7
View File
@@ -115,6 +115,13 @@
"NoAssignee": "Bez přiřazení",
"LastUpdated": "Poslední aktualizace",
"DueDate": "Datum splnění",
"IssueStartDate": "Datum zahájení",
"GanttDependency": "Dependency",
"GanttDependencyFinishToFinish": "Finish to finish",
"GanttDependencyFinishToStart": "Finish to start",
"GanttDependencyStartToFinish": "Start to finish",
"GanttDependencyStartToStart": "Start to start",
"GanttLag": "Lag",
"Manual": "Manuální",
"All": "Vše",
"PastWeek": "Minulý týden",
+7
View File
@@ -125,6 +125,13 @@
"NoAssignee": "Nicht zugewiesen",
"LastUpdated": "Zuletzt aktualisiert",
"DueDate": "Fälligkeitsdatum",
"IssueStartDate": "Startdatum",
"GanttDependency": "Abhängigkeit",
"GanttDependencyFinishToFinish": "Ende zu Ende",
"GanttDependencyFinishToStart": "Ende zu Anfang",
"GanttDependencyStartToFinish": "Anfang zu Ende",
"GanttDependencyStartToStart": "Anfang zu Anfang",
"GanttLag": "Verzögerung",
"Manual": "Manuell",
"All": "Alle",
"PastWeek": "Letzte Woche",
+7
View File
@@ -125,6 +125,13 @@
"NoAssignee": "No assignee",
"LastUpdated": "Last updated",
"DueDate": "Due date",
"IssueStartDate": "Start date",
"GanttDependency": "Dependency",
"GanttDependencyFinishToFinish": "Finish to finish",
"GanttDependencyFinishToStart": "Finish to start",
"GanttDependencyStartToFinish": "Start to finish",
"GanttDependencyStartToStart": "Start to start",
"GanttLag": "Lag",
"Manual": "Manual",
"All": "All",
"PastWeek": "Past week",
+7
View File
@@ -123,6 +123,13 @@
"NoAssignee": "Sin asignar",
"LastUpdated": "Última actualización",
"DueDate": "Fecha de vencimiento",
"IssueStartDate": "Fecha de inicio",
"GanttDependency": "Dependency",
"GanttDependencyFinishToFinish": "Finish to finish",
"GanttDependencyFinishToStart": "Finish to start",
"GanttDependencyStartToFinish": "Start to finish",
"GanttDependencyStartToStart": "Start to start",
"GanttLag": "Lag",
"Manual": "Manual",
"All": "Todos",
"PastWeek": "Semana pasada",
+7
View File
@@ -123,6 +123,13 @@
"NoAssignee": "Non assigné",
"LastUpdated": "Dernière mise à jour",
"DueDate": "Date d'échéance",
"IssueStartDate": "Date de début",
"GanttDependency": "Dependency",
"GanttDependencyFinishToFinish": "Finish to finish",
"GanttDependencyFinishToStart": "Finish to start",
"GanttDependencyStartToFinish": "Start to finish",
"GanttDependencyStartToStart": "Start to start",
"GanttLag": "Lag",
"Manual": "Manuel",
"All": "Tous",
"PastWeek": "La semaine passée",
+7
View File
@@ -123,6 +123,13 @@
"NoAssignee": "Nessun assegnatario",
"LastUpdated": "Ultimo aggiornamento",
"DueDate": "Data di scadenza",
"IssueStartDate": "Data di inizio",
"GanttDependency": "Dependency",
"GanttDependencyFinishToFinish": "Finish to finish",
"GanttDependencyFinishToStart": "Finish to start",
"GanttDependencyStartToFinish": "Start to finish",
"GanttDependencyStartToStart": "Start to start",
"GanttLag": "Lag",
"Manual": "Manuale",
"All": "Tutti",
"PastWeek": "Settimana scorsa",
+7
View File
@@ -123,6 +123,13 @@
"NoAssignee": "担当者なし",
"LastUpdated": "最終更新日",
"DueDate": "期日",
"IssueStartDate": "開始日",
"GanttDependency": "Dependency",
"GanttDependencyFinishToFinish": "Finish to finish",
"GanttDependencyFinishToStart": "Finish to start",
"GanttDependencyStartToFinish": "Start to finish",
"GanttDependencyStartToStart": "Start to start",
"GanttLag": "Lag",
"Manual": "手動",
"All": "すべて",
"PastWeek": "先週",
+7
View File
@@ -123,6 +123,13 @@
"NoAssignee": "담당자 없음",
"LastUpdated": "최근 업데이트",
"DueDate": "마감일",
"IssueStartDate": "시작일",
"GanttDependency": "Dependency",
"GanttDependencyFinishToFinish": "Finish to finish",
"GanttDependencyFinishToStart": "Finish to start",
"GanttDependencyStartToFinish": "Start to finish",
"GanttDependencyStartToStart": "Start to start",
"GanttLag": "Lag",
"Manual": "수동",
"All": "전체",
"PastWeek": "지난주",
+7
View File
@@ -123,6 +123,13 @@
"NoAssignee": "Sem atribuição",
"LastUpdated": "Última atualização",
"DueDate": "Data de vencimento",
"IssueStartDate": "Data de início",
"GanttDependency": "Dependency",
"GanttDependencyFinishToFinish": "Finish to finish",
"GanttDependencyFinishToStart": "Finish to start",
"GanttDependencyStartToFinish": "Start to finish",
"GanttDependencyStartToStart": "Start to start",
"GanttLag": "Lag",
"Manual": "Manual",
"All": "Todos",
"PastWeek": "Semana passada",
+7
View File
@@ -123,6 +123,13 @@
"NoAssignee": "Sem atribuição",
"LastUpdated": "Última atualização",
"DueDate": "Data de vencimento",
"IssueStartDate": "Data de início",
"GanttDependency": "Dependency",
"GanttDependencyFinishToFinish": "Finish to finish",
"GanttDependencyFinishToStart": "Finish to start",
"GanttDependencyStartToFinish": "Start to finish",
"GanttDependencyStartToStart": "Start to start",
"GanttLag": "Lag",
"Manual": "Manual",
"All": "Todos",
"PastWeek": "Semana passada",
+7
View File
@@ -125,6 +125,13 @@
"NoAssignee": "Нет исполнителя",
"LastUpdated": "Последнее обновление",
"DueDate": "Срок",
"IssueStartDate": "Дата начала",
"GanttDependency": "Dependency",
"GanttDependencyFinishToFinish": "Finish to finish",
"GanttDependencyFinishToStart": "Finish to start",
"GanttDependencyStartToFinish": "Start to finish",
"GanttDependencyStartToStart": "Start to start",
"GanttLag": "Lag",
"Manual": "Пользовательский",
"All": "Все",
"PastWeek": "Предыдущая неделя",
+7
View File
@@ -123,6 +123,13 @@
"NoAssignee": "Atanan yok",
"LastUpdated": "Son güncelleme",
"DueDate": "Bitiş tarihi",
"IssueStartDate": "Başlangıç tarihi",
"GanttDependency": "Dependency",
"GanttDependencyFinishToFinish": "Finish to finish",
"GanttDependencyFinishToStart": "Finish to start",
"GanttDependencyStartToFinish": "Start to finish",
"GanttDependencyStartToStart": "Start to start",
"GanttLag": "Lag",
"Manual": "Manuel",
"All": "Tümü",
"PastWeek": "Geçen hafta",
+7
View File
@@ -125,6 +125,13 @@
"NoAssignee": "无受理人",
"LastUpdated": "最后更新",
"DueDate": "截止日期",
"IssueStartDate": "开始日期",
"GanttDependency": "Dependency",
"GanttDependencyFinishToFinish": "Finish to finish",
"GanttDependencyFinishToStart": "Finish to start",
"GanttDependencyStartToFinish": "Start to finish",
"GanttDependencyStartToStart": "Start to start",
"GanttLag": "Lag",
"Manual": "手动",
"All": "全部",
"PastWeek": "过去一周",
@@ -186,6 +186,7 @@
priority: priority ?? IssuePriority.NoPriority,
space: _space as Ref<Project>,
component: component ?? $activeComponent ?? null,
startDate: null,
dueDate: null,
attachments: 0,
estimation: 0,
@@ -312,6 +313,7 @@
_id: generateId(),
space: _space as Ref<Project>,
subIssues: [],
startDate: null,
dueDate: null,
labels:
p.labels !== undefined
@@ -488,6 +490,7 @@
rank: '',
comments: 0,
subIssues: 0,
startDate: object.startDate,
dueDate: object.dueDate,
parents:
parentIssue != null
@@ -86,6 +86,7 @@
rank: '',
comments: 0,
subIssues: 0,
startDate: subIssue.startDate ?? null,
dueDate: null,
parents,
reportedTime: 0,
@@ -34,6 +34,7 @@
status: MilestoneStatus.Planned,
comments: 0,
attachments: 0,
startDate: null,
targetDate: Date.now() + 14 * 24 * 60 * 60 * 1000
}
@@ -71,6 +71,7 @@
assignee: project.defaultAssignee ?? null,
status: project.defaultIssueStatus,
space: project._id,
startDate: null,
dueDate: null,
subIssues: [],
attachments: 0,
+44
View File
@@ -119,6 +119,22 @@ export enum IssuePriority {
Low
}
/**
* Dependency kind between two Issues for Gantt scheduling.
*
* - `finish-to-start` (FS): A must finish before B can start. Most common.
* - `start-to-start` (SS): A must start before B can start.
* - `finish-to-finish` (FF): A must finish before B can finish.
* - `start-to-finish` (SF): A must start before B can finish. Rare.
*
* @public
*/
export type DependencyKind =
| 'finish-to-start'
| 'start-to-start'
| 'finish-to-finish'
| 'start-to-finish'
/**
* @public
*/
@@ -175,6 +191,7 @@ export interface Milestone extends Doc {
comments: number
attachments?: number
startDate: Timestamp | null // null = open-ended begin marker
targetDate: Timestamp
}
@@ -196,6 +213,8 @@ export interface Issue extends Task {
relations?: RelatedDocument[]
parents: IssueParentInfo[]
startDate: Timestamp | null // for Gantt scheduling; null = unscheduled
space: Ref<Project>
milestone?: Ref<Milestone> | null
@@ -236,6 +255,7 @@ export interface IssueDraft {
assignee: Ref<Person> | null
component: Ref<Component> | null
space: Ref<Project>
startDate: Timestamp | null
dueDate: Timestamp | null
milestone?: Ref<Milestone> | null
@@ -323,6 +343,22 @@ export interface IssueParentInfo {
space: Ref<Space>
}
/**
* Typed dependency between two Issues, used by the Gantt view to compute
* cascade scheduling and critical path.
*
* Persisted as an AttachedDoc collection 'relations' on the source Issue.
*
* @public
*/
export interface IssueRelation extends AttachedDoc {
attachedTo: Ref<Issue> // source / predecessor
target: Ref<Issue> // successor
kind: DependencyKind
/** Lag in schedule days; can be negative (overlap). */
lag: number
}
/**
* @public
*/
@@ -366,6 +402,7 @@ const pluginState = plugin(trackerId, {
class: {
Project: '' as Ref<Class<Project>>,
Issue: '' as Ref<Class<Issue>>,
IssueRelation: '' as Ref<Class<IssueRelation>>,
IssueTemplate: '' as Ref<Class<IssueTemplate>>,
Component: '' as Ref<Class<Component>>,
IssueStatus: '' as Ref<Class<IssueStatus>>,
@@ -520,6 +557,13 @@ const pluginState = plugin(trackerId, {
Project: '' as IntlString,
RelatedIssues: '' as IntlString,
Issue: '' as IntlString,
IssueStartDate: '' as IntlString,
GanttDependency: '' as IntlString,
GanttDependencyFinishToFinish: '' as IntlString,
GanttDependencyFinishToStart: '' as IntlString,
GanttDependencyStartToFinish: '' as IntlString,
GanttDependencyStartToStart: '' as IntlString,
GanttLag: '' as IntlString,
NewProject: '' as IntlString,
UnsetParentIssue: '' as IntlString,
ForbidCreateProjectPermission: '' as IntlString,
@@ -69,6 +69,7 @@ WithMarkup<Issue>,
| 'reports'
| 'childInfo'
| 'dueDate'
| 'startDate'
| 'kind'
| 'reviews'
| 'reviewThreads'
@@ -868,6 +868,7 @@ export class IssueSyncManager extends IssueSyncManagerBase implements DocSyncMan
rank: calcRank(lastOne, undefined),
comments: 0,
subIssues: 0,
startDate: null,
dueDate: null,
parents: [],
reportedTime: 0,
@@ -1169,6 +1169,7 @@ export class PullRequestSyncManager extends IssueSyncManagerBase implements DocS
rank: calcRank(lastOne, undefined),
comments: 0,
subIssues: 0,
startDate: null,
dueDate: null,
parents: [],
reportedTime: 0,