From 0e6d0dfc7ff0de0551c7c64a532971eb77ccde56 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:27:37 +0000 Subject: [PATCH] feat(tracker): add Gantt scheduling schema (startDate + IssueRelation) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 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 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 --- models/tracker/src/index.ts | 2 + models/tracker/src/types.ts | 29 ++++++++++++ packages/importer/src/importer/importer.ts | 1 + plugins/tracker-assets/lang/cs.json | 7 +++ plugins/tracker-assets/lang/de.json | 7 +++ plugins/tracker-assets/lang/en.json | 7 +++ plugins/tracker-assets/lang/es.json | 7 +++ plugins/tracker-assets/lang/fr.json | 7 +++ plugins/tracker-assets/lang/it.json | 7 +++ plugins/tracker-assets/lang/ja.json | 7 +++ plugins/tracker-assets/lang/ko.json | 7 +++ plugins/tracker-assets/lang/pt-br.json | 7 +++ plugins/tracker-assets/lang/pt.json | 7 +++ plugins/tracker-assets/lang/ru.json | 7 +++ plugins/tracker-assets/lang/tr.json | 7 +++ plugins/tracker-assets/lang/zh.json | 7 +++ .../src/components/CreateIssue.svelte | 3 ++ .../src/components/SubIssues.svelte | 1 + .../components/milestones/NewMilestone.svelte | 1 + .../templates/DraftIssueChildEditor.svelte | 1 + plugins/tracker/src/index.ts | 44 +++++++++++++++++++ .../github/pod-github/src/sync/issueBase.ts | 1 + services/github/pod-github/src/sync/issues.ts | 1 + .../pod-github/src/sync/pullrequests.ts | 1 + 24 files changed, 176 insertions(+) diff --git a/models/tracker/src/index.ts b/models/tracker/src/index.ts index 44d1b97b50..baabea1381 100644 --- a/models/tracker/src/index.ts +++ b/models/tracker/src/index.ts @@ -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, diff --git a/models/tracker/src/types.ts b/models/tracker/src/types.ts index 09d3421f30..369493111b 100644 --- a/models/tracker/src/types.ts +++ b/models/tracker/src/types.ts @@ -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 + @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 + + @Prop(TypeRef(tracker.class.Issue), tracker.string.Issue) + @Index(IndexKind.Indexed) + target!: Ref + + @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 diff --git a/packages/importer/src/importer/importer.ts b/packages/importer/src/importer/importer.ts index 73bdd7bb3b..5c5d30b7b5 100644 --- a/packages/importer/src/importer/importer.ts +++ b/packages/importer/src/importer/importer.ts @@ -591,6 +591,7 @@ export class WorkspaceImporter { rank, comments: issue.comments?.length ?? 0, subIssues: issue.subdocs.length, + startDate: null, dueDate: null, parents: parentsInfo, remainingTime, diff --git a/plugins/tracker-assets/lang/cs.json b/plugins/tracker-assets/lang/cs.json index f1e9d23d05..27764f0806 100644 --- a/plugins/tracker-assets/lang/cs.json +++ b/plugins/tracker-assets/lang/cs.json @@ -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", diff --git a/plugins/tracker-assets/lang/de.json b/plugins/tracker-assets/lang/de.json index 99fe946c9d..ae1645b864 100644 --- a/plugins/tracker-assets/lang/de.json +++ b/plugins/tracker-assets/lang/de.json @@ -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", diff --git a/plugins/tracker-assets/lang/en.json b/plugins/tracker-assets/lang/en.json index 4345dc5a0a..6b19e6e47f 100644 --- a/plugins/tracker-assets/lang/en.json +++ b/plugins/tracker-assets/lang/en.json @@ -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", diff --git a/plugins/tracker-assets/lang/es.json b/plugins/tracker-assets/lang/es.json index 2699c92764..909cd5d8aa 100644 --- a/plugins/tracker-assets/lang/es.json +++ b/plugins/tracker-assets/lang/es.json @@ -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", diff --git a/plugins/tracker-assets/lang/fr.json b/plugins/tracker-assets/lang/fr.json index 7b1f0c9466..de5343c765 100644 --- a/plugins/tracker-assets/lang/fr.json +++ b/plugins/tracker-assets/lang/fr.json @@ -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", diff --git a/plugins/tracker-assets/lang/it.json b/plugins/tracker-assets/lang/it.json index 7113d55111..8002444784 100644 --- a/plugins/tracker-assets/lang/it.json +++ b/plugins/tracker-assets/lang/it.json @@ -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", diff --git a/plugins/tracker-assets/lang/ja.json b/plugins/tracker-assets/lang/ja.json index 58f184c749..28744b81c5 100644 --- a/plugins/tracker-assets/lang/ja.json +++ b/plugins/tracker-assets/lang/ja.json @@ -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": "先週", diff --git a/plugins/tracker-assets/lang/ko.json b/plugins/tracker-assets/lang/ko.json index b89719af61..4d8b0f9106 100644 --- a/plugins/tracker-assets/lang/ko.json +++ b/plugins/tracker-assets/lang/ko.json @@ -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": "지난주", diff --git a/plugins/tracker-assets/lang/pt-br.json b/plugins/tracker-assets/lang/pt-br.json index 828c9ca50b..9a61364dee 100644 --- a/plugins/tracker-assets/lang/pt-br.json +++ b/plugins/tracker-assets/lang/pt-br.json @@ -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", diff --git a/plugins/tracker-assets/lang/pt.json b/plugins/tracker-assets/lang/pt.json index da5a3e85e1..37d6f04e6c 100644 --- a/plugins/tracker-assets/lang/pt.json +++ b/plugins/tracker-assets/lang/pt.json @@ -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", diff --git a/plugins/tracker-assets/lang/ru.json b/plugins/tracker-assets/lang/ru.json index 642877b385..e3f5c470db 100644 --- a/plugins/tracker-assets/lang/ru.json +++ b/plugins/tracker-assets/lang/ru.json @@ -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": "Предыдущая неделя", diff --git a/plugins/tracker-assets/lang/tr.json b/plugins/tracker-assets/lang/tr.json index 9f66c50ea5..cc146a5501 100644 --- a/plugins/tracker-assets/lang/tr.json +++ b/plugins/tracker-assets/lang/tr.json @@ -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", diff --git a/plugins/tracker-assets/lang/zh.json b/plugins/tracker-assets/lang/zh.json index 8e05a889e1..50c6244198 100644 --- a/plugins/tracker-assets/lang/zh.json +++ b/plugins/tracker-assets/lang/zh.json @@ -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": "过去一周", diff --git a/plugins/tracker-resources/src/components/CreateIssue.svelte b/plugins/tracker-resources/src/components/CreateIssue.svelte index 7878ea9d75..224ce28c9c 100644 --- a/plugins/tracker-resources/src/components/CreateIssue.svelte +++ b/plugins/tracker-resources/src/components/CreateIssue.svelte @@ -186,6 +186,7 @@ priority: priority ?? IssuePriority.NoPriority, space: _space as Ref, component: component ?? $activeComponent ?? null, + startDate: null, dueDate: null, attachments: 0, estimation: 0, @@ -312,6 +313,7 @@ _id: generateId(), space: _space as Ref, 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 diff --git a/plugins/tracker-resources/src/components/SubIssues.svelte b/plugins/tracker-resources/src/components/SubIssues.svelte index d0e4bf0835..86b8b2264e 100644 --- a/plugins/tracker-resources/src/components/SubIssues.svelte +++ b/plugins/tracker-resources/src/components/SubIssues.svelte @@ -86,6 +86,7 @@ rank: '', comments: 0, subIssues: 0, + startDate: subIssue.startDate ?? null, dueDate: null, parents, reportedTime: 0, diff --git a/plugins/tracker-resources/src/components/milestones/NewMilestone.svelte b/plugins/tracker-resources/src/components/milestones/NewMilestone.svelte index 6139262bc3..31b1fd3790 100644 --- a/plugins/tracker-resources/src/components/milestones/NewMilestone.svelte +++ b/plugins/tracker-resources/src/components/milestones/NewMilestone.svelte @@ -34,6 +34,7 @@ status: MilestoneStatus.Planned, comments: 0, attachments: 0, + startDate: null, targetDate: Date.now() + 14 * 24 * 60 * 60 * 1000 } diff --git a/plugins/tracker-resources/src/components/templates/DraftIssueChildEditor.svelte b/plugins/tracker-resources/src/components/templates/DraftIssueChildEditor.svelte index 899f5748ba..64fc96254a 100644 --- a/plugins/tracker-resources/src/components/templates/DraftIssueChildEditor.svelte +++ b/plugins/tracker-resources/src/components/templates/DraftIssueChildEditor.svelte @@ -71,6 +71,7 @@ assignee: project.defaultAssignee ?? null, status: project.defaultIssueStatus, space: project._id, + startDate: null, dueDate: null, subIssues: [], attachments: 0, diff --git a/plugins/tracker/src/index.ts b/plugins/tracker/src/index.ts index bc1cba1152..78a4dcae8d 100644 --- a/plugins/tracker/src/index.ts +++ b/plugins/tracker/src/index.ts @@ -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 milestone?: Ref | null @@ -236,6 +255,7 @@ export interface IssueDraft { assignee: Ref | null component: Ref | null space: Ref + startDate: Timestamp | null dueDate: Timestamp | null milestone?: Ref | null @@ -323,6 +343,22 @@ export interface IssueParentInfo { space: Ref } +/** + * 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 // source / predecessor + target: Ref // 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>, Issue: '' as Ref>, + IssueRelation: '' as Ref>, IssueTemplate: '' as Ref>, Component: '' as Ref>, IssueStatus: '' as Ref>, @@ -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, diff --git a/services/github/pod-github/src/sync/issueBase.ts b/services/github/pod-github/src/sync/issueBase.ts index bf4ce7535f..ff181599b6 100644 --- a/services/github/pod-github/src/sync/issueBase.ts +++ b/services/github/pod-github/src/sync/issueBase.ts @@ -69,6 +69,7 @@ WithMarkup, | 'reports' | 'childInfo' | 'dueDate' +| 'startDate' | 'kind' | 'reviews' | 'reviewThreads' diff --git a/services/github/pod-github/src/sync/issues.ts b/services/github/pod-github/src/sync/issues.ts index d4121953a8..07d7b2cea9 100644 --- a/services/github/pod-github/src/sync/issues.ts +++ b/services/github/pod-github/src/sync/issues.ts @@ -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, diff --git a/services/github/pod-github/src/sync/pullrequests.ts b/services/github/pod-github/src/sync/pullrequests.ts index 0d57422205..ebc6e9054d 100644 --- a/services/github/pod-github/src/sync/pullrequests.ts +++ b/services/github/pod-github/src/sync/pullrequests.ts @@ -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,