diff --git a/.claude/settings.json b/.claude/settings.json index 0f0d188..496f704 100644 --- a/.claude/settings.json +++ b/.claude/settings.json @@ -10,7 +10,8 @@ "Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); opts=d['projects']['Bonsai-webapp']['architect']['build']['options']; print\\(json.dumps\\({k:opts[k] for k in ['styles','scripts','assets'] if k in opts}, indent=2\\)\\)\")", "Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(d.get\\('main',''\\), d.get\\('module',''\\), d.get\\('types',''\\), d.get\\('exports',''\\)\\)\")", "Bash(grep -n \"^\\\\s*function \\\\$\\\\|const \\\\$ =\\\\|\\\\$ = \" /var/home/Gato/IdeaProjects/Bonsai-webapp/node_modules/frappe-gantt/dist/frappe-gantt.es.js)", - "Bash(grep -n \"function \\\\$\\\\b\\\\|const \\\\$ \" /var/home/Gato/IdeaProjects/Bonsai-webapp/node_modules/frappe-gantt/dist/frappe-gantt.es.js)" + "Bash(grep -n \"function \\\\$\\\\b\\\\|const \\\\$ \" /var/home/Gato/IdeaProjects/Bonsai-webapp/node_modules/frappe-gantt/dist/frappe-gantt.es.js)", + "Bash(grep -E \"\\\\.\\(ts|tsx\\)$\")" ], "additionalDirectories": [ "/var/home/Gato/IdeaProjects/Bonsai-webapp/src/app", diff --git a/src/app/dashboard/dashboard.spec.ts b/src/app/dashboard/dashboard.spec.ts index 01a0e00..56d56f7 100644 --- a/src/app/dashboard/dashboard.spec.ts +++ b/src/app/dashboard/dashboard.spec.ts @@ -13,6 +13,7 @@ const makeIssue = (overrides: Partial = {}): IssueEntity => ({ epic: '', name: 'Test Issue', startDate: '', + startDateMode: 'forced', endDate: '', dueDate: '', description: '', diff --git a/src/app/issues/issue-comments/issue-comments.spec.ts b/src/app/issues/issue-comments/issue-comments.spec.ts index 49089a0..74f3ae8 100644 --- a/src/app/issues/issue-comments/issue-comments.spec.ts +++ b/src/app/issues/issue-comments/issue-comments.spec.ts @@ -12,6 +12,7 @@ const makeIssue = (overrides: Partial = {}): IssueEntity => ({ epic: '', name: 'Test Issue', startDate: '', + startDateMode: 'forced', endDate: '', dueDate: '', description: '', diff --git a/src/app/issues/issue-comments/issue-comments.ts b/src/app/issues/issue-comments/issue-comments.ts index 81be269..5861332 100644 --- a/src/app/issues/issue-comments/issue-comments.ts +++ b/src/app/issues/issue-comments/issue-comments.ts @@ -171,6 +171,7 @@ export class IssueComments { assignee: '', epic: '', startDate: '', + startDateMode: 'forced', endDate: '', dueDate: '', description: '', diff --git a/src/app/issues/issue-detail/issue-detail.css b/src/app/issues/issue-detail/issue-detail.css index 27ef477..80cba94 100644 --- a/src/app/issues/issue-detail/issue-detail.css +++ b/src/app/issues/issue-detail/issue-detail.css @@ -240,3 +240,22 @@ margin-bottom: 0; } +/* Date mode label row */ +.field-label-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 0.3rem; +} + +.date-mode-select { + font-size: 0.7rem; + font-weight: 600; + color: #6b7280; + border: none; + background: transparent; + cursor: pointer; + padding: 0; + appearance: auto; +} + diff --git a/src/app/issues/issue-detail/issue-detail.html b/src/app/issues/issue-detail/issue-detail.html index 467e7ff..e7cc932 100644 --- a/src/app/issues/issue-detail/issue-detail.html +++ b/src/app/issues/issue-detail/issue-detail.html @@ -155,15 +155,41 @@
- - +
+ Date de début + @if (hasDependencies) { + + } +
+ @if (issue.startDateMode === 'calculated') { + + @if (startDateModeWarning) { +
{{ startDateModeWarning }}
+ } + } @else { + + }
diff --git a/src/app/issues/issue-detail/issue-detail.spec.ts b/src/app/issues/issue-detail/issue-detail.spec.ts index 19588c6..9dc5dcf 100644 --- a/src/app/issues/issue-detail/issue-detail.spec.ts +++ b/src/app/issues/issue-detail/issue-detail.spec.ts @@ -15,6 +15,7 @@ const makeIssue = (overrides: Partial = {}): IssueEntity => ({ epic: '', name: 'Test Issue', startDate: '', + startDateMode: 'forced', endDate: '', dueDate: '', description: '', @@ -299,6 +300,115 @@ describe('IssueDetail — existing issue', () => { }); }); + describe('hasDependencies', () => { + it('returns false when dependsOnIds is empty', () => { + (component as any).issue.dependsOnIds = []; + expect((component as any).hasDependencies).toBe(false); + }); + + it('returns true when dependsOnIds has entries', () => { + (component as any).issue.dependsOnIds = [2]; + expect((component as any).hasDependencies).toBe(true); + }); + }); + + describe('calculatedStartDate', () => { + it('returns empty string when there are no dependencies', () => { + (component as any).issue.dependsOnIds = []; + expect((component as any).calculatedStartDate).toBe(''); + }); + + it('returns empty string when dependencies have no endDate', () => { + store.upsert(makeIssue({ id: 10, endDate: '' })); + (component as any).issue.dependsOnIds = [10]; + expect((component as any).calculatedStartDate).toBe(''); + }); + + it('returns the endDate of the single dependency', () => { + store.upsert(makeIssue({ id: 10, endDate: '2026-03-01' })); + (component as any).issue.dependsOnIds = [10]; + expect((component as any).calculatedStartDate).toBe('2026-03-01'); + }); + + it('returns the latest endDate when multiple dependencies exist', () => { + store.upsert(makeIssue({ id: 10, endDate: '2026-03-01' })); + store.upsert(makeIssue({ id: 11, endDate: '2026-04-15' })); + store.upsert(makeIssue({ id: 12, endDate: '2026-02-20' })); + (component as any).issue.dependsOnIds = [10, 11, 12]; + expect((component as any).calculatedStartDate).toBe('2026-04-15'); + }); + + it('ignores dependencies without endDate in the max calculation', () => { + store.upsert(makeIssue({ id: 10, endDate: '2026-03-01' })); + store.upsert(makeIssue({ id: 11, endDate: '' })); + (component as any).issue.dependsOnIds = [10, 11]; + expect((component as any).calculatedStartDate).toBe('2026-03-01'); + }); + }); + + describe('startDateModeWarning', () => { + it('returns null in forced mode', () => { + (component as any).issue.startDateMode = 'forced'; + expect((component as any).startDateModeWarning).toBeNull(); + }); + + it('returns null in calculated mode when a dependency has an endDate', () => { + store.upsert(makeIssue({ id: 10, endDate: '2026-03-01' })); + (component as any).issue.dependsOnIds = [10]; + (component as any).issue.startDateMode = 'calculated'; + expect((component as any).startDateModeWarning).toBeNull(); + }); + + it('returns a warning message in calculated mode when no dependency has an endDate', () => { + store.upsert(makeIssue({ id: 10, endDate: '' })); + (component as any).issue.dependsOnIds = [10]; + (component as any).issue.startDateMode = 'calculated'; + expect((component as any).startDateModeWarning).toContain('date de fin'); + }); + }); + + describe('startDateModeValue setter', () => { + it('switching to calculated sets startDate from dependency endDate', () => { + store.upsert(makeIssue({ id: 10, endDate: '2026-05-01' })); + (component as any).issue.dependsOnIds = [10]; + (component as any).issue.startDateMode = 'forced'; + (component as any).startDateModeValue = 'calculated'; + expect((component as any).issue.startDate).toBe('2026-05-01'); + }); + + it('switching to calculated recalculates endDate', () => { + store.upsert(makeIssue({ id: 10, endDate: '2026-05-01' })); + (component as any).issue.dependsOnIds = [10]; + (component as any).issue.estimatedTime = 8; + (component as any).startDateModeValue = 'calculated'; + expect((component as any).issue.endDate).toBe('2026-05-01'); + }); + + it('switching to forced preserves the current startDate', () => { + (component as any).issue.startDate = '2026-04-10'; + (component as any).issue.startDateMode = 'calculated'; + (component as any).startDateModeValue = 'forced'; + expect((component as any).issue.startDate).toBe('2026-04-10'); + }); + }); + + describe('dateValidationError — calculated mode', () => { + it('does not flag dependency constraint in calculated mode', () => { + store.upsert(makeIssue({ id: 10, endDate: '2026-02-01' })); + (component as any).issue.dependsOnIds = [10]; + (component as any).issue.startDate = '2026-01-01'; + (component as any).issue.startDateMode = 'calculated'; + expect((component as any).dateValidationError).toBeNull(); + }); + + it('still flags startDate > endDate in calculated mode', () => { + (component as any).issue.startDate = '2026-02-01'; + (component as any).issue.endDate = '2026-01-01'; + (component as any).issue.startDateMode = 'calculated'; + expect((component as any).dateValidationError).toContain('supérieure à la date de fin'); + }); + }); + describe('recalculateEndDate (via estimatedTimeValue setter)', () => { it('sets endDate to startDate when estimatedTime is 8h or less', () => { (component as any).issue.startDate = '2026-06-01'; diff --git a/src/app/issues/issue-detail/issue-detail.ts b/src/app/issues/issue-detail/issue-detail.ts index c75737b..79ad155 100644 --- a/src/app/issues/issue-detail/issue-detail.ts +++ b/src/app/issues/issue-detail/issue-detail.ts @@ -1,4 +1,4 @@ -import { Component, inject } from '@angular/core'; +import { Component, effect, inject } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule } from '@angular/forms'; import { DomSanitizer, SafeHtml } from '@angular/platform-browser'; @@ -56,6 +56,18 @@ export class IssueDetail { this.showCreateInEpic = false; } }); + + effect(() => { + void this.issues(); + if (this.issue.startDateMode === 'calculated') { + const newStart = this.calculatedStartDate; + if (this.issue.startDate !== newStart) { + this.issue.startDate = newStart; + this.recalculateEndDate(); + this.saveIssue(); + } + } + }); } protected showAddDependency = false; @@ -78,6 +90,10 @@ export class IssueDetail { 'Technical Story', ]; + protected get hasDependencies(): boolean { + return this.issue.dependsOnIds.length > 0; + } + protected get dependencyIds(): number[] { return this.issue.dependsOnIds; } @@ -116,6 +132,33 @@ export class IssueDetail { await this.saveIssue(); } + protected get calculatedStartDate(): string { + const dates = this.issue.dependsOnIds + .map((id) => this.issuesStore.getById(id)?.endDate) + .filter((d): d is string => !!d); + if (dates.length === 0) return ''; + return dates.reduce((max, d) => (d > max ? d : max)); + } + + protected get startDateModeWarning(): string | null { + if (this.issue.startDateMode !== 'calculated') return null; + if (!this.calculatedStartDate) return "Aucune dépendance n'a de date de fin définie."; + return null; + } + + protected get startDateModeValue(): 'forced' | 'calculated' { + return this.issue.startDateMode; + } + + protected set startDateModeValue(mode: 'forced' | 'calculated') { + this.issue.startDateMode = mode; + if (mode === 'calculated') { + this.issue.startDate = this.calculatedStartDate; + } + this.recalculateEndDate(); + this.saveIssue(); + } + protected get estimatedTimeValue(): number | null { return this.issue.estimatedTime; } @@ -126,12 +169,14 @@ export class IssueDetail { } private recalculateEndDate(): void { - const { startDate, estimatedTime } = this.issue; - if (!startDate || estimatedTime === null || estimatedTime <= 0) { + const effectiveStart = + this.issue.startDateMode === 'calculated' ? this.calculatedStartDate : this.issue.startDate; + const { estimatedTime } = this.issue; + if (!effectiveStart || estimatedTime === null || estimatedTime <= 0) { this.issue.endDate = ''; return; } - const start = new Date(startDate); + const start = new Date(effectiveStart); const extraDays = Math.max(0, Math.ceil(estimatedTime / 8) - 1); start.setDate(start.getDate() + extraDays); this.issue.endDate = start.toISOString().split('T')[0]; @@ -197,6 +242,7 @@ export class IssueDetail { epic: this.issue.name, name, startDate: '', + startDateMode: 'calculated', endDate: '', dueDate: '', description: '', @@ -301,7 +347,7 @@ export class IssueDetail { if (startDate && endDate && startDate > endDate) { return 'La date de début ne peut pas être supérieure à la date de fin.'; } - if (startDate && this.issue.dependsOnIds.length > 0) { + if (this.issue.startDateMode !== 'calculated' && startDate && this.issue.dependsOnIds.length > 0) { for (const depId of this.issue.dependsOnIds) { const dep = this.issuesStore.getById(depId); if (dep?.endDate && startDate < dep.endDate) { @@ -492,6 +538,7 @@ export class IssueDetail { epic: '', name: '', startDate: '', + startDateMode: 'calculated', endDate: '', dueDate: '', description: '', @@ -515,6 +562,7 @@ export class IssueDetail { epic: '', name: '', startDate: '', + startDateMode: 'calculated', endDate: '', dueDate: '', description: '', diff --git a/src/app/issues/issues.spec.ts b/src/app/issues/issues.spec.ts index 944832c..1657d29 100644 --- a/src/app/issues/issues.spec.ts +++ b/src/app/issues/issues.spec.ts @@ -14,6 +14,7 @@ const makeIssue = (overrides: Partial = {}): IssueEntity => ({ epic: '', name: 'Test Issue', startDate: '', + startDateMode: 'forced', endDate: '', dueDate: '', description: '', diff --git a/src/app/issues/issues.store.spec.ts b/src/app/issues/issues.store.spec.ts index 2db28f4..ba08360 100644 --- a/src/app/issues/issues.store.spec.ts +++ b/src/app/issues/issues.store.spec.ts @@ -11,6 +11,7 @@ const makeIssue = (overrides: Partial = {}): IssueEntity => ({ epic: '', name: 'Test Issue', startDate: '', + startDateMode: 'forced', endDate: '', dueDate: '', description: '', @@ -183,6 +184,123 @@ describe('IssuesStore', () => { expect(store.getById(991)?.startDate).toBe('2026-01-01'); expect(store.getById(991)?.endDate).toBe('2026-01-31'); }); + + it('restores startDateMode when the API response omits it', async () => { + const issue = makeIssue({ id: 1, startDateMode: 'calculated' }); + const p = store.upsert(issue); + const apiResponse = { ...makeIssue({ id: 1 }), startDateMode: undefined }; + httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(apiResponse); + await p; + expect(store.getById(1)?.startDateMode).toBe('calculated'); + }); + + it('restores linkedIssueIds in comments when API response omits them', async () => { + const issueWithComment = makeIssue({ id: 1, comments: [{ id: 10, text: 'hello', createdAt: '', updatedAt: null, linkedIssueIds: [2, 3] }] }); + const apiResponse = makeIssue({ id: 1, comments: [{ id: 10, text: 'hello', createdAt: '', updatedAt: null, linkedIssueIds: undefined as any }] }); + const p = store.upsert(issueWithComment); + httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(apiResponse); + await p; + expect(store.getById(1)?.comments[0].linkedIssueIds).toEqual([2, 3]); + }); + + it('keeps existing linkedIssueIds in comments when already present in API response', async () => { + const issue = makeIssue({ id: 1, comments: [{ id: 10, text: 'hello', createdAt: '', updatedAt: null, linkedIssueIds: [5] }] }); + const apiResponse = makeIssue({ id: 1, comments: [{ id: 10, text: 'hello', createdAt: '', updatedAt: null, linkedIssueIds: [5] }] }); + const p = store.upsert(issue); + httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(apiResponse); + await p; + expect(store.getById(1)?.comments[0].linkedIssueIds).toEqual([5]); + }); + }); + + describe('cascade recalculation of calculated-mode issues', () => { + it('updates startDate of a calculated-mode dependent when its dependency endDate changes', async () => { + await loadWith([ + makeIssue({ id: 1, endDate: '2026-06-01' }), + makeIssue({ id: 2, startDateMode: 'calculated', dependsOnIds: [1], startDate: '2026-06-01', endDate: '' }), + ]); + const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' })); + httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' })); + await p; + expect(store.getById(2)?.startDate).toBe('2026-06-10'); + }); + + it('recalculates endDate of a calculated-mode dependent based on estimatedTime', async () => { + await loadWith([ + makeIssue({ id: 1, endDate: '2026-06-01' }), + makeIssue({ id: 2, startDateMode: 'calculated', dependsOnIds: [1], startDate: '2026-06-01', estimatedTime: 16, endDate: '2026-06-02' }), + ]); + const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' })); + httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' })); + await p; + expect(store.getById(2)?.startDate).toBe('2026-06-10'); + expect(store.getById(2)?.endDate).toBe('2026-06-11'); + }); + + it('clears endDate when dependency loses its endDate', async () => { + await loadWith([ + makeIssue({ id: 1, endDate: '2026-06-10' }), + makeIssue({ id: 2, startDateMode: 'calculated', dependsOnIds: [1], startDate: '2026-06-10', estimatedTime: 8, endDate: '2026-06-10' }), + ]); + const p = store.upsert(makeIssue({ id: 1, endDate: '' })); + httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, endDate: '' })); + await p; + expect(store.getById(2)?.startDate).toBe(''); + expect(store.getById(2)?.endDate).toBe(''); + }); + + it('cascades through a chain A → B → C', async () => { + await loadWith([ + makeIssue({ id: 1, endDate: '2026-06-01' }), + makeIssue({ id: 2, startDateMode: 'calculated', dependsOnIds: [1], startDate: '2026-06-01', estimatedTime: 8, endDate: '2026-06-01' }), + makeIssue({ id: 3, startDateMode: 'calculated', dependsOnIds: [2], startDate: '2026-06-01', estimatedTime: 16, endDate: '2026-06-02' }), + ]); + const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' })); + httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' })); + await p; + expect(store.getById(2)?.startDate).toBe('2026-06-10'); + expect(store.getById(2)?.endDate).toBe('2026-06-10'); + expect(store.getById(3)?.startDate).toBe('2026-06-10'); + expect(store.getById(3)?.endDate).toBe('2026-06-11'); + }); + + it('does not affect forced-mode issues', async () => { + await loadWith([ + makeIssue({ id: 1, endDate: '2026-06-01' }), + makeIssue({ id: 2, startDateMode: 'forced', dependsOnIds: [1], startDate: '2026-05-01', endDate: '2026-05-15' }), + ]); + const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' })); + httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' })); + await p; + expect(store.getById(2)?.startDate).toBe('2026-05-01'); + expect(store.getById(2)?.endDate).toBe('2026-05-15'); + }); + + it('uses the latest endDate among multiple dependencies', async () => { + await loadWith([ + makeIssue({ id: 1, endDate: '2026-06-01' }), + makeIssue({ id: 2, endDate: '2026-06-05' }), + makeIssue({ id: 3, startDateMode: 'calculated', dependsOnIds: [1, 2], startDate: '', estimatedTime: 8, endDate: '' }), + ]); + const p = store.upsert(makeIssue({ id: 1, endDate: '2026-06-10' })); + httpMock.expectOne({ method: 'PUT', url: `${API_BASE_URL}/issues/1` }).flush(makeIssue({ id: 1, endDate: '2026-06-10' })); + await p; + expect(store.getById(3)?.startDate).toBe('2026-06-10'); + expect(store.getById(3)?.endDate).toBe('2026-06-10'); + }); + + it('recalculates after deleteById removes a dependency', async () => { + await loadWith([ + makeIssue({ id: 1, endDate: '2026-06-10' }), + makeIssue({ id: 2, endDate: '2026-06-05' }), + makeIssue({ id: 3, startDateMode: 'calculated', dependsOnIds: [1, 2], startDate: '2026-06-10', estimatedTime: 8, endDate: '2026-06-10' }), + ]); + const p = store.deleteById(1); + httpMock.expectOne({ method: 'DELETE', url: `${API_BASE_URL}/issues/1` }).flush(null); + await p; + expect(store.getById(3)?.startDate).toBe('2026-06-05'); + expect(store.getById(3)?.endDate).toBe('2026-06-05'); + }); }); describe('deleteById', () => { diff --git a/src/app/issues/issues.store.ts b/src/app/issues/issues.store.ts index 3a1d0c8..31367c1 100644 --- a/src/app/issues/issues.store.ts +++ b/src/app/issues/issues.store.ts @@ -21,6 +21,7 @@ export type IssueEntity = { epic: string; name: string; startDate: string; + startDateMode: 'forced' | 'calculated'; endDate: string; dueDate: string; description: string; @@ -68,6 +69,7 @@ export class IssuesStore { const { id: _id, ...body } = normalized; const created = this.normalizeIssue(await firstValueFrom(this.api.create(body))); this.data.update((issues) => [...issues, created]); + this.recalculateCalculatedIssues(); return created; } else { const apiResult = await firstValueFrom(this.api.update(normalized.id, normalized)); @@ -80,6 +82,10 @@ export class IssuesStore { return { ...c, linkedIssueIds: sent?.linkedIssueIds ?? [] }; }); } + // L'API ne retourne pas startDateMode : on le restaure depuis les données envoyées. + if (apiResult.startDateMode == null) { + apiResult.startDateMode = normalized.startDateMode; + } const updated = this.normalizeIssue(apiResult); this.data.update((issues) => { const idx = issues.findIndex((i) => i.id === normalized.id); @@ -88,6 +94,7 @@ export class IssuesStore { copy[idx] = updated; return copy; }); + this.recalculateCalculatedIssues(); return updated; } } @@ -99,6 +106,41 @@ export class IssuesStore { .filter((i) => i.id !== id) .map((i) => ({ ...i, dependsOnIds: i.dependsOnIds.filter((d) => d !== id) })), ); + this.recalculateCalculatedIssues(); + } + + private recalculateCalculatedIssues(): void { + let anyChanged: boolean; + do { + anyChanged = false; + this.data.update((issues) => { + const result = issues.map((issue) => { + if (issue.startDateMode !== 'calculated') return issue; + const newStart = this.computeStartDate(issue, issues); + const newEnd = this.computeEndDate(newStart, issue.estimatedTime); + if (issue.startDate === newStart && issue.endDate === newEnd) return issue; + anyChanged = true; + return { ...issue, startDate: newStart, endDate: newEnd }; + }); + return anyChanged ? result : issues; + }); + } while (anyChanged); + } + + private computeStartDate(issue: IssueEntity, allIssues: IssueEntity[]): string { + const dates = issue.dependsOnIds + .map((id) => allIssues.find((i) => i.id === id)?.endDate) + .filter((d): d is string => !!d); + if (dates.length === 0) return ''; + return dates.reduce((max, d) => (d > max ? d : max)); + } + + private computeEndDate(startDate: string, estimatedTime: number | null): string { + if (!startDate || estimatedTime === null || estimatedTime <= 0) return ''; + const start = new Date(startDate); + const extraDays = Math.max(0, Math.ceil(estimatedTime / 8) - 1); + start.setDate(start.getDate() + extraDays); + return start.toISOString().split('T')[0]; } private normalizeIssue( @@ -113,6 +155,7 @@ export class IssuesStore { ...issue, type: issue.type ?? 'Story', startDate: issue.startDate ?? '', + startDateMode: issue.startDateMode === 'calculated' ? 'calculated' : 'forced', endDate: issue.endDate ?? '', estimatedTime: issue.estimatedTime ?? null, dependsOnIds: normalizedDependencies, diff --git a/src/app/milestones/milestone-detail/milestone-detail.spec.ts b/src/app/milestones/milestone-detail/milestone-detail.spec.ts index fd114cc..58c0e95 100644 --- a/src/app/milestones/milestone-detail/milestone-detail.spec.ts +++ b/src/app/milestones/milestone-detail/milestone-detail.spec.ts @@ -15,6 +15,7 @@ const makeIssue = (overrides: Partial = {}): IssueEntity => ({ epic: '', name: 'Test Issue', startDate: '', + startDateMode: 'forced', endDate: '', dueDate: '', description: '', diff --git a/src/app/milestones/milestone-detail/milestone-detail.ts b/src/app/milestones/milestone-detail/milestone-detail.ts index 410a413..5e15570 100644 --- a/src/app/milestones/milestone-detail/milestone-detail.ts +++ b/src/app/milestones/milestone-detail/milestone-detail.ts @@ -165,6 +165,7 @@ export class MilestoneDetail { epic: '', name, startDate: '', + startDateMode: 'forced', endDate: '', dueDate: '', description: '',