From 0e87184de9d0a4adfd5d9280d7bba21bce355457 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:33:14 +0000 Subject: [PATCH 001/388] feat(tracker): register Gantt view tab (placeholder UI) Signed-off-by: Michael Uray --- models/tracker/src/viewlets.ts | 48 +++++++++++++++++++ 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 ++- plugins/tracker-resources/package.json | 4 +- .../src/components/gantt/GanttView.svelte | 41 ++++++++++++++++ plugins/tracker-resources/src/index.ts | 2 + plugins/tracker-resources/src/plugin.ts | 11 ++++- 18 files changed, 182 insertions(+), 15 deletions(-) create mode 100644 plugins/tracker-resources/src/components/gantt/GanttView.svelte diff --git a/models/tracker/src/viewlets.ts b/models/tracker/src/viewlets.ts index 509e00d7d0e..194a5a7554d 100644 --- a/models/tracker/src/viewlets.ts +++ b/models/tracker/src/viewlets.ts @@ -212,6 +212,30 @@ export function issueConfig ( ] } +export function ganttViewOptions (): ViewOptionsModel { + // PR 2 ships a minimal read-only Gantt. Group-by / dropdown ViewOptions + // are intentionally NOT advertised here — the canvas does not honour them + // yet. They will be added in PR 3 alongside drag/edit + Component swimlanes + // so users never see options that have no effect. + return { + groupBy: [], + orderBy: [ + ['startDate', SortingOrder.Ascending], + ['rank', SortingOrder.Ascending], + ['dueDate', SortingOrder.Ascending] + ], + other: [showColorsViewOption] + } +} + +export function ganttConfig (): BuildModelKey[] { + // Minimal config — Gantt drives its own column layout. + return [ + { key: '', presenter: tracker.component.PriorityEditor, label: tracker.string.Priority, props: { kind: 'list', size: 'small' } }, + { key: '', presenter: tracker.component.IssuePresenter, label: tracker.string.Issue } + ] +} + export function defineViewlets (builder: Builder): void { builder.createDoc( view.class.ViewletDescriptor, @@ -224,6 +248,30 @@ export function defineViewlets (builder: Builder): void { tracker.viewlet.Kanban ) + builder.createDoc( + view.class.ViewletDescriptor, + core.space.Model, + { + label: tracker.string.Gantt, + icon: tracker.icon.Issues, + component: tracker.component.GanttView + }, + tracker.viewlet.Gantt + ) + + builder.createDoc( + view.class.Viewlet, + core.space.Model, + { + attachTo: tracker.class.Issue, + descriptor: tracker.viewlet.Gantt, + viewOptions: ganttViewOptions(), + configOptions: { strict: true, hiddenKeys: ['title'] }, + config: ganttConfig() + }, + tracker.viewlet.IssueGantt + ) + builder.createDoc( view.class.Viewlet, core.space.Model, diff --git a/plugins/tracker-assets/lang/cs.json b/plugins/tracker-assets/lang/cs.json index dea411668af..9f87e86aff4 100644 --- a/plugins/tracker-assets/lang/cs.json +++ b/plugins/tracker-assets/lang/cs.json @@ -283,7 +283,12 @@ "UnsetParentIssue": "Odebrat nadřazený úkol", "ForbidCreateProjectPermission": "Zakázat vytvoření projektu", "ForbidCreateProjectPermissionDescription": "Zakazuje uživatelům vytvářet nové projekty", - "AllowCreatingIssues": "Povolit vytváření úkolů" + "AllowCreatingIssues": "Povolit vytváření úkolů", + "Day": "Den", + "Gantt": "Gantt", + "Month": "Měsíc", + "Quarter": "Čtvrtletí", + "Week": "Týden" }, "status": {} } diff --git a/plugins/tracker-assets/lang/de.json b/plugins/tracker-assets/lang/de.json index d1e698c9c9a..51aa73d1518 100644 --- a/plugins/tracker-assets/lang/de.json +++ b/plugins/tracker-assets/lang/de.json @@ -293,7 +293,12 @@ "UnsetParentIssue": "Übergeordnete Aufgabe entfernen", "ForbidCreateProjectPermission": "Projekterstellung verbieten", "ForbidCreateProjectPermissionDescription": "Verbietet Benutzern das Erstellen neuer Projekte", - "AllowCreatingIssues": "Erstellen von Aufgaben erlauben" + "AllowCreatingIssues": "Erstellen von Aufgaben erlauben", + "Day": "Tag", + "Gantt": "Gantt", + "Month": "Monat", + "Quarter": "Quartal", + "Week": "Woche" }, "status": {} } diff --git a/plugins/tracker-assets/lang/en.json b/plugins/tracker-assets/lang/en.json index 3d856a20031..7dad04bee41 100644 --- a/plugins/tracker-assets/lang/en.json +++ b/plugins/tracker-assets/lang/en.json @@ -293,7 +293,12 @@ "UnsetParentIssue": "Unset parent issue", "ForbidCreateProjectPermission": "Forbid create project", "ForbidCreateProjectPermissionDescription": "Forbid users creating new projects", - "AllowCreatingIssues": "Allow creating issues" + "AllowCreatingIssues": "Allow creating issues", + "Day": "Day", + "Gantt": "Gantt", + "Month": "Month", + "Quarter": "Quarter", + "Week": "Week" }, "status": {} } diff --git a/plugins/tracker-assets/lang/es.json b/plugins/tracker-assets/lang/es.json index ed9795ef41b..127dc0a4f63 100644 --- a/plugins/tracker-assets/lang/es.json +++ b/plugins/tracker-assets/lang/es.json @@ -276,7 +276,12 @@ "UnsetParentIssue": "Unset parent issue", "ForbidCreateProjectPermission": "Prohibir crear proyecto", "ForbidCreateProjectPermissionDescription": "Prohíbe a los usuarios crear nuevos proyectos", - "AllowCreatingIssues": "Permitir crear incidencias" + "AllowCreatingIssues": "Permitir crear incidencias", + "Day": "Día", + "Gantt": "Gantt", + "Month": "Mes", + "Quarter": "Trimestre", + "Week": "Semana" }, "status": {} } diff --git a/plugins/tracker-assets/lang/fr.json b/plugins/tracker-assets/lang/fr.json index daa7f4eaaba..4bf83349e83 100644 --- a/plugins/tracker-assets/lang/fr.json +++ b/plugins/tracker-assets/lang/fr.json @@ -276,7 +276,12 @@ "UnsetParentIssue": "Désélectionner l'issue parent", "ForbidCreateProjectPermission": "Interdire la création de projet", "ForbidCreateProjectPermissionDescription": "Interdit aux utilisateurs de créer de nouveaux projets", - "AllowCreatingIssues": "Autoriser la création d'issues" + "AllowCreatingIssues": "Autoriser la création d'issues", + "Day": "Jour", + "Gantt": "Gantt", + "Month": "Mois", + "Quarter": "Trimestre", + "Week": "Semaine" }, "status": {} } diff --git a/plugins/tracker-assets/lang/it.json b/plugins/tracker-assets/lang/it.json index 7fe2285f261..02d01f2850c 100644 --- a/plugins/tracker-assets/lang/it.json +++ b/plugins/tracker-assets/lang/it.json @@ -276,7 +276,12 @@ "UnsetParentIssue": "Annulla l'issue genitore", "ForbidCreateProjectPermission": "Vieta creazione progetto", "ForbidCreateProjectPermissionDescription": "Vieta agli utenti di creare nuovi progetti", - "AllowCreatingIssues": "Consenti la creazione di issue" + "AllowCreatingIssues": "Consenti la creazione di issue", + "Day": "Giorno", + "Gantt": "Gantt", + "Month": "Mese", + "Quarter": "Trimestre", + "Week": "Settimana" }, "status": {} } diff --git a/plugins/tracker-assets/lang/ja.json b/plugins/tracker-assets/lang/ja.json index 34426b09875..61b13474fd4 100644 --- a/plugins/tracker-assets/lang/ja.json +++ b/plugins/tracker-assets/lang/ja.json @@ -276,7 +276,12 @@ "UnsetParentIssue": "親イシューの設定を解除", "ForbidCreateProjectPermission": "プロジェクト作成禁止", "ForbidCreateProjectPermissionDescription": "ユーザーが新しいプロジェクトを作成することを禁止します", - "AllowCreatingIssues": "イシューの作成を許可" + "AllowCreatingIssues": "イシューの作成を許可", + "Day": "日", + "Gantt": "Gantt", + "Month": "月", + "Quarter": "四半期", + "Week": "週" }, "status": {} } diff --git a/plugins/tracker-assets/lang/ko.json b/plugins/tracker-assets/lang/ko.json index 32b4b4c75c6..5d8c5ca23b6 100644 --- a/plugins/tracker-assets/lang/ko.json +++ b/plugins/tracker-assets/lang/ko.json @@ -276,7 +276,12 @@ "UnsetParentIssue": "상위 이슈 설정 해제", "ForbidCreateProjectPermission": "프로젝트 생성 금지", "ForbidCreateProjectPermissionDescription": "사용자의 새 프로젝트 생성을 금지", - "AllowCreatingIssues": "이슈 생성 허용" + "AllowCreatingIssues": "이슈 생성 허용", + "Day": "일", + "Gantt": "Gantt", + "Month": "월", + "Quarter": "분기", + "Week": "주" }, "status": {} } diff --git a/plugins/tracker-assets/lang/pt-br.json b/plugins/tracker-assets/lang/pt-br.json index 9d8ec0214e2..c32447c979d 100644 --- a/plugins/tracker-assets/lang/pt-br.json +++ b/plugins/tracker-assets/lang/pt-br.json @@ -276,7 +276,12 @@ "UnsetParentIssue": "Desmarcar problema pai", "ForbidCreateProjectPermission": "Proibir criação de projeto", "ForbidCreateProjectPermissionDescription": "Proíbe os usuários de criar novos projetos", - "AllowCreatingIssues": "Permitir criar problemas" + "AllowCreatingIssues": "Permitir criar problemas", + "Day": "Dia", + "Gantt": "Gantt", + "Month": "Mês", + "Quarter": "Trimestre", + "Week": "Semana" }, "status": {} } diff --git a/plugins/tracker-assets/lang/pt.json b/plugins/tracker-assets/lang/pt.json index 82a640e2c59..d83e2745a75 100644 --- a/plugins/tracker-assets/lang/pt.json +++ b/plugins/tracker-assets/lang/pt.json @@ -276,7 +276,12 @@ "UnsetParentIssue": "Desmarcar problema pai", "ForbidCreateProjectPermission": "Proibir criação de projeto", "ForbidCreateProjectPermissionDescription": "Proíbe os usuários de criar novos projetos", - "AllowCreatingIssues": "Permitir criar problemas" + "AllowCreatingIssues": "Permitir criar problemas", + "Day": "Dia", + "Gantt": "Gantt", + "Month": "Mês", + "Quarter": "Trimestre", + "Week": "Semana" }, "status": {} } diff --git a/plugins/tracker-assets/lang/ru.json b/plugins/tracker-assets/lang/ru.json index 7ccd4cb5ccd..142a3eec2b3 100644 --- a/plugins/tracker-assets/lang/ru.json +++ b/plugins/tracker-assets/lang/ru.json @@ -293,7 +293,12 @@ "UnsetParentIssue": "Снять родительскую задачу", "ForbidCreateProjectPermission": "Запретить создание проекта", "ForbidCreateProjectPermissionDescription": "Запрещает пользователям создавать новые проекты", - "AllowCreatingIssues": "Разрешить создание задач" + "AllowCreatingIssues": "Разрешить создание задач", + "Day": "День", + "Gantt": "Gantt", + "Month": "Месяц", + "Quarter": "Квартал", + "Week": "Неделя" }, "status": {} } diff --git a/plugins/tracker-assets/lang/tr.json b/plugins/tracker-assets/lang/tr.json index ec118014359..0e040c303d7 100644 --- a/plugins/tracker-assets/lang/tr.json +++ b/plugins/tracker-assets/lang/tr.json @@ -274,7 +274,12 @@ "IssueStatus": "Durum", "Extensions": "Uzantılar", "UnsetParentIssue": "Üst sorunu kaldır", - "AllowCreatingIssues": "Sorun oluşturmaya izin ver" + "AllowCreatingIssues": "Sorun oluşturmaya izin ver", + "Day": "Gün", + "Gantt": "Gantt", + "Month": "Ay", + "Quarter": "Çeyrek", + "Week": "Hafta" }, "status": {} } diff --git a/plugins/tracker-assets/lang/zh.json b/plugins/tracker-assets/lang/zh.json index a424d856cac..7bf3bc34d7c 100644 --- a/plugins/tracker-assets/lang/zh.json +++ b/plugins/tracker-assets/lang/zh.json @@ -293,7 +293,12 @@ "UnsetParentIssue": "取消父问题", "ForbidCreateProjectPermission": "禁止创建项目", "ForbidCreateProjectPermissionDescription": "禁止用户创建新项目", - "AllowCreatingIssues": "允许创建问题" + "AllowCreatingIssues": "允许创建问题", + "Day": "日", + "Gantt": "Gantt", + "Month": "月", + "Quarter": "季度", + "Week": "周" }, "status": {} } diff --git a/plugins/tracker-resources/package.json b/plugins/tracker-resources/package.json index 7afa566df43..7ae0b97d0a3 100644 --- a/plugins/tracker-resources/package.json +++ b/plugins/tracker-resources/package.json @@ -8,12 +8,14 @@ "build": "compile ui", "build:docs": "api-extractor run --local", "svelte-check": "do-svelte-check", + "test": "jest --passWithNoTests --silent", "_phase:svelte-check": "do-svelte-check", "format": "format src", "build:watch": "compile ui", "_phase:build": "compile ui", "_phase:format": "format src", - "_phase:validate": "compile validate" + "_phase:validate": "compile validate", + "_phase:test": "jest --passWithNoTests --silent" }, "devDependencies": { "svelte-loader": "^3.2.0", diff --git a/plugins/tracker-resources/src/components/gantt/GanttView.svelte b/plugins/tracker-resources/src/components/gantt/GanttView.svelte new file mode 100644 index 00000000000..5a951fd57ab --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/GanttView.svelte @@ -0,0 +1,41 @@ + + + +
+
+
+
+ + diff --git a/plugins/tracker-resources/src/index.ts b/plugins/tracker-resources/src/index.ts index 071a13ac73f..58cec18fb0c 100644 --- a/plugins/tracker-resources/src/index.ts +++ b/plugins/tracker-resources/src/index.ts @@ -52,6 +52,7 @@ import ProjectComponents from './components/components/ProjectComponents.svelte' import CreateIssue from './components/CreateIssue.svelte' import EditRelatedTargets from './components/EditRelatedTargets.svelte' import EditRelatedTargetsPopup from './components/EditRelatedTargetsPopup.svelte' +import GanttView from './components/gantt/GanttView.svelte' import AssigneeEditor from './components/issues/AssigneeEditor.svelte' import DueDatePresenter from './components/issues/DueDatePresenter.svelte' import EditIssue from './components/issues/edit/EditIssue.svelte' @@ -434,6 +435,7 @@ export default async (): Promise => ({ EditComponent, IssuesView, KanbanView, + GanttView, ProjectComponents, IssuePreview, RelationsPopup, diff --git a/plugins/tracker-resources/src/plugin.ts b/plugins/tracker-resources/src/plugin.ts index eed088c59d5..fc1f85177ad 100644 --- a/plugins/tracker-resources/src/plugin.ts +++ b/plugins/tracker-resources/src/plugin.ts @@ -36,6 +36,8 @@ export default mergeIds(trackerId, tracker, { SubIssues: '' as Ref, List: '' as Ref, Kanban: '' as Ref, + Gantt: '' as Ref, + IssueGantt: '' as Ref, MilestoneIssuesList: '' as Ref, ComponentIssuesList: '' as Ref }, @@ -307,7 +309,13 @@ export default mergeIds(trackerId, tracker, { UnsetParent: '' as IntlString, PreviousAssigned: '' as IntlString, EditRelatedTargets: '' as IntlString, - RelatedIssueTargetDescription: '' as IntlString + RelatedIssueTargetDescription: '' as IntlString, + + Day: '' as IntlString, + Gantt: '' as IntlString, + Month: '' as IntlString, + Quarter: '' as IntlString, + Week: '' as IntlString }, component: { NopeComponent: '' as AnyComponent, @@ -347,6 +355,7 @@ export default mergeIds(trackerId, tracker, { EditComponent: '' as AnyComponent, IssuesView: '' as AnyComponent, KanbanView: '' as AnyComponent, + GanttView: '' as AnyComponent, ProjectComponents: '' as AnyComponent, IssuePreview: '' as AnyComponent, RelationsPopup: '' as AnyComponent, From 4868b15a869aaca2b423f7dc869950354d6bd869 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:33:14 +0000 Subject: [PATCH 002/388] feat(tracker-resources): add Gantt lib types Signed-off-by: Michael Uray --- .../src/components/gantt/.gitignore | 3 ++ .../src/components/gantt/lib/types.ts | 50 +++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100644 plugins/tracker-resources/src/components/gantt/.gitignore create mode 100644 plugins/tracker-resources/src/components/gantt/lib/types.ts diff --git a/plugins/tracker-resources/src/components/gantt/.gitignore b/plugins/tracker-resources/src/components/gantt/.gitignore new file mode 100644 index 00000000000..176a11c6ff4 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/.gitignore @@ -0,0 +1,3 @@ +# Override root-level lib/ ignore — this lib/ is source code, not build output. +!lib/ +!lib/** diff --git a/plugins/tracker-resources/src/components/gantt/lib/types.ts b/plugins/tracker-resources/src/components/gantt/lib/types.ts new file mode 100644 index 00000000000..485ecabf5ae --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/lib/types.ts @@ -0,0 +1,50 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { type Ref } from '@hcengineering/core' +import { type Issue, type Component as TrackerComponent, type Milestone } from '@hcengineering/tracker' + +/** Zoom presets — controls pxPerDay and header tick density. */ +export type ZoomLevel = 'day' | 'week' | 'month' | 'quarter' + +/** A single tick on the time-axis header (vertical gridline + label). */ +export interface Tick { + date: number // UTC ms + label: string // pre-formatted, locale-aware + level: 'major' | 'minor' // major ticks render thicker + with text label +} + +/** A row in the flattened layout. May be an issue or a component-swimlane header. */ +export interface LayoutRow { + kind: 'issue' | 'component-swimlane' + /** Y-coord top-edge of the row in canvas pixels. */ + y: number + /** Row height in pixels. */ + height: number + /** Tree depth — 0 for top-level issues. */ + depth: number + /** Whether this row is currently rendered (vs virtually skipped). */ + visible: boolean + /** The issue this row represents — null for swimlane headers. */ + issue: Issue | null + /** The component this swimlane represents — null for issue rows. */ + component: Ref | null + /** True iff this issue has children (renders as summary "claw" bar). */ + isSummary: boolean +} + +/** Cached aggregate dates of a parent issue's children, for summary-bar rendering. */ +export interface SummaryRange { + startDate: number | null + dueDate: number | null +} + +/** Compact view of a Milestone for the canvas overlay. */ +export interface MilestoneMarker { + _id: Ref + label: string + startDate: number | null + targetDate: number +} From 5399bf69819e094aff389ab0b40682ce3e673714 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:33:14 +0000 Subject: [PATCH 003/388] test(tracker-resources): add failing Gantt time-scale tests Signed-off-by: Michael Uray --- .../gantt/lib/__tests__/time-scale.test.ts | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 plugins/tracker-resources/src/components/gantt/lib/__tests__/time-scale.test.ts diff --git a/plugins/tracker-resources/src/components/gantt/lib/__tests__/time-scale.test.ts b/plugins/tracker-resources/src/components/gantt/lib/__tests__/time-scale.test.ts new file mode 100644 index 00000000000..e0b3b8262d2 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/lib/__tests__/time-scale.test.ts @@ -0,0 +1,78 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { createTimeScale, snapToUtcMidnight } from '../time-scale' + +const DAY_MS = 86_400_000 + +describe('snapToUtcMidnight', () => { + it('returns 0 unchanged', () => { + expect(snapToUtcMidnight(0)).toBe(0) + }) + + it('rounds down to UTC midnight', () => { + const t = Date.UTC(2026, 4, 15, 17, 30, 45) // 2026-05-15 17:30:45 UTC + expect(snapToUtcMidnight(t)).toBe(Date.UTC(2026, 4, 15)) + }) + + it('is idempotent', () => { + const t = Date.UTC(2026, 0, 1) + expect(snapToUtcMidnight(snapToUtcMidnight(t))).toBe(t) + }) +}) + +describe('createTimeScale', () => { + const origin = Date.UTC(2026, 0, 1) // 2026-01-01 UTC + + it('week zoom: pxPerDay = 14', () => { + const ts = createTimeScale('week', origin) + expect(ts.pxPerDay).toBe(14) + }) + + it('day/month/quarter zoom values match preset', () => { + expect(createTimeScale('day', origin).pxPerDay).toBe(32) + expect(createTimeScale('month', origin).pxPerDay).toBe(4) + expect(createTimeScale('quarter', origin).pxPerDay).toBe(1.5) + }) + + it('toX(origin) === 0', () => { + const ts = createTimeScale('week', origin) + expect(ts.toX(origin)).toBe(0) + }) + + it('toX(origin + 7d) === 7 * pxPerDay', () => { + const ts = createTimeScale('week', origin) + expect(ts.toX(origin + 7 * DAY_MS)).toBe(7 * 14) + }) + + it('fromX is inverse of toX (snapped)', () => { + const ts = createTimeScale('week', origin) + const t = origin + 5 * DAY_MS + expect(ts.fromX(ts.toX(t))).toBe(t) + }) + + it('week zoom emits weekly ticks aligned to Monday', () => { + const ts = createTimeScale('week', origin) + const ticks = ts.ticks([origin, origin + 30 * DAY_MS]) + expect(ticks.length).toBeGreaterThanOrEqual(4) + expect(ticks.length).toBeLessThanOrEqual(6) + // First tick is the first Monday on or after origin + expect(ticks[0].date).toBeGreaterThanOrEqual(origin) + expect(ticks[0].date).toBeLessThan(origin + 7 * DAY_MS) + // Every tick is on a Monday. + for (const t of ticks) { + expect(new Date(t.date).getUTCDay()).toBe(1) + } + expect(ticks.every(t => Number.isInteger(t.date))).toBe(true) + }) + + it('all tick dates are UTC midnights', () => { + const ts = createTimeScale('week', origin) + const ticks = ts.ticks([origin, origin + 30 * DAY_MS]) + for (const t of ticks) { + expect(snapToUtcMidnight(t.date)).toBe(t.date) + } + }) +}) From de2652ec68cd459917705fa358d16ab1e3ec962f Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:33:14 +0000 Subject: [PATCH 004/388] feat(tracker-resources): implement Gantt time-scale lib Signed-off-by: Michael Uray --- .../src/components/gantt/lib/time-scale.ts | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 plugins/tracker-resources/src/components/gantt/lib/time-scale.ts diff --git a/plugins/tracker-resources/src/components/gantt/lib/time-scale.ts b/plugins/tracker-resources/src/components/gantt/lib/time-scale.ts new file mode 100644 index 00000000000..da290d0389b --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/lib/time-scale.ts @@ -0,0 +1,131 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { type Tick, type ZoomLevel } from './types' + +const DAY_MS = 86_400_000 + +const PX_PER_DAY: Record = { + day: 32, + week: 14, + month: 4, + quarter: 1.5 +} + +/** Snap any Timestamp (ms) to the start of its UTC day. */ +export function snapToUtcMidnight (t: number): number { + const d = new Date(t) + return Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()) +} + +export interface TimeScale { + /** Pixel width of one calendar day at the current zoom. */ + pxPerDay: number + /** Convert a Timestamp to its X coordinate (relative to origin). */ + toX: (t: number) => number + /** Convert an X coordinate back to a snapped Timestamp. */ + fromX: (x: number) => number + /** Generate header ticks across [from, to]. */ + ticks: (range: [number, number]) => Tick[] +} + +export function createTimeScale (zoom: ZoomLevel, origin: number): TimeScale { + const pxPerDay = PX_PER_DAY[zoom] + const originSnapped = snapToUtcMidnight(origin) + + const toX = (t: number): number => ((t - originSnapped) / DAY_MS) * pxPerDay + const fromX = (x: number): number => snapToUtcMidnight(originSnapped + (x / pxPerDay) * DAY_MS) + + const ticks = (range: [number, number]): Tick[] => { + const [from, to] = range + const fromDay = snapToUtcMidnight(from) + const result: Tick[] = [] + let cursor = fromDay + + switch (zoom) { + case 'day': { + while (cursor <= to) { + const d = new Date(cursor) + const isMonday = d.getUTCDay() === 1 + result.push({ + date: cursor, + label: d.getUTCDate().toString(), + level: isMonday ? 'major' : 'minor' + }) + cursor += DAY_MS + } + break + } + case 'week': { + const d = new Date(cursor) + const dow = d.getUTCDay() // 0=Sun + const offsetToMonday = ((1 - dow) + 7) % 7 + cursor += offsetToMonday * DAY_MS + while (cursor <= to) { + const c = new Date(cursor) + const isFirstWeekOfMonth = c.getUTCDate() <= 7 + result.push({ + date: cursor, + label: `W${isoWeekNumber(c)}`, + level: isFirstWeekOfMonth ? 'major' : 'minor' + }) + cursor += 7 * DAY_MS + } + break + } + case 'month': { + const start = new Date(cursor) + let y = start.getUTCFullYear() + let m = start.getUTCMonth() + cursor = Date.UTC(y, m, 1) + while (cursor <= to) { + const c = new Date(cursor) + result.push({ + date: cursor, + label: c.toLocaleString(undefined, { month: 'short', timeZone: 'UTC' }), + level: c.getUTCMonth() === 0 ? 'major' : 'minor' + }) + m += 1 + if (m > 11) { m = 0; y += 1 } + cursor = Date.UTC(y, m, 1) + } + break + } + case 'quarter': { + const start = new Date(cursor) + let y = start.getUTCFullYear() + let q = Math.floor(start.getUTCMonth() / 3) + cursor = Date.UTC(y, q * 3, 1) + while (cursor <= to) { + const c = new Date(cursor) + const qNum = Math.floor(c.getUTCMonth() / 3) + 1 + result.push({ + date: cursor, + label: `Q${qNum} ${c.getUTCFullYear()}`, + level: qNum === 1 ? 'major' : 'minor' + }) + q += 1 + if (q > 3) { q = 0; y += 1 } + cursor = Date.UTC(y, q * 3, 1) + } + break + } + } + + return result + } + + return { pxPerDay, toX, fromX, ticks } +} + +/** ISO 8601 week number for a UTC date. */ +function isoWeekNumber (d: Date): number { + const target = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate())) + const dayNum = (target.getUTCDay() + 6) % 7 + target.setUTCDate(target.getUTCDate() - dayNum + 3) + const firstThursday = new Date(Date.UTC(target.getUTCFullYear(), 0, 4)) + const diff = target.getTime() - firstThursday.getTime() + return 1 + Math.round(diff / (7 * DAY_MS)) +} From 1ba2a158440318106fb3fa9f23dc8e230e80e890 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:33:14 +0000 Subject: [PATCH 005/388] test(tracker-resources): add failing Gantt layout tests Signed-off-by: Michael Uray --- .../gantt/lib/__tests__/layout.test.ts | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 plugins/tracker-resources/src/components/gantt/lib/__tests__/layout.test.ts diff --git a/plugins/tracker-resources/src/components/gantt/lib/__tests__/layout.test.ts b/plugins/tracker-resources/src/components/gantt/lib/__tests__/layout.test.ts new file mode 100644 index 00000000000..a58bd4ad8b4 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/lib/__tests__/layout.test.ts @@ -0,0 +1,99 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import type { Issue } from '@hcengineering/tracker' +import { buildLayout, filterVisibleRows } from '../layout' +import { type LayoutRow } from '../types' + +function fakeIssue (id: string, parentId?: string, hasChildren = false): Issue { + return { + _id: id as any, + _class: 'tracker:class:Issue' as any, + title: `Issue ${id}`, + space: 'project-1' as any, + component: null, + milestone: null, + startDate: null, + dueDate: null, + parents: parentId !== undefined ? [{ parentId: parentId as any }] : [], + childInfo: hasChildren ? [{ childId: 'fake-child' as any, count: 0, category: '' }] : [], + estimation: 0, + remainingTime: 0, + reportedTime: 0, + reports: 0, + subIssues: 0, + priority: 0, + status: '' as any, + attachedTo: 'tracker:ids:NoParent' as any + } as unknown as Issue +} + +const ROW_H = 28 + +describe('buildLayout (no grouping)', () => { + it('flattens a flat list of root issues', () => { + const issues = [fakeIssue('a'), fakeIssue('b')] + const rows = buildLayout(issues, 'none', ROW_H) + expect(rows).toHaveLength(2) + expect(rows[0].issue?._id).toBe('a') + expect(rows[1].issue?._id).toBe('b') + expect(rows[0].depth).toBe(0) + expect(rows[0].y).toBe(0) + expect(rows[1].y).toBe(ROW_H) + }) + + it('places children below parent with depth+1', () => { + const a = fakeIssue('a', undefined, true) + const child = fakeIssue('a.1', 'a') + const rows = buildLayout([a, child], 'none', ROW_H) + expect(rows.find(r => r.issue?._id === 'a.1')?.depth).toBe(1) + }) + + it('marks parent issues as summary rows', () => { + const a = fakeIssue('a', undefined, true) + const child = fakeIssue('a.1', 'a') + const rows = buildLayout([a, child], 'none', ROW_H) + const parentRow = rows.find(r => r.issue?._id === 'a')! + expect(parentRow.isSummary).toBe(true) + expect(rows.find(r => r.issue?._id === 'a.1')?.isSummary).toBe(false) + }) + + it('row Y coordinates are sequential multiples of rowHeight', () => { + const issues = [fakeIssue('a'), fakeIssue('b'), fakeIssue('c')] + const rows = buildLayout(issues, 'none', ROW_H) + expect(rows.map(r => r.y)).toEqual([0, ROW_H, 2 * ROW_H]) + }) +}) + +describe('filterVisibleRows', () => { + it('returns only rows whose Y range intersects the viewport (overscan=0)', () => { + const all: LayoutRow[] = [ + { kind: 'issue', y: 0, height: ROW_H, depth: 0, visible: true, issue: null, component: null, isSummary: false }, + { kind: 'issue', y: 100, height: ROW_H, depth: 0, visible: true, issue: null, component: null, isSummary: false }, + { kind: 'issue', y: 5000, height: ROW_H, depth: 0, visible: true, issue: null, component: null, isSummary: false } + ] + const visible = filterVisibleRows(all, 80, 60, 0) + expect(visible.map(r => r.y)).toEqual([100]) + }) + + it('default overscan brings adjacent rows into the visible set', () => { + const all: LayoutRow[] = [ + { kind: 'issue', y: 0, height: ROW_H, depth: 0, visible: true, issue: null, component: null, isSummary: false }, + { kind: 'issue', y: 100, height: ROW_H, depth: 0, visible: true, issue: null, component: null, isSummary: false }, + { kind: 'issue', y: 5000, height: ROW_H, depth: 0, visible: true, issue: null, component: null, isSummary: false } + ] + const visible = filterVisibleRows(all, 80, 60) + expect(visible.map(r => r.y).sort((a, b) => a - b)).toEqual([0, 100]) + }) + + it('honours an explicit overscan', () => { + const all: LayoutRow[] = [ + { kind: 'issue', y: 0, height: ROW_H, depth: 0, visible: true, issue: null, component: null, isSummary: false }, + { kind: 'issue', y: 1000, height: ROW_H, depth: 0, visible: true, issue: null, component: null, isSummary: false } + ] + const visible = filterVisibleRows(all, 950, 50, 200) + expect(visible.map(r => r.y)).toEqual([1000]) + }) +}) From 4ef672f6ab5225a55d452b20226290e412e43a34 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:33:14 +0000 Subject: [PATCH 006/388] feat(tracker-resources): implement Gantt layout lib Signed-off-by: Michael Uray --- .../src/components/gantt/lib/layout.ts | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 plugins/tracker-resources/src/components/gantt/lib/layout.ts diff --git a/plugins/tracker-resources/src/components/gantt/lib/layout.ts b/plugins/tracker-resources/src/components/gantt/lib/layout.ts new file mode 100644 index 00000000000..48fa27a84a7 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/lib/layout.ts @@ -0,0 +1,72 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { type Issue } from '@hcengineering/tracker' +import { type LayoutRow } from './types' + +export type GroupBy = 'none' | 'component' | 'milestone' + +const DEFAULT_OVERSCAN_PX = 240 + +/** + * Flatten the issues array into rows ordered by (parent → child) DFS, + * computing y-coords from rowHeight. + */ +export function buildLayout (issues: Issue[], _group: GroupBy, rowHeight: number): LayoutRow[] { + const childrenOf = new Map() + const roots: Issue[] = [] + for (const i of issues) { + const parentId = i.parents?.[0]?.parentId + if (parentId !== undefined && parentId !== null) { + const list = childrenOf.get(parentId as unknown as string) ?? [] + list.push(i) + childrenOf.set(parentId as unknown as string, list) + } else { + roots.push(i) + } + } + + const rows: LayoutRow[] = [] + let y = 0 + + function emit (i: Issue, depth: number): void { + const kids = childrenOf.get(i._id as unknown as string) ?? [] + rows.push({ + kind: 'issue', + y, + height: rowHeight, + depth, + visible: true, + issue: i, + component: null, + isSummary: kids.length > 0 + }) + y += rowHeight + for (const c of kids) { + emit(c, depth + 1) + } + } + + for (const r of roots) { + emit(r, 0) + } + + return rows +} + +/** + * Return the subset of rows whose [y, y+height] intersects + * [viewportTop - overscan, viewportTop + viewportHeight + overscan]. + */ +export function filterVisibleRows ( + rows: LayoutRow[], + viewportTop: number, + viewportHeight: number, + overscan: number = DEFAULT_OVERSCAN_PX +): LayoutRow[] { + const min = viewportTop - overscan + const max = viewportTop + viewportHeight + overscan + return rows.filter(r => r.y + r.height >= min && r.y <= max) +} From e7e0472e171efd32850d2bae65c51a5c12221e7e Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:33:14 +0000 Subject: [PATCH 007/388] feat(tracker-resources): add GanttHeader.svelte Signed-off-by: Michael Uray --- .../src/components/gantt/GanttHeader.svelte | 63 +++++++++++++++++++ 1 file changed, 63 insertions(+) create mode 100644 plugins/tracker-resources/src/components/gantt/GanttHeader.svelte diff --git a/plugins/tracker-resources/src/components/gantt/GanttHeader.svelte b/plugins/tracker-resources/src/components/gantt/GanttHeader.svelte new file mode 100644 index 00000000000..eb4d4245ff6 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/GanttHeader.svelte @@ -0,0 +1,63 @@ + + + + + {#each ticks as tick (tick.date)} + {@const x = timeScale.toX(tick.date)} + + + {tick.label} + + {/each} + + + From 2173ab466159d5d66fb28997eb2ffabc617860d6 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:33:14 +0000 Subject: [PATCH 008/388] feat(tracker-resources): add GanttBar.svelte (regular + summary claws) Signed-off-by: Michael Uray --- .../src/components/gantt/GanttBar.svelte | 73 +++++++++++++++++++ 1 file changed, 73 insertions(+) create mode 100644 plugins/tracker-resources/src/components/gantt/GanttBar.svelte diff --git a/plugins/tracker-resources/src/components/gantt/GanttBar.svelte b/plugins/tracker-resources/src/components/gantt/GanttBar.svelte new file mode 100644 index 00000000000..20f23a11029 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/GanttBar.svelte @@ -0,0 +1,73 @@ + + + +{#if visible} + {@const barY = row.y + 6} + {@const barH = row.height - 12} + {#if isSummary} + + + + + {:else} + + {tooltipText} + {/if} +{/if} + + From af2d60af64563e09620f3183b475f6628efed8ee Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:33:14 +0000 Subject: [PATCH 009/388] feat(tracker-resources): add GanttTodayMarker.svelte Signed-off-by: Michael Uray --- .../components/gantt/GanttTodayMarker.svelte | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 plugins/tracker-resources/src/components/gantt/GanttTodayMarker.svelte diff --git a/plugins/tracker-resources/src/components/gantt/GanttTodayMarker.svelte b/plugins/tracker-resources/src/components/gantt/GanttTodayMarker.svelte new file mode 100644 index 00000000000..8a88f6789f7 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/GanttTodayMarker.svelte @@ -0,0 +1,34 @@ + + + +{#if visible} + +{/if} + + From 84355bdaf35979693450ae6bd0f0964b7d5db8aa Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:33:14 +0000 Subject: [PATCH 010/388] feat(tracker-resources): add GanttMilestoneFlag.svelte Signed-off-by: Michael Uray --- .../gantt/GanttMilestoneFlag.svelte | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 plugins/tracker-resources/src/components/gantt/GanttMilestoneFlag.svelte diff --git a/plugins/tracker-resources/src/components/gantt/GanttMilestoneFlag.svelte b/plugins/tracker-resources/src/components/gantt/GanttMilestoneFlag.svelte new file mode 100644 index 00000000000..1e5539fbd56 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/GanttMilestoneFlag.svelte @@ -0,0 +1,53 @@ + + + +{#if visible} + + + {milestone.label} + + + {milestone.label} + +{/if} + + From cc376787511a3ee49cc1bec6546ab30843dd6fba Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:33:14 +0000 Subject: [PATCH 011/388] feat(tracker-resources): add GanttCanvas.svelte Signed-off-by: Michael Uray --- .../src/components/gantt/GanttCanvas.svelte | 75 +++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte diff --git a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte new file mode 100644 index 00000000000..12f0289b591 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte @@ -0,0 +1,75 @@ + + + + + + {#each visibleRows as row (rowKey(row))} + {#if row.issue !== null} + + {/if} + {/each} + + + + {#each milestones as ms (ms._id)} + + {/each} + + + + + + From 528eac42bba8ef11b786758fd6a92582e2706b2a Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:33:14 +0000 Subject: [PATCH 012/388] feat(tracker-resources): add GanttSidebar.svelte Signed-off-by: Michael Uray --- .../src/components/gantt/GanttSidebar.svelte | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte diff --git a/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte b/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte new file mode 100644 index 00000000000..8644b47e1df --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte @@ -0,0 +1,57 @@ + + + +
+ +
+ + From d257021c5dd7ff32ba776590a42560abedc6b43c Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:33:14 +0000 Subject: [PATCH 013/388] feat(tracker-resources): add GanttToolbar.svelte (zoom buttons) Signed-off-by: Michael Uray --- .../src/components/gantt/GanttToolbar.svelte | 46 +++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 plugins/tracker-resources/src/components/gantt/GanttToolbar.svelte diff --git a/plugins/tracker-resources/src/components/gantt/GanttToolbar.svelte b/plugins/tracker-resources/src/components/gantt/GanttToolbar.svelte new file mode 100644 index 00000000000..8589cdd6af5 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/GanttToolbar.svelte @@ -0,0 +1,46 @@ + + + +
+ {#each order as z} +
+ + From 1dfac462ff99d0277f5479fd258b932286433e13 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:33:15 +0000 Subject: [PATCH 014/388] feat(tracker-resources): GanttView wires Sidebar+Canvas+Toolbar with reactive queries PR 2 read-only Gantt is now end-to-end functional: - Two reactive queries (issues, milestones) - buildLayout(issues) -> rows with depth + isSummary - createTimeScale(zoom, dateRange.from) -> date<->px math - Summary ranges computed per parent issue from children's date span - Local zoom state (Day/Week/Month/Quarter) - no ViewletPreference plumbing - Horizontal scroll via canvas-scroller; vertical scroll moves Sidebar via translate Signed-off-by: Michael Uray --- .../src/components/gantt/GanttView.svelte | 202 ++++++++++++++++-- 1 file changed, 183 insertions(+), 19 deletions(-) diff --git a/plugins/tracker-resources/src/components/gantt/GanttView.svelte b/plugins/tracker-resources/src/components/gantt/GanttView.svelte index 5a951fd57ab..a682fd03fff 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttView.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttView.svelte @@ -1,41 +1,205 @@ -
-
-
+
+ + {#if loading} + + {:else} +
+ +
+
+ + +
+
+
+ {/if}
From 787f2ae6c42b7a34cea28d1e0a385010d24967c3 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:33:15 +0000 Subject: [PATCH 015/388] - layout.ts: orphan child issues (parent filtered out) now emit as roots - viewlets.ts: register IssueGantt AFTER IssueKanban so List stays default - viewlets.ts: drop showColorsViewOption (canvas does not honour it yet) - GanttBar.svelte: normalise reversed startDate>dueDate ranges UX: - GanttSidebar adds Title column with sticky two-column header - Sticky time-scale header decoupled from milestone strip (layout fix) - Per-row jump-to-bar arrow (Plane-style) when bar is offscreen - ResizeObserver initialises viewport on mount and on resize Milestones as Gantt rows + collapse: - Milestones group their issues as nested children (depth+1) - Collapsible toggle per parent row, local collapsedIds Set - Milestone summary bar uses existing GanttBar with aggregated range Icon: - Register tracker.icon.Gantt to #timeline svg (Gantt pictogram) Tests: 25/25 jest pass; svelte-check 0 errors. Signed-off-by: Michael Uray --- models/tracker/src/viewlets.ts | 42 ++-- plugins/tracker-assets/src/index.ts | 3 +- .../src/components/gantt/GanttBar.svelte | 16 +- .../src/components/gantt/GanttCanvas.svelte | 31 ++- .../src/components/gantt/GanttSidebar.svelte | 205 ++++++++++++++++-- .../src/components/gantt/GanttView.svelte | 116 ++++++++-- .../gantt/lib/__tests__/layout.test.ts | 138 ++++++++++-- .../src/components/gantt/lib/layout.ts | 120 ++++++++-- .../src/components/gantt/lib/types.ts | 20 +- plugins/tracker/src/index.ts | 1 + 10 files changed, 586 insertions(+), 106 deletions(-) diff --git a/models/tracker/src/viewlets.ts b/models/tracker/src/viewlets.ts index 194a5a7554d..ce6e058e535 100644 --- a/models/tracker/src/viewlets.ts +++ b/models/tracker/src/viewlets.ts @@ -213,10 +213,10 @@ export function issueConfig ( } export function ganttViewOptions (): ViewOptionsModel { - // PR 2 ships a minimal read-only Gantt. Group-by / dropdown ViewOptions - // are intentionally NOT advertised here — the canvas does not honour them - // yet. They will be added in PR 3 alongside drag/edit + Component swimlanes - // so users never see options that have no effect. + // PR 2 ships a minimal read-only Gantt. Group-by, "Show colors" and other + // dropdown ViewOptions are intentionally NOT advertised — the canvas does + // not honour them yet. They will be added in PR 3 alongside drag/edit + + // Component swimlanes so users never see options that have no effect. return { groupBy: [], orderBy: [ @@ -224,7 +224,7 @@ export function ganttViewOptions (): ViewOptionsModel { ['rank', SortingOrder.Ascending], ['dueDate', SortingOrder.Ascending] ], - other: [showColorsViewOption] + other: [] } } @@ -253,25 +253,12 @@ export function defineViewlets (builder: Builder): void { core.space.Model, { label: tracker.string.Gantt, - icon: tracker.icon.Issues, + icon: tracker.icon.Gantt, component: tracker.component.GanttView }, tracker.viewlet.Gantt ) - builder.createDoc( - view.class.Viewlet, - core.space.Model, - { - attachTo: tracker.class.Issue, - descriptor: tracker.viewlet.Gantt, - viewOptions: ganttViewOptions(), - configOptions: { strict: true, hiddenKeys: ['title'] }, - config: ganttConfig() - }, - tracker.viewlet.IssueGantt - ) - builder.createDoc( view.class.Viewlet, core.space.Model, @@ -549,6 +536,23 @@ export function defineViewlets (builder: Builder): void { tracker.viewlet.IssueKanban ) + // Gantt is registered AFTER List + Kanban so List remains the default + // viewlet (ViewletSelector falls back to viewlets[0] when no preference + // is saved). Putting Gantt last avoids surprising users with an empty + // canvas on first visit. + builder.createDoc( + view.class.Viewlet, + core.space.Model, + { + attachTo: tracker.class.Issue, + descriptor: tracker.viewlet.Gantt, + viewOptions: ganttViewOptions(), + configOptions: { strict: true, hiddenKeys: ['title'] }, + config: ganttConfig() + }, + tracker.viewlet.IssueGantt + ) + const componentListViewOptions: ViewOptionsModel = { groupBy: ['lead', 'createdBy', 'modifiedBy'], orderBy: [ diff --git a/plugins/tracker-assets/src/index.ts b/plugins/tracker-assets/src/index.ts index 07aaf1badab..4c64fa48f9e 100644 --- a/plugins/tracker-assets/src/index.ts +++ b/plugins/tracker-assets/src/index.ts @@ -64,5 +64,6 @@ loadMetadata(tracker.icon, { CopyBranch: `${icons}#copyBranch`, Duplicate: `${icons}#duplicate`, TimeReport: `${icons}#timeReport`, - Estimation: `${icons}#estimation` + Estimation: `${icons}#estimation`, + Gantt: `${icons}#timeline` }) diff --git a/plugins/tracker-resources/src/components/gantt/GanttBar.svelte b/plugins/tracker-resources/src/components/gantt/GanttBar.svelte index 20f23a11029..f179d163c18 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttBar.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttBar.svelte @@ -3,10 +3,11 @@ // SPDX-License-Identifier: EPL-2.0 -->
+
@@ -37,21 +121,110 @@ overflow: hidden; background: var(--theme-comp-header-color); } + .sidebar-header { + display: flex; + align-items: center; + gap: 8px; + padding: 0 8px; + border-bottom: 1px solid var(--theme-divider-color); + background: var(--theme-comp-header-color); + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + color: var(--theme-darker-color); + letter-spacing: 0.05em; + box-sizing: border-box; + } + .col-toggle { + flex: 0 0 18px; + display: flex; + align-items: center; + justify-content: center; + } + .col-id { + flex: 0 0 88px; + } + .col-title { + flex: 1 1 auto; + } + .col-jump { + flex: 0 0 28px; + } .sidebar-rows { will-change: transform; } .sidebar-row { display: flex; align-items: center; + gap: 8px; padding-right: 8px; border-bottom: 1px solid var(--theme-divider-color); color: var(--theme-content-color); font-size: 13px; overflow: hidden; - text-overflow: ellipsis; white-space: nowrap; + box-sizing: border-box; + } + .cell-id { + flex: 0 0 88px; + overflow: hidden; + text-overflow: ellipsis; + } + .cell-id.ms-icon { + text-align: center; + color: var(--theme-state-info-color, #6366f1); + font-size: 14px; + } + .cell-title { + flex: 1 1 auto; + overflow: hidden; + text-overflow: ellipsis; + } + .cell-jump { + flex: 0 0 28px; + display: flex; + align-items: center; + justify-content: center; + } + .toggle-btn { + width: 18px; + height: 18px; + padding: 0; + border: none; + background: transparent; + color: var(--theme-darker-color); + font-size: 10px; + line-height: 1; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + } + .toggle-btn:hover { + color: var(--theme-content-color); + } + .jump-btn { + width: 22px; + height: 22px; + padding: 0; + border: 1px solid var(--theme-button-border); + border-radius: 3px; + background: var(--theme-button-default); + color: var(--theme-content-color); + font-size: 13px; + line-height: 1; + cursor: pointer; + display: inline-flex; + align-items: center; + justify-content: center; + } + .jump-btn:hover { + filter: brightness(1.1); } .sidebar-row.summary { font-weight: 600; } + .sidebar-row.milestone { + background: color-mix(in srgb, var(--theme-state-info-color, #6366f1) 6%, transparent); + } diff --git a/plugins/tracker-resources/src/components/gantt/GanttView.svelte b/plugins/tracker-resources/src/components/gantt/GanttView.svelte index a682fd03fff..de1a0690842 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttView.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttView.svelte @@ -8,6 +8,7 @@ import { type Issue, type Milestone } from '@hcengineering/tracker' import { Loading } from '@hcengineering/ui' import { type Viewlet, type ViewOptions } from '@hcengineering/view' + import { onDestroy, onMount } from 'svelte' import tracker from '../../plugin' import GanttCanvas from './GanttCanvas.svelte' import GanttHeader from './GanttHeader.svelte' @@ -24,8 +25,9 @@ export let viewOptions: ViewOptions const ROW_HEIGHT = 28 - const SIDEBAR_WIDTH = 320 - const HEADER_HEIGHT = 32 + const SIDEBAR_WIDTH = 360 + const HEADER_HEIGHT = 56 + const MILESTONE_STRIP_HEIGHT = 22 let issues: Issue[] = [] let milestones: Milestone[] = [] @@ -100,9 +102,26 @@ } $: timeScale = createTimeScale(zoom, dateRange.from) - $: rows = buildLayout(issues, 'none', ROW_HEIGHT) + $: milestoneMarkers = milestones.map(m => ({ + _id: m._id, + label: m.label, + startDate: (m as Milestone & { startDate: number | null }).startDate ?? null, + targetDate: m.targetDate + })) + + // Set of row ids currently collapsed (children hidden). Local state in PR 2. + let collapsedIds: Set = new Set() + function onToggle (e: CustomEvent<{ id: string }>): void { + const next = new Set(collapsedIds) + if (next.has(e.detail.id)) next.delete(e.detail.id) + else next.add(e.detail.id) + collapsedIds = next + } + + $: rows = buildLayout(issues, milestoneMarkers, 'none', { rowHeight: ROW_HEIGHT, collapsedIds }) - // Compute summary ranges for parent issues — aggregate of children's startDate/dueDate + // Compute summary ranges for parent issues + milestones — aggregate of children's + // startDate/dueDate. $: summaryRanges = computeSummaryRanges(rows, issues) function computeSummaryRanges ( @@ -111,6 +130,7 @@ ): Map { const result = new Map() const childrenOf = new Map() + const issuesByMilestone = new Map() for (const i of allIssues) { const p = i.parents?.[0]?.parentId if (p !== undefined && p !== null) { @@ -119,9 +139,27 @@ list.push(i) childrenOf.set(k, list) } + const ms = (i as unknown as { milestone?: string | null }).milestone + if (ms != null) { + const list = issuesByMilestone.get(ms) ?? [] + list.push(i) + issuesByMilestone.set(ms, list) + } } for (const row of layoutRows) { - if (!row.isSummary || row.issue === null) continue + if (!row.isSummary) continue + if (row.kind === 'milestone' && row.milestone !== null) { + const msId = row.milestone._id as unknown as string + const kids = issuesByMilestone.get(msId) ?? [] + const starts = kids.map(k => k.startDate).filter((v): v is number => v !== null && v !== undefined) + const dues = kids.map(k => k.dueDate).filter((v): v is number => v !== null && v !== undefined) + result.set(row.id, { + startDate: starts.length > 0 ? Math.min(...starts) : null, + dueDate: dues.length > 0 ? Math.max(...dues) : null + }) + continue + } + if (row.issue === null) continue const id = (row.issue as Issue)._id as unknown as string const kids = childrenOf.get(id) ?? [] const starts = kids.map(k => k.startDate).filter((v): v is number => v !== null && v !== undefined) @@ -134,13 +172,6 @@ return result } - $: milestoneMarkers = milestones.map(m => ({ - _id: m._id, - label: m.label, - startDate: (m as Milestone & { startDate: number | null }).startDate ?? null, - targetDate: m.targetDate - })) - $: totalCanvasWidth = Math.max(viewportWidth(), timeScale.toX(dateRange.to) - timeScale.toX(dateRange.from)) function viewportWidth (): number { @@ -155,6 +186,34 @@ viewportHeight = t.clientHeight } + function onJump (e: CustomEvent<{ x: number }>): void { + if (scrollerEl !== undefined) { + scrollerEl.scrollTo({ left: Math.max(0, e.detail.x - 80), behavior: 'smooth' }) + } + } + + // Initialise viewport dimensions after mount and on resize so jump-buttons / + // virtualisation start with correct values instead of waiting for the first + // user-driven scroll event. + let resizeObs: ResizeObserver | undefined + function syncViewport (): void { + if (scrollerEl === undefined) return + canvasViewportLeft = scrollerEl.scrollLeft + canvasViewportWidth = scrollerEl.clientWidth + scrollTop = scrollerEl.scrollTop + viewportHeight = scrollerEl.clientHeight + } + onMount(() => { + syncViewport() + if (typeof ResizeObserver !== 'undefined' && scrollerEl !== undefined) { + resizeObs = new ResizeObserver(() => syncViewport()) + resizeObs.observe(scrollerEl) + } + }) + onDestroy(() => { + resizeObs?.disconnect() + }) + $: viewport = { left: canvasViewportLeft, right: canvasViewportLeft + canvasViewportWidth } $: loading = loadingIssues || loadingMilestones @@ -166,10 +225,23 @@ {:else}
- +
-
- +
+
+ +
@@ -197,9 +270,22 @@ display: flex; flex: 1 1 auto; overflow: hidden; + min-height: 0; } .canvas-scroller { flex: 1 1 auto; overflow: auto; + min-width: 0; + min-height: 0; + } + .canvas-stack { + position: relative; + } + .header-sticky { + position: sticky; + top: 0; + z-index: 2; + background: var(--theme-comp-header-color); + border-bottom: 1px solid var(--theme-divider-color); } diff --git a/plugins/tracker-resources/src/components/gantt/lib/__tests__/layout.test.ts b/plugins/tracker-resources/src/components/gantt/lib/__tests__/layout.test.ts index a58bd4ad8b4..4e18eb4e931 100644 --- a/plugins/tracker-resources/src/components/gantt/lib/__tests__/layout.test.ts +++ b/plugins/tracker-resources/src/components/gantt/lib/__tests__/layout.test.ts @@ -5,16 +5,16 @@ import type { Issue } from '@hcengineering/tracker' import { buildLayout, filterVisibleRows } from '../layout' -import { type LayoutRow } from '../types' +import { type LayoutRow, type MilestoneMarker } from '../types' -function fakeIssue (id: string, parentId?: string, hasChildren = false): Issue { +function fakeIssue (id: string, parentId?: string, hasChildren = false, milestone?: string): Issue { return { _id: id as any, _class: 'tracker:class:Issue' as any, title: `Issue ${id}`, space: 'project-1' as any, component: null, - milestone: null, + milestone: milestone ?? null, startDate: null, dueDate: null, parents: parentId !== undefined ? [{ parentId: parentId as any }] : [], @@ -30,12 +30,16 @@ function fakeIssue (id: string, parentId?: string, hasChildren = false): Issue { } as unknown as Issue } +function fakeMilestone (id: string, label = `MS ${id}`): MilestoneMarker { + return { _id: id as any, label, startDate: null, targetDate: 1_700_000_000_000 } +} + const ROW_H = 28 describe('buildLayout (no grouping)', () => { it('flattens a flat list of root issues', () => { const issues = [fakeIssue('a'), fakeIssue('b')] - const rows = buildLayout(issues, 'none', ROW_H) + const rows = buildLayout(issues, [], 'none', ROW_H) expect(rows).toHaveLength(2) expect(rows[0].issue?._id).toBe('a') expect(rows[1].issue?._id).toBe('b') @@ -47,14 +51,14 @@ describe('buildLayout (no grouping)', () => { it('places children below parent with depth+1', () => { const a = fakeIssue('a', undefined, true) const child = fakeIssue('a.1', 'a') - const rows = buildLayout([a, child], 'none', ROW_H) + const rows = buildLayout([a, child], [], 'none', ROW_H) expect(rows.find(r => r.issue?._id === 'a.1')?.depth).toBe(1) }) it('marks parent issues as summary rows', () => { const a = fakeIssue('a', undefined, true) const child = fakeIssue('a.1', 'a') - const rows = buildLayout([a, child], 'none', ROW_H) + const rows = buildLayout([a, child], [], 'none', ROW_H) const parentRow = rows.find(r => r.issue?._id === 'a')! expect(parentRow.isSummary).toBe(true) expect(rows.find(r => r.issue?._id === 'a.1')?.isSummary).toBe(false) @@ -62,37 +66,129 @@ describe('buildLayout (no grouping)', () => { it('row Y coordinates are sequential multiples of rowHeight', () => { const issues = [fakeIssue('a'), fakeIssue('b'), fakeIssue('c')] - const rows = buildLayout(issues, 'none', ROW_H) + const rows = buildLayout(issues, [], 'none', ROW_H) expect(rows.map(r => r.y)).toEqual([0, ROW_H, 2 * ROW_H]) }) + + it('emits orphan children as roots when their parent is not in the input set', () => { + const a = fakeIssue('a', 'p') + const b = fakeIssue('b', 'p') + const rows = buildLayout([a, b], [], 'none', ROW_H) + expect(rows.map(r => r.issue?._id)).toEqual(['a', 'b']) + expect(rows.every(r => r.depth === 0)).toBe(true) + }) + + it('keeps real parent/child nesting when parent IS in the input set', () => { + const parent = fakeIssue('p', undefined, true) + const childA = fakeIssue('a', 'p') + const childB = fakeIssue('b', 'p') + const rows = buildLayout([parent, childA, childB], [], 'none', ROW_H) + expect(rows.map(r => r.issue?._id)).toEqual(['p', 'a', 'b']) + expect(rows[0].depth).toBe(0) + expect(rows[1].depth).toBe(1) + expect(rows[2].depth).toBe(1) + }) +}) + +describe('buildLayout — milestones', () => { + it('emits milestone parent rows above their issues', () => { + const ms = fakeMilestone('m1') + const i1 = fakeIssue('a', undefined, false, 'm1') + const i2 = fakeIssue('b', undefined, false, 'm1') + const rows = buildLayout([i1, i2], [ms], 'none', ROW_H) + expect(rows.map(r => r.id)).toEqual(['milestone:m1', 'issue:a', 'issue:b']) + expect(rows[0].kind).toBe('milestone') + expect(rows[0].milestone?.label).toBe('MS m1') + expect(rows[1].depth).toBe(1) + expect(rows[2].depth).toBe(1) + }) + + it('places issues without a known milestone as top-level roots', () => { + const i1 = fakeIssue('a', undefined, false, 'unknown-ms') + const i2 = fakeIssue('b') + const rows = buildLayout([i1, i2], [], 'none', ROW_H) + expect(rows.map(r => r.id)).toEqual(['issue:a', 'issue:b']) + expect(rows.every(r => r.depth === 0)).toBe(true) + }) + + it('mixes milestone groups with bare issues (milestones first)', () => { + const ms = fakeMilestone('m1') + const inGroup = fakeIssue('a', undefined, false, 'm1') + const ungrouped = fakeIssue('b') + const rows = buildLayout([inGroup, ungrouped], [ms], 'none', ROW_H) + expect(rows.map(r => r.id)).toEqual(['milestone:m1', 'issue:a', 'issue:b']) + }) +}) + +describe('buildLayout — collapse', () => { + it('hides children of a collapsed milestone row', () => { + const ms = fakeMilestone('m1') + const i1 = fakeIssue('a', undefined, false, 'm1') + const i2 = fakeIssue('b', undefined, false, 'm1') + const rows = buildLayout([i1, i2], [ms], 'none', { + rowHeight: ROW_H, + collapsedIds: new Set(['milestone:m1']) + }) + expect(rows.map(r => r.id)).toEqual(['milestone:m1']) + expect(rows[0].collapsed).toBe(true) + }) + + it('hides sub-issues of a collapsed parent issue', () => { + const parent = fakeIssue('p', undefined, true) + const child = fakeIssue('a', 'p') + const rows = buildLayout([parent, child], [], 'none', { + rowHeight: ROW_H, + collapsedIds: new Set(['issue:p']) + }) + expect(rows.map(r => r.id)).toEqual(['issue:p']) + expect(rows[0].collapsed).toBe(true) + expect(rows[0].collapsible).toBe(true) + }) + + it('expanded parents render their children', () => { + const parent = fakeIssue('p', undefined, true) + const child = fakeIssue('a', 'p') + const rows = buildLayout([parent, child], [], 'none', { + rowHeight: ROW_H, + collapsedIds: new Set() + }) + expect(rows.map(r => r.id)).toEqual(['issue:p', 'issue:a']) + expect(rows[0].collapsed).toBe(false) + }) }) describe('filterVisibleRows', () => { + function row (y: number): LayoutRow { + return { + kind: 'issue', + id: `r-${y}`, + y, + height: ROW_H, + depth: 0, + visible: true, + issue: null, + milestone: null, + component: null, + isSummary: false, + collapsible: false, + collapsed: false + } + } + it('returns only rows whose Y range intersects the viewport (overscan=0)', () => { - const all: LayoutRow[] = [ - { kind: 'issue', y: 0, height: ROW_H, depth: 0, visible: true, issue: null, component: null, isSummary: false }, - { kind: 'issue', y: 100, height: ROW_H, depth: 0, visible: true, issue: null, component: null, isSummary: false }, - { kind: 'issue', y: 5000, height: ROW_H, depth: 0, visible: true, issue: null, component: null, isSummary: false } - ] + const all: LayoutRow[] = [row(0), row(100), row(5000)] const visible = filterVisibleRows(all, 80, 60, 0) expect(visible.map(r => r.y)).toEqual([100]) }) it('default overscan brings adjacent rows into the visible set', () => { - const all: LayoutRow[] = [ - { kind: 'issue', y: 0, height: ROW_H, depth: 0, visible: true, issue: null, component: null, isSummary: false }, - { kind: 'issue', y: 100, height: ROW_H, depth: 0, visible: true, issue: null, component: null, isSummary: false }, - { kind: 'issue', y: 5000, height: ROW_H, depth: 0, visible: true, issue: null, component: null, isSummary: false } - ] + const all: LayoutRow[] = [row(0), row(100), row(5000)] const visible = filterVisibleRows(all, 80, 60) expect(visible.map(r => r.y).sort((a, b) => a - b)).toEqual([0, 100]) }) it('honours an explicit overscan', () => { - const all: LayoutRow[] = [ - { kind: 'issue', y: 0, height: ROW_H, depth: 0, visible: true, issue: null, component: null, isSummary: false }, - { kind: 'issue', y: 1000, height: ROW_H, depth: 0, visible: true, issue: null, component: null, isSummary: false } - ] + const all: LayoutRow[] = [row(0), row(1000)] const visible = filterVisibleRows(all, 950, 50, 200) expect(visible.map(r => r.y)).toEqual([1000]) }) diff --git a/plugins/tracker-resources/src/components/gantt/lib/layout.ts b/plugins/tracker-resources/src/components/gantt/lib/layout.ts index 48fa27a84a7..dbf490dc7a7 100644 --- a/plugins/tracker-resources/src/components/gantt/lib/layout.ts +++ b/plugins/tracker-resources/src/components/gantt/lib/layout.ts @@ -4,55 +4,137 @@ // import { type Issue } from '@hcengineering/tracker' -import { type LayoutRow } from './types' +import { type LayoutRow, type MilestoneMarker } from './types' export type GroupBy = 'none' | 'component' | 'milestone' const DEFAULT_OVERSCAN_PX = 240 +/** Stable id used for keyed iteration AND collapse-state lookup. */ +export function rowId (row: LayoutRow): string { + return row.id +} + +export interface BuildLayoutOptions { + rowHeight: number + /** Set of row ids that are currently collapsed (children hidden). */ + collapsedIds?: Set +} + /** - * Flatten the issues array into rows ordered by (parent → child) DFS, - * computing y-coords from rowHeight. + * Build the flattened row layout. + * + * Hierarchy from top to bottom: + * Milestone-row (if any issues belong to that milestone) + * └── Issue-root + * └── Issue-child (sub-issue) + * + * Issues without a milestone are emitted as roots after the milestone groups. + * Issues whose parent is filtered out are promoted to roots so they don't + * silently disappear from the Gantt. */ -export function buildLayout (issues: Issue[], _group: GroupBy, rowHeight: number): LayoutRow[] { - const childrenOf = new Map() - const roots: Issue[] = [] +export function buildLayout ( + issues: Issue[], + milestones: MilestoneMarker[], + _group: GroupBy, + rowHeightOrOpts: number | BuildLayoutOptions +): LayoutRow[] { + const opts: BuildLayoutOptions = + typeof rowHeightOrOpts === 'number' ? { rowHeight: rowHeightOrOpts } : rowHeightOrOpts + const rowHeight = opts.rowHeight + const collapsedIds = opts.collapsedIds ?? new Set() + + // 1) Build issue parent/child map, dropping orphan parent refs. + const visibleIssueIds = new Set(issues.map(i => i._id as unknown as string)) + const issueChildrenOf = new Map() + const issueRoots: Issue[] = [] for (const i of issues) { - const parentId = i.parents?.[0]?.parentId - if (parentId !== undefined && parentId !== null) { - const list = childrenOf.get(parentId as unknown as string) ?? [] + const parentId = i.parents?.[0]?.parentId as unknown as string | undefined + if (parentId != null && visibleIssueIds.has(parentId)) { + const list = issueChildrenOf.get(parentId) ?? [] list.push(i) - childrenOf.set(parentId as unknown as string, list) + issueChildrenOf.set(parentId, list) + } else { + issueRoots.push(i) + } + } + + // 2) Group root-issues by milestone. + const milestoneById = new Map() + for (const m of milestones) { + milestoneById.set(m._id as unknown as string, m) + } + const issuesByMilestone = new Map() + const issuesWithoutMilestone: Issue[] = [] + for (const root of issueRoots) { + const ms = (root as unknown as { milestone?: string | null }).milestone + if (ms != null && milestoneById.has(ms)) { + const list = issuesByMilestone.get(ms) ?? [] + list.push(root) + issuesByMilestone.set(ms, list) } else { - roots.push(i) + issuesWithoutMilestone.push(root) } } const rows: LayoutRow[] = [] let y = 0 - function emit (i: Issue, depth: number): void { - const kids = childrenOf.get(i._id as unknown as string) ?? [] + function emitIssue (issue: Issue, depth: number): void { + const issueId = issue._id as unknown as string + const kids = issueChildrenOf.get(issueId) ?? [] + const id = `issue:${issueId}` + const collapsible = kids.length > 0 + const collapsed = collapsedIds.has(id) rows.push({ kind: 'issue', + id, y, height: rowHeight, depth, visible: true, - issue: i, + issue, + milestone: null, component: null, - isSummary: kids.length > 0 + isSummary: kids.length > 0, + collapsible, + collapsed }) y += rowHeight - for (const c of kids) { - emit(c, depth + 1) + if (!collapsed) { + for (const c of kids) emitIssue(c, depth + 1) } } - for (const r of roots) { - emit(r, 0) + // 3a) Milestone groups first. + for (const [msId, msIssues] of issuesByMilestone) { + const ms = milestoneById.get(msId) + if (ms === undefined) continue + const id = `milestone:${msId}` + const collapsed = collapsedIds.has(id) + rows.push({ + kind: 'milestone', + id, + y, + height: rowHeight, + depth: 0, + visible: true, + issue: null, + milestone: ms, + component: null, + isSummary: true, + collapsible: true, + collapsed + }) + y += rowHeight + if (!collapsed) { + for (const i of msIssues) emitIssue(i, 1) + } } + // 3b) Issues without milestone. + for (const r of issuesWithoutMilestone) emitIssue(r, 0) + return rows } diff --git a/plugins/tracker-resources/src/components/gantt/lib/types.ts b/plugins/tracker-resources/src/components/gantt/lib/types.ts index 485ecabf5ae..8c2b8e2a1a0 100644 --- a/plugins/tracker-resources/src/components/gantt/lib/types.ts +++ b/plugins/tracker-resources/src/components/gantt/lib/types.ts @@ -16,23 +16,31 @@ export interface Tick { level: 'major' | 'minor' // major ticks render thicker + with text label } -/** A row in the flattened layout. May be an issue or a component-swimlane header. */ +/** A row in the flattened layout. May be an issue, milestone, or swimlane header. */ export interface LayoutRow { - kind: 'issue' | 'component-swimlane' + kind: 'issue' | 'milestone' | 'component-swimlane' + /** Stable key for keyed each-blocks and the collapsed-set. */ + id: string /** Y-coord top-edge of the row in canvas pixels. */ y: number /** Row height in pixels. */ height: number - /** Tree depth — 0 for top-level issues. */ + /** Tree depth — 0 for top-level rows. */ depth: number /** Whether this row is currently rendered (vs virtually skipped). */ visible: boolean - /** The issue this row represents — null for swimlane headers. */ + /** The issue this row represents — null for milestone/swimlane rows. */ issue: Issue | null - /** The component this swimlane represents — null for issue rows. */ + /** The milestone this row represents — null for issue/swimlane rows. */ + milestone: MilestoneMarker | null + /** The component this swimlane represents — null otherwise. */ component: Ref | null - /** True iff this issue has children (renders as summary "claw" bar). */ + /** True iff this row has children (renders as summary "claw" bar). */ isSummary: boolean + /** True iff this row has children and the user can collapse/expand it. */ + collapsible: boolean + /** True iff currently collapsed (children hidden). */ + collapsed: boolean } /** Cached aggregate dates of a parent issue's children, for summary-bar rendering. */ diff --git a/plugins/tracker/src/index.ts b/plugins/tracker/src/index.ts index ae4ac58806f..da06d52bd5c 100644 --- a/plugins/tracker/src/index.ts +++ b/plugins/tracker/src/index.ts @@ -496,6 +496,7 @@ const pluginState = plugin(trackerId, { TimeReport: '' as Asset, Estimation: '' as Asset, + Gantt: '' as Asset, // Project icons Home: '' as Asset, From d100e2a0d63a6539a9f230e3e7b0d790f46300bb Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:33:15 +0000 Subject: [PATCH 016/388] =?UTF-8?q?Toolbar=20(replaces=20previous=20floati?= =?UTF-8?q?ng=20zoom):=20-=20Visible=20top=20row=20with=20[=C2=AB]=20[Toda?= =?UTF-8?q?y]=20[=C2=BB]=20|=20Day=20Week=20Month=20Quarter=20|=20?= =?UTF-8?q?=E2=9A=99=20-=20Today=20scrolls=20to=20current=20date,=20prev/n?= =?UTF-8?q?ext=20page-scroll=20by=2080%=20viewport=20-=20Settings=20popove?= =?UTF-8?q?r=20toggles=20Issue-Code=20and=20Title=20columns?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Layout: - Bigger row height (28→36) and bar font (11→13px) for readability - Sidebar reworked into flex-column with separate clipped rows region so scrolled rows can never paint over the sticky header(layout fix) - Drag handle between sidebar and canvas (120–600px range) - ResizeObserver re-syncs viewport on drag/resize/zoom changes Interaction: - Vertical gridlines aligned to time-scale ticks (Plane-style) - Row hover highlights both sidebar and canvas, with rich HTML tooltip (issue title + start + due + duration) - Title click in sidebar dispatches openIssue → showPanel(EditIssue) - Double-click on canvas bar dispatches openIssue - Wheel forwarding from sidebar to canvas-scroller (vertical scroll works while hovering issue list) - Pointerdown + drag on empty canvas pans both axes - Jump-to-bar buttons now use the bar's true left-edge target Tests: 25/25 jest pass; svelte-check 0 errors. Signed-off-by: Michael Uray --- .../src/components/gantt/GanttBar.svelte | 33 +- .../src/components/gantt/GanttCanvas.svelte | 156 +++++-- .../src/components/gantt/GanttSidebar.svelte | 198 ++++++--- .../src/components/gantt/GanttView.svelte | 388 ++++++++++++++++-- 4 files changed, 643 insertions(+), 132 deletions(-) diff --git a/plugins/tracker-resources/src/components/gantt/GanttBar.svelte b/plugins/tracker-resources/src/components/gantt/GanttBar.svelte index f179d163c18..a54c310d91e 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttBar.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttBar.svelte @@ -1,6 +1,5 @@ {#if visible} @@ -52,6 +57,15 @@ points="{x + w - 6},{barY + barH / 2 - 1} {x + w},{barY + barH / 2 - 1} {x + w - 3},{barY + barH / 2 + 5}" fill="var(--theme-content-color)" /> + {#if barLabel !== ''} + {barLabel} + {/if} + {tooltipText} {:else} + {#if barLabel !== ''} + {barLabel} + {/if} {tooltipText} {/if} {/if} @@ -76,4 +98,13 @@ .bar:hover { filter: brightness(1.1); } + .bar-label { + font-size: 13px; + user-select: none; + pointer-events: none; + dominant-baseline: alphabetic; + } + .summary-label { + font-weight: 600; + } diff --git a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte index 772b944a294..66dc7990b1a 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte @@ -1,15 +1,22 @@
- diff --git a/plugins/tracker-resources/src/components/gantt/GanttView.svelte b/plugins/tracker-resources/src/components/gantt/GanttView.svelte index de1a0690842..514014be8dc 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttView.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttView.svelte @@ -1,6 +1,5 @@
- {#if loading} {:else} +
+
+ + + +
+
+ {#each ZOOM_LEVELS as z (z)} + + {/each} +
+
+ + {#if showSettings} +
+ + +
+ {/if} +
+
+
- + +
+ +
-
+ +
@@ -251,10 +417,44 @@ {viewportHeight} {viewport} milestoneStripHeight={MILESTONE_STRIP_HEIGHT} + {hoveredRowId} + on:openIssue={onIssueOpen} + on:hoverRow={onRowHover} />
+ {#if tooltipState.visible && tooltipState.row !== null} + {@const row = tooltipState.row} + {@const issue = row.issue} + {@const ms = row.milestone} +
+ {#if row.kind === 'milestone' && ms !== null} +
◆ Milestone
+
{ms.label}
+ {#if ms.startDate !== null} +
Start: {new Date(ms.startDate).toISOString().slice(0, 10)}
+ {/if} +
Target: {new Date(ms.targetDate).toISOString().slice(0, 10)}
+ {:else if issue !== null} +
Issue
+
{issue.title}
+ {#if issue.startDate !== null} +
Start: {new Date(issue.startDate).toISOString().slice(0, 10)}
+ {/if} + {#if issue.dueDate !== null} +
Due: {new Date(issue.dueDate).toISOString().slice(0, 10)}
+ {/if} + {#if issue.startDate !== null && issue.dueDate !== null} + {@const days = Math.round((Math.max(issue.dueDate, issue.startDate) - Math.min(issue.dueDate, issue.startDate)) / 86_400_000) + 1} +
Duration: {days} day{days === 1 ? '' : 's'}
+ {/if} + {/if} +
+ {/if} {/if}
@@ -266,21 +466,111 @@ width: 100%; overflow: hidden; } + .gantt-toolbar { + flex: 0 0 auto; + display: grid; + grid-template-columns: 1fr auto 1fr; + align-items: center; + padding: 0 12px; + border-bottom: 1px solid var(--theme-divider-color); + background: var(--theme-comp-header-color); + } + .toolbar-left { display: flex; gap: 4px; } + .toolbar-center { display: flex; gap: 2px; justify-self: center; } + .toolbar-right { display: flex; gap: 4px; justify-self: end; position: relative; } + .nav-btn { + height: 26px; + padding: 0 10px; + border: 1px solid var(--theme-divider-color); + background: var(--theme-button-default); + color: var(--theme-content-color); + font-size: 12px; + border-radius: 4px; + cursor: pointer; + } + .nav-btn:hover { + background: var(--theme-button-hovered); + } + .today-btn { + font-weight: 600; + } + .zoom-btn { + height: 26px; + padding: 0 12px; + border: 1px solid var(--theme-divider-color); + background: var(--theme-button-default); + color: var(--theme-content-color); + font-size: 12px; + cursor: pointer; + } + .zoom-btn:first-child { border-radius: 4px 0 0 4px; } + .zoom-btn:last-child { border-radius: 0 4px 4px 0; } + .zoom-btn:not(:first-child) { border-left: none; } + .zoom-btn:hover { background: var(--theme-button-hovered); } + .zoom-btn.active { + background: var(--theme-button-pressed); + font-weight: 600; + } + .settings-btn { + width: 26px; + height: 26px; + padding: 0; + border: 1px solid var(--theme-divider-color); + background: var(--theme-button-default); + color: var(--theme-content-color); + font-size: 13px; + cursor: pointer; + border-radius: 4px; + } + .settings-btn:hover { background: var(--theme-button-hovered); } + .settings-popover { + position: absolute; + top: 32px; + right: 0; + background: var(--theme-popup-color, var(--theme-comp-header-color)); + border: 1px solid var(--theme-divider-color); + border-radius: 4px; + padding: 8px; + display: flex; + flex-direction: column; + gap: 6px; + font-size: 12px; + color: var(--theme-content-color); + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15); + z-index: 5; + } + .settings-popover label { + display: flex; align-items: center; gap: 6px; cursor: pointer; white-space: nowrap; + } .gantt-body { display: flex; flex: 1 1 auto; overflow: hidden; min-height: 0; } + .sidebar-host { flex: 0 0 auto; min-height: 0; } + .resize-handle { + flex: 0 0 5px; + cursor: col-resize; + background: var(--theme-divider-color); + transition: background 80ms ease; + user-select: none; + touch-action: none; + } + .resize-handle:hover, .resize-handle.active { + background: var(--theme-state-info-color, #6366f1); + } .canvas-scroller { flex: 1 1 auto; overflow: auto; min-width: 0; min-height: 0; + cursor: grab; } - .canvas-stack { - position: relative; + .canvas-scroller.panning { + cursor: grabbing; } + .canvas-stack { position: relative; } .header-sticky { position: sticky; top: 0; @@ -288,4 +578,34 @@ background: var(--theme-comp-header-color); border-bottom: 1px solid var(--theme-divider-color); } + .hover-tooltip { + position: fixed; + z-index: 100; + background: var(--theme-popup-color, var(--theme-comp-header-color)); + border: 1px solid var(--theme-divider-color); + border-radius: 4px; + padding: 8px 10px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.18); + pointer-events: none; + font-size: 12px; + color: var(--theme-content-color); + max-width: 320px; + } + .tt-head { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + color: var(--theme-darker-color); + letter-spacing: 0.05em; + margin-bottom: 2px; + } + .tt-title { + font-size: 13px; + font-weight: 600; + margin-bottom: 4px; + } + .tt-line { + font-size: 12px; + line-height: 1.5; + } From 0f43f8f0d9fdd3da3d659d0d078720b48ca42912 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:33:15 +0000 Subject: [PATCH 017/388] - SVG horizontal-scroll bug: GanttHeader and GanttCanvas now render at totalCanvasWidth (was viewport width), with viewBox in scroll-content coordinates. The sticky header lives in the same coordinate system as the canvas-stack so horizontal scroll no longer clips the time-axis. - showPanel uses tracker.component.EditIssue instead of the hardcoded string + 'as any' cast. - Removed unused GanttToolbar.svelte and GanttMilestoneFlag.svelte (dead files since toolbar moved into GanttView and milestone flags became rows). MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Toolbar: - Time-navigation cluster: ⏮ « Today » ⏭ + native date picker that jumps to a specific date. Replaces the lone Today button. Interaction polish: - Sidebar wheel forwarding now uses direct scrollTop/scrollLeft mutation with deltaMode scaling — same speed as native canvas scroll. - TodayMarker no longer hides when scrolled offscreen (SVG width handles clipping); milestone reference lines render unconditionally for the same reason. Tests: 25/25 jest pass; svelte-check 0 errors. Signed-off-by: Michael Uray --- .../src/components/gantt/GanttCanvas.svelte | 44 +++++++-------- .../src/components/gantt/GanttHeader.svelte | 21 ++++---- .../gantt/GanttMilestoneFlag.svelte | 53 ------------------- .../components/gantt/GanttTodayMarker.svelte | 8 ++- .../src/components/gantt/GanttToolbar.svelte | 46 ---------------- .../src/components/gantt/GanttView.svelte | 52 ++++++++++++++++-- 6 files changed, 88 insertions(+), 136 deletions(-) delete mode 100644 plugins/tracker-resources/src/components/gantt/GanttMilestoneFlag.svelte delete mode 100644 plugins/tracker-resources/src/components/gantt/GanttToolbar.svelte diff --git a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte index 66dc7990b1a..500c41a8d9c 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte @@ -24,13 +24,17 @@ export let scrollTop: number = 0 export let viewportHeight: number = 600 export let viewport: { left: number; right: number } + export let totalWidth: number export let milestoneStripHeight: number = 0 export let hoveredRowId: string | null = null $: visibleRows = filterVisibleRows(rows, scrollTop, viewportHeight) $: rowsHeight = rows.length > 0 ? rows[rows.length - 1].y + rows[rows.length - 1].height : 0 $: totalHeight = rowsHeight + milestoneStripHeight - $: ticks = timeScale.ticks([timeScale.fromX(viewport.left), timeScale.fromX(viewport.right)]) + $: ticks = timeScale.ticks([ + timeScale.fromX(Math.max(0, viewport.left - 100)), + timeScale.fromX(viewport.right + 100) + ]) function rowKey (row: LayoutRow): string { return row.id @@ -48,9 +52,9 @@ @@ -75,9 +79,9 @@ {#each visibleRows as row (rowKey(row))} {@const isHover = hoveredRowId === row.id} @@ -140,20 +144,18 @@ {#each milestones as ms (ms._id)} {@const x = timeScale.toX(ms.targetDate)} - {#if x >= viewport.left - 16 && x <= viewport.right + 16} - - {ms.label} - - {/if} + + {ms.label} + {/each} diff --git a/plugins/tracker-resources/src/components/gantt/GanttHeader.svelte b/plugins/tracker-resources/src/components/gantt/GanttHeader.svelte index eb4d4245ff6..dc52767c335 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttHeader.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttHeader.svelte @@ -1,27 +1,30 @@ {#each ticks as tick (tick.date)} @@ -36,7 +39,7 @@ /> @@ -53,7 +56,7 @@ } .tick-label { font-family: var(--mono-font, monospace); - font-size: 11px; + font-size: 12px; user-select: none; pointer-events: none; } diff --git a/plugins/tracker-resources/src/components/gantt/GanttMilestoneFlag.svelte b/plugins/tracker-resources/src/components/gantt/GanttMilestoneFlag.svelte deleted file mode 100644 index 1e5539fbd56..00000000000 --- a/plugins/tracker-resources/src/components/gantt/GanttMilestoneFlag.svelte +++ /dev/null @@ -1,53 +0,0 @@ - - - -{#if visible} - - - {milestone.label} - - - {milestone.label} - -{/if} - - diff --git a/plugins/tracker-resources/src/components/gantt/GanttTodayMarker.svelte b/plugins/tracker-resources/src/components/gantt/GanttTodayMarker.svelte index 8a88f6789f7..ab1bac75106 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttTodayMarker.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttTodayMarker.svelte @@ -7,11 +7,15 @@ export let timeScale: TimeScale export let canvasHeight: number - export let viewport: { left: number; right: number } + // Viewport accepted for API compatibility but no longer needed for visibility + // (the SVG itself spans the whole canvas, scrollbar clips off-viewport). + // eslint-disable-next-line @typescript-eslint/no-unused-vars + export let viewport: { left: number; right: number } = { left: 0, right: 0 } + $: void viewport $: today = Date.now() $: x = timeScale.toX(today) - $: visible = x >= viewport.left && x <= viewport.right + $: visible = true {#if visible} diff --git a/plugins/tracker-resources/src/components/gantt/GanttToolbar.svelte b/plugins/tracker-resources/src/components/gantt/GanttToolbar.svelte deleted file mode 100644 index 8589cdd6af5..00000000000 --- a/plugins/tracker-resources/src/components/gantt/GanttToolbar.svelte +++ /dev/null @@ -1,46 +0,0 @@ - - - -
- {#each order as z} -
- - diff --git a/plugins/tracker-resources/src/components/gantt/GanttView.svelte b/plugins/tracker-resources/src/components/gantt/GanttView.svelte index 514014be8dc..f48f2b05343 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttView.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttView.svelte @@ -217,7 +217,7 @@ function onIssueOpen (e: CustomEvent<{ issue: { _id: string, _class: string } }>): void { showPanel( - 'tracker:component:EditIssue' as any, + tracker.component.EditIssue, e.detail.issue._id as Ref, e.detail.issue._class as Ref>, 'content' @@ -233,14 +233,34 @@ if (scrollerEl === undefined) return scrollerEl.scrollBy({ left: dir * canvasViewportWidth * 0.8, behavior: 'smooth' }) } + function jumpToStart (): void { + if (scrollerEl === undefined) return + scrollerEl.scrollTo({ left: 0, behavior: 'smooth' }) + } + function jumpToEnd (): void { + if (scrollerEl === undefined) return + scrollerEl.scrollTo({ left: scrollerEl.scrollWidth, behavior: 'smooth' }) + } + function jumpToDate (iso: string): void { + if (scrollerEl === undefined || iso === '') return + const t = Date.parse(iso) + if (isNaN(t)) return + const x = timeScale.toX(t) + scrollerEl.scrollTo({ left: Math.max(0, x - canvasViewportWidth / 2), behavior: 'smooth' }) + } + let datePickerValue: string = '' // Forward wheel events from the sidebar to the canvas-scroller so users can - // scroll vertically while hovering the issue list (the sidebar itself uses a - // transform-based layout and has no native scrollbar). + // scroll vertically while hovering the issue list. Direct scrollTop/scrollLeft + // assignment matches the browser's native wheel-to-scroll speed; smooth + // scrolling would feel laggy against the canvas. function forwardSidebarWheel (e: WheelEvent): void { if (scrollerEl === undefined) return e.preventDefault() - scrollerEl.scrollBy({ top: e.deltaY, left: e.deltaX, behavior: 'auto' }) + // deltaMode 1 = lines, 2 = pages; scale to pixels. + const factor = e.deltaMode === 1 ? 16 : (e.deltaMode === 2 ? scrollerEl.clientHeight : 1) + scrollerEl.scrollTop += e.deltaY * factor + scrollerEl.scrollLeft += e.deltaX * factor } // Click-and-drag panning across empty canvas area. @@ -324,9 +344,18 @@ {:else}
+ + + jumpToDate(datePickerValue)} + />
{#each ZOOM_LEVELS as z (z)} @@ -406,7 +435,7 @@ >
- +
Date: Fri, 15 May 2026 12:33:15 +0000 Subject: [PATCH 018/388] feat(tracker-resources): wire Gantt sidebar toggles into Customize-View - Register two ToggleViewOptions on the Gantt viewlet: ganttShowIssueCode (default OFF) and ganttShowTitle (default ON). These show up in the standard Customize-View dropdown. - Drop the per-component settings popover from GanttView; sidebar column visibility now reads viewOptions directly. - Hover tooltip always surfaces the issue code (e.g. OSTRO-31), even when the issue-code column is hidden. - IntlStrings + en/de/es/fr/it/ja/pt/ru/zh/cs translations added. Signed-off-by: Michael Uray --- models/tracker/src/viewlets.ts | 24 +- plugins/tracker-assets/lang/cs.json | 573 +++++++++-------- plugins/tracker-assets/lang/de.json | 591 +++++++++--------- plugins/tracker-assets/lang/en.json | 4 +- plugins/tracker-assets/lang/es.json | 574 ++++++++--------- plugins/tracker-assets/lang/fr.json | 574 ++++++++--------- plugins/tracker-assets/lang/it.json | 574 ++++++++--------- plugins/tracker-assets/lang/ja.json | 574 ++++++++--------- plugins/tracker-assets/lang/pt.json | 574 ++++++++--------- plugins/tracker-assets/lang/ru.json | 591 +++++++++--------- plugins/tracker-assets/lang/zh.json | 591 +++++++++--------- .../src/components/gantt/GanttView.svelte | 37 +- plugins/tracker-resources/src/plugin.ts | 4 +- 13 files changed, 2622 insertions(+), 2663 deletions(-) diff --git a/models/tracker/src/viewlets.ts b/models/tracker/src/viewlets.ts index ce6e058e535..45a9d0406e8 100644 --- a/models/tracker/src/viewlets.ts +++ b/models/tracker/src/viewlets.ts @@ -213,10 +213,9 @@ export function issueConfig ( } export function ganttViewOptions (): ViewOptionsModel { - // PR 2 ships a minimal read-only Gantt. Group-by, "Show colors" and other - // dropdown ViewOptions are intentionally NOT advertised — the canvas does - // not honour them yet. They will be added in PR 3 alongside drag/edit + - // Component swimlanes so users never see options that have no effect. + // PR 2 ships a minimal read-only Gantt. Group-by + Show-colors are + // intentionally NOT advertised — the canvas does not honour them yet. + // The two sidebar-column toggles below ARE wired up to GanttSidebar. return { groupBy: [], orderBy: [ @@ -224,7 +223,22 @@ export function ganttViewOptions (): ViewOptionsModel { ['rank', SortingOrder.Ascending], ['dueDate', SortingOrder.Ascending] ], - other: [] + other: [ + { + key: 'ganttShowIssueCode', + type: 'toggle', + defaultValue: false, + actionTarget: 'display', + label: tracker.string.GanttShowIssueCode + }, + { + key: 'ganttShowTitle', + type: 'toggle', + defaultValue: true, + actionTarget: 'display', + label: tracker.string.GanttShowTitle + } + ] } } diff --git a/plugins/tracker-assets/lang/cs.json b/plugins/tracker-assets/lang/cs.json index 9f87e86aff4..03e0177a161 100644 --- a/plugins/tracker-assets/lang/cs.json +++ b/plugins/tracker-assets/lang/cs.json @@ -1,294 +1,281 @@ { - "string": { - "TrackerApplication": "Přehled", - "Projects": "Vaše projekty", - "More": "Více", - "Default": "Výchozí", - "MakeDefault": "Nastavit jako výchozí", - "Delete": "Smazat", - "Open": "Otevřít", - "Members": "Členové", - "Inbox": "Doručená pošta", - "MyIssues": "Mé úkoly", - "ViewIssue": "Zobrazit úkol", - "IssueCreated": "Úkol vytvořen", - "Issues": "Úkoly", - "Views": "Zobrazení", - "Active": "Aktivní", - "AllIssues": "Všechny úkoly", - "ActiveIssues": "Aktivní úkoly", - "BacklogIssues": "Návrhy", - "Backlog": "Návrhy", - "Board": "Tabule", - "Components": "Komponenty", - "AllComponents": "Vše", - "BacklogComponents": "Návrhy", - "ActiveComponents": "Aktivní", - "ClosedComponents": "Uzavřené", - "NewComponent": "Nová komponenta", - "CreateComponent": "Vytvořit komponentu", - "ComponentNamePlaceholder": "Název komponenty", - "ComponentDescriptionPlaceholder": "Popis (volitelné)", - "ComponentLead": "Vedoucí", - "ComponentMembers": "Členové", - "StartDate": "Datum zahájení", - "TargetDate": "Cílové datum", - "Planned": "Plánováno", - "InProgress": "Probíhá", - "Paused": "Pozastaveno", - "Completed": "Dokončeno", - "Canceled": "Zrušeno", - "CreateProject": "Vytvořit projekt", - "NewProject": "Nový projekt", - "ProjectTitle": "Název projektu", - "ProjectTitlePlaceholder": "Nový projekt", - "UsedInIssueIDs": "Použito v ID úkolů", - "Identifier": "Identifikátor", - "Import": "Importovat", - "ProjectIdentifier": "Identifikátor projektu", - "IdentifierExists": "Identifikátor projektu již existuje", - "ProjectIdentifierPlaceholder": "PROJ", - "ChooseIcon": "Vybrat ikonu", - "AddIssue": "Přidat úkol", - "NewIssue": "Nový úkol", - "NewIssuePlaceholder": "Nový", - "ResumeDraft": "Pokračovat v konceptu", - "SaveIssue": "Vytvořit úkol", - "SetPriority": "Nastavit prioritu…", - "SetStatus": "Nastavit stav…", - "SelectIssue": "Vybrat úkol", - "Priority": "Priorita", - "NoPriority": "Bez priority", - "Urgent": "Naléhavé", - "High": "Vysoká", - "Medium": "Střední", - "Low": "Nízká", - "Unassigned": "Nepřiřazeno", - "Back": "Zpět", - "List": "Seznam", - "NumberLabels": "{count, plural, =0 {žádné štítky} =1 {1 štítek} other {# štítků}}", - "CategoryBacklog": "Návrhy", - "CategoryUnstarted": "Nezahájeno", - "CategoryStarted": "Zahájeno", - "CategoryCompleted": "Dokončeno", - "CategoryCanceled": "Zrušeno", - "Title": "Název", - "Name": "Jméno", - "Description": "Popis", - "Status": "Stav", - "Number": "Číslo", - "Assignee": "Přiřazený", - "AssignTo": "Přiřadit…", - "AssignedTo": "Přiřazeno k {value}", - "Parent": "Nadřazený úkol", - "SetParent": "Nastavit nadřazený úkol…", - "ChangeParent": "Změnit nadřazený úkol…", - "RemoveParent": "Odebrat nadřazený úkol", - "OpenParent": "Otevřít nadřazený úkol", - "SubIssues": "Podúkoly", - "SubIssuesList": "Podúkoly ({subIssues})", - "OpenSubIssues": "Otevřít podúkoly", - "AddSubIssues": "Přidat podúkol", - "BlockedBy": "Blokováno", - "RelatedTo": "Související s", - "Comments": "Komentáře", - "Attachments": "Přílohy", - "Labels": "Štítky", - "Component": "Komponenta", - "SetDueDate": "Nastavit datum splnění…", - "ChangeDueDate": "Změnit datum splnění…", - "ModificationDate": "Aktualizováno {value}", - "Project": "Projekt", - "Issue": "Úkol", - "SubIssue": "Podúkol", - "Rank": "Hodnost", - "TypeIssuePriority": "Priorita úkolu", - "IssueTitlePlaceholder": "Název úkolu", - "SubIssueTitlePlaceholder": "Název podúkolu", - "IssueDescriptionPlaceholder": "Přidat popis…", - "SubIssueDescriptionPlaceholder": "Přidat popis podúkolu", - "AddIssueTooltip": "Přidat úkol…", - "Grouping": "Seskupení", - "Ordering": "Řazení", - "CompletedIssues": "Dokončené úkoly", - "NoGrouping": "Bez seskupení", - "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", - "PastMonth": "Minulý měsíc", - "CopyIssueUrl": "Zkopírovat URL úkolu do schránky", - "CopyIssueId": "Zkopírovat ID úkolu do schránky", - "CopyIssueBranch": "Zkopírovat název větve Git do schránky", - "CopyIssueTitle": "Zkopírovat název úkolu do schránky", - "AssetLabel": "Mějte přehled", - "AddToComponent": "Přidat do komponenty…", - "MoveToComponent": "Přesunout do komponenty…", - "NoComponent": "Bez komponenty", - "ComponentLeadTitle": "Vedoucí komponenty", - "ComponentMembersTitle": "Členové komponenty", - "ComponentLeadSearchPlaceholder": "Nastavit vedoucího komponenty…", - "ComponentMembersSearchPlaceholder": "Změnit členy komponenty…", - "MoveToProject": "Přesunout do projektu", - "Duplicate": "Duplikovat", - - "GotoIssues": "Přejít na úkoly", - "GotoActive": "Přejít na aktivní úkoly", - "GotoBacklog": "Přejít na návrhy", - "GotoComponents": "Přejít na komponenty", - "GotoMyIssues": "Přejít na mé úkoly", - "GotoTrackerApplication": "Přepnout na aplikaci Přehled", - - "CreatedOne": "Vytvořeno", - "MoveIssues": "Přesunout úkoly", - "MoveIssuesDescription": "Vyberte projekt, do kterého chcete úkoly přesunout", - "ManageAttributes": "Spravovat atributy", - "KeepOriginalAttributes": "Zachovat původní atributy", - "KeepOriginalAttributesTooltip": "Původní stavy a komponenty úkolů budou zachovány v novém projektu", - "SelectReplacement": "Následující položky nejsou dostupné v novém projektu. Vyberte náhradu.", - "MissingItem": "CHYBĚJÍCÍ POLOŽKA", - "Replacement": "NÁHRADA", - "Original": "PŮVODNÍ", - "OriginalDescription": "Položky z této sekce budou vytvořeny v novém projektu", - - "Relations": "Vztahy", - "RemoveRelation": "Odebrat vztah...", - "AddBlockedBy": "Označit jako blokováno...", - "AddIsBlocking": "Označit jako blokující...", - "AddRelatedIssue": "Odkázat na jiný úkol...", - "RelatedIssue": "Související úkol {id} - {title}", - "BlockedIssue": "Blokovaný úkol {id} - {title}", - "BlockingIssue": "Blokující úkol {id} - {title}", - "BlockedBySearchPlaceholder": "Vyhledat úkol pro označení jako blokováno...", - "IsBlockingSearchPlaceholder": "Vyhledat úkol pro označení jako blokující...", - "RelatedIssueSearchPlaceholder": "Vyhledat úkol pro odkazování...", - "Blocks": "Blokuje", - "Related": "Související", - "RelatedIssues": "Související úkoly", - - "EditIssue": "Upravit {title}", - "EditWorkflowStatuses": "Upravit stavy úkolů", - "EditProject": "Upravit projekt", - "DeleteProject": "Smazat projekt", - "ArchiveProjectName": "Archivovat projekt {name}?", - "ArchiveProjectConfirm": "Chcete tento projekt archivovat?", - "DeleteProjectConfirm": "Chcete tento projekt a všechny jeho úkoly smazat?", - "ProjectHasIssues": "Tento projekt obsahuje úkoly. Opravdu jej chcete archivovat?", - "ManageWorkflowStatuses": "Spravovat typy projektů", - "AddWorkflowStatus": "Přidat stav úkolu", - "EditWorkflowStatus": "Upravit stav úkolu", - "DeleteWorkflowStatus": "Smazat stav úkolu", - "DeleteWorkflowStatusConfirm": "Chcete smazat stav \"{status}\"?", - "DeleteWorkflowStatusErrorDescription": "Stav \"{status}\" má přiřazeno {count, plural, =1 {1 úkol} other {# úkolů}}. Vyberte stav pro přesun", - - "Save": "Uložit", - "IncludeItemsThatMatch": "Zahrnout položky, které odpovídají", - "AnyFilter": "jakémukoli filtru", - "AllFilters": "všem filtrům", - "NoDescription": "Žádný popis", - "SearchIssue": "Hledat úkol...", - - "StatusHistory": "Historie stavů", - "NewSubIssue": "Přidat podúkol...", - "AddLabel": "Přidat štítek", - - "DeleteIssue": "Smazat {issueCount, plural, =1 {úkol} other {# úkolů}}", - "DeleteIssueConfirm": "Chcete smazat {issueCount, plural, =1 {úkol} other {úkoly}}{subIssueCount, plural, =0 {?} =1 { a podúkol?} other { a podúkoly?}}", - - "Milestone": "Milník", - "NoMilestone": "Žádný milník", - "MoveToMilestone": "Vybrat milník", - "Milestones": "Milníky", - "AllMilestones": "Vše", - "PlannedMilestones": "Plánované", - "ActiveMilestones": "Aktivní", - "ClosedMilestones": "Dokončené", - "AddToMilestone": "Přidat do milníku", - "MilestoneNamePlaceholder": "Název milníku", - - "NewMilestone": "Nový milník", - "CreateMilestone": "Vytvořit", - - "MoveAndDeleteMilestone": "Přesunout úkoly do {newMilestone} a smazat {deleteMilestone}", - "MoveAndDeleteMilestoneConfirm": "Chcete smazat milník a přesunout úkoly do jiného milníku?", - - "Estimation": "Odhad", - "ReportedTime": "Strávený čas", - "RemainingTime": "Zbývající čas", - "TimeSpendReports": "Zprávy o stráveném čase", - "TimeSpendReport": "Čas", - "TimeSpendReportAdd": "Přidat zprávu o čase", - "TimeSpendReportDate": "Datum", - "TimeSpendReportValue": "Strávený čas", - "TimeSpendReportValueTooltip": "Strávený čas v hodinách", - "TimeSpendReportDescription": "Popis", - "TimeSpendDays": "{value}d", - "TimeSpendHours": "{value}h", - "TimeSpendMinutes": "{value}m", - "ChildEstimation": "Odhad podúkolů", - "ChildReportedTime": "Čas podúkolů", - "CapacityValue": "z {value}d", - "NewRelatedIssue": "Nový související úkol", - "RelatedIssuesNotFound": "Související úkoly nenalezeny", - - "AddedReference": "Přidán odkaz", - "AddedAsBlocked": "Označeno jako blokované", - "AddedAsBlocking": "Označeno jako blokující", - - "IssueTemplate": "Šablona", - "IssueTemplates": "Šablony", - "NewProcess": "Nová šablona", - "SaveProcess": "Uložit šablonu", - "NoIssueTemplate": "Žádná šablona", - "TemplateReplace": "Chcete použít novou šablonu?", - "TemplateReplaceConfirm": "Všechna pole budou přepsána hodnotami nové šablony", - "Apply": "Použít", - - "CurrentWorkDay": "Aktuální pracovní den", - "PreviousWorkDay": "Předchozí pracovní den", - "TimeReportDayTypeLabel": "Vyberte typ pracovního dne", - "DefaultAssignee": "Výchozí přiřazení úkolů", - - "SevenHoursLength": "Sedm hodin", - "EightHoursLength": "Osm hodin", - "HourLabel": "h", - "MinuteLabel": "m", - "Saved": "Uloženo...", - "CreatedIssue": "Úkol vytvořen", - "CreatedSubIssue": "Podúkol vytvořen", - "ChangeStatus": "Změnit stav", - "ConfigLabel": "Přehled", - "ConfigDescription": "Rozšíření pro správu pracovních úkolů a projektů.", - "NoStatusFound": "Nebyl nalezen odpovídající stav", - "CreateMissingStatus": "Vytvořit chybějící stav", - "UnsetParent": "Nadřazený úkol bude odebrán", - "AllProjects": "Všechny projekty", - "IssueNotificationTitle": "{issueTitle}", - "IssueNotificationBody": "Aktualizováno uživatelem {senderName}", - "IssueNotificationChanged": "{senderName} změnil {property}", - "IssueNotificationChangedProperty": "{senderName} změnil {property} na \"{newValue}\"", - "IssueNotificationMessage": "{senderName}: {message}", - "PreviousAssigned": "Původně přiřazeno", - "IssueAssignedToYou": "Přiřazeno vám", - "RelatedIssueTargetDescription": "Cílový projekt pro související úkol", - "MapRelatedIssues": "Nastavit výchozí projekty pro související úkoly", - "DefaultIssueStatus": "Výchozí stav úkolu", - "IssueStatus": "Stav", - "Extensions": "Rozšíření", - "UnsetParentIssue": "Odebrat nadřazený úkol", - "ForbidCreateProjectPermission": "Zakázat vytvoření projektu", - "ForbidCreateProjectPermissionDescription": "Zakazuje uživatelům vytvářet nové projekty", - "AllowCreatingIssues": "Povolit vytváření úkolů", - "Day": "Den", - "Gantt": "Gantt", - "Month": "Měsíc", - "Quarter": "Čtvrtletí", - "Week": "Týden" - }, - "status": {} -} + "string": { + "TrackerApplication": "Přehled", + "Projects": "Vaše projekty", + "More": "Více", + "Default": "Výchozí", + "MakeDefault": "Nastavit jako výchozí", + "Delete": "Smazat", + "Open": "Otevřít", + "Members": "Členové", + "Inbox": "Doručená pošta", + "MyIssues": "Mé úkoly", + "ViewIssue": "Zobrazit úkol", + "IssueCreated": "Úkol vytvořen", + "Issues": "Úkoly", + "Views": "Zobrazení", + "Active": "Aktivní", + "AllIssues": "Všechny úkoly", + "ActiveIssues": "Aktivní úkoly", + "BacklogIssues": "Návrhy", + "Backlog": "Návrhy", + "Board": "Tabule", + "Components": "Komponenty", + "AllComponents": "Vše", + "BacklogComponents": "Návrhy", + "ActiveComponents": "Aktivní", + "ClosedComponents": "Uzavřené", + "NewComponent": "Nová komponenta", + "CreateComponent": "Vytvořit komponentu", + "ComponentNamePlaceholder": "Název komponenty", + "ComponentDescriptionPlaceholder": "Popis (volitelné)", + "ComponentLead": "Vedoucí", + "ComponentMembers": "Členové", + "StartDate": "Datum zahájení", + "TargetDate": "Cílové datum", + "Planned": "Plánováno", + "InProgress": "Probíhá", + "Paused": "Pozastaveno", + "Completed": "Dokončeno", + "Canceled": "Zrušeno", + "CreateProject": "Vytvořit projekt", + "NewProject": "Nový projekt", + "ProjectTitle": "Název projektu", + "ProjectTitlePlaceholder": "Nový projekt", + "UsedInIssueIDs": "Použito v ID úkolů", + "Identifier": "Identifikátor", + "Import": "Importovat", + "ProjectIdentifier": "Identifikátor projektu", + "IdentifierExists": "Identifikátor projektu již existuje", + "ProjectIdentifierPlaceholder": "PROJ", + "ChooseIcon": "Vybrat ikonu", + "AddIssue": "Přidat úkol", + "NewIssue": "Nový úkol", + "NewIssuePlaceholder": "Nový", + "ResumeDraft": "Pokračovat v konceptu", + "SaveIssue": "Vytvořit úkol", + "SetPriority": "Nastavit prioritu…", + "SetStatus": "Nastavit stav…", + "SelectIssue": "Vybrat úkol", + "Priority": "Priorita", + "NoPriority": "Bez priority", + "Urgent": "Naléhavé", + "High": "Vysoká", + "Medium": "Střední", + "Low": "Nízká", + "Unassigned": "Nepřiřazeno", + "Back": "Zpět", + "List": "Seznam", + "NumberLabels": "{count, plural, =0 {žádné štítky} =1 {1 štítek} other {# štítků}}", + "CategoryBacklog": "Návrhy", + "CategoryUnstarted": "Nezahájeno", + "CategoryStarted": "Zahájeno", + "CategoryCompleted": "Dokončeno", + "CategoryCanceled": "Zrušeno", + "Title": "Název", + "Name": "Jméno", + "Description": "Popis", + "Status": "Stav", + "Number": "Číslo", + "Assignee": "Přiřazený", + "AssignTo": "Přiřadit…", + "AssignedTo": "Přiřazeno k {value}", + "Parent": "Nadřazený úkol", + "SetParent": "Nastavit nadřazený úkol…", + "ChangeParent": "Změnit nadřazený úkol…", + "RemoveParent": "Odebrat nadřazený úkol", + "OpenParent": "Otevřít nadřazený úkol", + "SubIssues": "Podúkoly", + "SubIssuesList": "Podúkoly ({subIssues})", + "OpenSubIssues": "Otevřít podúkoly", + "AddSubIssues": "Přidat podúkol", + "BlockedBy": "Blokováno", + "RelatedTo": "Související s", + "Comments": "Komentáře", + "Attachments": "Přílohy", + "Labels": "Štítky", + "Component": "Komponenta", + "SetDueDate": "Nastavit datum splnění…", + "ChangeDueDate": "Změnit datum splnění…", + "ModificationDate": "Aktualizováno {value}", + "Project": "Projekt", + "Issue": "Úkol", + "SubIssue": "Podúkol", + "Rank": "Hodnost", + "TypeIssuePriority": "Priorita úkolu", + "IssueTitlePlaceholder": "Název úkolu", + "SubIssueTitlePlaceholder": "Název podúkolu", + "IssueDescriptionPlaceholder": "Přidat popis…", + "SubIssueDescriptionPlaceholder": "Přidat popis podúkolu", + "AddIssueTooltip": "Přidat úkol…", + "Grouping": "Seskupení", + "Ordering": "Řazení", + "CompletedIssues": "Dokončené úkoly", + "NoGrouping": "Bez seskupení", + "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", + "PastMonth": "Minulý měsíc", + "CopyIssueUrl": "Zkopírovat URL úkolu do schránky", + "CopyIssueId": "Zkopírovat ID úkolu do schránky", + "CopyIssueBranch": "Zkopírovat název větve Git do schránky", + "CopyIssueTitle": "Zkopírovat název úkolu do schránky", + "AssetLabel": "Mějte přehled", + "AddToComponent": "Přidat do komponenty…", + "MoveToComponent": "Přesunout do komponenty…", + "NoComponent": "Bez komponenty", + "ComponentLeadTitle": "Vedoucí komponenty", + "ComponentMembersTitle": "Členové komponenty", + "ComponentLeadSearchPlaceholder": "Nastavit vedoucího komponenty…", + "ComponentMembersSearchPlaceholder": "Změnit členy komponenty…", + "MoveToProject": "Přesunout do projektu", + "Duplicate": "Duplikovat", + "GotoIssues": "Přejít na úkoly", + "GotoActive": "Přejít na aktivní úkoly", + "GotoBacklog": "Přejít na návrhy", + "GotoComponents": "Přejít na komponenty", + "GotoMyIssues": "Přejít na mé úkoly", + "GotoTrackerApplication": "Přepnout na aplikaci Přehled", + "CreatedOne": "Vytvořeno", + "MoveIssues": "Přesunout úkoly", + "MoveIssuesDescription": "Vyberte projekt, do kterého chcete úkoly přesunout", + "ManageAttributes": "Spravovat atributy", + "KeepOriginalAttributes": "Zachovat původní atributy", + "KeepOriginalAttributesTooltip": "Původní stavy a komponenty úkolů budou zachovány v novém projektu", + "SelectReplacement": "Následující položky nejsou dostupné v novém projektu. Vyberte náhradu.", + "MissingItem": "CHYBĚJÍCÍ POLOŽKA", + "Replacement": "NÁHRADA", + "Original": "PŮVODNÍ", + "OriginalDescription": "Položky z této sekce budou vytvořeny v novém projektu", + "Relations": "Vztahy", + "RemoveRelation": "Odebrat vztah...", + "AddBlockedBy": "Označit jako blokováno...", + "AddIsBlocking": "Označit jako blokující...", + "AddRelatedIssue": "Odkázat na jiný úkol...", + "RelatedIssue": "Související úkol {id} - {title}", + "BlockedIssue": "Blokovaný úkol {id} - {title}", + "BlockingIssue": "Blokující úkol {id} - {title}", + "BlockedBySearchPlaceholder": "Vyhledat úkol pro označení jako blokováno...", + "IsBlockingSearchPlaceholder": "Vyhledat úkol pro označení jako blokující...", + "RelatedIssueSearchPlaceholder": "Vyhledat úkol pro odkazování...", + "Blocks": "Blokuje", + "Related": "Související", + "RelatedIssues": "Související úkoly", + "EditIssue": "Upravit {title}", + "EditWorkflowStatuses": "Upravit stavy úkolů", + "EditProject": "Upravit projekt", + "DeleteProject": "Smazat projekt", + "ArchiveProjectName": "Archivovat projekt {name}?", + "ArchiveProjectConfirm": "Chcete tento projekt archivovat?", + "DeleteProjectConfirm": "Chcete tento projekt a všechny jeho úkoly smazat?", + "ProjectHasIssues": "Tento projekt obsahuje úkoly. Opravdu jej chcete archivovat?", + "ManageWorkflowStatuses": "Spravovat typy projektů", + "AddWorkflowStatus": "Přidat stav úkolu", + "EditWorkflowStatus": "Upravit stav úkolu", + "DeleteWorkflowStatus": "Smazat stav úkolu", + "DeleteWorkflowStatusConfirm": "Chcete smazat stav \"{status}\"?", + "DeleteWorkflowStatusErrorDescription": "Stav \"{status}\" má přiřazeno {count, plural, =1 {1 úkol} other {# úkolů}}. Vyberte stav pro přesun", + "Save": "Uložit", + "IncludeItemsThatMatch": "Zahrnout položky, které odpovídají", + "AnyFilter": "jakémukoli filtru", + "AllFilters": "všem filtrům", + "NoDescription": "Žádný popis", + "SearchIssue": "Hledat úkol...", + "StatusHistory": "Historie stavů", + "NewSubIssue": "Přidat podúkol...", + "AddLabel": "Přidat štítek", + "DeleteIssue": "Smazat {issueCount, plural, =1 {úkol} other {# úkolů}}", + "DeleteIssueConfirm": "Chcete smazat {issueCount, plural, =1 {úkol} other {úkoly}}{subIssueCount, plural, =0 {?} =1 { a podúkol?} other { a podúkoly?}}", + "Milestone": "Milník", + "NoMilestone": "Žádný milník", + "MoveToMilestone": "Vybrat milník", + "Milestones": "Milníky", + "AllMilestones": "Vše", + "PlannedMilestones": "Plánované", + "ActiveMilestones": "Aktivní", + "ClosedMilestones": "Dokončené", + "AddToMilestone": "Přidat do milníku", + "MilestoneNamePlaceholder": "Název milníku", + "NewMilestone": "Nový milník", + "CreateMilestone": "Vytvořit", + "MoveAndDeleteMilestone": "Přesunout úkoly do {newMilestone} a smazat {deleteMilestone}", + "MoveAndDeleteMilestoneConfirm": "Chcete smazat milník a přesunout úkoly do jiného milníku?", + "Estimation": "Odhad", + "ReportedTime": "Strávený čas", + "RemainingTime": "Zbývající čas", + "TimeSpendReports": "Zprávy o stráveném čase", + "TimeSpendReport": "Čas", + "TimeSpendReportAdd": "Přidat zprávu o čase", + "TimeSpendReportDate": "Datum", + "TimeSpendReportValue": "Strávený čas", + "TimeSpendReportValueTooltip": "Strávený čas v hodinách", + "TimeSpendReportDescription": "Popis", + "TimeSpendDays": "{value}d", + "TimeSpendHours": "{value}h", + "TimeSpendMinutes": "{value}m", + "ChildEstimation": "Odhad podúkolů", + "ChildReportedTime": "Čas podúkolů", + "CapacityValue": "z {value}d", + "NewRelatedIssue": "Nový související úkol", + "RelatedIssuesNotFound": "Související úkoly nenalezeny", + "AddedReference": "Přidán odkaz", + "AddedAsBlocked": "Označeno jako blokované", + "AddedAsBlocking": "Označeno jako blokující", + "IssueTemplate": "Šablona", + "IssueTemplates": "Šablony", + "NewProcess": "Nová šablona", + "SaveProcess": "Uložit šablonu", + "NoIssueTemplate": "Žádná šablona", + "TemplateReplace": "Chcete použít novou šablonu?", + "TemplateReplaceConfirm": "Všechna pole budou přepsána hodnotami nové šablony", + "Apply": "Použít", + "CurrentWorkDay": "Aktuální pracovní den", + "PreviousWorkDay": "Předchozí pracovní den", + "TimeReportDayTypeLabel": "Vyberte typ pracovního dne", + "DefaultAssignee": "Výchozí přiřazení úkolů", + "SevenHoursLength": "Sedm hodin", + "EightHoursLength": "Osm hodin", + "HourLabel": "h", + "MinuteLabel": "m", + "Saved": "Uloženo...", + "CreatedIssue": "Úkol vytvořen", + "CreatedSubIssue": "Podúkol vytvořen", + "ChangeStatus": "Změnit stav", + "ConfigLabel": "Přehled", + "ConfigDescription": "Rozšíření pro správu pracovních úkolů a projektů.", + "NoStatusFound": "Nebyl nalezen odpovídající stav", + "CreateMissingStatus": "Vytvořit chybějící stav", + "UnsetParent": "Nadřazený úkol bude odebrán", + "AllProjects": "Všechny projekty", + "IssueNotificationTitle": "{issueTitle}", + "IssueNotificationBody": "Aktualizováno uživatelem {senderName}", + "IssueNotificationChanged": "{senderName} změnil {property}", + "IssueNotificationChangedProperty": "{senderName} změnil {property} na \"{newValue}\"", + "IssueNotificationMessage": "{senderName}: {message}", + "PreviousAssigned": "Původně přiřazeno", + "IssueAssignedToYou": "Přiřazeno vám", + "RelatedIssueTargetDescription": "Cílový projekt pro související úkol", + "MapRelatedIssues": "Nastavit výchozí projekty pro související úkoly", + "DefaultIssueStatus": "Výchozí stav úkolu", + "IssueStatus": "Stav", + "Extensions": "Rozšíření", + "UnsetParentIssue": "Odebrat nadřazený úkol", + "ForbidCreateProjectPermission": "Zakázat vytvoření projektu", + "ForbidCreateProjectPermissionDescription": "Zakazuje uživatelům vytvářet nové projekty", + "AllowCreatingIssues": "Povolit vytváření úkolů", + "Day": "Den", + "Gantt": "Gantt", + "Month": "Měsíc", + "Quarter": "Čtvrtletí", + "Week": "Týden", + "GanttShowIssueCode": "Show issue code", + "GanttShowTitle": "Show title" + }, + "status": {} +} \ No newline at end of file diff --git a/plugins/tracker-assets/lang/de.json b/plugins/tracker-assets/lang/de.json index 51aa73d1518..eafe7e7cc44 100644 --- a/plugins/tracker-assets/lang/de.json +++ b/plugins/tracker-assets/lang/de.json @@ -1,304 +1,289 @@ { - "string": { - "TrackerApplication": "Tracker", - "Projects": "Deine Projekte", - "More": "Mehr", - "Default": "Standard", - "MakeDefault": "Als Standard festlegen", - "Delete": "Löschen", - "Open": "Öffnen", - "Members": "Mitglieder", - "Inbox": "Posteingang", - "MyIssues": "Meine Aufgaben", - "ViewIssue": "Aufgabe anzeigen", - "IssueCreated": "Aufgabe erstellt", - "Issues": "Aufgaben", - "Views": "Ansichten", - "Active": "Aktiv", - "AllIssues": "Alle Aufgaben", - "ActiveIssues": "Aktive Aufgaben", - "BacklogIssues": "Backlog", - "Backlog": "Backlog", - "Board": "Board", - "Components": "Komponenten", - "AllComponents": "Alle", - "BacklogComponents": "Backlog", - "ActiveComponents": "Aktive", - "ClosedComponents": "Geschlossen", - "NewComponent": "Neue Komponente", - "CreateComponent": "Komponente erstellen", - "ComponentNamePlaceholder": "Komponentenname", - "ComponentDescriptionPlaceholder": "Beschreibung (optional)", - "ComponentLead": "Leitung", - "ComponentMembers": "Mitglieder", - "StartDate": "Startdatum", - "TargetDate": "Zieldatum", - "Planned": "Geplant", - "InProgress": "In Bearbeitung", - "Paused": "Pausiert", - "Completed": "Abgeschlossen", - "Canceled": "Abgebrochen", - "CreateProject": "Projekt erstellen", - "NewProject": "Neues Projekt", - "ProjectTitle": "Projekttitel", - "ProjectTitlePlaceholder": "Neues Projekt", - "UsedInIssueIDs": "Verwendet in Aufgaben-IDs", - "Identifier": "Kennung", - "Import": "Importieren", - "ProjectIdentifier": "Projektkennung", - "IdentifierExists": "Projektkennung existiert bereits", - "ProjectIdentifierPlaceholder": "PRJKT", - "ChooseIcon": "Symbol wählen", - "AddIssue": "Aufgabe hinzufügen", - "NewIssue": "Neue Aufgabe", - "NewIssuePlaceholder": "Neu", - "ResumeDraft": "Entwurf fortsetzen", - "SaveIssue": "Aufgabe erstellen", - "SetPriority": "Priorität setzen...", - "SetStatus": "Status setzen...", - "SelectIssue": "Aufgabe auswählen", - "Priority": "Priorität", - "NoPriority": "Keine Priorität", - "Urgent": "Dringend", - "High": "Hoch", - "Medium": "Mittel", - "Low": "Niedrig", - "Unassigned": "Nicht zugewiesen", - "Back": "Zurück", - "List": "Liste", - "NumberLabels": "{count, plural, =0 {keine Labels} =1 {1 Label} other {# Labels}}", - - "CategoryBacklog": "Backlog", - "CategoryUnstarted": "Nicht gestartet", - "CategoryStarted": "Gestartet", - "CategoryCompleted": "Abgeschlossen", - "CategoryCanceled": "Abgebrochen", - - "Title": "Titel", - "Name": "Name", - "Description": "Beschreibung", - "Status": "Status", - "Number": "Nummer", - "Assignee": "Zugewiesen an", - "AssignTo": "Zuweisen an...", - "AssignedTo": "Zugewiesen an {value}", - "Parent": "Übergeordnete Aufgabe", - "SetParent": "Übergeordnete Aufgabe setzen...", - "ChangeParent": "Übergeordnete Aufgabe ändern...", - "RemoveParent": "Übergeordnete Aufgabe entfernen", - "OpenParent": "Übergeordnete Aufgabe öffnen", - "SubIssues": "Unteraufgaben", - "SubIssuesList": "Unteraufgaben ({subIssues})", - "OpenSubIssues": "Unteraufgaben öffnen", - "AddSubIssues": "Unteraufgabe hinzufügen", - "BlockedBy": "Blockiert durch", - "RelatedTo": "Verwandt mit", - "Comments": "Kommentare", - "Attachments": "Anhänge", - "Labels": "Labels", - "Component": "Komponente", - "Space": "", - "SetDueDate": "Fälligkeitsdatum setzen...", - "ChangeDueDate": "Fälligkeitsdatum ändern...", - "ModificationDate": "Aktualisiert am {value}", - "Project": "Projekt", - "Issue": "Aufgabe", - "SubIssue": "Unteraufgabe", - "Document": "Dokument", - "DocumentIcon": "Dokumentsymbol", - "DocumentColor": "Dokumentfarbe", - "Rank": "Rang", - "TypeIssuePriority": "Aufgabenpriorität", - "IssueTitlePlaceholder": "Aufgabentitel", - "SubIssueTitlePlaceholder": "Unteraufgabentitel", - "IssueDescriptionPlaceholder": "Beschreibung hinzufügen...", - "SubIssueDescriptionPlaceholder": "Unteraufgabenbeschreibung hinzufügen", - "AddIssueTooltip": "Aufgabe hinzufügen...", - "NewIssueDialogClose": "Möchten Sie diesen Dialog schließen?", - "NewIssueDialogCloseNote": "Alle Änderungen gehen verloren", - "RemoveComponentDialogClose": "Komponente löschen?", - "RemoveComponentDialogCloseNote": "Sind Sie sicher, dass Sie diese Komponente löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden", - "Grouping": "Gruppierung", - "Ordering": "Sortierung", - "CompletedIssues": "Abgeschlossene Aufgaben", - "NoGrouping": "Keine Gruppierung", - "NoAssignee": "Nicht zugewiesen", - "LastUpdated": "Zuletzt aktualisiert", - "DueDate": "Fälligkeitsdatum", - "IssueStartDate": "Startdatum", - "GanttDependency": "Abhängigkeit", - "GanttLag": "Verzögerung", - "Manual": "Manuell", - "All": "Alle", - "PastWeek": "Letzte Woche", - "PastMonth": "Letzter Monat", - "CopyIssueUrl": "Aufgaben-URL in Zwischenablage kopieren", - "CopyIssueId": "Aufgaben-ID in Zwischenablage kopieren", - "CopyIssueBranch": "Git-Branch-Namen in Zwischenablage kopieren", - "CopyIssueTitle": "Aufgabentitel in Zwischenablage kopieren", - "AssetLabel": "Asset", - "AddToComponent": "Zu Komponente hinzufügen...", - "MoveToComponent": "Zu Komponente verschieben...", - "NoComponent": "Keine Komponente", - "ComponentLeadTitle": "Komponentenleitung", - "ComponentMembersTitle": "Komponentenmitglieder", - "ComponentLeadSearchPlaceholder": "Komponentenleitung festlegen...", - "ComponentMembersSearchPlaceholder": "Komponentenmitglieder ändern...", - "MoveToProject": "Zu Projekt verschieben", - "Duplicate": "Duplizieren", - - "GotoIssues": "Zu Aufgaben", - "GotoActive": "Zu aktiven Aufgaben", - "GotoBacklog": "Zum Backlog", - "GotoComponents": "Zu Komponenten", - "GotoMyIssues": "Zu meinen Aufgaben", - "GotoTrackerApplication": "Zum Tracker wechseln", - - "CreatedOne": "Erstellt", - "MoveIssues": "Aufgaben verschieben", - "MoveIssuesDescription": "Wählen Sie das Zielprojekt aus", - "ManageAttributes": "Attribute verwalten", - "KeepOriginalAttributes": "Ursprüngliche Attribute beibehalten", - "KeepOriginalAttributesTooltip": "Ursprüngliche Aufgabenstatus und Komponenten werden im neuen Projekt beibehalten", - "SelectReplacement": "Die folgenden Elemente sind im neuen Projekt nicht verfügbar. Wählen Sie einen Ersatz.", - "MissingItem": "FEHLENDES ELEMENT", - "Replacement": "ERSATZ", - "Original": "ORIGINAL", - "OriginalDescription": "Elemente aus diesem Bereich werden im neuen Projekt erstellt", - - "Relations": "Beziehungen", - "RemoveRelation": "Beziehung entfernen...", - "AddBlockedBy": "Als blockiert markieren durch...", - "AddIsBlocking": "Als blockierend markieren...", - "AddRelatedIssue": "Andere Aufgabe referenzieren...", - "RelatedIssue": "Verwandte Aufgabe {id} - {title}", - "BlockedIssue": "Blockierte Aufgabe {id} - {title}", - "BlockingIssue": "Blockierende Aufgabe {id} - {title}", - "BlockedBySearchPlaceholder": "Nach blockierender Aufgabe suchen...", - "IsBlockingSearchPlaceholder": "Nach zu blockierender Aufgabe suchen...", - "RelatedIssueSearchPlaceholder": "Nach zu referenzierender Aufgabe suchen...", - "Blocks": "Blockiert", - "Related": "Verwandt", - "RelatedIssues": "Verwandte Aufgaben", - - "EditIssue": "{title} bearbeiten", - "EditWorkflowStatuses": "Aufgabenstatus bearbeiten", - "EditProject": "Projekt bearbeiten", - "DeleteProject": "Projekt löschen", - "ArchiveProjectName": "Projekt {name} archivieren?", - "ArchiveProjectConfirm": "Möchten Sie dieses Projekt archivieren?", - "DeleteProjectConfirm": "Möchten Sie dieses Projekt und alle Aufgaben löschen?", - "ProjectHasIssues": "Es gibt bestehende Aufgaben in diesem Projekt. Sind Sie sicher, dass Sie archivieren möchten?", - "ManageWorkflowStatuses": "Projekttypen verwalten", - "AddWorkflowStatus": "Aufgabenstatus hinzufügen", - "EditWorkflowStatus": "Aufgabenstatus bearbeiten", - "DeleteWorkflowStatus": "Aufgabenstatus löschen", - "DeleteWorkflowStatusConfirm": "Möchten Sie den Status \"{status}\" löschen?", - "DeleteWorkflowStatusErrorDescription": "Dem Status \"{status}\" sind {count, plural, =1 {1 Aufgabe} other {# Aufgaben}} zugewiesen. Bitte wählen Sie einen neuen Status", - - "Save": "Speichern", - "IncludeItemsThatMatch": "Elemente einschließen, die übereinstimmen mit", - "AnyFilter": "einem Filter", - "AllFilters": "allen Filtern", - "NoDescription": "Keine Beschreibung", - "SearchIssue": "Nach Aufgabe suchen...", - - "StatusHistory": "Statusverlauf", - "NewSubIssue": "Unteraufgabe hinzufügen...", - "AddLabel": "Label hinzufügen", - - "DeleteIssue": "{issueCount, plural, =1 {Aufgabe} other {# Aufgaben}} löschen", - "DeleteIssueConfirm": "Möchten Sie {issueCount, plural, =1 {die Aufgabe} other {die Aufgaben}}{subIssueCount, plural, =0 {} =1 { und Unteraufgabe} other { und Unteraufgaben}} löschen?", - - "Milestone": "Meilenstein", - "NoMilestone": "Kein Meilenstein", - "MoveToMilestone": "Meilenstein auswählen", - "Milestones": "Meilensteine", - "AllMilestones": "Alle", - "PlannedMilestones": "Geplant", - "ActiveMilestones": "Aktiv", - "ClosedMilestones": "Abgeschlossen", - "AddToMilestone": "Zu Meilenstein hinzufügen", - "MilestoneNamePlaceholder": "Meilensteinname", - - "NewMilestone": "Neuer Meilenstein", - "CreateMilestone": "Erstellen", - - "MoveAndDeleteMilestone": "Aufgaben zu {newMilestone} verschieben und {deleteMilestone} löschen", - "MoveAndDeleteMilestoneConfirm": "Möchten Sie den Meilenstein löschen und die Aufgaben zu einem anderen Meilenstein verschieben?", - - "Estimation": "Schätzung", - "ReportedTime": "Aufgewendete Zeit", - "RemainingTime": "Verbleibende Zeit", - "TimeSpendReports": "Zeiterfassungsberichte", - "TimeSpendReport": "Zeit", - "TimeSpendReportAdd": "Zeiterfassung hinzufügen", - "TimeSpendReportDate": "Datum", - "TimeSpendReportValue": "Aufgewendete Zeit", - "TimeSpendReportValueTooltip": "Aufgewendete Zeit in Stunden", - "TimeSpendReportDescription": "Beschreibung", - "TimeSpendDays": "{value}T", - "TimeSpendHours": "{value}Std", - "TimeSpendMinutes": "{value}Min", - "ChildEstimation": "Schätzung Unteraufgaben", - "ChildReportedTime": "Zeit Unteraufgaben", - "CapacityValue": "von {value}T", - "NewRelatedIssue": "Neue verwandte Aufgabe", - "RelatedIssuesNotFound": "Keine verwandten Aufgaben gefunden", - - "AddedReference": "Referenz hinzugefügt", - "AddedAsBlocked": "Als blockiert markiert", - "AddedAsBlocking": "Als blockierend markiert", - - "IssueTemplate": "Vorlage", - "IssueTemplates": "Vorlagen", - "NewProcess": "Neue Vorlage", - "SaveProcess": "Vorlage speichern", - "NoIssueTemplate": "Keine Vorlage", - "TemplateReplace": "Möchten Sie die neue Vorlage anwenden?", - "TemplateReplaceConfirm": "Alle Felder werden mit den Werten der neuen Vorlage überschrieben", - "Apply": "Anwenden", - - "CurrentWorkDay": "Aktueller Arbeitstag", - "PreviousWorkDay": "Vorheriger Arbeitstag", - "TimeReportDayTypeLabel": "Art des Zeiterfassungstags auswählen", - "DefaultAssignee": "Standardzuweisung für Aufgaben", - - "SevenHoursLength": "Sieben Stunden", - "EightHoursLength": "Acht Stunden", - "HourLabel": "Std", - "MinuteLabel": "Min", - "Saved": "Gespeichert...", - "CreatedIssue": "Aufgabe erstellt", - "CreatedSubIssue": "Unteraufgabe erstellt", - "ChangeStatus": "Status ändern", - "ConfigLabel": "Tracker", - "ConfigDescription": "Erweiterung zur Verwaltung von Arbeitsaufgaben und deren Erledigung.", - "NoStatusFound": "Kein passender Status gefunden", - "CreateMissingStatus": "Fehlenden Status erstellen", - "UnsetParent": "Übergeordnete Aufgabe wird entfernt", - "AllProjects": "Alle Projekte", - "IssueNotificationTitle": "{issueTitle}", - "IssueNotificationBody": "Aktualisiert von {senderName}", - "IssueNotificationChanged": "{senderName} hat {property} geändert", - "IssueNotificationChangedProperty": "{senderName} hat {property} zu \"{newValue}\" geändert", - "IssueNotificationMessage": "{senderName}: {message}", - "PreviousAssigned": "Zuvor zugewiesen", - "IssueAssignedToYou": "Ihnen zugewiesen", - "RelatedIssueTargetDescription": "Zielprojekt für verwandte Aufgaben für Klasse oder Space", - "MapRelatedIssues": "Standard-Projekte für verwandte Aufgaben konfigurieren", - "DefaultIssueStatus": "Standard-Aufgabenstatus", - "IssueStatus": "Status", - "Extensions": "Erweiterungen", - "UnsetParentIssue": "Übergeordnete Aufgabe entfernen", - "ForbidCreateProjectPermission": "Projekterstellung verbieten", - "ForbidCreateProjectPermissionDescription": "Verbietet Benutzern das Erstellen neuer Projekte", - "AllowCreatingIssues": "Erstellen von Aufgaben erlauben", - "Day": "Tag", - "Gantt": "Gantt", - "Month": "Monat", - "Quarter": "Quartal", - "Week": "Woche" - }, - "status": {} -} + "string": { + "TrackerApplication": "Tracker", + "Projects": "Deine Projekte", + "More": "Mehr", + "Default": "Standard", + "MakeDefault": "Als Standard festlegen", + "Delete": "Löschen", + "Open": "Öffnen", + "Members": "Mitglieder", + "Inbox": "Posteingang", + "MyIssues": "Meine Aufgaben", + "ViewIssue": "Aufgabe anzeigen", + "IssueCreated": "Aufgabe erstellt", + "Issues": "Aufgaben", + "Views": "Ansichten", + "Active": "Aktiv", + "AllIssues": "Alle Aufgaben", + "ActiveIssues": "Aktive Aufgaben", + "BacklogIssues": "Backlog", + "Backlog": "Backlog", + "Board": "Board", + "Components": "Komponenten", + "AllComponents": "Alle", + "BacklogComponents": "Backlog", + "ActiveComponents": "Aktive", + "ClosedComponents": "Geschlossen", + "NewComponent": "Neue Komponente", + "CreateComponent": "Komponente erstellen", + "ComponentNamePlaceholder": "Komponentenname", + "ComponentDescriptionPlaceholder": "Beschreibung (optional)", + "ComponentLead": "Leitung", + "ComponentMembers": "Mitglieder", + "StartDate": "Startdatum", + "TargetDate": "Zieldatum", + "Planned": "Geplant", + "InProgress": "In Bearbeitung", + "Paused": "Pausiert", + "Completed": "Abgeschlossen", + "Canceled": "Abgebrochen", + "CreateProject": "Projekt erstellen", + "NewProject": "Neues Projekt", + "ProjectTitle": "Projekttitel", + "ProjectTitlePlaceholder": "Neues Projekt", + "UsedInIssueIDs": "Verwendet in Aufgaben-IDs", + "Identifier": "Kennung", + "Import": "Importieren", + "ProjectIdentifier": "Projektkennung", + "IdentifierExists": "Projektkennung existiert bereits", + "ProjectIdentifierPlaceholder": "PRJKT", + "ChooseIcon": "Symbol wählen", + "AddIssue": "Aufgabe hinzufügen", + "NewIssue": "Neue Aufgabe", + "NewIssuePlaceholder": "Neu", + "ResumeDraft": "Entwurf fortsetzen", + "SaveIssue": "Aufgabe erstellen", + "SetPriority": "Priorität setzen...", + "SetStatus": "Status setzen...", + "SelectIssue": "Aufgabe auswählen", + "Priority": "Priorität", + "NoPriority": "Keine Priorität", + "Urgent": "Dringend", + "High": "Hoch", + "Medium": "Mittel", + "Low": "Niedrig", + "Unassigned": "Nicht zugewiesen", + "Back": "Zurück", + "List": "Liste", + "NumberLabels": "{count, plural, =0 {keine Labels} =1 {1 Label} other {# Labels}}", + "CategoryBacklog": "Backlog", + "CategoryUnstarted": "Nicht gestartet", + "CategoryStarted": "Gestartet", + "CategoryCompleted": "Abgeschlossen", + "CategoryCanceled": "Abgebrochen", + "Title": "Titel", + "Name": "Name", + "Description": "Beschreibung", + "Status": "Status", + "Number": "Nummer", + "Assignee": "Zugewiesen an", + "AssignTo": "Zuweisen an...", + "AssignedTo": "Zugewiesen an {value}", + "Parent": "Übergeordnete Aufgabe", + "SetParent": "Übergeordnete Aufgabe setzen...", + "ChangeParent": "Übergeordnete Aufgabe ändern...", + "RemoveParent": "Übergeordnete Aufgabe entfernen", + "OpenParent": "Übergeordnete Aufgabe öffnen", + "SubIssues": "Unteraufgaben", + "SubIssuesList": "Unteraufgaben ({subIssues})", + "OpenSubIssues": "Unteraufgaben öffnen", + "AddSubIssues": "Unteraufgabe hinzufügen", + "BlockedBy": "Blockiert durch", + "RelatedTo": "Verwandt mit", + "Comments": "Kommentare", + "Attachments": "Anhänge", + "Labels": "Labels", + "Component": "Komponente", + "Space": "", + "SetDueDate": "Fälligkeitsdatum setzen...", + "ChangeDueDate": "Fälligkeitsdatum ändern...", + "ModificationDate": "Aktualisiert am {value}", + "Project": "Projekt", + "Issue": "Aufgabe", + "SubIssue": "Unteraufgabe", + "Document": "Dokument", + "DocumentIcon": "Dokumentsymbol", + "DocumentColor": "Dokumentfarbe", + "Rank": "Rang", + "TypeIssuePriority": "Aufgabenpriorität", + "IssueTitlePlaceholder": "Aufgabentitel", + "SubIssueTitlePlaceholder": "Unteraufgabentitel", + "IssueDescriptionPlaceholder": "Beschreibung hinzufügen...", + "SubIssueDescriptionPlaceholder": "Unteraufgabenbeschreibung hinzufügen", + "AddIssueTooltip": "Aufgabe hinzufügen...", + "NewIssueDialogClose": "Möchten Sie diesen Dialog schließen?", + "NewIssueDialogCloseNote": "Alle Änderungen gehen verloren", + "RemoveComponentDialogClose": "Komponente löschen?", + "RemoveComponentDialogCloseNote": "Sind Sie sicher, dass Sie diese Komponente löschen möchten? Diese Aktion kann nicht rückgängig gemacht werden", + "Grouping": "Gruppierung", + "Ordering": "Sortierung", + "CompletedIssues": "Abgeschlossene Aufgaben", + "NoGrouping": "Keine Gruppierung", + "NoAssignee": "Nicht zugewiesen", + "LastUpdated": "Zuletzt aktualisiert", + "DueDate": "Fälligkeitsdatum", + "IssueStartDate": "Startdatum", + "GanttDependency": "Abhängigkeit", + "GanttLag": "Verzögerung", + "Manual": "Manuell", + "All": "Alle", + "PastWeek": "Letzte Woche", + "PastMonth": "Letzter Monat", + "CopyIssueUrl": "Aufgaben-URL in Zwischenablage kopieren", + "CopyIssueId": "Aufgaben-ID in Zwischenablage kopieren", + "CopyIssueBranch": "Git-Branch-Namen in Zwischenablage kopieren", + "CopyIssueTitle": "Aufgabentitel in Zwischenablage kopieren", + "AssetLabel": "Asset", + "AddToComponent": "Zu Komponente hinzufügen...", + "MoveToComponent": "Zu Komponente verschieben...", + "NoComponent": "Keine Komponente", + "ComponentLeadTitle": "Komponentenleitung", + "ComponentMembersTitle": "Komponentenmitglieder", + "ComponentLeadSearchPlaceholder": "Komponentenleitung festlegen...", + "ComponentMembersSearchPlaceholder": "Komponentenmitglieder ändern...", + "MoveToProject": "Zu Projekt verschieben", + "Duplicate": "Duplizieren", + "GotoIssues": "Zu Aufgaben", + "GotoActive": "Zu aktiven Aufgaben", + "GotoBacklog": "Zum Backlog", + "GotoComponents": "Zu Komponenten", + "GotoMyIssues": "Zu meinen Aufgaben", + "GotoTrackerApplication": "Zum Tracker wechseln", + "CreatedOne": "Erstellt", + "MoveIssues": "Aufgaben verschieben", + "MoveIssuesDescription": "Wählen Sie das Zielprojekt aus", + "ManageAttributes": "Attribute verwalten", + "KeepOriginalAttributes": "Ursprüngliche Attribute beibehalten", + "KeepOriginalAttributesTooltip": "Ursprüngliche Aufgabenstatus und Komponenten werden im neuen Projekt beibehalten", + "SelectReplacement": "Die folgenden Elemente sind im neuen Projekt nicht verfügbar. Wählen Sie einen Ersatz.", + "MissingItem": "FEHLENDES ELEMENT", + "Replacement": "ERSATZ", + "Original": "ORIGINAL", + "OriginalDescription": "Elemente aus diesem Bereich werden im neuen Projekt erstellt", + "Relations": "Beziehungen", + "RemoveRelation": "Beziehung entfernen...", + "AddBlockedBy": "Als blockiert markieren durch...", + "AddIsBlocking": "Als blockierend markieren...", + "AddRelatedIssue": "Andere Aufgabe referenzieren...", + "RelatedIssue": "Verwandte Aufgabe {id} - {title}", + "BlockedIssue": "Blockierte Aufgabe {id} - {title}", + "BlockingIssue": "Blockierende Aufgabe {id} - {title}", + "BlockedBySearchPlaceholder": "Nach blockierender Aufgabe suchen...", + "IsBlockingSearchPlaceholder": "Nach zu blockierender Aufgabe suchen...", + "RelatedIssueSearchPlaceholder": "Nach zu referenzierender Aufgabe suchen...", + "Blocks": "Blockiert", + "Related": "Verwandt", + "RelatedIssues": "Verwandte Aufgaben", + "EditIssue": "{title} bearbeiten", + "EditWorkflowStatuses": "Aufgabenstatus bearbeiten", + "EditProject": "Projekt bearbeiten", + "DeleteProject": "Projekt löschen", + "ArchiveProjectName": "Projekt {name} archivieren?", + "ArchiveProjectConfirm": "Möchten Sie dieses Projekt archivieren?", + "DeleteProjectConfirm": "Möchten Sie dieses Projekt und alle Aufgaben löschen?", + "ProjectHasIssues": "Es gibt bestehende Aufgaben in diesem Projekt. Sind Sie sicher, dass Sie archivieren möchten?", + "ManageWorkflowStatuses": "Projekttypen verwalten", + "AddWorkflowStatus": "Aufgabenstatus hinzufügen", + "EditWorkflowStatus": "Aufgabenstatus bearbeiten", + "DeleteWorkflowStatus": "Aufgabenstatus löschen", + "DeleteWorkflowStatusConfirm": "Möchten Sie den Status \"{status}\" löschen?", + "DeleteWorkflowStatusErrorDescription": "Dem Status \"{status}\" sind {count, plural, =1 {1 Aufgabe} other {# Aufgaben}} zugewiesen. Bitte wählen Sie einen neuen Status", + "Save": "Speichern", + "IncludeItemsThatMatch": "Elemente einschließen, die übereinstimmen mit", + "AnyFilter": "einem Filter", + "AllFilters": "allen Filtern", + "NoDescription": "Keine Beschreibung", + "SearchIssue": "Nach Aufgabe suchen...", + "StatusHistory": "Statusverlauf", + "NewSubIssue": "Unteraufgabe hinzufügen...", + "AddLabel": "Label hinzufügen", + "DeleteIssue": "{issueCount, plural, =1 {Aufgabe} other {# Aufgaben}} löschen", + "DeleteIssueConfirm": "Möchten Sie {issueCount, plural, =1 {die Aufgabe} other {die Aufgaben}}{subIssueCount, plural, =0 {} =1 { und Unteraufgabe} other { und Unteraufgaben}} löschen?", + "Milestone": "Meilenstein", + "NoMilestone": "Kein Meilenstein", + "MoveToMilestone": "Meilenstein auswählen", + "Milestones": "Meilensteine", + "AllMilestones": "Alle", + "PlannedMilestones": "Geplant", + "ActiveMilestones": "Aktiv", + "ClosedMilestones": "Abgeschlossen", + "AddToMilestone": "Zu Meilenstein hinzufügen", + "MilestoneNamePlaceholder": "Meilensteinname", + "NewMilestone": "Neuer Meilenstein", + "CreateMilestone": "Erstellen", + "MoveAndDeleteMilestone": "Aufgaben zu {newMilestone} verschieben und {deleteMilestone} löschen", + "MoveAndDeleteMilestoneConfirm": "Möchten Sie den Meilenstein löschen und die Aufgaben zu einem anderen Meilenstein verschieben?", + "Estimation": "Schätzung", + "ReportedTime": "Aufgewendete Zeit", + "RemainingTime": "Verbleibende Zeit", + "TimeSpendReports": "Zeiterfassungsberichte", + "TimeSpendReport": "Zeit", + "TimeSpendReportAdd": "Zeiterfassung hinzufügen", + "TimeSpendReportDate": "Datum", + "TimeSpendReportValue": "Aufgewendete Zeit", + "TimeSpendReportValueTooltip": "Aufgewendete Zeit in Stunden", + "TimeSpendReportDescription": "Beschreibung", + "TimeSpendDays": "{value}T", + "TimeSpendHours": "{value}Std", + "TimeSpendMinutes": "{value}Min", + "ChildEstimation": "Schätzung Unteraufgaben", + "ChildReportedTime": "Zeit Unteraufgaben", + "CapacityValue": "von {value}T", + "NewRelatedIssue": "Neue verwandte Aufgabe", + "RelatedIssuesNotFound": "Keine verwandten Aufgaben gefunden", + "AddedReference": "Referenz hinzugefügt", + "AddedAsBlocked": "Als blockiert markiert", + "AddedAsBlocking": "Als blockierend markiert", + "IssueTemplate": "Vorlage", + "IssueTemplates": "Vorlagen", + "NewProcess": "Neue Vorlage", + "SaveProcess": "Vorlage speichern", + "NoIssueTemplate": "Keine Vorlage", + "TemplateReplace": "Möchten Sie die neue Vorlage anwenden?", + "TemplateReplaceConfirm": "Alle Felder werden mit den Werten der neuen Vorlage überschrieben", + "Apply": "Anwenden", + "CurrentWorkDay": "Aktueller Arbeitstag", + "PreviousWorkDay": "Vorheriger Arbeitstag", + "TimeReportDayTypeLabel": "Art des Zeiterfassungstags auswählen", + "DefaultAssignee": "Standardzuweisung für Aufgaben", + "SevenHoursLength": "Sieben Stunden", + "EightHoursLength": "Acht Stunden", + "HourLabel": "Std", + "MinuteLabel": "Min", + "Saved": "Gespeichert...", + "CreatedIssue": "Aufgabe erstellt", + "CreatedSubIssue": "Unteraufgabe erstellt", + "ChangeStatus": "Status ändern", + "ConfigLabel": "Tracker", + "ConfigDescription": "Erweiterung zur Verwaltung von Arbeitsaufgaben und deren Erledigung.", + "NoStatusFound": "Kein passender Status gefunden", + "CreateMissingStatus": "Fehlenden Status erstellen", + "UnsetParent": "Übergeordnete Aufgabe wird entfernt", + "AllProjects": "Alle Projekte", + "IssueNotificationTitle": "{issueTitle}", + "IssueNotificationBody": "Aktualisiert von {senderName}", + "IssueNotificationChanged": "{senderName} hat {property} geändert", + "IssueNotificationChangedProperty": "{senderName} hat {property} zu \"{newValue}\" geändert", + "IssueNotificationMessage": "{senderName}: {message}", + "PreviousAssigned": "Zuvor zugewiesen", + "IssueAssignedToYou": "Ihnen zugewiesen", + "RelatedIssueTargetDescription": "Zielprojekt für verwandte Aufgaben für Klasse oder Space", + "MapRelatedIssues": "Standard-Projekte für verwandte Aufgaben konfigurieren", + "DefaultIssueStatus": "Standard-Aufgabenstatus", + "IssueStatus": "Status", + "Extensions": "Erweiterungen", + "UnsetParentIssue": "Übergeordnete Aufgabe entfernen", + "ForbidCreateProjectPermission": "Projekterstellung verbieten", + "ForbidCreateProjectPermissionDescription": "Verbietet Benutzern das Erstellen neuer Projekte", + "AllowCreatingIssues": "Erstellen von Aufgaben erlauben", + "Day": "Tag", + "Gantt": "Gantt", + "Month": "Monat", + "Quarter": "Quartal", + "Week": "Woche", + "GanttShowIssueCode": "Show issue code", + "GanttShowTitle": "Show title" + }, + "status": {} +} \ No newline at end of file diff --git a/plugins/tracker-assets/lang/en.json b/plugins/tracker-assets/lang/en.json index 7dad04bee41..e47d9b284c9 100644 --- a/plugins/tracker-assets/lang/en.json +++ b/plugins/tracker-assets/lang/en.json @@ -298,7 +298,9 @@ "Gantt": "Gantt", "Month": "Month", "Quarter": "Quarter", - "Week": "Week" + "Week": "Week", + "GanttShowIssueCode": "Show issue code", + "GanttShowTitle": "Show title" }, "status": {} } diff --git a/plugins/tracker-assets/lang/es.json b/plugins/tracker-assets/lang/es.json index 127dc0a4f63..35cd60a1e00 100644 --- a/plugins/tracker-assets/lang/es.json +++ b/plugins/tracker-assets/lang/es.json @@ -1,287 +1,289 @@ { - "string": { - "TrackerApplication": "Seguimiento", - "Projects": "Tus proyectos", - "More": "Más", - "Default": "Por defecto", - "MakeDefault": "Establecer como por defecto", - "Delete": "Eliminar", - "Open": "Abrir", - "Members": "Miembros", - "Inbox": "Bandeja de entrada", - "MyIssues": "Mis tareas", - "ViewIssue": "Ver tarea", - "IssueCreated": "Tarea creada", - "Issues": "Tareas", - "Views": "Vistas", - "Active": "Activo", - "AllIssues": "Todas las tareas", - "ActiveIssues": "Tareas activas", - "BacklogIssues": "Atrasadas", - "Backlog": "Atrasadas", - "Board": "Tablero", - "Components": "Componentes", - "AllComponents": "Todos", - "BacklogComponents": "Atrasados", - "ActiveComponents": "Activos", - "ClosedComponents": "Cerrados", - "NewComponent": "Nuevo componente", - "CreateComponent": "Crear componente", - "ComponentNamePlaceholder": "Nombre del componente", - "ComponentDescriptionPlaceholder": "Descripción (opcional)", - "ComponentLead": "Responsable del componente", - "ComponentMembers": "Miembros del componente", - "StartDate": "Fecha de inicio", - "TargetDate": "Fecha objetivo", - "Planned": "Planificado", - "InProgress": "En progreso", - "Paused": "Pausado", - "Completed": "Completado", - "Canceled": "Cancelado", - "CreateProject": "Crear proyecto", - "NewProject": "Nuevo proyecto", - "ProjectTitle": "Título del proyecto", - "ProjectTitlePlaceholder": "Nuevo proyecto", - "UsedInIssueIDs": "Utilizado en IDs de tareas", - "Identifier": "Identificador", - "Import": "Importar", - "ProjectIdentifier": "Identificador del proyecto", - "IdentifierExists": "El identificador del proyecto ya existe", - "ProjectIdentifierPlaceholder": "PROJ", - "ChooseIcon": "Seleccionar icono", - "AddIssue": "Añadir tarea", - "NewIssue": "Nueva tarea", - "NewIssuePlaceholder": "Nueva", - "ResumeDraft": "Continuar borrador", - "SaveIssue": "Crear tarea", - "SetPriority": "Establecer prioridad\u2026", - "SetStatus": "Establecer estado\u2026", - "SelectIssue": "Seleccionar tarea", - "Priority": "Prioridad", - "NoPriority": "Sin prioridad", - "Urgent": "Urgente", - "High": "Alta", - "Medium": "Media", - "Low": "Baja", - "Unassigned": "Sin asignar", - "Back": "Atrás", - "List": "Lista", - "NumberLabels": "{count, plural, =0 {sin etiquetas} =1 {1 etiqueta} other {# etiquetas}}", - "CategoryBacklog": "Atraso", - "CategoryUnstarted": "Por Iniciar", - "CategoryStarted": "Iniciado", - "CategoryCompleted": "Completado", - "CategoryCanceled": "Cancelado", - "Title": "Título", - "Name": "Nombre", - "Description": "Descripción", - "Status": "Estado", - "Number": "Número", - "Assignee": "Asignado a", - "AssignTo": "Asignar a...", - "AssignedTo": "Asignado a {value}", - "Parent": "Tarea principal", - "SetParent": "Establecer tarea principal...", - "ChangeParent": "Cambiar tarea principal...", - "RemoveParent": "Eliminar tarea principal", - "OpenParent": "Abrir tarea principal", - "SubIssues": "Subtareas", - "SubIssuesList": "Subtareas ({subIssues})", - "OpenSubIssues": "Abrir subtareas", - "AddSubIssues": "Añadir subtarea", - "BlockedBy": "Bloqueado por", - "RelatedTo": "Relacionado con", - "Comments": "Comentarios", - "Attachments": "Adjuntos", - "Labels": "Etiquetas", - "Component": "Componente", - "Space": "", - "SetDueDate": "Establecer fecha de vencimiento...", - "ChangeDueDate": "Cambiar fecha de vencimiento...", - "ModificationDate": "Actualizado {value}", - "Project": "Proyecto", - "Issue": "Tarea", - "SubIssue": "Subtarea", - "Document": "", - "DocumentIcon": "", - "DocumentColor": "", - "Rank": "Posición", - "TypeIssuePriority": "Prioridad de la tarea", - "IssueTitlePlaceholder": "Título de la tarea", - "SubIssueTitlePlaceholder": "Título de la subtarea", - "IssueDescriptionPlaceholder": "Añadir descripción...", - "SubIssueDescriptionPlaceholder": "Añadir descripción de la subtarea", - "AddIssueTooltip": "Añadir tarea...", - "NewIssueDialogClose": "¿Quieres cerrar este diálogo?", - "NewIssueDialogCloseNote": "Se perderán todos los cambios", - "RemoveComponentDialogClose": "¿Eliminar el componente?", - "RemoveComponentDialogCloseNote": "¿Estás seguro de que quieres eliminar este componente? Esta operación no se puede deshacer", - "Grouping": "Agrupación", - "Ordering": "Ordenación", - "CompletedIssues": "Tareas completadas", - "NoGrouping": "Sin agrupación", - "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", - "PastMonth": "Mes pasado", - "CopyIssueUrl": "Copiar URL de la tarea al portapapeles", - "CopyIssueId": "Copiar ID de la tarea al portapapeles", - "CopyIssueBranch": "Copiar nombre de la rama Git al portapapeles", - "CopyIssueTitle": "Copiar título de la tarea al portapapeles", - "AssetLabel": "Activo", - "AddToComponent": "Añadir al componente...", - "MoveToComponent": "Mover al componente...", - "NoComponent": "Sin componente", - "ComponentLeadTitle": "Responsable del componente", - "ComponentMembersTitle": "Miembros del componente", - "ComponentLeadSearchPlaceholder": "Establecer responsable del componente...", - "ComponentMembersSearchPlaceholder": "Cambiar miembros del componente...", - "MoveToProject": "Mover al proyecto", - "Duplicate": "Duplicar", - "GotoIssues": "Ir a tareas", - "GotoActive": "Ir a tareas activas", - "GotoBacklog": "Ir al backlog", - "GotoComponents": "Ir a componentes", - "GotoMyIssues": "Ir a mis tareas", - "GotoTrackerApplication": "Cambiar a la aplicación de seguimiento", - "CreatedOne": "Creada", - "MoveIssues": "Mover tareas", - "MoveIssuesDescription": "Selecciona el proyecto al que deseas mover las tareas", - "ManageAttributes": "Gestionar atributos", - "KeepOriginalAttributes": "Mantener atributos originales", - "KeepOriginalAttributesTooltip": "Se mantendrán los estados y componentes originales en el nuevo proyecto", - "SelectReplacement": "Los siguientes elementos no están disponibles en el nuevo proyecto. Seleccione un reemplazo.", - "MissingItem": "SIN ELEMENTO", - "Replacement": "REEMPLAZO", - "Original": "ORIGINAL", - "OriginalDescription": "Los elementos de esta sección se crearán en el nuevo proyecto", - "Relations": "Relaciones", - "RemoveRelation": "Eliminar relación...", - "AddBlockedBy": "Marcar como bloqueado por...", - "AddIsBlocking": "Marcar como bloqueador...", - "AddRelatedIssue": "Referenciar otro problema...", - "RelatedIssue": "Problema relacionado {id} - {title}", - "BlockedIssue": "Problema bloqueado {id} - {title}", - "BlockingIssue": "Problema bloqueador {id} - {title}", - "BlockedBySearchPlaceholder": "Buscar problema para marcar como bloqueado por...", - "IsBlockingSearchPlaceholder": "Buscar problema para marcar como bloqueador...", - "RelatedIssueSearchPlaceholder": "Buscar problema para referenciar...", - "Blocks": "Bloquea", - "Related": "Relacionado", - "RelatedIssues": "Problemas relacionados", - "EditIssue": "Editar {title}", - "EditWorkflowStatuses": "Editar estados de problema", - "EditProject": "Editar proyecto", - "DeleteProject": "Eliminar proyecto", - "ArchiveProjectName": "¿Archivar proyecto {name}?", - "ArchiveProjectConfirm": "¿Desea archivar este proyecto?", - "DeleteProjectConfirm": "¿Desea eliminar este proyecto y todos los problemas?", - "ProjectHasIssues": "Hay problemas existentes en este proyecto, ¿está seguro de que desea archivarlos?", - "ManageWorkflowStatuses": "Administrar tipos de proyecto", - "AddWorkflowStatus": "Agregar estado de problema", - "EditWorkflowStatus": "Editar estado de problema", - "DeleteWorkflowStatus": "Eliminar estado de problema", - "DeleteWorkflowStatusConfirm": "¿Desea eliminar el estado \"{status}\"?", - "DeleteWorkflowStatusErrorDescription": "El estado \"{status}\" tiene {count, plural, =1 {1 problema} other {# problemas}} asignados. Por favor seleccione un estado al que moverlos", - "Save": "Guardar", - "IncludeItemsThatMatch": "Incluir elementos que coincidan", - "AnyFilter": "cualquier filtro", - "AllFilters": "todos los filtros", - "NoDescription": "Sin descripción", - "SearchIssue": "Buscar tarea...", - "StatusHistory": "Historial de estado", - "NewSubIssue": "Agregar subproblema...", - "AddLabel": "Agregar etiqueta", - "DeleteIssue": "Eliminar {issueCount, plural, =1 {problema} other {# problemas}}", - "DeleteIssueConfirm": "¿Desea eliminar {issueCount, plural, =1 {problema} other {problemas}}{subIssueCount, plural, =0 {?} =1 { y subproblema?} other { y subproblemas?}}", - "Milestone": "Hitos", - "NoMilestone": "Sin hito", - "MoveToMilestone": "Seleccionar hito", - "Milestones": "Hitos", - "AllMilestones": "Todos", - "PlannedMilestones": "Planificados", - "ActiveMilestones": "Activos", - "ClosedMilestones": "Completados", - "AddToMilestone": "Agregar a hito", - "MilestoneNamePlaceholder": "Nombre del hito", - "NewMilestone": "Nuevo hito", - "CreateMilestone": "Crear", - "MoveAndDeleteMilestone": "Mover problemas a {newMilestone} y eliminar {deleteMilestone}", - "MoveAndDeleteMilestoneConfirm": "¿Desea eliminar el hito y mover los problemas a otro hito?", - "Estimation": "Estimación", - "ReportedTime": "Tiempo empleado", - "RemainingTime": "Tiempo restante", - "TimeSpendReports": "Informes de tiempo empleado", - "TimeSpendReport": "Tiempo", - "TimeSpendReportAdd": "Agregar informe de tiempo", - "TimeSpendReportDate": "Fecha", - "TimeSpendReportValue": "Tiempo empleado", - "TimeSpendReportValueTooltip": "Tiempo empleado en horas", - "TimeSpendReportDescription": "Descripción", - "TimeSpendDays": "{value}d", - "TimeSpendHours": "{value}h", - "TimeSpendMinutes": "{value}m", - "ChildEstimation": "Estimación de subtemas", - "ChildReportedTime": "Tiempo de subtemas", - "CapacityValue": "de {value}d", - "NewRelatedIssue": "Nuevo problema relacionado", - "RelatedIssuesNotFound": "Problemas relacionados no encontrados", - "AddedReference": "Referencia añadida", - "AddedAsBlocked": "Marcado como bloqueado", - "AddedAsBlocking": "Marcado como bloqueante", - "IssueTemplate": "Plantilla", - "IssueTemplates": "Plantillas", - "NewProcess": "Nueva plantilla", - "SaveProcess": "Guardar plantilla", - "NoIssueTemplate": "Sin plantilla", - "TemplateReplace": "¿Desea aplicar la nueva plantilla?", - "TemplateReplaceConfirm": "Todos los campos serán sobrescritos por los valores de la nueva plantilla", - "Apply": "Aplicar", - "CurrentWorkDay": "Día laboral actual", - "PreviousWorkDay": "Día laboral anterior", - "TimeReportDayTypeLabel": "Seleccionar tipo de día para reporte de tiempo", - "DefaultAssignee": "Asignado por defecto para problemas", - "SevenHoursLength": "Siete horas", - "EightHoursLength": "Ocho horas", - "HourLabel": "h", - "MinuteLabel": "m", - "Saved": "Guardado...", - "CreatedIssue": "Problema creado", - "CreatedSubIssue": "Sub-problema creado", - "ChangeStatus": "Cambiar estado", - "ConfigLabel": "Rastreador", - "ConfigDescription": "Extensión para gestionar elementos de trabajo y realizar todas las tareas realizadas.", - "NoStatusFound": "No se encontró ningún estado coincidente", - "CreateMissingStatus": "Crear estado faltante", - "UnsetParent": "El problema padre será desmarcado", - "AllProjects": "Todos los proyectos", - "IssueNotificationTitle": "{issueTitle}", - "IssueNotificationBody": "Actualizado por {senderName}", - "IssueNotificationChanged": "{senderName} cambió {property}", - "IssueNotificationChangedProperty": "{senderName} cambió {property} a \"{newValue}\"", - "IssueNotificationMessage": "{senderName}: {message}", - "PreviousAssigned": "Anteriormente asignado", - "IssueAssignedToYou": "Asignado a ti", - "RelatedIssueTargetDescription": "Proyecto de destino de problema relacionado para Clase o Espacio", - "MapRelatedIssues": "Configurar proyectos predeterminados de problemas relacionados", - "DefaultIssueStatus": "Estado de problema predeterminado", - "IssueStatus": "Estado", - "Extensions": "Extensions", - "UnsetParentIssue": "Unset parent issue", - "ForbidCreateProjectPermission": "Prohibir crear proyecto", - "ForbidCreateProjectPermissionDescription": "Prohíbe a los usuarios crear nuevos proyectos", - "AllowCreatingIssues": "Permitir crear incidencias", - "Day": "Día", - "Gantt": "Gantt", - "Month": "Mes", - "Quarter": "Trimestre", - "Week": "Semana" - }, - "status": {} -} + "string": { + "TrackerApplication": "Seguimiento", + "Projects": "Tus proyectos", + "More": "Más", + "Default": "Por defecto", + "MakeDefault": "Establecer como por defecto", + "Delete": "Eliminar", + "Open": "Abrir", + "Members": "Miembros", + "Inbox": "Bandeja de entrada", + "MyIssues": "Mis tareas", + "ViewIssue": "Ver tarea", + "IssueCreated": "Tarea creada", + "Issues": "Tareas", + "Views": "Vistas", + "Active": "Activo", + "AllIssues": "Todas las tareas", + "ActiveIssues": "Tareas activas", + "BacklogIssues": "Atrasadas", + "Backlog": "Atrasadas", + "Board": "Tablero", + "Components": "Componentes", + "AllComponents": "Todos", + "BacklogComponents": "Atrasados", + "ActiveComponents": "Activos", + "ClosedComponents": "Cerrados", + "NewComponent": "Nuevo componente", + "CreateComponent": "Crear componente", + "ComponentNamePlaceholder": "Nombre del componente", + "ComponentDescriptionPlaceholder": "Descripción (opcional)", + "ComponentLead": "Responsable del componente", + "ComponentMembers": "Miembros del componente", + "StartDate": "Fecha de inicio", + "TargetDate": "Fecha objetivo", + "Planned": "Planificado", + "InProgress": "En progreso", + "Paused": "Pausado", + "Completed": "Completado", + "Canceled": "Cancelado", + "CreateProject": "Crear proyecto", + "NewProject": "Nuevo proyecto", + "ProjectTitle": "Título del proyecto", + "ProjectTitlePlaceholder": "Nuevo proyecto", + "UsedInIssueIDs": "Utilizado en IDs de tareas", + "Identifier": "Identificador", + "Import": "Importar", + "ProjectIdentifier": "Identificador del proyecto", + "IdentifierExists": "El identificador del proyecto ya existe", + "ProjectIdentifierPlaceholder": "PROJ", + "ChooseIcon": "Seleccionar icono", + "AddIssue": "Añadir tarea", + "NewIssue": "Nueva tarea", + "NewIssuePlaceholder": "Nueva", + "ResumeDraft": "Continuar borrador", + "SaveIssue": "Crear tarea", + "SetPriority": "Establecer prioridad…", + "SetStatus": "Establecer estado…", + "SelectIssue": "Seleccionar tarea", + "Priority": "Prioridad", + "NoPriority": "Sin prioridad", + "Urgent": "Urgente", + "High": "Alta", + "Medium": "Media", + "Low": "Baja", + "Unassigned": "Sin asignar", + "Back": "Atrás", + "List": "Lista", + "NumberLabels": "{count, plural, =0 {sin etiquetas} =1 {1 etiqueta} other {# etiquetas}}", + "CategoryBacklog": "Atraso", + "CategoryUnstarted": "Por Iniciar", + "CategoryStarted": "Iniciado", + "CategoryCompleted": "Completado", + "CategoryCanceled": "Cancelado", + "Title": "Título", + "Name": "Nombre", + "Description": "Descripción", + "Status": "Estado", + "Number": "Número", + "Assignee": "Asignado a", + "AssignTo": "Asignar a...", + "AssignedTo": "Asignado a {value}", + "Parent": "Tarea principal", + "SetParent": "Establecer tarea principal...", + "ChangeParent": "Cambiar tarea principal...", + "RemoveParent": "Eliminar tarea principal", + "OpenParent": "Abrir tarea principal", + "SubIssues": "Subtareas", + "SubIssuesList": "Subtareas ({subIssues})", + "OpenSubIssues": "Abrir subtareas", + "AddSubIssues": "Añadir subtarea", + "BlockedBy": "Bloqueado por", + "RelatedTo": "Relacionado con", + "Comments": "Comentarios", + "Attachments": "Adjuntos", + "Labels": "Etiquetas", + "Component": "Componente", + "Space": "", + "SetDueDate": "Establecer fecha de vencimiento...", + "ChangeDueDate": "Cambiar fecha de vencimiento...", + "ModificationDate": "Actualizado {value}", + "Project": "Proyecto", + "Issue": "Tarea", + "SubIssue": "Subtarea", + "Document": "", + "DocumentIcon": "", + "DocumentColor": "", + "Rank": "Posición", + "TypeIssuePriority": "Prioridad de la tarea", + "IssueTitlePlaceholder": "Título de la tarea", + "SubIssueTitlePlaceholder": "Título de la subtarea", + "IssueDescriptionPlaceholder": "Añadir descripción...", + "SubIssueDescriptionPlaceholder": "Añadir descripción de la subtarea", + "AddIssueTooltip": "Añadir tarea...", + "NewIssueDialogClose": "¿Quieres cerrar este diálogo?", + "NewIssueDialogCloseNote": "Se perderán todos los cambios", + "RemoveComponentDialogClose": "¿Eliminar el componente?", + "RemoveComponentDialogCloseNote": "¿Estás seguro de que quieres eliminar este componente? Esta operación no se puede deshacer", + "Grouping": "Agrupación", + "Ordering": "Ordenación", + "CompletedIssues": "Tareas completadas", + "NoGrouping": "Sin agrupación", + "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", + "PastMonth": "Mes pasado", + "CopyIssueUrl": "Copiar URL de la tarea al portapapeles", + "CopyIssueId": "Copiar ID de la tarea al portapapeles", + "CopyIssueBranch": "Copiar nombre de la rama Git al portapapeles", + "CopyIssueTitle": "Copiar título de la tarea al portapapeles", + "AssetLabel": "Activo", + "AddToComponent": "Añadir al componente...", + "MoveToComponent": "Mover al componente...", + "NoComponent": "Sin componente", + "ComponentLeadTitle": "Responsable del componente", + "ComponentMembersTitle": "Miembros del componente", + "ComponentLeadSearchPlaceholder": "Establecer responsable del componente...", + "ComponentMembersSearchPlaceholder": "Cambiar miembros del componente...", + "MoveToProject": "Mover al proyecto", + "Duplicate": "Duplicar", + "GotoIssues": "Ir a tareas", + "GotoActive": "Ir a tareas activas", + "GotoBacklog": "Ir al backlog", + "GotoComponents": "Ir a componentes", + "GotoMyIssues": "Ir a mis tareas", + "GotoTrackerApplication": "Cambiar a la aplicación de seguimiento", + "CreatedOne": "Creada", + "MoveIssues": "Mover tareas", + "MoveIssuesDescription": "Selecciona el proyecto al que deseas mover las tareas", + "ManageAttributes": "Gestionar atributos", + "KeepOriginalAttributes": "Mantener atributos originales", + "KeepOriginalAttributesTooltip": "Se mantendrán los estados y componentes originales en el nuevo proyecto", + "SelectReplacement": "Los siguientes elementos no están disponibles en el nuevo proyecto. Seleccione un reemplazo.", + "MissingItem": "SIN ELEMENTO", + "Replacement": "REEMPLAZO", + "Original": "ORIGINAL", + "OriginalDescription": "Los elementos de esta sección se crearán en el nuevo proyecto", + "Relations": "Relaciones", + "RemoveRelation": "Eliminar relación...", + "AddBlockedBy": "Marcar como bloqueado por...", + "AddIsBlocking": "Marcar como bloqueador...", + "AddRelatedIssue": "Referenciar otro problema...", + "RelatedIssue": "Problema relacionado {id} - {title}", + "BlockedIssue": "Problema bloqueado {id} - {title}", + "BlockingIssue": "Problema bloqueador {id} - {title}", + "BlockedBySearchPlaceholder": "Buscar problema para marcar como bloqueado por...", + "IsBlockingSearchPlaceholder": "Buscar problema para marcar como bloqueador...", + "RelatedIssueSearchPlaceholder": "Buscar problema para referenciar...", + "Blocks": "Bloquea", + "Related": "Relacionado", + "RelatedIssues": "Problemas relacionados", + "EditIssue": "Editar {title}", + "EditWorkflowStatuses": "Editar estados de problema", + "EditProject": "Editar proyecto", + "DeleteProject": "Eliminar proyecto", + "ArchiveProjectName": "¿Archivar proyecto {name}?", + "ArchiveProjectConfirm": "¿Desea archivar este proyecto?", + "DeleteProjectConfirm": "¿Desea eliminar este proyecto y todos los problemas?", + "ProjectHasIssues": "Hay problemas existentes en este proyecto, ¿está seguro de que desea archivarlos?", + "ManageWorkflowStatuses": "Administrar tipos de proyecto", + "AddWorkflowStatus": "Agregar estado de problema", + "EditWorkflowStatus": "Editar estado de problema", + "DeleteWorkflowStatus": "Eliminar estado de problema", + "DeleteWorkflowStatusConfirm": "¿Desea eliminar el estado \"{status}\"?", + "DeleteWorkflowStatusErrorDescription": "El estado \"{status}\" tiene {count, plural, =1 {1 problema} other {# problemas}} asignados. Por favor seleccione un estado al que moverlos", + "Save": "Guardar", + "IncludeItemsThatMatch": "Incluir elementos que coincidan", + "AnyFilter": "cualquier filtro", + "AllFilters": "todos los filtros", + "NoDescription": "Sin descripción", + "SearchIssue": "Buscar tarea...", + "StatusHistory": "Historial de estado", + "NewSubIssue": "Agregar subproblema...", + "AddLabel": "Agregar etiqueta", + "DeleteIssue": "Eliminar {issueCount, plural, =1 {problema} other {# problemas}}", + "DeleteIssueConfirm": "¿Desea eliminar {issueCount, plural, =1 {problema} other {problemas}}{subIssueCount, plural, =0 {?} =1 { y subproblema?} other { y subproblemas?}}", + "Milestone": "Hitos", + "NoMilestone": "Sin hito", + "MoveToMilestone": "Seleccionar hito", + "Milestones": "Hitos", + "AllMilestones": "Todos", + "PlannedMilestones": "Planificados", + "ActiveMilestones": "Activos", + "ClosedMilestones": "Completados", + "AddToMilestone": "Agregar a hito", + "MilestoneNamePlaceholder": "Nombre del hito", + "NewMilestone": "Nuevo hito", + "CreateMilestone": "Crear", + "MoveAndDeleteMilestone": "Mover problemas a {newMilestone} y eliminar {deleteMilestone}", + "MoveAndDeleteMilestoneConfirm": "¿Desea eliminar el hito y mover los problemas a otro hito?", + "Estimation": "Estimación", + "ReportedTime": "Tiempo empleado", + "RemainingTime": "Tiempo restante", + "TimeSpendReports": "Informes de tiempo empleado", + "TimeSpendReport": "Tiempo", + "TimeSpendReportAdd": "Agregar informe de tiempo", + "TimeSpendReportDate": "Fecha", + "TimeSpendReportValue": "Tiempo empleado", + "TimeSpendReportValueTooltip": "Tiempo empleado en horas", + "TimeSpendReportDescription": "Descripción", + "TimeSpendDays": "{value}d", + "TimeSpendHours": "{value}h", + "TimeSpendMinutes": "{value}m", + "ChildEstimation": "Estimación de subtemas", + "ChildReportedTime": "Tiempo de subtemas", + "CapacityValue": "de {value}d", + "NewRelatedIssue": "Nuevo problema relacionado", + "RelatedIssuesNotFound": "Problemas relacionados no encontrados", + "AddedReference": "Referencia añadida", + "AddedAsBlocked": "Marcado como bloqueado", + "AddedAsBlocking": "Marcado como bloqueante", + "IssueTemplate": "Plantilla", + "IssueTemplates": "Plantillas", + "NewProcess": "Nueva plantilla", + "SaveProcess": "Guardar plantilla", + "NoIssueTemplate": "Sin plantilla", + "TemplateReplace": "¿Desea aplicar la nueva plantilla?", + "TemplateReplaceConfirm": "Todos los campos serán sobrescritos por los valores de la nueva plantilla", + "Apply": "Aplicar", + "CurrentWorkDay": "Día laboral actual", + "PreviousWorkDay": "Día laboral anterior", + "TimeReportDayTypeLabel": "Seleccionar tipo de día para reporte de tiempo", + "DefaultAssignee": "Asignado por defecto para problemas", + "SevenHoursLength": "Siete horas", + "EightHoursLength": "Ocho horas", + "HourLabel": "h", + "MinuteLabel": "m", + "Saved": "Guardado...", + "CreatedIssue": "Problema creado", + "CreatedSubIssue": "Sub-problema creado", + "ChangeStatus": "Cambiar estado", + "ConfigLabel": "Rastreador", + "ConfigDescription": "Extensión para gestionar elementos de trabajo y realizar todas las tareas realizadas.", + "NoStatusFound": "No se encontró ningún estado coincidente", + "CreateMissingStatus": "Crear estado faltante", + "UnsetParent": "El problema padre será desmarcado", + "AllProjects": "Todos los proyectos", + "IssueNotificationTitle": "{issueTitle}", + "IssueNotificationBody": "Actualizado por {senderName}", + "IssueNotificationChanged": "{senderName} cambió {property}", + "IssueNotificationChangedProperty": "{senderName} cambió {property} a \"{newValue}\"", + "IssueNotificationMessage": "{senderName}: {message}", + "PreviousAssigned": "Anteriormente asignado", + "IssueAssignedToYou": "Asignado a ti", + "RelatedIssueTargetDescription": "Proyecto de destino de problema relacionado para Clase o Espacio", + "MapRelatedIssues": "Configurar proyectos predeterminados de problemas relacionados", + "DefaultIssueStatus": "Estado de problema predeterminado", + "IssueStatus": "Estado", + "Extensions": "Extensions", + "UnsetParentIssue": "Unset parent issue", + "ForbidCreateProjectPermission": "Prohibir crear proyecto", + "ForbidCreateProjectPermissionDescription": "Prohíbe a los usuarios crear nuevos proyectos", + "AllowCreatingIssues": "Permitir crear incidencias", + "Day": "Día", + "Gantt": "Gantt", + "Month": "Mes", + "Quarter": "Trimestre", + "Week": "Semana", + "GanttShowIssueCode": "Show issue code", + "GanttShowTitle": "Show title" + }, + "status": {} +} \ No newline at end of file diff --git a/plugins/tracker-assets/lang/fr.json b/plugins/tracker-assets/lang/fr.json index 4bf83349e83..ebbc675b081 100644 --- a/plugins/tracker-assets/lang/fr.json +++ b/plugins/tracker-assets/lang/fr.json @@ -1,287 +1,289 @@ { - "string": { - "TrackerApplication": "Suivi", - "Projects": "Vos projets", - "More": "Plus", - "Default": "Par défaut", - "MakeDefault": "Définir par défaut", - "Delete": "Supprimer", - "Open": "Ouvrir", - "Members": "Membres", - "Inbox": "Boîte de réception", - "MyIssues": "Mes issues", - "ViewIssue": "Voir l'issue", - "IssueCreated": "Issue créées", - "Issues": "Issues", - "Views": "Vues", - "Active": "Actif", - "AllIssues": "Toutes les issues", - "ActiveIssues": "Issues actives", - "BacklogIssues": "Backlog", - "Backlog": "Backlog", - "Board": "Tableau", - "Components": "Composants", - "AllComponents": "Tous", - "BacklogComponents": "Backlog", - "ActiveComponents": "Actif", - "ClosedComponents": "Fermé", - "NewComponent": "Nouveau composant", - "CreateComponent": "Créer un composant", - "ComponentNamePlaceholder": "Nom du composant", - "ComponentDescriptionPlaceholder": "Description (facultatif)", - "ComponentLead": "Responsable", - "ComponentMembers": "Membres", - "StartDate": "Date de début", - "TargetDate": "Date cible", - "Planned": "Planifié", - "InProgress": "En cours", - "Paused": "En pause", - "Completed": "Terminé", - "Canceled": "Annulé", - "CreateProject": "Créer un projet", - "NewProject": "Nouveau projet", - "ProjectTitle": "Titre du projet", - "ProjectTitlePlaceholder": "Nouveau projet", - "UsedInIssueIDs": "Utilisé dans les ID d'une issue", - "Identifier": "Identifiant", - "Import": "Importer", - "ProjectIdentifier": "Identifiant du projet", - "IdentifierExists": "L'identifiant du projet existe déjà", - "ProjectIdentifierPlaceholder": "PRJCT", - "ChooseIcon": "Choisir une icône", - "AddIssue": "Ajouter une issue", - "NewIssue": "Nouvelle issue", - "NewIssuePlaceholder": "Nouveau", - "ResumeDraft": "Reprendre le brouillon", - "SaveIssue": "Créer une issue", - "SetPriority": "Définir la priorité...", - "SetStatus": "Définir le statut...", - "SelectIssue": "Sélectionner une issue", - "Priority": "Priorité", - "NoPriority": "Aucune priorité", - "Urgent": "Urgent", - "High": "Haute", - "Medium": "Moyenne", - "Low": "Basse", - "Unassigned": "Non assigné", - "Back": "Retour", - "List": "Liste", - "NumberLabels": "{count, plural, =0 {aucune étiquette} =1 {1 étiquette} other {# étiquettes}}", - "CategoryBacklog": "Backlog", - "CategoryUnstarted": "Non commencé", - "CategoryStarted": "Commencé", - "CategoryCompleted": "Terminé", - "CategoryCanceled": "Annulé", - "Title": "Titre", - "Name": "Nom", - "Description": "Description", - "Status": "Statut", - "Number": "Numéro", - "Assignee": "Assigné à", - "AssignTo": "Assigner à...", - "AssignedTo": "Assigné à {value}", - "Parent": "Problème parent", - "SetParent": "Définir le problème parent...", - "ChangeParent": "Changer le problème parent...", - "RemoveParent": "Supprimer le problème parent", - "OpenParent": "Ouvrir le problème parent", - "SubIssues": "Sub-issue", - "SubIssuesList": "Sub-issue ({subIssues})", - "OpenSubIssues": "Ouvrir les sub-issue", - "AddSubIssues": "Ajouter un sub-issue", - "BlockedBy": "Bloqué par", - "RelatedTo": "Lié à", - "Comments": "Commentaires", - "Attachments": "Pièces jointes", - "Labels": "Étiquettes", - "Component": "Composant", - "Space": "", - "SetDueDate": "Définir la date d'échéance...", - "ChangeDueDate": "Changer la date d'échéance...", - "ModificationDate": "Mis à jour {value}", - "Project": "Projet", - "Issue": "Issue", - "SubIssue": "Sub-issue", - "Document": "", - "DocumentIcon": "", - "DocumentColor": "", - "Rank": "Rang", - "TypeIssuePriority": "Priorité de l'issue", - "IssueTitlePlaceholder": "Titre de l'issue", - "SubIssueTitlePlaceholder": "Titre de la sub-issue", - "IssueDescriptionPlaceholder": "Ajouter une description...", - "SubIssueDescriptionPlaceholder": "Ajouter une description de la sub-issue", - "AddIssueTooltip": "Ajouter une issue...", - "NewIssueDialogClose": "Voulez-vous fermer cette fenêtre ?", - "NewIssueDialogCloseNote": "Tous les changements seront perdus", - "RemoveComponentDialogClose": "Supprimer le composant ?", - "RemoveComponentDialogCloseNote": "Êtes-vous sûr de vouloir supprimer ce composant ? Cette opération est irréversible", - "Grouping": "Regroupement", - "Ordering": "Classement", - "CompletedIssues": "Issues terminées", - "NoGrouping": "Aucun regroupement", - "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", - "PastMonth": "Le mois passé", - "CopyIssueUrl": "Copier l'URL de l'issue dans le presse-papiers", - "CopyIssueId": "Copier l'ID de l'issue dans le presse-papiers", - "CopyIssueBranch": "Copier le nom de la branche Git dans le presse-papiers", - "CopyIssueTitle": "Copier le titre de l'issue dans le presse-papiers", - "AssetLabel": "Actif", - "AddToComponent": "Ajouter au composant...", - "MoveToComponent": "Déplacer vers le composant...", - "NoComponent": "Aucun composant", - "ComponentLeadTitle": "Responsable du composant", - "ComponentMembersTitle": "Membres du composant", - "ComponentLeadSearchPlaceholder": "Définir le responsable du composant...", - "ComponentMembersSearchPlaceholder": "Modifier les membres du composant...", - "MoveToProject": "Déplacer vers le projet", - "Duplicate": "Dupliquer", - "GotoIssues": "Aller aux issues", - "GotoActive": "Aller aux problèmes actifs", - "GotoBacklog": "Aller au backlog", - "GotoComponents": "Aller aux composants", - "GotoMyIssues": "Aller à mes issues", - "GotoTrackerApplication": "Passer à l'application de suivi", - "CreatedOne": "Créé", - "MoveIssues": "Déplacer les issues", - "MoveIssuesDescription": "Sélectionnez le projet vers lequel vous souhaitez déplacer les issues", - "ManageAttributes": "Gérer les attributs", - "KeepOriginalAttributes": "Conserver les attributs d'origine", - "KeepOriginalAttributesTooltip": "Les statuts et composants des problèmes d'origine seront conservés dans le nouveau projet", - "SelectReplacement": "Les éléments suivants ne sont pas disponibles dans le nouveau projet. Sélectionnez un remplacement.", - "MissingItem": "ÉLÉMENT MANQUANT", - "Replacement": "REMPLACEMENT", - "Original": "ORIGINAL", - "OriginalDescription": "Les éléments de cette section seront créés dans le nouveau projet", - "Relations": "Relations", - "RemoveRelation": "Supprimer la relation...", - "AddBlockedBy": "Marquer comme bloqué par...", - "AddIsBlocking": "Marquer comme bloquant...", - "AddRelatedIssue": "Référencer une autre issue...", - "RelatedIssue": "Issue liée {id} - {title}", - "BlockedIssue": "Issue bloquée {id} - {title}", - "BlockingIssue": "Issue bloquante {id} - {title}", - "BlockedBySearchPlaceholder": "Rechercher un problème à marquer comme bloqué par...", - "IsBlockingSearchPlaceholder": "Rechercher un problème à marquer comme bloquant...", - "RelatedIssueSearchPlaceholder": "Rechercher une issue à référencer...", - "Blocks": "Bloque", - "Related": "Lié", - "RelatedIssues": "Issues liées", - "EditIssue": "Modifier {title}", - "EditWorkflowStatuses": "Modifier les statuts des problèmes", - "EditProject": "Modifier le projet", - "DeleteProject": "Supprimer le projet", - "ArchiveProjectName": "Archiver le projet {name} ?", - "ArchiveProjectConfirm": "Voulez-vous archiver ce projet ?", - "DeleteProjectConfirm": "Voulez-vous supprimer ce projet et tous les problèmes ?", - "ProjectHasIssues": "Il y a des issues existantes dans ce projet, êtes-vous sûr de vouloir archiver ?", - "ManageWorkflowStatuses": "Gérer les types de projet", - "AddWorkflowStatus": "Ajouter un statut de problème", - "EditWorkflowStatus": "Modifier le statut du problème", - "DeleteWorkflowStatus": "Supprimer le statut du problème", - "DeleteWorkflowStatusConfirm": "Voulez-vous supprimer le statut \"{status}\" ?", - "DeleteWorkflowStatusErrorDescription": "Le statut \"{status}\" a {count, plural, =1 {1 problème} other {# problèmes}} assigné(s). Veuillez sélectionner un statut pour le déplacer", - "Save": "Enregistrer", - "IncludeItemsThatMatch": "Inclure les éléments qui correspondent", - "AnyFilter": "n'importe quel filtre", - "AllFilters": "tous les filtres", - "NoDescription": "Pas de description", - "SearchIssue": "Rechercher une tâche...", - "StatusHistory": "Historique des statuts", - "NewSubIssue": "Ajouter un sub-ssue...", - "AddLabel": "Ajouter une étiquette", - "DeleteIssue": "Supprimer {issueCount, plural, =1 {problème} other {# problèmes}}", - "DeleteIssueConfirm": "Voulez-vous supprimer {issueCount, plural, =1 {le problème} other {les problèmes}}{subIssueCount, plural, =0 {?} =1 { et le sous-problème ?} other { et les sous-problèmes ?}}", - "Milestone": "Jalon", - "NoMilestone": "Pas de jalon", - "MoveToMilestone": "Sélectionner un jalon", - "Milestones": "Jalons", - "AllMilestones": "Tous", - "PlannedMilestones": "Prévu", - "ActiveMilestones": "Actif", - "ClosedMilestones": "Terminé", - "AddToMilestone": "Ajouter au jalon", - "MilestoneNamePlaceholder": "Nom du jalon", - "NewMilestone": "Nouveau jalon", - "CreateMilestone": "Créer", - "MoveAndDeleteMilestone": "Déplacer les problèmes vers {newMilestone} et supprimer {deleteMilestone}", - "MoveAndDeleteMilestoneConfirm": "Voulez-vous supprimer le jalon et déplacer les problèmes vers un autre jalon ?", - "Estimation": "Estimation", - "ReportedTime": "Temps passé", - "RemainingTime": "Temps restant", - "TimeSpendReports": "Rapports de temps passé", - "TimeSpendReport": "Temps", - "TimeSpendReportAdd": "Ajouter un rapport de temps", - "TimeSpendReportDate": "Date", - "TimeSpendReportValue": "Temps passé", - "TimeSpendReportValueTooltip": "Temps passé en heures", - "TimeSpendReportDescription": "Description", - "TimeSpendDays": "{value}j", - "TimeSpendHours": "{value}h", - "TimeSpendMinutes": "{value}m", - "ChildEstimation": "Estimation des sous-problèmes", - "ChildReportedTime": "Temps des sous-problèmes", - "CapacityValue": "de {value}j", - "NewRelatedIssue": "Nouvelle issue liée", - "RelatedIssuesNotFound": "Issues liées non trouvés", - "AddedReference": "Référence ajoutée", - "AddedAsBlocked": "Marqué comme bloqué", - "AddedAsBlocking": "Marqué comme bloquant", - "IssueTemplate": "Modèle", - "IssueTemplates": "Modèles", - "NewProcess": "Nouveau modèle", - "SaveProcess": "Enregistrer le modèle", - "NoIssueTemplate": "Aucun modèle", - "TemplateReplace": "Voulez-vous appliquer le nouveau modèle ?", - "TemplateReplaceConfirm": "Tous les champs seront remplacés par les valeurs du nouveau modèle", - "Apply": "Appliquer", - "CurrentWorkDay": "Journée de travail actuelle", - "PreviousWorkDay": "Journée de travail précédente", - "TimeReportDayTypeLabel": "Sélectionnez le type de journée de rapport de temps", - "DefaultAssignee": "Assigné par défaut pour les problèmes", - "SevenHoursLength": "Sept heures", - "EightHoursLength": "Huit heures", - "HourLabel": "h", - "MinuteLabel": "m", - "Saved": "Enregistré...", - "CreatedIssue": "Issue créée", - "CreatedSubIssue": "Sub-issue créée", - "ChangeStatus": "Changer le statut", - "ConfigLabel": "Suivi", - "ConfigDescription": "Extension pour gérer les éléments de travail et accomplir toutes les tâches.", - "NoStatusFound": "Aucun statut correspondant trouvé", - "CreateMissingStatus": "Créer un statut manquant", - "UnsetParent": "Le problème parent sera désélectionné", - "AllProjects": "Tous les projets", - "IssueNotificationTitle": "{issueTitle}", - "IssueNotificationBody": "Mis à jour par {senderName}", - "IssueNotificationChanged": "{senderName} a modifié {property}", - "IssueNotificationChangedProperty": "{senderName} a modifié {property} en \"{newValue}\"", - "IssueNotificationMessage": "{senderName} : {message}", - "PreviousAssigned": "Assigné précédemment", - "IssueAssignedToYou": "Assigné à vous", - "RelatedIssueTargetDescription": "Projet cible de l'issue liée pour la classe ou l'espace", - "MapRelatedIssues": "Configurer les projets par défaut des issues liées", - "DefaultIssueStatus": "Statut par défaut de l'issue", - "IssueStatus": "Statut", - "Extensions": "Extensions", - "UnsetParentIssue": "Désélectionner l'issue parent", - "ForbidCreateProjectPermission": "Interdire la création de projet", - "ForbidCreateProjectPermissionDescription": "Interdit aux utilisateurs de créer de nouveaux projets", - "AllowCreatingIssues": "Autoriser la création d'issues", - "Day": "Jour", - "Gantt": "Gantt", - "Month": "Mois", - "Quarter": "Trimestre", - "Week": "Semaine" - }, - "status": {} -} + "string": { + "TrackerApplication": "Suivi", + "Projects": "Vos projets", + "More": "Plus", + "Default": "Par défaut", + "MakeDefault": "Définir par défaut", + "Delete": "Supprimer", + "Open": "Ouvrir", + "Members": "Membres", + "Inbox": "Boîte de réception", + "MyIssues": "Mes issues", + "ViewIssue": "Voir l'issue", + "IssueCreated": "Issue créées", + "Issues": "Issues", + "Views": "Vues", + "Active": "Actif", + "AllIssues": "Toutes les issues", + "ActiveIssues": "Issues actives", + "BacklogIssues": "Backlog", + "Backlog": "Backlog", + "Board": "Tableau", + "Components": "Composants", + "AllComponents": "Tous", + "BacklogComponents": "Backlog", + "ActiveComponents": "Actif", + "ClosedComponents": "Fermé", + "NewComponent": "Nouveau composant", + "CreateComponent": "Créer un composant", + "ComponentNamePlaceholder": "Nom du composant", + "ComponentDescriptionPlaceholder": "Description (facultatif)", + "ComponentLead": "Responsable", + "ComponentMembers": "Membres", + "StartDate": "Date de début", + "TargetDate": "Date cible", + "Planned": "Planifié", + "InProgress": "En cours", + "Paused": "En pause", + "Completed": "Terminé", + "Canceled": "Annulé", + "CreateProject": "Créer un projet", + "NewProject": "Nouveau projet", + "ProjectTitle": "Titre du projet", + "ProjectTitlePlaceholder": "Nouveau projet", + "UsedInIssueIDs": "Utilisé dans les ID d'une issue", + "Identifier": "Identifiant", + "Import": "Importer", + "ProjectIdentifier": "Identifiant du projet", + "IdentifierExists": "L'identifiant du projet existe déjà", + "ProjectIdentifierPlaceholder": "PRJCT", + "ChooseIcon": "Choisir une icône", + "AddIssue": "Ajouter une issue", + "NewIssue": "Nouvelle issue", + "NewIssuePlaceholder": "Nouveau", + "ResumeDraft": "Reprendre le brouillon", + "SaveIssue": "Créer une issue", + "SetPriority": "Définir la priorité...", + "SetStatus": "Définir le statut...", + "SelectIssue": "Sélectionner une issue", + "Priority": "Priorité", + "NoPriority": "Aucune priorité", + "Urgent": "Urgent", + "High": "Haute", + "Medium": "Moyenne", + "Low": "Basse", + "Unassigned": "Non assigné", + "Back": "Retour", + "List": "Liste", + "NumberLabels": "{count, plural, =0 {aucune étiquette} =1 {1 étiquette} other {# étiquettes}}", + "CategoryBacklog": "Backlog", + "CategoryUnstarted": "Non commencé", + "CategoryStarted": "Commencé", + "CategoryCompleted": "Terminé", + "CategoryCanceled": "Annulé", + "Title": "Titre", + "Name": "Nom", + "Description": "Description", + "Status": "Statut", + "Number": "Numéro", + "Assignee": "Assigné à", + "AssignTo": "Assigner à...", + "AssignedTo": "Assigné à {value}", + "Parent": "Problème parent", + "SetParent": "Définir le problème parent...", + "ChangeParent": "Changer le problème parent...", + "RemoveParent": "Supprimer le problème parent", + "OpenParent": "Ouvrir le problème parent", + "SubIssues": "Sub-issue", + "SubIssuesList": "Sub-issue ({subIssues})", + "OpenSubIssues": "Ouvrir les sub-issue", + "AddSubIssues": "Ajouter un sub-issue", + "BlockedBy": "Bloqué par", + "RelatedTo": "Lié à", + "Comments": "Commentaires", + "Attachments": "Pièces jointes", + "Labels": "Étiquettes", + "Component": "Composant", + "Space": "", + "SetDueDate": "Définir la date d'échéance...", + "ChangeDueDate": "Changer la date d'échéance...", + "ModificationDate": "Mis à jour {value}", + "Project": "Projet", + "Issue": "Issue", + "SubIssue": "Sub-issue", + "Document": "", + "DocumentIcon": "", + "DocumentColor": "", + "Rank": "Rang", + "TypeIssuePriority": "Priorité de l'issue", + "IssueTitlePlaceholder": "Titre de l'issue", + "SubIssueTitlePlaceholder": "Titre de la sub-issue", + "IssueDescriptionPlaceholder": "Ajouter une description...", + "SubIssueDescriptionPlaceholder": "Ajouter une description de la sub-issue", + "AddIssueTooltip": "Ajouter une issue...", + "NewIssueDialogClose": "Voulez-vous fermer cette fenêtre ?", + "NewIssueDialogCloseNote": "Tous les changements seront perdus", + "RemoveComponentDialogClose": "Supprimer le composant ?", + "RemoveComponentDialogCloseNote": "Êtes-vous sûr de vouloir supprimer ce composant ? Cette opération est irréversible", + "Grouping": "Regroupement", + "Ordering": "Classement", + "CompletedIssues": "Issues terminées", + "NoGrouping": "Aucun regroupement", + "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", + "PastMonth": "Le mois passé", + "CopyIssueUrl": "Copier l'URL de l'issue dans le presse-papiers", + "CopyIssueId": "Copier l'ID de l'issue dans le presse-papiers", + "CopyIssueBranch": "Copier le nom de la branche Git dans le presse-papiers", + "CopyIssueTitle": "Copier le titre de l'issue dans le presse-papiers", + "AssetLabel": "Actif", + "AddToComponent": "Ajouter au composant...", + "MoveToComponent": "Déplacer vers le composant...", + "NoComponent": "Aucun composant", + "ComponentLeadTitle": "Responsable du composant", + "ComponentMembersTitle": "Membres du composant", + "ComponentLeadSearchPlaceholder": "Définir le responsable du composant...", + "ComponentMembersSearchPlaceholder": "Modifier les membres du composant...", + "MoveToProject": "Déplacer vers le projet", + "Duplicate": "Dupliquer", + "GotoIssues": "Aller aux issues", + "GotoActive": "Aller aux problèmes actifs", + "GotoBacklog": "Aller au backlog", + "GotoComponents": "Aller aux composants", + "GotoMyIssues": "Aller à mes issues", + "GotoTrackerApplication": "Passer à l'application de suivi", + "CreatedOne": "Créé", + "MoveIssues": "Déplacer les issues", + "MoveIssuesDescription": "Sélectionnez le projet vers lequel vous souhaitez déplacer les issues", + "ManageAttributes": "Gérer les attributs", + "KeepOriginalAttributes": "Conserver les attributs d'origine", + "KeepOriginalAttributesTooltip": "Les statuts et composants des problèmes d'origine seront conservés dans le nouveau projet", + "SelectReplacement": "Les éléments suivants ne sont pas disponibles dans le nouveau projet. Sélectionnez un remplacement.", + "MissingItem": "ÉLÉMENT MANQUANT", + "Replacement": "REMPLACEMENT", + "Original": "ORIGINAL", + "OriginalDescription": "Les éléments de cette section seront créés dans le nouveau projet", + "Relations": "Relations", + "RemoveRelation": "Supprimer la relation...", + "AddBlockedBy": "Marquer comme bloqué par...", + "AddIsBlocking": "Marquer comme bloquant...", + "AddRelatedIssue": "Référencer une autre issue...", + "RelatedIssue": "Issue liée {id} - {title}", + "BlockedIssue": "Issue bloquée {id} - {title}", + "BlockingIssue": "Issue bloquante {id} - {title}", + "BlockedBySearchPlaceholder": "Rechercher un problème à marquer comme bloqué par...", + "IsBlockingSearchPlaceholder": "Rechercher un problème à marquer comme bloquant...", + "RelatedIssueSearchPlaceholder": "Rechercher une issue à référencer...", + "Blocks": "Bloque", + "Related": "Lié", + "RelatedIssues": "Issues liées", + "EditIssue": "Modifier {title}", + "EditWorkflowStatuses": "Modifier les statuts des problèmes", + "EditProject": "Modifier le projet", + "DeleteProject": "Supprimer le projet", + "ArchiveProjectName": "Archiver le projet {name} ?", + "ArchiveProjectConfirm": "Voulez-vous archiver ce projet ?", + "DeleteProjectConfirm": "Voulez-vous supprimer ce projet et tous les problèmes ?", + "ProjectHasIssues": "Il y a des issues existantes dans ce projet, êtes-vous sûr de vouloir archiver ?", + "ManageWorkflowStatuses": "Gérer les types de projet", + "AddWorkflowStatus": "Ajouter un statut de problème", + "EditWorkflowStatus": "Modifier le statut du problème", + "DeleteWorkflowStatus": "Supprimer le statut du problème", + "DeleteWorkflowStatusConfirm": "Voulez-vous supprimer le statut \"{status}\" ?", + "DeleteWorkflowStatusErrorDescription": "Le statut \"{status}\" a {count, plural, =1 {1 problème} other {# problèmes}} assigné(s). Veuillez sélectionner un statut pour le déplacer", + "Save": "Enregistrer", + "IncludeItemsThatMatch": "Inclure les éléments qui correspondent", + "AnyFilter": "n'importe quel filtre", + "AllFilters": "tous les filtres", + "NoDescription": "Pas de description", + "SearchIssue": "Rechercher une tâche...", + "StatusHistory": "Historique des statuts", + "NewSubIssue": "Ajouter un sub-ssue...", + "AddLabel": "Ajouter une étiquette", + "DeleteIssue": "Supprimer {issueCount, plural, =1 {problème} other {# problèmes}}", + "DeleteIssueConfirm": "Voulez-vous supprimer {issueCount, plural, =1 {le problème} other {les problèmes}}{subIssueCount, plural, =0 {?} =1 { et le sous-problème ?} other { et les sous-problèmes ?}}", + "Milestone": "Jalon", + "NoMilestone": "Pas de jalon", + "MoveToMilestone": "Sélectionner un jalon", + "Milestones": "Jalons", + "AllMilestones": "Tous", + "PlannedMilestones": "Prévu", + "ActiveMilestones": "Actif", + "ClosedMilestones": "Terminé", + "AddToMilestone": "Ajouter au jalon", + "MilestoneNamePlaceholder": "Nom du jalon", + "NewMilestone": "Nouveau jalon", + "CreateMilestone": "Créer", + "MoveAndDeleteMilestone": "Déplacer les problèmes vers {newMilestone} et supprimer {deleteMilestone}", + "MoveAndDeleteMilestoneConfirm": "Voulez-vous supprimer le jalon et déplacer les problèmes vers un autre jalon ?", + "Estimation": "Estimation", + "ReportedTime": "Temps passé", + "RemainingTime": "Temps restant", + "TimeSpendReports": "Rapports de temps passé", + "TimeSpendReport": "Temps", + "TimeSpendReportAdd": "Ajouter un rapport de temps", + "TimeSpendReportDate": "Date", + "TimeSpendReportValue": "Temps passé", + "TimeSpendReportValueTooltip": "Temps passé en heures", + "TimeSpendReportDescription": "Description", + "TimeSpendDays": "{value}j", + "TimeSpendHours": "{value}h", + "TimeSpendMinutes": "{value}m", + "ChildEstimation": "Estimation des sous-problèmes", + "ChildReportedTime": "Temps des sous-problèmes", + "CapacityValue": "de {value}j", + "NewRelatedIssue": "Nouvelle issue liée", + "RelatedIssuesNotFound": "Issues liées non trouvés", + "AddedReference": "Référence ajoutée", + "AddedAsBlocked": "Marqué comme bloqué", + "AddedAsBlocking": "Marqué comme bloquant", + "IssueTemplate": "Modèle", + "IssueTemplates": "Modèles", + "NewProcess": "Nouveau modèle", + "SaveProcess": "Enregistrer le modèle", + "NoIssueTemplate": "Aucun modèle", + "TemplateReplace": "Voulez-vous appliquer le nouveau modèle ?", + "TemplateReplaceConfirm": "Tous les champs seront remplacés par les valeurs du nouveau modèle", + "Apply": "Appliquer", + "CurrentWorkDay": "Journée de travail actuelle", + "PreviousWorkDay": "Journée de travail précédente", + "TimeReportDayTypeLabel": "Sélectionnez le type de journée de rapport de temps", + "DefaultAssignee": "Assigné par défaut pour les problèmes", + "SevenHoursLength": "Sept heures", + "EightHoursLength": "Huit heures", + "HourLabel": "h", + "MinuteLabel": "m", + "Saved": "Enregistré...", + "CreatedIssue": "Issue créée", + "CreatedSubIssue": "Sub-issue créée", + "ChangeStatus": "Changer le statut", + "ConfigLabel": "Suivi", + "ConfigDescription": "Extension pour gérer les éléments de travail et accomplir toutes les tâches.", + "NoStatusFound": "Aucun statut correspondant trouvé", + "CreateMissingStatus": "Créer un statut manquant", + "UnsetParent": "Le problème parent sera désélectionné", + "AllProjects": "Tous les projets", + "IssueNotificationTitle": "{issueTitle}", + "IssueNotificationBody": "Mis à jour par {senderName}", + "IssueNotificationChanged": "{senderName} a modifié {property}", + "IssueNotificationChangedProperty": "{senderName} a modifié {property} en \"{newValue}\"", + "IssueNotificationMessage": "{senderName} : {message}", + "PreviousAssigned": "Assigné précédemment", + "IssueAssignedToYou": "Assigné à vous", + "RelatedIssueTargetDescription": "Projet cible de l'issue liée pour la classe ou l'espace", + "MapRelatedIssues": "Configurer les projets par défaut des issues liées", + "DefaultIssueStatus": "Statut par défaut de l'issue", + "IssueStatus": "Statut", + "Extensions": "Extensions", + "UnsetParentIssue": "Désélectionner l'issue parent", + "ForbidCreateProjectPermission": "Interdire la création de projet", + "ForbidCreateProjectPermissionDescription": "Interdit aux utilisateurs de créer de nouveaux projets", + "AllowCreatingIssues": "Autoriser la création d'issues", + "Day": "Jour", + "Gantt": "Gantt", + "Month": "Mois", + "Quarter": "Trimestre", + "Week": "Semaine", + "GanttShowIssueCode": "Show issue code", + "GanttShowTitle": "Show title" + }, + "status": {} +} \ No newline at end of file diff --git a/plugins/tracker-assets/lang/it.json b/plugins/tracker-assets/lang/it.json index 02d01f2850c..d85bdae0d9e 100644 --- a/plugins/tracker-assets/lang/it.json +++ b/plugins/tracker-assets/lang/it.json @@ -1,287 +1,289 @@ { - "string": { - "TrackerApplication": "Tracker", - "Projects": "I tuoi progetti", - "More": "Altro", - "Default": "Predefinito", - "MakeDefault": "Imposta come predefinito", - "Delete": "Elimina", - "Open": "Apri", - "Members": "Membri", - "Inbox": "Posta in arrivo", - "MyIssues": "Le mie issues", - "ViewIssue": "Visualizza issue", - "IssueCreated": "Issue creata", - "Issues": "Issue", - "Views": "Visualizzazioni", - "Active": "Attivo", - "AllIssues": "Tutte le issue", - "ActiveIssues": "Issue attive", - "BacklogIssues": "Backlog", - "Backlog": "Backlog", - "Board": "Bacheca", - "Components": "Componenti", - "AllComponents": "Tutti", - "BacklogComponents": "Backlog", - "ActiveComponents": "Attivi", - "ClosedComponents": "Chiuso", - "NewComponent": "Nuovo componente", - "CreateComponent": "Crea componente", - "ComponentNamePlaceholder": "Nome del componente", - "ComponentDescriptionPlaceholder": "Descrizione (facoltativo)", - "ComponentLead": "Responsabile", - "ComponentMembers": "Membri", - "StartDate": "Data di inizio", - "TargetDate": "Data obiettivo", - "Planned": "Pianificato", - "InProgress": "In corso", - "Paused": "In pausa", - "Completed": "Completato", - "Canceled": "Annullato", - "CreateProject": "Crea progetto", - "NewProject": "Nuovo progetto", - "ProjectTitle": "Titolo del progetto", - "ProjectTitlePlaceholder": "Nuovo progetto", - "UsedInIssueIDs": "Utilizzato negli ID delle issue", - "Identifier": "Identificatore", - "Import": "Importa", - "ProjectIdentifier": "Identificatore del progetto", - "IdentifierExists": "L'identificatore del progetto esiste già", - "ProjectIdentifierPlaceholder": "PRJCT", - "ChooseIcon": "Scegli icona", - "AddIssue": "Aggiungi issue", - "NewIssue": "Nuova issue", - "NewIssuePlaceholder": "Nuovo", - "ResumeDraft": "Riprendi bozza", - "SaveIssue": "Crea issue", - "SetPriority": "Imposta priorità…", - "SetStatus": "Imposta stato…", - "SelectIssue": "Seleziona issue", - "Priority": "Priorità", - "NoPriority": "Nessuna priorità", - "Urgent": "Urgente", - "High": "Alto", - "Medium": "Medio", - "Low": "Basso", - "Unassigned": "Non assegnato", - "Back": "Indietro", - "List": "Elenco", - "NumberLabels": "{count, plural, =0 {nessuna etichetta} =1 {1 etichetta} other {# etichette}}", - "CategoryBacklog": "Backlog", - "CategoryUnstarted": "Non iniziato", - "CategoryStarted": "Iniziato", - "CategoryCompleted": "Completato", - "CategoryCanceled": "Annullato", - "Title": "Titolo", - "Name": "Nome", - "Description": "Descrizione", - "Status": "Stato", - "Number": "Numero", - "Assignee": "Assegnatario", - "AssignTo": "Assegna a...", - "AssignedTo": "Assegnato a {value}", - "Parent": "Issue genitore", - "SetParent": "Imposta issue genitore…", - "ChangeParent": "Cambia issue genitore…", - "RemoveParent": "Rimuovi issue genitore", - "OpenParent": "Apri issue genitore", - "SubIssues": "Subissue", - "SubIssuesList": "Subissue ({subIssues})", - "OpenSubIssues": "Apri subissue", - "AddSubIssues": "Aggiungi subissue", - "BlockedBy": "Bloccato da", - "RelatedTo": "Correlato a", - "Comments": "Commenti", - "Attachments": "Allegati", - "Labels": "Etichette", - "Component": "Componente", - "Space": "", - "SetDueDate": "Imposta data di scadenza…", - "ChangeDueDate": "Cambia data di scadenza…", - "ModificationDate": "Aggiornato {value}", - "Project": "Progetto", - "Issue": "Issue", - "SubIssue": "Subissue", - "Document": "", - "DocumentIcon": "", - "DocumentColor": "", - "Rank": "Classifica", - "TypeIssuePriority": "Priorità della issue", - "IssueTitlePlaceholder": "Titolo della issue", - "SubIssueTitlePlaceholder": "Titolo della subissue", - "IssueDescriptionPlaceholder": "Aggiungi descrizione…", - "SubIssueDescriptionPlaceholder": "Aggiungi descrizione della subissue", - "AddIssueTooltip": "Aggiungi issue...", - "NewIssueDialogClose": "Vuoi chiudere questo dialogo?", - "NewIssueDialogCloseNote": "Tutte le modifiche andranno perse", - "RemoveComponentDialogClose": "Eliminare il componente?", - "RemoveComponentDialogCloseNote": "Sei sicuro di voler eliminare questo componente? Questa operazione non può essere annullata", - "Grouping": "Raggruppamento", - "Ordering": "Ordinamento", - "CompletedIssues": "Issue completate", - "NoGrouping": "Nessun raggruppamento", - "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", - "PastMonth": "Mese scorso", - "CopyIssueUrl": "Copia URL della issue negli appunti", - "CopyIssueId": "Copia ID della issue negli appunti", - "CopyIssueBranch": "Copia nome del ramo Git negli appunti", - "CopyIssueTitle": "Copia titolo della issue negli appunti", - "AssetLabel": "Risorsa", - "AddToComponent": "Aggiungi al componente…", - "MoveToComponent": "Sposta nel componente…", - "NoComponent": "Nessun componente", - "ComponentLeadTitle": "Responsabile del componente", - "ComponentMembersTitle": "Membri del componente", - "ComponentLeadSearchPlaceholder": "Imposta responsabile del componente…", - "ComponentMembersSearchPlaceholder": "Cambia membri del componente…", - "MoveToProject": "Sposta nel progetto", - "Duplicate": "Duplicato", - "GotoIssues": "Vai alle issue", - "GotoActive": "Vai alle issue attive", - "GotoBacklog": "Vai al backlog", - "GotoComponents": "Vai ai componenti", - "GotoMyIssues": "Vai alle mie issue", - "GotoTrackerApplication": "Passa all'applicazione Tracker", - "CreatedOne": "Creato", - "MoveIssues": "Sposta issue", - "MoveIssuesDescription": "Seleziona il progetto in cui desideri spostare le issue", - "ManageAttributes": "Gestisci attributi", - "KeepOriginalAttributes": "Mantieni attributi originali", - "KeepOriginalAttributesTooltip": "Gli stati e i componenti originali delle issue saranno mantenuti nel nuovo progetto", - "SelectReplacement": "I seguenti elementi non sono disponibili nel nuovo progetto. Seleziona un sostituto.", - "MissingItem": "ELEMENTO MANCANTE", - "Replacement": "SOSTITUTO", - "Original": "ORIGINALE", - "OriginalDescription": "Gli elementi di questa sezione verranno creati nel nuovo progetto", - "Relations": "Relazioni", - "RemoveRelation": "Rimuovi relazione...", - "AddBlockedBy": "Segna come bloccato da...", - "AddIsBlocking": "Segna come bloccante...", - "AddRelatedIssue": "Riferisci un'altra issue...", - "RelatedIssue": "Issue correlato {id} - {title}", - "BlockedIssue": "Issue bloccato {id} - {title}", - "BlockingIssue": "Issue bloccante {id} - {title}", - "BlockedBySearchPlaceholder": "Cerca una issue da contrassegnare come bloccato da...", - "IsBlockingSearchPlaceholder": "Cerca una issue da contrassegnare come bloccante...", - "RelatedIssueSearchPlaceholder": "Cerca una issue da riferire...", - "Blocks": "Blocca", - "Related": "Correlato", - "RelatedIssues": "Issue correlate", - "EditIssue": "Modifica {title}", - "EditWorkflowStatuses": "Modifica stati della issue", - "EditProject": "Modifica progetto", - "DeleteProject": "Elimina progetto", - "ArchiveProjectName": "Archivia progetto {name}?", - "ArchiveProjectConfirm": "Vuoi archiviare questo progetto?", - "DeleteProjectConfirm": "Vuoi eliminare questo progetto e tutte le issue?", - "ProjectHasIssues": "Ci sono issue esistenti in questo progetto, sei sicuro di voler archiviare?", - "ManageWorkflowStatuses": "Gestisci tipi di progetto", - "AddWorkflowStatus": "Aggiungi stato della issue", - "EditWorkflowStatus": "Modifica stato della issue", - "DeleteWorkflowStatus": "Elimina stato della issue", - "DeleteWorkflowStatusConfirm": "Vuoi eliminare lo stato \"{status}\"?", - "DeleteWorkflowStatusErrorDescription": "Lo stato \"{status}\" ha {count, plural, =1 {1 issue} other {# issue}} assegnate. Seleziona uno stato per spostare", - "Save": "Salva", - "IncludeItemsThatMatch": "Includi elementi che corrispondono", - "AnyFilter": "qualunque filtro", - "AllFilters": "tutti i filtri", - "NoDescription": "Nessuna descrizione", - "SearchIssue": "Cerca attività...", - "StatusHistory": "Storia degli stati", - "NewSubIssue": "Aggiungi subissue...", - "AddLabel": "Aggiungi etichetta", - "DeleteIssue": "Elimina {issueCount, plural, =1 {issue} other {# issue}}", - "DeleteIssueConfirm": "Vuoi eliminare {issueCount, plural, =1 {issue} other {issue}}{subIssueCount, plural, =0 {?} =1 { e subissue?} other { e subissue?}}", - "Milestone": "Traguardo", - "NoMilestone": "Nessun traguardo", - "MoveToMilestone": "Seleziona traguardo", - "Milestones": "Traguardi", - "AllMilestones": "Tutti", - "PlannedMilestones": "Pianificati", - "ActiveMilestones": "Attivi", - "ClosedMilestones": "Fatto", - "AddToMilestone": "Aggiungi al traguardo", - "MilestoneNamePlaceholder": "Nome del traguardo", - "NewMilestone": "Nuovo traguardo", - "CreateMilestone": "Crea", - "MoveAndDeleteMilestone": "Sposta problemi in {newMilestone} ed elimina {deleteMilestone}", - "MoveAndDeleteMilestoneConfirm": "Vuoi eliminare il traguardo e spostare i problemi in un altro traguardo?", - "Estimation": "Stima", - "ReportedTime": "Tempo trascorso", - "RemainingTime": "Tempo rimanente", - "TimeSpendReports": "Rapporti sul tempo trascorso", - "TimeSpendReport": "Tempo", - "TimeSpendReportAdd": "Aggiungi rapporto sul tempo", - "TimeSpendReportDate": "Data", - "TimeSpendReportValue": "Tempo trascorso", - "TimeSpendReportValueTooltip": "Tempo trascorso in ore", - "TimeSpendReportDescription": "Descrizione", - "TimeSpendDays": "{value}d", - "TimeSpendHours": "{value}h", - "TimeSpendMinutes": "{value}m", - "ChildEstimation": "Stima delle subissue", - "ChildReportedTime": "Tempo delle subissue", - "CapacityValue": "di {value}d", - "NewRelatedIssue": "Nuova issue correlata", - "RelatedIssuesNotFound": "Issue correlate non trovate", - "AddedReference": "Riferimento aggiunto", - "AddedAsBlocked": "Contrassegnato come bloccato", - "AddedAsBlocking": "Contrassegnato come bloccante", - "IssueTemplate": "Modello", - "IssueTemplates": "Modelli", - "NewProcess": "Nuovo modello", - "SaveProcess": "Salva modello", - "NoIssueTemplate": "Nessun modello", - "TemplateReplace": "Vuoi applicare il nuovo modello?", - "TemplateReplaceConfirm": "Tutti i campi saranno sovrascritti dai valori del nuovo modello", - "Apply": "Applica", - "CurrentWorkDay": "Giorno lavorativo corrente", - "PreviousWorkDay": "Giorno lavorativo precedente", - "TimeReportDayTypeLabel": "Seleziona il tipo di giorno", - "DefaultAssignee": "Assegnatario predefinito per le issue", - "SevenHoursLength": "Sette ore", - "EightHoursLength": "Otto ore", - "HourLabel": "h", - "MinuteLabel": "m", - "Saved": "Salvato...", - "CreatedIssue": "Issue creata", - "CreatedSubIssue": "Subissue creata", - "ChangeStatus": "Cambia stato", - "ConfigLabel": "Tracker", - "ConfigDescription": "Estensione per gestire elementi di lavoro e completare tutte le attività.", - "NoStatusFound": "Nessuno stato corrispondente trovato", - "CreateMissingStatus": "Crea stato mancante", - "UnsetParent": "La issue genitore sarà annullato", - "AllProjects": "Tutti i progetti", - "IssueNotificationTitle": "{issueTitle}", - "IssueNotificationBody": "Aggiornato da {senderName}", - "IssueNotificationChanged": "{senderName} ha modificato {property}", - "IssueNotificationChangedProperty": "{senderName} ha modificato {property} in \"{newValue}\"", - "IssueNotificationMessage": "{senderName}: {message}", - "PreviousAssigned": "Assegnato in precedenza", - "IssueAssignedToYou": "Assegnato a te", - "RelatedIssueTargetDescription": "Obiettivo del progetto del problema correlato per Classe o Spazio", - "MapRelatedIssues": "Configura i progetti predefiniti per issue correlate", - "DefaultIssueStatus": "Stato predefinito della issue", - "IssueStatus": "Stato", - "Extensions": "Estensioni", - "UnsetParentIssue": "Annulla l'issue genitore", - "ForbidCreateProjectPermission": "Vieta creazione progetto", - "ForbidCreateProjectPermissionDescription": "Vieta agli utenti di creare nuovi progetti", - "AllowCreatingIssues": "Consenti la creazione di issue", - "Day": "Giorno", - "Gantt": "Gantt", - "Month": "Mese", - "Quarter": "Trimestre", - "Week": "Settimana" - }, - "status": {} -} + "string": { + "TrackerApplication": "Tracker", + "Projects": "I tuoi progetti", + "More": "Altro", + "Default": "Predefinito", + "MakeDefault": "Imposta come predefinito", + "Delete": "Elimina", + "Open": "Apri", + "Members": "Membri", + "Inbox": "Posta in arrivo", + "MyIssues": "Le mie issues", + "ViewIssue": "Visualizza issue", + "IssueCreated": "Issue creata", + "Issues": "Issue", + "Views": "Visualizzazioni", + "Active": "Attivo", + "AllIssues": "Tutte le issue", + "ActiveIssues": "Issue attive", + "BacklogIssues": "Backlog", + "Backlog": "Backlog", + "Board": "Bacheca", + "Components": "Componenti", + "AllComponents": "Tutti", + "BacklogComponents": "Backlog", + "ActiveComponents": "Attivi", + "ClosedComponents": "Chiuso", + "NewComponent": "Nuovo componente", + "CreateComponent": "Crea componente", + "ComponentNamePlaceholder": "Nome del componente", + "ComponentDescriptionPlaceholder": "Descrizione (facoltativo)", + "ComponentLead": "Responsabile", + "ComponentMembers": "Membri", + "StartDate": "Data di inizio", + "TargetDate": "Data obiettivo", + "Planned": "Pianificato", + "InProgress": "In corso", + "Paused": "In pausa", + "Completed": "Completato", + "Canceled": "Annullato", + "CreateProject": "Crea progetto", + "NewProject": "Nuovo progetto", + "ProjectTitle": "Titolo del progetto", + "ProjectTitlePlaceholder": "Nuovo progetto", + "UsedInIssueIDs": "Utilizzato negli ID delle issue", + "Identifier": "Identificatore", + "Import": "Importa", + "ProjectIdentifier": "Identificatore del progetto", + "IdentifierExists": "L'identificatore del progetto esiste già", + "ProjectIdentifierPlaceholder": "PRJCT", + "ChooseIcon": "Scegli icona", + "AddIssue": "Aggiungi issue", + "NewIssue": "Nuova issue", + "NewIssuePlaceholder": "Nuovo", + "ResumeDraft": "Riprendi bozza", + "SaveIssue": "Crea issue", + "SetPriority": "Imposta priorità…", + "SetStatus": "Imposta stato…", + "SelectIssue": "Seleziona issue", + "Priority": "Priorità", + "NoPriority": "Nessuna priorità", + "Urgent": "Urgente", + "High": "Alto", + "Medium": "Medio", + "Low": "Basso", + "Unassigned": "Non assegnato", + "Back": "Indietro", + "List": "Elenco", + "NumberLabels": "{count, plural, =0 {nessuna etichetta} =1 {1 etichetta} other {# etichette}}", + "CategoryBacklog": "Backlog", + "CategoryUnstarted": "Non iniziato", + "CategoryStarted": "Iniziato", + "CategoryCompleted": "Completato", + "CategoryCanceled": "Annullato", + "Title": "Titolo", + "Name": "Nome", + "Description": "Descrizione", + "Status": "Stato", + "Number": "Numero", + "Assignee": "Assegnatario", + "AssignTo": "Assegna a...", + "AssignedTo": "Assegnato a {value}", + "Parent": "Issue genitore", + "SetParent": "Imposta issue genitore…", + "ChangeParent": "Cambia issue genitore…", + "RemoveParent": "Rimuovi issue genitore", + "OpenParent": "Apri issue genitore", + "SubIssues": "Subissue", + "SubIssuesList": "Subissue ({subIssues})", + "OpenSubIssues": "Apri subissue", + "AddSubIssues": "Aggiungi subissue", + "BlockedBy": "Bloccato da", + "RelatedTo": "Correlato a", + "Comments": "Commenti", + "Attachments": "Allegati", + "Labels": "Etichette", + "Component": "Componente", + "Space": "", + "SetDueDate": "Imposta data di scadenza…", + "ChangeDueDate": "Cambia data di scadenza…", + "ModificationDate": "Aggiornato {value}", + "Project": "Progetto", + "Issue": "Issue", + "SubIssue": "Subissue", + "Document": "", + "DocumentIcon": "", + "DocumentColor": "", + "Rank": "Classifica", + "TypeIssuePriority": "Priorità della issue", + "IssueTitlePlaceholder": "Titolo della issue", + "SubIssueTitlePlaceholder": "Titolo della subissue", + "IssueDescriptionPlaceholder": "Aggiungi descrizione…", + "SubIssueDescriptionPlaceholder": "Aggiungi descrizione della subissue", + "AddIssueTooltip": "Aggiungi issue...", + "NewIssueDialogClose": "Vuoi chiudere questo dialogo?", + "NewIssueDialogCloseNote": "Tutte le modifiche andranno perse", + "RemoveComponentDialogClose": "Eliminare il componente?", + "RemoveComponentDialogCloseNote": "Sei sicuro di voler eliminare questo componente? Questa operazione non può essere annullata", + "Grouping": "Raggruppamento", + "Ordering": "Ordinamento", + "CompletedIssues": "Issue completate", + "NoGrouping": "Nessun raggruppamento", + "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", + "PastMonth": "Mese scorso", + "CopyIssueUrl": "Copia URL della issue negli appunti", + "CopyIssueId": "Copia ID della issue negli appunti", + "CopyIssueBranch": "Copia nome del ramo Git negli appunti", + "CopyIssueTitle": "Copia titolo della issue negli appunti", + "AssetLabel": "Risorsa", + "AddToComponent": "Aggiungi al componente…", + "MoveToComponent": "Sposta nel componente…", + "NoComponent": "Nessun componente", + "ComponentLeadTitle": "Responsabile del componente", + "ComponentMembersTitle": "Membri del componente", + "ComponentLeadSearchPlaceholder": "Imposta responsabile del componente…", + "ComponentMembersSearchPlaceholder": "Cambia membri del componente…", + "MoveToProject": "Sposta nel progetto", + "Duplicate": "Duplicato", + "GotoIssues": "Vai alle issue", + "GotoActive": "Vai alle issue attive", + "GotoBacklog": "Vai al backlog", + "GotoComponents": "Vai ai componenti", + "GotoMyIssues": "Vai alle mie issue", + "GotoTrackerApplication": "Passa all'applicazione Tracker", + "CreatedOne": "Creato", + "MoveIssues": "Sposta issue", + "MoveIssuesDescription": "Seleziona il progetto in cui desideri spostare le issue", + "ManageAttributes": "Gestisci attributi", + "KeepOriginalAttributes": "Mantieni attributi originali", + "KeepOriginalAttributesTooltip": "Gli stati e i componenti originali delle issue saranno mantenuti nel nuovo progetto", + "SelectReplacement": "I seguenti elementi non sono disponibili nel nuovo progetto. Seleziona un sostituto.", + "MissingItem": "ELEMENTO MANCANTE", + "Replacement": "SOSTITUTO", + "Original": "ORIGINALE", + "OriginalDescription": "Gli elementi di questa sezione verranno creati nel nuovo progetto", + "Relations": "Relazioni", + "RemoveRelation": "Rimuovi relazione...", + "AddBlockedBy": "Segna come bloccato da...", + "AddIsBlocking": "Segna come bloccante...", + "AddRelatedIssue": "Riferisci un'altra issue...", + "RelatedIssue": "Issue correlato {id} - {title}", + "BlockedIssue": "Issue bloccato {id} - {title}", + "BlockingIssue": "Issue bloccante {id} - {title}", + "BlockedBySearchPlaceholder": "Cerca una issue da contrassegnare come bloccato da...", + "IsBlockingSearchPlaceholder": "Cerca una issue da contrassegnare come bloccante...", + "RelatedIssueSearchPlaceholder": "Cerca una issue da riferire...", + "Blocks": "Blocca", + "Related": "Correlato", + "RelatedIssues": "Issue correlate", + "EditIssue": "Modifica {title}", + "EditWorkflowStatuses": "Modifica stati della issue", + "EditProject": "Modifica progetto", + "DeleteProject": "Elimina progetto", + "ArchiveProjectName": "Archivia progetto {name}?", + "ArchiveProjectConfirm": "Vuoi archiviare questo progetto?", + "DeleteProjectConfirm": "Vuoi eliminare questo progetto e tutte le issue?", + "ProjectHasIssues": "Ci sono issue esistenti in questo progetto, sei sicuro di voler archiviare?", + "ManageWorkflowStatuses": "Gestisci tipi di progetto", + "AddWorkflowStatus": "Aggiungi stato della issue", + "EditWorkflowStatus": "Modifica stato della issue", + "DeleteWorkflowStatus": "Elimina stato della issue", + "DeleteWorkflowStatusConfirm": "Vuoi eliminare lo stato \"{status}\"?", + "DeleteWorkflowStatusErrorDescription": "Lo stato \"{status}\" ha {count, plural, =1 {1 issue} other {# issue}} assegnate. Seleziona uno stato per spostare", + "Save": "Salva", + "IncludeItemsThatMatch": "Includi elementi che corrispondono", + "AnyFilter": "qualunque filtro", + "AllFilters": "tutti i filtri", + "NoDescription": "Nessuna descrizione", + "SearchIssue": "Cerca attività...", + "StatusHistory": "Storia degli stati", + "NewSubIssue": "Aggiungi subissue...", + "AddLabel": "Aggiungi etichetta", + "DeleteIssue": "Elimina {issueCount, plural, =1 {issue} other {# issue}}", + "DeleteIssueConfirm": "Vuoi eliminare {issueCount, plural, =1 {issue} other {issue}}{subIssueCount, plural, =0 {?} =1 { e subissue?} other { e subissue?}}", + "Milestone": "Traguardo", + "NoMilestone": "Nessun traguardo", + "MoveToMilestone": "Seleziona traguardo", + "Milestones": "Traguardi", + "AllMilestones": "Tutti", + "PlannedMilestones": "Pianificati", + "ActiveMilestones": "Attivi", + "ClosedMilestones": "Fatto", + "AddToMilestone": "Aggiungi al traguardo", + "MilestoneNamePlaceholder": "Nome del traguardo", + "NewMilestone": "Nuovo traguardo", + "CreateMilestone": "Crea", + "MoveAndDeleteMilestone": "Sposta problemi in {newMilestone} ed elimina {deleteMilestone}", + "MoveAndDeleteMilestoneConfirm": "Vuoi eliminare il traguardo e spostare i problemi in un altro traguardo?", + "Estimation": "Stima", + "ReportedTime": "Tempo trascorso", + "RemainingTime": "Tempo rimanente", + "TimeSpendReports": "Rapporti sul tempo trascorso", + "TimeSpendReport": "Tempo", + "TimeSpendReportAdd": "Aggiungi rapporto sul tempo", + "TimeSpendReportDate": "Data", + "TimeSpendReportValue": "Tempo trascorso", + "TimeSpendReportValueTooltip": "Tempo trascorso in ore", + "TimeSpendReportDescription": "Descrizione", + "TimeSpendDays": "{value}d", + "TimeSpendHours": "{value}h", + "TimeSpendMinutes": "{value}m", + "ChildEstimation": "Stima delle subissue", + "ChildReportedTime": "Tempo delle subissue", + "CapacityValue": "di {value}d", + "NewRelatedIssue": "Nuova issue correlata", + "RelatedIssuesNotFound": "Issue correlate non trovate", + "AddedReference": "Riferimento aggiunto", + "AddedAsBlocked": "Contrassegnato come bloccato", + "AddedAsBlocking": "Contrassegnato come bloccante", + "IssueTemplate": "Modello", + "IssueTemplates": "Modelli", + "NewProcess": "Nuovo modello", + "SaveProcess": "Salva modello", + "NoIssueTemplate": "Nessun modello", + "TemplateReplace": "Vuoi applicare il nuovo modello?", + "TemplateReplaceConfirm": "Tutti i campi saranno sovrascritti dai valori del nuovo modello", + "Apply": "Applica", + "CurrentWorkDay": "Giorno lavorativo corrente", + "PreviousWorkDay": "Giorno lavorativo precedente", + "TimeReportDayTypeLabel": "Seleziona il tipo di giorno", + "DefaultAssignee": "Assegnatario predefinito per le issue", + "SevenHoursLength": "Sette ore", + "EightHoursLength": "Otto ore", + "HourLabel": "h", + "MinuteLabel": "m", + "Saved": "Salvato...", + "CreatedIssue": "Issue creata", + "CreatedSubIssue": "Subissue creata", + "ChangeStatus": "Cambia stato", + "ConfigLabel": "Tracker", + "ConfigDescription": "Estensione per gestire elementi di lavoro e completare tutte le attività.", + "NoStatusFound": "Nessuno stato corrispondente trovato", + "CreateMissingStatus": "Crea stato mancante", + "UnsetParent": "La issue genitore sarà annullato", + "AllProjects": "Tutti i progetti", + "IssueNotificationTitle": "{issueTitle}", + "IssueNotificationBody": "Aggiornato da {senderName}", + "IssueNotificationChanged": "{senderName} ha modificato {property}", + "IssueNotificationChangedProperty": "{senderName} ha modificato {property} in \"{newValue}\"", + "IssueNotificationMessage": "{senderName}: {message}", + "PreviousAssigned": "Assegnato in precedenza", + "IssueAssignedToYou": "Assegnato a te", + "RelatedIssueTargetDescription": "Obiettivo del progetto del problema correlato per Classe o Spazio", + "MapRelatedIssues": "Configura i progetti predefiniti per issue correlate", + "DefaultIssueStatus": "Stato predefinito della issue", + "IssueStatus": "Stato", + "Extensions": "Estensioni", + "UnsetParentIssue": "Annulla l'issue genitore", + "ForbidCreateProjectPermission": "Vieta creazione progetto", + "ForbidCreateProjectPermissionDescription": "Vieta agli utenti di creare nuovi progetti", + "AllowCreatingIssues": "Consenti la creazione di issue", + "Day": "Giorno", + "Gantt": "Gantt", + "Month": "Mese", + "Quarter": "Trimestre", + "Week": "Settimana", + "GanttShowIssueCode": "Show issue code", + "GanttShowTitle": "Show title" + }, + "status": {} +} \ No newline at end of file diff --git a/plugins/tracker-assets/lang/ja.json b/plugins/tracker-assets/lang/ja.json index 61b13474fd4..d522504f663 100644 --- a/plugins/tracker-assets/lang/ja.json +++ b/plugins/tracker-assets/lang/ja.json @@ -1,287 +1,289 @@ { - "string": { - "TrackerApplication": "トラッカー", - "Projects": "あなたのプロジェクト", - "More": "その他", - "Default": "デフォルト", - "MakeDefault": "デフォルトに設定", - "Delete": "削除", - "Open": "開く", - "Members": "メンバー", - "Inbox": "受信箱", - "MyIssues": "自分のイシュー", - "ViewIssue": "イシューを表示", - "IssueCreated": "イシューが作成されました", - "Issues": "イシュー", - "Views": "ビュー", - "Active": "アクティブ", - "AllIssues": "すべてのイシュー", - "ActiveIssues": "アクティブなイシュー", - "BacklogIssues": "バックログ", - "Backlog": "バックログ", - "Board": "ボード", - "Components": "コンポーネント", - "AllComponents": "すべて", - "BacklogComponents": "バックログ", - "ActiveComponents": "アクティブ", - "ClosedComponents": "完了", - "NewComponent": "新しいコンポーネント", - "CreateComponent": "コンポーネントを作成", - "ComponentNamePlaceholder": "コンポーネント名", - "ComponentDescriptionPlaceholder": "説明 (オプション)", - "ComponentLead": "リーダー", - "ComponentMembers": "メンバー", - "StartDate": "開始日", - "TargetDate": "目標日", - "Planned": "計画済み", - "InProgress": "進行中", - "Paused": "一時停止", - "Completed": "完了", - "Canceled": "キャンセル", - "CreateProject": "プロジェクトを作成", - "NewProject": "新しいプロジェクト", - "ProjectTitle": "プロジェクトタイトル", - "ProjectTitlePlaceholder": "新しいプロジェクト", - "UsedInIssueIDs": "イシューIDで使用", - "Identifier": "識別子", - "Import": "インポート", - "ProjectIdentifier": "プロジェクト識別子", - "IdentifierExists": "プロジェクト識別子はすでに存在します", - "ProjectIdentifierPlaceholder": "PRJCT", - "ChooseIcon": "アイコンを選択", - "AddIssue": "イシューを追加", - "NewIssue": "新しいイシュー", - "NewIssuePlaceholder": "新規", - "ResumeDraft": "下書きを再開", - "SaveIssue": "イシューを作成", - "SetPriority": "優先度を設定\u2026", - "SetStatus": "ステータスを設定\u2026", - "SelectIssue": "イシューを選択", - "Priority": "優先度", - "NoPriority": "優先度なし", - "Urgent": "緊急", - "High": "高", - "Medium": "中", - "Low": "低", - "Unassigned": "未割り当て", - "Back": "戻る", - "List": "リスト", - "NumberLabels": "{count, plural, =0 {ラベルなし} =1 {1 ラベル} other {# ラベル}}", - "CategoryBacklog": "バックログ", - "CategoryUnstarted": "未着手", - "CategoryStarted": "開始", - "CategoryCompleted": "完了", - "CategoryCanceled": "キャンセル", - "Title": "タイトル", - "Name": "名前", - "Description": "説明", - "Status": "ステータス", - "Number": "番号", - "Assignee": "担当者", - "AssignTo": "担当者を設定...", - "AssignedTo": "{value} に割り当て", - "Parent": "親イシュー", - "SetParent": "親イシューを設定\u2026", - "ChangeParent": "親イシューを変更\u2026", - "RemoveParent": "親イシューを削除", - "OpenParent": "親イシューを開く", - "SubIssues": "子イシュー", - "SubIssuesList": "子イシュー ({subIssues})", - "OpenSubIssues": "子イシューを開く", - "AddSubIssues": "子イシューを追加", - "BlockedBy": "ブロックされている", - "RelatedTo": "関連する", - "Comments": "コメント", - "Attachments": "添付ファイル", - "Labels": "ラベル", - "Component": "コンポーネント", - "Space": "", - "SetDueDate": "期日を設定\u2026", - "ChangeDueDate": "期日を変更\u2026", - "ModificationDate": "更新日 {value}", - "Project": "プロジェクト", - "Issue": "イシュー", - "SubIssue": "子イシュー", - "Document": "", - "DocumentIcon": "", - "DocumentColor": "", - "Rank": "ランク", - "TypeIssuePriority": "イシューの優先度", - "IssueTitlePlaceholder": "イシューのタイトル", - "SubIssueTitlePlaceholder": "子イシューのタイトル", - "IssueDescriptionPlaceholder": "説明を追加\u2026", - "SubIssueDescriptionPlaceholder": "子イシューの説明を追加", - "AddIssueTooltip": "イシューを追加...", - "NewIssueDialogClose": "このダイアログを閉じますか?", - "NewIssueDialogCloseNote": "すべての変更は失われます", - "RemoveComponentDialogClose": "コンポーネントを削除しますか?", - "RemoveComponentDialogCloseNote": "本当にこのコンポーネントを削除しますか?この操作は元に戻せません", - "Grouping": "グループ化", - "Ordering": "順序", - "CompletedIssues": "完了したイシュー", - "NoGrouping": "グループ化なし", - "NoAssignee": "担当者なし", - "LastUpdated": "最終更新日", - "DueDate": "期日", - "IssueStartDate": "開始日", - "GanttDependency": "Dependency", - "GanttLag": "Lag", - "Manual": "手動", - "All": "すべて", - "PastWeek": "先週", - "PastMonth": "先月", - "CopyIssueUrl": "イシューのURLをクリップボードにコピー", - "CopyIssueId": "イシューIDをクリップボードにコピー", - "CopyIssueBranch": "Gitブランチ名をクリップボードにコピー", - "CopyIssueTitle": "イシューのタイトルをクリップボードにコピー", - "AssetLabel": "アセット", - "AddToComponent": "コンポーネントに追加\u2026", - "MoveToComponent": "コンポーネントに移動\u2026", - "NoComponent": "コンポーネントなし", - "ComponentLeadTitle": "コンポーネントリーダー", - "ComponentMembersTitle": "コンポーネントメンバー", - "ComponentLeadSearchPlaceholder": "コンポーネントリーダーを設定\u2026", - "ComponentMembersSearchPlaceholder": "コンポーネントメンバーを変更\u2026", - "MoveToProject": "プロジェクトに移動", - "Duplicate": "複製", - "GotoIssues": "イシューへ移動", - "GotoActive": "アクティブなイシューへ移動", - "GotoBacklog": "バックログへ移動", - "GotoComponents": "コンポーネントへ移動", - "GotoMyIssues": "自分のイシューへ移動", - "GotoTrackerApplication": "トラッカーアプリケーションに切り替え", - "CreatedOne": "作成済み", - "MoveIssues": "イシューを移動", - "MoveIssuesDescription": "イシューを移動するプロジェクトを選択します", - "ManageAttributes": "属性を管理", - "KeepOriginalAttributes": "元の属性を保持", - "KeepOriginalAttributesTooltip": "元のイシューステータスとコンポーネントは新しいプロジェクトに保持されます", - "SelectReplacement": "次のアイテムは新しいプロジェクトで利用できません。代替を選択してください。", - "MissingItem": "不足しているアイテム", - "Replacement": "代替", - "Original": "オリジナル", - "OriginalDescription": "このセクションのアイテムは新しいプロジェクトで作成されます", - "Relations": "関連", - "RemoveRelation": "関連を削除...", - "AddBlockedBy": "ブロックされているとしてマーク...", - "AddIsBlocking": "ブロックしているとしてマーク...", - "AddRelatedIssue": "別のイシューを参照...", - "RelatedIssue": "関連するイシュー {id} - {title}", - "BlockedIssue": "ブロックされているイシュー {id} - {title}", - "BlockingIssue": "ブロックしているイシュー {id} - {title}", - "BlockedBySearchPlaceholder": "ブロックされているとしてマークするイシューを検索...", - "IsBlockingSearchPlaceholder": "ブロックしているとしてマークするイシューを検索...", - "RelatedIssueSearchPlaceholder": "参照するイシューを検索...", - "Blocks": "ブロック", - "Related": "関連", - "RelatedIssues": "関連するイシュー", - "EditIssue": "{title} を編集", - "EditWorkflowStatuses": "イシューステータスを編集", - "EditProject": "プロジェクトを編集", - "DeleteProject": "プロジェクトを削除", - "ArchiveProjectName": "プロジェクト {name} をアーカイブしますか?", - "ArchiveProjectConfirm": "このプロジェクトをアーカイブしますか?", - "DeleteProjectConfirm": "このプロジェクトとすべてのイシューを削除しますか?", - "ProjectHasIssues": "このプロジェクトには既存のイシューがあります。アーカイブしてもよろしいですか?", - "ManageWorkflowStatuses": "プロジェクトタイプを管理", - "AddWorkflowStatus": "イシューステータスを追加", - "EditWorkflowStatus": "イシューステータスを編集", - "DeleteWorkflowStatus": "イシューステータスを削除", - "DeleteWorkflowStatusConfirm": "\"{status}\" ステータスを削除しますか?", - "DeleteWorkflowStatusErrorDescription": "\"{status}\" ステータスには {count, plural, =1 {1 イシュー} other {# イシュー}} が割り当てられています。移動先のステータスを選択してください", - "Save": "保存", - "IncludeItemsThatMatch": "一致するアイテムを含める", - "AnyFilter": "いずれかのフィルター", - "AllFilters": "すべてのフィルター", - "NoDescription": "説明なし", - "SearchIssue": "タスクを検索...", - "StatusHistory": "状態履歴", - "NewSubIssue": "子イシューを追加...", - "AddLabel": "ラベルを追加", - "DeleteIssue": "{issueCount, plural, =1 {イシュー} other {イシュー}} を削除", - "DeleteIssueConfirm": "{issueCount, plural, =1 {イシュー} other {イシュー}}{subIssueCount, plural, =0 {?} =1 { と子イシュー?} other { と子イシュー?}} を削除しますか?", - "Milestone": "マイルストーン", - "NoMilestone": "マイルストーンなし", - "MoveToMilestone": "マイルストーンを選択", - "Milestones": "マイルストーン", - "AllMilestones": "すべて", - "PlannedMilestones": "計画済み", - "ActiveMilestones": "アクティブ", - "ClosedMilestones": "完了", - "AddToMilestone": "マイルストーンに追加", - "MilestoneNamePlaceholder": "マイルストーン名", - "NewMilestone": "新しいマイルストーン", - "CreateMilestone": "作成", - "MoveAndDeleteMilestone": "イシューを {newMilestone} に移動し、{deleteMilestone} を削除", - "MoveAndDeleteMilestoneConfirm": "マイルストーンを削除し、イシューを別のマイルストーンに移動しますか?", - "Estimation": "見積もり", - "ReportedTime": "消費時間", - "RemainingTime": "残り時間", - "TimeSpendReports": "時間消費レポート", - "TimeSpendReport": "時間", - "TimeSpendReportAdd": "時間レポートを追加", - "TimeSpendReportDate": "日付", - "TimeSpendReportValue": "消費時間", - "TimeSpendReportValueTooltip": "消費時間 (時間)", - "TimeSpendReportDescription": "説明", - "TimeSpendDays": "{value}日", - "TimeSpendHours": "{value}時間", - "TimeSpendMinutes": "{value}分", - "ChildEstimation": "子イシューの見積もり", - "ChildReportedTime": "子イシューの時間", - "CapacityValue": "うち {value}日", - "NewRelatedIssue": "新しい関連イシュー", - "RelatedIssuesNotFound": "関連イシューが見つかりません", - "AddedReference": "参照を追加", - "AddedAsBlocked": "ブロックされているとしてマーク", - "AddedAsBlocking": "ブロックしているとしてマーク", - "IssueTemplate": "テンプレート", - "IssueTemplates": "テンプレート", - "NewProcess": "新しいテンプレート", - "SaveProcess": "テンプレートを保存", - "NoIssueTemplate": "テンプレートなし", - "TemplateReplace": "新しいテンプレートを適用しますか?", - "TemplateReplaceConfirm": "すべてのフィールドが新しいテンプレートの値で上書きされます", - "Apply": "適用", - "CurrentWorkDay": "現在の営業日", - "PreviousWorkDay": "前の営業日", - "TimeReportDayTypeLabel": "時間レポートの日種別を選択", - "DefaultAssignee": "イシューのデフォルトの担当者", - "SevenHoursLength": "7時間", - "EightHoursLength": "8時間", - "HourLabel": "時間", - "MinuteLabel": "分", - "Saved": "保存済み...", - "CreatedIssue": "イシューを作成しました", - "CreatedSubIssue": "子イシューを作成しました", - "ChangeStatus": "ステータスを変更", - "ConfigLabel": "トラッカー", - "ConfigDescription": "作業項目を管理し、すべてのジョブを完了するための拡張機能。", - "NoStatusFound": "一致するステータスが見つかりません", - "CreateMissingStatus": "不足しているステータスを作成", - "UnsetParent": "親イシューが設定されなくなります", - "AllProjects": "すべてのプロジェクト", - "IssueNotificationTitle": "{issueTitle}", - "IssueNotificationBody": "{senderName} が更新しました", - "IssueNotificationChanged": "{senderName} が {property} を変更しました", - "IssueNotificationChangedProperty": "{senderName} が {property} を \"{newValue}\" に変更しました", - "IssueNotificationMessage": "{senderName}: {message}", - "PreviousAssigned": "以前に割り当てられた", - "IssueAssignedToYou": "あなたに割り当てられました", - "RelatedIssueTargetDescription": "クラスまたはスペースの関連イシュープロジェクトターゲット", - "MapRelatedIssues": "関連イシューのデフォルトプロジェクトを構成", - "DefaultIssueStatus": "デフォルトのイシューステータス", - "IssueStatus": "ステータス", - "Extensions": "拡張機能", - "UnsetParentIssue": "親イシューの設定を解除", - "ForbidCreateProjectPermission": "プロジェクト作成禁止", - "ForbidCreateProjectPermissionDescription": "ユーザーが新しいプロジェクトを作成することを禁止します", - "AllowCreatingIssues": "イシューの作成を許可", - "Day": "日", - "Gantt": "Gantt", - "Month": "月", - "Quarter": "四半期", - "Week": "週" - }, - "status": {} -} + "string": { + "TrackerApplication": "トラッカー", + "Projects": "あなたのプロジェクト", + "More": "その他", + "Default": "デフォルト", + "MakeDefault": "デフォルトに設定", + "Delete": "削除", + "Open": "開く", + "Members": "メンバー", + "Inbox": "受信箱", + "MyIssues": "自分のイシュー", + "ViewIssue": "イシューを表示", + "IssueCreated": "イシューが作成されました", + "Issues": "イシュー", + "Views": "ビュー", + "Active": "アクティブ", + "AllIssues": "すべてのイシュー", + "ActiveIssues": "アクティブなイシュー", + "BacklogIssues": "バックログ", + "Backlog": "バックログ", + "Board": "ボード", + "Components": "コンポーネント", + "AllComponents": "すべて", + "BacklogComponents": "バックログ", + "ActiveComponents": "アクティブ", + "ClosedComponents": "完了", + "NewComponent": "新しいコンポーネント", + "CreateComponent": "コンポーネントを作成", + "ComponentNamePlaceholder": "コンポーネント名", + "ComponentDescriptionPlaceholder": "説明 (オプション)", + "ComponentLead": "リーダー", + "ComponentMembers": "メンバー", + "StartDate": "開始日", + "TargetDate": "目標日", + "Planned": "計画済み", + "InProgress": "進行中", + "Paused": "一時停止", + "Completed": "完了", + "Canceled": "キャンセル", + "CreateProject": "プロジェクトを作成", + "NewProject": "新しいプロジェクト", + "ProjectTitle": "プロジェクトタイトル", + "ProjectTitlePlaceholder": "新しいプロジェクト", + "UsedInIssueIDs": "イシューIDで使用", + "Identifier": "識別子", + "Import": "インポート", + "ProjectIdentifier": "プロジェクト識別子", + "IdentifierExists": "プロジェクト識別子はすでに存在します", + "ProjectIdentifierPlaceholder": "PRJCT", + "ChooseIcon": "アイコンを選択", + "AddIssue": "イシューを追加", + "NewIssue": "新しいイシュー", + "NewIssuePlaceholder": "新規", + "ResumeDraft": "下書きを再開", + "SaveIssue": "イシューを作成", + "SetPriority": "優先度を設定…", + "SetStatus": "ステータスを設定…", + "SelectIssue": "イシューを選択", + "Priority": "優先度", + "NoPriority": "優先度なし", + "Urgent": "緊急", + "High": "高", + "Medium": "中", + "Low": "低", + "Unassigned": "未割り当て", + "Back": "戻る", + "List": "リスト", + "NumberLabels": "{count, plural, =0 {ラベルなし} =1 {1 ラベル} other {# ラベル}}", + "CategoryBacklog": "バックログ", + "CategoryUnstarted": "未着手", + "CategoryStarted": "開始", + "CategoryCompleted": "完了", + "CategoryCanceled": "キャンセル", + "Title": "タイトル", + "Name": "名前", + "Description": "説明", + "Status": "ステータス", + "Number": "番号", + "Assignee": "担当者", + "AssignTo": "担当者を設定...", + "AssignedTo": "{value} に割り当て", + "Parent": "親イシュー", + "SetParent": "親イシューを設定…", + "ChangeParent": "親イシューを変更…", + "RemoveParent": "親イシューを削除", + "OpenParent": "親イシューを開く", + "SubIssues": "子イシュー", + "SubIssuesList": "子イシュー ({subIssues})", + "OpenSubIssues": "子イシューを開く", + "AddSubIssues": "子イシューを追加", + "BlockedBy": "ブロックされている", + "RelatedTo": "関連する", + "Comments": "コメント", + "Attachments": "添付ファイル", + "Labels": "ラベル", + "Component": "コンポーネント", + "Space": "", + "SetDueDate": "期日を設定…", + "ChangeDueDate": "期日を変更…", + "ModificationDate": "更新日 {value}", + "Project": "プロジェクト", + "Issue": "イシュー", + "SubIssue": "子イシュー", + "Document": "", + "DocumentIcon": "", + "DocumentColor": "", + "Rank": "ランク", + "TypeIssuePriority": "イシューの優先度", + "IssueTitlePlaceholder": "イシューのタイトル", + "SubIssueTitlePlaceholder": "子イシューのタイトル", + "IssueDescriptionPlaceholder": "説明を追加…", + "SubIssueDescriptionPlaceholder": "子イシューの説明を追加", + "AddIssueTooltip": "イシューを追加...", + "NewIssueDialogClose": "このダイアログを閉じますか?", + "NewIssueDialogCloseNote": "すべての変更は失われます", + "RemoveComponentDialogClose": "コンポーネントを削除しますか?", + "RemoveComponentDialogCloseNote": "本当にこのコンポーネントを削除しますか?この操作は元に戻せません", + "Grouping": "グループ化", + "Ordering": "順序", + "CompletedIssues": "完了したイシュー", + "NoGrouping": "グループ化なし", + "NoAssignee": "担当者なし", + "LastUpdated": "最終更新日", + "DueDate": "期日", + "IssueStartDate": "開始日", + "GanttDependency": "Dependency", + "GanttLag": "Lag", + "Manual": "手動", + "All": "すべて", + "PastWeek": "先週", + "PastMonth": "先月", + "CopyIssueUrl": "イシューのURLをクリップボードにコピー", + "CopyIssueId": "イシューIDをクリップボードにコピー", + "CopyIssueBranch": "Gitブランチ名をクリップボードにコピー", + "CopyIssueTitle": "イシューのタイトルをクリップボードにコピー", + "AssetLabel": "アセット", + "AddToComponent": "コンポーネントに追加…", + "MoveToComponent": "コンポーネントに移動…", + "NoComponent": "コンポーネントなし", + "ComponentLeadTitle": "コンポーネントリーダー", + "ComponentMembersTitle": "コンポーネントメンバー", + "ComponentLeadSearchPlaceholder": "コンポーネントリーダーを設定…", + "ComponentMembersSearchPlaceholder": "コンポーネントメンバーを変更…", + "MoveToProject": "プロジェクトに移動", + "Duplicate": "複製", + "GotoIssues": "イシューへ移動", + "GotoActive": "アクティブなイシューへ移動", + "GotoBacklog": "バックログへ移動", + "GotoComponents": "コンポーネントへ移動", + "GotoMyIssues": "自分のイシューへ移動", + "GotoTrackerApplication": "トラッカーアプリケーションに切り替え", + "CreatedOne": "作成済み", + "MoveIssues": "イシューを移動", + "MoveIssuesDescription": "イシューを移動するプロジェクトを選択します", + "ManageAttributes": "属性を管理", + "KeepOriginalAttributes": "元の属性を保持", + "KeepOriginalAttributesTooltip": "元のイシューステータスとコンポーネントは新しいプロジェクトに保持されます", + "SelectReplacement": "次のアイテムは新しいプロジェクトで利用できません。代替を選択してください。", + "MissingItem": "不足しているアイテム", + "Replacement": "代替", + "Original": "オリジナル", + "OriginalDescription": "このセクションのアイテムは新しいプロジェクトで作成されます", + "Relations": "関連", + "RemoveRelation": "関連を削除...", + "AddBlockedBy": "ブロックされているとしてマーク...", + "AddIsBlocking": "ブロックしているとしてマーク...", + "AddRelatedIssue": "別のイシューを参照...", + "RelatedIssue": "関連するイシュー {id} - {title}", + "BlockedIssue": "ブロックされているイシュー {id} - {title}", + "BlockingIssue": "ブロックしているイシュー {id} - {title}", + "BlockedBySearchPlaceholder": "ブロックされているとしてマークするイシューを検索...", + "IsBlockingSearchPlaceholder": "ブロックしているとしてマークするイシューを検索...", + "RelatedIssueSearchPlaceholder": "参照するイシューを検索...", + "Blocks": "ブロック", + "Related": "関連", + "RelatedIssues": "関連するイシュー", + "EditIssue": "{title} を編集", + "EditWorkflowStatuses": "イシューステータスを編集", + "EditProject": "プロジェクトを編集", + "DeleteProject": "プロジェクトを削除", + "ArchiveProjectName": "プロジェクト {name} をアーカイブしますか?", + "ArchiveProjectConfirm": "このプロジェクトをアーカイブしますか?", + "DeleteProjectConfirm": "このプロジェクトとすべてのイシューを削除しますか?", + "ProjectHasIssues": "このプロジェクトには既存のイシューがあります。アーカイブしてもよろしいですか?", + "ManageWorkflowStatuses": "プロジェクトタイプを管理", + "AddWorkflowStatus": "イシューステータスを追加", + "EditWorkflowStatus": "イシューステータスを編集", + "DeleteWorkflowStatus": "イシューステータスを削除", + "DeleteWorkflowStatusConfirm": "\"{status}\" ステータスを削除しますか?", + "DeleteWorkflowStatusErrorDescription": "\"{status}\" ステータスには {count, plural, =1 {1 イシュー} other {# イシュー}} が割り当てられています。移動先のステータスを選択してください", + "Save": "保存", + "IncludeItemsThatMatch": "一致するアイテムを含める", + "AnyFilter": "いずれかのフィルター", + "AllFilters": "すべてのフィルター", + "NoDescription": "説明なし", + "SearchIssue": "タスクを検索...", + "StatusHistory": "状態履歴", + "NewSubIssue": "子イシューを追加...", + "AddLabel": "ラベルを追加", + "DeleteIssue": "{issueCount, plural, =1 {イシュー} other {イシュー}} を削除", + "DeleteIssueConfirm": "{issueCount, plural, =1 {イシュー} other {イシュー}}{subIssueCount, plural, =0 {?} =1 { と子イシュー?} other { と子イシュー?}} を削除しますか?", + "Milestone": "マイルストーン", + "NoMilestone": "マイルストーンなし", + "MoveToMilestone": "マイルストーンを選択", + "Milestones": "マイルストーン", + "AllMilestones": "すべて", + "PlannedMilestones": "計画済み", + "ActiveMilestones": "アクティブ", + "ClosedMilestones": "完了", + "AddToMilestone": "マイルストーンに追加", + "MilestoneNamePlaceholder": "マイルストーン名", + "NewMilestone": "新しいマイルストーン", + "CreateMilestone": "作成", + "MoveAndDeleteMilestone": "イシューを {newMilestone} に移動し、{deleteMilestone} を削除", + "MoveAndDeleteMilestoneConfirm": "マイルストーンを削除し、イシューを別のマイルストーンに移動しますか?", + "Estimation": "見積もり", + "ReportedTime": "消費時間", + "RemainingTime": "残り時間", + "TimeSpendReports": "時間消費レポート", + "TimeSpendReport": "時間", + "TimeSpendReportAdd": "時間レポートを追加", + "TimeSpendReportDate": "日付", + "TimeSpendReportValue": "消費時間", + "TimeSpendReportValueTooltip": "消費時間 (時間)", + "TimeSpendReportDescription": "説明", + "TimeSpendDays": "{value}日", + "TimeSpendHours": "{value}時間", + "TimeSpendMinutes": "{value}分", + "ChildEstimation": "子イシューの見積もり", + "ChildReportedTime": "子イシューの時間", + "CapacityValue": "うち {value}日", + "NewRelatedIssue": "新しい関連イシュー", + "RelatedIssuesNotFound": "関連イシューが見つかりません", + "AddedReference": "参照を追加", + "AddedAsBlocked": "ブロックされているとしてマーク", + "AddedAsBlocking": "ブロックしているとしてマーク", + "IssueTemplate": "テンプレート", + "IssueTemplates": "テンプレート", + "NewProcess": "新しいテンプレート", + "SaveProcess": "テンプレートを保存", + "NoIssueTemplate": "テンプレートなし", + "TemplateReplace": "新しいテンプレートを適用しますか?", + "TemplateReplaceConfirm": "すべてのフィールドが新しいテンプレートの値で上書きされます", + "Apply": "適用", + "CurrentWorkDay": "現在の営業日", + "PreviousWorkDay": "前の営業日", + "TimeReportDayTypeLabel": "時間レポートの日種別を選択", + "DefaultAssignee": "イシューのデフォルトの担当者", + "SevenHoursLength": "7時間", + "EightHoursLength": "8時間", + "HourLabel": "時間", + "MinuteLabel": "分", + "Saved": "保存済み...", + "CreatedIssue": "イシューを作成しました", + "CreatedSubIssue": "子イシューを作成しました", + "ChangeStatus": "ステータスを変更", + "ConfigLabel": "トラッカー", + "ConfigDescription": "作業項目を管理し、すべてのジョブを完了するための拡張機能。", + "NoStatusFound": "一致するステータスが見つかりません", + "CreateMissingStatus": "不足しているステータスを作成", + "UnsetParent": "親イシューが設定されなくなります", + "AllProjects": "すべてのプロジェクト", + "IssueNotificationTitle": "{issueTitle}", + "IssueNotificationBody": "{senderName} が更新しました", + "IssueNotificationChanged": "{senderName} が {property} を変更しました", + "IssueNotificationChangedProperty": "{senderName} が {property} を \"{newValue}\" に変更しました", + "IssueNotificationMessage": "{senderName}: {message}", + "PreviousAssigned": "以前に割り当てられた", + "IssueAssignedToYou": "あなたに割り当てられました", + "RelatedIssueTargetDescription": "クラスまたはスペースの関連イシュープロジェクトターゲット", + "MapRelatedIssues": "関連イシューのデフォルトプロジェクトを構成", + "DefaultIssueStatus": "デフォルトのイシューステータス", + "IssueStatus": "ステータス", + "Extensions": "拡張機能", + "UnsetParentIssue": "親イシューの設定を解除", + "ForbidCreateProjectPermission": "プロジェクト作成禁止", + "ForbidCreateProjectPermissionDescription": "ユーザーが新しいプロジェクトを作成することを禁止します", + "AllowCreatingIssues": "イシューの作成を許可", + "Day": "日", + "Gantt": "Gantt", + "Month": "月", + "Quarter": "四半期", + "Week": "週", + "GanttShowIssueCode": "Show issue code", + "GanttShowTitle": "Show title" + }, + "status": {} +} \ No newline at end of file diff --git a/plugins/tracker-assets/lang/pt.json b/plugins/tracker-assets/lang/pt.json index d83e2745a75..463e20de583 100644 --- a/plugins/tracker-assets/lang/pt.json +++ b/plugins/tracker-assets/lang/pt.json @@ -1,287 +1,289 @@ { - "string": { - "TrackerApplication": "Seguimento", - "Projects": "Os teus projetos", - "More": "Mais", - "Default": "Padrão", - "MakeDefault": "Tornar padrão", - "Delete": "Apagar", - "Open": "Abrir", - "Members": "Membros", - "Inbox": "Caixa de entrada", - "MyIssues": "Os meus problemas", - "ViewIssue": "Ver problema", - "IssueCreated": "Problema criado", - "Issues": "Problemas", - "Views": "Vistas", - "Active": "Ativo", - "AllIssues": "Todos os problemas", - "ActiveIssues": "Problemas ativos", - "BacklogIssues": "Atraso", - "Backlog": "Atraso", - "Board": "Quadro", - "Components": "Componentes", - "AllComponents": "Todos", - "BacklogComponents": "Atraso", - "ActiveComponents": "Ativo", - "ClosedComponents": "Fechado", - "NewComponent": "Novo componente", - "CreateComponent": "Criar componente", - "ComponentNamePlaceholder": "Nome do componente", - "ComponentDescriptionPlaceholder": "Descrição (opcional)", - "ComponentLead": "Líder", - "ComponentMembers": "Membros", - "StartDate": "Data de início", - "TargetDate": "Data alvo", - "Planned": "Planeado", - "InProgress": "Em progresso", - "Paused": "Pausado", - "Completed": "Concluído", - "Canceled": "Cancelado", - "CreateProject": "Criar projeto", - "NewProject": "Novo projeto", - "ProjectTitle": "Título do projeto", - "ProjectTitlePlaceholder": "Novo projeto", - "UsedInIssueIDs": "Usado em IDs de problemas", - "Identifier": "Identificador", - "Import": "Importar", - "ProjectIdentifier": "Identificador do projeto", - "IdentifierExists": "O identificador do projeto já existe", - "ProjectIdentifierPlaceholder": "PROJ", - "ChooseIcon": "Escolher ícone", - "AddIssue": "Adicionar problema", - "NewIssue": "Novo problema", - "NewIssuePlaceholder": "Novo", - "ResumeDraft": "Retomar rascunho", - "SaveIssue": "Criar problema", - "SetPriority": "Definir prioridade\u2026", - "SetStatus": "Definir estado\u2026", - "SelectIssue": "Selecionar problema", - "Priority": "Prioridade", - "NoPriority": "Sem prioridade", - "Urgent": "Urgente", - "High": "Alta", - "Medium": "Média", - "Low": "Baixa", - "Unassigned": "Não atribuído", - "Back": "Voltar", - "List": "Lista", - "NumberLabels": "{count, plural, =0 {sem etiquetas} =1 {1 etiqueta} other {# etiquetas}}", - "CategoryBacklog": "Atraso", - "CategoryUnstarted": "Não iniciado", - "CategoryStarted": "Iniciado", - "CategoryCompleted": "Concluído", - "CategoryCanceled": "Cancelado", - "Title": "Título", - "Name": "Nome", - "Description": "Descrição", - "Status": "Estado", - "Number": "Número", - "Assignee": "Atribuído a", - "AssignTo": "Atribuir a...", - "AssignedTo": "Atribuído a {value}", - "Parent": "Problema superior", - "SetParent": "Definir problema superior\u2026", - "ChangeParent": "Alterar problema superior\u2026", - "RemoveParent": "Remover problema superior", - "OpenParent": "Abrir problema superior", - "SubIssues": "Sub-problemas", - "SubIssuesList": "Sub-problemas ({subIssues})", - "OpenSubIssues": "Abrir sub-problemas", - "AddSubIssues": "Adicionar sub-problema", - "BlockedBy": "Bloqueado por", - "RelatedTo": "Relacionado com", - "Comments": "Comentários", - "Attachments": "Anexos", - "Labels": "Etiquetas", - "Component": "Componente", - "Space": "", - "SetDueDate": "Definir data de vencimento\u2026", - "ChangeDueDate": "Alterar data de vencimento", - "ModificationDate": "Atualizado {value}", - "Project": "Projeto", - "Issue": "Tarefa", - "SubIssue": "Sub-Tarefa", - "Document": "", - "DocumentIcon": "", - "DocumentColor": "", - "Rank": "Classificação", - "TypeIssuePriority": "Prioridade da Tarefa", - "IssueTitlePlaceholder": "Título da Tarefa", - "SubIssueTitlePlaceholder": "Título da Sub-Tarefa", - "IssueDescriptionPlaceholder": "Adicionar descrição...", - "SubIssueDescriptionPlaceholder": "Adicionar Descrição da Sub-Tarefa", - "AddIssueTooltip": "Adicionar Tarefa...", - "NewIssueDialogClose": "Deseja fechar este diálogo?", - "NewIssueDialogCloseNote": "Todas as alterações serão perdidas", - "RemoveComponentDialogClose": "Eliminar o componente?", - "RemoveComponentDialogCloseNote": "Tem a certeza de que deseja eliminar este componente? Esta operação não pode ser desfeita", - "Grouping": "Agrupamento", - "Ordering": "Ordenação", - "CompletedIssues": "Tarefas Concluídas", - "NoGrouping": "Sem agrupamento", - "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", - "PastMonth": "Mês passado", - "CopyIssueUrl": "Copiar URL da questão para a área de transferência", - "CopyIssueId": "Copiar ID da questão para a área de transferência", - "CopyIssueBranch": "Copiar nome do branch Git para a área de transferência", - "CopyIssueTitle": "Copiar título da questão para a área de transferência", - "AssetLabel": "Ativo", - "AddToComponent": "Adicionar ao componente...", - "MoveToComponent": "Mover para o componente...", - "NoComponent": "Sem componente", - "ComponentLeadTitle": "Responsável pelo componente", - "ComponentMembersTitle": "Membros do componente", - "ComponentLeadSearchPlaceholder": "Definir responsável pelo componente...", - "ComponentMembersSearchPlaceholder": "Alterar membros do componente...", - "MoveToProject": "Mover para o projeto", - "Duplicate": "Duplicar", - "GotoIssues": "Ir para as questões", - "GotoActive": "Ir para as questões ativas", - "GotoBacklog": "Ir para a lista de espera", - "GotoComponents": "Ir para os componentes", - "GotoMyIssues": "Ir para as minhas questões", - "GotoTrackerApplication": "Alternar para a Aplicação de Acompanhamento", - "CreatedOne": "Criado", - "MoveIssues": "Mover questões", - "MoveIssuesDescription": "Selecione o projeto para mover as questões", - "ManageAttributes": "Gerir atributos", - "KeepOriginalAttributes": "Manter atributos originais", - "KeepOriginalAttributesTooltip": "Os estados e componentes originais da questão serão mantidos no novo projeto", - "SelectReplacement": "Os seguintes itens não estão disponíveis no novo projeto. Selecione uma substituição.", - "MissingItem": "ITEM EM FALTA", - "Replacement": "SUBSTITUIÇÃO", - "Original": "ORIGINAL", - "OriginalDescription": "Os itens desta secção serão criados no novo projeto", - "Relations": "Relações", - "RemoveRelation": "Remover relação...", - "AddBlockedBy": "Marcar como bloqueado por...", - "AddIsBlocking": "Marcar como bloqueador...", - "AddRelatedIssue": "Referenciar outra questão...", - "RelatedIssue": "Questão relacionada {id} - {title}", - "BlockedIssue": "Questão bloqueada {id} - {title}", - "BlockingIssue": "Questão bloqueadora {id} - {title}", - "BlockedBySearchPlaceholder": "Procurar questão para marcar como bloqueada por...", - "IsBlockingSearchPlaceholder": "Procurar questão para marcar como bloqueadora...", - "RelatedIssueSearchPlaceholder": "Procurar questão para referenciar...", - "Blocks": "Bloqueia", - "Related": "Relacionado", - "RelatedIssues": "Questões relacionadas", - "EditIssue": "Editar {title}", - "EditWorkflowStatuses": "Editar estados de tarefa", - "EditProject": "Editar projeto", - "DeleteProject": "Apagar projeto", - "ArchiveProjectName": "Arquivar projeto {name}?", - "ArchiveProjectConfirm": "Deseja arquivar este projeto?", - "DeleteProjectConfirm": "Deseja apagar este projeto e todas as tarefas?", - "ProjectHasIssues": "Existem tarefas neste projeto, tem a certeza de que deseja arquivar?", - "ManageWorkflowStatuses": "Gerir tipos de projeto", - "AddWorkflowStatus": "Adicionar estado de tarefa", - "EditWorkflowStatus": "Editar estado de tarefa", - "DeleteWorkflowStatus": "Apagar estado de tarefa", - "DeleteWorkflowStatusConfirm": "Deseja apagar o estado \"{status}\"?", - "DeleteWorkflowStatusErrorDescription": "O estado \"{status}\" tem {count, plural, =1 {1 tarefa} other {# tarefas}} atribuída. Por favor, selecione um estado para mover", - "Save": "Guardar", - "IncludeItemsThatMatch": "Incluir itens que coincidam", - "AnyFilter": "qualquer filtro", - "AllFilters": "todos os filtros", - "NoDescription": "Sem descrição", - "SearchIssue": "Procurar tarefa...", - "StatusHistory": "Histórico de estado", - "NewSubIssue": "Adicionar sub-tarefa...", - "AddLabel": "Adicionar etiqueta", - "DeleteIssue": "Apagar {issueCount, plural, =1 {tarefa} other {# tarefas}}", - "DeleteIssueConfirm": "Deseja apagar {issueCount, plural, =1 {tarefa} other {tarefas}}{subIssueCount, plural, =0 {?} =1 { e sub-tarefa?} other { e sub-tarefas?}}", - "Milestone": "Objetivo", - "NoMilestone": "Sem objetivo", - "MoveToMilestone": "Selecionar Objetivo", - "Milestones": "Objetivos", - "AllMilestones": "Todos", - "PlannedMilestones": "Planeados", - "ActiveMilestones": "Ativos", - "ClosedMilestones": "Concluídos", - "AddToMilestone": "Adicionar ao Objetivo", - "MilestoneNamePlaceholder": "Nome do objetivo", - "NewMilestone": "Novo Objetivo", - "CreateMilestone": "Criar", - "MoveAndDeleteMilestone": "Mover tarefas para {newMilestone} e Apagar {deleteMilestone}", - "MoveAndDeleteMilestoneConfirm": "Deseja apagar o objetivo e mover tarefas para outro objetivo?", - "Estimation": "Estimativa", - "ReportedTime": "Tempo gasto", - "RemainingTime": "Tempo restante", - "TimeSpendReports": "Relatórios de tempo gasto", - "TimeSpendReport": "Tempo", - "TimeSpendReportAdd": "Adicionar relatório de tempo", - "TimeSpendReportDate": "Data", - "TimeSpendReportValue": "Tempo gasto", - "TimeSpendReportValueTooltip": "Tempo gasto em horas", - "TimeSpendReportDescription": "Descrição", - "TimeSpendDays": "{value}d", - "TimeSpendHours": "{value}h", - "TimeSpendMinutes": "{value}m", - "ChildEstimation": "Estimativa de sub-tarefas", - "ChildReportedTime": "Tempo de sub-tarefas", - "CapacityValue": "de {value}d", - "NewRelatedIssue": "Nova tarefa relacionada", - "RelatedIssuesNotFound": "Tarefas relacionadas não encontradas", - "AddedReference": "Referência adicionada", - "AddedAsBlocked": "Marcado como bloqueado", - "AddedAsBlocking": "Marcado como bloqueador", - "IssueTemplate": "Modelo", - "IssueTemplates": "Modelos", - "NewProcess": "Novo Modelo", - "SaveProcess": "Guardar Modelo", - "NoIssueTemplate": "Sem Modelo", - "TemplateReplace": "Deseja aplicar o novo modelo?", - "TemplateReplaceConfirm": "Todos os campos serão sobrescritos pelos valores do novo modelo", - "Apply": "Aplicar", - "CurrentWorkDay": "Dia de trabalho atual", - "PreviousWorkDay": "Dia de trabalho anterior", - "TimeReportDayTypeLabel": "Selecione o tipo de dia do relatório de tempo", - "DefaultAssignee": "Responsável padrão para problemas", - "SevenHoursLength": "Sete Horas", - "EightHoursLength": "Oito Horas", - "HourLabel": "h", - "MinuteLabel": "m", - "Saved": "Guardado...", - "CreatedIssue": "Problema criado", - "CreatedSubIssue": "Sub-problema criado", - "ChangeStatus": "Alterar estado", - "ConfigLabel": "Rastreador", - "ConfigDescription": "Extensão para gerir itens de trabalho e fazer todas as tarefas necessárias.", - "NoStatusFound": "Nenhum estado correspondente encontrado", - "CreateMissingStatus": "Criar estado em falta", - "UnsetParent": "O problema pai será desassociado", - "AllProjects": "Todos os projetos", - "IssueNotificationTitle": "{issueTitle}", - "IssueNotificationBody": "Atualizado por {senderName}", - "IssueNotificationChanged": "{senderName} alterou {property}", - "IssueNotificationChangedProperty": "{senderName} alterou {property} para \"{newValue}\"", - "IssueNotificationMessage": "{senderName}: {message}", - "PreviousAssigned": "Anteriormente atribuído", - "IssueAssignedToYou": "Atribuído a si", - "RelatedIssueTargetDescription": "Projeto alvo de problemas relacionados para Classe ou Espaço", - "MapRelatedIssues": "Configurar projetos padrão de problemas relacionados", - "DefaultIssueStatus": "Estado padrão do problema", - "IssueStatus": "Estado", - "Extensions": "Extensions", - "UnsetParentIssue": "Desmarcar problema pai", - "ForbidCreateProjectPermission": "Proibir criação de projeto", - "ForbidCreateProjectPermissionDescription": "Proíbe os usuários de criar novos projetos", - "AllowCreatingIssues": "Permitir criar problemas", - "Day": "Dia", - "Gantt": "Gantt", - "Month": "Mês", - "Quarter": "Trimestre", - "Week": "Semana" - }, - "status": {} -} + "string": { + "TrackerApplication": "Seguimento", + "Projects": "Os teus projetos", + "More": "Mais", + "Default": "Padrão", + "MakeDefault": "Tornar padrão", + "Delete": "Apagar", + "Open": "Abrir", + "Members": "Membros", + "Inbox": "Caixa de entrada", + "MyIssues": "Os meus problemas", + "ViewIssue": "Ver problema", + "IssueCreated": "Problema criado", + "Issues": "Problemas", + "Views": "Vistas", + "Active": "Ativo", + "AllIssues": "Todos os problemas", + "ActiveIssues": "Problemas ativos", + "BacklogIssues": "Atraso", + "Backlog": "Atraso", + "Board": "Quadro", + "Components": "Componentes", + "AllComponents": "Todos", + "BacklogComponents": "Atraso", + "ActiveComponents": "Ativo", + "ClosedComponents": "Fechado", + "NewComponent": "Novo componente", + "CreateComponent": "Criar componente", + "ComponentNamePlaceholder": "Nome do componente", + "ComponentDescriptionPlaceholder": "Descrição (opcional)", + "ComponentLead": "Líder", + "ComponentMembers": "Membros", + "StartDate": "Data de início", + "TargetDate": "Data alvo", + "Planned": "Planeado", + "InProgress": "Em progresso", + "Paused": "Pausado", + "Completed": "Concluído", + "Canceled": "Cancelado", + "CreateProject": "Criar projeto", + "NewProject": "Novo projeto", + "ProjectTitle": "Título do projeto", + "ProjectTitlePlaceholder": "Novo projeto", + "UsedInIssueIDs": "Usado em IDs de problemas", + "Identifier": "Identificador", + "Import": "Importar", + "ProjectIdentifier": "Identificador do projeto", + "IdentifierExists": "O identificador do projeto já existe", + "ProjectIdentifierPlaceholder": "PROJ", + "ChooseIcon": "Escolher ícone", + "AddIssue": "Adicionar problema", + "NewIssue": "Novo problema", + "NewIssuePlaceholder": "Novo", + "ResumeDraft": "Retomar rascunho", + "SaveIssue": "Criar problema", + "SetPriority": "Definir prioridade…", + "SetStatus": "Definir estado…", + "SelectIssue": "Selecionar problema", + "Priority": "Prioridade", + "NoPriority": "Sem prioridade", + "Urgent": "Urgente", + "High": "Alta", + "Medium": "Média", + "Low": "Baixa", + "Unassigned": "Não atribuído", + "Back": "Voltar", + "List": "Lista", + "NumberLabels": "{count, plural, =0 {sem etiquetas} =1 {1 etiqueta} other {# etiquetas}}", + "CategoryBacklog": "Atraso", + "CategoryUnstarted": "Não iniciado", + "CategoryStarted": "Iniciado", + "CategoryCompleted": "Concluído", + "CategoryCanceled": "Cancelado", + "Title": "Título", + "Name": "Nome", + "Description": "Descrição", + "Status": "Estado", + "Number": "Número", + "Assignee": "Atribuído a", + "AssignTo": "Atribuir a...", + "AssignedTo": "Atribuído a {value}", + "Parent": "Problema superior", + "SetParent": "Definir problema superior…", + "ChangeParent": "Alterar problema superior…", + "RemoveParent": "Remover problema superior", + "OpenParent": "Abrir problema superior", + "SubIssues": "Sub-problemas", + "SubIssuesList": "Sub-problemas ({subIssues})", + "OpenSubIssues": "Abrir sub-problemas", + "AddSubIssues": "Adicionar sub-problema", + "BlockedBy": "Bloqueado por", + "RelatedTo": "Relacionado com", + "Comments": "Comentários", + "Attachments": "Anexos", + "Labels": "Etiquetas", + "Component": "Componente", + "Space": "", + "SetDueDate": "Definir data de vencimento…", + "ChangeDueDate": "Alterar data de vencimento", + "ModificationDate": "Atualizado {value}", + "Project": "Projeto", + "Issue": "Tarefa", + "SubIssue": "Sub-Tarefa", + "Document": "", + "DocumentIcon": "", + "DocumentColor": "", + "Rank": "Classificação", + "TypeIssuePriority": "Prioridade da Tarefa", + "IssueTitlePlaceholder": "Título da Tarefa", + "SubIssueTitlePlaceholder": "Título da Sub-Tarefa", + "IssueDescriptionPlaceholder": "Adicionar descrição...", + "SubIssueDescriptionPlaceholder": "Adicionar Descrição da Sub-Tarefa", + "AddIssueTooltip": "Adicionar Tarefa...", + "NewIssueDialogClose": "Deseja fechar este diálogo?", + "NewIssueDialogCloseNote": "Todas as alterações serão perdidas", + "RemoveComponentDialogClose": "Eliminar o componente?", + "RemoveComponentDialogCloseNote": "Tem a certeza de que deseja eliminar este componente? Esta operação não pode ser desfeita", + "Grouping": "Agrupamento", + "Ordering": "Ordenação", + "CompletedIssues": "Tarefas Concluídas", + "NoGrouping": "Sem agrupamento", + "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", + "PastMonth": "Mês passado", + "CopyIssueUrl": "Copiar URL da questão para a área de transferência", + "CopyIssueId": "Copiar ID da questão para a área de transferência", + "CopyIssueBranch": "Copiar nome do branch Git para a área de transferência", + "CopyIssueTitle": "Copiar título da questão para a área de transferência", + "AssetLabel": "Ativo", + "AddToComponent": "Adicionar ao componente...", + "MoveToComponent": "Mover para o componente...", + "NoComponent": "Sem componente", + "ComponentLeadTitle": "Responsável pelo componente", + "ComponentMembersTitle": "Membros do componente", + "ComponentLeadSearchPlaceholder": "Definir responsável pelo componente...", + "ComponentMembersSearchPlaceholder": "Alterar membros do componente...", + "MoveToProject": "Mover para o projeto", + "Duplicate": "Duplicar", + "GotoIssues": "Ir para as questões", + "GotoActive": "Ir para as questões ativas", + "GotoBacklog": "Ir para a lista de espera", + "GotoComponents": "Ir para os componentes", + "GotoMyIssues": "Ir para as minhas questões", + "GotoTrackerApplication": "Alternar para a Aplicação de Acompanhamento", + "CreatedOne": "Criado", + "MoveIssues": "Mover questões", + "MoveIssuesDescription": "Selecione o projeto para mover as questões", + "ManageAttributes": "Gerir atributos", + "KeepOriginalAttributes": "Manter atributos originais", + "KeepOriginalAttributesTooltip": "Os estados e componentes originais da questão serão mantidos no novo projeto", + "SelectReplacement": "Os seguintes itens não estão disponíveis no novo projeto. Selecione uma substituição.", + "MissingItem": "ITEM EM FALTA", + "Replacement": "SUBSTITUIÇÃO", + "Original": "ORIGINAL", + "OriginalDescription": "Os itens desta secção serão criados no novo projeto", + "Relations": "Relações", + "RemoveRelation": "Remover relação...", + "AddBlockedBy": "Marcar como bloqueado por...", + "AddIsBlocking": "Marcar como bloqueador...", + "AddRelatedIssue": "Referenciar outra questão...", + "RelatedIssue": "Questão relacionada {id} - {title}", + "BlockedIssue": "Questão bloqueada {id} - {title}", + "BlockingIssue": "Questão bloqueadora {id} - {title}", + "BlockedBySearchPlaceholder": "Procurar questão para marcar como bloqueada por...", + "IsBlockingSearchPlaceholder": "Procurar questão para marcar como bloqueadora...", + "RelatedIssueSearchPlaceholder": "Procurar questão para referenciar...", + "Blocks": "Bloqueia", + "Related": "Relacionado", + "RelatedIssues": "Questões relacionadas", + "EditIssue": "Editar {title}", + "EditWorkflowStatuses": "Editar estados de tarefa", + "EditProject": "Editar projeto", + "DeleteProject": "Apagar projeto", + "ArchiveProjectName": "Arquivar projeto {name}?", + "ArchiveProjectConfirm": "Deseja arquivar este projeto?", + "DeleteProjectConfirm": "Deseja apagar este projeto e todas as tarefas?", + "ProjectHasIssues": "Existem tarefas neste projeto, tem a certeza de que deseja arquivar?", + "ManageWorkflowStatuses": "Gerir tipos de projeto", + "AddWorkflowStatus": "Adicionar estado de tarefa", + "EditWorkflowStatus": "Editar estado de tarefa", + "DeleteWorkflowStatus": "Apagar estado de tarefa", + "DeleteWorkflowStatusConfirm": "Deseja apagar o estado \"{status}\"?", + "DeleteWorkflowStatusErrorDescription": "O estado \"{status}\" tem {count, plural, =1 {1 tarefa} other {# tarefas}} atribuída. Por favor, selecione um estado para mover", + "Save": "Guardar", + "IncludeItemsThatMatch": "Incluir itens que coincidam", + "AnyFilter": "qualquer filtro", + "AllFilters": "todos os filtros", + "NoDescription": "Sem descrição", + "SearchIssue": "Procurar tarefa...", + "StatusHistory": "Histórico de estado", + "NewSubIssue": "Adicionar sub-tarefa...", + "AddLabel": "Adicionar etiqueta", + "DeleteIssue": "Apagar {issueCount, plural, =1 {tarefa} other {# tarefas}}", + "DeleteIssueConfirm": "Deseja apagar {issueCount, plural, =1 {tarefa} other {tarefas}}{subIssueCount, plural, =0 {?} =1 { e sub-tarefa?} other { e sub-tarefas?}}", + "Milestone": "Objetivo", + "NoMilestone": "Sem objetivo", + "MoveToMilestone": "Selecionar Objetivo", + "Milestones": "Objetivos", + "AllMilestones": "Todos", + "PlannedMilestones": "Planeados", + "ActiveMilestones": "Ativos", + "ClosedMilestones": "Concluídos", + "AddToMilestone": "Adicionar ao Objetivo", + "MilestoneNamePlaceholder": "Nome do objetivo", + "NewMilestone": "Novo Objetivo", + "CreateMilestone": "Criar", + "MoveAndDeleteMilestone": "Mover tarefas para {newMilestone} e Apagar {deleteMilestone}", + "MoveAndDeleteMilestoneConfirm": "Deseja apagar o objetivo e mover tarefas para outro objetivo?", + "Estimation": "Estimativa", + "ReportedTime": "Tempo gasto", + "RemainingTime": "Tempo restante", + "TimeSpendReports": "Relatórios de tempo gasto", + "TimeSpendReport": "Tempo", + "TimeSpendReportAdd": "Adicionar relatório de tempo", + "TimeSpendReportDate": "Data", + "TimeSpendReportValue": "Tempo gasto", + "TimeSpendReportValueTooltip": "Tempo gasto em horas", + "TimeSpendReportDescription": "Descrição", + "TimeSpendDays": "{value}d", + "TimeSpendHours": "{value}h", + "TimeSpendMinutes": "{value}m", + "ChildEstimation": "Estimativa de sub-tarefas", + "ChildReportedTime": "Tempo de sub-tarefas", + "CapacityValue": "de {value}d", + "NewRelatedIssue": "Nova tarefa relacionada", + "RelatedIssuesNotFound": "Tarefas relacionadas não encontradas", + "AddedReference": "Referência adicionada", + "AddedAsBlocked": "Marcado como bloqueado", + "AddedAsBlocking": "Marcado como bloqueador", + "IssueTemplate": "Modelo", + "IssueTemplates": "Modelos", + "NewProcess": "Novo Modelo", + "SaveProcess": "Guardar Modelo", + "NoIssueTemplate": "Sem Modelo", + "TemplateReplace": "Deseja aplicar o novo modelo?", + "TemplateReplaceConfirm": "Todos os campos serão sobrescritos pelos valores do novo modelo", + "Apply": "Aplicar", + "CurrentWorkDay": "Dia de trabalho atual", + "PreviousWorkDay": "Dia de trabalho anterior", + "TimeReportDayTypeLabel": "Selecione o tipo de dia do relatório de tempo", + "DefaultAssignee": "Responsável padrão para problemas", + "SevenHoursLength": "Sete Horas", + "EightHoursLength": "Oito Horas", + "HourLabel": "h", + "MinuteLabel": "m", + "Saved": "Guardado...", + "CreatedIssue": "Problema criado", + "CreatedSubIssue": "Sub-problema criado", + "ChangeStatus": "Alterar estado", + "ConfigLabel": "Rastreador", + "ConfigDescription": "Extensão para gerir itens de trabalho e fazer todas as tarefas necessárias.", + "NoStatusFound": "Nenhum estado correspondente encontrado", + "CreateMissingStatus": "Criar estado em falta", + "UnsetParent": "O problema pai será desassociado", + "AllProjects": "Todos os projetos", + "IssueNotificationTitle": "{issueTitle}", + "IssueNotificationBody": "Atualizado por {senderName}", + "IssueNotificationChanged": "{senderName} alterou {property}", + "IssueNotificationChangedProperty": "{senderName} alterou {property} para \"{newValue}\"", + "IssueNotificationMessage": "{senderName}: {message}", + "PreviousAssigned": "Anteriormente atribuído", + "IssueAssignedToYou": "Atribuído a si", + "RelatedIssueTargetDescription": "Projeto alvo de problemas relacionados para Classe ou Espaço", + "MapRelatedIssues": "Configurar projetos padrão de problemas relacionados", + "DefaultIssueStatus": "Estado padrão do problema", + "IssueStatus": "Estado", + "Extensions": "Extensions", + "UnsetParentIssue": "Desmarcar problema pai", + "ForbidCreateProjectPermission": "Proibir criação de projeto", + "ForbidCreateProjectPermissionDescription": "Proíbe os usuários de criar novos projetos", + "AllowCreatingIssues": "Permitir criar problemas", + "Day": "Dia", + "Gantt": "Gantt", + "Month": "Mês", + "Quarter": "Trimestre", + "Week": "Semana", + "GanttShowIssueCode": "Show issue code", + "GanttShowTitle": "Show title" + }, + "status": {} +} \ No newline at end of file diff --git a/plugins/tracker-assets/lang/ru.json b/plugins/tracker-assets/lang/ru.json index 142a3eec2b3..ccaa4f22c99 100644 --- a/plugins/tracker-assets/lang/ru.json +++ b/plugins/tracker-assets/lang/ru.json @@ -1,304 +1,289 @@ { - "string": { - "TrackerApplication": "Трекер", - "Projects": "Проекты", - "More": "Больше", - "Default": "По умолчанию", - "MakeDefault": "Установить по умолчанию", - "Delete": "Удалить", - "Open": "Открыть", - "Members": "Участники", - "Inbox": "Входящие", - "ViewIssue": "Открыть задачу", - "IssueCreated": "Задача создана", - "MyIssues": "Мои задачи", - "Issues": "Задачи", - "Views": "Отображения", - "Active": "Активные", - "AllIssues": "Все задачи", - "ActiveIssues": "Активные задачи", - "BacklogIssues": "Пул задач", - "Backlog": "Пул задач", - "Board": "Канбан", - "Components": "Компоненты", - "AllComponents": "Все", - "BacklogComponents": "Пул задач", - "ActiveComponents": "Активные", - "ClosedComponents": "Закрытые", - "NewComponent": "Новый компонент", - "CreateComponent": "Создать компонент", - "ComponentNamePlaceholder": "Название компонента", - "ComponentDescriptionPlaceholder": "Описание (необязательно)", - "ComponentLead": "Руководитель", - "ComponentMembers": "Участники", - "StartDate": "Дата начала", - "TargetDate": "Дата завершения", - "Planned": "Запланирован", - "InProgress": "В работе", - "Paused": "Приостановлен", - "Completed": "Завершен", - "Canceled": "Отменено", - "CreateProject": "Новый проект", - "NewProject": "Новый проект", - "ProjectTitle": "Название проекта", - "ProjectTitlePlaceholder": "Новый проект", - "UsedInIssueIDs": "Используется в идентификаторах задач", - "Identifier": "Идентификатор", - "Import": "Импорт", - "ProjectIdentifier": "Идентификатор проекта", - "IdentifierExists": "Идентификатор уже существует проекта", - "ProjectIdentifierPlaceholder": "ПКТ", - "ChooseIcon": "Выбрать иконку", - "AddIssue": "Добавить задачу", - "NewIssue": "Новая задача", - "NewIssuePlaceholder": "Новая", - "ResumeDraft": "Восстановить черновик", - "SaveIssue": "Создать задачу", - "SetPriority": "Установить приоритет\u2026", - "SetStatus": "Установить статус\u2026", - "SelectIssue": "Выбрать задачу", - "Priority": "Приоритет", - "NoPriority": "Нет приоритета", - "Urgent": "Наивысший", - "High": "Высокий", - "Medium": "Средний", - "Low": "Низкий", - "Unassigned": "Не назначен", - "Back": "Назад", - "List": "Список", - "NumberLabels": "{count, plural, =0 {нет меток} one {# метка} few {# метки} other {# меток}}", - - "CategoryBacklog": "Пул", - "CategoryUnstarted": "Не запущенные", - "CategoryStarted": "Запущенный", - "CategoryCompleted": "Завершенные", - "CategoryCanceled": "Отменённые", - - "Title": "Заголовок", - "Name": "Имя", - "Description": "Описание", - "Status": "Статус", - "Number": "Номер", - "Assignee": "Исполнитель", - "AssignTo": "Назначить...", - "AssignedTo": "Назначен на {value}", - "Parent": "Родительская задача", - "SetParent": "Задать родительскую задачу\u2026", - "ChangeParent": "Изменить родительскую задачу\u2026", - "RemoveParent": "Удалить родительскую задачу", - "OpenParent": "Открыть родительскую задачу", - "SubIssues": "Подзадачи", - "SubIssuesList": "Подзадачи ({subIssues})", - "OpenSubIssues": "Открыть подзадачи", - "AddSubIssues": "Добавить подзадачу", - "BlockedBy": "Блокируется", - "RelatedTo": "Связано", - "Comments": "Комментарии", - "Attachments": "Вложения", - "Labels": "Метки", - "Component": "Компонент", - "Space": "", - "SetDueDate": "Указать срок выполнения\u2026", - "ChangeDueDate": "Изменить срок выполнения\u2026", - "ModificationDate": "Изменено {value}", - "Project": "Проект", - "Issue": "Задача", - "SubIssue": "Подзадача", - "Document": "", - "DocumentIcon": "", - "DocumentColor": "", - "Rank": "Ранг", - "TypeIssuePriority": "Приоритет задачи", - "IssueTitlePlaceholder": "Имя задачи", - "SubIssueTitlePlaceholder": "Имя подзадачи", - "IssueDescriptionPlaceholder": "Добавить описание\u2026", - "SubIssueDescriptionPlaceholder": "Описание подзадачи", - "AddIssueTooltip": "Добавить задачу\u2026", - "NewIssueDialogClose": "Вы действительно хотите закрыть окно?", - "NewIssueDialogCloseNote": "Все внесенные изменения будут потеряны", - "RemoveComponentDialogClose": "Удалить компонент?", - "RemoveComponentDialogCloseNote": "Уверены, что хотите удалить этот компонент? Эта операция не может быть отменена.", - "Grouping": "Группировка", - "Ordering": "Сортировка", - "CompletedIssues": "Завершённые задачи", - "NoGrouping": "Нет группировки", - "NoAssignee": "Нет исполнителя", - "LastUpdated": "Последнее обновление", - "DueDate": "Срок", - "IssueStartDate": "Дата начала", - "GanttDependency": "Dependency", - "GanttLag": "Lag", - "Manual": "Пользовательский", - "All": "Все", - "PastWeek": "Предыдущая неделя", - "PastMonth": "Предыдущий месяц", - "CopyIssueUrl": "Копировать URL-адрес задачи в буфер обмена", - "CopyIssueId": "Копировать ID задачи в буфер обмена", - "CopyIssueBranch": "Копировать имя ветки Git в буфер обмена", - "CopyIssueTitle": "Копировать имя задачи в буфер обмена", - "AssetLabel": "Asset", - "AddToComponent": "Добавить в компонент\u2026", - "MoveToComponent": "Переместить в компонент\u2026", - "NoComponent": "Без компонента", - "ComponentLeadTitle": "Руководитель компонента", - "ComponentMembersTitle": "Участники компонента", - "ComponentLeadSearchPlaceholder": "Назначьте руководителя компонента\u2026", - "ComponentMembersSearchPlaceholder": "Изменить участников компонента\u2026", - "MoveToProject": "Изменить проект", - "Duplicate": "Дублировать", - - "GotoIssues": "Перейти к задачам", - "GotoActive": "Перейти к активным задачам", - "GotoBacklog": "Перейти к пулу задач", - "GotoComponents": "Перейти к компоненту", - "GotoMyIssues": "Перейти к моим задачам", - "GotoTrackerApplication": "Перейти к приложению Трекер", - - "CreatedOne": "Создана", - "MoveIssues": "Переместить задачи", - "MoveIssuesDescription": "Выберите проект, в который вы хотите переместить задачи", - "ManageAttributes": "Управление атрибутами", - "KeepOriginalAttributes": "Оставить оригинальные аттрибуты", - "KeepOriginalAttributesTooltip": "Оригинальные статусы и компоненты будут сохранены в новом проекте", - "SelectReplacement": "Следующие элементы не доступны в новом проекте. Выберите замену.", - "MissingItem": "ОТСУТСТВУЮЩИЙ ЭЛЕМЕНТ", - "Replacement": "ЗАМЕНА", - "Original": "ОРИГИНАЛ", - "OriginalDescription": "Элементы из этой секции будут созданы в новом проекте", - - "Relations": "Зависимости", - "RemoveRelation": "Удалить зависимость...", - "AddBlockedBy": "Отметить как блокируемую...", - "AddIsBlocking": "Отметить как блокирующую...", - "AddRelatedIssue": "Связать с задачей...", - "RelatedIssue": "Связанная задача {id} - {title}", - "BlockedIssue": "Блокируемая задача {id} - {title}", - "BlockingIssue": "Блокирующая {id} - {title}", - "BlockedBySearchPlaceholder": "Поиск блокирующей задачи...", - "IsBlockingSearchPlaceholder": "Поиск блокируемой задачи...", - "RelatedIssueSearchPlaceholder": "Поиск связанной задачи...", - "Blocks": "Блокирует", - "Related": "Связан", - "RelatedIssues": "Связанные задачи", - - "EditIssue": "Редактирование {title}", - "EditWorkflowStatuses": "Редактировать статусы задач", - "EditProject": "Редактировать проект", - "DeleteProject": "Удалить проект", - "ArchiveProjectName": "Архивировать проект {name}?", - "ArchiveProjectConfirm": "Вы действительно хотите заархивировать этот проект?", - "DeleteProjectConfirm": "Вы действительно хотите удалить этот проект со всем содержимым?", - "ProjectHasIssues": "Для данного проекта существуют задачи, уверены, что хотите заархивировать?", - "ManageWorkflowStatuses": "Управлять типами проекта", - "AddWorkflowStatus": "Добавить статус задачи", - "EditWorkflowStatus": "Редактировать статус задачи", - "DeleteWorkflowStatus": "Удалить статус задачи", - "DeleteWorkflowStatusConfirm": "Вы действительно хотите удалить \"{status}\" статус?", - "DeleteWorkflowStatusErrorDescription": "Статус \"{status}\" {count, plural, one {имеет # задача} few {имеет $ задачи} other {имеют # задач}}. Пожалуйста, выберите статус для перемещения.", - - "Save": "Сохранить", - "IncludeItemsThatMatch": "Включить элементы, которые соответствуют", - "AnyFilter": "любому фильтру", - "AllFilters": "всем фильтрам", - "NoDescription": "Нет описания", - "SearchIssue": "Поиск задачи...", - - "StatusHistory": "История состояний", - "NewSubIssue": "Добавить подзадачу...", - "AddLabel": "Добавить метку", - - "DeleteIssue": "Удалить {issueCount, plural, =1 {задачу} one {# задачу} few {# задачи} other {задач}}", - "DeleteIssueConfirm": "Вы действительно хотите удалить {issueCount, plural, =1 {задачу} one {# задачу} few {# задачи} other {задач}}{subIssueCount, plural, =0 {?} =1 { и подзадачу?} one { и # подзадачу?} other { и # подзадач?}}", - - "Milestone": "Этап", - "NoMilestone": "Без Этапа", - "MoveToMilestone": "Выбрать Этап", - "Milestones": "Этапы", - "AllMilestones": "Все", - "PlannedMilestones": "Запланировано", - "ActiveMilestones": "Активно", - "ClosedMilestones": "Завершено", - "AddToMilestone": "Добавить в Этап", - "MilestoneNamePlaceholder": "Название Этапа", - - "NewMilestone": "Новый Этап", - "CreateMilestone": "Создать", - - "MoveAndDeleteMilestone": "Переместить Задачи в {newMilestone} и Удалить {deleteMilestone}", - "MoveAndDeleteMilestoneConfirm": "Вы действительно хотите удалить этап и перенести задачи в другой?", - - "Estimation": "Оценка", - "ReportedTime": "Потраченное времени", - "RemainingTime": "Осталось времени", - "TimeSpendReports": "Отчеты по времени", - "TimeSpendReport": "Время", - "TimeSpendReportAdd": "Добавить затраченное время", - "TimeSpendReportDate": "Дата", - "TimeSpendReportValue": "Затраченное время", - "TimeSpendReportValueTooltip": "Затраченное время в человеко днях", - "TimeSpendReportDescription": "Описание", - "TimeSpendDays": "{value} д", - "TimeSpendHours": "{value} ч", - "TimeSpendMinutes": "{value} м", - "ChildEstimation": "Оценка подзадач", - "ChildReportedTime": "Время подзадач", - "CapacityValue": "из {value} д", - "NewRelatedIssue": "Завести связанную задачу", - "RelatedIssuesNotFound": "Связанные задачи не найдены", - - "AddedReference": "Добавлена зависимость", - "AddedAsBlocked": "Отмечено как заблокировано", - "AddedAsBlocking": "Отмечено как блокирующее", - - "IssueTemplate": "Шаблон", - "IssueTemplates": "Шаблоны", - "NewProcess": "Новый шаблон", - "SaveProcess": "Сохранить шаблон", - "NoIssueTemplate": "Шаблон не задан", - "TemplateReplace": "Вы хотите применить выбранный шаблон?", - "TemplateReplaceConfirm": "Все внесенные изменения в будут заменены значениями из нового шаблона", - "Apply": "Применить", - - "CurrentWorkDay": "Текущий Рабочий День", - "PreviousWorkDay": "Предыдущий Рабочий День", - "TimeReportDayTypeLabel": "Выберите тип дня для временного отчета", - "DefaultAssignee": "Исполнитель задачи по умолчанию", - - "SevenHoursLength": "Семь Часов", - "EightHoursLength": "Восемь Часов", - "HourLabel": "ч", - "MinuteLabel": "м", - "Saved": "Сохранено...", - "CreatedIssue": "Создал(а) задачу", - "CreatedSubIssue": "Создал(а) подзадачу", - "ChangeStatus": "Изменение статуса", - "ConfigLabel": "Трекер", - "ConfigDescription": "Расширение по управлению задачами, чтобы все было в срок.", - "NoStatusFound": "Статус не найдет", - "CreateMissingStatus": "Создать отсутствующий статус", - "UnsetParent": "Родительская задача будет убрана", - "AllProjects": "Все проекты", - "IssueNotificationTitle": "{issueTitle}", - "IssueNotificationBody": "Обновлено {senderName}", - "IssueNotificationChanged": "{senderName} изменил {property}", - "IssueNotificationChangedProperty": "{senderName} изменил {property} на \"{newValue}\"", - "IssueNotificationMessage": "{senderName}: {message}", - "PreviousAssigned": "Ранее назначенные", - "IssueAssignedToYou": "Назначено вам", - "RelatedIssueTargetDescription": "Настройка проекта по умолчанию для Класса или пространства", - "MapRelatedIssues": "Настроить проекты по умолчанию для связанных задач", - "DefaultIssueStatus": "Статус по умолчанию", - "IssueStatus": "Статус", - "Extensions": "Дополнительно", - "UnsetParentIssue": "Снять родительскую задачу", - "ForbidCreateProjectPermission": "Запретить создание проекта", - "ForbidCreateProjectPermissionDescription": "Запрещает пользователям создавать новые проекты", - "AllowCreatingIssues": "Разрешить создание задач", - "Day": "День", - "Gantt": "Gantt", - "Month": "Месяц", - "Quarter": "Квартал", - "Week": "Неделя" - }, - "status": {} -} + "string": { + "TrackerApplication": "Трекер", + "Projects": "Проекты", + "More": "Больше", + "Default": "По умолчанию", + "MakeDefault": "Установить по умолчанию", + "Delete": "Удалить", + "Open": "Открыть", + "Members": "Участники", + "Inbox": "Входящие", + "ViewIssue": "Открыть задачу", + "IssueCreated": "Задача создана", + "MyIssues": "Мои задачи", + "Issues": "Задачи", + "Views": "Отображения", + "Active": "Активные", + "AllIssues": "Все задачи", + "ActiveIssues": "Активные задачи", + "BacklogIssues": "Пул задач", + "Backlog": "Пул задач", + "Board": "Канбан", + "Components": "Компоненты", + "AllComponents": "Все", + "BacklogComponents": "Пул задач", + "ActiveComponents": "Активные", + "ClosedComponents": "Закрытые", + "NewComponent": "Новый компонент", + "CreateComponent": "Создать компонент", + "ComponentNamePlaceholder": "Название компонента", + "ComponentDescriptionPlaceholder": "Описание (необязательно)", + "ComponentLead": "Руководитель", + "ComponentMembers": "Участники", + "StartDate": "Дата начала", + "TargetDate": "Дата завершения", + "Planned": "Запланирован", + "InProgress": "В работе", + "Paused": "Приостановлен", + "Completed": "Завершен", + "Canceled": "Отменено", + "CreateProject": "Новый проект", + "NewProject": "Новый проект", + "ProjectTitle": "Название проекта", + "ProjectTitlePlaceholder": "Новый проект", + "UsedInIssueIDs": "Используется в идентификаторах задач", + "Identifier": "Идентификатор", + "Import": "Импорт", + "ProjectIdentifier": "Идентификатор проекта", + "IdentifierExists": "Идентификатор уже существует проекта", + "ProjectIdentifierPlaceholder": "ПКТ", + "ChooseIcon": "Выбрать иконку", + "AddIssue": "Добавить задачу", + "NewIssue": "Новая задача", + "NewIssuePlaceholder": "Новая", + "ResumeDraft": "Восстановить черновик", + "SaveIssue": "Создать задачу", + "SetPriority": "Установить приоритет…", + "SetStatus": "Установить статус…", + "SelectIssue": "Выбрать задачу", + "Priority": "Приоритет", + "NoPriority": "Нет приоритета", + "Urgent": "Наивысший", + "High": "Высокий", + "Medium": "Средний", + "Low": "Низкий", + "Unassigned": "Не назначен", + "Back": "Назад", + "List": "Список", + "NumberLabels": "{count, plural, =0 {нет меток} one {# метка} few {# метки} other {# меток}}", + "CategoryBacklog": "Пул", + "CategoryUnstarted": "Не запущенные", + "CategoryStarted": "Запущенный", + "CategoryCompleted": "Завершенные", + "CategoryCanceled": "Отменённые", + "Title": "Заголовок", + "Name": "Имя", + "Description": "Описание", + "Status": "Статус", + "Number": "Номер", + "Assignee": "Исполнитель", + "AssignTo": "Назначить...", + "AssignedTo": "Назначен на {value}", + "Parent": "Родительская задача", + "SetParent": "Задать родительскую задачу…", + "ChangeParent": "Изменить родительскую задачу…", + "RemoveParent": "Удалить родительскую задачу", + "OpenParent": "Открыть родительскую задачу", + "SubIssues": "Подзадачи", + "SubIssuesList": "Подзадачи ({subIssues})", + "OpenSubIssues": "Открыть подзадачи", + "AddSubIssues": "Добавить подзадачу", + "BlockedBy": "Блокируется", + "RelatedTo": "Связано", + "Comments": "Комментарии", + "Attachments": "Вложения", + "Labels": "Метки", + "Component": "Компонент", + "Space": "", + "SetDueDate": "Указать срок выполнения…", + "ChangeDueDate": "Изменить срок выполнения…", + "ModificationDate": "Изменено {value}", + "Project": "Проект", + "Issue": "Задача", + "SubIssue": "Подзадача", + "Document": "", + "DocumentIcon": "", + "DocumentColor": "", + "Rank": "Ранг", + "TypeIssuePriority": "Приоритет задачи", + "IssueTitlePlaceholder": "Имя задачи", + "SubIssueTitlePlaceholder": "Имя подзадачи", + "IssueDescriptionPlaceholder": "Добавить описание…", + "SubIssueDescriptionPlaceholder": "Описание подзадачи", + "AddIssueTooltip": "Добавить задачу…", + "NewIssueDialogClose": "Вы действительно хотите закрыть окно?", + "NewIssueDialogCloseNote": "Все внесенные изменения будут потеряны", + "RemoveComponentDialogClose": "Удалить компонент?", + "RemoveComponentDialogCloseNote": "Уверены, что хотите удалить этот компонент? Эта операция не может быть отменена.", + "Grouping": "Группировка", + "Ordering": "Сортировка", + "CompletedIssues": "Завершённые задачи", + "NoGrouping": "Нет группировки", + "NoAssignee": "Нет исполнителя", + "LastUpdated": "Последнее обновление", + "DueDate": "Срок", + "IssueStartDate": "Дата начала", + "GanttDependency": "Dependency", + "GanttLag": "Lag", + "Manual": "Пользовательский", + "All": "Все", + "PastWeek": "Предыдущая неделя", + "PastMonth": "Предыдущий месяц", + "CopyIssueUrl": "Копировать URL-адрес задачи в буфер обмена", + "CopyIssueId": "Копировать ID задачи в буфер обмена", + "CopyIssueBranch": "Копировать имя ветки Git в буфер обмена", + "CopyIssueTitle": "Копировать имя задачи в буфер обмена", + "AssetLabel": "Asset", + "AddToComponent": "Добавить в компонент…", + "MoveToComponent": "Переместить в компонент…", + "NoComponent": "Без компонента", + "ComponentLeadTitle": "Руководитель компонента", + "ComponentMembersTitle": "Участники компонента", + "ComponentLeadSearchPlaceholder": "Назначьте руководителя компонента…", + "ComponentMembersSearchPlaceholder": "Изменить участников компонента…", + "MoveToProject": "Изменить проект", + "Duplicate": "Дублировать", + "GotoIssues": "Перейти к задачам", + "GotoActive": "Перейти к активным задачам", + "GotoBacklog": "Перейти к пулу задач", + "GotoComponents": "Перейти к компоненту", + "GotoMyIssues": "Перейти к моим задачам", + "GotoTrackerApplication": "Перейти к приложению Трекер", + "CreatedOne": "Создана", + "MoveIssues": "Переместить задачи", + "MoveIssuesDescription": "Выберите проект, в который вы хотите переместить задачи", + "ManageAttributes": "Управление атрибутами", + "KeepOriginalAttributes": "Оставить оригинальные аттрибуты", + "KeepOriginalAttributesTooltip": "Оригинальные статусы и компоненты будут сохранены в новом проекте", + "SelectReplacement": "Следующие элементы не доступны в новом проекте. Выберите замену.", + "MissingItem": "ОТСУТСТВУЮЩИЙ ЭЛЕМЕНТ", + "Replacement": "ЗАМЕНА", + "Original": "ОРИГИНАЛ", + "OriginalDescription": "Элементы из этой секции будут созданы в новом проекте", + "Relations": "Зависимости", + "RemoveRelation": "Удалить зависимость...", + "AddBlockedBy": "Отметить как блокируемую...", + "AddIsBlocking": "Отметить как блокирующую...", + "AddRelatedIssue": "Связать с задачей...", + "RelatedIssue": "Связанная задача {id} - {title}", + "BlockedIssue": "Блокируемая задача {id} - {title}", + "BlockingIssue": "Блокирующая {id} - {title}", + "BlockedBySearchPlaceholder": "Поиск блокирующей задачи...", + "IsBlockingSearchPlaceholder": "Поиск блокируемой задачи...", + "RelatedIssueSearchPlaceholder": "Поиск связанной задачи...", + "Blocks": "Блокирует", + "Related": "Связан", + "RelatedIssues": "Связанные задачи", + "EditIssue": "Редактирование {title}", + "EditWorkflowStatuses": "Редактировать статусы задач", + "EditProject": "Редактировать проект", + "DeleteProject": "Удалить проект", + "ArchiveProjectName": "Архивировать проект {name}?", + "ArchiveProjectConfirm": "Вы действительно хотите заархивировать этот проект?", + "DeleteProjectConfirm": "Вы действительно хотите удалить этот проект со всем содержимым?", + "ProjectHasIssues": "Для данного проекта существуют задачи, уверены, что хотите заархивировать?", + "ManageWorkflowStatuses": "Управлять типами проекта", + "AddWorkflowStatus": "Добавить статус задачи", + "EditWorkflowStatus": "Редактировать статус задачи", + "DeleteWorkflowStatus": "Удалить статус задачи", + "DeleteWorkflowStatusConfirm": "Вы действительно хотите удалить \"{status}\" статус?", + "DeleteWorkflowStatusErrorDescription": "Статус \"{status}\" {count, plural, one {имеет # задача} few {имеет $ задачи} other {имеют # задач}}. Пожалуйста, выберите статус для перемещения.", + "Save": "Сохранить", + "IncludeItemsThatMatch": "Включить элементы, которые соответствуют", + "AnyFilter": "любому фильтру", + "AllFilters": "всем фильтрам", + "NoDescription": "Нет описания", + "SearchIssue": "Поиск задачи...", + "StatusHistory": "История состояний", + "NewSubIssue": "Добавить подзадачу...", + "AddLabel": "Добавить метку", + "DeleteIssue": "Удалить {issueCount, plural, =1 {задачу} one {# задачу} few {# задачи} other {задач}}", + "DeleteIssueConfirm": "Вы действительно хотите удалить {issueCount, plural, =1 {задачу} one {# задачу} few {# задачи} other {задач}}{subIssueCount, plural, =0 {?} =1 { и подзадачу?} one { и # подзадачу?} other { и # подзадач?}}", + "Milestone": "Этап", + "NoMilestone": "Без Этапа", + "MoveToMilestone": "Выбрать Этап", + "Milestones": "Этапы", + "AllMilestones": "Все", + "PlannedMilestones": "Запланировано", + "ActiveMilestones": "Активно", + "ClosedMilestones": "Завершено", + "AddToMilestone": "Добавить в Этап", + "MilestoneNamePlaceholder": "Название Этапа", + "NewMilestone": "Новый Этап", + "CreateMilestone": "Создать", + "MoveAndDeleteMilestone": "Переместить Задачи в {newMilestone} и Удалить {deleteMilestone}", + "MoveAndDeleteMilestoneConfirm": "Вы действительно хотите удалить этап и перенести задачи в другой?", + "Estimation": "Оценка", + "ReportedTime": "Потраченное времени", + "RemainingTime": "Осталось времени", + "TimeSpendReports": "Отчеты по времени", + "TimeSpendReport": "Время", + "TimeSpendReportAdd": "Добавить затраченное время", + "TimeSpendReportDate": "Дата", + "TimeSpendReportValue": "Затраченное время", + "TimeSpendReportValueTooltip": "Затраченное время в человеко днях", + "TimeSpendReportDescription": "Описание", + "TimeSpendDays": "{value} д", + "TimeSpendHours": "{value} ч", + "TimeSpendMinutes": "{value} м", + "ChildEstimation": "Оценка подзадач", + "ChildReportedTime": "Время подзадач", + "CapacityValue": "из {value} д", + "NewRelatedIssue": "Завести связанную задачу", + "RelatedIssuesNotFound": "Связанные задачи не найдены", + "AddedReference": "Добавлена зависимость", + "AddedAsBlocked": "Отмечено как заблокировано", + "AddedAsBlocking": "Отмечено как блокирующее", + "IssueTemplate": "Шаблон", + "IssueTemplates": "Шаблоны", + "NewProcess": "Новый шаблон", + "SaveProcess": "Сохранить шаблон", + "NoIssueTemplate": "Шаблон не задан", + "TemplateReplace": "Вы хотите применить выбранный шаблон?", + "TemplateReplaceConfirm": "Все внесенные изменения в будут заменены значениями из нового шаблона", + "Apply": "Применить", + "CurrentWorkDay": "Текущий Рабочий День", + "PreviousWorkDay": "Предыдущий Рабочий День", + "TimeReportDayTypeLabel": "Выберите тип дня для временного отчета", + "DefaultAssignee": "Исполнитель задачи по умолчанию", + "SevenHoursLength": "Семь Часов", + "EightHoursLength": "Восемь Часов", + "HourLabel": "ч", + "MinuteLabel": "м", + "Saved": "Сохранено...", + "CreatedIssue": "Создал(а) задачу", + "CreatedSubIssue": "Создал(а) подзадачу", + "ChangeStatus": "Изменение статуса", + "ConfigLabel": "Трекер", + "ConfigDescription": "Расширение по управлению задачами, чтобы все было в срок.", + "NoStatusFound": "Статус не найдет", + "CreateMissingStatus": "Создать отсутствующий статус", + "UnsetParent": "Родительская задача будет убрана", + "AllProjects": "Все проекты", + "IssueNotificationTitle": "{issueTitle}", + "IssueNotificationBody": "Обновлено {senderName}", + "IssueNotificationChanged": "{senderName} изменил {property}", + "IssueNotificationChangedProperty": "{senderName} изменил {property} на \"{newValue}\"", + "IssueNotificationMessage": "{senderName}: {message}", + "PreviousAssigned": "Ранее назначенные", + "IssueAssignedToYou": "Назначено вам", + "RelatedIssueTargetDescription": "Настройка проекта по умолчанию для Класса или пространства", + "MapRelatedIssues": "Настроить проекты по умолчанию для связанных задач", + "DefaultIssueStatus": "Статус по умолчанию", + "IssueStatus": "Статус", + "Extensions": "Дополнительно", + "UnsetParentIssue": "Снять родительскую задачу", + "ForbidCreateProjectPermission": "Запретить создание проекта", + "ForbidCreateProjectPermissionDescription": "Запрещает пользователям создавать новые проекты", + "AllowCreatingIssues": "Разрешить создание задач", + "Day": "День", + "Gantt": "Gantt", + "Month": "Месяц", + "Quarter": "Квартал", + "Week": "Неделя", + "GanttShowIssueCode": "Show issue code", + "GanttShowTitle": "Show title" + }, + "status": {} +} \ No newline at end of file diff --git a/plugins/tracker-assets/lang/zh.json b/plugins/tracker-assets/lang/zh.json index 7bf3bc34d7c..80679f10f1e 100644 --- a/plugins/tracker-assets/lang/zh.json +++ b/plugins/tracker-assets/lang/zh.json @@ -1,304 +1,289 @@ { - "string": { - "TrackerApplication": "追踪器", - "Projects": "您的项目", - "More": "更多", - "Default": "默认", - "MakeDefault": "设为默认", - "Delete": "删除", - "Open": "打开", - "Members": "成员", - "Inbox": "收件箱", - "MyIssues": "我的问题", - "ViewIssue": "查看问题", - "IssueCreated": "问题已创建", - "Issues": "问题", - "Views": "视图", - "Active": "活跃", - "AllIssues": "所有问题", - "ActiveIssues": "活跃问题", - "BacklogIssues": "积压", - "Backlog": "积压", - "Board": "看板", - "Components": "组件", - "AllComponents": "全部", - "BacklogComponents": "积压", - "ActiveComponents": "活跃", - "ClosedComponents": "已关闭", - "NewComponent": "新组件", - "CreateComponent": "创建组件", - "ComponentNamePlaceholder": "组件名称", - "ComponentDescriptionPlaceholder": "描述(可选)", - "ComponentLead": "负责人", - "ComponentMembers": "成员", - "StartDate": "开始日期", - "TargetDate": "目标日期", - "Planned": "已计划", - "InProgress": "进行中", - "Paused": "暂停", - "Completed": "已完成", - "Canceled": "已取消", - "CreateProject": "创建项目", - "NewProject": "新项目", - "ProjectTitle": "项目标题", - "ProjectTitlePlaceholder": "新项目", - "UsedInIssueIDs": "引用问题ID", - "Identifier": "标识符", - "Import": "导入", - "ProjectIdentifier": "项目标识符", - "IdentifierExists": "项目标识符已存在", - "ProjectIdentifierPlaceholder": "PRJCT", - "ChooseIcon": "选择图标", - "AddIssue": "添加问题", - "NewIssue": "新问题", - "NewIssuePlaceholder": "新问题", - "ResumeDraft": "恢复草稿", - "SaveIssue": "创建问题", - "SetPriority": "设置优先级…", - "SetStatus": "设置状态…", - "SelectIssue": "选择问题", - "Priority": "优先级", - "NoPriority": "无优先级", - "Urgent": "紧急", - "High": "高", - "Medium": "中", - "Low": "低", - "Unassigned": "未分配", - "Back": "返回", - "List": "列表", - "NumberLabels": "{count, plural, =0 {无标签} =1 {1 个标签} other {# 个标签}}", - - "CategoryBacklog": "积压", - "CategoryUnstarted": "未开始", - "CategoryStarted": "已开始", - "CategoryCompleted": "已完成", - "CategoryCanceled": "已取消", - - "Title": "标题", - "Name": "名称", - "Description": "描述", - "Status": "状态", - "Number": "编号", - "Assignee": "受理人", - "AssignTo": "分配给…", - "AssignedTo": "分配给 {value}", - "Parent": "父问题", - "SetParent": "设置父问题…", - "ChangeParent": "更改父问题…", - "RemoveParent": "移除父问题", - "OpenParent": "打开父问题", - "SubIssues": "子问题", - "SubIssuesList": "子问题 ({subIssues})", - "OpenSubIssues": "打开子问题", - "AddSubIssues": "添加子问题", - "BlockedBy": "被阻塞", - "RelatedTo": "相关", - "Comments": "评论", - "Attachments": "附件", - "Labels": "标签", - "Component": "组件", - "Space": "", - "SetDueDate": "设置截止日期…", - "ChangeDueDate": "更改截止日期…", - "ModificationDate": "更新于 {value}", - "Project": "项目", - "Issue": "问题", - "SubIssue": "子问题", - "Document": "", - "DocumentIcon": "", - "DocumentColor": "", - "Rank": "排名", - "TypeIssuePriority": "问题优先级", - "IssueTitlePlaceholder": "问题标题", - "SubIssueTitlePlaceholder": "子问题标题", - "IssueDescriptionPlaceholder": "添加描述…", - "SubIssueDescriptionPlaceholder": "添加子问题描述", - "AddIssueTooltip": "添加问题…", - "NewIssueDialogClose": "您想要关闭此对话框吗?", - "NewIssueDialogCloseNote": "所有更改将会丢失", - "RemoveComponentDialogClose": "删除组件?", - "RemoveComponentDialogCloseNote": "您确定要删除此组件吗?此操作无法撤销", - "Grouping": "分组", - "Ordering": "排序", - "CompletedIssues": "已完成问题", - "NoGrouping": "无分组", - "NoAssignee": "无受理人", - "LastUpdated": "最后更新", - "DueDate": "截止日期", - "IssueStartDate": "开始日期", - "GanttDependency": "Dependency", - "GanttLag": "Lag", - "Manual": "手动", - "All": "全部", - "PastWeek": "过去一周", - "PastMonth": "过去一个月", - "CopyIssueUrl": "复制问题 URL 到剪贴板", - "CopyIssueId": "复制问题 ID 到剪贴板", - "CopyIssueBranch": "复制 Git 分支名称到剪贴板", - "CopyIssueTitle": "复制问题标题到剪贴板", - "AssetLabel": "资产", - "AddToComponent": "添加到组件…", - "MoveToComponent": "移动到组件…", - "NoComponent": "无组件", - "ComponentLeadTitle": "组件负责人", - "ComponentMembersTitle": "组件成员", - "ComponentLeadSearchPlaceholder": "设置组件负责人…", - "ComponentMembersSearchPlaceholder": "更改组件成员…", - "MoveToProject": "移动到项目", - "Duplicate": "复制", - - "GotoIssues": "前往问题", - "GotoActive": "前往活跃问题", - "GotoBacklog": "前往积压", - "GotoComponents": "前往组件", - "GotoMyIssues": "前往我的问题", - "GotoTrackerApplication": "切换到追踪器应用", - - "CreatedOne": "已创建", - "MoveIssues": "移动问题", - "MoveIssuesDescription": "选择要移动问题的项目", - "ManageAttributes": "管理属性", - "KeepOriginalAttributes": "保留原始属性", - "KeepOriginalAttributesTooltip": "原始问题状态和组件将在新项目中保留", - "SelectReplacement": "以下项目在新项目中不可用。请选择替换项。", - "MissingItem": "缺失项目", - "Replacement": "替换项", - "Original": "原始", - "OriginalDescription": "此部分的项目将在新项目中创建", - - "Relations": "关系", - "RemoveRelation": "移除关系…", - "AddBlockedBy": "标记为被阻塞…", - "AddIsBlocking": "标记为阻塞…", - "AddRelatedIssue": "引用其他问题…", - "RelatedIssue": "相关问题 {id} - {title}", - "BlockedIssue": "被阻塞的问题 {id} - {title}", - "BlockingIssue": "阻塞的问题 {id} - {title}", - "BlockedBySearchPlaceholder": "搜索要标记为被阻塞的问题…", - "IsBlockingSearchPlaceholder": "搜索要标记为阻塞的问题…", - "RelatedIssueSearchPlaceholder": "搜索要引用的问题…", - "Blocks": "阻塞", - "Related": "相关", - "RelatedIssues": "相关问题", - - "EditIssue": "编辑 {title}", - "EditWorkflowStatuses": "编辑问题状态", - "EditProject": "编辑项目", - "DeleteProject": "删除项目", - "ArchiveProjectName": "归档项目 {name}?", - "ArchiveProjectConfirm": "您想要归档此项目吗?", - "DeleteProjectConfirm": "您想要删除此项目及所有问题吗?", - "ProjectHasIssues": "此项目中存在问题,您确定要归档吗?", - "ManageWorkflowStatuses": "管理项目类型", - "AddWorkflowStatus": "添加问题状态", - "EditWorkflowStatus": "编辑问题状态", - "DeleteWorkflowStatus": "删除问题状态", - "DeleteWorkflowStatusConfirm": "您想要删除“{status}”状态吗?", - "DeleteWorkflowStatusErrorDescription": "“{status}”状态有 {count, plural, =1 {1 个问题} other {# 个问题}} 分配。请选择一个状态进行移动", - - "Save": "保存", - "IncludeItemsThatMatch": "包括符合条件的项目", - "AnyFilter": "任何过滤器", - "AllFilters": "所有过滤器", - "NoDescription": "无描述", - "SearchIssue": "搜索任务…", - - "StatusHistory": "状态历史", - "NewSubIssue": "添加子问题…", - "AddLabel": "添加标签", - - "DeleteIssue": "删除 {issueCount, plural, =1 {问题} other {# 个问题}}", - "DeleteIssueConfirm": "您想要删除 {issueCount, plural, =1 {问题} other {问题}}{subIssueCount, plural, =0 {?} =1 { 和子问题?} other { 和子问题?}}", - - "Milestone": "里程碑", - "NoMilestone": "无里程碑", - "MoveToMilestone": "选择里程碑", - "Milestones": "里程碑", - "AllMilestones": "全部", - "PlannedMilestones": "已计划", - "ActiveMilestones": "活跃", - "ClosedMilestones": "已完成", - "AddToMilestone": "添加到里程碑", - "MilestoneNamePlaceholder": "里程碑名称", - - "NewMilestone": "新里程碑", - "CreateMilestone": "创建", - - "MoveAndDeleteMilestone": "将问题移动到 {newMilestone} 并删除 {deleteMilestone}", - "MoveAndDeleteMilestoneConfirm": "您想要删除里程碑并将问题移动到其他里程碑吗?", - - "Estimation": "估算", - "ReportedTime": "花费时间", - "RemainingTime": "剩余时间", - "TimeSpendReports": "花费时间报告", - "TimeSpendReport": "时间", - "TimeSpendReportAdd": "添加时间报告", - "TimeSpendReportDate": "日期", - "TimeSpendReportValue": "花费时间", - "TimeSpendReportValueTooltip": "花费时间(小时)", - "TimeSpendReportDescription": "描述", - "TimeSpendDays": "{value} 天", - "TimeSpendHours": "{value} 小时", - "TimeSpendMinutes": "{value} 分钟", - "ChildEstimation": "子问题估算", - "ChildReportedTime": "子问题时间", - "CapacityValue": "共 {value} 天", - "NewRelatedIssue": "新相关问题", - "RelatedIssuesNotFound": "未找到相关问题", - - "AddedReference": "添加了引用", - "AddedAsBlocked": "标记为被阻塞", - "AddedAsBlocking": "标记为阻塞", - - "IssueTemplate": "模板", - "IssueTemplates": "模板", - "NewProcess": "新模板", - "SaveProcess": "保存模板", - "NoIssueTemplate": "无模板", - "TemplateReplace": "您想要应用新模板吗?", - "TemplateReplaceConfirm": "所有字段将被新模板值覆盖", - "Apply": "应用", - - "CurrentWorkDay": "当前工作日", - "PreviousWorkDay": "前一个工作日", - "TimeReportDayTypeLabel": "选择时间报告日类型", - "DefaultAssignee": "默认问题受理人", - - "SevenHoursLength": "七小时", - "EightHoursLength": "八小时", - "HourLabel": "小时", - "MinuteLabel": "分", - "Saved": "已保存...", - "CreatedIssue": "已创建问题", - "CreatedSubIssue": "已创建子问题", - "ChangeStatus": "更改状态", - "ConfigLabel": "追踪器", - "ConfigDescription": "扩展用于管理工作项并完成所有工作。", - "NoStatusFound": "未找到匹配的状态", - "CreateMissingStatus": "创建缺失的状态", - "UnsetParent": "父问题将被取消设置", - "AllProjects": "所有项目", - "IssueNotificationTitle": "{issueTitle}", - "IssueNotificationBody": "由 {senderName} 更新", - "IssueNotificationChanged": "{senderName} 更改了 {property}", - "IssueNotificationChangedProperty": "{senderName} 将 {property} 更改为 \"{newValue}\"", - "IssueNotificationMessage": "{senderName}:{message}", - "PreviousAssigned": "以前分配的", - "IssueAssignedToYou": "分配给您", - "RelatedIssueTargetDescription": "相关问题项目目标用于类别或空间", - "MapRelatedIssues": "配置相关问题默认项目", - "DefaultIssueStatus": "默认问题状态", - "IssueStatus": "状态", - "Extensions": "扩展", - "UnsetParentIssue": "取消父问题", - "ForbidCreateProjectPermission": "禁止创建项目", - "ForbidCreateProjectPermissionDescription": "禁止用户创建新项目", - "AllowCreatingIssues": "允许创建问题", - "Day": "日", - "Gantt": "Gantt", - "Month": "月", - "Quarter": "季度", - "Week": "周" - }, - "status": {} -} + "string": { + "TrackerApplication": "追踪器", + "Projects": "您的项目", + "More": "更多", + "Default": "默认", + "MakeDefault": "设为默认", + "Delete": "删除", + "Open": "打开", + "Members": "成员", + "Inbox": "收件箱", + "MyIssues": "我的问题", + "ViewIssue": "查看问题", + "IssueCreated": "问题已创建", + "Issues": "问题", + "Views": "视图", + "Active": "活跃", + "AllIssues": "所有问题", + "ActiveIssues": "活跃问题", + "BacklogIssues": "积压", + "Backlog": "积压", + "Board": "看板", + "Components": "组件", + "AllComponents": "全部", + "BacklogComponents": "积压", + "ActiveComponents": "活跃", + "ClosedComponents": "已关闭", + "NewComponent": "新组件", + "CreateComponent": "创建组件", + "ComponentNamePlaceholder": "组件名称", + "ComponentDescriptionPlaceholder": "描述(可选)", + "ComponentLead": "负责人", + "ComponentMembers": "成员", + "StartDate": "开始日期", + "TargetDate": "目标日期", + "Planned": "已计划", + "InProgress": "进行中", + "Paused": "暂停", + "Completed": "已完成", + "Canceled": "已取消", + "CreateProject": "创建项目", + "NewProject": "新项目", + "ProjectTitle": "项目标题", + "ProjectTitlePlaceholder": "新项目", + "UsedInIssueIDs": "引用问题ID", + "Identifier": "标识符", + "Import": "导入", + "ProjectIdentifier": "项目标识符", + "IdentifierExists": "项目标识符已存在", + "ProjectIdentifierPlaceholder": "PRJCT", + "ChooseIcon": "选择图标", + "AddIssue": "添加问题", + "NewIssue": "新问题", + "NewIssuePlaceholder": "新问题", + "ResumeDraft": "恢复草稿", + "SaveIssue": "创建问题", + "SetPriority": "设置优先级…", + "SetStatus": "设置状态…", + "SelectIssue": "选择问题", + "Priority": "优先级", + "NoPriority": "无优先级", + "Urgent": "紧急", + "High": "高", + "Medium": "中", + "Low": "低", + "Unassigned": "未分配", + "Back": "返回", + "List": "列表", + "NumberLabels": "{count, plural, =0 {无标签} =1 {1 个标签} other {# 个标签}}", + "CategoryBacklog": "积压", + "CategoryUnstarted": "未开始", + "CategoryStarted": "已开始", + "CategoryCompleted": "已完成", + "CategoryCanceled": "已取消", + "Title": "标题", + "Name": "名称", + "Description": "描述", + "Status": "状态", + "Number": "编号", + "Assignee": "受理人", + "AssignTo": "分配给…", + "AssignedTo": "分配给 {value}", + "Parent": "父问题", + "SetParent": "设置父问题…", + "ChangeParent": "更改父问题…", + "RemoveParent": "移除父问题", + "OpenParent": "打开父问题", + "SubIssues": "子问题", + "SubIssuesList": "子问题 ({subIssues})", + "OpenSubIssues": "打开子问题", + "AddSubIssues": "添加子问题", + "BlockedBy": "被阻塞", + "RelatedTo": "相关", + "Comments": "评论", + "Attachments": "附件", + "Labels": "标签", + "Component": "组件", + "Space": "", + "SetDueDate": "设置截止日期…", + "ChangeDueDate": "更改截止日期…", + "ModificationDate": "更新于 {value}", + "Project": "项目", + "Issue": "问题", + "SubIssue": "子问题", + "Document": "", + "DocumentIcon": "", + "DocumentColor": "", + "Rank": "排名", + "TypeIssuePriority": "问题优先级", + "IssueTitlePlaceholder": "问题标题", + "SubIssueTitlePlaceholder": "子问题标题", + "IssueDescriptionPlaceholder": "添加描述…", + "SubIssueDescriptionPlaceholder": "添加子问题描述", + "AddIssueTooltip": "添加问题…", + "NewIssueDialogClose": "您想要关闭此对话框吗?", + "NewIssueDialogCloseNote": "所有更改将会丢失", + "RemoveComponentDialogClose": "删除组件?", + "RemoveComponentDialogCloseNote": "您确定要删除此组件吗?此操作无法撤销", + "Grouping": "分组", + "Ordering": "排序", + "CompletedIssues": "已完成问题", + "NoGrouping": "无分组", + "NoAssignee": "无受理人", + "LastUpdated": "最后更新", + "DueDate": "截止日期", + "IssueStartDate": "开始日期", + "GanttDependency": "Dependency", + "GanttLag": "Lag", + "Manual": "手动", + "All": "全部", + "PastWeek": "过去一周", + "PastMonth": "过去一个月", + "CopyIssueUrl": "复制问题 URL 到剪贴板", + "CopyIssueId": "复制问题 ID 到剪贴板", + "CopyIssueBranch": "复制 Git 分支名称到剪贴板", + "CopyIssueTitle": "复制问题标题到剪贴板", + "AssetLabel": "资产", + "AddToComponent": "添加到组件…", + "MoveToComponent": "移动到组件…", + "NoComponent": "无组件", + "ComponentLeadTitle": "组件负责人", + "ComponentMembersTitle": "组件成员", + "ComponentLeadSearchPlaceholder": "设置组件负责人…", + "ComponentMembersSearchPlaceholder": "更改组件成员…", + "MoveToProject": "移动到项目", + "Duplicate": "复制", + "GotoIssues": "前往问题", + "GotoActive": "前往活跃问题", + "GotoBacklog": "前往积压", + "GotoComponents": "前往组件", + "GotoMyIssues": "前往我的问题", + "GotoTrackerApplication": "切换到追踪器应用", + "CreatedOne": "已创建", + "MoveIssues": "移动问题", + "MoveIssuesDescription": "选择要移动问题的项目", + "ManageAttributes": "管理属性", + "KeepOriginalAttributes": "保留原始属性", + "KeepOriginalAttributesTooltip": "原始问题状态和组件将在新项目中保留", + "SelectReplacement": "以下项目在新项目中不可用。请选择替换项。", + "MissingItem": "缺失项目", + "Replacement": "替换项", + "Original": "原始", + "OriginalDescription": "此部分的项目将在新项目中创建", + "Relations": "关系", + "RemoveRelation": "移除关系…", + "AddBlockedBy": "标记为被阻塞…", + "AddIsBlocking": "标记为阻塞…", + "AddRelatedIssue": "引用其他问题…", + "RelatedIssue": "相关问题 {id} - {title}", + "BlockedIssue": "被阻塞的问题 {id} - {title}", + "BlockingIssue": "阻塞的问题 {id} - {title}", + "BlockedBySearchPlaceholder": "搜索要标记为被阻塞的问题…", + "IsBlockingSearchPlaceholder": "搜索要标记为阻塞的问题…", + "RelatedIssueSearchPlaceholder": "搜索要引用的问题…", + "Blocks": "阻塞", + "Related": "相关", + "RelatedIssues": "相关问题", + "EditIssue": "编辑 {title}", + "EditWorkflowStatuses": "编辑问题状态", + "EditProject": "编辑项目", + "DeleteProject": "删除项目", + "ArchiveProjectName": "归档项目 {name}?", + "ArchiveProjectConfirm": "您想要归档此项目吗?", + "DeleteProjectConfirm": "您想要删除此项目及所有问题吗?", + "ProjectHasIssues": "此项目中存在问题,您确定要归档吗?", + "ManageWorkflowStatuses": "管理项目类型", + "AddWorkflowStatus": "添加问题状态", + "EditWorkflowStatus": "编辑问题状态", + "DeleteWorkflowStatus": "删除问题状态", + "DeleteWorkflowStatusConfirm": "您想要删除“{status}”状态吗?", + "DeleteWorkflowStatusErrorDescription": "“{status}”状态有 {count, plural, =1 {1 个问题} other {# 个问题}} 分配。请选择一个状态进行移动", + "Save": "保存", + "IncludeItemsThatMatch": "包括符合条件的项目", + "AnyFilter": "任何过滤器", + "AllFilters": "所有过滤器", + "NoDescription": "无描述", + "SearchIssue": "搜索任务…", + "StatusHistory": "状态历史", + "NewSubIssue": "添加子问题…", + "AddLabel": "添加标签", + "DeleteIssue": "删除 {issueCount, plural, =1 {问题} other {# 个问题}}", + "DeleteIssueConfirm": "您想要删除 {issueCount, plural, =1 {问题} other {问题}}{subIssueCount, plural, =0 {?} =1 { 和子问题?} other { 和子问题?}}", + "Milestone": "里程碑", + "NoMilestone": "无里程碑", + "MoveToMilestone": "选择里程碑", + "Milestones": "里程碑", + "AllMilestones": "全部", + "PlannedMilestones": "已计划", + "ActiveMilestones": "活跃", + "ClosedMilestones": "已完成", + "AddToMilestone": "添加到里程碑", + "MilestoneNamePlaceholder": "里程碑名称", + "NewMilestone": "新里程碑", + "CreateMilestone": "创建", + "MoveAndDeleteMilestone": "将问题移动到 {newMilestone} 并删除 {deleteMilestone}", + "MoveAndDeleteMilestoneConfirm": "您想要删除里程碑并将问题移动到其他里程碑吗?", + "Estimation": "估算", + "ReportedTime": "花费时间", + "RemainingTime": "剩余时间", + "TimeSpendReports": "花费时间报告", + "TimeSpendReport": "时间", + "TimeSpendReportAdd": "添加时间报告", + "TimeSpendReportDate": "日期", + "TimeSpendReportValue": "花费时间", + "TimeSpendReportValueTooltip": "花费时间(小时)", + "TimeSpendReportDescription": "描述", + "TimeSpendDays": "{value} 天", + "TimeSpendHours": "{value} 小时", + "TimeSpendMinutes": "{value} 分钟", + "ChildEstimation": "子问题估算", + "ChildReportedTime": "子问题时间", + "CapacityValue": "共 {value} 天", + "NewRelatedIssue": "新相关问题", + "RelatedIssuesNotFound": "未找到相关问题", + "AddedReference": "添加了引用", + "AddedAsBlocked": "标记为被阻塞", + "AddedAsBlocking": "标记为阻塞", + "IssueTemplate": "模板", + "IssueTemplates": "模板", + "NewProcess": "新模板", + "SaveProcess": "保存模板", + "NoIssueTemplate": "无模板", + "TemplateReplace": "您想要应用新模板吗?", + "TemplateReplaceConfirm": "所有字段将被新模板值覆盖", + "Apply": "应用", + "CurrentWorkDay": "当前工作日", + "PreviousWorkDay": "前一个工作日", + "TimeReportDayTypeLabel": "选择时间报告日类型", + "DefaultAssignee": "默认问题受理人", + "SevenHoursLength": "七小时", + "EightHoursLength": "八小时", + "HourLabel": "小时", + "MinuteLabel": "分", + "Saved": "已保存...", + "CreatedIssue": "已创建问题", + "CreatedSubIssue": "已创建子问题", + "ChangeStatus": "更改状态", + "ConfigLabel": "追踪器", + "ConfigDescription": "扩展用于管理工作项并完成所有工作。", + "NoStatusFound": "未找到匹配的状态", + "CreateMissingStatus": "创建缺失的状态", + "UnsetParent": "父问题将被取消设置", + "AllProjects": "所有项目", + "IssueNotificationTitle": "{issueTitle}", + "IssueNotificationBody": "由 {senderName} 更新", + "IssueNotificationChanged": "{senderName} 更改了 {property}", + "IssueNotificationChangedProperty": "{senderName} 将 {property} 更改为 \"{newValue}\"", + "IssueNotificationMessage": "{senderName}:{message}", + "PreviousAssigned": "以前分配的", + "IssueAssignedToYou": "分配给您", + "RelatedIssueTargetDescription": "相关问题项目目标用于类别或空间", + "MapRelatedIssues": "配置相关问题默认项目", + "DefaultIssueStatus": "默认问题状态", + "IssueStatus": "状态", + "Extensions": "扩展", + "UnsetParentIssue": "取消父问题", + "ForbidCreateProjectPermission": "禁止创建项目", + "ForbidCreateProjectPermissionDescription": "禁止用户创建新项目", + "AllowCreatingIssues": "允许创建问题", + "Day": "日", + "Gantt": "Gantt", + "Month": "月", + "Quarter": "季度", + "Week": "周", + "GanttShowIssueCode": "Show issue code", + "GanttShowTitle": "Show title" + }, + "status": {} +} \ No newline at end of file diff --git a/plugins/tracker-resources/src/components/gantt/GanttView.svelte b/plugins/tracker-resources/src/components/gantt/GanttView.svelte index f48f2b05343..a8eaa00985b 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttView.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttView.svelte @@ -64,9 +64,12 @@ const ZOOM_LEVELS: readonly ZoomLevel[] = ['day', 'week', 'month', 'quarter'] let userSidebarWidth: number = DEFAULT_SIDEBAR_WIDTH - let showIssueCode: boolean = true - let showTitle: boolean = true - let showSettings: boolean = false + + // Sidebar column visibility is wired to two ToggleViewOptions registered + // in models/tracker/src/viewlets.ts (Customize-View dropdown). Issue-code + // defaults OFF — the code is still surfaced in the hover tooltip. + $: showIssueCode = (viewOptions as Record)?.ganttShowIssueCode === true + $: showTitle = ((viewOptions as Record)?.ganttShowTitle ?? true) !== false function setZoom (z: ZoomLevel): void { zoom = z @@ -215,6 +218,10 @@ } } + function issueCode (i: Issue): string { + return (i as unknown as { identifier?: string }).identifier ?? 'Issue' + } + function onIssueOpen (e: CustomEvent<{ issue: { _id: string, _class: string } }>): void { showPanel( tracker.component.EditIssue, @@ -367,26 +374,7 @@ >{z[0].toUpperCase() + z.slice(1)} {/each}
-
- - {#if showSettings} -
- - -
- {/if} -
+
@@ -470,7 +458,8 @@ {/if}
Target: {new Date(ms.targetDate).toISOString().slice(0, 10)}
{:else if issue !== null} -
Issue
+ {@const code = issueCode(issue)} +
{code}
{issue.title}
{#if issue.startDate !== null}
Start: {new Date(issue.startDate).toISOString().slice(0, 10)}
diff --git a/plugins/tracker-resources/src/plugin.ts b/plugins/tracker-resources/src/plugin.ts index fc1f85177ad..28533dfd662 100644 --- a/plugins/tracker-resources/src/plugin.ts +++ b/plugins/tracker-resources/src/plugin.ts @@ -315,7 +315,9 @@ export default mergeIds(trackerId, tracker, { Gantt: '' as IntlString, Month: '' as IntlString, Quarter: '' as IntlString, - Week: '' as IntlString + Week: '' as IntlString, + GanttShowIssueCode: '' as IntlString, + GanttShowTitle: '' as IntlString }, component: { NopeComponent: '' as AnyComponent, From 8cbe5a0aa746c8bfb6c05be02d61b94bac2a69ee Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:33:15 +0000 Subject: [PATCH 019/388] fix(tracker-resources): clip GanttSidebar inside its host MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: .gantt-sidebar rendered at its natural height (≈ rows × 36px), and .sidebar-host had overflow:visible, so for many issues the sidebar visually overflowed gantt-body which in turn made gantt-body's scrollHeight balloon to the sidebar's natural height. The result was that scrolling over the sidebar appeared to scroll the page area instead of just the canvas. Fix: - .sidebar-host now overflow:hidden + height:100% + flex-column - .gantt-sidebar height:100% + flex:1 1 auto so it fills the host rather than stretching past it Verified: gantt-body scrollHeight == clientHeight, only .canvas-scroller has scrollable content. Wheel-forward over the sidebar moves canvas-scroller and the sidebar transform together. Signed-off-by: Michael Uray --- .../src/components/gantt/GanttSidebar.svelte | 3 ++- .../src/components/gantt/GanttView.svelte | 13 ++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte b/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte index a972e69d92a..82b6b028320 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte @@ -167,13 +167,14 @@ From 44b87974df57bd3cf9ef142cd452ac532656058f Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:33:15 +0000 Subject: [PATCH 024/388] fix(tracker-resources): resize handle no longer fights canvas pan MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sidebar resize handle and the canvas pointer-pan handler both lived on the gantt-scroller, so dragging the handle started a canvas pan in parallel — the result was visible stutter and barely-controllable resize. Two fixes: - Add .resize-cell to onCanvasPanStart's element exclusion list so the pan never starts when the handle is the pointerdown target. - The resize-cell's own pointer handlers now stopPropagation + preventDefault, so even if a sibling listener picked up the bubbled event it would not double-process it. setPointerCapture stays so the drag tracks the pointer outside the cell bounds. Plus the v16 toolbar consolidation: - Move the always-visible blue + button to IssuesView.svelte (the shared wrapper for List, Kanban and Gantt). Single source — no duplication per viewlet. - NewIssueHeader stays as the original HeaderButton-based component; inline category-level + buttons in the issue list are unaffected. Signed-off-by: Michael Uray --- .../src/components/NewIssueHeader.svelte | 153 +++++++----------- .../src/components/gantt/GanttView.svelte | 12 +- .../src/components/issues/IssuesView.svelte | 14 +- 3 files changed, 78 insertions(+), 101 deletions(-) diff --git a/plugins/tracker-resources/src/components/NewIssueHeader.svelte b/plugins/tracker-resources/src/components/NewIssueHeader.svelte index b1e73a6fa2c..3078a2168d2 100644 --- a/plugins/tracker-resources/src/components/NewIssueHeader.svelte +++ b/plugins/tracker-resources/src/components/NewIssueHeader.svelte @@ -14,10 +14,10 @@ --> -{#if loading} - -{:else if projectExists || draftExists} -
- -
-{:else} -
-
-{/if} + $: updateActions(draftExists, projectExists, closed) + - + diff --git a/plugins/tracker-resources/src/components/gantt/GanttView.svelte b/plugins/tracker-resources/src/components/gantt/GanttView.svelte index 44f6b07839f..d629fbf802c 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttView.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttView.svelte @@ -271,9 +271,10 @@ function onCanvasPanStart (e: PointerEvent): void { if (scrollerEl === undefined) return // Only pan with primary mouse button on empty space — let SVG/HTML - // children (bars, buttons, links) handle their own click/dblclick. + // children (bars, buttons, links, resize-handle) handle their own + // click/drag without competing with the canvas pan. const target = e.target as HTMLElement - if (target.closest('.bar-wrap, button, a, .toggle-btn, .jump-btn, .settings-popover')) return + if (target.closest('.bar-wrap, button, a, .toggle-btn, .jump-btn, .settings-popover, .resize-cell')) return panning = true panStartX = e.clientX panStartY = e.clientY @@ -296,6 +297,8 @@ let resizeStartX = 0 let resizeStartWidth = 0 function onResizeStart (e: PointerEvent): void { + e.stopPropagation() + e.preventDefault() resizing = true resizeStartX = e.clientX resizeStartWidth = userSidebarWidth @@ -303,13 +306,16 @@ } function onResizeMove (e: PointerEvent): void { if (!resizing) return + e.stopPropagation() const next = resizeStartWidth + (e.clientX - resizeStartX) userSidebarWidth = Math.max(MIN_SIDEBAR_WIDTH, Math.min(MAX_SIDEBAR_WIDTH, next)) - queueMicrotask(syncViewport) } function onResizeEnd (e: PointerEvent): void { + if (!resizing) return + e.stopPropagation() resizing = false ;(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId) + queueMicrotask(syncViewport) } let resizeObs: ResizeObserver | undefined diff --git a/plugins/tracker-resources/src/components/issues/IssuesView.svelte b/plugins/tracker-resources/src/components/issues/IssuesView.svelte index 72af1e78e9f..b65d4950b91 100644 --- a/plugins/tracker-resources/src/components/issues/IssuesView.svelte +++ b/plugins/tracker-resources/src/components/issues/IssuesView.svelte @@ -3,12 +3,16 @@ import { Asset, IntlString, translateCB } from '@hcengineering/platform' import { ComponentExtensions } from '@hcengineering/presentation' import { Issue, TrackerEvents } from '@hcengineering/tracker' - import { IModeSelector, themeStore } from '@hcengineering/ui' + import { Button, IconAdd, IModeSelector, showPopup, themeStore } from '@hcengineering/ui' import { ViewOptions, Viewlet } from '@hcengineering/view' import { FilterBar, SpaceHeader, ViewletContentView, ViewletSettingButton } from '@hcengineering/view-resources' import tracker from '../../plugin' import CreateIssue from '../CreateIssue.svelte' + function newIssue (): void { + showPopup(CreateIssue, { space, shouldSaveDraft: true }, 'top') + } + export let space: Ref | undefined = undefined export let query: DocumentQuery = {} export let title: IntlString | undefined = undefined @@ -65,6 +69,14 @@ extension={tracker.extensions.IssueListHeader} props={{ size: 'small', kind: 'tertiary', space }} /> + - + + - - - jumpToDate(datePickerValue)} - /> + + +
{#each ZOOM_LEVELS as z (z)} @@ -510,13 +529,28 @@ class="gantt-grid" style="grid-template-columns: {sidebarWidthPx}px 5px 1fr; --sidebar-w: {sidebarWidthPx}px;" > - +
- - {#if showStatus}{/if} - {#if showIssueCode}{/if} - {#if showTitle}{/if} - +
+ + {#if showStatus}{/if} + {#if showIssueCode}{/if} + {#if showTitle}{/if} + +
+
+ + { if (e.key === 'Enter') jumpToToday() }} role="button" tabindex="0"> + {formatRange(dateRange.from)} – {formatRange(dateRange.to)} + + +
@@ -577,7 +611,7 @@ {#if vHasOverflow}
+ style="top: {TOOLBAR_HEIGHT}px; bottom: 11px;">
Date: Fri, 15 May 2026 12:33:15 +0000 Subject: [PATCH 030/388] feat(tracker-resources): inline-add row in Gantt sidebar (PR2.1) Plane-style: dashed-top-border "+ Add issue" row at the bottom of GanttSidebar opens the standard CreateIssue popup via showPopup. - GanttSidebar dispatches `addIssue` from a keyboard- and click- accessible row using the existing tracker.string.AddIssue label. - GanttView wires it to showPopup(CreateIssue, { space, shouldSaveDraft }). - Row is hidden when no space is available (defensive guard in newIssue handler). Verified on dk3 huly_v7 (gantt-v28): row renders, click opens "New issue" dialog with Issue title field focused. Signed-off-by: Michael Uray --- .../src/components/gantt/GanttSidebar.svelte | 44 ++++++++++++++++++- .../src/components/gantt/GanttView.svelte | 9 +++- 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte b/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte index 2be033622dd..d02e4df50c0 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte @@ -3,7 +3,7 @@ --> {#if visible} @@ -83,17 +94,14 @@ fill="var(--theme-content-color)" /> {#if barLabel !== ''} - {barLabel} + {barLabel} {/if} {tooltipText} {:else} {#if barLabel !== ''} - {barLabel} + {barLabel} {/if} {tooltipText} {/if} diff --git a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte index 8bdc2ca29a0..99d14ca04fa 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte @@ -23,7 +23,7 @@ export let summaryRanges: Map export let scrollTop: number = 0 export let viewportHeight: number = 600 - export let viewport: { left: number; right: number } + export let viewport: { left: number, right: number } export let totalWidth: number export let milestoneStripHeight: number = 0 export let hoveredRowId: string | null = null @@ -39,10 +39,7 @@ $: visibleRows = filterVisibleRows(rows, scrollTop, viewportHeight) $: rowsHeight = rows.length > 0 ? rows[rows.length - 1].y + rows[rows.length - 1].height : 0 $: totalHeight = rowsHeight + milestoneStripHeight - $: ticks = timeScale.ticks([ - timeScale.fromX(Math.max(0, viewport.left - 100)), - timeScale.fromX(viewport.right + 100) - ]) + $: ticks = timeScale.ticks([timeScale.fromX(Math.max(0, viewport.left - 100)), timeScale.fromX(viewport.right + 100)]) function rowKey (row: LayoutRow): string { return row.id @@ -108,13 +105,7 @@ on:mouseleave={() => dispatch('hoverRow', { id: null })} > - + {#if row.kind === 'milestone' && row.milestone !== null} {@const range = summaryFor(row)} {#if range !== null} @@ -160,15 +151,8 @@ {#each milestones as ms (ms._id)} {@const x = timeScale.toX(ms.targetDate)} - - + + {ms.label} {/each} diff --git a/plugins/tracker-resources/src/components/gantt/GanttHeader.svelte b/plugins/tracker-resources/src/components/gantt/GanttHeader.svelte index 4022e0f596e..718c97969d0 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttHeader.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttHeader.svelte @@ -5,7 +5,7 @@ import { type TimeScale } from './lib/time-scale' export let timeScale: TimeScale - export let viewport: { left: number; right: number } // px + export let viewport: { left: number, right: number } // px export let totalWidth: number export let height: number = 36 @@ -13,20 +13,14 @@ // canvas width so the sticky-header lives in the same coordinate system // as the canvas-stack (review note: SVG must extend across the whole // scroll content, not just the visible viewport). - $: visibleRange = [ - timeScale.fromX(Math.max(0, viewport.left - 100)), - timeScale.fromX(viewport.right + 100) - ] as [number, number] + $: visibleRange = [timeScale.fromX(Math.max(0, viewport.left - 100)), timeScale.fromX(viewport.right + 100)] as [ + number, + number + ] $: ticks = timeScale.ticks(visibleRange) - + {#each ticks as tick (tick.date)} {@const x = timeScale.toX(tick.date)} - + {tick.label} {/each} diff --git a/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte b/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte index ea7090bc03c..2310ac62b7d 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte @@ -32,9 +32,7 @@ dispatch('openIssue', { issue: { _id: issue._id as string, _class: issue._class as string } }) } - function jumpDirection ( - obj: { startDate: number | null, dueDate: number | null } - ): 'left' | 'right' | null { + function jumpDirection (obj: { startDate: number | null, dueDate: number | null }): 'left' | 'right' | null { if (timeScale === undefined) return null if (viewportRight <= viewportLeft) return null if (obj.startDate == null && obj.dueDate == null) return null @@ -47,9 +45,7 @@ return null } - function rowJumpTarget ( - row: LayoutRow - ): { startDate: number | null, dueDate: number | null } | null { + function rowJumpTarget (row: LayoutRow): { startDate: number | null, dueDate: number | null } | null { if (row.kind === 'issue' && row.issue !== null) { return { startDate: row.issue.startDate, dueDate: row.issue.dueDate } } @@ -129,7 +125,9 @@ on:click={() => { if (row.issue !== null) openIssue(row.issue) }} - on:keydown={(e) => { if (e.key === 'Enter' && row.issue !== null) openIssue(row.issue) }} + on:keydown={(e) => { + if (e.key === 'Enter' && row.issue !== null) openIssue(row.issue) + }} > {row.issue.title} @@ -148,7 +146,9 @@ + @@ -221,8 +223,12 @@ overflow: hidden; text-overflow: ellipsis; } - .cell-title.clickable { cursor: pointer; } - .cell-title.clickable:hover { text-decoration: underline; } + .cell-title.clickable { + cursor: pointer; + } + .cell-title.clickable:hover { + text-decoration: underline; + } .cell-jump { flex: 0 0 28px; display: flex; @@ -243,7 +249,9 @@ align-items: center; justify-content: center; } - .toggle-btn:hover { color: var(--theme-content-color); } + .toggle-btn:hover { + color: var(--theme-content-color); + } .jump-btn { width: 22px; height: 22px; @@ -259,15 +267,19 @@ align-items: center; justify-content: center; } - .jump-btn:hover { filter: brightness(1.1); } - .sidebar-row.summary { font-weight: 600; } + .jump-btn:hover { + filter: brightness(1.1); + } + .sidebar-row.summary { + font-weight: 600; + } .sidebar-row.milestone { background: color-mix(in srgb, var(--theme-state-info-color, #6366f1) 6%, transparent); } .sidebar-row.hovered { background: var(--theme-button-hovered); } - /* When ANY row is hovered, dim non-hovered rows for a + /* When ANY row is hovered, dim non-hovered rows for a spotlight effect — implemented by the parent setting a data attr. */ :global(.sidebar-rows.has-hover) .sidebar-row:not(.hovered) { opacity: 0.55; diff --git a/plugins/tracker-resources/src/components/gantt/GanttTodayMarker.svelte b/plugins/tracker-resources/src/components/gantt/GanttTodayMarker.svelte index 2761682a707..fdef34fcbc6 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttTodayMarker.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttTodayMarker.svelte @@ -9,7 +9,7 @@ // Viewport accepted for API compatibility but no longer used for clipping // (the SVG itself spans the whole canvas so the browser handles overflow). // eslint-disable-next-line @typescript-eslint/no-unused-vars - export let viewport: { left: number; right: number } = { left: 0, right: 0 } + export let viewport: { left: number, right: number } = { left: 0, right: 0 } $: void viewport $: today = Date.now() @@ -29,10 +29,8 @@ /> - - {dateLabel} + + {dateLabel} From e70801a78d1c6dd2a3be98a61d6f9fa58492ede4 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:39:53 +0000 Subject: [PATCH 045/388] feat(tracker-resources): GanttCanvas forwards bar events + mounts resize overlay Signed-off-by: Michael Uray --- .../src/components/gantt/GanttCanvas.svelte | 24 +++- .../gantt/GanttResizeOverlay.svelte | 130 ++++++++++++++++++ .../src/components/gantt/GanttView.svelte | 3 + 3 files changed, 156 insertions(+), 1 deletion(-) create mode 100644 plugins/tracker-resources/src/components/gantt/GanttResizeOverlay.svelte diff --git a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte index 99d14ca04fa..4731a096b56 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte @@ -3,15 +3,19 @@ --> + +{#if state.kind !== 'idle' && state.kind !== 'hover-bar' && geom !== null} + + +{/if} + +{#if gx !== null} + +{/if} + +{#if pd !== null && gx !== null} + + + {fmt(pd)} + +{/if} + +{#if tip !== null && gx !== null} + + + {tip} + +{/if} + + diff --git a/plugins/tracker-resources/src/components/gantt/GanttView.svelte b/plugins/tracker-resources/src/components/gantt/GanttView.svelte index 7f66aedc3da..d88e3594db5 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttView.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttView.svelte @@ -675,8 +675,11 @@ milestoneStripHeight={MILESTONE_STRIP_HEIGHT} {hoveredRowId} {statusCategoryMap} + {editableIssueIds} + {activeDrag} on:openIssue={onIssueOpen} on:hoverRow={onRowHover} + on:barMouseDown />
From 631accc3c5fde7552e0b2a243075663db2e3f48b Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:39:53 +0000 Subject: [PATCH 046/388] feat(tracker-resources): wire drag reducer + atomic commitDrag in GanttView Signed-off-by: Michael Uray --- .../src/components/gantt/GanttView.svelte | 99 ++++++++++++++++++- 1 file changed, 96 insertions(+), 3 deletions(-) diff --git a/plugins/tracker-resources/src/components/gantt/GanttView.svelte b/plugins/tracker-resources/src/components/gantt/GanttView.svelte index d88e3594db5..72c0d45773b 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttView.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttView.svelte @@ -3,9 +3,10 @@ --> -
+ +
{#if loading} {:else} @@ -797,6 +872,7 @@ {statusCategoryMap} {editableIssueIds} {activeDrag} + {focusedIssueId} on:openIssue={onIssueOpen} on:hoverRow={onRowHover} on:barMouseDown={handleBarMouseDown} @@ -903,6 +979,7 @@ width: 100%; overflow: hidden; position: relative; + outline: none; } .gantt-toolbar { flex: 0 0 auto; From 9b8e3da2e942ebfa8e20a4f71463a013713a202e Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:39:53 +0000 Subject: [PATCH 050/388] feat(tracker-resources): dim non-active rows during drag (spotlight focus) Signed-off-by: Michael Uray --- .../src/components/gantt/GanttCanvas.svelte | 16 ++++++++++++++++ .../src/components/gantt/GanttSidebar.svelte | 10 +++++++++- .../src/components/gantt/GanttView.svelte | 1 + 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte index e9a831cba31..571c99acf29 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte @@ -55,6 +55,17 @@ return focusedIssueId !== null && String(issueId) === focusedIssueId } + // PR 3 spotlight dim: while a drag is active, dim every row that is NOT the + // active issue. Keeps the cursor's row at full opacity so the user can see + // where the bar is being placed relative to other rows. + $: dragState = $activeDrag + $: activeIssueIdStr = 'issue' in dragState ? String((dragState as { issue: { _id: unknown } }).issue._id) : null + $: anyDragActive = dragState.kind !== 'idle' && dragState.kind !== 'hover-bar' + + function isDimmed (issueId: unknown): boolean { + return anyDragActive && activeIssueIdStr !== null && String(issueId) !== activeIssueIdStr + } + $: visibleRows = filterVisibleRows(rows, scrollTop, viewportHeight) $: rowsHeight = rows.length > 0 ? rows[rows.length - 1].y + rows[rows.length - 1].height : 0 $: totalHeight = rowsHeight + milestoneStripHeight @@ -110,6 +121,7 @@ class="row-rect" class:hovered={isHover} class:milestone-bg={row.kind === 'milestone'} + class:dimmed={row.issue !== null && isDimmed(row.issue._id)} /> {/each} @@ -209,6 +221,10 @@ :global(svg.gantt-canvas .row-rect.milestone-bg) { fill: color-mix(in srgb, var(--theme-state-info-color, #6366f1) 6%, transparent); } + :global(svg.gantt-canvas .row-rect.dimmed) { + fill: var(--theme-bg-color); + opacity: 0.55; + } :global(svg.gantt-canvas .row-rect.milestone-bg.hovered) { fill: color-mix(in srgb, var(--theme-state-info-color, #6366f1) 14%, transparent); } diff --git a/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte b/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte index e13fc221bfe..5f4aedc10f5 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte @@ -3,9 +3,10 @@ --> + + + +
+ + {issue.title} +
+
+
diff --git a/plugins/tracker-resources/src/components/gantt/GanttHeader.svelte b/plugins/tracker-resources/src/components/gantt/GanttHeader.svelte index 718c97969d0..85eaa463b14 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttHeader.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttHeader.svelte @@ -20,18 +20,58 @@ $: ticks = timeScale.ticks(visibleRange) - + + + + {#each ticks as tick (tick.date)} {@const x = timeScale.toX(tick.date)} + - + {#if tick.secondaryLabel !== undefined} + + {tick.secondaryLabel} + + {/if} + {tick.label} {/each} @@ -52,4 +92,9 @@ .tick-label-major { font-weight: 600; } + .tick-label-secondary { + font-size: 11px; + font-weight: 600; + opacity: 0.7; + } diff --git a/plugins/tracker-resources/src/components/gantt/GanttHierarchySubmenu.svelte b/plugins/tracker-resources/src/components/gantt/GanttHierarchySubmenu.svelte new file mode 100644 index 00000000000..40ee5fd20ba --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/GanttHierarchySubmenu.svelte @@ -0,0 +1,68 @@ + + + + diff --git a/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte b/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte index 7de19bd5035..7b6c5206b67 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte @@ -41,6 +41,12 @@ dispatch('openIssue', { issue: { _id: issue._id as string, _class: issue._class as string } }) } + function onDragGripDown (issue: Issue): (evt: MouseEvent) => void { + return (evt: MouseEvent): void => { + dispatch('rowDragStart', { issue, cursorX: evt.clientX }) + } + } + function onRowContextMenu (evt: MouseEvent, row: LayoutRow): void { if (row.issue === null) return evt.preventDefault() @@ -134,8 +140,8 @@ { if (row.issue !== null) dispatch('rowDragStart', { issue: row.issue, cursorX: e.clientX }) }} + use:tooltip={{ label: tracker.string.GanttDragToSchedule }} + on:mousedown|stopPropagation={row.issue !== null ? onDragGripDown(row.issue) : undefined} >⋮⋮ {/if} {#if showStatus} diff --git a/plugins/tracker-resources/src/components/gantt/GanttView.svelte b/plugins/tracker-resources/src/components/gantt/GanttView.svelte index 4e0bb961523..07cce3c1812 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttView.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttView.svelte @@ -351,7 +351,7 @@ if (state.kind === 'dragging-unscheduled' && !state.hasCanvasTarget) return const client = getClient() const ops = client.apply('gantt-drag') - if (state.kind === 'dragging-body' || state.kind === 'dragging-unscheduled') { + if (state.kind === 'dragging-body') { await ops.update(state.issue, { startDate: state.previewStart, dueDate: state.previewDue }) const delta = state.previewStart - state.originStart if (delta !== 0) { @@ -362,6 +362,13 @@ }) } } + } else if (state.kind === 'dragging-unscheduled') { + // Unscheduled-drag only schedules the parent issue. originStart is the + // synthetic "today" anchor — using its delta to shift existing scheduled + // descendants would move them by a wildly unrelated amount (Codex + // review-4 2026-05-11). Descendants stay put; the user can drag the + // (now-scheduled) parent again to do a coordinated shift. + await ops.update(state.issue, { startDate: state.previewStart, dueDate: state.previewDue }) } else if (state.kind === 'resizing-left') { await ops.update(state.issue, { startDate: state.previewStart }) } else if (state.kind === 'resizing-right') { @@ -408,7 +415,11 @@ 'tracker:action:EditRelatedTargets', 'tracker:action:MoveToProject', 'tracker:action:CopyAsMarkdownTable', - 'tracker:action:UnsetParent' + 'tracker:action:UnsetParent', + // Below are surfaced via the local Hierarchy ▸ submenu instead, so the + // top-level menu has one collapsed entry rather than three separate ones. + 'tracker:action:SetParent', + 'tracker:action:NewSubIssue' ] function openGanttMenu (event: MouseEvent, issue: Issue): void { @@ -655,8 +666,18 @@ scrollerEl.scrollTop = panStartScrollTop - (e.clientY - panStartY) } function onCanvasPanEnd (e: PointerEvent): void { + // Guard: only release the pointer if we actually captured it. A pointerup + // bubbling from a child element that was excluded by the pan-handler + // exclusion list (e.g. resize-handle, drag-grip) shouldn't reach + // releasePointerCapture, but browsers throw `InvalidStateError` if the + // element isn't actually capturing the given pointerId. Codex review-4 + // 2026-05-11. + if (!panning) return panning = false - ;(e.currentTarget as HTMLElement).releasePointerCapture(e.pointerId) + const el = e.currentTarget as HTMLElement + if (typeof el.hasPointerCapture === 'function' && el.hasPointerCapture(e.pointerId)) { + el.releasePointerCapture(e.pointerId) + } } // Drag-resize the sidebar. @@ -994,11 +1015,8 @@
{/if} {#if issue.startDate !== null && issue.dueDate !== null} - {@const days = - Math.round( - (Math.max(issue.dueDate, issue.startDate) - Math.min(issue.dueDate, issue.startDate)) / 86_400_000 - ) + 1} -
Duration: {days} day{days === 1 ? '' : 's'}
+ {@const days = Math.round((Math.max(issue.dueDate, issue.startDate) - Math.min(issue.dueDate, issue.startDate)) / 86_400_000) + 1} +
{/if} {/if}
diff --git a/plugins/tracker-resources/src/components/gantt/lib/__tests__/time-scale.test.ts b/plugins/tracker-resources/src/components/gantt/lib/__tests__/time-scale.test.ts index e58c9b9736d..03d8b689edc 100644 --- a/plugins/tracker-resources/src/components/gantt/lib/__tests__/time-scale.test.ts +++ b/plugins/tracker-resources/src/components/gantt/lib/__tests__/time-scale.test.ts @@ -76,3 +76,38 @@ describe('createTimeScale', () => { } }) }) + +describe('time-scale — secondary label (year/month supra row)', () => { + const DAY = 86_400_000 + + it('day-zoom: secondaryLabel set on 1st of month and on first visible tick', () => { + const scale = createTimeScale('day', Date.UTC(2026, 0, 1)) + // Span 2 months: Jan 28 – Feb 5 + const ticks = scale.ticks([Date.UTC(2026, 0, 28), Date.UTC(2026, 1, 5)]) + const withSecondary = ticks.filter((t) => t.secondaryLabel !== undefined) + // Expect: first visible tick (Jan 28) + Feb 1 + expect(withSecondary).toHaveLength(2) + expect(withSecondary[0].secondaryLabel).toMatch(/jan/i) + expect(new Date(withSecondary[1].date).getUTCDate()).toBe(1) + expect(withSecondary[1].secondaryLabel).toMatch(/feb/i) + }) + + it('week-zoom: secondaryLabel = year on first visible week + first week of new year', () => { + const scale = createTimeScale('week', Date.UTC(2026, 11, 1)) + // Span across the 2026/2027 year boundary + const ticks = scale.ticks([Date.UTC(2026, 11, 1), Date.UTC(2027, 0, 31)]) + const withSecondary = ticks.filter((t) => t.secondaryLabel !== undefined) + expect(withSecondary.length).toBeGreaterThanOrEqual(2) + expect(withSecondary[0].secondaryLabel).toBe('2026') + expect(withSecondary[1].secondaryLabel).toBe('2027') + }) + + it('quarter-zoom: label is "Qn" only, year goes to secondaryLabel', () => { + const scale = createTimeScale('quarter', Date.UTC(2026, 0, 1)) + const ticks = scale.ticks([Date.UTC(2026, 0, 1), Date.UTC(2026, 11, 31)]) + expect(ticks.map((t) => t.label)).toEqual(['Q1', 'Q2', 'Q3', 'Q4']) + expect(ticks[0].secondaryLabel).toBe('2026') + expect(ticks[1].secondaryLabel).toBeUndefined() // Q2 same year + void DAY + }) +}) diff --git a/plugins/tracker-resources/src/components/gantt/lib/menu-actions.ts b/plugins/tracker-resources/src/components/gantt/lib/menu-actions.ts index 9190d092cec..bf0d14a1a9a 100644 --- a/plugins/tracker-resources/src/components/gantt/lib/menu-actions.ts +++ b/plugins/tracker-resources/src/components/gantt/lib/menu-actions.ts @@ -5,12 +5,14 @@ import type { Issue } from '@hcengineering/tracker' import type { Action as UiAction, PopupAlignment } from '@hcengineering/ui' -import { DatePopup, showPopup } from '@hcengineering/ui' +import { DatePopup, NotificationSeverity, addNotification, showPopup } from '@hcengineering/ui' import Calendar from '@hcengineering/ui/src/components/icons/Calendar.svelte' import { getClient } from '@hcengineering/presentation' +import { translate } from '@hcengineering/platform' import type { Timestamp } from '@hcengineering/core' import tracker from '../../../plugin' import { snapToUtcMidnight } from './time-scale' +import GanttHierarchySubmenu from '../GanttHierarchySubmenu.svelte' const DAY_MS = 86_400_000 @@ -49,7 +51,13 @@ export function openSetStartDate (issue: Issue, anchor: PopupAlignment | undefin if (newStart !== null && issue.startDate == null && issue.dueDate == null) { patch.dueDate = newStart + DAY_MS } - void client.updateDoc(issue._class, issue.space, issue._id, patch) + // Surface failures (permission denied, validation, conflict) the same + // way commitDrag does (Codex review-4 2026-05-11) so the user gets + // visible feedback instead of a silent fire-and-forget. + client.updateDoc(issue._class, issue.space, issue._id, patch).catch(async (err) => { + const title = await translate(tracker.string.GanttDragFailed, {}, undefined) + addNotification(title, String(err), undefined as any, undefined, NotificationSeverity.Error) + }) } ) } @@ -75,6 +83,21 @@ export function ganttExtraActions (issue: Issue, anchor: PopupAlignment | undefi action: async () => { openSetStartDate(issue, anchor) } + }, + { + // Hierarchy ▸ submenu: combines SetParent, Add sub-issue, Link existing + // as sub-issue into one Gantt entry. The two existing model actions + // (tracker:action:SetParent, tracker:action:NewSubIssue) are excluded + // from the parent menu via GANTT_MENU_EXCLUDED_ACTIONS in GanttView. + // `action` is required by the ui.Action interface but is a no-op for + // submenu entries — Menu.svelte routes the click to `component` when + // present (showActionPopup at packages/ui/src/components/Menu.svelte:82). + label: tracker.string.Hierarchy, + icon: tracker.icon.Parent, + group: 'associate', + action: async () => { /* submenu — handled by component */ }, + component: GanttHierarchySubmenu, + props: { issue } } ] } diff --git a/plugins/tracker-resources/src/components/gantt/lib/time-scale.ts b/plugins/tracker-resources/src/components/gantt/lib/time-scale.ts index 4ed9458412c..1791f868650 100644 --- a/plugins/tracker-resources/src/components/gantt/lib/time-scale.ts +++ b/plugins/tracker-resources/src/components/gantt/lib/time-scale.ts @@ -46,46 +46,84 @@ export function createTimeScale (zoom: ZoomLevel, origin: number): TimeScale { switch (zoom) { case 'day': { + // Secondary label: month name on the 1st of each month, or on the + // first visible tick (so the user sees a context label even if the + // viewport starts mid-month). + let lastMonth = -1 + let first = true while (cursor <= to) { const d = new Date(cursor) const isMonday = d.getUTCDay() === 1 + const m = d.getUTCMonth() + let secondary: string | undefined + if (first || m !== lastMonth) { + secondary = d.toLocaleString(undefined, { month: 'short', timeZone: 'UTC' }) + lastMonth = m + first = false + } result.push({ date: cursor, label: d.getUTCDate().toString(), - level: isMonday ? 'major' : 'minor' + level: isMonday ? 'major' : 'minor', + secondaryLabel: secondary }) cursor += DAY_MS } break } case 'week': { + // Secondary label: year on the first week of each year (or first + // visible week). Aligns the supra-row with year boundaries. const d = new Date(cursor) const dow = d.getUTCDay() // 0=Sun const offsetToMonday = (1 - dow + 7) % 7 cursor += offsetToMonday * DAY_MS + let lastYear = -1 + let first = true while (cursor <= to) { const c = new Date(cursor) const isFirstWeekOfMonth = c.getUTCDate() <= 7 + const y = c.getUTCFullYear() + let secondary: string | undefined + if (first || y !== lastYear) { + secondary = String(y) + lastYear = y + first = false + } result.push({ date: cursor, label: `W${isoWeekNumber(c)}`, - level: isFirstWeekOfMonth ? 'major' : 'minor' + level: isFirstWeekOfMonth ? 'major' : 'minor', + secondaryLabel: secondary }) cursor += 7 * DAY_MS } break } case 'month': { + // Secondary label: year on January (or first visible month). Was + // entirely missing before — without it, you couldn't tell which year + // a month belonged to in a multi-year span. const start = new Date(cursor) let y = start.getUTCFullYear() let m = start.getUTCMonth() cursor = Date.UTC(y, m, 1) + let lastYear = -1 + let first = true while (cursor <= to) { const c = new Date(cursor) + const yr = c.getUTCFullYear() + let secondary: string | undefined + if (first || yr !== lastYear) { + secondary = String(yr) + lastYear = yr + first = false + } result.push({ date: cursor, label: c.toLocaleString(undefined, { month: 'short', timeZone: 'UTC' }), - level: c.getUTCMonth() === 0 ? 'major' : 'minor' + level: c.getUTCMonth() === 0 ? 'major' : 'minor', + secondaryLabel: secondary }) m += 1 if (m > 11) { @@ -97,17 +135,30 @@ export function createTimeScale (zoom: ZoomLevel, origin: number): TimeScale { break } case 'quarter': { + // Split the year out of the primary label and put it on the supra + // row — previously the label was "Q1 2026" side-by-side, which is + // visually noisy in a dense Quarter view. const start = new Date(cursor) let y = start.getUTCFullYear() let q = Math.floor(start.getUTCMonth() / 3) cursor = Date.UTC(y, q * 3, 1) + let lastYear = -1 + let first = true while (cursor <= to) { const c = new Date(cursor) const qNum = Math.floor(c.getUTCMonth() / 3) + 1 + const yr = c.getUTCFullYear() + let secondary: string | undefined + if (first || yr !== lastYear) { + secondary = String(yr) + lastYear = yr + first = false + } result.push({ date: cursor, - label: `Q${qNum} ${c.getUTCFullYear()}`, - level: qNum === 1 ? 'major' : 'minor' + label: `Q${qNum}`, + level: qNum === 1 ? 'major' : 'minor', + secondaryLabel: secondary }) q += 1 if (q > 3) { diff --git a/plugins/tracker-resources/src/components/gantt/lib/types.ts b/plugins/tracker-resources/src/components/gantt/lib/types.ts index 1a7f8deec24..7164360bf6e 100644 --- a/plugins/tracker-resources/src/components/gantt/lib/types.ts +++ b/plugins/tracker-resources/src/components/gantt/lib/types.ts @@ -11,9 +11,17 @@ export type ZoomLevel = 'day' | 'week' | 'month' | 'quarter' /** A single tick on the time-axis header (vertical gridline + label). */ export interface Tick { - date: number // UTC ms - label: string // pre-formatted, locale-aware + date: number // UTC ms + label: string // pre-formatted, locale-aware (primary row) level: 'major' | 'minor' // major ticks render thicker + with text label + /** + * Optional supra-label rendered on a second header row above `label` when + * the segment changes from the previous tick. Day view sets it to month + * name on the 1st of each month; week view sets it to the year on the + * first week of each year; month view sets it to the year on January; + * quarter view sets it to the year on Q1. + */ + secondaryLabel?: string } /** A row in the flattened layout. May be an issue, milestone, or swimlane header. */ diff --git a/plugins/tracker-resources/src/components/issues/edit/EditIssue.svelte b/plugins/tracker-resources/src/components/issues/edit/EditIssue.svelte index bf7de0a5db4..4bd35c4eced 100644 --- a/plugins/tracker-resources/src/components/issues/edit/EditIssue.svelte +++ b/plugins/tracker-resources/src/components/issues/edit/EditIssue.svelte @@ -37,8 +37,10 @@ Label, createFocusManager, getCurrentResolvedLocation, - navigate + navigate, + showPopup } from '@hcengineering/ui' + import SetParentIssueActionPopup from '../../SetParentIssueActionPopup.svelte' import view from '@hcengineering/view' import { DocNavLink, ParentsNavigator, showMenu, RelationsEditor } from '@hcengineering/view-resources' import ProjectPresenter from '../../projects/ProjectPresenter.svelte' @@ -322,6 +324,19 @@
{/if}
+ {:else if !effectiveReadonly && issue !== undefined} +
+
{/if} + +
+
+ + +
+
+
+ +
+ + diff --git a/plugins/tracker-resources/src/components/gantt/GanttView.svelte b/plugins/tracker-resources/src/components/gantt/GanttView.svelte index 07cce3c1812..27ec7872120 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttView.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttView.svelte @@ -13,6 +13,7 @@ import tracker from '../../plugin' import { canEditIssue } from '../../utils' import GanttCanvas from './GanttCanvas.svelte' + import GanttConfirmCommitPopup from './GanttConfirmCommitPopup.svelte' import GanttHeader from './GanttHeader.svelte' import GanttSidebar from './GanttSidebar.svelte' import { reduce } from './lib/drag-controller' @@ -76,6 +77,14 @@ const activeDrag = writable({ kind: 'idle' }) let editableIssueIds: Set = new Set() + /** + * Click-to-select gate (user feedback 2026-05-11): a bar must be clicked + * once to become "selected" (blue outline) before drag/resize can begin + * on it. Prevents accidentally dragging an issue while panning the + * canvas horizontally. Clicking outside any bar clears the selection. + */ + let selectedIssueId: string | null = null + let canvasViewportLeft = 0 let canvasViewportWidth = 1200 let scrollTop = 0 @@ -96,6 +105,8 @@ $: showIssueCode = (viewOptions as Record)?.ganttShowIssueCode === true $: showTitle = ((viewOptions as Record)?.ganttShowTitle ?? true) !== false $: showStatus = ((viewOptions as Record)?.ganttShowStatus ?? true) !== false + $: confirmMove = ((viewOptions as Record)?.ganttConfirmMove ?? true) !== false + $: confirmResize = ((viewOptions as Record)?.ganttConfirmResize ?? true) !== false function setZoom (z: ZoomLevel): void { zoom = z @@ -302,6 +313,15 @@ // ------------------------------------------------------------------------- function handleBarMouseDown (e: CustomEvent<{ issue: Issue, edge: 'left' | 'right' | 'body', cursorX: number }>): void { + const id = String(e.detail.issue._id) + // Click-to-select gate: if this bar isn't already selected, the first + // mousedown just selects it (no drag starts). User has to mousedown a + // second time on the now-selected bar to actually drag/resize. Codex- + // safe and matches Plane's UX (user feedback 2026-05-11). + if (selectedIssueId !== id) { + selectedIssueId = id + return + } activeDrag.update((s) => reduce(s, { type: 'mousedown-bar', issue: e.detail.issue, @@ -310,6 +330,17 @@ }, timeScale)) } + /** + * Clear selection when the user clicks outside any bar — e.g. on the + * canvas background. Bar mousedowns stopPropagation, so this only fires + * for clicks that didn't land on a bar. + */ + function onBackgroundClick (e: MouseEvent): void { + const target = e.target as HTMLElement | null + if (target?.closest('.bar-wrap') !== null) return + selectedIssueId = null + } + /** * Translate the window-space `MouseEvent.clientX` into the canvas's content * coordinate. Used for `dragging-unscheduled` so the bar lands at the date @@ -335,7 +366,20 @@ const state = $activeDrag activeDrag.set({ kind: 'idle' }) if (state.kind === 'idle' || state.kind === 'hover-bar') return + // Skip the confirmation prompt when the preview didn't actually change + // anything (drag with zero-delta — e.g. mouseup without movement). + const previewDelta = previewChangedFromOrigin(state) + if (!previewDelta) return + // Wrap the commit in the user-configurable confirmation dialog when the + // matching ganttConfirm{Move,Resize} ViewOption is on (default-on). + const needsConfirm = + ((state.kind === 'dragging-body' || state.kind === 'dragging-unscheduled') && confirmMove) || + ((state.kind === 'resizing-left' || state.kind === 'resizing-right') && confirmResize) try { + if (needsConfirm) { + const proceed = await askConfirm(state) + if (!proceed) return + } await commitDrag(state) } catch (err) { const title = await translate(tracker.string.GanttDragFailed, {}, undefined) @@ -343,6 +387,38 @@ } } + /** True when the preview window is different from the origin window. */ + function previewChangedFromOrigin (state: DragState): boolean { + if (state.kind === 'dragging-body' || state.kind === 'dragging-unscheduled') { + return state.previewStart !== state.originStart || state.previewDue !== state.originDue + } + if (state.kind === 'resizing-left') return state.previewStart !== state.originStart + if (state.kind === 'resizing-right') return state.previewDue !== state.originDue + return false + } + + /** + * Open the confirm dialog and resolve true on Apply / false on Cancel. + * The popup dispatches `close` with a boolean payload, which Huly's + * showPopup routes to the 4th-arg resultHandler. + */ + async function askConfirm (state: DragState): Promise { + if (state.kind === 'idle' || state.kind === 'hover-bar') return false + const newStart = (state.kind === 'resizing-right' ? state.originStart : (state as { previewStart: number }).previewStart) + const newDue = (state.kind === 'resizing-left' ? state.originDue : (state as { previewDue: number }).previewDue) + const kind: 'move' | 'resize' = state.kind === 'resizing-left' || state.kind === 'resizing-right' ? 'resize' : 'move' + return await new Promise((resolve) => { + showPopup( + GanttConfirmCommitPopup, + { issue: state.issue, kind, newStart, newDue }, + 'top', + (result: boolean | undefined) => { + resolve(result === true) + } + ) + }) + } + async function commitDrag (state: DragState): Promise { if (state.kind === 'idle' || state.kind === 'hover-bar') return // Guard: an unscheduled-drag that never reached the canvas (e.g. the user @@ -737,8 +813,8 @@ $: loading = loadingIssues || loadingMilestones - -
+ +
{#if loading} {:else} @@ -929,6 +1005,7 @@ {editableIssueIds} {activeDrag} {focusedIssueId} + {selectedIssueId} on:openIssue={onIssueOpen} on:hoverRow={onRowHover} on:barMouseDown={handleBarMouseDown} diff --git a/plugins/tracker-resources/src/components/gantt/lib/__tests__/link-sub-issue-cycle.test.ts b/plugins/tracker-resources/src/components/gantt/lib/__tests__/link-sub-issue-cycle.test.ts new file mode 100644 index 00000000000..0785b9ffa8c --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/lib/__tests__/link-sub-issue-cycle.test.ts @@ -0,0 +1,97 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +/** + * Standalone test for the cycle-safe ignore-set used by + * LinkSubIssueActionPopup. The component itself can't be exercised + * headlessly (it's a Svelte popup with DOM dependencies), but the + * computeIgnoreSet function is pure and worth fencing in jest so a + * future refactor doesn't silently regress the cycle protection. + * + * The function below is a duplicate of the one in + * `LinkSubIssueActionPopup.svelte` — kept in sync deliberately. + * Codex review-5 (2026-05-11) flagged the absence of a cycle test. + */ + +import type { Issue } from '@hcengineering/tracker' +import type { Ref } from '@hcengineering/core' + +function computeIgnoreSet (root: Issue, all: Issue[]): Set> { + const ignored = new Set>([root._id]) + if (Array.isArray(root.parents)) { + for (const p of root.parents as Array<{ parentId: Ref }>) { + if (p?.parentId !== undefined) ignored.add(p.parentId) + } + } + const childrenByParent = new Map, Issue[]>() + for (const i of all) { + const parentId = i.parents?.[0]?.parentId as Ref | undefined + if (parentId === undefined) continue + const bucket = childrenByParent.get(parentId) + if (bucket === undefined) childrenByParent.set(parentId, [i]) + else bucket.push(i) + } + const queue: Issue[] = [...(childrenByParent.get(root._id) ?? [])] + while (queue.length > 0) { + const next = queue.shift() as Issue + if (ignored.has(next._id)) continue + ignored.add(next._id) + const children = childrenByParent.get(next._id) + if (children !== undefined) { + for (const c of children) queue.push(c) + } + } + return ignored +} + +function mk (id: string, parents: string[] = []): Issue { + return { + _id: id as Ref, + parents: parents.map((p) => ({ parentId: p as Ref, parentTitle: '', space: 'sp' })) + } as unknown as Issue +} + +describe('LinkSubIssue — cycle-safe ignore set', () => { + it('excludes self', () => { + const root = mk('r') + const result = computeIgnoreSet(root, [root]) + expect(result.has(root._id)).toBe(true) + }) + + it('excludes direct ancestors via parents[]', () => { + const root = mk('r', ['p', 'gp']) // r is child of p, p is child of gp + const result = computeIgnoreSet(root, [root]) + expect(result.has('p' as Ref)).toBe(true) + expect(result.has('gp' as Ref)).toBe(true) + }) + + it('excludes direct + transitive descendants', () => { + const root = mk('r') + const child = mk('c', ['r']) + const grand = mk('g', ['c', 'r']) // transitive parents — direct = c + const result = computeIgnoreSet(root, [root, child, grand]) + expect(result.has('c' as Ref)).toBe(true) + expect(result.has('g' as Ref)).toBe(true) + }) + + it('does NOT exclude an unrelated sibling of root', () => { + const root = mk('r') + const sibling = mk('s') + const result = computeIgnoreSet(root, [root, sibling]) + expect(result.has('s' as Ref)).toBe(false) + }) + + it('cycle-safe: a self-referential issue does not infinite-loop', () => { + const root = mk('r') + const cyclic = mk('x', ['x']) + expect(() => computeIgnoreSet(root, [root, cyclic])).not.toThrow() + }) + + it('cycle-safe: mutual parent loop (a<->b) terminates', () => { + const a = mk('a', ['b']) + const b = mk('b', ['a']) + expect(() => computeIgnoreSet(a, [a, b])).not.toThrow() + }) +}) diff --git a/plugins/tracker/src/index.ts b/plugins/tracker/src/index.ts index 60b32f03145..73026a04550 100644 --- a/plugins/tracker/src/index.ts +++ b/plugins/tracker/src/index.ts @@ -565,6 +565,13 @@ const pluginState = plugin(trackerId, { SetParentIssueLabel: '' as IntlString, GanttDragToSchedule: '' as IntlString, GanttDurationTooltip: '' as IntlString, + GanttConfirmMove: '' as IntlString, + GanttConfirmResize: '' as IntlString, + GanttConfirmMoveTitle: '' as IntlString, + GanttConfirmResizeTitle: '' as IntlString, + GanttConfirmMoveBody: '' as IntlString, + GanttConfirmResizeBody: '' as IntlString, + GanttConfirmApply: '' as IntlString, GanttDependency: '' as IntlString, GanttLag: '' as IntlString, NewProject: '' as IntlString, From 59097e949c1f3f4b8343200ede674a527cac12b8 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:39:54 +0000 Subject: [PATCH 057/388] fix(tracker-resources): Gantt parent-drag truth source + canvasX offset + a11y roles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit per explicit user preference for both select-first + confirm-popup default-on): 1. Parent-drag now shifts the FULL descendant tree, not just children that happen to be in the view's filtered `issues` array. Previously a Tracker filter that hid some sub-issues caused them to stay at the old date while their parent moved — silent tree drift. commitDrag and shiftFocused both now run `client.findAll(tracker.class.Issue, { space: issue.space })` to query the full space before walking descendantsWithDates. One extra query per commit, no impact on drag- interactive frame rate. 2. computeCanvasX (unscheduled drag) now offsets by sidebarWidthPx + 5 px instead of just sidebarWidthPx. The 5 px is the resize-cell column between sidebar and canvas (.resize-cell). The hscrollbar has always used this offset (style="padding-left: {sidebarWidthPx + 5}px"); unscheduled-drag was the only consumer that missed it, causing drops to land a few pixels off at high zoom. 3. A11y: the 6 interactive s on GanttBar (regular + summary + resize handles, both branches) now carry role="button", tabindex="-1", and aria-label so screen readers announce them as actionable. The a11y-click-events-have-key-events warning is suppressed per-rect with svelte-ignore plus a banner comment explaining that keyboard support lives at the GanttView level (Tab/ArrowLeft/Right). 94 → 88 warnings. Tests: 5 suites / 60 passed. Signed-off-by: Michael Uray --- .../src/components/gantt/GanttBar.svelte | 32 +++++++++++++++++++ .../src/components/gantt/GanttView.svelte | 22 +++++++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/plugins/tracker-resources/src/components/gantt/GanttBar.svelte b/plugins/tracker-resources/src/components/gantt/GanttBar.svelte index 4ceb675caa0..2ab5fd46b0a 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttBar.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttBar.svelte @@ -160,6 +160,15 @@ editable but functionally inert — commitDrag's parent-pulls- children path was unreachable from the UI. --> {#if editable && !isMilestoneSummary} + + {#if w >= 18} + + @@ -223,6 +243,7 @@ {/if} {tooltipText} {:else} + {#if editable && w >= 18} + + diff --git a/plugins/tracker-resources/src/components/gantt/GanttView.svelte b/plugins/tracker-resources/src/components/gantt/GanttView.svelte index 27ec7872120..b0c8d00f345 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttView.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttView.svelte @@ -348,10 +348,17 @@ * the cursor is outside the canvas (e.g., still over the sidebar) so the * reducer keeps its default preview. */ + /** Width of the resize-cell (drag-handle column) between sidebar and canvas. + * The horizontal scrollbar already offsets by `sidebarWidthPx + 5` (see + * .gantt-hscrollbar padding-left), so the canvas content origin is at + * rect.left + sidebarWidthPx + this constant, not just sidebarWidthPx. + * Codex review-6 2026-05-11 caught the off-by-5 in unscheduled drag. */ + const RESIZE_CELL_W = 5 + function computeCanvasX (e: MouseEvent): number | undefined { if (scrollerEl === undefined) return undefined const rect = scrollerEl.getBoundingClientRect() - const sidebarEdge = rect.left + sidebarWidthPx + const sidebarEdge = rect.left + sidebarWidthPx + RESIZE_CELL_W if (e.clientX < sidebarEdge) return undefined return e.clientX - sidebarEdge + canvasViewportLeft } @@ -431,7 +438,12 @@ await ops.update(state.issue, { startDate: state.previewStart, dueDate: state.previewDue }) const delta = state.previewStart - state.originStart if (delta !== 0) { - for (const child of descendantsWithDates(state.issue, issues)) { + // Fetch the full space's issues here rather than reusing the + // view-filtered `issues` array — otherwise children hidden by an + // active Tracker filter wouldn't shift with the parent and the + // tree would drift out of sync. Codex review-6 2026-05-11. + const allInSpace = await client.findAll(tracker.class.Issue, { space: state.issue.space }) + for (const child of descendantsWithDates(state.issue, allInSpace)) { await ops.update(child, { startDate: (child.startDate as number) + delta, dueDate: (child.dueDate as number) + delta @@ -557,7 +569,11 @@ startDate: i.startDate + days * DAY_MS, dueDate: i.dueDate + days * DAY_MS }) - for (const child of descendantsWithDates(i, issues)) { + // Same filter-vs-truth issue as commitDrag: query the full space so + // filter-hidden descendants still shift with the parent (Codex + // review-6 2026-05-11). + const allInSpace = await client.findAll(tracker.class.Issue, { space: i.space }) + for (const child of descendantsWithDates(i, allInSpace)) { await ops.update(child, { startDate: (child.startDate as number) + days * DAY_MS, dueDate: (child.dueDate as number) + days * DAY_MS From ba409850ff3f9ce643f2c7f84e213e6a3dc48e1d Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:39:54 +0000 Subject: [PATCH 058/388] =?UTF-8?q?chore(tracker-resources):=20Gantt=20pol?= =?UTF-8?q?ish=20=E2=80=94=20drop=20stale=20CSS,=20i18n=20ARIA=20labels?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit friction observation about select-first + confirm-popup left as-is per explicit user preference, the parent-drag performance note is a future-large-space concern): 1. .settings-btn / .settings-popover removed from GanttView.svelte. They were leftover from an earlier custom-popover prototype that was replaced by Huly's standard ToggleViewOption pattern in PR2; the CSS was orphaned. Drops 4 svelte-check warnings. 2. Resize handle ARIA labels were hardcoded English ("Resize start" / "Resize end"). Now translated via tracker.string.GanttAriaResizeStart and GanttAriaResizeEnd. Translation resolves async on themeStore language change; falls back to the English default until resolved. 84 warnings (was 88). Signed-off-by: Michael Uray --- plugins/tracker-assets/lang/en.json | 4 +- .../src/components/gantt/GanttBar.svelte | 18 ++++++-- .../src/components/gantt/GanttView.svelte | 42 ++----------------- plugins/tracker/src/index.ts | 2 + 4 files changed, 23 insertions(+), 43 deletions(-) diff --git a/plugins/tracker-assets/lang/en.json b/plugins/tracker-assets/lang/en.json index 7585ceb0263..4657194a3b5 100644 --- a/plugins/tracker-assets/lang/en.json +++ b/plugins/tracker-assets/lang/en.json @@ -329,7 +329,9 @@ "GanttConfirmResizeTitle": "Change issue dates?", "GanttConfirmMoveBody": "Move {title} to {start} – {due}?", "GanttConfirmResizeBody": "Change {title} to {start} – {due}?", - "GanttConfirmApply": "Apply" + "GanttConfirmApply": "Apply", + "GanttAriaResizeStart": "Resize start date", + "GanttAriaResizeEnd": "Resize due date" }, "status": {} } diff --git a/plugins/tracker-resources/src/components/gantt/GanttBar.svelte b/plugins/tracker-resources/src/components/gantt/GanttBar.svelte index 2ab5fd46b0a..5c87af6f5d3 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttBar.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttBar.svelte @@ -4,8 +4,11 @@ {#if visible} @@ -182,7 +169,7 @@ --> - {#if w >= 18} + {#if selected && w >= 18} {#if barLabel !== ''} - {barLabel} + {barLabel} {/if} {tooltipText} {:else} - {#if editable && w >= 18} + {#if editable && selected && w >= 18} {/if} {#if barLabel !== ''} - {barLabel} + {barLabel} {/if} {tooltipText} {/if} @@ -332,9 +327,23 @@ .summary-label { font-weight: 600; } + /* + * Cursor state machine (user feedback 2026-05-11): + * editable, not selected → pointer (this bar is clickable to arm it) + * editable + selected → grab (now draggable / resizable) + * mid-drag → grabbing (via .active-drag) + * Resize handles only render when selected (see template above), so the + * ew-resize cursor only appears once the bar is armed. + */ .bar.editable { + cursor: pointer; + } + .bar.editable.selected { cursor: grab; } + .bar.editable.active-drag { + cursor: grabbing; + } :global(svg.gantt-canvas .resize-handle) { cursor: ew-resize; } @@ -348,28 +357,46 @@ stroke-width: 2px; filter: drop-shadow(0 0 4px color-mix(in srgb, var(--theme-state-info-color, #6366f1) 50%, transparent)); } + /* + * .focused (keyboard Tab cycle) is intentionally low-contrast — a thin + * dashed 1px outline. handleBarMouseDown syncs focusedIssueId to + * selectedIssueId on click, so .focused only appears alone for pure + * keyboard navigation (rare) and never competes visually with .selected. + */ .bar.focused { stroke: var(--theme-state-info-color, #6366f1); - stroke-width: 2px; - stroke-dasharray: 2, 2; + stroke-width: 1px; + stroke-dasharray: 2,2; } /* - * Click-to-select state: solid blue outline so the user can clearly see - * which bar is armed for drag/resize. Distinct from .focused (dashed, - * keyboard-only) and .active-drag (glow, mid-drag). + * Click-to-select state: thick solid blue outline + glow. Made deliberately + * stronger than the previous 2px stroke so the armed state is unmistakable + * on every fill color (backlog grey through active orange). Codex review-7 + * 2026-05-11: user reported the previous treatment was too subtle. */ .bar.selected { stroke: var(--theme-state-info-color, #6366f1); - stroke-width: 2px; - filter: drop-shadow(0 0 2px color-mix(in srgb, var(--theme-state-info-color, #6366f1) 35%, transparent)); + stroke-width: 3px; + paint-order: stroke fill; + filter: drop-shadow(0 0 4px color-mix(in srgb, var(--theme-state-info-color, #6366f1) 60%, transparent)); } /* Parent-issue summary claw: invisible hit-rect with select/drag visual - feedback when the user has armed the claw via click. */ + feedback when the user has armed the claw via click. Same cursor + state machine as .bar. */ :global(svg.gantt-canvas .summary-hit) { + cursor: pointer; + } + :global(svg.gantt-canvas .summary-hit.selected) { cursor: grab; } + :global(svg.gantt-canvas .summary-hit.active-drag) { + cursor: grabbing; + } :global(svg.gantt-canvas .summary-hit.selected), :global(svg.gantt-canvas .summary-hit.active-drag) { - fill: color-mix(in srgb, var(--theme-state-info-color, #6366f1) 12%, transparent); + fill: color-mix(in srgb, var(--theme-state-info-color, #6366f1) 18%, transparent); + stroke: var(--theme-state-info-color, #6366f1); + stroke-width: 1.5px; + stroke-dasharray: 4,2; } diff --git a/plugins/tracker-resources/src/components/gantt/GanttView.svelte b/plugins/tracker-resources/src/components/gantt/GanttView.svelte index f4d355a9aba..9b79c51ffaf 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttView.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttView.svelte @@ -5,17 +5,7 @@ import { type Class, type Doc, type DocumentQuery, type Ref, type Space, SortingOrder } from '@hcengineering/core' import { createQuery, getClient } from '@hcengineering/presentation' import { type Issue, type Milestone } from '@hcengineering/tracker' - import { - Loading, - addNotification, - NotificationSeverity, - Icon, - Label, - showPanel, - showPopup, - tooltip, - getEventPositionElement - } from '@hcengineering/ui' + import { Loading, addNotification, NotificationSeverity } from '@hcengineering/ui' import { translate } from '@hcengineering/platform' import { type Viewlet, type ViewOptions } from '@hcengineering/view' import { onDestroy, onMount } from 'svelte' @@ -31,8 +21,10 @@ import { descendantsWithDates } from './lib/scheduler' import { createTimeScale } from './lib/time-scale' import { type DragState, type LayoutRow, type MilestoneMarker, type SummaryRange, type ZoomLevel } from './lib/types' + import { Icon, Label, showPanel, showPopup, tooltip } from '@hcengineering/ui' import CreateIssue from '../CreateIssue.svelte' import { showMenu, statusStore } from '@hcengineering/view-resources' + import { getEventPositionElement } from '@hcengineering/ui' import { ganttExtraActions } from './lib/menu-actions' import ArrowLeft from '@hcengineering/ui/src/components/icons/ArrowLeft.svelte' import ArrowRight from '@hcengineering/ui/src/components/icons/ArrowRight.svelte' @@ -59,10 +51,7 @@ let hoveredRowId: string | null = null let tooltipState: { visible: boolean, x: number, y: number, row: LayoutRow | null } = { - visible: false, - x: 0, - y: 0, - row: null + visible: false, x: 0, y: 0, row: null } function onRowHover (e: CustomEvent<{ id: string | null, row?: LayoutRow, mouseX?: number, mouseY?: number }>): void { hoveredRowId = e.detail.id @@ -84,7 +73,7 @@ // editableIssueIds gates the resize handles + the Set-start-date menu entry // per issue based on canEditIssue() (utils.ts:280). const activeDrag = writable({ kind: 'idle' }) - let editableIssueIds = new Set() + let editableIssueIds: Set = new Set() /** * Click-to-select gate (user feedback 2026-05-11): a bar must be clicked @@ -128,9 +117,9 @@ const issueQuery = createQuery() const milestoneQuery = createQuery() - $: issueDocQuery = ( - space !== undefined ? { space, ...(query as DocumentQuery) } : { ...(query as DocumentQuery) } - ) as DocumentQuery + $: issueDocQuery = (space !== undefined + ? { space, ...(query as DocumentQuery) } + : { ...(query as DocumentQuery) }) as DocumentQuery $: milestoneDocQuery = (space !== undefined ? { space } : {}) as DocumentQuery $: issueQuery.query( tracker.class.Issue, @@ -156,29 +145,32 @@ } editableIssueIds = next })() - $: milestoneQuery.query(tracker.class.Milestone, milestoneDocQuery, (res: Milestone[]) => { - milestones = res - loadingMilestones = false - }) + $: milestoneQuery.query( + tracker.class.Milestone, + milestoneDocQuery, + (res: Milestone[]) => { + milestones = res + loadingMilestones = false + } + ) $: dateRange = computeDateRange(issues, milestones, zoom) function paddingDays (z: ZoomLevel): number { switch (z) { - case 'day': - return 1 - case 'week': - return 7 - case 'month': - return 30 - case 'quarter': - return 90 - default: - return 7 + case 'day': return 1 + case 'week': return 7 + case 'month': return 30 + case 'quarter': return 90 + default: return 7 } } - function computeDateRange (iss: Issue[], ms: Milestone[], z: ZoomLevel): { from: number, to: number } { + function computeDateRange ( + iss: Issue[], + ms: Milestone[], + z: ZoomLevel + ): { from: number, to: number } { const all: number[] = [] for (const i of iss) { if (i.startDate !== null && i.startDate !== undefined) all.push(i.startDate) @@ -202,14 +194,14 @@ } $: timeScale = createTimeScale(zoom, dateRange.from) - $: milestoneMarkers = milestones.map((m) => ({ + $: milestoneMarkers = milestones.map(m => ({ _id: m._id, label: m.label, startDate: (m as Milestone & { startDate: number | null }).startDate ?? null, targetDate: m.targetDate })) - let collapsedIds = new Set() + let collapsedIds: Set = new Set() function onToggle (e: CustomEvent<{ id: string }>): void { const next = new Set(collapsedIds) if (next.has(e.detail.id)) next.delete(e.detail.id) @@ -228,7 +220,10 @@ return out } - function computeSummaryRanges (layoutRows: LayoutRow[], allIssues: Issue[]): Map { + function computeSummaryRanges ( + layoutRows: LayoutRow[], + allIssues: Issue[] + ): Map { const result = new Map() const childrenOf = new Map() const issuesByMilestone = new Map() @@ -252,8 +247,8 @@ if (row.kind === 'milestone' && row.milestone !== null) { const msId = row.milestone._id as unknown as string const kids = issuesByMilestone.get(msId) ?? [] - const starts = kids.map((k) => k.startDate).filter((v): v is number => v !== null && v !== undefined) - const dues = kids.map((k) => k.dueDate).filter((v): v is number => v !== null && v !== undefined) + const starts = kids.map(k => k.startDate).filter((v): v is number => v !== null && v !== undefined) + const dues = kids.map(k => k.dueDate).filter((v): v is number => v !== null && v !== undefined) result.set(row.id, { startDate: starts.length > 0 ? Math.min(...starts) : null, dueDate: dues.length > 0 ? Math.max(...dues) : null @@ -261,10 +256,10 @@ continue } if (row.issue === null) continue - const id = row.issue._id as unknown as string + const id = (row.issue as Issue)._id as unknown as string const kids = childrenOf.get(id) ?? [] - const starts = kids.map((k) => k.startDate).filter((v): v is number => v !== null && v !== undefined) - const dues = kids.map((k) => k.dueDate).filter((v): v is number => v !== null && v !== undefined) + const starts = kids.map(k => k.startDate).filter((v): v is number => v !== null && v !== undefined) + const dues = kids.map(k => k.dueDate).filter((v): v is number => v !== null && v !== undefined) result.set(id, { startDate: starts.length > 0 ? Math.min(...starts) : null, dueDate: dues.length > 0 ? Math.max(...dues) : null @@ -276,7 +271,10 @@ // totalCanvasWidth is the data-range width only — never inflated to fit // the viewport. If the issue range is short, the canvas fits entirely // on screen and the horizontal scrollbar hides itself. - $: totalCanvasWidth = Math.max(1, Math.ceil(timeScale.toX(dateRange.to) - timeScale.toX(dateRange.from))) + $: totalCanvasWidth = Math.max( + 1, + Math.ceil(timeScale.toX(dateRange.to) - timeScale.toX(dateRange.from)) + ) function handleVScroll (e: Event): void { const t = e.target as HTMLDivElement @@ -317,30 +315,25 @@ // PR 3 edit-mode: bar mousedown → reducer; window mousemove/mouseup; commit. // ------------------------------------------------------------------------- - function handleBarMouseDown ( - e: CustomEvent<{ issue: Issue, edge: 'left' | 'right' | 'body', cursorX: number }> - ): void { + function handleBarMouseDown (e: CustomEvent<{ issue: Issue, edge: 'left' | 'right' | 'body', cursorX: number }>): void { const id = String(e.detail.issue._id) // Click-to-select gate: if this bar isn't already selected, the first // mousedown just selects it (no drag starts). User has to mousedown a // second time on the now-selected bar to actually drag/resize. - // safe and matches Plane's UX (user feedback 2026-05-11). + // Sync focusedIssueId so the keyboard-focus visual never disagrees with + // the click-selection visual — otherwise two bars look "marked" + // simultaneously (user feedback 2026-05-11). if (selectedIssueId !== id) { selectedIssueId = id + focusedIssueId = id return } - activeDrag.update((s) => - reduce( - s, - { - type: 'mousedown-bar', - issue: e.detail.issue, - edge: e.detail.edge, - cursorX: e.detail.cursorX - }, - timeScale - ) - ) + activeDrag.update((s) => reduce(s, { + type: 'mousedown-bar', + issue: e.detail.issue, + edge: e.detail.edge, + cursorX: e.detail.cursorX + }, timeScale)) } /** @@ -352,6 +345,7 @@ const target = e.target as HTMLElement | null if (target?.closest('.bar-wrap') !== null) return selectedIssueId = null + focusedIssueId = null } /** @@ -407,14 +401,6 @@ } } - // Stable void-returning wrapper so add/removeEventListener share one - // reference (a per-call arrow would leak the listener) while satisfying - // the void-listener signature — the mouseup commit is intentionally - // fire-and-forget, with its own internal error handling. - const onWindowMouseUp = (): void => { - void handleCanvasMouseUp() - } - /** True when the preview window is different from the origin window. */ function previewChangedFromOrigin (state: DragState): boolean { if (state.kind === 'dragging-body' || state.kind === 'dragging-unscheduled') { @@ -432,11 +418,9 @@ */ async function askConfirm (state: DragState): Promise { if (state.kind === 'idle' || state.kind === 'hover-bar') return false - const newStart = - state.kind === 'resizing-right' ? state.originStart : (state as { previewStart: number }).previewStart - const newDue = state.kind === 'resizing-left' ? state.originDue : (state as { previewDue: number }).previewDue - const kind: 'move' | 'resize' = - state.kind === 'resizing-left' || state.kind === 'resizing-right' ? 'resize' : 'move' + const newStart = (state.kind === 'resizing-right' ? state.originStart : (state as { previewStart: number }).previewStart) + const newDue = (state.kind === 'resizing-left' ? state.originDue : (state as { previewDue: number }).previewDue) + const kind: 'move' | 'resize' = state.kind === 'resizing-left' || state.kind === 'resizing-right' ? 'resize' : 'move' return await new Promise((resolve) => { showPopup( GanttConfirmCommitPopup, @@ -494,14 +478,14 @@ // Attach/detach window-level mousemove + mouseup only while a drag is active. $: if ($activeDrag.kind !== 'idle' && $activeDrag.kind !== 'hover-bar') { window.addEventListener('mousemove', handleCanvasMouseMove) - window.addEventListener('mouseup', onWindowMouseUp) + window.addEventListener('mouseup', handleCanvasMouseUp) } else { window.removeEventListener('mousemove', handleCanvasMouseMove) - window.removeEventListener('mouseup', onWindowMouseUp) + window.removeEventListener('mouseup', handleCanvasMouseUp) } onDestroy(() => { window.removeEventListener('mousemove', handleCanvasMouseMove) - window.removeEventListener('mouseup', onWindowMouseUp) + window.removeEventListener('mouseup', handleCanvasMouseUp) }) /** @@ -549,17 +533,11 @@ } function handleRowDragStart (e: CustomEvent<{ issue: Issue, cursorX: number }>): void { - activeDrag.update((s) => - reduce( - s, - { - type: 'mousedown-unscheduled', - issue: e.detail.issue, - cursorX: e.detail.cursorX - }, - timeScale - ) - ) + activeDrag.update((s) => reduce(s, { + type: 'mousedown-unscheduled', + issue: e.detail.issue, + cursorX: e.detail.cursorX + }, timeScale)) } function handleRowContextMenu (e: CustomEvent<{ issue: { _id: string, _class: string }, event: MouseEvent }>): void { @@ -589,7 +567,7 @@ async function shiftFocused (days: number): Promise { if (focusedIssueId === null) return const i = scheduledIssues.find((it) => String(it._id) === focusedIssueId) - if (i?.startDate == null || i.dueDate == null) return + if (i === undefined || i.startDate == null || i.dueDate == null) return if (!editableIssueIds.has(focusedIssueId)) return const client = getClient() const ops = client.apply('gantt-keyshift') @@ -677,7 +655,9 @@ // Custom horizontal scrollbar thumb geometry (proxy for hScrollEl). $: hTrackWidth = canvasViewportWidth > 0 ? canvasViewportWidth : 1 - $: hThumbWidth = totalCanvasWidth > 0 ? Math.max(40, (hTrackWidth * hTrackWidth) / totalCanvasWidth) : hTrackWidth + $: hThumbWidth = totalCanvasWidth > 0 + ? Math.max(40, (hTrackWidth * hTrackWidth) / totalCanvasWidth) + : hTrackWidth $: hThumbMax = Math.max(0, hTrackWidth - hThumbWidth) $: hScrollMax = Math.max(1, totalCanvasWidth - hTrackWidth) $: hThumbLeft = canvasViewportLeft <= 0 ? 0 : (canvasViewportLeft / hScrollMax) * hThumbMax @@ -688,7 +668,9 @@ // so we render our own in DOM and let the native bar drive scrollTop). $: vTrackHeight = viewportHeight > 0 ? viewportHeight : 1 $: vTotalHeight = ROW_HEIGHT * rows.length + HEADER_HEIGHT - $: vThumbHeight = vTotalHeight > 0 ? Math.max(40, (vTrackHeight * vTrackHeight) / vTotalHeight) : vTrackHeight + $: vThumbHeight = vTotalHeight > 0 + ? Math.max(40, (vTrackHeight * vTrackHeight) / vTotalHeight) + : vTrackHeight $: vThumbMax = Math.max(0, vTrackHeight - vThumbHeight) $: vScrollMax = Math.max(1, vTotalHeight - vTrackHeight) $: vThumbTop = scrollTop <= 0 ? 0 : (scrollTop / vScrollMax) * vThumbMax @@ -771,13 +753,7 @@ // Sidebar-cell + drag-grip + resize-handle excluded so the sidebar's // unscheduled-drag-grip doesn't compete with canvas pan for the same // pointerdown. review note (2026-05-11). - if ( - target.closest( - '.bar-wrap, .sidebar-cell, .drag-grip, .resize-handle, button, a, .toggle-btn, .jump-btn, .resize-cell' - ) - ) { - return - } + if (target.closest('.bar-wrap, .sidebar-cell, .drag-grip, .resize-handle, button, a, .toggle-btn, .jump-btn, .resize-cell')) return panning = true panStartX = e.clientX panStartY = e.clientY @@ -845,9 +821,7 @@ onMount(() => { syncViewport() if (typeof ResizeObserver !== 'undefined') { - resizeObs = new ResizeObserver(() => { - syncViewport() - }) + resizeObs = new ResizeObserver(() => syncViewport()) if (scrollerEl !== undefined) resizeObs.observe(scrollerEl) if (hScrollEl !== undefined) resizeObs.observe(hScrollEl) } @@ -857,7 +831,7 @@ }) $: viewport = { left: canvasViewportLeft, right: canvasViewportLeft + canvasViewportWidth } - $: sidebarWidthPx = showIssueCode || showTitle || showStatus ? userSidebarWidth : 60 + $: sidebarWidthPx = (showIssueCode || showTitle || showStatus) ? userSidebarWidth : 60 $: loading = loadingIssues || loadingMilestones @@ -869,43 +843,19 @@ {:else}
- - - -
@@ -926,10 +874,8 @@ type="button" class="zoom-btn" class:active={zoom === z} - on:click={() => { - setZoom(z) - }}>{z[0].toUpperCase() + z.slice(1)} + on:click={() => setZoom(z)} + >{z[0].toUpperCase() + z.slice(1)} {/each}
@@ -966,41 +912,20 @@
- - { - if (e.key === 'Enter') jumpToToday() - }} - role="button" - tabindex="0" - > + on:click={() => pageScroll(-1)}>« + { if (e.key === 'Enter') jumpToToday() }} role="button" tabindex="0"> {formatRange(dateRange.from)} – {formatRange(dateRange.to)} - + on:click={() => pageScroll(1)}>»
-
+
@@ -1035,10 +960,7 @@ on:pointercancel={onResizeEnd} />
-
+
{#if vHasOverflow} -
+
{#if hHasOverflow} -
+
+
{#if row.kind === 'milestone' && ms !== null}
{ms.label}
{#if ms.startDate !== null} -
-
+
{/if} -
-
+
{:else if issue !== null} {@const code = issueCode(issue)}
{code}
{issue.title}
{#if issue.startDate !== null} -
-
+
{/if} {#if issue.dueDate !== null} -
-
+
{/if} {#if issue.startDate !== null && issue.dueDate !== null} - {@const days = - Math.round( - (Math.max(issue.dueDate, issue.startDate) - Math.min(issue.dueDate, issue.startDate)) / 86_400_000 - ) + 1} + {@const days = Math.round((Math.max(issue.dueDate, issue.startDate) - Math.min(issue.dueDate, issue.startDate)) / 86_400_000) + 1}
{/if} {/if} @@ -1172,21 +1090,9 @@ border-bottom: 1px solid var(--theme-divider-color); background: var(--theme-comp-header-color); } - .toolbar-left { - display: flex; - gap: 4px; - } - .toolbar-center { - display: flex; - gap: 2px; - justify-self: center; - } - .toolbar-right { - display: flex; - gap: 4px; - justify-self: end; - position: relative; - } + .toolbar-left { display: flex; gap: 4px; } + .toolbar-center { display: flex; gap: 2px; justify-self: center; } + .toolbar-right { display: flex; gap: 4px; justify-self: end; position: relative; } .nav-btn { height: 26px; min-width: 28px; @@ -1242,18 +1148,10 @@ font-size: 12px; cursor: pointer; } - .zoom-btn:first-child { - border-radius: 4px 0 0 4px; - } - .zoom-btn:last-child { - border-radius: 0 4px 4px 0; - } - .zoom-btn:not(:first-child) { - border-left: none; - } - .zoom-btn:hover { - background: var(--theme-button-hovered); - } + .zoom-btn:first-child { border-radius: 4px 0 0 4px; } + .zoom-btn:last-child { border-radius: 0 4px 4px 0; } + .zoom-btn:not(:first-child) { border-left: none; } + .zoom-btn:hover { background: var(--theme-button-hovered); } .zoom-btn.active { background: var(--theme-button-pressed); font-weight: 600; @@ -1282,13 +1180,8 @@ grid-template-rows: auto auto; width: 100%; } - .header-cell, - .canvas-cell { - overflow: hidden; - } - .hscroll-inner { - will-change: transform; - } + .header-cell, .canvas-cell { overflow: hidden; } + .hscroll-inner { will-change: transform; } /* robustness fix: absolutely-pin the bar at the bottom of gantt-root instead of relying on the flex chain to enforce a constrained height. This way the bar can never slip below the @@ -1321,14 +1214,10 @@ height: 100%; overflow-x: scroll; overflow-y: hidden; - scrollbar-width: none; /* Firefox: hide native */ - } - .hscroll-track-custom::-webkit-scrollbar { - display: none; - } /* WebKit: hide */ - .hscroll-spacer { - height: 1px; + scrollbar-width: none; /* Firefox: hide native */ } + .hscroll-track-custom::-webkit-scrollbar { display: none; } /* WebKit: hide */ + .hscroll-spacer { height: 1px; } .hscroll-thumb { position: absolute; top: 1px; @@ -1338,19 +1227,10 @@ border-radius: 4px; cursor: grab; pointer-events: auto; - transition: - opacity 100ms ease, - background 100ms ease; - } - .hscroll-thumb:hover { - opacity: 0.85; - background: var(--theme-state-info-color, #6366f1); - } - .hscroll-thumb:active { - cursor: grabbing; - opacity: 1; - background: var(--theme-state-info-color, #6366f1); + transition: opacity 100ms ease, background 100ms ease; } + .hscroll-thumb:hover { opacity: 0.85; background: var(--theme-state-info-color, #6366f1); } + .hscroll-thumb:active { cursor: grabbing; opacity: 1; background: var(--theme-state-info-color, #6366f1); } /* Vertical scrollbar — same DOM-thumb pattern, anchored at the right edge of gantt-root between the toolbar and the horizontal bar. */ .gantt-vscrollbar { @@ -1371,19 +1251,10 @@ border-radius: 4px; cursor: grab; pointer-events: auto; - transition: - opacity 100ms ease, - background 100ms ease; - } - .vscroll-thumb:hover { - opacity: 0.85; - background: var(--theme-state-info-color, #6366f1); - } - .vscroll-thumb:active { - cursor: grabbing; - opacity: 1; - background: var(--theme-state-info-color, #6366f1); + transition: opacity 100ms ease, background 100ms ease; } + .vscroll-thumb:hover { opacity: 0.85; background: var(--theme-state-info-color, #6366f1); } + .vscroll-thumb:active { cursor: grabbing; opacity: 1; background: var(--theme-state-info-color, #6366f1); } .cell { box-sizing: border-box; } @@ -1431,33 +1302,18 @@ cursor: pointer; font-size: 14px; } - .range-nav:hover { - background: var(--theme-button-hovered); - } + .range-nav:hover { background: var(--theme-button-hovered); } .range-text { cursor: pointer; user-select: none; font-weight: 500; } - .range-text:hover { - color: var(--theme-state-info-color, #6366f1); - text-decoration: underline; - } - .corner .col-toggle { - flex: 0 0 18px; - } - .corner .col-status { - flex: 0 0 22px; - } - .corner .col-id { - flex: 0 0 80px; - } - .corner .col-title { - flex: 1 1 auto; - } - .corner .col-jump { - flex: 0 0 28px; - } + .range-text:hover { color: var(--theme-state-info-color, #6366f1); text-decoration: underline; } + .corner .col-toggle { flex: 0 0 18px; } + .corner .col-status { flex: 0 0 22px; } + .corner .col-id { flex: 0 0 80px; } + .corner .col-title { flex: 1 1 auto; } + .corner .col-jump { flex: 0 0 28px; } .resize-corner { position: sticky; top: 0; @@ -1489,8 +1345,7 @@ user-select: none; touch-action: none; } - .resize-cell:hover, - .resize-cell.active { + .resize-cell:hover, .resize-cell.active { background: var(--theme-state-info-color, #6366f1); } .canvas-cell { From 06cfec0bffa1eda756e0094dc28536899c75834b Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:53:23 +0000 Subject: [PATCH 064/388] =?UTF-8?q?fix(tracker-resources):=20Gantt=20bar?= =?UTF-8?q?=20selection=20=E2=80=94=20reactive=20prop=20helpers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous isEditable/isFocused/isSelected functions hid their underlying state dependencies (editableIssueIds Set, focusedIssueId, selectedIssueId) from Svelte's template dependency tracker. The template binding 'editable={isEditable(row.issue._id)}' only re-evaluated when its visible references (isEditable function ref and row.issue._id) changed — never when the underlying Set or string mutated. Initial pass: editableIssueIds is empty, bars render editable=false. Async canEditIssue loop populates the Set, but the template never re-runs the binding. GanttBar.onBarDown early-returns on !editable, so click/drag/resize all silently failed — user reported clicking a bar did nothing. Same staleness applied to selectedIssueId and focusedIssueId: even after handleBarMouseDown set selectedIssueId, the binding never re-evaluated, so the .selected class never reached the DOM. Fix: promote the three helpers to '$:' reactive declarations. Svelte now sees the underlying state as a dependency and reassigns the function on every relevant change, which invalidates the template binding and re-evaluates the prop. Verified end-to-end in Playwright on dk3 huly_v7 stack (gantt-v39): - 5 bars all editable=true after createQuery + canEditIssue loop - single click sets exactly one .selected (and synced .focused) - click on another bar deselects the previous one - click on background clears both selected and focused - drag from selected bar enters .active-drag with cursor=grabbing - mouseup leaves active-drag state cleanly - resize handles only render when the bar is selected Signed-off-by: Michael Uray --- .../src/components/gantt/GanttCanvas.svelte | 24 ++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte index 59fc9815c64..8d38478773e 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte @@ -48,17 +48,19 @@ return statusCategoryMap.get(String(sid)) ?? null } - function isEditable (issueId: unknown): boolean { - return editableIssueIds.has(String(issueId)) - } - - function isFocused (issueId: unknown): boolean { - return focusedIssueId !== null && String(issueId) === focusedIssueId - } - - function isSelected (issueId: unknown): boolean { - return selectedIssueId !== null && String(issueId) === selectedIssueId - } + // PR3.1 fix (2026-05-11): the previous `function isEditable/isFocused/ + // isSelected (id)` indirection hid the Set / id dependency from Svelte's + // template dependency tracker — `editable={isEditable(row.issue._id)}` + // only re-evaluated when `isEditable` or `row.issue._id` changed, never + // when the underlying `editableIssueIds` Set mutated. Result: bars stayed + // editable=false even after the async canEditIssue loop populated the Set, + // so click/drag/resize all silently failed because GanttBar.onBarDown + // early-returns on `!editable`. Reactive `$:` declarations make the deps + // explicit; closures stay closures, but template re-runs when the inputs + // change. + $: isEditable = (issueId: unknown): boolean => editableIssueIds.has(String(issueId)) + $: isFocused = (issueId: unknown): boolean => focusedIssueId !== null && String(issueId) === focusedIssueId + $: isSelected = (issueId: unknown): boolean => selectedIssueId !== null && String(issueId) === selectedIssueId // PR 3 spotlight dim: while a drag is active, dim every row that is NOT the // active issue. Keeps the cursor's row at full opacity so the user can see From 39ece573b38ef7877e61ace0355a9290c5a0434d Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:53:23 +0000 Subject: [PATCH 065/388] =?UTF-8?q?feat(tracker-resources):=20Gantt=20side?= =?UTF-8?q?bar=20=E2=80=94=20milestone=20single-click=20opens=20EditMilest?= =?UTF-8?q?one?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - canEditMilestone() helper in utils.ts, mirrors canEditIssue's guest-vs-member logic (non-guests can edit, guests cannot) - Milestone label cell in GanttSidebar gets class='cell-title clickable' + role='link' + on:click → openMilestone, matching the issue row - New openMilestone event on the sidebar dispatcher, threaded through GanttView.onMilestoneOpen → showPopup(EditMilestone, …) User feedback 2026-05-11: milestones in the Gantt sidebar were a plain non-interactive label; only issues opened on click. PR3.2 brings the sidebar to parity. Canvas-bar edit-mode parity for milestones (drag / resize) is deferred to PR3.3 — that requires a DragState refactor across 5 files to accept both Issue and Milestone payloads. Signed-off-by: Michael Uray --- .../src/components/gantt/GanttSidebar.svelte | 18 ++++++++++++++++-- .../src/components/gantt/GanttView.svelte | 9 +++++++++ plugins/tracker-resources/src/utils.ts | 18 ++++++++++++++++++ 3 files changed, 43 insertions(+), 2 deletions(-) diff --git a/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte b/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte index 0ea134e95ad..64b4eadc1d0 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte @@ -4,7 +4,7 @@ + + + + From 13977d91fb5b81d4f3733bbb42c659411ffad47b Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:53:23 +0000 Subject: [PATCH 080/388] feat(tracker-resources): GanttDependencyArrow bezier + lag pill (PR4a) Signed-off-by: Michael Uray --- .../gantt/GanttDependencyArrow.svelte | 109 ++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 plugins/tracker-resources/src/components/gantt/GanttDependencyArrow.svelte diff --git a/plugins/tracker-resources/src/components/gantt/GanttDependencyArrow.svelte b/plugins/tracker-resources/src/components/gantt/GanttDependencyArrow.svelte new file mode 100644 index 00000000000..89cb52e7d45 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/GanttDependencyArrow.svelte @@ -0,0 +1,109 @@ + + + +{#if path !== null && head !== null && mid !== null} + + + + + + {#if signedLag(relation.lag) !== '' } + + + + {pillText} + + {/if} + +{/if} + + From c50c14189caba090ed2541549b51adac785611eb Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:53:24 +0000 Subject: [PATCH 081/388] feat(tracker-resources): GanttDependencyLayer overlay (PR4a) Iterates IssueRelation list, renders one GanttDependencyArrow each, plus a live bezier preview tied to connector-drawing / connector-target-hover drag states. Signed-off-by: Michael Uray --- .../gantt/GanttDependencyLayer.svelte | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 plugins/tracker-resources/src/components/gantt/GanttDependencyLayer.svelte diff --git a/plugins/tracker-resources/src/components/gantt/GanttDependencyLayer.svelte b/plugins/tracker-resources/src/components/gantt/GanttDependencyLayer.svelte new file mode 100644 index 00000000000..5ab66d08dff --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/GanttDependencyLayer.svelte @@ -0,0 +1,78 @@ + + + + + {#each relations as rel (rel._id)} + {@const src = barRects.get(String(rel.attachedTo)) ?? null} + {@const dst = barRects.get(String(rel.target)) ?? null} + + {/each} + + {#if live !== null} + + + {/if} + + + From 5f05e90b60a2c12feaa020ec49154bb0710a056f Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:53:24 +0000 Subject: [PATCH 082/388] feat(tracker-resources): DependencyEditor popup (PR4a) Kind dropdown (FS/SS/FF/SF), lag NumberInput (-30..+90), delete with inline confirm, save/cancel. canEdit gates write actions; writes use the same client.apply().commit() pattern as PR3's drag-controller. Signed-off-by: Michael Uray --- .../src/components/DependencyEditor.svelte | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 plugins/tracker-resources/src/components/DependencyEditor.svelte diff --git a/plugins/tracker-resources/src/components/DependencyEditor.svelte b/plugins/tracker-resources/src/components/DependencyEditor.svelte new file mode 100644 index 00000000000..2a1429e86e0 --- /dev/null +++ b/plugins/tracker-resources/src/components/DependencyEditor.svelte @@ -0,0 +1,174 @@ + + + +
+
+ +
+
+ + +
+
+ +
+ +
+
+ + {#if confirmingDelete} +
+
+ {:else} + + {/if} +
+ + From a2a5a2ce070dfd8ea9d0304bfca8d66f815c095b Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:53:24 +0000 Subject: [PATCH 083/388] fix(tracker-resources): GanttDependencyLayer use stroke-dasharray:0 not :none (PR4a) 'none' is not a valid stroke-dasharray value; browsers fall back to solid silently. Use the canonical '0' reset so the CSS is portable. Signed-off-by: Michael Uray --- .../src/components/gantt/GanttDependencyLayer.svelte | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/plugins/tracker-resources/src/components/gantt/GanttDependencyLayer.svelte b/plugins/tracker-resources/src/components/gantt/GanttDependencyLayer.svelte index 5ab66d08dff..6d3eff2b567 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttDependencyLayer.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttDependencyLayer.svelte @@ -72,7 +72,9 @@ stroke-dasharray: 5,4; } :global(svg.gantt-canvas .live-connector.targeting) { - stroke-dasharray: none; + /* "0" is the canonical "no dash" reset; "none" is not a valid value + for stroke-dasharray and browsers fall back silently. */ + stroke-dasharray: 0; stroke: #475569; } From 2abb02b4265a20d7a6fd3c84b088a6ae84145c9f Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:53:24 +0000 Subject: [PATCH 084/388] feat(tracker-resources): GanttBar hover state + connector dot mount (PR4a) - Local `hovered` flag, surfaced via barHover event for hover-emphasize. - ConnectorDot mounts only when editable && hovered && issue-target, excluding milestone and summary-claw bars from connector dragging. Signed-off-by: Michael Uray --- .../src/components/gantt/GanttBar.svelte | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/plugins/tracker-resources/src/components/gantt/GanttBar.svelte b/plugins/tracker-resources/src/components/gantt/GanttBar.svelte index 031860010ab..146b4e034dd 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttBar.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttBar.svelte @@ -11,6 +11,7 @@ import tracker from '../../plugin' import type { TimeScale } from './lib/time-scale' import type { DragState, DragTarget } from './lib/types' + import GanttConnectorDot from './GanttConnectorDot.svelte' // Bar is rendered for both Issues and synthetic milestone summaries; the // structural subset below is all the bar geometry needs. @@ -39,8 +40,12 @@ const dispatch = createEventDispatcher<{ barMouseDown: { target: DragTarget, edge: 'left' | 'right' | 'body', cursorX: number } contextMenu: { issue: Issue, event: MouseEvent } + connectorDown: { source: Issue, cursorClientX: number, cursorClientY: number } + barHover: { issue: Issue | null } }>() + let hovered = false + function onBarContextMenu (evt: MouseEvent): void { // Context-menu is currently issue-only (PR3 menu wires Issue actions); // milestone bars don't surface it. PR3.3 keeps this gated until the @@ -189,6 +194,14 @@ aria-label={issue.title} on:mousedown={onBarDown('body')} on:contextmenu={onBarContextMenu} + on:mouseenter={() => { + hovered = true + if (dragTarget !== undefined && dragTarget.kind === 'issue') dispatch('barHover', { issue: dragTarget.doc }) + }} + on:mouseleave={() => { + hovered = false + dispatch('barHover', { issue: null }) + }} /> {#if selected && w >= 18} @@ -273,6 +286,14 @@ aria-label={issue.title} on:mousedown={onBarDown('body')} on:contextmenu={onBarContextMenu} + on:mouseenter={() => { + hovered = true + if (dragTarget !== undefined && dragTarget.kind === 'issue') dispatch('barHover', { issue: dragTarget.doc }) + }} + on:mouseleave={() => { + hovered = false + dispatch('barHover', { issue: null }) + }} /> {#if editable && selected && w >= 18} @@ -306,6 +327,20 @@ on:contextmenu={onBarContextMenu} /> {/if} + {#if editable && hovered && dragTarget !== undefined && dragTarget.kind === 'issue' && w >= 18} + { + if (dragTarget === undefined || dragTarget.kind !== 'issue') return + dispatch('connectorDown', { + source: dragTarget.doc, + cursorClientX: e.detail.cursorX, + cursorClientY: e.detail.cursorY + }) + }} + /> + {/if} {#if barLabel !== ''} Date: Fri, 15 May 2026 12:53:24 +0000 Subject: [PATCH 085/388] fix(tracker-resources): GanttBar isMilestoneSummary use dragTarget (PR4a follow-up) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR3.3 renamed export issueObj → dragTarget but left this $: block referencing the now-undeclared issueObj. The expression always evaluated true (undefined === undefined), so isMilestoneSummary collapsed to isSummary and parent-issue summary claws lost their hit-rect. Replace with the dragTarget-discriminated check. Signed-off-by: Michael Uray --- .../src/components/gantt/GanttBar.svelte | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/plugins/tracker-resources/src/components/gantt/GanttBar.svelte b/plugins/tracker-resources/src/components/gantt/GanttBar.svelte index 146b4e034dd..22c30241b9e 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttBar.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttBar.svelte @@ -140,10 +140,15 @@ $: tooltipText = visible ? `${issue.title} (${new Date(startVal).toISOString().slice(0, 10)} → ${new Date(dueVal).toISOString().slice(0, 10)})` : '' - // Milestone summaries pass no issueObj because they aggregate child dates - // synthetically — those stay read-only. Parent-issue summaries DO have an - // issueObj (the parent), and that one is editable. - $: isMilestoneSummary = isSummary && issueObj === undefined + // Milestone-synthetic-summary claws were dropped in PR3.3 (the milestone + // is now rendered as its own editable bar). Parent-issue summaries have a + // dragTarget of kind='issue' carrying the parent Issue. So the only + // remaining case for `isMilestoneSummary` is a summary row without an + // issue-target — defensive: keeps the gate intact if a future row-kind + // mounts GanttBar without a dragTarget. Was `issueObj === undefined` + // before PR3.3 renamed `issueObj` → `dragTarget` (latent regression + // surfaced during PR4a code review 2026-05-11). + $: isMilestoneSummary = isSummary && (dragTarget === undefined || dragTarget.kind !== 'issue') // ARIA labels for the resize handles — translated up-front so the rect // can use them as plain strings (svg `aria-label` accepts only strings). From 12dc202c7561a54b496f581881d60a4d75da1fe9 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 12:53:24 +0000 Subject: [PATCH 086/388] feat(tracker-resources): GanttCanvas mounts DependencyLayer + barRects (PR4a) Reactive Map, BarRect> computed from the same row geometry that positions GanttBar; threaded into GanttDependencyLayer so arrows can anchor without a separate measurement pass. Signed-off-by: Michael Uray --- .../src/components/gantt/GanttCanvas.svelte | 60 ++++++++++++++++++- 1 file changed, 59 insertions(+), 1 deletion(-) diff --git a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte index ba0b612fab0..9475941401e 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte @@ -4,24 +4,40 @@ +
+{:else} + + {/if} {/each}
dispatch('addIssue')} - on:keydown={(e) => { - if (e.key === 'Enter' || e.key === ' ') dispatch('addIssue') - }} + on:keydown={(e) => { if (e.key === 'Enter' || e.key === ' ') dispatch('addIssue') }} > +
+{/if} From d3f6261879ba224853f9d683fe55f395b1736df4 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 13:59:54 +0000 Subject: [PATCH 157/388] test(tracker-resources): cover groupRowsToLayoutRows adapter Three additional assertions covering the LayoutRow shape produced for group-header rows (collapsible=true, issue/milestone=null, groupKey/ Count/Label populated), the issue-row carry-through of groupKey for the canvas tint, and the preservation of y / height from the source group rows. Signed-off-by: Michael Uray --- .../gantt/lib/__tests__/build-rows.test.ts | 197 ++++++++++++++++++ 1 file changed, 197 insertions(+) create mode 100644 plugins/tracker-resources/src/components/gantt/lib/__tests__/build-rows.test.ts diff --git a/plugins/tracker-resources/src/components/gantt/lib/__tests__/build-rows.test.ts b/plugins/tracker-resources/src/components/gantt/lib/__tests__/build-rows.test.ts new file mode 100644 index 00000000000..fdd56b6f882 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/lib/__tests__/build-rows.test.ts @@ -0,0 +1,197 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import type { Issue } from '@hcengineering/tracker' +import { + GROUP_HEADER_HEIGHT, + buildGroupedRows, + groupRowsToLayoutRows, + type GanttGroupRow +} from '../build-rows' + +const ROW_HEIGHT = 32 + +function makeIssue (id: string, over: Partial = {}): Issue { + return { + _id: id, + _class: 'tracker:class:Issue', + space: 'space1', + status: 's-default', + priority: 0, + assignee: null, + component: null, + milestone: null, + title: id, + rank: '0', + identifier: id, + number: 1, + estimation: 0, + reportedTime: 0, + childInfo: [], + description: null, + subIssues: 0, + parents: [], + labels: 0, + ...over + } as unknown as Issue +} + +describe('buildGroupedRows — none (no grouping)', () => { + it('emits issue rows in input order with depth=0 and sequential y', () => { + const issues = [makeIssue('a'), makeIssue('b'), makeIssue('c')] + const rows = buildGroupedRows(issues, 'none', { + rowHeight: ROW_HEIGHT, + collapsedGroups: new Set() + }) + expect(rows).toHaveLength(3) + expect(rows.every((r): r is Extract => r.kind === 'issue')).toBe(true) + expect(rows.map(r => r.y)).toEqual([0, ROW_HEIGHT, 2 * ROW_HEIGHT]) + expect((rows[0] as any).depth).toBe(0) + }) + + it('returns empty rows for empty input', () => { + expect(buildGroupedRows([], 'none', { rowHeight: ROW_HEIGHT, collapsedGroups: new Set() })).toEqual([]) + }) +}) + +describe('buildGroupedRows — group by status', () => { + const issues = [ + makeIssue('a', { status: 's-backlog' as any }), + makeIssue('b', { status: 's-progress' as any }), + makeIssue('c', { status: 's-backlog' as any }) + ] + + it('emits one header per group and issues underneath', () => { + const rows = buildGroupedRows(issues, 'status', { + rowHeight: ROW_HEIGHT, + collapsedGroups: new Set() + }) + + // Expect: header(s-backlog) + a + c + header(s-progress) + b + expect(rows).toHaveLength(5) + + const h1 = rows[0] + expect(h1.kind).toBe('group-header') + expect((h1 as any).groupKey).toBe('s-backlog') + expect((h1 as any).count).toBe(2) + expect((h1 as any).collapsed).toBe(false) + expect(h1.y).toBe(0) + + expect(rows[1].kind).toBe('issue') + expect(rows[1].y).toBe(GROUP_HEADER_HEIGHT) + expect(rows[2].kind).toBe('issue') + expect(rows[2].y).toBe(GROUP_HEADER_HEIGHT + ROW_HEIGHT) + + const h2 = rows[3] + expect(h2.kind).toBe('group-header') + expect((h2 as any).groupKey).toBe('s-progress') + expect((h2 as any).count).toBe(1) + expect(h2.y).toBe(GROUP_HEADER_HEIGHT + 2 * ROW_HEIGHT) + + expect(rows[4].kind).toBe('issue') + expect(rows[4].y).toBe(2 * GROUP_HEADER_HEIGHT + 2 * ROW_HEIGHT) + }) + + it('collapses a group so its issues are not emitted but the header remains', () => { + const rows = buildGroupedRows(issues, 'status', { + rowHeight: ROW_HEIGHT, + collapsedGroups: new Set(['s-backlog']) + }) + // Expect: header(s-backlog collapsed=true) + header(s-progress) + b + expect(rows).toHaveLength(3) + expect(rows[0].kind).toBe('group-header') + expect((rows[0] as any).collapsed).toBe(true) + expect((rows[0] as any).count).toBe(2) // count is the original group size + expect(rows[1].kind).toBe('group-header') + expect(rows[2].kind).toBe('issue') + expect((rows[2] as any).issue._id).toBe('b') + expect(rows[1].y).toBe(GROUP_HEADER_HEIGHT) // second header sits right after the first + expect(rows[2].y).toBe(2 * GROUP_HEADER_HEIGHT) + }) +}) + +describe('buildGroupedRows — group by priority sorts numerically', () => { + it('emits lanes in numeric priority order', () => { + const issues = [ + makeIssue('a', { priority: 3 as any }), + makeIssue('b', { priority: 1 as any }), + makeIssue('c', { priority: 2 as any }) + ] + const rows = buildGroupedRows(issues, 'priority', { + rowHeight: ROW_HEIGHT, + collapsedGroups: new Set() + }) + const headerKeys = rows.filter(r => r.kind === 'group-header').map(r => (r as any).groupKey) + expect(headerKeys).toEqual(['1', '2', '3']) + }) +}) + +describe('buildGroupedRows — group by assignee sentinel sorts last', () => { + it('puts the unassigned bucket at the end', () => { + const issues = [ + makeIssue('a', { assignee: null }), + makeIssue('b', { assignee: 'p-1' as any }), + makeIssue('c', { assignee: 'p-2' as any }) + ] + const rows = buildGroupedRows(issues, 'assignee', { + rowHeight: ROW_HEIGHT, + collapsedGroups: new Set() + }) + const headerKeys = rows.filter(r => r.kind === 'group-header').map(r => (r as any).groupKey) + expect(headerKeys).toEqual(['p-1', 'p-2', '__unassigned__']) + }) +}) + +describe('groupRowsToLayoutRows adapter', () => { + const issues = [ + makeIssue('a', { status: 's-1' as any }), + makeIssue('b', { status: 's-1' as any }) + ] + const grouped = buildGroupedRows(issues, 'status', { rowHeight: ROW_HEIGHT, collapsedGroups: new Set() }) + + it('maps group-header rows to LayoutRow with group metadata', () => { + const layout = groupRowsToLayoutRows(grouped) + expect(layout[0].kind).toBe('group-header') + expect(layout[0].issue).toBeNull() + expect(layout[0].milestone).toBeNull() + expect(layout[0].collapsible).toBe(true) + expect(layout[0].groupKey).toBe('s-1') + expect(layout[0].groupCount).toBe(2) + expect(layout[0].groupLabel).toBe('s-1') + }) + + it('maps issue rows to LayoutRow carrying groupKey for canvas tint', () => { + const layout = groupRowsToLayoutRows(grouped) + expect(layout[1].kind).toBe('issue') + expect(layout[1].issue?._id).toBe('a') + expect(layout[1].groupKey).toBe('s-1') + expect(layout[1].collapsible).toBe(false) + }) + + it('preserves y / height from the source group rows', () => { + const layout = groupRowsToLayoutRows(grouped) + expect(layout[0].y).toBe(0) + expect(layout[0].height).toBe(GROUP_HEADER_HEIGHT) + expect(layout[1].y).toBe(GROUP_HEADER_HEIGHT) + expect(layout[1].height).toBe(ROW_HEIGHT) + }) +}) + +describe('buildGroupedRows — sort hook within a group', () => { + it('applies the within-group comparator before emission', () => { + const issues = [ + makeIssue('z', { status: 'x' as any, title: 'Zebra' }), + makeIssue('a', { status: 'x' as any, title: 'Aardvark' }) + ] + const rows = buildGroupedRows(issues, 'status', { + rowHeight: ROW_HEIGHT, + collapsedGroups: new Set(), + withinGroupCompare: (a, b) => a.title.localeCompare(b.title) + }) + const issueRows = rows.filter(r => r.kind === 'issue') + expect((issueRows[0] as any).issue._id).toBe('a') + expect((issueRows[1] as any).issue._id).toBe('z') + }) +}) From 0fb104a26f883e30e323c348a6264c87499f5000 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 14:00:28 +0000 Subject: [PATCH 158/388] fix(tracker-resources): pass IntlString to Button label in SchedulingModeEditor Signed-off-by: Michael Uray --- .../issues/SchedulingModeEditor.svelte | 86 +++++++++++++++++++ 1 file changed, 86 insertions(+) create mode 100644 plugins/tracker-resources/src/components/issues/SchedulingModeEditor.svelte diff --git a/plugins/tracker-resources/src/components/issues/SchedulingModeEditor.svelte b/plugins/tracker-resources/src/components/issues/SchedulingModeEditor.svelte new file mode 100644 index 00000000000..325d496848b --- /dev/null +++ b/plugins/tracker-resources/src/components/issues/SchedulingModeEditor.svelte @@ -0,0 +1,86 @@ + + + + + From fc0a34c12e565021afa6e0ba6cb1f8495500d1cd Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 14:01:04 +0000 Subject: [PATCH 159/388] test(tracker-resources): tighten sidebar-sort fixture typing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mkIssue test fixture was using a Partial spread + a literal 'string' for _id, which violates the branded Ref shape under strict svelte-check. Replace with an explicit IssueOverrides interface that casts _id once and feeds the rest of the Issue fields through fixed defaults — keeps the test surface readable while making svelte-check happy. Signed-off-by: Michael Uray --- .../gantt/lib/__tests__/sidebar-sort.test.ts | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 plugins/tracker-resources/src/components/gantt/lib/__tests__/sidebar-sort.test.ts diff --git a/plugins/tracker-resources/src/components/gantt/lib/__tests__/sidebar-sort.test.ts b/plugins/tracker-resources/src/components/gantt/lib/__tests__/sidebar-sort.test.ts new file mode 100644 index 00000000000..033d0557e57 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/lib/__tests__/sidebar-sort.test.ts @@ -0,0 +1,213 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import type { Ref } from '@hcengineering/core' +import type { Issue } from '@hcengineering/tracker' +import { IssuePriority } from '@hcengineering/tracker' +import { cycleSort, comparatorFor, type GanttSortState } from '../sidebar-sort' + +interface IssueOverrides { + _id: string + title?: string + identifier?: string + priority?: IssuePriority + estimation?: number + startDate?: number | null + dueDate?: number | null + modifiedOn?: number + createdOn?: number +} + +function mkIssue (overrides: IssueOverrides): Issue { + return { + _id: overrides._id as Ref, + title: overrides.title ?? '', + identifier: overrides.identifier ?? '', + priority: overrides.priority ?? IssuePriority.NoPriority, + estimation: overrides.estimation ?? 0, + startDate: overrides.startDate ?? null, + dueDate: overrides.dueDate ?? null, + modifiedOn: overrides.modifiedOn ?? 0, + createdOn: overrides.createdOn ?? 0, + assignee: null, + component: null, + milestone: null, + status: 's', + rank: '', + space: 'sp' + } as unknown as Issue +} + +describe('cycleSort', () => { + it('null state + click column → asc on that column', () => { + const next = cycleSort({ column: null, direction: 'asc' }, 'title') + expect(next).toEqual({ column: 'title', direction: 'asc' }) + }) + + it('asc on column + click same column → desc', () => { + const next = cycleSort({ column: 'title', direction: 'asc' }, 'title') + expect(next).toEqual({ column: 'title', direction: 'desc' }) + }) + + it('desc on column + click same column → null (off)', () => { + const next = cycleSort({ column: 'title', direction: 'desc' }, 'title') + expect(next).toEqual({ column: null, direction: 'asc' }) + }) + + it('clicking a different column resets to asc on the new column', () => { + const next = cycleSort({ column: 'title', direction: 'desc' }, 'priority') + expect(next).toEqual({ column: 'priority', direction: 'asc' }) + }) + + it('null state stays null when input is the same null state', () => { + const start: GanttSortState = { column: null, direction: 'asc' } + expect(cycleSort(start, 'identifier')).toEqual({ column: 'identifier', direction: 'asc' }) + }) +}) + +describe('comparatorFor — string columns', () => { + const issues = [ + mkIssue({ _id: 'a', title: 'Banana', identifier: 'OST-2' }), + mkIssue({ _id: 'b', title: 'apple', identifier: 'OST-1' }), + mkIssue({ _id: 'c', title: 'Cherry', identifier: 'OST-3' }) + ] + + it('title asc uses locale-compare (case-insensitive)', () => { + const cmp = comparatorFor('title', 'asc') + const sorted = [...issues].sort(cmp).map((i) => i.title) + expect(sorted).toEqual(['apple', 'Banana', 'Cherry']) + }) + + it('title desc reverses the asc order', () => { + const cmp = comparatorFor('title', 'desc') + const sorted = [...issues].sort(cmp).map((i) => i.title) + expect(sorted).toEqual(['Cherry', 'Banana', 'apple']) + }) + + it('identifier asc sorts lexicographically', () => { + const cmp = comparatorFor('identifier', 'asc') + const sorted = [...issues].sort(cmp).map((i) => i.identifier) + expect(sorted).toEqual(['OST-1', 'OST-2', 'OST-3']) + }) +}) + +describe('comparatorFor — enum columns', () => { + const issues = [ + mkIssue({ _id: 'a', priority: IssuePriority.Low }), + mkIssue({ _id: 'b', priority: IssuePriority.Urgent }), + mkIssue({ _id: 'c', priority: IssuePriority.NoPriority }), + mkIssue({ _id: 'd', priority: IssuePriority.Medium }) + ] + + it('priority asc orders by enum value (NoPriority=0 first)', () => { + const cmp = comparatorFor('priority', 'asc') + const sorted = [...issues].sort(cmp).map((i) => i.priority) + expect(sorted).toEqual([ + IssuePriority.NoPriority, + IssuePriority.Urgent, + IssuePriority.Medium, + IssuePriority.Low + ]) + }) + + it('priority desc inverts the order', () => { + const cmp = comparatorFor('priority', 'desc') + const sorted = [...issues].sort(cmp).map((i) => i.priority) + expect(sorted).toEqual([ + IssuePriority.Low, + IssuePriority.Medium, + IssuePriority.Urgent, + IssuePriority.NoPriority + ]) + }) +}) + +describe('comparatorFor — number columns', () => { + const issues = [ + mkIssue({ _id: 'a', estimation: 5 }), + mkIssue({ _id: 'b', estimation: 1 }), + mkIssue({ _id: 'c', estimation: 10 }) + ] + + it('estimation asc', () => { + const cmp = comparatorFor('estimation', 'asc') + const sorted = [...issues].sort(cmp).map((i) => i.estimation) + expect(sorted).toEqual([1, 5, 10]) + }) + + it('estimation desc', () => { + const cmp = comparatorFor('estimation', 'desc') + const sorted = [...issues].sort(cmp).map((i) => i.estimation) + expect(sorted).toEqual([10, 5, 1]) + }) +}) + +describe('comparatorFor — date columns with nulls-last semantics', () => { + const D1 = Date.UTC(2026, 0, 10) + const D2 = Date.UTC(2026, 0, 20) + const D3 = Date.UTC(2026, 0, 30) + const issues = [ + mkIssue({ _id: 'a', startDate: D2 }), + mkIssue({ _id: 'b', startDate: null }), + mkIssue({ _id: 'c', startDate: D1 }), + mkIssue({ _id: 'd', startDate: D3 }) + ] + + it('startDate asc with nulls last', () => { + const cmp = comparatorFor('startDate', 'asc') + const sorted = [...issues].sort(cmp).map((i) => i.startDate) + expect(sorted).toEqual([D1, D2, D3, null]) + }) + + it('startDate desc with nulls last (still last)', () => { + const cmp = comparatorFor('startDate', 'desc') + const sorted = [...issues].sort(cmp).map((i) => i.startDate) + expect(sorted).toEqual([D3, D2, D1, null]) + }) + + it('dueDate honours nulls-last identically', () => { + const list = [ + mkIssue({ _id: 'a', dueDate: D1 }), + mkIssue({ _id: 'b', dueDate: null }) + ] + const cmp = comparatorFor('dueDate', 'asc') + const sorted = [...list].sort(cmp).map((i) => i.dueDate) + expect(sorted).toEqual([D1, null]) + }) + + it('modifiedOn asc orders numerically', () => { + const list = [ + mkIssue({ _id: 'a', modifiedOn: 200 }), + mkIssue({ _id: 'b', modifiedOn: 100 }), + mkIssue({ _id: 'c', modifiedOn: 300 }) + ] + const cmp = comparatorFor('modifiedOn', 'asc') + const sorted = [...list].sort(cmp).map((i) => i.modifiedOn) + expect(sorted).toEqual([100, 200, 300]) + }) +}) + +describe('comparatorFor — fallback', () => { + it('unknown column returns 0 (preserves original order)', () => { + const cmp = comparatorFor('progress', 'asc') + const a = mkIssue({ _id: 'a' }) + const b = mkIssue({ _id: 'b' }) + expect(cmp(a, b)).toBe(0) + }) + + it('slack column is treated as non-orderable in v1 (returns 0)', () => { + const cmp = comparatorFor('slack', 'asc') + const a = mkIssue({ _id: 'a' }) + const b = mkIssue({ _id: 'b' }) + expect(cmp(a, b)).toBe(0) + }) + + it('predecessors column is non-orderable in v1', () => { + const cmp = comparatorFor('predecessors', 'asc') + const a = mkIssue({ _id: 'a' }) + const b = mkIssue({ _id: 'b' }) + expect(cmp(a, b)).toBe(0) + }) +}) From 2d066a6ddda39f19b5b0c44a6128f5684d762cfa Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 14:01:04 +0000 Subject: [PATCH 160/388] feat(tracker-resources): add GanttSidebarHeaderCell + GanttSidebarColumn components GanttSidebarHeaderCell handles the sortable header label and the column resize handle on the right edge. Resize uses window-level mousemove/up listeners with clean teardown on mouseup or component destroy. GanttSidebarColumn dispatches per-column-key cell rendering: identifier (IssuePresenter), title (clickable openIssue), status (StatusBadge), priority/estimation/dates as read-only text, predecessors/slack reusing the existing helpers. Inline-edit popups are reserved for Phase 3a.v2 when the undo-manager (Phase 3c) is in place. EN+DE i18n added for the 16 column header labels, the extended-mode toggle and the sort-breaks-hierarchy warning tooltip. Signed-off-by: Michael Uray --- plugins/tracker-assets/lang/de.json | 20 +- plugins/tracker-assets/lang/en.json | 20 +- .../gantt/GanttSidebarColumn.svelte | 194 ++++++++++++++++++ .../gantt/GanttSidebarHeaderCell.svelte | 184 +++++++++++++++++ plugins/tracker-resources/src/plugin.ts | 21 +- 5 files changed, 436 insertions(+), 3 deletions(-) create mode 100644 plugins/tracker-resources/src/components/gantt/GanttSidebarColumn.svelte create mode 100644 plugins/tracker-resources/src/components/gantt/GanttSidebarHeaderCell.svelte diff --git a/plugins/tracker-assets/lang/de.json b/plugins/tracker-assets/lang/de.json index f2c3830d111..606704c029e 100644 --- a/plugins/tracker-assets/lang/de.json +++ b/plugins/tracker-assets/lang/de.json @@ -390,7 +390,25 @@ "GanttHelpTitle": "Tastenkürzel", "GanttHelpEsc": "Esc oder ? zum Schließen drücken", "GanttExport": "Als PNG exportieren", - "GanttExportFailed": "Export fehlgeschlagen" + "GanttExportFailed": "Export fehlgeschlagen", + "GanttSidebarColumnsExtended": "Erweiterte Sidebar-Spalten", + "GanttSortBreaksHierarchy": "Sortierung bricht die Hierarchie — Unter-Issues werden vom Eltern-Issue gelöst.", + "GanttSidebarColIdentifier": "ID", + "GanttSidebarColTitle": "Titel", + "GanttSidebarColStatus": "Status", + "GanttSidebarColPriority": "Priorität", + "GanttSidebarColAssignee": "Bearbeiter", + "GanttSidebarColEstimation": "Schätzung", + "GanttSidebarColComponent": "Komponente", + "GanttSidebarColMilestone": "Meilenstein", + "GanttSidebarColPredecessors": "Vorgänger", + "GanttSidebarColSlack": "Puffer", + "GanttSidebarColStartDate": "Start", + "GanttSidebarColDueDate": "Fällig", + "GanttSidebarColDeadline": "Deadline", + "GanttSidebarColProgress": "Fortschritt", + "GanttSidebarColModifiedOn": "Geändert", + "GanttSidebarColCreatedOn": "Erstellt" }, "status": {} } diff --git a/plugins/tracker-assets/lang/en.json b/plugins/tracker-assets/lang/en.json index efba3ff322d..70e43e5d3ae 100644 --- a/plugins/tracker-assets/lang/en.json +++ b/plugins/tracker-assets/lang/en.json @@ -391,7 +391,25 @@ "GanttHelpTitle": "Keyboard shortcuts", "GanttHelpEsc": "Press Esc or ? to close", "GanttExport": "Export to PNG", - "GanttExportFailed": "Export failed" + "GanttExportFailed": "Export failed", + "GanttSidebarColumnsExtended": "Extended sidebar columns", + "GanttSortBreaksHierarchy": "Sorting breaks the hierarchy view — sub-issues are detached from their parent.", + "GanttSidebarColIdentifier": "ID", + "GanttSidebarColTitle": "Title", + "GanttSidebarColStatus": "Status", + "GanttSidebarColPriority": "Priority", + "GanttSidebarColAssignee": "Assignee", + "GanttSidebarColEstimation": "Estimation", + "GanttSidebarColComponent": "Component", + "GanttSidebarColMilestone": "Milestone", + "GanttSidebarColPredecessors": "Predecessors", + "GanttSidebarColSlack": "Slack", + "GanttSidebarColStartDate": "Start", + "GanttSidebarColDueDate": "Due", + "GanttSidebarColDeadline": "Deadline", + "GanttSidebarColProgress": "Progress", + "GanttSidebarColModifiedOn": "Modified", + "GanttSidebarColCreatedOn": "Created" }, "status": {} } diff --git a/plugins/tracker-resources/src/components/gantt/GanttSidebarColumn.svelte b/plugins/tracker-resources/src/components/gantt/GanttSidebarColumn.svelte new file mode 100644 index 00000000000..91d95f968b8 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/GanttSidebarColumn.svelte @@ -0,0 +1,194 @@ + + + + + + diff --git a/plugins/tracker-resources/src/components/gantt/GanttSidebarHeaderCell.svelte b/plugins/tracker-resources/src/components/gantt/GanttSidebarHeaderCell.svelte new file mode 100644 index 00000000000..67191b16c76 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/GanttSidebarHeaderCell.svelte @@ -0,0 +1,184 @@ + + + +
+ + + + + diff --git a/plugins/tracker-resources/src/plugin.ts b/plugins/tracker-resources/src/plugin.ts index 92a0dd47aa4..3393cc2d3ed 100644 --- a/plugins/tracker-resources/src/plugin.ts +++ b/plugins/tracker-resources/src/plugin.ts @@ -371,7 +371,26 @@ export default mergeIds(trackerId, tracker, { GanttHelpTitle: '' as IntlString, GanttHelpEsc: '' as IntlString, GanttExport: '' as IntlString, - GanttExportFailed: '' as IntlString + GanttExportFailed: '' as IntlString, + // Phase 3a — sidebar inline-grid columns + GanttSidebarColumnsExtended: '' as IntlString, + GanttSortBreaksHierarchy: '' as IntlString, + GanttSidebarColIdentifier: '' as IntlString, + GanttSidebarColTitle: '' as IntlString, + GanttSidebarColStatus: '' as IntlString, + GanttSidebarColPriority: '' as IntlString, + GanttSidebarColAssignee: '' as IntlString, + GanttSidebarColEstimation: '' as IntlString, + GanttSidebarColComponent: '' as IntlString, + GanttSidebarColMilestone: '' as IntlString, + GanttSidebarColPredecessors: '' as IntlString, + GanttSidebarColSlack: '' as IntlString, + GanttSidebarColStartDate: '' as IntlString, + GanttSidebarColDueDate: '' as IntlString, + GanttSidebarColDeadline: '' as IntlString, + GanttSidebarColProgress: '' as IntlString, + GanttSidebarColModifiedOn: '' as IntlString, + GanttSidebarColCreatedOn: '' as IntlString }, component: { NopeComponent: '' as AnyComponent, From 65a7c41cc2c1decb5976e95321f6a7da79d5418d Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 14:01:04 +0000 Subject: [PATCH 161/388] feat(tracker-resources): tag cascade commits with cascadeToken for downstream consumers Signed-off-by: Michael Uray --- .../src/components/gantt/GanttView.svelte | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/plugins/tracker-resources/src/components/gantt/GanttView.svelte b/plugins/tracker-resources/src/components/gantt/GanttView.svelte index 5be131999f7..bdafee1b5fc 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttView.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttView.svelte @@ -7,6 +7,7 @@ import { type Issue, type IssueRelation, type Milestone, type Project, type WorkingDaysConfig } from '@hcengineering/tracker' import { connectedIssueIds } from './lib/dependency-router' import { wouldCreateCycle, simulateCascade, addScheduleDays } from './lib/scheduler' + import { newCascadeToken } from './lib/cascade-token' import { fsAnchor, ssAnchor, ffAnchor, sfAnchor } from './lib/working-days' import { computeCriticalPath } from './lib/critical-path' import type { CriticalPathResult } from './lib/types' @@ -864,7 +865,11 @@ for (const i of allInSpace) allByRef.set(i._id, i) if (altKey) { - const ops = client.apply(undefined, 'gantt-cascade-bypass') + // Tier-2 Item 5 — cascadeToken plumbing. Tag every cascade-related + // commit with a unique token (scope-string) so Tier-2 Item 6 (bulk- + // drag) and Tier-4 Item 14 (cascade-shift notification) can correlate + // every sub-Tx of one user-action to a single batch downstream. + const ops = client.apply(undefined, newCascadeToken('gantt-cascade-bypass')) for (const pe of primaryEdits) { await ops.update(pe.issue, { startDate: pe.newStart, dueDate: pe.newDue }) } @@ -964,7 +969,7 @@ return } } - const ops = client.apply(undefined, 'gantt-no-cascade') + const ops = client.apply(undefined, newCascadeToken('gantt-no-cascade')) for (const pe of result.primary) { await ops.update(pe.issue, { startDate: pe.newStart, dueDate: pe.newDue }) } @@ -1033,7 +1038,7 @@ shifts: CascadeShift[] ): Promise { const client = getClient() - const ops = client.apply(undefined, 'gantt-cascade-commit') + const ops = client.apply(undefined, newCascadeToken('gantt-cascade-commit')) for (const pe of primary) { await ops.update(pe.issue, { startDate: pe.newStart, dueDate: pe.newDue }) } From 684280160570eff3d45acd0ac225ee5da7d9f0fa Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 14:01:04 +0000 Subject: [PATCH 162/388] feat(tracker-resources): add sidebar-columns pure helpers for Phase 3a Introduce SidebarColumnKey union, DEFAULT_COLUMNS, DEFAULT_WIDTHS map, MIN/MAX_WIDTH clamps and a defensive parseColumns coercer that always yields at least one valid column. 16 unit tests cover the legal and malformed input paths. Signed-off-by: Michael Uray --- .../lib/__tests__/sidebar-columns.test.ts | 99 ++++++++++++++ .../components/gantt/lib/sidebar-columns.ts | 124 ++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 plugins/tracker-resources/src/components/gantt/lib/__tests__/sidebar-columns.test.ts create mode 100644 plugins/tracker-resources/src/components/gantt/lib/sidebar-columns.ts diff --git a/plugins/tracker-resources/src/components/gantt/lib/__tests__/sidebar-columns.test.ts b/plugins/tracker-resources/src/components/gantt/lib/__tests__/sidebar-columns.test.ts new file mode 100644 index 00000000000..325d3cc9698 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/lib/__tests__/sidebar-columns.test.ts @@ -0,0 +1,99 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { + parseColumns, + clampWidth, + DEFAULT_WIDTHS, + DEFAULT_COLUMNS, + ALL_COLUMN_KEYS, + MIN_WIDTH, + MAX_WIDTH, + type SidebarColumnKey +} from '../sidebar-columns' + +describe('sidebar-columns: parseColumns', () => { + it('returns DEFAULT_COLUMNS for undefined', () => { + expect(parseColumns(undefined)).toEqual(DEFAULT_COLUMNS) + }) + + it('returns DEFAULT_COLUMNS for null', () => { + expect(parseColumns(null)).toEqual(DEFAULT_COLUMNS) + }) + + it('returns DEFAULT_COLUMNS for non-array values', () => { + expect(parseColumns(42)).toEqual(DEFAULT_COLUMNS) + expect(parseColumns('identifier')).toEqual(DEFAULT_COLUMNS) + expect(parseColumns({ identifier: true })).toEqual(DEFAULT_COLUMNS) + }) + + it('keeps a valid single-column array', () => { + expect(parseColumns(['identifier'])).toEqual(['identifier']) + }) + + it('filters out unknown column keys', () => { + expect(parseColumns(['identifier', 'bogus', 'title'])).toEqual(['identifier', 'title']) + }) + + it('returns DEFAULT_COLUMNS when array is empty (min 1 column rule)', () => { + expect(parseColumns([])).toEqual(DEFAULT_COLUMNS) + }) + + it('returns DEFAULT_COLUMNS when all entries are invalid', () => { + expect(parseColumns(['bogus', 'other'])).toEqual(DEFAULT_COLUMNS) + }) + + it('preserves order of valid entries', () => { + expect(parseColumns(['title', 'identifier'])).toEqual(['title', 'identifier']) + }) + + it('deduplicates repeated keys', () => { + expect(parseColumns(['title', 'title', 'identifier'])).toEqual(['title', 'identifier']) + }) +}) + +describe('sidebar-columns: clampWidth', () => { + it('clamps below MIN_WIDTH', () => { + expect(clampWidth(20)).toBe(MIN_WIDTH) + expect(clampWidth(0)).toBe(MIN_WIDTH) + expect(clampWidth(-5)).toBe(MIN_WIDTH) + }) + + it('clamps above MAX_WIDTH', () => { + expect(clampWidth(500)).toBe(MAX_WIDTH) + expect(clampWidth(10_000)).toBe(MAX_WIDTH) + }) + + it('returns value unchanged within range', () => { + expect(clampWidth(40)).toBe(40) + expect(clampWidth(150)).toBe(150) + expect(clampWidth(400)).toBe(400) + }) + + it('rounds fractional pixel inputs', () => { + expect(clampWidth(150.7)).toBe(151) + expect(clampWidth(150.3)).toBe(150) + }) +}) + +describe('sidebar-columns: constants', () => { + it('DEFAULT_WIDTHS covers every ALL_COLUMN_KEYS entry', () => { + for (const key of ALL_COLUMN_KEYS) { + expect(typeof DEFAULT_WIDTHS[key]).toBe('number') + expect(DEFAULT_WIDTHS[key]).toBeGreaterThanOrEqual(MIN_WIDTH) + expect(DEFAULT_WIDTHS[key]).toBeLessThanOrEqual(MAX_WIDTH) + } + }) + + it('DEFAULT_COLUMNS is a subset of ALL_COLUMN_KEYS', () => { + for (const k of DEFAULT_COLUMNS) { + expect(ALL_COLUMN_KEYS).toContain(k as SidebarColumnKey) + } + }) + + it('MIN_WIDTH < MAX_WIDTH', () => { + expect(MIN_WIDTH).toBeLessThan(MAX_WIDTH) + }) +}) diff --git a/plugins/tracker-resources/src/components/gantt/lib/sidebar-columns.ts b/plugins/tracker-resources/src/components/gantt/lib/sidebar-columns.ts new file mode 100644 index 00000000000..7df0cd4ea7c --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/lib/sidebar-columns.ts @@ -0,0 +1,124 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +/** + * Phase 3a — Sidebar inline-grid columns. + * + * Pure helpers for the configurable Gantt sidebar column system. All inputs + * are runtime-unknown (ViewOption storage) so the parsing path is defensive: + * unknown shapes degrade gracefully to {@link DEFAULT_COLUMNS}. Width values + * are clamped between {@link MIN_WIDTH} and {@link MAX_WIDTH} on every entry + * — the resize handle in the header writes through `clampWidth`, so even a + * misbehaving pointer (or a tampered preference) cannot push the sidebar + * into an unusable state. + */ + +/** Union of all built-in column keys recognised in Phase 3a v1. */ +export type SidebarColumnKey = + | 'identifier' + | 'title' + | 'status' + | 'priority' + | 'assignee' + | 'estimation' + | 'component' + | 'milestone' + | 'predecessors' + | 'slack' + | 'startDate' + | 'dueDate' + | 'deadline' + | 'progress' + | 'modifiedOn' + | 'createdOn' + +/** Authoritative list of every recognised column key, ordered by intuitive UI grouping. */ +export const ALL_COLUMN_KEYS: readonly SidebarColumnKey[] = [ + 'identifier', + 'title', + 'status', + 'priority', + 'assignee', + 'estimation', + 'component', + 'milestone', + 'predecessors', + 'slack', + 'startDate', + 'dueDate', + 'deadline', + 'progress', + 'modifiedOn', + 'createdOn' +] + +/** + * Default visible columns when no user preference exists — preserves the + * pre-Phase-3a sidebar exactly, so existing users see no surface change + * on first upgrade. + */ +export const DEFAULT_COLUMNS: readonly SidebarColumnKey[] = [ + 'identifier', + 'title', + 'predecessors', + 'slack' +] + +/** Per-column default pixel width. Tweak in tandem with `.cell-{key}` CSS. */ +export const DEFAULT_WIDTHS: Record = { + identifier: 80, + title: 240, + status: 100, + priority: 80, + assignee: 140, + estimation: 80, + component: 120, + milestone: 140, + predecessors: 140, + slack: 60, + startDate: 100, + dueDate: 100, + deadline: 100, + progress: 80, + modifiedOn: 100, + createdOn: 100 +} + +/** Hard floor — narrower than this and the cell content is unreadable. */ +export const MIN_WIDTH = 40 +/** Hard ceiling — wider than this and the sidebar drowns the canvas. */ +export const MAX_WIDTH = 400 + +const KNOWN_SET: ReadonlySet = new Set(ALL_COLUMN_KEYS) + +/** + * Coerce an unknown ViewOption value into a SidebarColumnKey[]. Filters + * unknown keys, deduplicates, and falls back to {@link DEFAULT_COLUMNS} + * when the input shape is wrong or the resulting list would be empty. + */ +export function parseColumns (raw: unknown): SidebarColumnKey[] { + if (!Array.isArray(raw)) return [...DEFAULT_COLUMNS] + const seen = new Set() + const out: SidebarColumnKey[] = [] + for (const entry of raw) { + if (typeof entry !== 'string') continue + if (!KNOWN_SET.has(entry)) continue + const key = entry as SidebarColumnKey + if (seen.has(key)) continue + seen.add(key) + out.push(key) + } + if (out.length === 0) return [...DEFAULT_COLUMNS] + return out +} + +/** Clamp + round a width value so persisted/dragged values stay sane. */ +export function clampWidth (px: number): number { + if (!Number.isFinite(px)) return MIN_WIDTH + const rounded = Math.round(px) + if (rounded < MIN_WIDTH) return MIN_WIDTH + if (rounded > MAX_WIDTH) return MAX_WIDTH + return rounded +} From e5e9b78a3085546b09abcf44bedc1986a9374bc9 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 14:03:06 +0000 Subject: [PATCH 163/388] feat(tracker-resources): wire Phase 3b filter-bar + group-by swimlanes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GanttView.svelte: - Read ganttGroupBy from viewOptions (safe fallback 'none') - In-memory ganttFilter + collapsedGroups (per-mode reset on group switch) - filteredIssues feeds buildLayout (legacy) OR buildGroupedRows - onToggle routes 'group:*' ids to collapsedGroups - sortedRows short-circuits when groupBy active (sort applied within lane) - Toolbar-right gains: Filter button with badge + popup (priority chips), Group-By dropdown with overrides-hierarchy tooltip - Ctrl+F/Cmd+F shortcut toggles the filter popup (was reserved in Phase 1) GanttSidebar.svelte: - Renders 'group-header' rows in both legacy and extended-grid layouts with chevron, label, count; spans full grid width GanttCanvas.svelte: - Paints lane-tint band behind 'group-header' rows (.group-header-bg) lib/types.ts: - LayoutRow.kind union extended with 'group-header' - Optional groupKey/groupLabel/groupCount fields lib/build-rows.ts: - groupRowsToLayoutRows adapter so existing Sidebar/Canvas iterate the same LayoutRow[] for both legacy and grouped modes Strings: - 22 new EN+DE keys for GroupBy / Filter / sentinel labels 256 → 289 Jest tests (green); svelte-check passes. Signed-off-by: Michael Uray --- plugins/tracker-assets/lang/de.json | 23 +- plugins/tracker-assets/lang/en.json | 23 +- .../src/components/gantt/GanttCanvas.svelte | 11 +- .../src/components/gantt/GanttSidebar.svelte | 102 +----- .../src/components/gantt/GanttView.svelte | 322 +++++++++++++++++- .../src/components/gantt/lib/build-rows.ts | 196 +++++++++++ .../src/components/gantt/lib/types.ts | 17 +- plugins/tracker-resources/src/plugin.ts | 24 +- 8 files changed, 603 insertions(+), 115 deletions(-) create mode 100644 plugins/tracker-resources/src/components/gantt/lib/build-rows.ts diff --git a/plugins/tracker-assets/lang/de.json b/plugins/tracker-assets/lang/de.json index 606704c029e..50728a383ba 100644 --- a/plugins/tracker-assets/lang/de.json +++ b/plugins/tracker-assets/lang/de.json @@ -408,7 +408,28 @@ "GanttSidebarColDeadline": "Deadline", "GanttSidebarColProgress": "Fortschritt", "GanttSidebarColModifiedOn": "Geändert", - "GanttSidebarColCreatedOn": "Erstellt" + "GanttSidebarColCreatedOn": "Erstellt", + "GanttGroupBy": "Gruppieren nach", + "GanttGroupByNone": "Keine", + "GanttGroupByStatus": "Status", + "GanttGroupByPriority": "Priorität", + "GanttGroupByAssignee": "Bearbeiter", + "GanttGroupByComponent": "Komponente", + "GanttGroupByMilestone": "Meilenstein", + "GanttGroupByLabel": "Label", + "GanttGroupOverridesHierarchy": "Gruppierung hebt die Hierarchie-Einrückung auf. Setze Gruppieren auf Keine, um die Eltern/Kind-Struktur zu sehen.", + "GanttUnassigned": "Nicht zugewiesen", + "GanttNoComponent": "Keine Komponente", + "GanttNoMilestone": "Kein Meilenstein", + "GanttNoLabel": "Kein Label", + "GanttUnknownGroup": "(unbekannt)", + "GanttAllIssues": "Alle Issues", + "GanttFilter": "Filter", + "GanttFilterClear": "Filter zurücksetzen", + "GanttFilterByStatus": "Status", + "GanttFilterByPriority": "Priorität", + "GanttFilterByAssignee": "Bearbeiter", + "GanttFilterEmpty": "Keine Issues entsprechen dem aktiven Filter" }, "status": {} } diff --git a/plugins/tracker-assets/lang/en.json b/plugins/tracker-assets/lang/en.json index 70e43e5d3ae..b3169e2700d 100644 --- a/plugins/tracker-assets/lang/en.json +++ b/plugins/tracker-assets/lang/en.json @@ -409,7 +409,28 @@ "GanttSidebarColDeadline": "Deadline", "GanttSidebarColProgress": "Progress", "GanttSidebarColModifiedOn": "Modified", - "GanttSidebarColCreatedOn": "Created" + "GanttSidebarColCreatedOn": "Created", + "GanttGroupBy": "Group by", + "GanttGroupByNone": "None", + "GanttGroupByStatus": "Status", + "GanttGroupByPriority": "Priority", + "GanttGroupByAssignee": "Assignee", + "GanttGroupByComponent": "Component", + "GanttGroupByMilestone": "Milestone", + "GanttGroupByLabel": "Label", + "GanttGroupOverridesHierarchy": "Group-By overrides hierarchy indentation. Set Group by to None to see the parent/child tree.", + "GanttUnassigned": "Unassigned", + "GanttNoComponent": "No component", + "GanttNoMilestone": "No milestone", + "GanttNoLabel": "No label", + "GanttUnknownGroup": "(unknown)", + "GanttAllIssues": "All issues", + "GanttFilter": "Filter", + "GanttFilterClear": "Clear filter", + "GanttFilterByStatus": "Status", + "GanttFilterByPriority": "Priority", + "GanttFilterByAssignee": "Assignee", + "GanttFilterEmpty": "No issues match the active filter" }, "status": {} } diff --git a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte index 344513dc6f1..f08212a0f83 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte @@ -223,7 +223,9 @@ + bars so they appear behind the bar geometry. Phase 3b: group-header + rows get an explicit tint band so the swimlane is visually distinct + across the canvas, matching the sidebar's group-header row. --> {#each visibleRows as row (rowKey(row))} {@const isHover = hoveredRowId === row.id} @@ -235,6 +237,7 @@ class="row-rect" class:hovered={isHover} class:milestone-bg={row.kind === 'milestone'} + class:group-header-bg={row.kind === 'group-header'} class:dimmed={row.issue !== null && isDimmed(row.issue._id)} /> {/each} @@ -465,6 +468,12 @@ :global(svg.gantt-canvas .row-rect.milestone-bg.hovered) { fill: color-mix(in srgb, var(--theme-state-info-color, #6366f1) 14%, transparent); } + /* Phase 3b — swimlane header band painted behind the group-header row + so the lane boundary reads across the entire canvas width. */ + :global(svg.gantt-canvas .row-rect.group-header-bg) { + fill: var(--theme-divider-color); + opacity: 0.7; + } /* Phase-2 weekend / holiday tint — theme-aware via divider colour so it stays subtle in both light and dark themes. */ :global(svg.gantt-canvas .non-working-day-rect) { diff --git a/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte b/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte index 7c8f6a69633..da2ad9b9830 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttSidebar.svelte @@ -23,7 +23,6 @@ type SidebarColumnKey } from './lib/sidebar-columns' import type { GanttSortState, SortDirection } from './lib/sidebar-sort' - import { computeYViewport, sliceVisibleRows } from './lib/y-viewport' export let rows: LayoutRow[] export let width: number = 280 @@ -50,17 +49,6 @@ export let columns: readonly SidebarColumnKey[] = DEFAULT_COLUMNS export let widths: Record = { ...DEFAULT_WIDTHS } export let sort: GanttSortState = { column: null, direction: 'asc' } - // Tier-3 Item 5 — Y-axis virtualization. When all three are set, only rows - // intersecting the [scrollTop, scrollTop+viewportHeight] band are rendered, - // with absolute-positioning so the DOM aligns with the canvas bars at the - // same y. When any is omitted (e.g. fixtures/tests), the legacy render - // path renders every row in document order — bit-for-bit compatible with - // prior phases. - export let scrollTop: number = 0 - export let viewportHeight: number = 0 - export let rowHeight: number = 0 - /** Extra rows rendered above + below the visible band. Default 5 (spec §4). */ - export let overscan: number = 5 /** Set of columns that have a meaningful comparator — others are non-sortable. */ const SORTABLE_COLUMNS: ReadonlySet = new Set([ @@ -110,44 +98,6 @@ $: activeIssueIdStr = activeDragTargetId(dragState) $: anyDragActive = dragState.kind !== 'idle' && dragState.kind !== 'hover-bar' - // Tier-3 Item 5 — virtualization is "on" when the parent wires the three - // viewport props. Off → render every row (legacy path, preserved for - // tests / embed previews). - $: virtualizationOn = rowHeight > 0 && viewportHeight > 0 - // When the parent applies a sort that re-orders the `rows` array, each - // row's `.y` field (set by `buildLayout` on the un-sorted ordering) no - // longer matches the sorted position. Re-stamp `y = index × rowHeight` - // so the absolute-positioned rows visually appear in the order they're - // passed in. This is identical to buildLayout's own y assignment for - // uniform-height rows, just keyed on the current (post-sort) index. - $: indexedRows = virtualizationOn - ? rows.map((r, i) => ({ row: r, vy: i * rowHeight })) - : rows.map((r) => ({ row: r, vy: r.y })) - // Total scrollable height — rowCount × rowHeight, matching the canvas. - $: totalRowsHeight = rows.length * (rowHeight > 0 ? rowHeight : 0) || (rows.length > 0 ? rows[rows.length - 1].y + rows[rows.length - 1].height : 0) - $: yViewport = virtualizationOn - ? computeYViewport({ - rowCount: rows.length, - rowHeight, - scrollTop, - viewportHeight, - overscan - }) - : null - // Slice on the re-stamped vy (so post-sort order matters) and project the - // result back to `{ row, vy }` pairs the template iterates over. - $: visibleIndexed = (virtualizationOn && yViewport !== null) - ? indexedRows.filter((p) => { - return p.vy + (p.row.height ?? rowHeight) > scrollTop - overscan * rowHeight && - p.vy < scrollTop + viewportHeight + overscan * rowHeight - }) - : indexedRows - // When virtualizing, render the slice anchored to its re-stamped vy; the - // wrapper carries an explicit height so the scroller's scrollHeight - // matches the unvirtualized layout. The add-issue-row sits BELOW the - // spacer so it always renders at the bottom of the list. - $: spacerHeight = virtualizationOn ? totalRowsHeight : 0 - const dispatch = createEventDispatcher<{ jump: { x: number } toggle: { id: string } @@ -270,14 +220,8 @@ /> {/each}
- {:else} -
`) for keyed each-blocks. */ + id: string + issue: Issue + depth: number + y: number + height: number + /** Group this issue belongs to, so the canvas can tint by lane. */ + groupKey: string + } + +export interface BuildGroupedRowsOptions { + rowHeight: number + collapsedGroups: Set + /** + * Optional sort comparator applied *within* each group before emission. + * Omitted ⇒ stable insertion order from the input array. + */ + withinGroupCompare?: (a: Issue, b: Issue) => number +} + +/** + * Group `issues` by `groupBy` and produce the flat row stream. + * + * - `groupBy === 'none'`: no headers, issues emitted in input order. + * - Otherwise: one header per non-empty bucket, followed by its issues + * unless the bucket is collapsed. A collapsed bucket still emits its + * header (so the user can re-expand) but skips the issues. + */ +export function buildGroupedRows ( + issues: readonly Issue[], + groupBy: GroupByKey, + opts: BuildGroupedRowsOptions +): GanttGroupRow[] { + const { rowHeight, collapsedGroups, withinGroupCompare } = opts + const rows: GanttGroupRow[] = [] + let y = 0 + + if (groupBy === 'none') { + for (const issue of issues) { + rows.push({ + kind: 'issue', + id: `issue:${String(issue._id)}`, + issue, + depth: 0, + y, + height: rowHeight, + groupKey: '__none__' + }) + y += rowHeight + } + return rows + } + + // Bucket issues by group key, preserving insertion order within each bucket. + const buckets = new Map() + for (const issue of issues) { + const key = resolveGroupKey(issue, groupBy) + const list = buckets.get(key) + if (list === undefined) { + buckets.set(key, [issue]) + } else { + list.push(issue) + } + } + + const orderedKeys = sortGroupKeys([...buckets.keys()], groupBy) + for (const key of orderedKeys) { + const bucket = buckets.get(key) + if (bucket === undefined) continue + const collapsed = collapsedGroups.has(key) + rows.push({ + kind: 'group-header', + id: `group:${key}`, + groupKey: key, + label: getGroupLabel(key, groupBy), + count: bucket.length, + collapsed, + y, + height: GROUP_HEADER_HEIGHT + }) + y += GROUP_HEADER_HEIGHT + if (collapsed) continue + const ordered = withinGroupCompare !== undefined ? [...bucket].sort(withinGroupCompare) : bucket + for (const issue of ordered) { + rows.push({ + kind: 'issue', + id: `issue:${String(issue._id)}`, + issue, + depth: 0, + y, + height: rowHeight, + groupKey: key + }) + y += rowHeight + } + } + + return rows +} + +/** + * Convert a `GanttGroupRow[]` into the shared `LayoutRow[]` shape that the + * existing Sidebar/Canvas components iterate over. Group headers materialise + * as `kind: 'group-header'` rows with `collapsible: true`. Issue rows carry + * over the original group key so the canvas can tint the lane behind them. + * + * Defensive: when the input contains a row referencing a `null` issue (not + * produced by `buildGroupedRows`, but guards against future callers), the + * row is skipped to keep the legacy LayoutRow invariants intact. + */ +export function groupRowsToLayoutRows ( + rows: readonly GanttGroupRow[] +): LayoutRow[] { + const out: LayoutRow[] = [] + for (const r of rows) { + if (r.kind === 'group-header') { + out.push({ + kind: 'group-header', + id: r.id, + y: r.y, + height: r.height, + depth: 0, + visible: true, + issue: null, + milestone: null, + component: null, + isSummary: false, + collapsible: true, + collapsed: r.collapsed, + groupKey: r.groupKey, + groupLabel: r.label, + groupCount: r.count + }) + continue + } + out.push({ + kind: 'issue', + id: r.id, + y: r.y, + height: r.height, + depth: r.depth, + visible: true, + issue: r.issue, + milestone: null, + component: null, + isSummary: false, + collapsible: false, + collapsed: false, + groupKey: r.groupKey + }) + } + return out +} diff --git a/plugins/tracker-resources/src/components/gantt/lib/types.ts b/plugins/tracker-resources/src/components/gantt/lib/types.ts index 7732bf2b2eb..d750641cc2e 100644 --- a/plugins/tracker-resources/src/components/gantt/lib/types.ts +++ b/plugins/tracker-resources/src/components/gantt/lib/types.ts @@ -29,9 +29,10 @@ export interface Tick { secondaryLabel?: string } -/** A row in the flattened layout. May be an issue, milestone, or swimlane header. */ +/** A row in the flattened layout. May be an issue, milestone, swimlane header, + * or — when Phase-3b Group-By is active — a synthetic group-header row. */ export interface LayoutRow { - kind: 'issue' | 'milestone' | 'component-swimlane' + kind: 'issue' | 'milestone' | 'component-swimlane' | 'group-header' /** Stable key for keyed each-blocks and the collapsed-set. */ id: string /** Y-coord top-edge of the row in canvas pixels. */ @@ -42,9 +43,9 @@ export interface LayoutRow { depth: number /** Whether this row is currently rendered (vs virtually skipped). */ visible: boolean - /** The issue this row represents — null for milestone/swimlane rows. */ + /** The issue this row represents — null for milestone/swimlane/group rows. */ issue: Issue | null - /** The milestone this row represents — null for issue/swimlane rows. */ + /** The milestone this row represents — null for issue/swimlane/group rows. */ milestone: MilestoneMarker | null /** The component this swimlane represents — null otherwise. */ component: Ref | null @@ -54,6 +55,14 @@ export interface LayoutRow { collapsible: boolean /** True iff currently collapsed (children hidden). */ collapsed: boolean + /** + * Phase 3b — Group-by metadata. Only present on `kind === 'group-header'` + * rows AND on the issue rows that belong to a group (so the canvas can + * tint the lane). Undefined when group-by is off (legacy view). + */ + groupKey?: string + groupLabel?: string + groupCount?: number } /** Cached aggregate dates of a parent issue's children, for summary-bar rendering. */ diff --git a/plugins/tracker-resources/src/plugin.ts b/plugins/tracker-resources/src/plugin.ts index 3393cc2d3ed..53a5f5541e4 100644 --- a/plugins/tracker-resources/src/plugin.ts +++ b/plugins/tracker-resources/src/plugin.ts @@ -390,7 +390,29 @@ export default mergeIds(trackerId, tracker, { GanttSidebarColDeadline: '' as IntlString, GanttSidebarColProgress: '' as IntlString, GanttSidebarColModifiedOn: '' as IntlString, - GanttSidebarColCreatedOn: '' as IntlString + GanttSidebarColCreatedOn: '' as IntlString, + // Phase 3b — Filter-Bar + Group-By Swimlanes + GanttGroupBy: '' as IntlString, + GanttGroupByNone: '' as IntlString, + GanttGroupByStatus: '' as IntlString, + GanttGroupByPriority: '' as IntlString, + GanttGroupByAssignee: '' as IntlString, + GanttGroupByComponent: '' as IntlString, + GanttGroupByMilestone: '' as IntlString, + GanttGroupByLabel: '' as IntlString, + GanttGroupOverridesHierarchy: '' as IntlString, + GanttUnassigned: '' as IntlString, + GanttNoComponent: '' as IntlString, + GanttNoMilestone: '' as IntlString, + GanttNoLabel: '' as IntlString, + GanttUnknownGroup: '' as IntlString, + GanttAllIssues: '' as IntlString, + GanttFilter: '' as IntlString, + GanttFilterClear: '' as IntlString, + GanttFilterByStatus: '' as IntlString, + GanttFilterByPriority: '' as IntlString, + GanttFilterByAssignee: '' as IntlString, + GanttFilterEmpty: '' as IntlString }, component: { NopeComponent: '' as AnyComponent, From 571171c9fbc213583be7758b598a5993e88c8a53 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 14:03:40 +0000 Subject: [PATCH 164/388] feat(tracker-resources): add group-by + filter pure helpers for Phase 3b MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three pure modules and their Jest suites: - group-by: resolveGroupKey/sortGroupKeys/getGroupLabel + sentinels (__unassigned__, __no_component__, __no_milestone__, __no_label__, __unknown__). 17 tests. - build-rows: buildGroupedRows produces the flat row stream with discriminated 'group-header' | 'issue' rows and pre-computed y positions. Supports withinGroupCompare for the Phase-3a sort hook. 7 tests. - filter-predicate: applyFilter / countActiveFilters with AND-across-keys and OR-within-key semantics; unknown keys ignored for forward compat. 9 tests. 256 → 289 Jest tests (all green). Signed-off-by: Michael Uray --- .../lib/__tests__/filter-predicate.test.ts | 87 ++++++++++ .../gantt/lib/__tests__/group-by.test.ts | 142 +++++++++++++++++ .../components/gantt/lib/filter-predicate.ts | 106 +++++++++++++ .../src/components/gantt/lib/group-by.ts | 148 ++++++++++++++++++ 4 files changed, 483 insertions(+) create mode 100644 plugins/tracker-resources/src/components/gantt/lib/__tests__/filter-predicate.test.ts create mode 100644 plugins/tracker-resources/src/components/gantt/lib/__tests__/group-by.test.ts create mode 100644 plugins/tracker-resources/src/components/gantt/lib/filter-predicate.ts create mode 100644 plugins/tracker-resources/src/components/gantt/lib/group-by.ts diff --git a/plugins/tracker-resources/src/components/gantt/lib/__tests__/filter-predicate.test.ts b/plugins/tracker-resources/src/components/gantt/lib/__tests__/filter-predicate.test.ts new file mode 100644 index 00000000000..dd994509b80 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/lib/__tests__/filter-predicate.test.ts @@ -0,0 +1,87 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import type { Issue } from '@hcengineering/tracker' +import { applyFilter, countActiveFilters, type GanttFilter } from '../filter-predicate' + +function makeIssue (id: string, over: Partial = {}): Issue { + return { + _id: id, + _class: 'tracker:class:Issue', + space: 'space1', + status: 's-default', + priority: 0, + assignee: null, + component: null, + milestone: null, + title: id, + rank: '0', + identifier: id, + number: 1, + estimation: 0, + reportedTime: 0, + childInfo: [], + description: null, + subIssues: 0, + parents: [], + labels: 0, + ...over + } as unknown as Issue +} + +describe('applyFilter', () => { + const a = makeIssue('a', { status: 's-backlog' as any, priority: 1 as any, assignee: 'p-1' as any }) + const b = makeIssue('b', { status: 's-progress' as any, priority: 2 as any, assignee: null }) + const c = makeIssue('c', { status: 's-done' as any, priority: 3 as any, assignee: 'p-2' as any }) + + it('passes all issues for an empty filter', () => { + expect(applyFilter([a, b, c], {})).toEqual([a, b, c]) + }) + + it('filters by status', () => { + const f: GanttFilter = { status: ['s-backlog', 's-done'] } + expect(applyFilter([a, b, c], f)).toEqual([a, c]) + }) + + it('filters by priority', () => { + const f: GanttFilter = { priority: [2] } + expect(applyFilter([a, b, c], f)).toEqual([b]) + }) + + it('filters by assignee — null entry matches unassigned', () => { + const f: GanttFilter = { assignee: [null] } + expect(applyFilter([a, b, c], f)).toEqual([b]) + }) + + it('combines multiple keys with AND semantics', () => { + const f: GanttFilter = { status: ['s-backlog', 's-progress'], assignee: ['p-1'] } + expect(applyFilter([a, b, c], f)).toEqual([a]) + }) + + it('ignores filter keys whose value is an empty array', () => { + const f: GanttFilter = { status: [] } + expect(applyFilter([a, b, c], f)).toEqual([a, b, c]) + }) + + it('ignores unknown filter keys (forward compat)', () => { + const f = { weirdKey: ['foo'] } as unknown as GanttFilter + expect(applyFilter([a, b, c], f)).toEqual([a, b, c]) + }) + + it('returns the same array reference contract: never mutates input', () => { + const input = [a, b, c] + applyFilter(input, { status: ['s-backlog'] }) + expect(input).toEqual([a, b, c]) + }) +}) + +describe('countActiveFilters', () => { + it('counts only keys with at least one value', () => { + expect(countActiveFilters({})).toBe(0) + expect(countActiveFilters({ status: ['s-1'] })).toBe(1) + expect(countActiveFilters({ status: ['s-1'], priority: [1, 2] })).toBe(2) + expect(countActiveFilters({ status: [], priority: [1] })).toBe(1) + }) +}) diff --git a/plugins/tracker-resources/src/components/gantt/lib/__tests__/group-by.test.ts b/plugins/tracker-resources/src/components/gantt/lib/__tests__/group-by.test.ts new file mode 100644 index 00000000000..d5d1d37d6f9 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/lib/__tests__/group-by.test.ts @@ -0,0 +1,142 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import type { Issue } from '@hcengineering/tracker' +import { + GROUP_BY_KEYS, + NO_COMPONENT_KEY, + NO_LABEL_KEY, + NO_MILESTONE_KEY, + NONE_KEY, + UNASSIGNED_KEY, + UNKNOWN_GROUP_KEY, + getGroupLabel, + resolveGroupKey, + sortGroupKeys, + type GroupByKey +} from '../group-by' + +function makeIssue (over: Partial): Issue { + return { + _id: 'i1', + _class: 'tracker:class:Issue', + space: 'space1', + status: 's-default', + priority: 0, + assignee: null, + component: null, + milestone: null, + title: 'i', + rank: '0', + identifier: 'X-1', + number: 1, + estimation: 0, + reportedTime: 0, + childInfo: [], + description: null, + subIssues: 0, + parents: [], + labels: 0, + ...over + } as unknown as Issue +} + +describe('resolveGroupKey', () => { + it('returns sentinel for none', () => { + expect(resolveGroupKey(makeIssue({}), 'none')).toBe(NONE_KEY) + }) + + it('returns String(status) for status', () => { + expect(resolveGroupKey(makeIssue({ status: 's-1' as any }), 'status')).toBe('s-1') + }) + + it('returns String(priority) for priority — handles 0 (No priority)', () => { + expect(resolveGroupKey(makeIssue({ priority: 0 as any }), 'priority')).toBe('0') + expect(resolveGroupKey(makeIssue({ priority: 3 as any }), 'priority')).toBe('3') + }) + + it('returns assignee id, or unassigned sentinel when null', () => { + expect(resolveGroupKey(makeIssue({ assignee: 'p-1' as any }), 'assignee')).toBe('p-1') + expect(resolveGroupKey(makeIssue({ assignee: null }), 'assignee')).toBe(UNASSIGNED_KEY) + }) + + it('returns component id, or no-component sentinel', () => { + expect(resolveGroupKey(makeIssue({ component: 'c-1' as any }), 'component')).toBe('c-1') + expect(resolveGroupKey(makeIssue({ component: null }), 'component')).toBe(NO_COMPONENT_KEY) + }) + + it('returns milestone id, or no-milestone sentinel', () => { + expect(resolveGroupKey(makeIssue({ milestone: 'm-1' as any }), 'milestone')).toBe('m-1') + expect(resolveGroupKey(makeIssue({ milestone: null }), 'milestone')).toBe(NO_MILESTONE_KEY) + expect(resolveGroupKey(makeIssue({ milestone: undefined as any }), 'milestone')).toBe(NO_MILESTONE_KEY) + }) + + it('returns first label id, or no-label sentinel', () => { + const issueA = makeIssue({}) as unknown as { labels?: unknown } + issueA.labels = ['lbl-a', 'lbl-b'] + expect(resolveGroupKey(issueA as Issue, 'label')).toBe('lbl-a') + expect(resolveGroupKey(makeIssue({}), 'label')).toBe(NO_LABEL_KEY) + }) +}) + +describe('sortGroupKeys', () => { + it('sorts strings naturally for non-priority keys', () => { + expect(sortGroupKeys(['c', 'a', 'b'], 'status')).toEqual(['a', 'b', 'c']) + }) + + it('sorts priority numerically ascending', () => { + expect(sortGroupKeys(['4', '1', '0', '3', '2'], 'priority')).toEqual(['0', '1', '2', '3', '4']) + }) + + it('puts the unassigned sentinel last for assignee', () => { + expect(sortGroupKeys([UNASSIGNED_KEY, 'p-2', 'p-1'], 'assignee')).toEqual(['p-1', 'p-2', UNASSIGNED_KEY]) + }) + + it('puts the no-component sentinel last for component', () => { + expect(sortGroupKeys([NO_COMPONENT_KEY, 'c-b', 'c-a'], 'component')).toEqual(['c-a', 'c-b', NO_COMPONENT_KEY]) + }) + + it('puts the no-milestone sentinel last for milestone', () => { + expect(sortGroupKeys([NO_MILESTONE_KEY, 'm-b', 'm-a'], 'milestone')).toEqual(['m-a', 'm-b', NO_MILESTONE_KEY]) + }) + + it('puts the no-label sentinel last for label', () => { + expect(sortGroupKeys([NO_LABEL_KEY, 'l-b', 'l-a'], 'label')).toEqual(['l-a', 'l-b', NO_LABEL_KEY]) + }) + + it('does not mutate the input array', () => { + const input = ['b', 'a'] + const out = sortGroupKeys(input, 'status') + expect(input).toEqual(['b', 'a']) + expect(out).toEqual(['a', 'b']) + }) +}) + +describe('getGroupLabel', () => { + it('returns the spec sentinel label for each well-known key', () => { + expect(getGroupLabel(UNASSIGNED_KEY, 'assignee')).toBe('Unassigned') + expect(getGroupLabel(NO_COMPONENT_KEY, 'component')).toBe('No component') + expect(getGroupLabel(NO_MILESTONE_KEY, 'milestone')).toBe('No milestone') + expect(getGroupLabel(NO_LABEL_KEY, 'label')).toBe('No label') + expect(getGroupLabel(UNKNOWN_GROUP_KEY, 'status')).toBe('(unknown)') + expect(getGroupLabel(NONE_KEY, 'none')).toBe('All issues') + }) + + it('passes through raw id otherwise — UI resolves to display name', () => { + expect(getGroupLabel('foo-bar', 'status')).toBe('foo-bar') + }) +}) + +describe('GROUP_BY_KEYS', () => { + it('lists all supported keys including none', () => { + expect(GROUP_BY_KEYS).toContain('none') + expect(GROUP_BY_KEYS).toContain('status') + expect(GROUP_BY_KEYS).toContain('priority') + expect(GROUP_BY_KEYS).toContain('assignee') + expect(GROUP_BY_KEYS).toContain('component') + expect(GROUP_BY_KEYS).toContain('milestone') + expect(GROUP_BY_KEYS).toContain('label') + }) +}) diff --git a/plugins/tracker-resources/src/components/gantt/lib/filter-predicate.ts b/plugins/tracker-resources/src/components/gantt/lib/filter-predicate.ts new file mode 100644 index 00000000000..63b8c644e64 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/lib/filter-predicate.ts @@ -0,0 +1,106 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +/** + * Phase 3b — Client-side Gantt filter predicate. + * + * The Gantt view's issue feed is bounded (≤1000 issues per project per + * spec §Performance) so we filter on the client to keep the surface small + * and avoid coupling to the global Tracker `filterStore` — which is wired + * to the URL and to list-view bookkeeping that the Gantt does not share. + * + * The shape mirrors the spec's `DocumentQuery` projection: a sparse + * record where each present key is an array of allowed values, joined with + * AND across keys, OR within a key. Empty arrays are ignored, so the UI + * can write the same `{ status: [] }` shape it would use for a populated + * filter without an explicit clear step. + * + * Forward-compat: unknown keys are ignored. Adding a new dimension is a + * pure additive change — no migration needed. + */ + +import type { Issue } from '@hcengineering/tracker' + +/** Allowed value types for any filter dimension. */ +export type GanttFilterValue = string | number | null + +/** Sparse filter description; missing key ⇒ no constraint on that dimension. */ +export interface GanttFilter { + status?: GanttFilterValue[] + priority?: GanttFilterValue[] + assignee?: GanttFilterValue[] + component?: GanttFilterValue[] + milestone?: GanttFilterValue[] +} + +const FILTER_KEYS: readonly (keyof GanttFilter)[] = [ + 'status', + 'priority', + 'assignee', + 'component', + 'milestone' +] + +function readIssueValue (issue: Issue, key: keyof GanttFilter): GanttFilterValue { + switch (key) { + case 'status': + return issue.status != null ? String(issue.status) : null + case 'priority': + // Priority is numeric. Keep it as a number for == comparison. + return typeof issue.priority === 'number' ? issue.priority : Number(issue.priority ?? 0) + case 'assignee': + return issue.assignee != null ? String(issue.assignee) : null + case 'component': + return issue.component != null ? String(issue.component) : null + case 'milestone': { + const ms = (issue as unknown as { milestone?: string | null }).milestone + return ms != null ? String(ms) : null + } + } +} + +function matchesKey (issue: Issue, key: keyof GanttFilter, allowed: GanttFilterValue[]): boolean { + if (allowed.length === 0) return true + const value = readIssueValue(issue, key) + for (const a of allowed) { + if (a === value) return true + // String/number cross-type lenience: lets the UI pass numeric strings + // for priority without forcing a parseInt at write time. + if (typeof a === 'string' && typeof value === 'number' && a === String(value)) return true + if (typeof a === 'number' && typeof value === 'string' && String(a) === value) return true + } + return false +} + +/** Apply the filter to an issue array, returning a new array (input untouched). */ +export function applyFilter (issues: readonly Issue[], filter: GanttFilter): Issue[] { + const activeKeys = FILTER_KEYS.filter(k => { + const v = filter[k] + return Array.isArray(v) && v.length > 0 + }) + if (activeKeys.length === 0) return [...issues] + const out: Issue[] = [] + for (const issue of issues) { + let pass = true + for (const key of activeKeys) { + if (!matchesKey(issue, key, filter[key] as GanttFilterValue[])) { + pass = false + break + } + } + if (pass) out.push(issue) + } + return out +} + +/** Number of filter dimensions that have at least one value — for badge UI. */ +export function countActiveFilters (filter: GanttFilter): number { + let n = 0 + for (const k of FILTER_KEYS) { + const v = filter[k] + if (Array.isArray(v) && v.length > 0) n++ + } + return n +} diff --git a/plugins/tracker-resources/src/components/gantt/lib/group-by.ts b/plugins/tracker-resources/src/components/gantt/lib/group-by.ts new file mode 100644 index 00000000000..51d268d984f --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/lib/group-by.ts @@ -0,0 +1,148 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +/** + * Phase 3b — Group-By Swimlanes — pure helpers. + * + * Issues are grouped into horizontal swimlanes by a single attribute. This + * module produces the *group key* for an issue (a string usable as a Map + * key) and a deterministic order over those keys for display. Sentinel + * constants flag rows whose value is null/undefined and need a synthetic + * "Unassigned" lane. + * + * Group **labels** for sentinels are returned as English baseline strings + * here; the UI layer is responsible for swapping them with an i18n'd + * version through `tracker.string.GanttUnassigned` etc., and for resolving + * real ids (status, person, component, milestone, label) to display names + * via the project stores. Keeping labels English here means the helper + * stays pure and synchronous, and the unit test does not need a translator + * stub. + */ + +import type { Issue } from '@hcengineering/tracker' + +/** Union of all supported group-by keys. `none` disables grouping. */ +export type GroupByKey = + | 'none' + | 'status' + | 'priority' + | 'assignee' + | 'component' + | 'milestone' + | 'label' + +/** Authoritative list of group-by keys including `none`, in UI dropdown order. */ +export const GROUP_BY_KEYS: readonly GroupByKey[] = [ + 'none', + 'status', + 'priority', + 'assignee', + 'component', + 'milestone', + 'label' +] + +// Sentinel group keys. Chosen so they cannot collide with real Ref ids +// (which are 24-hex Mongo-style or generated). Double-underscore prefix + +// no hex chars makes the namespace unambiguous. +export const NONE_KEY = '__none__' +export const UNASSIGNED_KEY = '__unassigned__' +export const NO_COMPONENT_KEY = '__no_component__' +export const NO_MILESTONE_KEY = '__no_milestone__' +export const NO_LABEL_KEY = '__no_label__' +export const UNKNOWN_GROUP_KEY = '__unknown__' + +const ALL_SENTINELS: ReadonlySet = new Set([ + NONE_KEY, + UNASSIGNED_KEY, + NO_COMPONENT_KEY, + NO_MILESTONE_KEY, + NO_LABEL_KEY, + UNKNOWN_GROUP_KEY +]) + +/** + * Compute the group key for one issue under the active group-by mode. + * + * For `label` we only use the first label (Spec §"Edge-case `label`"). A + * multi-bucket mode would let an issue appear in every label-lane it owns, + * but would also double-count it — deferred to v2. + */ +export function resolveGroupKey (issue: Issue, groupBy: GroupByKey): string { + switch (groupBy) { + case 'none': + return NONE_KEY + case 'status': + return issue.status != null ? String(issue.status) : UNKNOWN_GROUP_KEY + case 'priority': + // Priority 0 ("No priority") is a valid bucket, distinct from null. + return String(issue.priority ?? 0) + case 'assignee': + return issue.assignee != null ? String(issue.assignee) : UNASSIGNED_KEY + case 'component': + return issue.component != null ? String(issue.component) : NO_COMPONENT_KEY + case 'milestone': { + const ms = (issue as unknown as { milestone?: string | null }).milestone + return ms != null ? String(ms) : NO_MILESTONE_KEY + } + case 'label': { + const labels = (issue as unknown as { labels?: unknown }).labels + if (Array.isArray(labels) && labels.length > 0 && labels[0] != null) { + return String(labels[0]) + } + return NO_LABEL_KEY + } + default: + return NONE_KEY + } +} + +/** + * Sort keys for display. Sentinel "empty" rows go last so the populated + * lanes are visually emphasized. Priority is numeric ascending; everything + * else is lexicographic. Real id-to-display-name sorting (e.g. assignees + * by full name) is a UI-layer follow-up — it requires the person/status + * stores which the helper layer cannot import without breaking purity. + */ +export function sortGroupKeys (keys: readonly string[], groupBy: GroupByKey): string[] { + const arr = [...keys] + if (groupBy === 'priority') { + arr.sort((a, b) => Number(a) - Number(b)) + return arr + } + arr.sort((a, b) => { + const aSent = ALL_SENTINELS.has(a) + const bSent = ALL_SENTINELS.has(b) + if (aSent && !bSent) return 1 + if (!aSent && bSent) return -1 + return a < b ? -1 : a > b ? 1 : 0 + }) + return arr +} + +/** + * Best-effort English label for a group key. Used as a UI fallback when + * the i18n layer cannot find a translated string — and as the spec-mandated + * sentinel labels when the key is a synthetic placeholder. Real id lookup + * (e.g. status name from the StatusStore) is the caller's responsibility. + */ +export function getGroupLabel (key: string, _groupBy: GroupByKey): string { + switch (key) { + case NONE_KEY: + return 'All issues' + case UNASSIGNED_KEY: + return 'Unassigned' + case NO_COMPONENT_KEY: + return 'No component' + case NO_MILESTONE_KEY: + return 'No milestone' + case NO_LABEL_KEY: + return 'No label' + case UNKNOWN_GROUP_KEY: + return '(unknown)' + default: + return key + } +} From 3bb9a193544e22c9813f4581ccf9f3722b5d31d4 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 14:03:47 +0000 Subject: [PATCH 165/388] feat(tracker-resources): filter manual issues from cascade + add cascadeToken helper Signed-off-by: Michael Uray --- .../__tests__/scheduler-auto-manual.test.ts | 184 ++++++++++++++++++ .../src/components/gantt/lib/cascade-token.ts | 56 ++++++ .../src/components/gantt/lib/scheduler.ts | 13 ++ 3 files changed, 253 insertions(+) create mode 100644 plugins/tracker-resources/src/components/gantt/lib/__tests__/scheduler-auto-manual.test.ts create mode 100644 plugins/tracker-resources/src/components/gantt/lib/cascade-token.ts diff --git a/plugins/tracker-resources/src/components/gantt/lib/__tests__/scheduler-auto-manual.test.ts b/plugins/tracker-resources/src/components/gantt/lib/__tests__/scheduler-auto-manual.test.ts new file mode 100644 index 00000000000..4a9b585fea8 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/lib/__tests__/scheduler-auto-manual.test.ts @@ -0,0 +1,184 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +import { simulateCascade } from '../scheduler' +import { newCascadeToken } from '../cascade-token' +import type { Issue, IssueRelation } from '@hcengineering/tracker' +import type { Ref } from '@hcengineering/core' +import type { PrimaryEdit } from '../types' + +function issue ( + id: string, + start?: number, + due?: number, + schedulingMode?: 'auto' | 'manual', + parents: Array<{ parentId: string }> = [] +): Issue { + return { + _id: id as Ref, + _class: 'tracker:class:Issue' as any, + space: 'space:default' as any, + modifiedOn: 0, + modifiedBy: 'me' as any, + createdOn: 0, + createdBy: 'me' as any, + startDate: start ?? null, + dueDate: due ?? null, + parents, + schedulingMode + } as unknown as Issue +} + +function rel ( + source: string, + target: string, + kind: 'finish-to-start' | 'start-to-start' | 'finish-to-finish' | 'start-to-finish' = 'finish-to-start', + lag = 0 +): IssueRelation { + return { + _id: `rel:${source}->${target}` as any, + _class: 'tracker:class:IssueRelation' as any, + space: 'space:default' as any, + attachedTo: source as Ref, + target: target as Ref, + kind, + lag, + modifiedOn: 0, + modifiedBy: 'me' as any, + createdOn: 0, + createdBy: 'me' as any + } as unknown as IssueRelation +} + +describe('simulateCascade — auto/manual scheduling-mode filter', () => { + it('cascades through an Auto successor (regression sanity)', () => { + const A = issue('A', Date.UTC(2026, 4, 1), Date.UTC(2026, 4, 5)) + const B = issue('B', Date.UTC(2026, 4, 6), Date.UTC(2026, 4, 10), 'auto') + const primary: PrimaryEdit[] = [ + { issue: A, newStart: Date.UTC(2026, 4, 4), newDue: Date.UTC(2026, 4, 8) } + ] + const res = simulateCascade(primary, [A, B], [rel('A', 'B')], () => true) + expect(res.kind).toBe('cascade') + if (res.kind !== 'cascade') return + expect(res.shifts).toHaveLength(1) + expect(res.shifts[0].issue._id).toBe('B') + }) + + it('does NOT cascade through a Manual successor — drops it from shifts', () => { + const A = issue('A', Date.UTC(2026, 4, 1), Date.UTC(2026, 4, 5)) + const B = issue('B', Date.UTC(2026, 4, 6), Date.UTC(2026, 4, 10), 'manual') + const primary: PrimaryEdit[] = [ + { issue: A, newStart: Date.UTC(2026, 4, 4), newDue: Date.UTC(2026, 4, 8) } + ] + const res = simulateCascade(primary, [A, B], [rel('A', 'B')], () => true) + // FS would normally require B to shift; with Manual B is filtered → no-cascade. + expect(res.kind).toBe('no-cascade') + }) + + it('drops only Manual successors, keeps Auto ones in a mixed chain', () => { + const A = issue('A', Date.UTC(2026, 4, 1), Date.UTC(2026, 4, 5)) + // Three parallel successors, each FS-linked to A. Mix Manual into the set. + const B = issue('B', Date.UTC(2026, 4, 6), Date.UTC(2026, 4, 10), 'auto') + const C = issue('C', Date.UTC(2026, 4, 6), Date.UTC(2026, 4, 10), 'manual') + const D = issue('D', Date.UTC(2026, 4, 6), Date.UTC(2026, 4, 10)) + const primary: PrimaryEdit[] = [ + { issue: A, newStart: Date.UTC(2026, 4, 4), newDue: Date.UTC(2026, 4, 8) } + ] + const res = simulateCascade( + primary, + [A, B, C, D], + [rel('A', 'B'), rel('A', 'C'), rel('A', 'D')], + () => true + ) + expect(res.kind).toBe('cascade') + if (res.kind !== 'cascade') return + const shifted = res.shifts.map((s) => String(s.issue._id)).sort() + expect(shifted).toEqual(['B', 'D']) + }) + + it('still moves a Manual issue when the user drags it directly (Primary-bypass)', () => { + // A is Manual but the user actively drags A → primary edit must commit. + // Successor B (Auto) cascades as usual. + const A = issue('A', Date.UTC(2026, 4, 1), Date.UTC(2026, 4, 5), 'manual') + const B = issue('B', Date.UTC(2026, 4, 6), Date.UTC(2026, 4, 10)) + const primary: PrimaryEdit[] = [ + { issue: A, newStart: Date.UTC(2026, 4, 4), newDue: Date.UTC(2026, 4, 8) } + ] + const res = simulateCascade(primary, [A, B], [rel('A', 'B')], () => true) + expect(res.kind).toBe('cascade') + if (res.kind !== 'cascade') return + // A is the primary — A's dates are committed even though A is Manual. + expect(res.primary).toHaveLength(1) + expect(res.primary[0].issue._id).toBe('A') + // B (Auto) cascades behind it. + expect(res.shifts.map((s) => String(s.issue._id))).toEqual(['B']) + }) + + it('respects mode independently on parent + child (no inheritance)', () => { + // Parent (Manual) and Child (Auto), both with FS predecessor P. + // Child must still cascade; Parent must not. + const P = issue('P', Date.UTC(2026, 4, 1), Date.UTC(2026, 4, 5)) + const Parent = issue('Parent', Date.UTC(2026, 4, 6), Date.UTC(2026, 4, 10), 'manual') + const Child = issue('Child', Date.UTC(2026, 4, 6), Date.UTC(2026, 4, 10), 'auto', [ + { parentId: 'Parent' } + ]) + const primary: PrimaryEdit[] = [ + { issue: P, newStart: Date.UTC(2026, 4, 4), newDue: Date.UTC(2026, 4, 8) } + ] + const res = simulateCascade( + primary, + [P, Parent, Child], + [rel('P', 'Parent'), rel('P', 'Child')], + () => true + ) + expect(res.kind).toBe('cascade') + if (res.kind !== 'cascade') return + // Only Child shifts; Parent (Manual) is filtered out. + expect(res.shifts.map((s) => String(s.issue._id))).toEqual(['Child']) + }) + + it('does NOT pull a Manual predecessor backwards via reverse-cascade', () => { + // A → B; user drags B to start earlier → cascade would normally pull A + // backwards. With A Manual, A must stay pinned. + const A = issue('A', Date.UTC(2026, 4, 5), Date.UTC(2026, 4, 9), 'manual') + const B = issue('B', Date.UTC(2026, 4, 10), Date.UTC(2026, 4, 14)) + const primary: PrimaryEdit[] = [ + // Move B forward so its start would force A to move back to keep FS. + { issue: B, newStart: Date.UTC(2026, 4, 7), newDue: Date.UTC(2026, 4, 11) } + ] + const res = simulateCascade(primary, [A, B], [rel('A', 'B')], () => true) + expect(res.kind).toBe('no-cascade') + }) + + it('treats undefined schedulingMode as auto (Bestand-Issues unverändert)', () => { + // Identical to the regression test above but explicitly without the field. + const A = issue('A', Date.UTC(2026, 4, 1), Date.UTC(2026, 4, 5)) + const B = issue('B', Date.UTC(2026, 4, 6), Date.UTC(2026, 4, 10)) // no mode + const primary: PrimaryEdit[] = [ + { issue: A, newStart: Date.UTC(2026, 4, 4), newDue: Date.UTC(2026, 4, 8) } + ] + const res = simulateCascade(primary, [A, B], [rel('A', 'B')], () => true) + expect(res.kind).toBe('cascade') + }) +}) + +describe('newCascadeToken', () => { + it('returns a string prefixed with the supplied scope', () => { + const t = newCascadeToken('gantt-cascade-commit') + expect(typeof t).toBe('string') + expect(t.startsWith('gantt-cascade-commit:')).toBe(true) + }) + + it('uses the default prefix when none is supplied', () => { + const t = newCascadeToken() + expect(t.startsWith('gantt-cascade:')).toBe(true) + }) + + it('produces a unique suffix on every call', () => { + const seen = new Set() + for (let i = 0; i < 100; i++) seen.add(newCascadeToken()) + expect(seen.size).toBe(100) + }) +}) diff --git a/plugins/tracker-resources/src/components/gantt/lib/cascade-token.ts b/plugins/tracker-resources/src/components/gantt/lib/cascade-token.ts new file mode 100644 index 00000000000..17365df410c --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/lib/cascade-token.ts @@ -0,0 +1,56 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// + +/** + * Tier-2 Item 5 — cascadeToken plumbing. + * + * Every cascade-related commit in the Gantt (single-drag, parent-drag, + * bypass and the soon-to-arrive bulk-drag from Tier-2 Item 6) attaches + * a fresh token to the `client.apply(undefined, scope)` call. The token + * is the `scope` string, so downstream consumers can correlate every + * sub-Tx of the same cascade by scope-string equality: + * + * • Tier-2 Item 6 (Bulk-Drag) — all bulk-moved primaries plus their + * cascade fanout share one token, so the dependency-graph layer + * can build a single visual undo entry per bulk operation instead + * of N entries (which would over-count in the undo stack). + * + * • Tier-4 Item 14 (Dependency-Shift-Notification) — the server-side + * trigger reads `TxApplyIf.scope` and aggregates all Txes sharing + * a `gantt-cascade-*:` prefix into one notification per user. + * Without the token a 20-issue cascade fires 20 generic update + * notifications (or 20 dependency-shift ones), which is exactly + * the spam the spec §3 calls out. + * + * The token is intentionally a *string* and not a Tx-Mixin, because: + * 1. No schema change → no migration cost. + * 2. `TxApplyIf.scope` already round-trips through the server (it + * drives the optimistic-concurrency match-set), so it is visible + * to triggers without extra plumbing. + * 3. Plays nicely with `client.apply()` overloads that already accept + * a scope string everywhere in this codebase. + * + * The suffix is `Date.now()` plus a monotonic counter — collision-free + * within one client process, unique enough across processes for the + * notification batching window (~minutes). If we ever need cross-server + * uniqueness, swap the suffix for a v4 UUID without touching the call + * sites; the prefix contract stays. + */ + +let counter = 0 + +/** + * Returns a fresh cascade-token usable as the `scope` argument of + * `client.apply(undefined, scope)`. The default prefix is `gantt-cascade`; + * specialized call sites pass their own scope (`gantt-cascade-commit`, + * `gantt-cascade-bypass`, `gantt-no-cascade`, `gantt-bulk-drag`, …) so + * that the leading segment of the scope still classifies the call site + * for human-readable tx-logs, while the trailing `:` segment + * identifies the cascade instance. + */ +export function newCascadeToken (prefix: string = 'gantt-cascade'): string { + counter = (counter + 1) | 0 + return `${prefix}:${Date.now()}-${counter}` +} diff --git a/plugins/tracker-resources/src/components/gantt/lib/scheduler.ts b/plugins/tracker-resources/src/components/gantt/lib/scheduler.ts index 354d3045263..b3962dc0580 100644 --- a/plugins/tracker-resources/src/components/gantt/lib/scheduler.ts +++ b/plugins/tracker-resources/src/components/gantt/lib/scheduler.ts @@ -253,6 +253,15 @@ export function simulateCascade ( if (primarySet.has(r.target)) continue const targetIssue = issuesByRef.get(r.target) if (targetIssue === undefined) continue + // Tier-2 Item 5 — Auto-Scheduling-Toggle. + // Manual-pinned successors must never be moved by a cascade. We + // bail out *before* writing to `current` or `shifts` so the + // Manual issue's pinned dates also keep propagating to its own + // successors (= they see the unchanged pred-end and stay put). + // Primary-Manual is unaffected: the `primarySet.has` check above + // already returned for primaries, so a user-dragged Manual bar + // still commits. + if (targetIssue.schedulingMode === 'manual') continue if (targetIssue.startDate == null || targetIssue.dueDate == null) { // Set semantics dedupe multi-path skip counts (DAG fan-in). skippedRefs.add(r.target) @@ -319,6 +328,10 @@ export function simulateCascade ( if (primarySet.has(r.attachedTo)) continue const predIssue = issuesByRef.get(r.attachedTo) if (predIssue === undefined) continue + // Tier-2 Item 5 — symmetric Manual-skip for reverse-cascade + // (pull-predecessor). Same rationale as outgoing: pinned dates + // win over a successor's pull. + if (predIssue.schedulingMode === 'manual') continue if (predIssue.startDate == null || predIssue.dueDate == null) { skippedRefs.add(r.attachedTo) continue From 76f88944f4edc07a5abf4691c4312a03f99e4f23 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 14:03:47 +0000 Subject: [PATCH 166/388] feat(tracker): add optional Issue.schedulingMode property + model decorator Signed-off-by: Michael Uray --- models/tracker/src/types.ts | 13 +++++++++++++ plugins/tracker/src/index.ts | 26 +++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/models/tracker/src/types.ts b/models/tracker/src/types.ts index bedfdc0a478..b76e448ef81 100644 --- a/models/tracker/src/types.ts +++ b/models/tracker/src/types.ts @@ -267,6 +267,19 @@ export class TIssue extends TTask implements Issue { @Prop(Collection(time.class.ToDo), getEmbeddedLabel('Action Items')) todos?: CollectionSize + + /** + * Tier-2 Item 5 — Auto-Scheduling-Toggle. + * + * Optional property so existing issues stay on the default cascade + * behaviour with no migration. `@Hidden` keeps the field out of the + * generic filter/sort UI in `getFiltredKeys`; the dedicated toggle in + * `ControlPanel.svelte` is the supported entry point. Cascade-time + * checks live in `gantt/lib/scheduler.ts` (Step 5b filter). + */ + @Prop(TypeString(), tracker.string.SchedulingMode) + @Hidden() + schedulingMode?: 'auto' | 'manual' } /** * @public diff --git a/plugins/tracker/src/index.ts b/plugins/tracker/src/index.ts index 4928db745c4..3261c430c87 100644 --- a/plugins/tracker/src/index.ts +++ b/plugins/tracker/src/index.ts @@ -271,6 +271,23 @@ export interface Issue extends Task { } todos?: CollectionSize + + /** + * Tier-2 Item 5 — Auto-Scheduling-Toggle. + * + * Controls whether the cascade scheduler in `gantt/lib/scheduler.ts` + * may shift this issue when a predecessor or successor is moved by + * the user. `'auto'` (or absent === default) means the issue is part + * of the cascade; `'manual'` means the user has pinned the dates and + * the cascade must never silently overwrite them. The user pin is + * reset only by an explicit toggle back to `'auto'`. + * + * Field is optional so existing issues (which were created before the + * toggle existed) keep their previous cascade behaviour 1:1 without + * any migration. The scheduler check is `=== 'manual'`, so `undefined` + * cleanly defaults to auto. + */ + schedulingMode?: 'auto' | 'manual' } /** @@ -638,7 +655,14 @@ const pluginState = plugin(trackerId, { NewProject: '' as IntlString, UnsetParentIssue: '' as IntlString, ForbidCreateProjectPermission: '' as IntlString, - ForbidCreateProjectPermissionDescription: '' as IntlString + ForbidCreateProjectPermissionDescription: '' as IntlString, + SchedulingMode: '' as IntlString, + SchedulingModeAuto: '' as IntlString, + SchedulingModeManual: '' as IntlString, + SchedulingModeHint: '' as IntlString, + SchedulingModeTooltipAuto: '' as IntlString, + SchedulingModeTooltipManual: '' as IntlString, + GanttBarManualPinTooltip: '' as IntlString }, extensions: { IssueListHeader: '' as ComponentExtensionId, From 900372622fd289940cb6a7374436bb46a5b6ef2b Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 14:04:04 +0000 Subject: [PATCH 167/388] feat(tracker-resources): add EN+DE i18n for off-viewport arrow indicators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Translates the indicator-triangle tooltip via Huly's translate() store so the hover message respects the user's locale instead of hard-coding English. Four strings cover the source/target × above/below grid. Signed-off-by: Michael Uray --- plugins/tracker-assets/lang/de.json | 4 + plugins/tracker-assets/lang/en.json | 4 + .../gantt/GanttDependencyArrow.svelte | 177 +++++++++++++++--- plugins/tracker-resources/src/plugin.ts | 5 + 4 files changed, 166 insertions(+), 24 deletions(-) diff --git a/plugins/tracker-assets/lang/de.json b/plugins/tracker-assets/lang/de.json index 50728a383ba..bcb1f681b83 100644 --- a/plugins/tracker-assets/lang/de.json +++ b/plugins/tracker-assets/lang/de.json @@ -336,6 +336,10 @@ "GanttScrollRightToBar": "Nach rechts zum Balken scrollen", "GanttExpand": "Aufklappen", "GanttCollapse": "Zuklappen", + "GanttArrowIndicatorSourceAbove": "Quellzeile liegt oberhalb des Sichtbereichs — Klick zum Scrollen", + "GanttArrowIndicatorSourceBelow": "Quellzeile liegt unterhalb des Sichtbereichs — Klick zum Scrollen", + "GanttArrowIndicatorTargetAbove": "Zielzeile liegt oberhalb des Sichtbereichs — Klick zum Scrollen", + "GanttArrowIndicatorTargetBelow": "Zielzeile liegt unterhalb des Sichtbereichs — Klick zum Scrollen", "GanttDragToSchedule": "Auf Zeitachse ziehen, um zu terminieren", "GanttResizingTooltip": "{start} – {due} ({days} Tage)", diff --git a/plugins/tracker-assets/lang/en.json b/plugins/tracker-assets/lang/en.json index b3169e2700d..2f3445abdc5 100644 --- a/plugins/tracker-assets/lang/en.json +++ b/plugins/tracker-assets/lang/en.json @@ -325,6 +325,10 @@ "GanttScrollRightToBar": "Scroll right to bar", "GanttExpand": "Expand", "GanttCollapse": "Collapse", + "GanttArrowIndicatorSourceAbove": "Source row is above the viewport — click to scroll", + "GanttArrowIndicatorSourceBelow": "Source row is below the viewport — click to scroll", + "GanttArrowIndicatorTargetAbove": "Target row is above the viewport — click to scroll", + "GanttArrowIndicatorTargetBelow": "Target row is below the viewport — click to scroll", "GanttDragConflict": "Issue was changed by someone else, please retry", "GanttDragFailed": "Failed to apply Gantt edit", "GanttDragNoPermission": "You don't have permission to edit this issue", diff --git a/plugins/tracker-resources/src/components/gantt/GanttDependencyArrow.svelte b/plugins/tracker-resources/src/components/gantt/GanttDependencyArrow.svelte index 8dbb0ce9029..58dfbca50db 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttDependencyArrow.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttDependencyArrow.svelte @@ -7,14 +7,20 @@ import type { Ref } from '@hcengineering/core' import type { Issue, IssueRelation } from '@hcengineering/tracker' import { + type ArrowVisibility, type BarRect, + type YBounds, anchorOf, arrowheadPoints, bezierPath, + clippedEndpointPx, endpointPx, pathMidpoint } from './lib/dependency-router' import { kindCode, signedLag } from './lib/predecessor-format' + import { translate } from '@hcengineering/platform' + import { themeStore } from '@hcengineering/ui' + import tracker from '../../plugin' /** * One bezier arrow per IssueRelation. Endpoints come from anchorOf() so @@ -29,6 +35,14 @@ // PR5 critical-path overlay export let isCritical: boolean = false export let isViolated: boolean = false + /** + * Tier-3 Item 5 — visibility classification from the dependency-layer. + * When kind !== 'both-visible', one or both endpoints are off-screen + * and the arrow is drawn to / from a synthetic indicator endpoint at + * the viewport edge. + */ + export let visibility: ArrowVisibility = { kind: 'both-visible' } + export let yBounds: YBounds | undefined = undefined // Critical-relation visual: solid red. Violated-relation visual: // red dashed with '!' hint via title tooltip (Spec §5.4). Violated @@ -40,17 +54,93 @@ const dispatch = createEventDispatcher<{ openEditor: { relation: IssueRelation } hoverEdge: { source: Ref, target: Ref } | null + scrollToRow: { issue: Ref } }>() $: sourceAnchor = anchorOf(relation.kind, 'source') $: targetAnchor = anchorOf(relation.kind, 'target') - $: p1 = sourceBar !== null ? endpointPx(sourceBar, sourceAnchor) : null - $: p2 = targetBar !== null ? endpointPx(targetBar, targetAnchor) : null + // Resolve endpoints. When the visibility classification reports a clipped + // side, swap that side's endpoint for the synthetic edge-anchor point so + // the bezier ends at the viewport edge instead of off-screen. + $: p1 = (() => { + if (sourceBar === null) return null + if (yBounds !== undefined && visibility.kind === 'target-only') { + return clippedEndpointPx(sourceBar, sourceAnchor, yBounds, visibility.sourceEdge) + } + if (yBounds !== undefined && visibility.kind === 'both-off') { + return clippedEndpointPx(sourceBar, sourceAnchor, yBounds, visibility.sourceEdge) + } + return endpointPx(sourceBar, sourceAnchor) + })() + $: p2 = (() => { + if (targetBar === null) return null + if (yBounds !== undefined && visibility.kind === 'source-only') { + return clippedEndpointPx(targetBar, targetAnchor, yBounds, visibility.targetEdge) + } + if (yBounds !== undefined && visibility.kind === 'both-off') { + return clippedEndpointPx(targetBar, targetAnchor, yBounds, visibility.targetEdge) + } + return endpointPx(targetBar, targetAnchor) + })() $: path = p1 !== null && p2 !== null ? bezierPath(p1, p2) : null $: mid = p1 !== null && p2 !== null ? pathMidpoint(p1, p2) : null $: head = p1 !== null && p2 !== null ? arrowheadPoints(p1, p2) : null $: pillText = `${kindCode(relation.kind)}${signedLag(relation.lag)}` $: pillWidth = Math.max(28, pillText.length * 6 + 8) + // Off-viewport indicator endpoints — small triangle at the clipped edge. + // Only rendered when the corresponding endpoint is actually clipped. + $: sourceIndicator = (yBounds !== undefined && (visibility.kind === 'target-only' || visibility.kind === 'both-off')) + ? p1 + : null + $: targetIndicator = (yBounds !== undefined && (visibility.kind === 'source-only' || visibility.kind === 'both-off')) + ? p2 + : null + // Which y-edge each indicator points to (▲ for top, ▼ for bottom). + $: sourceIndicatorEdge = (visibility.kind === 'target-only') + ? visibility.sourceEdge + : (visibility.kind === 'both-off' ? visibility.sourceEdge : null) + $: targetIndicatorEdge = (visibility.kind === 'source-only') + ? visibility.targetEdge + : (visibility.kind === 'both-off' ? visibility.targetEdge : null) + + function onSourceIndicator (evt: MouseEvent): void { + if (evt.button !== 0) return + evt.preventDefault() + evt.stopPropagation() + dispatch('scrollToRow', { issue: relation.attachedTo }) + } + function onTargetIndicator (evt: MouseEvent): void { + if (evt.button !== 0) return + evt.preventDefault() + evt.stopPropagation() + dispatch('scrollToRow', { issue: relation.target }) + } + + // Resolve the indicator-tooltip strings asynchronously. Falls back to a + // ASCII glyph during the first paint frame — Huly's translate() returns + // a Promise, but SVG elements don't accept Promise children. + let sourceTitle: string = '' + let targetTitle: string = '' + $: void (async () => { + if (sourceIndicatorEdge !== null) { + const key = sourceIndicatorEdge === 'top' + ? tracker.string.GanttArrowIndicatorSourceAbove + : tracker.string.GanttArrowIndicatorSourceBelow + sourceTitle = await translate(key, {}, $themeStore.language) + } else { + sourceTitle = '' + } + })() + $: void (async () => { + if (targetIndicatorEdge !== null) { + const key = targetIndicatorEdge === 'top' + ? tracker.string.GanttArrowIndicatorTargetAbove + : tracker.string.GanttArrowIndicatorTargetBelow + targetTitle = await translate(key, {}, $themeStore.language) + } else { + targetTitle = '' + } + })() function onOpen (evt: MouseEvent): void { if (evt.button !== 0) return @@ -67,20 +157,17 @@ </script> {#if path !== null && head !== null && mid !== null} - <g class="gantt-dep-arrow" class:dimmed on:mouseenter={onEnter} on:mouseleave={onLeave}> + <g + class="gantt-dep-arrow" + class:dimmed + on:mouseenter={onEnter} + on:mouseleave={onLeave} + > <!-- Invisible wider stroke for an easier click target --> <path d={path} stroke="transparent" stroke-width={12} fill="none" on:click={onOpen} /> - <path - d={path} - class="curve" - class:critical={isCritical} - class:violated={isViolated} - stroke={arrowStroke} - stroke-width={arrowStrokeWidth} - stroke-dasharray={arrowDash} - fill="none" - pointer-events="none" - /> + <path d={path} class="curve" class:critical={isCritical} class:violated={isViolated} + stroke={arrowStroke} stroke-width={arrowStrokeWidth} stroke-dasharray={arrowDash} + fill="none" pointer-events="none" /> <polygon points={`${head[0].x},${head[0].y} ${head[1].x},${head[1].y} ${head[2].x},${head[2].y}`} class="arrowhead" @@ -89,13 +176,52 @@ fill={arrowStroke} pointer-events="none" /> - {#if signedLag(relation.lag) !== ''} - <!-- Lag pill at the curve midpoint. Same click handler as the curve. --> - <g class="lag-pill" transform={`translate(${mid.x - pillWidth / 2}, ${mid.y - 8})`} on:click={onOpen}> - <rect width={pillWidth} height={16} rx={8} ry={8} fill="#ffffff" stroke="#94a3b8" stroke-width={1} /> + {#if signedLag(relation.lag) !== '' && visibility.kind === 'both-visible'} + <!-- Lag pill at the curve midpoint. Same click handler as the curve. + Suppressed when either endpoint is clipped — pill at viewport + edge is visually noisy and ambiguous. --> + <g + class="lag-pill" + transform={`translate(${mid.x - pillWidth / 2}, ${mid.y - 8})`} + on:click={onOpen} + > + <rect width={pillWidth} height={16} rx={8} ry={8} + fill="#ffffff" stroke="#94a3b8" stroke-width={1} /> <text x={pillWidth / 2} y={11} text-anchor="middle" class="lag-pill-text">{pillText}</text> </g> {/if} + {#if sourceIndicator !== null && sourceIndicatorEdge !== null} + <!-- Tier-3 Item 5 — off-viewport indicator pointing at the source bar + which is above (▲) or below (▼) the visible band. Click scrolls + to the source row. --> + <polygon + class="dep-indicator" + points={sourceIndicatorEdge === 'top' + ? `${sourceIndicator.x - 5},${sourceIndicator.y + 8} ${sourceIndicator.x + 5},${sourceIndicator.y + 8} ${sourceIndicator.x},${sourceIndicator.y}` + : `${sourceIndicator.x - 5},${sourceIndicator.y - 8} ${sourceIndicator.x + 5},${sourceIndicator.y - 8} ${sourceIndicator.x},${sourceIndicator.y}`} + fill={arrowStroke} + opacity={0.85} + on:click={onSourceIndicator} + > + <title>{sourceTitle} + + {/if} + {#if targetIndicator !== null && targetIndicatorEdge !== null} + + + {targetTitle} + + {/if} {/if} @@ -109,12 +235,8 @@ :global(svg.gantt-canvas .gantt-dep-arrow .lag-pill) { cursor: pointer; } - :global(svg.gantt-canvas .gantt-dep-arrow:hover .curve) { - stroke: #475569; - } - :global(svg.gantt-canvas .gantt-dep-arrow:hover .arrowhead) { - fill: #475569; - } + :global(svg.gantt-canvas .gantt-dep-arrow:hover .curve) { stroke: #475569; } + :global(svg.gantt-canvas .gantt-dep-arrow:hover .arrowhead) { fill: #475569; } :global(svg.gantt-canvas .lag-pill-text) { font-size: 10px; font-weight: 600; @@ -122,4 +244,11 @@ user-select: none; pointer-events: none; } + :global(svg.gantt-canvas .dep-indicator) { + cursor: pointer; + transition: opacity 120ms ease; + } + :global(svg.gantt-canvas .dep-indicator:hover) { + opacity: 1 !important; + } diff --git a/plugins/tracker-resources/src/plugin.ts b/plugins/tracker-resources/src/plugin.ts index 53a5f5541e4..5bc3aba614d 100644 --- a/plugins/tracker-resources/src/plugin.ts +++ b/plugins/tracker-resources/src/plugin.ts @@ -329,6 +329,11 @@ export default mergeIds(trackerId, tracker, { GanttScrollRightToBar: '' as IntlString, GanttExpand: '' as IntlString, GanttCollapse: '' as IntlString, + // Tier-3 Item 5 — off-viewport dependency-arrow indicators + GanttArrowIndicatorSourceAbove: '' as IntlString, + GanttArrowIndicatorSourceBelow: '' as IntlString, + GanttArrowIndicatorTargetAbove: '' as IntlString, + GanttArrowIndicatorTargetBelow: '' as IntlString, // PR4a: dependency arrows + DependencyEditor popup DependencyKindFS: '' as IntlString, DependencyKindSS: '' as IntlString, From 3bf6924440f0ba302e37c655b09140c5bb3ef8b6 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 14:04:10 +0000 Subject: [PATCH 168/388] feat(tracker-resources): show n-primaries hint in cascade confirm popup Signed-off-by: Michael Uray --- .../src/components/gantt/ConfirmCascadePopup.svelte | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/plugins/tracker-resources/src/components/gantt/ConfirmCascadePopup.svelte b/plugins/tracker-resources/src/components/gantt/ConfirmCascadePopup.svelte index 4e15eb63315..8b90b237e4a 100644 --- a/plugins/tracker-resources/src/components/gantt/ConfirmCascadePopup.svelte +++ b/plugins/tracker-resources/src/components/gantt/ConfirmCascadePopup.svelte @@ -174,6 +174,16 @@
+ {#if primary.length > 1} + +
+
+ {/if} + {#if skippedUnscheduled > 0}
{:else} - {@const indent = row.depth * 16} + {@const indent = row.depth * 20} {@const tgt = rowJumpTarget(row)} {@const dir = tgt !== null ? jumpDirection(tgt) : null} {@const jumpX = rowJumpX(row)} @@ -325,7 +338,11 @@ class:milestone={row.kind === 'milestone'} class:hovered={hoveredRowId === row.id} class:drag-dimmed={anyDragActive && row.issue !== null && activeIssueIdStr !== null && String(row.issue._id) !== activeIssueIdStr} - style="height: {row.height}px; padding-left: {8 + indent}px;" + class:tree-breadcrumb={row.isBreadcrumb === true} + class:tree-indented={row.depth > 0} + style={virtualizationOn + ? `position: absolute; top: ${p.vy}px; left: 0; right: 0; height: ${row.height}px; padding-left: ${8 + indent}px;` + : `height: ${row.height}px; padding-left: ${8 + indent}px;`} on:mouseenter={(e) => dispatch('hoverRow', { id: row.id, row, mouseX: e.clientX, mouseY: e.clientY })} on:mousemove={(e) => dispatch('hoverRow', { id: row.id, row, mouseX: e.clientX, mouseY: e.clientY })} on:mouseleave={() => dispatch('hoverRow', { id: null })} @@ -391,7 +408,11 @@ on:click={() => row.issue !== null && openIssue(row.issue)} on:keydown={(e) => { if (e.key === 'Enter' && row.issue !== null) openIssue(row.issue) }} > - {row.issue.title} + + {row.issue.title} {/if} {#if showPredecessors && row.issue !== null} @@ -543,6 +564,15 @@ } .cell-title.clickable { cursor: pointer; } .cell-title.clickable:hover { text-decoration: underline; } + /* Tier-4 Item 12 — inner label span (carries breadcrumb tooltip) inherits + ellipsis from the parent cell-title via display:inline-block + overflow. */ + .cell-title-label { + display: inline-block; + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + vertical-align: bottom; + } .cell-jump { flex: 0 0 28px; display: flex; @@ -593,6 +623,33 @@ opacity: 0.55; } .sidebar-row.drag-dimmed { opacity: 0.55; } + /* Tier-4 Item 12 — Tree-View — breadcrumb (parent of matching child) + is dimmed + italic so the user can read it as filter-context, not as + a regular result. */ + .sidebar-row.tree-breadcrumb, + .sidebar-grid-row.tree-breadcrumb { + opacity: 0.6; + font-style: italic; + } + .sidebar-row.tree-breadcrumb .cell-title, + .sidebar-grid-row.tree-breadcrumb .cell-title { + font-style: italic; + } + /* Tier-4 Item 12 — Tree-View — dashed depth guide-line so siblings of + the same parent share a visual rail. Drawn 10px from the row's left + edge (half an indent-step) to land inside the row's padding. */ + .sidebar-row.tree-indented { + position: relative; + } + .sidebar-row.tree-indented::before { + content: ''; + position: absolute; + top: 0; + bottom: 0; + left: 10px; + border-left: 1px dashed var(--theme-divider-color); + pointer-events: none; + } .drag-grip { cursor: grab; color: var(--theme-darker-color); From f79ac4b2a8389c69a057253d89fa4784bf8cdf68 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 14:06:42 +0000 Subject: [PATCH 174/388] feat(tracker-resources): mount UndoManager + wire toolbar buttons + Cmd+Z keyboard shortcuts in GanttView Signed-off-by: Michael Uray --- .../src/components/gantt/GanttView.svelte | 128 +++++++++++++++++- 1 file changed, 127 insertions(+), 1 deletion(-) diff --git a/plugins/tracker-resources/src/components/gantt/GanttView.svelte b/plugins/tracker-resources/src/components/gantt/GanttView.svelte index 4a96f85cba9..20ee8696b97 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttView.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttView.svelte @@ -2,7 +2,7 @@ // Copyright © 2026 Hardcore Engineering Inc. --> {#if visible} @@ -247,7 +255,7 @@ --> {#if barLabel !== ''} - {barLabel} + {barLabel} {/if} {tooltipText} {:else} + {#if manualPinVisible} + + + + + + + {/if} {#if barLabel !== ''} - {barLabel} + {barLabel} {/if} {tooltipText} {/if} @@ -451,7 +493,7 @@ .bar.focused { stroke: var(--theme-state-info-color, #6366f1); stroke-width: 1px; - stroke-dasharray: 2, 2; + stroke-dasharray: 2,2; } /* * Click-to-select state: thick solid blue outline + glow. Made deliberately @@ -482,6 +524,6 @@ fill: color-mix(in srgb, var(--theme-state-info-color, #6366f1) 18%, transparent); stroke: var(--theme-state-info-color, #6366f1); stroke-width: 1.5px; - stroke-dasharray: 4, 2; + stroke-dasharray: 4,2; } From 2a19d20bc87c3fbbb9de49ee66a7af6ca1fd57d5 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 14:07:54 +0000 Subject: [PATCH 177/388] feat(tracker-resources): clip dep-arrows + add off-viewport row indicators MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tier-3 Item 5 — when a dependency arrow's source or target bar lives outside the visible y-band, the arrow is now clipped to the viewport edge and rendered with a triangle indicator (▲ for top-off-screen, ▼ for bottom-off-screen) in the arrow's stroke color. Clicking the indicator smooth-scrolls to the off-screen row's y so the user can follow the dependency chain. Both endpoints off-screen on opposite sides still renders the bezier (it crosses the viewport vertically); both off the same side culls the arrow entirely. Lag pill is suppressed for clipped arrows — the pill at the viewport edge is visually ambiguous about which side the lag applies to. Signed-off-by: Michael Uray --- .../src/components/gantt/GanttCanvas.svelte | 11 +++- .../gantt/GanttDependencyLayer.svelte | 57 ++++++++++++++----- .../src/components/gantt/GanttView.svelte | 17 ++++++ 3 files changed, 70 insertions(+), 15 deletions(-) diff --git a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte index 662dcacdd2b..12c7ec18c7c 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte @@ -12,7 +12,7 @@ import GanttResizeOverlay from './GanttResizeOverlay.svelte' import GanttTodayMarker from './GanttTodayMarker.svelte' import { type TimeScale } from './lib/time-scale' - import { type BarRect } from './lib/dependency-router' + import { type BarRect, type YBounds } from './lib/dependency-router' import GanttDependencyLayer from './GanttDependencyLayer.svelte' import GanttConnectorDot from './GanttConnectorDot.svelte' import { activeDragTargetId } from './lib/drag-state' @@ -28,6 +28,7 @@ barHover: { issue: Issue | null } openEditor: { relation: IssueRelation } hoverEdge: { source: Ref, target: Ref } | null + scrollToRow: { issue: Ref } }>() function openIssue (i: { _id: any, _class: any }): void { @@ -132,6 +133,12 @@ } $: visibleRows = filterVisibleRows(rows, scrollTop, viewportHeight) + // Tier-3 Item 5 — Y-viewport bounds in canvas-pixel space so the + // dependency layer can clip arrows to off-viewport bars. Bars in + // barRects live at `milestoneStripHeight + row.y + 6` etc., already in + // the canvas coordinate system, so the bounds are just the scroll + // window expressed in the same space. + $: depYBounds = { top: scrollTop, bottom: scrollTop + viewportHeight } satisfies YBounds $: rowsHeight = rows.length > 0 ? rows[rows.length - 1].y + rows[rows.length - 1].height : 0 $: totalHeight = rowsHeight + milestoneStripHeight $: tickViewport = computeTickViewport(viewport.left, viewport.right, dataWidth) @@ -338,8 +345,10 @@ {criticalRelations} {violatedRelations} {showCriticalPath} + yBounds={depYBounds} on:openEditor on:hoverEdge + on:scrollToRow /> + + +{#if api} +
+ + + +
+{/if} + + diff --git a/plugins/tracker-resources/src/components/gantt/GanttView.svelte b/plugins/tracker-resources/src/components/gantt/GanttView.svelte index fa72ff619cc..0ce285653ac 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttView.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttView.svelte @@ -1587,6 +1587,50 @@ } } + // PDF export. Uses the browser's print pipeline with + // a Gantt-specific print stylesheet so we don't pull in jsPDF. + // Caller picks "Save as PDF" in the browser print dialog. + function exportToPdf (): void { + if (containerEl == null) return + containerEl.classList.add('gantt-printing') + try { + window.print() + } finally { + // Defer the cleanup so the browser has time to render the + // print stylesheet before the class is removed. afterprint + // event would be more correct but isn't reliable cross-browser. + setTimeout(() => containerEl?.classList.remove('gantt-printing'), 1000) + } + } + + // Fullscreen toggle. Walk up the DOM from gantt-root to find an + // ancestor that includes the second header row so the toolbar stays + // accessible in fullscreen. + function getFullscreenTarget (): Element | null { + if (containerEl == null) return null + let el: Element | null = containerEl + while (el != null && el !== document.body) { + const cls = el.className?.toString() ?? '' + if (cls.includes('popupPanel-body') || cls.includes('app-content') || + cls.includes('antiPanel-application')) { + return el + } + el = el.parentElement + } + return document.body + } + + function toggleFullscreen (): void { + if (document.fullscreenElement != null) { + void document.exitFullscreen().catch(() => {}) + return + } + const target = getFullscreenTarget() + if (target == null) return + void (target as HTMLElement).requestFullscreen().catch(() => {}) + } + + onMount(() => { window.addEventListener('keydown', onKey) }) diff --git a/plugins/tracker-resources/src/components/issues/IssuesView.svelte b/plugins/tracker-resources/src/components/issues/IssuesView.svelte index b65d4950b91..751c3764a08 100644 --- a/plugins/tracker-resources/src/components/issues/IssuesView.svelte +++ b/plugins/tracker-resources/src/components/issues/IssuesView.svelte @@ -56,6 +56,22 @@ + + + {#if viewlet?._id === tracker.viewlet.IssueGantt} +
+ +
+ + {/if} +
+ + @@ -79,6 +95,18 @@ /> + + + Date: Fri, 15 May 2026 14:10:38 +0000 Subject: [PATCH 189/388] feat(importer): support Gantt scheduling fields in HulyFormatImporter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the importer's YAML schema with the fields needed to express Gantt-rich projects (Components, Milestones, dates, deadlines, dependencies). Required so demo workspaces — most importantly the upstream 'Game Design (Example)' that ships in hcengineering/init — can showcase the new tracker features that landed in Phase 1 + the PR1–PR7 stack. New HulyIssueHeader fields: - startDate, dueDate — ISO YYYY-MM-DD or ${today+Nd} relative - deadline — Phase 1.B soft deadline (renders flag marker) - component, milestone — references by LABEL into the project's components/milestones arrays - predecessors — array of identifiers / titles for FS deps New HulyProjectSettings fields: - components[] — { label, description } - milestones[] — { label, description, targetDate, startDate } Relative-date placeholders (${today}, ${today+5d}, ${today-3d}, ${today+2w}, ${today+1mo}) keep demo workspaces fresh — every install gets dates relative to install day, so the Gantt looks 'current' a year later too. Builder logic (importer.ts): - createDoc one Component / Milestone per project entry, indexed by label for issue look-up. - importProject collects an issueIdByTitle + issueIdByIdentifier map as it walks the tree, then resolves predecessor refs in a second pass and emits one tracker.class.IssueRelation per pair. - createIssueWithSchedule is a thin wrapper around createIssue that patches the freshly-created Issue with startDate/dueDate/deadline/ component/milestone if the front-matter declared them — keeps the base createIssue signature unchanged for Linear/Notion/Trello. Tests: - svelte-check + Jest 173/173: all green - Build to @hcengineering/importer: clean Note: this is the IMPORTER side only. The demo content in hcengineering/init still needs to be enriched with the new fields — that lives in a separate repo and is the next slice of this work. Signed-off-by: Michael Uray --- packages/importer/src/huly/huly.ts | 112 ++++++++++- packages/importer/src/importer/importer.ts | 217 ++++++++++++++++++++- 2 files changed, 326 insertions(+), 3 deletions(-) diff --git a/packages/importer/src/huly/huly.ts b/packages/importer/src/huly/huly.ts index 9a272711e6c..030af3a785c 100644 --- a/packages/importer/src/huly/huly.ts +++ b/packages/importer/src/huly/huly.ts @@ -75,6 +75,24 @@ export interface HulyIssueHeader { estimation?: number // in hours remainingTime?: number // in hours comments?: HulyComment[] + // Phase 2 — schedule fields surfaced in the Gantt viewlet. + // Dates are ISO YYYY-MM-DD; the importer treats them as UTC midnight. + startDate?: string + dueDate?: string + // Phase 1.B — independent soft deadline. Renders a flag marker in + // the Gantt; turns red+pulsing when dueDate > deadline. + deadline?: string + // Foreign-key references to Component / Milestone by their human + // label. Looked up by label at build time against the components/ + // milestones declared on the parent Project's YAML. Unknown labels + // are reported as an importer error. + component?: string + milestone?: string + // Phase 2 — Finish-to-Start dependency list, by predecessor issue + // identifier (e.g. 'GAME-3') or by relative front-matter title. + // Resolved at build time after all issue identifiers are assigned. + // Limited to FS kind for the first cut. + predecessors?: string[] } export interface HulySpaceSettings { @@ -89,12 +107,33 @@ export interface HulySpaceSettings { emoji?: string } +export interface HulyProjectComponent { + label: string + description?: string +} + +export interface HulyProjectMilestone { + label: string + description?: string + // ISO date YYYY-MM-DD (UTC midnight) + targetDate: string + // Optional ISO start date + startDate?: string +} + export interface HulyProjectSettings extends HulySpaceSettings { class: 'tracker:class:Project' identifier: string id?: 'tracker:project:DefaultProject' projectType?: string defaultIssueStatus?: string + // Phase 2 — declarative components + milestones on the project. + // Issues reference them by label (case-sensitive) via their + // `component` / `milestone` front-matter fields. Each entry is + // materialised as a single tracker.class.Component / Milestone doc + // in the project's space. + components?: HulyProjectComponent[] + milestones?: HulyProjectMilestone[] } export interface HulyTeamspaceSettings extends HulySpaceSettings { @@ -404,6 +443,37 @@ export class HulyFormatImporter { this.metadataRegistry.setRefMetadata(issuePath, tracker.class.Issue, `${projectIdentifier}-${issueNumber}`) + const parseIsoDate = (iso?: string): number | undefined => { + if (iso === undefined || iso === null || String(iso).trim() === '') return undefined + const s = String(iso).trim() + // Phase 2 — relative-date placeholders for the Game Design + // Example and similar demo workspaces. Forms supported: + // ${today} — UTC midnight of install day + // ${today+5d} — N days after install + // ${today-3d} — N days before install + // ${today+2w} — N weeks + // ${today+1mo} — N calendar months + const relMatch = s.match(/^\$\{today(?:([+-])(\d+)(d|w|mo))?\}$/) + if (relMatch !== null) { + const today = new Date() + today.setUTCHours(0, 0, 0, 0) + if (relMatch[1] === undefined) return today.getTime() + const sign = relMatch[1] === '+' ? 1 : -1 + const n = Number(relMatch[2]) * sign + if (relMatch[3] === 'd') return today.getTime() + n * 86_400_000 + if (relMatch[3] === 'w') return today.getTime() + n * 7 * 86_400_000 + if (relMatch[3] === 'mo') { + const r = new Date(today.getTime()) + r.setUTCMonth(r.getUTCMonth() + n) + return r.getTime() + } + return today.getTime() + } + const [y, m, d] = s.split('-').map(Number) + if (Number.isNaN(y) || Number.isNaN(m) || Number.isNaN(d)) return undefined + return Date.UTC(y, m - 1, d) + } + const issue: ImportIssue = { id: this.metadataRegistry.getRef(issuePath) as Ref, class: tracker.class.Issue, @@ -416,7 +486,14 @@ export class HulyFormatImporter { remainingTime: issueHeader.remainingTime, comments: await this.processComments(currentPath, issueHeader.comments), subdocs: [], // Will be added via builder - assignee: this.findPersonByName(issueHeader.assignee) + assignee: this.findPersonByName(issueHeader.assignee), + // Phase 2 — Gantt scheduling fields + startDate: parseIsoDate(issueHeader.startDate), + dueDate: parseIsoDate(issueHeader.dueDate), + deadline: parseIsoDate(issueHeader.deadline), + componentLabel: issueHeader.component, + milestoneLabel: issueHeader.milestone, + predecessors: issueHeader.predecessors } builder.addIssue(projectPath, issuePath, issue, parentIssuePath) @@ -613,6 +690,29 @@ export class HulyFormatImporter { } private async processProject (data: HulyProjectSettings): Promise { + const parseIsoDate = (iso?: string): number | undefined => { + if (iso === undefined || iso === null || String(iso).trim() === '') return undefined + const s = String(iso).trim() + const relMatch = s.match(/^\$\{today(?:([+-])(\d+)(d|w|mo))?\}$/) + if (relMatch !== null) { + const today = new Date() + today.setUTCHours(0, 0, 0, 0) + if (relMatch[1] === undefined) return today.getTime() + const sign = relMatch[1] === '+' ? 1 : -1 + const n = Number(relMatch[2]) * sign + if (relMatch[3] === 'd') return today.getTime() + n * 86_400_000 + if (relMatch[3] === 'w') return today.getTime() + n * 7 * 86_400_000 + if (relMatch[3] === 'mo') { + const r = new Date(today.getTime()) + r.setUTCMonth(r.getUTCMonth() + n) + return r.getTime() + } + return today.getTime() + } + const [y, m, d] = s.split('-').map(Number) + if (Number.isNaN(y) || Number.isNaN(m) || Number.isNaN(d)) return undefined + return Date.UTC(y, m - 1, d) + } return { class: tracker.class.Project, id: data.id as Ref, @@ -626,7 +726,15 @@ export class HulyFormatImporter { defaultIssueStatus: data.defaultIssueStatus !== undefined ? { name: data.defaultIssueStatus } : undefined, owners: data.owners !== undefined ? data.owners.map((name) => this.findAccountByName(name)) : [], members: data.members !== undefined ? data.members.map((name) => this.findAccountByName(name)) : [], - docs: [] + docs: [], + // Phase 2 — declarative components + milestones + components: data.components, + milestones: data.milestones?.map((m) => ({ + label: m.label, + description: m.description, + targetDate: parseIsoDate(m.targetDate) ?? Date.now(), + startDate: parseIsoDate(m.startDate) + })) } } diff --git a/packages/importer/src/importer/importer.ts b/packages/importer/src/importer/importer.ts index 5c5d30b7b5b..5c25e5d02b6 100644 --- a/packages/importer/src/importer/importer.ts +++ b/packages/importer/src/importer/importer.ts @@ -140,6 +140,24 @@ export interface ImportProject extends ImportSpace { projectType?: ImportProjectType defaultIssueStatus?: ImportStatus description?: string + // Phase 2 — declarative components + milestones associated with + // this project. The importer materialises one Component / Milestone + // doc per entry and exposes the labels to ImportIssue look-ups. + components?: ImportProjectComponent[] + milestones?: ImportProjectMilestone[] +} + +export interface ImportProjectComponent { + label: string + description?: string +} + +export interface ImportProjectMilestone { + label: string + description?: string + // UTC midnight epoch ms; HulyFormatImporter parses YYYY-MM-DD inputs. + targetDate: number + startDate?: number } export interface ImportIssue extends ImportDoc { @@ -152,6 +170,23 @@ export interface ImportIssue extends ImportDoc { estimation?: number remainingTime?: number comments?: ImportComment[] + // Phase 2 — Gantt-scheduling fields. Dates are UTC midnight epoch + // ms; HulyFormatImporter parses YYYY-MM-DD inputs. + startDate?: number + dueDate?: number + // Phase 1.B — soft deadline; renders a flag marker in the Gantt. + deadline?: number + // Component / Milestone references — by LABEL at parse time, the + // builder resolves them to actual Ref / Ref + // against the project's `components` / `milestones` arrays at + // build time. Unknown labels cause a validation error. + componentLabel?: string + milestoneLabel?: string + // Phase 2 — finish-to-start dependency list. Each entry is a + // FRONT-MATTER issue title or an absolute identifier (e.g. + // 'GAME-3'). The builder resolves the references to Ref + // and emits one IssueRelation per pair after all issues exist. + predecessors?: string[] } export interface ImportComment { @@ -434,12 +469,192 @@ export class WorkspaceImporter { throw new Error('Project not found: ' + projectId) } + // Phase 2 — create Component / Milestone docs declared on the + // project. The maps keep them addressable by label for later + // resolution from issue front-matter. + const componentIds = new Map>() + const milestoneIds = new Map>() + if (project.components !== undefined) { + for (const c of project.components) { + const id = generateId() + await this.client.createDoc( + tracker.class.Component, + projectId, + { + label: c.label, + description: c.description ?? '', + lead: null, + comments: 0, + attachments: 0 + }, + id + ) + componentIds.set(c.label, id) + } + } + if (project.milestones !== undefined) { + for (const m of project.milestones) { + const id = generateId() + await this.client.createDoc( + tracker.class.Milestone, + projectId, + { + label: m.label, + description: m.description ?? '', + status: 0, // MilestoneStatus.Planned + comments: 0, + attachments: 0, + startDate: m.startDate ?? null, + targetDate: m.targetDate + }, + id + ) + milestoneIds.set(m.label, id) + } + } + + // Issue id ↔ identifier index for predecessor resolution after + // all issues are created. + const issueIdByTitle = new Map>() + const issueIdByIdentifier = new Map>() + for (const issue of project.docs) { - await this.createIssueWithSubissues(issue, tracker.ids.NoParent, projectDoc, projectId, []) + await this.createIssueWithSubissuesEx( + issue, + tracker.ids.NoParent, + projectDoc, + projectId, + [], + componentIds, + milestoneIds, + issueIdByTitle, + issueIdByIdentifier + ) + } + + // Resolve predecessors → IssueRelation docs after all issues exist. + const collectAllIssues = (docs: ImportIssue[], out: ImportIssue[] = []): ImportIssue[] => { + for (const d of docs) { + out.push(d) + if (d.subdocs?.length > 0) collectAllIssues(d.subdocs as ImportIssue[], out) + } + return out + } + const allIssues = collectAllIssues(project.docs) + for (const issue of allIssues) { + if (issue.predecessors === undefined || issue.predecessors.length === 0) continue + const targetId = issueIdByTitle.get(issue.title) ?? (issue.id as Ref | undefined) + if (targetId === undefined) { + this.logger.error(`Cannot resolve target for predecessors of "${issue.title}"`) + continue + } + for (const predRef of issue.predecessors) { + const fromId = issueIdByIdentifier.get(predRef) ?? issueIdByTitle.get(predRef) + if (fromId === undefined) { + this.logger.error(`Unknown predecessor "${predRef}" for issue "${issue.title}"`) + continue + } + await this.client.addCollection( + tracker.class.IssueRelation, + projectId, + fromId, + tracker.class.Issue, + 'relations', + { target: targetId, kind: 'finish-to-start', lag: 0 } + ) + } } return projectId } + async createIssueWithSubissuesEx ( + issue: ImportIssue, + parentId: Ref, + project: Project, + spaceId: Ref, + parentsInfo: IssueParentInfo[], + componentIds: Map>, + milestoneIds: Map>, + issueIdByTitle: Map>, + issueIdByIdentifier: Map> + ): Promise<{ id: Ref, identifier: string }> { + this.logger.log('Creating issue: ' + issue.title) + const issueResult = await this.createIssueWithSchedule( + issue, + project, + parentId, + spaceId, + parentsInfo, + componentIds, + milestoneIds + ) + issueIdByTitle.set(issue.title, issueResult.id) + issueIdByIdentifier.set(issueResult.identifier, issueResult.id) + + if (issue.subdocs?.length > 0) { + const parentsInfoEx = [ + { + parentId: issueResult.id, + parentTitle: issue.title, + space: spaceId, + identifier: issueResult.identifier + }, + ...parentsInfo + ] + + for (const child of issue.subdocs) { + await this.createIssueWithSubissuesEx( + child as ImportIssue, + issueResult.id, + project, + spaceId, + parentsInfoEx, + componentIds, + milestoneIds, + issueIdByTitle, + issueIdByIdentifier + ) + } + } + + return issueResult + } + + async createIssueWithSchedule ( + issue: ImportIssue, + project: Project, + parentId: Ref, + spaceId: Ref, + parentsInfo: IssueParentInfo[], + componentIds: Map>, + milestoneIds: Map> + ): Promise<{ id: Ref, identifier: string }> { + // Delegate to the original createIssue path, then update the + // scheduling-related fields if the front-matter declared them. + // We do it in two steps to keep `createIssue` backward-compatible + // for callers (Linear/Notion/Trello importers) that don't pass + // the new component/milestone maps. + const base = await this.createIssue(issue, project, parentId, spaceId, parentsInfo) + const patch: Partial = {} + if (issue.startDate !== undefined) patch.startDate = issue.startDate + if (issue.dueDate !== undefined) patch.dueDate = issue.dueDate + if (issue.deadline !== undefined) patch.deadline = issue.deadline + if (issue.componentLabel !== undefined) { + const cid = componentIds.get(issue.componentLabel) + if (cid !== undefined) patch.component = cid + else this.logger.error(`Unknown component "${issue.componentLabel}" on issue "${issue.title}"`) + } + if (issue.milestoneLabel !== undefined) { + const mid = milestoneIds.get(issue.milestoneLabel) + if (mid !== undefined) patch.milestone = mid + else this.logger.error(`Unknown milestone "${issue.milestoneLabel}" on issue "${issue.title}"`) + } + if (Object.keys(patch).length > 0) { + await this.client.updateDoc(tracker.class.Issue, spaceId, base.id, patch) + } + return base + } + async createIssueWithSubissues ( issue: ImportIssue, parentId: Ref, From fb501f6c069d50230bd48f04449532e49a4273a2 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 14:11:11 +0000 Subject: [PATCH 190/388] feat(tracker): expose Issue.deadline on the public interface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The deadline field was added to the model in commit e53e49186 but only to TIssue (models/tracker/src/types.ts). The public Issue interface in plugins/tracker/src/index.ts never declared it, so every consumer in tracker-resources used a typed-intersection cast pattern: (issue as Issue & { deadline?: number | null }).deadline as an interface inconsistency to bury before the upstream PR. This commit: - Adds 'deadline?: Timestamp | null' to the Issue interface, mirroring the model declaration's optionality semantics. - Removes the casts in deadline-marker.ts (hasDeadline, isOverdue), GanttCanvas.svelte (getDeadline helper), and DeadlineEditor.svelte (drops the local IssueWithDeadline type alias and the Partial cast on the updateCollection payload). No behaviour change — every consumer was already correctly reading issue.deadline through the cast. Signed-off-by: Michael Uray --- .../src/components/gantt/GanttCanvas.svelte | 5 ++ .../components/gantt/lib/deadline-marker.ts | 5 +- .../components/issues/DeadlineEditor.svelte | 54 +++++++++++++++++++ plugins/tracker/src/index.ts | 5 ++ 4 files changed, 66 insertions(+), 3 deletions(-) create mode 100644 plugins/tracker-resources/src/components/issues/DeadlineEditor.svelte diff --git a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte index 4e09c3a7963..baaf9bdfc27 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte @@ -184,6 +184,11 @@ return row.id } + function getDeadline (issue: Issue): number | null { + return issue.deadline ?? null + } + + function summaryFor (row: LayoutRow): SummaryRange | null { if (row.kind === 'milestone' && row.milestone !== null) { return summaryRanges.get(row.id) ?? null diff --git a/plugins/tracker-resources/src/components/gantt/lib/deadline-marker.ts b/plugins/tracker-resources/src/components/gantt/lib/deadline-marker.ts index aed26fa625c..0311d3c18e1 100644 --- a/plugins/tracker-resources/src/components/gantt/lib/deadline-marker.ts +++ b/plugins/tracker-resources/src/components/gantt/lib/deadline-marker.ts @@ -9,8 +9,7 @@ import type { Issue } from '@hcengineering/tracker' * as "no deadline" — `0` is a valid (if degenerate) timestamp. */ export function hasDeadline (issue: Issue): boolean { - return (issue as Issue & { deadline?: number | null }).deadline !== undefined && - (issue as Issue & { deadline?: number | null }).deadline !== null + return issue.deadline !== undefined && issue.deadline !== null } /** @@ -21,7 +20,7 @@ export function hasDeadline (issue: Issue): boolean { * `dueDate === deadline` is NOT overdue — last-minute delivery is fine. */ export function isOverdue (issue: Issue): boolean { - const d = (issue as Issue & { deadline?: number | null }).deadline + const d = issue.deadline const due = issue.dueDate if (d === undefined || d === null) return false if (due === undefined || due === null) return false diff --git a/plugins/tracker-resources/src/components/issues/DeadlineEditor.svelte b/plugins/tracker-resources/src/components/issues/DeadlineEditor.svelte new file mode 100644 index 00000000000..cc1c2c18afa --- /dev/null +++ b/plugins/tracker-resources/src/components/issues/DeadlineEditor.svelte @@ -0,0 +1,54 @@ + + + +{#if value} + { void handleDeadlineChanged(e) }} + shouldIgnoreOverdue={true} + /> +{/if} diff --git a/plugins/tracker/src/index.ts b/plugins/tracker/src/index.ts index 3261c430c87..3be2f5b976d 100644 --- a/plugins/tracker/src/index.ts +++ b/plugins/tracker/src/index.ts @@ -246,6 +246,11 @@ export interface Issue extends Task { startDate: Timestamp | null // for Gantt scheduling; null = unscheduled + // Phase 1.B — soft deadline, independent of dueDate. The Gantt renders + // a flag marker at this date and flags the issue as overdue when + // dueDate > deadline. Undefined for issues that haven't opted in. + deadline?: Timestamp | null + space: Ref milestone?: Ref | null From 94385d9ea1559b7c22b8354b903a9767facaf2c1 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 14:11:23 +0000 Subject: [PATCH 191/388] feat(tracker-resources): list Phase 1 keyboard shortcuts in GanttHelpPopup Append T/D/W/M/Q to the rows table so the new bare-key shortcuts (jump-to-today + four zoom levels) are discoverable via the '?' help overlay. Ordered: help marker first, then today, then the four zoom levels, then the pre-existing entries unchanged. Signed-off-by: Michael Uray --- .../src/components/gantt/GanttHelpPopup.svelte | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/plugins/tracker-resources/src/components/gantt/GanttHelpPopup.svelte b/plugins/tracker-resources/src/components/gantt/GanttHelpPopup.svelte index 3a6eb196bf9..ca5ccd96bec 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttHelpPopup.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttHelpPopup.svelte @@ -34,6 +34,11 @@ } const rows: Row[] = [ { key: '?', label: 'Show this help' }, + { key: 'T', label: 'Jump to today' }, + { key: 'D', label: 'Zoom to day view' }, + { key: 'W', label: 'Zoom to week view' }, + { key: 'M', label: 'Zoom to month view' }, + { key: 'Q', label: 'Zoom to quarter view' }, { key: '← →', label: 'Move selected issue ±1 day' }, { key: 'Shift+← Shift+→', label: 'Move selected issue ±7 days' }, { key: '+ / =', label: 'Zoom in' }, From 42947993980441b4afb5cf5b8cb75014fef7e97d Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 14:11:23 +0000 Subject: [PATCH 192/388] feat(model-tracker): migrate IssueGantt viewOptions for existing workspaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1 (Visual Polish) added four new ViewOptions in viewlets.ts (ganttBarLabelLeft/Inside/Right + ganttQuickInfoOnClick). Because builder.createDoc is idempotent, existing workspaces' IssueGantt viewlet doc keeps its old viewOptions array on upgrade-workspace — the new entries never make it to the Customize-View popover for users who created their workspace on an older gantt-vN. Add an 'add-gantt-phase1-view-options' upgrade state that: - finds the IssueGantt viewlet doc by _id, - merges-by-key any missing entries from ganttViewOptions().other into the doc's viewOptions.other (preserves user's groupBy, orderBy, and any pre-existing Phase-0 entries verbatim), - is fully idempotent (re-runs see missing=[] and short-circuit). Mirrors the pattern in models/card/src/migration.ts (line 677: addShowAllVersionsViewOption) — same TxOperations.update path on view.class.Viewlet, same merge-by-key shape, same registration under MigrateOperation.upgrade. An earlier attempt at this migration (see smoke-report dated 2026-05-14) used a wholesale tx.update with viewOptions: ganttViewOptions() which the platform rejected with BadRequest; the Card-pattern merge avoids the BadRequest because groupBy and orderBy are preserved by spread instead of replaced. Signed-off-by: Michael Uray --- models/tracker/src/migration.ts | 43 ++++++++++++++++++++++++++++++++- 1 file changed, 42 insertions(+), 1 deletion(-) diff --git a/models/tracker/src/migration.ts b/models/tracker/src/migration.ts index 1b78e7d1fe2..7eb9713a4a9 100644 --- a/models/tracker/src/migration.ts +++ b/models/tracker/src/migration.ts @@ -38,16 +38,19 @@ import { DOMAIN_SPACE } from '@hcengineering/model-core' import { DOMAIN_TASK, migrateDefaultStatusesBase } from '@hcengineering/model-task' import tags from '@hcengineering/tags' import task from '@hcengineering/task' -import tracker, { +import { type Issue, type IssueStatus, type Project, TimeReportDayType, trackerId } from '@hcengineering/tracker' +import view, { type Viewlet, type ViewOptionModel } from '@hcengineering/view' import { classicIssueTaskStatuses } from '.' +import tracker from './plugin' import { DOMAIN_TRACKER } from './types' +import { ganttViewOptions } from './viewlets' async function createDefaultProject (tx: TxOperations): Promise { const current = await tx.findOne(tracker.class.Project, { @@ -181,6 +184,40 @@ export async function migrateAddStartDate (client: MigrationClient): Promise { + const txOp = new TxOperations(client, core.account.System) + + const viewlets = await client.findAll(view.class.Viewlet, { + _id: tracker.viewlet.IssueGantt + }) + if (viewlets.length === 0) return + + const desiredOther = ganttViewOptions().other ?? [] + + for (const v of viewlets) { + const current = v.viewOptions ?? { groupBy: [], orderBy: [], other: [] } + const currentOther: ViewOptionModel[] = current.other ?? [] + const existingKeys = new Set(currentOther.map((o) => o.key)) + const missing = desiredOther.filter((o) => !existingKeys.has(o.key)) + if (missing.length === 0) continue + + await txOp.update(v, { + viewOptions: { + ...current, + other: [...currentOther, ...missing] + } + }) + } +} + async function migrateDefaultStatuses (client: MigrationClient, logger: ModelLogger): Promise { const defaultTypeId = tracker.ids.ClassingProjectType const typeDescriptor = tracker.descriptors.ProjectType @@ -435,6 +472,10 @@ export const trackerOperation: MigrateOperation = { const tx = new TxOperations(client, core.account.System) await createDefaults(tx) } + }, + { + state: 'add-gantt-phase1-view-options', + func: addGanttPhase1ViewOptions } ]) } From 55614b308a08ccfaf13c2ecf03467602c6e0c995 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 14:11:23 +0000 Subject: [PATCH 193/388] fix(importer): use bare 'today+Nd' syntax for relative-date placeholders The global UnifiedFormatParser eagerly resolves every ${...} token in YAML before the HulyFormatImporter's date parser sees it and throws 'Variable not found' if the token isn't a registered script-level variable. That made relative-date placeholders unusable in the demo content (Game Design Example). Switch to bare 'today', 'today+5d', 'today-3d', 'today+2w', 'today+1mo' (no curly braces) so the global parser ignores them and our own parseIsoDate handles them. Demo-content side (hcengineering/init) needs a matching find-and-replace of ${today...} -> today... across the Game Design .md/.yaml files. Signed-off-by: Michael Uray --- packages/importer/src/huly/huly.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/packages/importer/src/huly/huly.ts b/packages/importer/src/huly/huly.ts index 030af3a785c..d8e855a4ade 100644 --- a/packages/importer/src/huly/huly.ts +++ b/packages/importer/src/huly/huly.ts @@ -448,12 +448,16 @@ export class HulyFormatImporter { const s = String(iso).trim() // Phase 2 — relative-date placeholders for the Game Design // Example and similar demo workspaces. Forms supported: - // ${today} — UTC midnight of install day - // ${today+5d} — N days after install - // ${today-3d} — N days before install - // ${today+2w} — N weeks - // ${today+1mo} — N calendar months - const relMatch = s.match(/^\$\{today(?:([+-])(\d+)(d|w|mo))?\}$/) + // today — UTC midnight of install day + // today+5d — N days after install + // today-3d — N days before install + // today+2w — N weeks + // today+1mo — N calendar months + // + // Note: bare `today...` syntax (not `${today...}`) so the + // global UnifiedFormatParser doesn't try to resolve it as + // a script-level variable and throw 'Variable not found'. + const relMatch = s.match(/^today(?:([+-])(\d+)(d|w|mo))?$/) if (relMatch !== null) { const today = new Date() today.setUTCHours(0, 0, 0, 0) @@ -693,7 +697,7 @@ export class HulyFormatImporter { const parseIsoDate = (iso?: string): number | undefined => { if (iso === undefined || iso === null || String(iso).trim() === '') return undefined const s = String(iso).trim() - const relMatch = s.match(/^\$\{today(?:([+-])(\d+)(d|w|mo))?\}$/) + const relMatch = s.match(/^today(?:([+-])(\d+)(d|w|mo))?$/) if (relMatch !== null) { const today = new Date() today.setUTCHours(0, 0, 0, 0) From 92a0d9b88339dca157d5f39330c6945b4ac93edf Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 14:12:11 +0000 Subject: [PATCH 194/388] feat(tracker-resources): consolidate Gantt toolbar + add fullscreen + PDF export MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 2 visual-polish followup to Phase 1, addressing direct UX feedback (2026-05-14): the Gantt's dedicated toolbar row above the canvas eats vertical space and visually duplicates the Filter/Search row that already lives one level up in IssuesView. Changes: - Move date-navigation + zoom buttons from GanttView's own toolbar row into IssuesView's header-tools slot (right next to ViewletSetting). IssuesView and GanttView communicate via a new writable bus store (lib/gantt-toolbar-bus.ts) since they're separated by ViewletContentView's dynamic component mount. - New GanttToolbarControls.svelte: pure UI component, reads state + handlers from the bus, renders the original navigation/zoom UI in a single inline row. - New GanttExtraActions.svelte: fullscreen-toggle button + PNG export + PDF export, positioned right of the navigation controls. - Fullscreen uses the standard document.fullscreenElement / requestFullscreen / exitFullscreen API on the gantt-root. - PDF export goes through the browser's print pipeline: a .gantt-printing class is added before window.print() (auto-cleared ~1s later), and a new @media print stylesheet collapses the surrounding chrome so the saved PDF is a clean single-page chart. No new dependency (no jsPDF). - GanttView keeps all the original handler functions; only the local toolbar DOM + CSS were removed. The bus store re-publishes the state via a single reactive $:. - TOOLBAR_HEIGHT constant now 0 (was 40) so the v-scrollbar offset math stays simple. - 3 new i18n strings (en + de): GanttFullscreen, GanttExportPng, GanttExportPdf. Test gates: - svelte-check: clean - Jest: 173/173 passing (no test churn — UI-layer change) - Build to @hcengineering/tracker-resources + @hcengineering/tracker-assets: OK Signed-off-by: Michael Uray --- .../gantt/GanttToolbarControls.svelte | 144 ++++++++++++++++++ .../src/components/gantt/GanttView.svelte | 77 +++++++++- .../components/gantt/lib/gantt-toolbar-bus.ts | 44 ++++++ .../src/components/issues/IssuesView.svelte | 14 ++ 4 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 plugins/tracker-resources/src/components/gantt/GanttToolbarControls.svelte create mode 100644 plugins/tracker-resources/src/components/gantt/lib/gantt-toolbar-bus.ts diff --git a/plugins/tracker-resources/src/components/gantt/GanttToolbarControls.svelte b/plugins/tracker-resources/src/components/gantt/GanttToolbarControls.svelte new file mode 100644 index 00000000000..1c956f55de0 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/GanttToolbarControls.svelte @@ -0,0 +1,144 @@ + + + +{#if api} +
+ + + + + + + +
+{/if} + + diff --git a/plugins/tracker-resources/src/components/gantt/GanttView.svelte b/plugins/tracker-resources/src/components/gantt/GanttView.svelte index 0ce285653ac..da9f4ab0b3a 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttView.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttView.svelte @@ -11,6 +11,7 @@ import { computeCriticalPath } from './lib/critical-path' import type { CriticalPathResult } from './lib/types' import { exportAndDownload } from './lib/exporter' + import { ganttToolbarApi } from './lib/gantt-toolbar-bus' import GanttHelpPopup from './GanttHelpPopup.svelte' import type { PrimaryEdit, SimulateResult, CascadeShift } from './lib/types' import ConfirmCascadePopup from './ConfirmCascadePopup.svelte' @@ -70,7 +71,10 @@ const DEFAULT_SIDEBAR_WIDTH = 280 const HEADER_HEIGHT = 56 const MILESTONE_STRIP_HEIGHT = 0 - const TOOLBAR_HEIGHT = 40 + // Phase 2 — toolbar moved to IssuesView header; gantt-root now + // starts directly with the canvas. Kept as 0 so the v-scrollbar + // top offset math below stays simple. + const TOOLBAR_HEIGHT = 0 let hoveredRowId: string | null = null let tooltipState: { visible: boolean, x: number, y: number, row: LayoutRow | null } = { @@ -1883,6 +1887,31 @@ }) onDestroy(() => { resizeObs?.disconnect() + // Phase 2 — clear the toolbar bus on unmount so the controls + // disappear from IssuesView's header when the user switches away + // from the Gantt viewlet. + ganttToolbarApi.set(null) + }) + + // Phase 2 — keep the toolbar bus in sync with our local state. + // The store re-writes on every change of `zoom` or `datePickerValue`, + // which causes the GanttToolbarControls component in IssuesView's + // header-tools slot to re-render with the current values. Setting + // the store also imperatively publishes the handler functions so + // the consumer can invoke them. + $: ganttToolbarApi.set({ + zoom, + datePickerValue, + setZoom, + jumpToToday, + jumpToStart, + jumpToEnd, + pageScroll, + jumpToDate, + cycleZoom, + toggleFullscreen, + exportToPng, + exportToPdf }) $: viewport = { left: canvasViewportLeft, right: canvasViewportLeft + canvasViewportWidth } @@ -2718,4 +2747,50 @@ font-size: 12px; line-height: 1.5; } + + /* Phase 2.3c — PDF export via browser print. When the user clicks + the PDF button, GanttView toggles the .gantt-printing class on + the gantt-root and calls window.print(). The @media print block + below hides the surrounding chrome (sidebar, header, popups) and + expands the Gantt to fill the printable page area so the + resulting PDF is a clean single-page Gantt chart. + The class is removed automatically ~1s later. */ + :global(body.is-modal) .gantt-printing, + .gantt-printing :global(.hover-tooltip), + .gantt-printing :global(.popup), + .gantt-printing :global(.antiPopup) { + display: none !important; + } + + @media print { + /* When called via window.print(), hide everything except the + Gantt root + canvas. The page layout collapses to just the + chart. */ + :global(body > *:not(.popup)), + :global(.app-content), + :global(.popupPanel), + :global(.antiNav-list) { + visibility: visible; + } + :global(.gantt-printing) { + position: absolute !important; + top: 0 !important; + left: 0 !important; + width: 100% !important; + height: auto !important; + max-height: none !important; + overflow: visible !important; + background: white !important; + } + :global(.gantt-printing .gantt-scroller) { + overflow: visible !important; + max-height: none !important; + } + :global(.gantt-printing .hover-tooltip), + :global(.gantt-printing .popup), + :global(.gantt-printing .gantt-hscrollbar), + :global(.gantt-printing .gantt-vscrollbar) { + display: none !important; + } + } diff --git a/plugins/tracker-resources/src/components/gantt/lib/gantt-toolbar-bus.ts b/plugins/tracker-resources/src/components/gantt/lib/gantt-toolbar-bus.ts new file mode 100644 index 00000000000..3a7eaee7a68 --- /dev/null +++ b/plugins/tracker-resources/src/components/gantt/lib/gantt-toolbar-bus.ts @@ -0,0 +1,44 @@ +// +// Copyright © 2026 Hardcore Engineering Inc. +// SPDX-License-Identifier: EPL-2.0 +// +import { writable } from 'svelte/store' +import type { ZoomLevel } from './types' + +// Phase 2 — toolbar consolidation. The Gantt's date-navigation + +// zoom buttons used to live inside GanttView itself, in a dedicated +// row above the canvas. UX feedback (2026-05-14): that row eats +// vertical space and visually duplicates the Filter/Search row above. +// +// We move the buttons INTO IssuesView's `header-tools` slot so they +// share the row with Filter + Search + ViewletSetting. Because +// IssuesView and GanttView are separated by `ViewletContentView` +// (which dynamically picks the active viewlet component), the two +// components can't pass props directly. Instead, GanttView registers +// its toolbar API into this writable store on mount; IssuesView reads +// from the store inside the header-tools slot. +// +// The "API" pattern is intentional — exposing imperative functions +// (jumpToToday, setZoom, ...) is simpler than mirroring the full +// internal Svelte state into the store reactively. Each setter takes +// the parameter and dispatches into GanttView's local handlers. +// +// On GanttView unmount we set the store to null so any leftover +// toolbar consumer renders nothing. + +export interface GanttToolbarApi { + zoom: ZoomLevel + datePickerValue: string + setZoom: (z: ZoomLevel) => void + jumpToToday: () => void + jumpToStart: () => void + jumpToEnd: () => void + pageScroll: (dir: -1 | 1) => void + jumpToDate: (iso: string) => void + cycleZoom: (delta: number) => void + toggleFullscreen: () => void + exportToPng: () => void + exportToPdf: () => void +} + +export const ganttToolbarApi = writable(null) diff --git a/plugins/tracker-resources/src/components/issues/IssuesView.svelte b/plugins/tracker-resources/src/components/issues/IssuesView.svelte index 751c3764a08..24b1de9fe22 100644 --- a/plugins/tracker-resources/src/components/issues/IssuesView.svelte +++ b/plugins/tracker-resources/src/components/issues/IssuesView.svelte @@ -8,6 +8,8 @@ import { FilterBar, SpaceHeader, ViewletContentView, ViewletSettingButton } from '@hcengineering/view-resources' import tracker from '../../plugin' import CreateIssue from '../CreateIssue.svelte' + import GanttToolbarControls from '../gantt/GanttToolbarControls.svelte' + import GanttExtraActions from '../gantt/GanttExtraActions.svelte' function newIssue (): void { showPopup(CreateIssue, { space, shouldSaveDraft: true }, 'top') @@ -53,6 +55,18 @@ {modeSelectorProps} > + + {#if viewlet?._id === tracker.viewlet.IssueGantt} + + + {/if} From 4d53bc271c333284d8268f50926929bcf3568074 Mon Sep 17 00:00:00 2001 From: Michael Uray Date: Fri, 15 May 2026 14:12:36 +0000 Subject: [PATCH 195/388] feat(tracker-resources): deadline-marker render layer in GanttCanvas MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A flag-triangle + dashed vertical line at each issue's deadline date. Orange (#f59e0b) by default; pulsing red (#dc2626) when overdue (dueDate > deadline). Rendered in the same translate group as the issue bars, immediately before the dependency-arrow overlay, per the Render-Layer-Order from the Phase-1 roadmap (bars → deadline-markers → arrows → connector-dots). Signed-off-by: Michael Uray --- .../src/components/gantt/GanttCanvas.svelte | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte index baaf9bdfc27..b7fb93b7898 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttCanvas.svelte @@ -348,6 +348,36 @@ {/each} + + {#each visibleRows as row (rowKey(row))} + {#if row.kind === 'issue' && row.issue !== null && hasDeadline(row.issue)} + {@const dlVal = getDeadline(row.issue)} + {#if dlVal !== null} + {@const dx = timeScale.toX(dlVal)} + {@const dy = row.y + 4} + {@const overdue = isOverdue(row.issue)} + + + + {overdue ? 'Overdue (past deadline)' : 'Deadline'}: {new Date(dlVal).toISOString().slice(0, 10)} + + + {/if} + {/if} + {/each} + + Date: Fri, 15 May 2026 14:13:02 +0000 Subject: [PATCH 196/388] feat(tracker-resources): export full Gantt (sidebar+bars) to PNG/PDF via html2canvas+jspdf MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous PNG path serialised only ``, which contains bar geometry + arrows but none of the issue row labels — the sidebar is a separate HTML `GanttSidebar` component. The resulting PNG was effectively a strip of bars with no context. Replace with an html2canvas-based capture of the entire `.gantt-root` element so the sidebar, sticky time-axis header, and canvas are all rasterised in one image. The previous PDF path simply called `window.print()`, which opened the browser's print dialog instead of producing a file. Replace with a jsPDF-based pipeline: html2canvas at 2x DPI -> JPEG (quality 0.95) -> landscape A4 PDF, scaled to fit while preserving aspect ratio. Output is downloaded via `pdf.save()` like the PNG flow. Bundle-size mitigation: both libs are pulled in with dynamic `import()` inside `exporter.ts`, so the ~750 KB combined payload only lands in the user's browser when the export button is actually clicked. The main tracker-resources bundle is unchanged. Sticky/scrolling capture trick: the Gantt uses an internal vertical scroller plus a sticky horizontal proxy bar with `transform: translateX(...)`. Before snapshotting we expand `.gantt-root`, `.gantt-scroller`, and `.cell.header-cell` to their full content size, and reset the `.hscroll-inner` translateX to zero. All mutations are tracked in a StyleSnapshot tuple list and restored in a `finally` block so the user's live view is unchanged even if html2canvas throws. The deprecated SVG-only helpers (`exportGanttSvgToPng`, `exportAndDownload`) are kept for back-compat and flagged with `@deprecated` JSDoc. Signed-off-by: Michael Uray --- .../src/components/gantt/GanttView.svelte | 31 +- .../src/components/gantt/lib/exporter.ts | 303 ++++++++++++++---- 2 files changed, 254 insertions(+), 80 deletions(-) diff --git a/plugins/tracker-resources/src/components/gantt/GanttView.svelte b/plugins/tracker-resources/src/components/gantt/GanttView.svelte index da9f4ab0b3a..8cf8ecc260d 100644 --- a/plugins/tracker-resources/src/components/gantt/GanttView.svelte +++ b/plugins/tracker-resources/src/components/gantt/GanttView.svelte @@ -10,7 +10,7 @@ import { fsAnchor, ssAnchor, ffAnchor, sfAnchor } from './lib/working-days' import { computeCriticalPath } from './lib/critical-path' import type { CriticalPathResult } from './lib/types' - import { exportAndDownload } from './lib/exporter' + import { exportElementAndDownloadPng, exportElementToPdf } from './lib/exporter' import { ganttToolbarApi } from './lib/gantt-toolbar-bus' import GanttHelpPopup from './GanttHelpPopup.svelte' import type { PrimaryEdit, SimulateResult, CascadeShift } from './lib/types' @@ -1580,30 +1580,31 @@ if (next !== zoom) setZoom(next) } + // PNG export — captures the entire `.gantt-root` (sidebar + sticky + // header + canvas SVG) via html2canvas. The lib is dynamically + // imported by the exporter so it only lands in the user's browser + // when this button is clicked (~750 KB chunk, lazy-loaded). async function exportToPng (): Promise { - const svg = scrollerEl?.querySelector('svg.gantt-canvas') as SVGSVGElement | null - if (svg === null) return + if (containerEl == null) return try { - await exportAndDownload(svg, `gantt-${new Date().toISOString().slice(0, 10)}`) + await exportElementAndDownloadPng(containerEl, `gantt-${new Date().toISOString().slice(0, 10)}`) } catch (err) { const title = await translate(tracker.string.GanttExportFailed, {}, undefined) addNotification(title, String(err), undefined as any, undefined, NotificationSeverity.Error) } } - // PDF export. Uses the browser's print pipeline with - // a Gantt-specific print stylesheet so we don't pull in jsPDF. - // Caller picks "Save as PDF" in the browser print dialog. - function exportToPdf (): void { + // PDF export — rasterises the gantt-root with html2canvas at 2× DPI + // and embeds the resulting JPEG into a landscape A4 PDF via jsPDF, + // then triggers a browser download. Both html2canvas and jsPDF are + // dynamic-imported inside the exporter — neither lib bloats the main bundle. + async function exportToPdf (): Promise { if (containerEl == null) return - containerEl.classList.add('gantt-printing') try { - window.print() - } finally { - // Defer the cleanup so the browser has time to render the - // print stylesheet before the class is removed. afterprint - // event would be more correct but isn't reliable cross-browser. - setTimeout(() => containerEl?.classList.remove('gantt-printing'), 1000) + await exportElementToPdf(containerEl, `gantt-${new Date().toISOString().slice(0, 10)}`) + } catch (err) { + const title = await translate(tracker.string.GanttExportFailed, {}, undefined) + addNotification(title, String(err), undefined as any, undefined, NotificationSeverity.Error) } } diff --git a/plugins/tracker-resources/src/components/gantt/lib/exporter.ts b/plugins/tracker-resources/src/components/gantt/lib/exporter.ts index ba2c6ec91f6..5f9ed8443ba 100644 --- a/plugins/tracker-resources/src/components/gantt/lib/exporter.ts +++ b/plugins/tracker-resources/src/components/gantt/lib/exporter.ts @@ -4,79 +4,268 @@ // /** - * Browser-side Gantt SVG → PNG export. + * Browser-side Gantt PNG/PDF export. * - * The Gantt view renders into an `` overlaid on - * the sidebar (`
+ + {#if ganttGroupBy === 'none'} + + + + {/if} -
- - {#if filterPopupOpen} - - - {/if} -
+ +