Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- The task name column in the table can now be resized
- The timeline groups tasks under their project when it shows several projects, and each project can be collapsed to a single row ([#258](https://github.com/dotpm/obsidian-pm/issues/258))
- A collapsed task or project shows how far everything folded into it reaches, as a line out to the earliest start and the latest end ([#180](https://github.com/dotpm/obsidian-pm/issues/180))

### Fixed

Expand Down
1 change: 1 addition & 0 deletions packages/core/src/locales/bn.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@
"gantt.quarterYear": "ত্রৈ{quarter} {year}",
"gantt.saveDateFailed": "তারিখের পরিবর্তন সংরক্ষণ করা যায়নি। আবার চেষ্টা করুন।",
"gantt.setDatesFailed": "টাস্কের তারিখ নির্ধারণ করা যায়নি। আবার চেষ্টা করুন।",
"gantt.spanTooltip": "সব কাজ: {start} – {end}",
"gantt.today": "আজ",
"gantt.tooltipAssignees": "দায়িত্বে: {names}",
"gantt.tooltipDate": "তারিখ: {date}",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/locales/de.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@
"gantt.quarterYear": "Q{quarter} {year}",
"gantt.saveDateFailed": "Die Datumsänderung konnte nicht gespeichert werden. Bitte versuche es erneut.",
"gantt.setDatesFailed": "Die Termine konnten nicht festgelegt werden. Bitte versuche es erneut.",
"gantt.spanTooltip": "Alle Aufgaben: {start} – {end}",
"gantt.today": "Heute",
"gantt.tooltipAssignees": "Zuständig: {names}",
"gantt.tooltipDate": "Datum: {date}",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@
"gantt.quarterYear": "Q{quarter} {year}",
"gantt.saveDateFailed": "Failed to save date change. Please try again.",
"gantt.setDatesFailed": "Failed to set task dates. Please try again.",
"gantt.spanTooltip": "All tasks: {start} – {end}",
"gantt.today": "Today",
"gantt.tooltipAssignees": "Assignees: {names}",
"gantt.tooltipDate": "Date: {date}",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/locales/es.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@
"gantt.quarterYear": "T{quarter} {year}",
"gantt.saveDateFailed": "No se pudo guardar el cambio de fecha. Inténtelo de nuevo.",
"gantt.setDatesFailed": "No se pudieron definir las fechas de la tarea. Inténtelo de nuevo.",
"gantt.spanTooltip": "Todas las tareas: {start} – {end}",
"gantt.today": "Hoy",
"gantt.tooltipAssignees": "Responsables: {names}",
"gantt.tooltipDate": "Fecha: {date}",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/locales/fr.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@
"gantt.quarterYear": "T{quarter} {year}",
"gantt.saveDateFailed": "Échec de l'enregistrement de la date. Réessayez.",
"gantt.setDatesFailed": "Échec de la définition des dates de la tâche. Réessayez.",
"gantt.spanTooltip": "Toutes les tâches : {start} – {end}",
"gantt.today": "Aujourd'hui",
"gantt.tooltipAssignees": "Responsables : {names}",
"gantt.tooltipDate": "Date : {date}",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/locales/it.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@
"gantt.quarterYear": "T{quarter} {year}",
"gantt.saveDateFailed": "Impossibile salvare la modifica della data. Riprova.",
"gantt.setDatesFailed": "Impossibile impostare le date dell'attività. Riprova.",
"gantt.spanTooltip": "Tutte le attività: {start} – {end}",
"gantt.today": "Oggi",
"gantt.tooltipAssignees": "Assegnatari: {names}",
"gantt.tooltipDate": "Data: {date}",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/locales/ja.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
"gantt.quarterYear": "{year}年 Q{quarter}",
"gantt.saveDateFailed": "日付の変更を保存できませんでした。もう一度お試しください。",
"gantt.setDatesFailed": "タスクの日付を設定できませんでした。もう一度お試しください。",
"gantt.spanTooltip": "すべてのタスク: {start} – {end}",
"gantt.today": "今日",
"gantt.tooltipAssignees": "担当者:{names}",
"gantt.tooltipDate": "日付:{date}",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/locales/ko.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
"gantt.quarterYear": "{year}년 {quarter}분기",
"gantt.saveDateFailed": "날짜 변경을 저장하지 못했습니다. 다시 시도해 주세요.",
"gantt.setDatesFailed": "작업 날짜를 지정하지 못했습니다. 다시 시도해 주세요.",
"gantt.spanTooltip": "모든 작업: {start} – {end}",
"gantt.today": "오늘",
"gantt.tooltipAssignees": "담당자: {names}",
"gantt.tooltipDate": "날짜: {date}",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/locales/pl.json
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@
"gantt.quarterYear": "K{quarter} {year}",
"gantt.saveDateFailed": "Nie udało się zapisać zmiany daty. Spróbuj ponownie.",
"gantt.setDatesFailed": "Nie udało się ustawić dat zadania. Spróbuj ponownie.",
"gantt.spanTooltip": "Wszystkie zadania: {start} – {end}",
"gantt.today": "Dzisiaj",
"gantt.tooltipAssignees": "Osoby przypisane: {names}",
"gantt.tooltipDate": "Data: {date}",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/locales/pt-BR.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@
"gantt.quarterYear": "T{quarter} {year}",
"gantt.saveDateFailed": "Não foi possível salvar a mudança de data. Tente novamente.",
"gantt.setDatesFailed": "Não foi possível definir as datas da tarefa. Tente novamente.",
"gantt.spanTooltip": "Todas as tarefas: {start} – {end}",
"gantt.today": "Hoje",
"gantt.tooltipAssignees": "Responsáveis: {names}",
"gantt.tooltipDate": "Data: {date}",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/locales/ro.json
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,7 @@
"gantt.quarterYear": "T{quarter} {year}",
"gantt.saveDateFailed": "Salvarea datei nu a reușit. Încearcă din nou.",
"gantt.setDatesFailed": "Datele sarcinii nu au putut fi setate. Încearcă din nou.",
"gantt.spanTooltip": "Toate sarcinile: {start} – {end}",
"gantt.today": "Astăzi",
"gantt.tooltipAssignees": "Responsabili: {names}",
"gantt.tooltipDate": "Dată: {date}",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,7 @@
"gantt.quarterYear": "{quarter} кв. {year}",
"gantt.saveDateFailed": "Не удалось сохранить новую дату. Попробуйте ещё раз.",
"gantt.setDatesFailed": "Не удалось задать даты задачи. Попробуйте ещё раз.",
"gantt.spanTooltip": "Все задачи: {start} – {end}",
"gantt.today": "Сегодня",
"gantt.tooltipAssignees": "Исполнители: {names}",
"gantt.tooltipDate": "Дата: {date}",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/locales/sv.json
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,7 @@
"gantt.quarterYear": "Kv{quarter} {year}",
"gantt.saveDateFailed": "Det gick inte att spara datumändringen. Försök igen.",
"gantt.setDatesFailed": "Det gick inte att ange uppgiftens datum. Försök igen.",
"gantt.spanTooltip": "Alla uppgifter: {start} – {end}",
"gantt.today": "Idag",
"gantt.tooltipAssignees": "Ansvariga: {names}",
"gantt.tooltipDate": "Datum: {date}",
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/locales/zh.json
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@
"gantt.quarterYear": "{year}年第{quarter}季度",
"gantt.saveDateFailed": "保存日期更改失败,请重试。",
"gantt.setDatesFailed": "设置任务日期失败,请重试。",
"gantt.spanTooltip": "所有任务:{start} – {end}",
"gantt.today": "今天",
"gantt.tooltipAssignees": "负责人:{names}",
"gantt.tooltipDate": "日期:{date}",
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -389,6 +389,8 @@ export interface PMSettings {
collapsedTasks: Record<string, string[]>
/** Paths of projects whose sub-projects are collapsed in the project list. */
collapsedProjects: string[]
/** Paths of projects folded to their own row in a timeline covering several projects. */
collapsedProjectRows: string[]
/** Whether the project list shows archived projects. */
showArchivedProjects: boolean
projectList: ProjectListState
Expand Down Expand Up @@ -482,6 +484,7 @@ export const DEFAULT_SETTINGS: PMSettings = {
scopeViews: {},
collapsedTasks: {},
collapsedProjects: [],
collapsedProjectRows: [],
showArchivedProjects: false,
projectList: makeDefaultProjectList(),
projectListViews: [],
Expand Down
104 changes: 104 additions & 0 deletions packages/ui/src/gantt/bars.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
// @vitest-environment happy-dom
import { describe, it, expect } from 'vitest'
import { makeTask, Temporal } from '@dotpm/core'
import { svgEl } from '#dom'
import type { GanttCanvas } from './canvas'
import { barExtent, dateSpan, drawSpanWhiskers, spanExtent } from './bars'
import { buildTimelineConfig } from './TimelineConfig'

const range = (tasks: Parameters<typeof dateSpan>[0]) => {
const span = dateSpan(tasks)
return span && [span.start.toString(), span.end.toString()]
}

describe('dateSpan', () => {
it('reaches past the parent to its latest subtask', () => {
const parent = makeTask({
start: '2026-10-05',
due: '2026-10-20',
subtasks: [makeTask({ start: '2026-10-15', due: '2026-11-03' })]
})
expect(range([parent])).toEqual(['2026-10-05', '2026-11-03'])
})

it('counts a subtask that starts before its parent', () => {
const parent = makeTask({
start: '2026-10-05',
due: '2026-10-20',
subtasks: [makeTask({ start: '', subtasks: [makeTask({ start: '2026-09-28', due: '2026-10-01' })] })]
})
expect(range([parent])).toEqual(['2026-09-28', '2026-10-20'])
})

it('takes a task with only one date as that single day', () => {
expect(range([makeTask({ start: '', due: '2026-10-07' }), makeTask({ start: '2026-10-09' })])).toEqual([
'2026-10-07',
'2026-10-09'
])
})

it('is null when nothing is dated', () => {
expect(dateSpan([makeTask({ start: '', subtasks: [makeTask({ start: '' })] })])).toBeNull()
})
})

describe('drawSpanWhiskers', () => {
const sample = [makeTask({ start: '2026-09-01', due: '2026-12-31' })]
const canvas = (): GanttCanvas => ({
svgEl: svgEl('svg', {}),
headerSvgEl: svgEl('svg', {}),
cfg: buildTimelineConfig(sample, 'week'),
weekLabel: 'weekNumber'
})
const span = (start: string, end: string) => ({
start: Temporal.PlainDate.from(start),
end: Temporal.PlainDate.from(end)
})
/** The subpaths of the drawn line, as [x1, y1, x2, y2]. */
const strokes = (group: SVGGElement | null) =>
(group?.querySelector('.pm-gantt-span-line')?.getAttribute('d') ?? '')
.split('M')
.filter((part) => part.trim())
.map((part) => part.replace('L', ' ').trim().split(/\s+/).map(Number))
const horizontal = (s: number[]) => s[1] === s[3]

it('runs a capped line from the bar out to the last folded day, on that side only', () => {
const c = canvas()
const task = makeTask({ start: '2026-10-05', due: '2026-10-09' })
const folded = span('2026-10-05', '2026-11-03')
const group = drawSpanWhiskers(svgEl('g', {}), c, 0, folded, 'red', barExtent(c, task))

const barEnd = barExtent(c, task)?.x2
const { x2 } = spanExtent(c, folded)
const lines = strokes(group)
expect(lines.filter(horizontal)).toEqual([[x2, lines[1][1], barEnd, lines[1][1]]])
expect(lines.filter((s) => !horizontal(s)).map((s) => s[0])).toEqual([x2])
})

it('draws on both sides when the folded tasks start earlier and end later', () => {
const c = canvas()
const task = makeTask({ start: '2026-10-05', due: '2026-10-09' })
const lines = strokes(
drawSpanWhiskers(svgEl('g', {}), c, 0, span('2026-09-20', '2026-11-03'), 'red', barExtent(c, task))
)
expect(lines.filter(horizontal)).toHaveLength(2)
expect(lines.filter((s) => !horizontal(s))).toHaveLength(2)
})

it('draws nothing when the folded tasks stay inside the bar', () => {
const c = canvas()
const task = makeTask({ start: '2026-10-05', due: '2026-10-20' })
expect(
drawSpanWhiskers(svgEl('g', {}), c, 0, span('2026-10-06', '2026-10-20'), 'red', barExtent(c, task))
).toBeNull()
})

it('spans the whole range without a bar, as for a project', () => {
const c = canvas()
const folded = span('2026-10-05', '2026-11-03')
const lines = strokes(drawSpanWhiskers(svgEl('g', {}), c, 0, folded, 'red', null))
const { x1, x2 } = spanExtent(c, folded)
expect(lines.filter(horizontal).map((s) => [s[0], s[2]])).toEqual([[x1, x2]])
expect(lines.filter((s) => !horizontal(s)).map((s) => s[0])).toEqual([x1, x2])
})
})
Loading