mirror of
https://github.com/hcengineering/platform.git
synced 2026-08-17 18:05:42 +02:00
feat(tracker): Gantt scheduling schema (startDate + IssueRelation) + version bump (#10851)
* 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>
* test(model-tracker): add migrateAddStartDate jest tests
3 tests covering migrateAddStartDate:
- writes startDate=null to Issues in DOMAIN_TASK with the right filter
- writes startDate=null to Milestones in DOMAIN_TRACKER with the right filter
- issues exactly two update calls (one per class)
Follows the MigrationClient mock pattern from
models/chat/src/__tests__/migration.test.ts.
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
* feat(model-tracker): add migrateAddStartDate + wire into trackerOperation
Backfills startDate=null on existing Issues (DOMAIN_TASK) and Milestones
(DOMAIN_TRACKER) so the new schema field has a defined value on every
pre-existing document. Idempotent via the standard tryMigrate state-key
mechanism (state: 'gantt-add-startdate').
Verified domain choices against existing migration helpers:
- migrateIdentifiers / passIdentifierToParentInfo use DOMAIN_TASK for
Issues (lines 145, 161 in this file).
- TMilestone @Model decorator confirms DOMAIN_TRACKER for Milestones
(models/tracker/src/types.ts:372).
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
* feat(tracker): expose Issue.startDate / Milestone.startDate in UI; tighten typing
UI changes (so the new schema fields are actually editable, in chronological
order Start → Due/Target):
- New StartDateEditor.svelte (mirrors DueDateEditor.svelte for startDate)
- ControlPanel: render Start Date row above Due Date row in the issue
side panel; both always-visible (no `!== null` guard) so users can set
them on issues that don't have a date yet
- NewMilestone form: Start Date input above Target Date input
- Milestone list view: Start Date column before Target Date column
- TIssueRelation: tighten interface to `extends AttachedDoc<Issue, 'relations'>`
so attachedTo + collection are statically typed. The model class
re-declares `collection: 'relations'` to match the narrower base.
- Drop 4 unused Dependency-kind IntlString keys (FinishToFinish,
FinishToStart, StartToFinish, StartToStart) — they had no consumer
in PR 1; will be re-introduced in PR 4 (dependency editor).
- Simplify migration.ts comments — drop ageing line-references.
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
* fix(tracker): set explicit @Prop ranks for Milestone date fields
The DocAttributeBar side panel sorts attributes by attr.rank ?? toRank(_id)
(see plugins/view-resources/src/components/ClassAttributeBar.svelte:42-47),
so without explicit ranks the visible order on a Milestone was hash-based
(startDate before Status, breaking the chronological flow the user expects).
Set ranks so the side panel renders Status → Start date → Target date.
Comments and attachments stay where they are (they're collections, filtered
out of the attribute panel by categorizeFields).
Issues are unaffected — the Issue side panel is the custom ControlPanel.svelte
which renders Start date / Due date in explicit slots (see PR 1's UI commit).
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
* fix(tracker-resources): EditMilestone renders Status/Start/Target in body in chronological order
The right-side DocAttributeBar sorts attributes by attr.rank ?? toRank(_id),
giving startDate before status (toRank('startDate') < toRank('status')
lexicographically). Setting an explicit rank via @Prop's third arg did not
propagate through the workspace upgrade for existing Attribute documents
in the model TX log — the rank made it into the bundled txes but the
existing Attribute creation TXes are not replaced on upgrade-workspace.
Pivot: render Status, Start date, Target date in the EditMilestone body
in explicit chronological order, and add 'status', 'startDate', 'targetDate'
to ignoreKeys so they don't appear duplicated in the side panel. This
mirrors how Issue's ControlPanel.svelte handles its date fields.
Reverts the no-op @Prop rank attempt.
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
* fix(fulltext): bump model version to 0.7.423 to match deployed workspaces
The fulltext-pod's compiled model version (baked into bundle/model.json via
common/scripts/version.txt at build time) lags whenever the workspaces have
been migrated to a newer patch but the pod was not rebuilt. In that state the
indexer rejects every incoming Tx with a `wrong version` warning, new issues
silently fail to land in Elasticsearch, and search returns empty results for
any document created after the migration.
Bumping `version.txt` aligns the compiled model with the workspaces. All
future builds (front, transactor, workspace, tool, fulltext) will emit
0.7.423, the indexer accepts the Tx stream again, and the deferred backlog
gets consumed automatically — no manual reindex needed.
This commit is the build-side companion to the schema migration in this
same PR. Without it the fulltext-pod cannot consume the migrated workspace's
Tx events.
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
* chore: apply rush format after develop merge
Resolves the failing formatting check requested by @ArtyomSavchenko in
review of #10851 after the develop branch merge.
Affects three files in our PR scope:
- models/tracker/src/migration.ts: collapse short multi-line client.update call
- plugins/tracker/src/index.ts: inline DependencyKind union + IssueRelation comment
- plugins/tracker-resources/src/components/milestones/EditMilestone.svelte:
reformat inline arrow handlers, move QueryIssuesList block ahead of <style>
No logic changes; deterministic prettier output.
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
* test(tracker): fix milestone page-object selectors after startDate field addition
The Gantt schema PR added Milestone.startDate, which:
1. Adds a second datetime-button to the NewMilestone form pool. The
existing 'div.antiCard-pool button.datetime-button' locator matched
both buttons and tripped Playwright's strict-mode check. Scope the
target-date locator to .last() and add a sibling .first() helper for
the start-date button.
2. Moves Status / Start date / Target date editors from the
auto-generated side panel into EditMilestone's body
(div.dates-row > div.date-cell > span.cell-label + <button>) in
chronological order. The label span no longer has a sibling <div>
wrapping the button — the button is a direct sibling. Switch the
buttonStatus/buttonTargetDate XPath to following-sibling::button[1]
and match the new class="cell-label" span. Add a buttonStartDate
helper for the new editor row.
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
* test(tracker): shift buttonEstimation index after startDate row addition
ControlPanel.svelte (issue side panel) now renders the Start date and
Due date rows unconditionally — pre-PR the Due date row was conditional
on issue.dueDate !== null and the Start date row didn't exist at all.
Both new rows emit a <div><button> pair via DueDatePresenter, which the
existing (//span[text()='Estimation']/../div/button)[3] XPath counts as
extra matches and pushes the Estimation button from the 3rd to the 5th
direct div/button under the popupPanel-body__aside-grid.
Direct div/button order under the grid (document order):
1. CreatedBy (EmployeeBox > UserBox div > Button)
2. Assignee (AssigneeEditor div > Button)
3. Start date (NEW — StartDateEditor > DueDatePresenter div > button.datetime-button)
4. Due date (NEW — DueDateEditor > DueDatePresenter div > button.datetime-button)
5. Estimation (AttributeBarEditor div > Button)
buttonAssignee at [2] is unchanged. textEstimation uses 'following-sibling::div[1]'
(first sibling), which is unaffected by additions earlier in the grid.
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
* chore: apply rush format (prettier compliance for CI)
CI's rush fast-format --branch develop step flagged
tests/sanity/tests/model/tracker/milestones-details-page.ts for a
missing blank line between the buttonTargetDate locator (introduced in
86b1c19ee8) and the next field. Apply the local 'rush format' result.
The two other files CI flagged
(plugins/process-resources/src/components/settings/BindingsEditor.svelte
and ImportSlotsPopup.svelte) were actually upstream changes from PR
#10921 (Fix add tag) that landed after our last develop merge — the
preceding merge of upstream/develop into this branch resolves those
diffs.
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
* fix(tests/tracker): use contains() for cell-label class to survive Svelte CSS scoping
The Svelte 4 compiler appends a scoped `svelte-<hash>` class to every
element matched by a component-local CSS selector. EditMilestone.svelte
styles `.cell-label` locally, so each label span ends up as
`<span class="cell-label svelte-XXXXX">` at runtime, not the bare
`<span class="cell-label">` shipped in source. The previous XPath
locator used strict `@class="cell-label"` and never matched.
Switch buttonStatus / buttonStartDate / buttonTargetDate to the standard
`contains(concat(' ', normalize-space(@class), ' '), ' cell-label ')`
class-match idiom so the locators tolerate the added scoped class.
Verified against the playwright accessibility snapshot from the failed
run (artifact playwright-results, hash 07a8f36b...md): the Status row
renders as a generic with text 'Status' immediately followed by a
button 'In progress' as the next direct sibling, matching the rest of
the XPath.
Fixes 5 milestone.spec.ts failures observed in run 27816114236:
- Create a Milestone (locator timeout on checkIssue → buttonStatus)
- Edit a Milestone (locator timeout on editIssue → buttonStatus.click)
- Delete a Milestone (locator timeout on checkIssue → buttonStatus)
plus their two retries each.
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
---------
Signed-off-by: Michael Uray <michaeluray@users.noreply.github.com>
Co-authored-by: Michael Uray <michaeluray@users.noreply.github.com>
Co-authored-by: Artyom Savchenko <armisav@gmail.com>
This commit is contained in:
co-authored by
Michael Uray
Artyom Savchenko
parent
4891c65b3d
commit
abe0cb9625
@@ -1 +1 @@
|
||||
"0.7.422"
|
||||
"0.7.423"
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
})
|
||||
@@ -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,
|
||||
|
||||
@@ -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, {
|
||||
@@ -170,6 +171,16 @@ 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
|
||||
@@ -398,6 +409,11 @@ export const trackerOperation: MigrateOperation = {
|
||||
state: 'migrateDefaultTypeMixins',
|
||||
mode: 'upgrade',
|
||||
func: migrateDefaultTypeMixins
|
||||
},
|
||||
{
|
||||
state: 'gantt-add-startdate',
|
||||
mode: 'upgrade',
|
||||
func: migrateAddStartDate
|
||||
}
|
||||
])
|
||||
},
|
||||
|
||||
@@ -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,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
|
||||
*/
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -663,7 +663,7 @@ export function defineViewlets (builder: Builder): void {
|
||||
viewOptions: milestoneOptions,
|
||||
configOptions: {
|
||||
strict: true,
|
||||
hiddenKeys: ['targetDate', 'label', 'description']
|
||||
hiddenKeys: ['startDate', 'targetDate', 'label', 'description']
|
||||
},
|
||||
config: [
|
||||
{
|
||||
@@ -672,6 +672,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,
|
||||
|
||||
@@ -591,6 +591,7 @@ export class WorkspaceImporter {
|
||||
rank,
|
||||
comments: issue.comments?.length ?? 0,
|
||||
subIssues: issue.subdocs.length,
|
||||
startDate: null,
|
||||
dueDate: null,
|
||||
parents: parentsInfo,
|
||||
remainingTime,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -125,6 +125,9 @@
|
||||
"NoAssignee": "Nicht zugewiesen",
|
||||
"LastUpdated": "Zuletzt aktualisiert",
|
||||
"DueDate": "Fälligkeitsdatum",
|
||||
"IssueStartDate": "Startdatum",
|
||||
"GanttDependency": "Abhängigkeit",
|
||||
"GanttLag": "Verzögerung",
|
||||
"Manual": "Manuell",
|
||||
"All": "Alle",
|
||||
"PastWeek": "Letzte Woche",
|
||||
|
||||
@@ -125,6 +125,9 @@
|
||||
"NoAssignee": "No assignee",
|
||||
"LastUpdated": "Last updated",
|
||||
"DueDate": "Due date",
|
||||
"IssueStartDate": "Start date",
|
||||
"GanttDependency": "Dependency",
|
||||
"GanttLag": "Lag",
|
||||
"Manual": "Manual",
|
||||
"All": "All",
|
||||
"PastWeek": "Past week",
|
||||
|
||||
@@ -123,6 +123,9 @@
|
||||
"NoAssignee": "Sin asignar",
|
||||
"LastUpdated": "Última actualización",
|
||||
"DueDate": "Fecha de vencimiento",
|
||||
"IssueStartDate": "Fecha de inicio",
|
||||
"GanttDependency": "Dependency",
|
||||
"GanttLag": "Lag",
|
||||
"Manual": "Manual",
|
||||
"All": "Todos",
|
||||
"PastWeek": "Semana pasada",
|
||||
|
||||
@@ -123,6 +123,9 @@
|
||||
"NoAssignee": "Non assigné",
|
||||
"LastUpdated": "Dernière mise à jour",
|
||||
"DueDate": "Date d'échéance",
|
||||
"IssueStartDate": "Date de début",
|
||||
"GanttDependency": "Dependency",
|
||||
"GanttLag": "Lag",
|
||||
"Manual": "Manuel",
|
||||
"All": "Tous",
|
||||
"PastWeek": "La semaine passée",
|
||||
|
||||
@@ -123,6 +123,9 @@
|
||||
"NoAssignee": "Nessun assegnatario",
|
||||
"LastUpdated": "Ultimo aggiornamento",
|
||||
"DueDate": "Data di scadenza",
|
||||
"IssueStartDate": "Data di inizio",
|
||||
"GanttDependency": "Dependency",
|
||||
"GanttLag": "Lag",
|
||||
"Manual": "Manuale",
|
||||
"All": "Tutti",
|
||||
"PastWeek": "Settimana scorsa",
|
||||
|
||||
@@ -123,6 +123,9 @@
|
||||
"NoAssignee": "担当者なし",
|
||||
"LastUpdated": "最終更新日",
|
||||
"DueDate": "期日",
|
||||
"IssueStartDate": "開始日",
|
||||
"GanttDependency": "Dependency",
|
||||
"GanttLag": "Lag",
|
||||
"Manual": "手動",
|
||||
"All": "すべて",
|
||||
"PastWeek": "先週",
|
||||
|
||||
@@ -123,6 +123,9 @@
|
||||
"NoAssignee": "담당자 없음",
|
||||
"LastUpdated": "최근 업데이트",
|
||||
"DueDate": "마감일",
|
||||
"IssueStartDate": "시작일",
|
||||
"GanttDependency": "Dependency",
|
||||
"GanttLag": "Lag",
|
||||
"Manual": "수동",
|
||||
"All": "전체",
|
||||
"PastWeek": "지난주",
|
||||
|
||||
@@ -123,6 +123,9 @@
|
||||
"NoAssignee": "Sem atribuição",
|
||||
"LastUpdated": "Última atualização",
|
||||
"DueDate": "Data de vencimento",
|
||||
"IssueStartDate": "Data de início",
|
||||
"GanttDependency": "Dependency",
|
||||
"GanttLag": "Lag",
|
||||
"Manual": "Manual",
|
||||
"All": "Todos",
|
||||
"PastWeek": "Semana passada",
|
||||
|
||||
@@ -123,6 +123,9 @@
|
||||
"NoAssignee": "Sem atribuição",
|
||||
"LastUpdated": "Última atualização",
|
||||
"DueDate": "Data de vencimento",
|
||||
"IssueStartDate": "Data de início",
|
||||
"GanttDependency": "Dependency",
|
||||
"GanttLag": "Lag",
|
||||
"Manual": "Manual",
|
||||
"All": "Todos",
|
||||
"PastWeek": "Semana passada",
|
||||
|
||||
@@ -125,6 +125,9 @@
|
||||
"NoAssignee": "Нет исполнителя",
|
||||
"LastUpdated": "Последнее обновление",
|
||||
"DueDate": "Срок",
|
||||
"IssueStartDate": "Дата начала",
|
||||
"GanttDependency": "Dependency",
|
||||
"GanttLag": "Lag",
|
||||
"Manual": "Пользовательский",
|
||||
"All": "Все",
|
||||
"PastWeek": "Предыдущая неделя",
|
||||
|
||||
@@ -123,6 +123,9 @@
|
||||
"NoAssignee": "Atanan yok",
|
||||
"LastUpdated": "Son güncelleme",
|
||||
"DueDate": "Bitiş tarihi",
|
||||
"IssueStartDate": "Başlangıç tarihi",
|
||||
"GanttDependency": "Dependency",
|
||||
"GanttLag": "Lag",
|
||||
"Manual": "Manuel",
|
||||
"All": "Tümü",
|
||||
"PastWeek": "Geçen hafta",
|
||||
|
||||
@@ -125,6 +125,9 @@
|
||||
"NoAssignee": "无受理人",
|
||||
"LastUpdated": "最后更新",
|
||||
"DueDate": "截止日期",
|
||||
"IssueStartDate": "开始日期",
|
||||
"GanttDependency": "Dependency",
|
||||
"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,
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
<!--
|
||||
// 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. You may
|
||||
// obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
//
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { WithLookup } from '@hcengineering/core'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Issue } from '@hcengineering/tracker'
|
||||
import { DueDatePresenter } from '@hcengineering/ui'
|
||||
|
||||
export let value: WithLookup<Issue>
|
||||
export let width: string | undefined = undefined
|
||||
export let editable: boolean = true
|
||||
|
||||
const client = getClient()
|
||||
|
||||
const handleStartDateChanged = async (newStartDate: number | undefined | null): Promise<void> => {
|
||||
if (newStartDate === undefined || value.startDate === newStartDate) {
|
||||
return
|
||||
}
|
||||
|
||||
await client.updateCollection(
|
||||
value._class,
|
||||
value.space,
|
||||
value._id,
|
||||
value.attachedTo,
|
||||
value.attachedToClass,
|
||||
value.collection,
|
||||
{ startDate: newStartDate }
|
||||
)
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if value}
|
||||
<DueDatePresenter
|
||||
kind={'link'}
|
||||
value={value.startDate}
|
||||
{width}
|
||||
{editable}
|
||||
onChange={(e) => handleStartDateChanged(e)}
|
||||
shouldIgnoreOverdue={true}
|
||||
/>
|
||||
{/if}
|
||||
@@ -35,6 +35,7 @@
|
||||
import DueDateEditor from '../DueDateEditor.svelte'
|
||||
import PriorityEditor from '../PriorityEditor.svelte'
|
||||
import RelationEditor from '../RelationEditor.svelte'
|
||||
import StartDateEditor from '../StartDateEditor.svelte'
|
||||
import StatusEditor from '../StatusEditor.svelte'
|
||||
import notification from '@hcengineering/notification'
|
||||
|
||||
@@ -60,6 +61,7 @@
|
||||
'number',
|
||||
'assignee',
|
||||
'component',
|
||||
'startDate',
|
||||
'dueDate',
|
||||
'milestone',
|
||||
'relations',
|
||||
@@ -202,14 +204,17 @@
|
||||
</span>
|
||||
<MilestoneEditor value={issue} space={issue.space} size={'medium'} isEditable={!readonly} />
|
||||
|
||||
{#if issue.dueDate !== null}
|
||||
<div class="divider" />
|
||||
|
||||
<span class="labelOnPanel">
|
||||
<Label label={tracker.string.IssueStartDate} />
|
||||
</span>
|
||||
<StartDateEditor value={issue} width={'100%'} editable={!readonly} />
|
||||
|
||||
<span class="labelOnPanel">
|
||||
<Label label={tracker.string.DueDate} />
|
||||
</span>
|
||||
<DueDateEditor value={issue} width={'100%'} editable={!readonly} />
|
||||
{/if}
|
||||
|
||||
{#if keys.length > 0}
|
||||
<div class="divider" />
|
||||
|
||||
@@ -16,9 +16,10 @@
|
||||
import { AttachmentStyleBoxEditor } from '@hcengineering/attachment-resources'
|
||||
import { getClient } from '@hcengineering/presentation'
|
||||
import { Milestone } from '@hcengineering/tracker'
|
||||
import { EditBox, Label } from '@hcengineering/ui'
|
||||
import { DatePresenter, EditBox, Label } from '@hcengineering/ui'
|
||||
import { createEventDispatcher, onMount } from 'svelte'
|
||||
import tracker from '../../plugin'
|
||||
import MilestoneStatusEditor from './MilestoneStatusEditor.svelte'
|
||||
import QueryIssuesList from '../issues/edit/QueryIssuesList.svelte'
|
||||
|
||||
export let object: Milestone
|
||||
@@ -33,12 +34,27 @@
|
||||
await client.update(object, { [field]: value })
|
||||
}
|
||||
|
||||
async function changeStartDate (value: number | null | undefined): Promise<void> {
|
||||
await client.update(object, { startDate: value ?? null })
|
||||
}
|
||||
async function changeTargetDate (value: number | null | undefined): Promise<void> {
|
||||
if (value === null || value === undefined) return
|
||||
await client.update(object, { targetDate: value })
|
||||
}
|
||||
|
||||
$: if (oldLabel !== object.label) {
|
||||
oldLabel = object.label
|
||||
rawLabel = object.label
|
||||
}
|
||||
|
||||
onMount(() => dispatch('open', { ignoreKeys: ['label', 'description', 'attachments'] }))
|
||||
// status / startDate / targetDate are rendered in this component's body in
|
||||
// chronological order (Status → Start → Target). Hide them from the
|
||||
// auto-generated side panel so they don't appear twice.
|
||||
onMount(() =>
|
||||
dispatch('open', {
|
||||
ignoreKeys: ['label', 'description', 'attachments', 'status', 'startDate', 'targetDate']
|
||||
})
|
||||
)
|
||||
$: descriptionKey = client.getHierarchy().getAttribute(tracker.class.Component, 'description')
|
||||
let descriptionBox: AttachmentStyleBoxEditor
|
||||
</script>
|
||||
@@ -58,6 +74,37 @@
|
||||
}}
|
||||
/>
|
||||
|
||||
<div class="dates-row mt-4">
|
||||
<div class="date-cell">
|
||||
<span class="cell-label"><Label label={tracker.string.Status} /></span>
|
||||
<MilestoneStatusEditor value={object.status} {object} kind="regular" />
|
||||
</div>
|
||||
<div class="date-cell">
|
||||
<span class="cell-label"><Label label={tracker.string.StartDate} /></span>
|
||||
<DatePresenter
|
||||
value={object.startDate}
|
||||
editable
|
||||
kind={'regular'}
|
||||
size={'medium'}
|
||||
on:change={(e) => {
|
||||
void changeStartDate(e.detail)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div class="date-cell">
|
||||
<span class="cell-label"><Label label={tracker.string.TargetDate} /></span>
|
||||
<DatePresenter
|
||||
value={object.targetDate}
|
||||
editable
|
||||
kind={'regular'}
|
||||
size={'medium'}
|
||||
on:change={(e) => {
|
||||
void changeTargetDate(e.detail)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="w-full mt-6">
|
||||
<AttachmentStyleBoxEditor
|
||||
focusIndex={30}
|
||||
@@ -83,3 +130,23 @@
|
||||
</svelte:fragment>
|
||||
</QueryIssuesList>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.dates-row {
|
||||
display: flex;
|
||||
gap: 1.5rem;
|
||||
align-items: flex-start;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.date-cell {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
min-width: 8rem;
|
||||
}
|
||||
.cell-label {
|
||||
font-size: 0.85rem;
|
||||
color: var(--theme-darker-color);
|
||||
font-weight: 500;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
status: MilestoneStatus.Planned,
|
||||
comments: 0,
|
||||
attachments: 0,
|
||||
startDate: null,
|
||||
targetDate: Date.now() + 14 * 24 * 60 * 60 * 1000
|
||||
}
|
||||
|
||||
@@ -76,6 +77,14 @@
|
||||
/>
|
||||
<svelte:fragment slot="pool">
|
||||
<MilestoneStatusEditor bind:value={object.status} {object} kind="regular" />
|
||||
<DatePresenter
|
||||
bind:value={object.startDate}
|
||||
editable
|
||||
label={tracker.string.StartDate}
|
||||
detail={ui.string.SelectDate}
|
||||
kind={'regular'}
|
||||
size={'large'}
|
||||
/>
|
||||
<DatePresenter
|
||||
bind:value={object.targetDate}
|
||||
editable
|
||||
|
||||
@@ -71,6 +71,7 @@
|
||||
assignee: project.defaultAssignee ?? null,
|
||||
status: project.defaultIssueStatus,
|
||||
space: project._id,
|
||||
startDate: null,
|
||||
dueDate: null,
|
||||
subIssues: [],
|
||||
attachments: 0,
|
||||
|
||||
@@ -119,6 +119,18 @@ 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 +187,7 @@ export interface Milestone extends Doc {
|
||||
comments: number
|
||||
attachments?: number
|
||||
|
||||
startDate: Timestamp | null // null = open-ended begin marker
|
||||
targetDate: Timestamp
|
||||
}
|
||||
|
||||
@@ -196,6 +209,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 +251,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 +339,21 @@ 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<Issue, 'relations'> {
|
||||
target: Ref<Issue> // successor
|
||||
kind: DependencyKind
|
||||
/** Lag in schedule days; can be negative (overlap). */
|
||||
lag: number
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
@@ -366,6 +397,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 +552,9 @@ const pluginState = plugin(trackerId, {
|
||||
Project: '' as IntlString,
|
||||
RelatedIssues: '' as IntlString,
|
||||
Issue: '' as IntlString,
|
||||
IssueStartDate: '' as IntlString,
|
||||
GanttDependency: '' 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,
|
||||
|
||||
@@ -29,7 +29,11 @@ export class IssuesDetailsPage extends CommonTrackerPage {
|
||||
readonly textEstimation = (): Locator =>
|
||||
this.page.locator('//span[text()="Estimation"]/following-sibling::div[1]/button/span')
|
||||
|
||||
readonly buttonEstimation = (): Locator => this.page.locator('(//span[text()="Estimation"]/../div/button)[3]')
|
||||
// ControlPanel now renders Start Date + Due Date rows unconditionally
|
||||
// (Gantt schema PR — Issue.startDate). Both editors emit a `<div><button>`
|
||||
// pair that sits between Assignee (div/button #2) and Estimation, pushing
|
||||
// Estimation from the 3rd to the 5th `div/button` under the side-panel grid.
|
||||
readonly buttonEstimation = (): Locator => this.page.locator('(//span[text()="Estimation"]/../div/button)[5]')
|
||||
readonly buttonCreatedBy = (): Locator =>
|
||||
this.page.locator('//span[text()="Created by"]/following-sibling::div[1]/button')
|
||||
|
||||
|
||||
@@ -4,8 +4,30 @@ import { NewMilestone } from './types'
|
||||
|
||||
export class MilestonesDetailsPage extends CommonTrackerPage {
|
||||
inputTitle = (): Locator => this.page.locator('div.popupPanel-body input[type="text"]')
|
||||
buttonStatus = (): Locator => this.page.locator('//span[text()="Status"]/following-sibling::div[1]/button')
|
||||
buttonTargetDate = (): Locator => this.page.locator('//span[text()="Target date"]/following-sibling::div[1]/button')
|
||||
// EditMilestone now renders Status / Start date / Target date inline in the
|
||||
// body (`div.date-cell > span.cell-label + <Editor button>`) instead of the
|
||||
// auto-generated side panel (Gantt schema PR — Milestone.startDate). The
|
||||
// button is the immediate sibling of the label span, no wrapping div.
|
||||
//
|
||||
// NOTE: Svelte 4 appends a scoped `svelte-<hash>` class to elements matched
|
||||
// by component-local CSS selectors, so the rendered DOM is
|
||||
// `<span class="cell-label svelte-XXXXX">`. Strict `@class="cell-label"`
|
||||
// fails against that — use `contains(@class, 'cell-label')` instead.
|
||||
buttonStatus = (): Locator =>
|
||||
this.page.locator(
|
||||
'//span[contains(concat(" ", normalize-space(@class), " "), " cell-label ") and normalize-space(.)="Status"]/following-sibling::button[1]'
|
||||
)
|
||||
|
||||
buttonStartDate = (): Locator =>
|
||||
this.page.locator(
|
||||
'//span[contains(concat(" ", normalize-space(@class), " "), " cell-label ") and normalize-space(.)="Start date"]/following-sibling::button[1]'
|
||||
)
|
||||
|
||||
buttonTargetDate = (): Locator =>
|
||||
this.page.locator(
|
||||
'//span[contains(concat(" ", normalize-space(@class), " "), " cell-label ") and normalize-space(.)="Target date"]/following-sibling::button[1]'
|
||||
)
|
||||
|
||||
inputMilestoneName = (): Locator => this.page.locator('input[placeholder="Milestone name"]')
|
||||
inputDescription = (): Locator => this.page.locator('div.inputMsg div.tiptap')
|
||||
buttonYesMoveAndDeleteMilestonePopup = (): Locator =>
|
||||
|
||||
@@ -14,8 +14,14 @@ export class MilestonesPage extends CommonTrackerPage {
|
||||
buttonNewMilestoneSetStatus = (): Locator =>
|
||||
this.page.locator('form[id="tracker:string:NewMilestone"] div.antiCard-pool button[type="button"]')
|
||||
|
||||
// NewMilestone form now renders Start Date before Target Date in the pool
|
||||
// (Gantt schema PR added Milestone.startDate). Match the LAST datetime-button
|
||||
// so this resolves to the target-date button without strict-mode violations.
|
||||
buttonNewMilestoneStartDate = (): Locator =>
|
||||
this.page.locator('form[id="tracker:string:NewMilestone"] div.antiCard-pool button.datetime-button').first()
|
||||
|
||||
buttonNewMilestoneTargetDate = (): Locator =>
|
||||
this.page.locator('form[id="tracker:string:NewMilestone"] div.antiCard-pool button.datetime-button')
|
||||
this.page.locator('form[id="tracker:string:NewMilestone"] div.antiCard-pool button.datetime-button').last()
|
||||
|
||||
buttonNewMilestoneCreate = (): Locator =>
|
||||
this.page.locator('form[id="tracker:string:NewMilestone"] button[type="submit"]')
|
||||
|
||||
Reference in New Issue
Block a user