Skip to content

Commit 0c5b468

Browse files
authored
feat(gantt): configurable tooltip + live parent-stretch (follow-up to #1672) (#1689)
* fix(gantt): progress % no longer occluded; replace 6 missing prebuilt-CSS utilities with inline styles The connector dot used `-right-2`, which the prebuilt components stylesheet never emits, so it collapsed to its auto top-left position and covered the progress label at the bar's start. Pin it with inline `right:-8px` and give `cursor:crosshair` inline too. Audited every GanttView class token against the prebuilt CSS and inlined the other genuinely-absent utilities: resize-handle `w-1.5`/`cursor-ew-resize`, progress-fill `rounded-l-sm`, grid `z-0`, and the sm: responsive variants (`sm:h-[50px]`, `sm:w-20`, `sm:hidden`, `-ml-1`, `h-[45%]`, `touch-manipulation`) via a scoped media-query style block. * feat(gantt): configurable hover tooltip via tooltipFields The 悬浮详情 (hover tooltip) is now dynamically configurable like the component's other field-bound properties. A view declares tooltipFields on its gantt config — field names, or { field, label } to override the label — and ObjectGantt resolves each against the record (select options to their label, lookups to the embedded record name, dates/numbers/currency/percent through the shared @object-ui/fields formatters), feeding GanttView a task.fields array. When present these label/value rows replace the default start to end / duration / progress line; unconfigured tooltips keep it. - types: add tooltipFields to GanttConfig (ObjectUI display extension) - GanttView: render task.fields as a two-column grid in the tooltip - ObjectGantt: resolve + type-format tooltipFields per record - tests: +2 ObjectGantt cases (label override, schema label, select option, date/number formatting; omitted when unconfigured) - verify: scripts/verify-tooltip-fields.mjs (6/6) + docs screenshot * feat(gantt): parent summary stretches live while dragging a child Previously an ancestor summary only re-rolled on drop — mid-drag it stayed at its pre-drag span, so a child dragged past the parent edge appeared to escape the bracket until release. Now every ancestor summary of a single dragged task re-rolls its span over its leaf descendants (substituting the dragged leaf's previewed dates) on each pointermove, so the parent bar grows/shrinks in real time. The pinned edge (earliest child) stays put — it stretches, not shifts — and the live width matches the rollup that commits on drop (no flicker). - getLiveRowStyle: live rollup branch for stretch-ancestor summary rows - dragStretchAncestorIds memo + taskById map drive the ancestor lookup - liveStyle now uses the live path for stretched ancestors too - verify: scripts/verify-child-stretch.mjs (4/4) + 2 docs screenshots * docs(skill): document gantt view config in page-builder guide * feat(gantt): Phase 6 — critical path, auto-schedule, export PNG Three additive Phase 6 features, all driven from new toolbar buttons: - Critical path (CPM): new pure `scheduling.ts` runs forward/backward passes over the dependency graph to find the zero-slack chain. A toolbar toggle (Activity icon) highlights critical task bars, milestones, summary bars and the joining link arrows in red. `criticalPathDefault` prop / `schema.criticalPath` start it on. The diamond fixture proves the maths: Frontend (23d) is critical, the parallel Backend (20d) is not. - Auto-schedule (顺延): `computeProjectReschedule` pushes each successor as late as its links require, preserving durations and never pulling earlier; honors fs/ss/ff/sf and leaves summary rollups fixed. A toolbar button (Wand2, shown only when onTaskUpdate is wired) applies it in one shot. ObjectGantt enables it whenever dependenciesField is configured. - Export PNG: a toolbar button (Download) rebuilds the WHOLE chart (virtualization-independent) into a standalone SVG with concrete hex colors — prebuilt theme CSS vars don't resolve in a detached SVG — then rasterizes to a 2x PNG via canvas and downloads gantt-<mode>.png. Zero deps. Tests: 12 new unit tests for scheduling.ts (80 total pass), tsc clean. Browser verification (verify-phase6.mjs) 13/13 pass; screenshots 15–16 persisted. ROADMAP + verification README updated. * feat(gantt): Phase 6.2 — baselines, working calendar, undo/redo - Baselines: baselineStart/baselineEnd render a thin reference strip under each row (summary/task/milestone); showBaselines prop + ObjectGantt baselineStartField/baselineEndField mapping. - Working calendar: WorkingCalendar (skipWeekends + holidays) measures durations in working days; critical-path and reschedule snap to working-day boundaries (fs/ss via nextWorkingDay, ff/sf via subWorkingDays). Derived in ObjectGantt from schema.skipWeekends/holidays. - Undo/redo: commitTaskUpdates records before/after field deltas (group drag + auto-schedule batched), replayed via onTaskUpdate; toolbar buttons + Ctrl/Cmd+Z / Shift+Z / Ctrl+Y; only shown when onTaskUpdate is wired. - Tests: 5 working-calendar cases (85 total pass), tsc clean. - Verification: scripts/verify-phase6b.mjs, 16/16 browser checks pass, screenshots 17-20 persisted under docs/verification/. * feat(gantt): explicit readOnly switch (prop, demo, skill, tests) Add an explicit readOnly prop to GanttView that disables every write interaction in one place via prop-shadowing — bar drag/resize/progress, inline edit, context-menu edit/delete, dependency-link drag, row reorder, auto-schedule, and the Undo/Redo toolbar buttons — regardless of which callbacks are wired. Non-mutating affordances (task click, view switch, zoom, critical path, export, fullscreen) stay live. - ObjectGantt maps the view schema's top-level readOnly to the prop. - demo: ?readonly=1 nav link. - 3 unit tests assert no writes fire even when all callbacks + readOnly are passed (88 tests total). - verify-phase6b.mjs: +6 read-only browser checks (22/22), screenshot 21-read-only.png. - page-builder skill + README document readOnly. * fix(gantt): export PNG now includes baselines and custom markers The PNG export re-draws the whole chart into a standalone SVG, but it was written before baselines (Phase 6.2) and never drew custom markers — so the downloaded image dropped both: the planned-vs-actual baseline strips and the vertical marker lines/labels (sprint boundaries, deadlines) that the live chart shows. exportPng now draws, with concrete colors (CSS vars don't resolve in a detached SVG): - baseline strips for tasks carrying baselineStart/baselineEnd, gated by showBaselines, at the row-bottom strip beneath the live bar; - custom markers as vertical lines + label chips, with hsl(var(--primary)) falling back to a concrete indigo. verify-phase6.mjs gains 2 checks (15/15): the export SVG contains 3 baseline fills and both marker labels (Sprint 2 + Code freeze). Verified in-browser by capturing the generated SVG (3 baselines, both markers, 0 NaN). * fix(gantt): export month/year header band; unclip marker labels Two visual fixes reported against the export PNG and the live chart: 1. Export PNG had no month/year context — the live header is two rows (a month/year group band over the day/week/… unit labels) but exportPng drew only the unit row. It now renders both: the headerGroups band on top (bold month/year labels + group dividers) over the unit labels, taller header (18+18px). The downloaded image now reads like the on-screen chart. 2. The Today / custom-marker label chips were clipped at the top. They sat at top:-8 inside the timeline scroll container, so the overflow clipped the part above the content's top edge. Moved to top:2 with z-30 so each chip sits fully inside the chart just under the header, no longer occluded. verify-phase6.mjs gains a month-band assertion (16/16): the export SVG now contains a 4-digit year from the group band. Verified in-browser — all three label chips measure clippedAbove:false, and the export header band renders. * fix(gantt): fill the timeline viewport in week/month/quarter mode With a fixed per-unit column width, a short date span in week/month/quarter mode produced a grid far narrower than the timeline area, leaving a large blank gap on the right (day mode already overflows, so it looked fine there). When the end date is auto-derived, timelineRange now extends with empty trailing calendar columns until the grid reaches the right edge — the scale stays honest (real calendar units), only the visible future grows. No-op in day mode and when the caller controls endDate. Also refresh a stale verify-browser assertion: t4 carries configured tooltipFields (Owner/Status/Effort) which replace the default date·duration·progress line, so the tooltip check now asserts those fields instead of '30%'. verify-browser.mjs: +1 viewport-fill check (19/19); verified week/month/ quarter all fill in-browser (content width ≥ viewport). * fix(gantt): fit-to-width instead of padding empty calendar columns The previous viewport-fill extended the timeline with empty trailing calendar units until the grid reached the right edge. In coarse modes that meant years of blank columns (a 2.5-month project in month mode padded out to ~2030). Back that out: keep the natural date span and STRETCH the column width ("zoom to fit") so a short project fills the viewport with a handful of real columns. A manual zoom overrides it; a long project that already overflows keeps the base width and scrolls. Gated on auto-derived end (a caller-pinned endDate keeps fixed width). Verify: week ≤24 cols + month ≤8 cols + both fill the viewport (verify-browser 22/22, phase6 16/16, 88 unit tests). Docs updated. * fix(gantt): localize dates to i18n language + complete stale gantt locale packs 国际化一致性修复,两处: 1. 日期跟随 i18n 语言,而非浏览器 locale。GanttView 把 provider 的 language 透传到所有面向用户的 toLocaleDateString(表头月份/年、 单位标签、tooltip、拖拽与编辑日期片)经由 dateLocale。此前会出现 chrome 是英文、日历却是中文日期(浏览器为 zh-CN)的割裂;现在两者 始终一致。无 I18nProvider 时 language 为 undefined,回退浏览器 locale,保持独立嵌入/单测行为不变。 2. 补全过期的内置 gantt 语言包。@object-ui/i18n 的 en.ts / zh.ts 中 gantt: 命名空间停留在 Phase 2,缺少 Phase 4–6 新增的 toolbar (viewMode/criticalPath/autoSchedule/exportPng/undo/redo/fullscreen)、 viewMode、row、aria、tooltip、menu 等键 —— 套了 I18nProvider 的应用 会把这些渲染成原始 key(如 gantt.viewMode.day)。现已对齐插件内 GANTT_DEFAULT_TRANSLATIONS 的完整键集;其余语言经 fallbackLng 优雅 降级为英文。 demo 包一层 I18nProvider 并提供 English · 中文 切换(?lang=en/zh), 新增 scripts/verify-i18n.mjs 断言两种语言下 chrome/日期均完整本地化、 无 gantt.* 原始 key 泄漏(13/13 通过),留痕 22/23 截图。 测试:tsc(i18n+gantt) 干净;单测 88/88(gantt)、113/113(i18n); verify-browser 22/22、verify-phase6 全绿、verify-i18n 13/13。 * feat(gantt): 动态 Group by — groupBy accessor + ObjectGantt groupByField 将叶子任务按 groupBy(task)=>{key,label} 分桶,在合成的分组汇总行下渲染, 替换原有 parent 层级,复用现有 rollup/折叠/汇总渲染。纯展示层变换:时间轴范围、 关键路径、自动排程仍读取真实 tasks;合成分组行带 data.__group,不可拖拽。 ObjectGantt 暴露为 groupByField(select 选项/lookup 解析为显示标签)。 - GanttView: groupBy / ungroupedLabel props + displayTasks 变换,beginDrag 守卫 - ObjectGantt: groupByField 配置 + groupByAccessor - demo: ?group=owner|status 切换 - 8 个单测 + verify-groupby.mjs(6/6 通过,截图 24/25) * feat(gantt): 资源/工作负载视图 — per-resource load histogram with over-allocation 新增 ResourceWorkload 视图:按资源(负责人/状态等)聚合任务负载,每个资源一行直方图, 列负载 = Σ effort(重叠任务),超过 capacity 的列标红(过载)。纯聚合逻辑在 workload.ts (7 个单测),渲染与时间轴几何在 ResourceWorkload.tsx(5 个渲染测试)。ObjectGantt 通过 resourceView + assigneeField/effortField/capacity 配置开启;demo 提供 ?resource=owner|status。 新增 gantt.resource.* 四个 i18n key(en/zh + 插件内置回退)。 浏览器验证 5/5 通过,留痕 26/27(中英)/28(中文)三张截图。 * feat(gantt): 非线性工作时间轴 — fold weekends/holidays out of the day axis In day mode, when a workingCalendar marks weekends (skipWeekends) or explicit holidays as non-working, those columns are now dropped from the grid entirely — Friday sits against Monday, so the timeline shows only working time. This makes date→px non-linear, so all positioning (bars, dependency arrows, milestones, Today line, custom markers) is routed through a single dateToX/xToDate pair that interpolates within the owning column. Drag/resize advance by working columns (Fri +1 → Mon). Coarser scales and the no-calendar path keep the plain linear axis — dateToX is algebraically identical there. - 7 unit tests (GanttView.workaxis.test.tsx): fold count, weekend drop, weekend-straddling bar compression, working-day drag, holiday fold, day-mode gating, linear equivalence. - verify-workaxis.mjs browser check (5/5) + 3 screenshots (linear / folded / folded-zh) under docs/verification/. * docs(skill): document group-by, resource/workload view, and non-linear axis Page-builder gantt guide now covers groupByField (swimlanes), resourceView + assigneeField/effortField/capacity (workload histogram), and the day-mode weekend/holiday axis folding (非线性工作时间轴) added to the skipWeekends/holidays options. * feat(gantt): 快速筛选 — 可配置多维度过滤栏 + 自动缩放时间轴 - QuickFilterBar 展示组件:每个维度一个多选下拉,AND 组合;全选折叠为无约束 - ObjectGantt 集成:选项解析优先级(显式 options → schema select/enum 全域 → lookup/master_detail 引用对象全量拉取 → 数据去重兜底);lookup 按内嵌记录 id 匹配 - autoZoomToFilter(默认 true):筛选后按剩余任务重算时间轴范围;false 则钉住全量跨度 - 23 个单测(QuickFilterBar + ObjectGantt.quickfilter)+ verify-quickfilter.mjs 9/9 浏览器验证留痕(截图 32–37) - 文档:page-builder.md 增补 quickFilters/autoZoomToFilter;verification README 新增快速筛选章节 * feat(gantt): 依赖增删 + 类型选择(右键链接菜单 + 添加紧前/紧后) - GanttView 新增 onDependencyDelete prop(readOnly 时禁用) - 在依赖连线上叠加透明命中路径(pointer-events: stroke 覆盖 svg 的 none), 右键打开链接菜单:切换 FS/SS/FF/SF 类型 或 移除依赖 - 任务右键菜单新增 添加紧前依赖/添加紧后依赖,弹出候选任务选择器 (排除自身与已连接任务) - ObjectGantt:handleDependencyCreate 改为按 (source→target) upsert 类型, 新增 handleDependencyDelete;序列化保形(CSV 保持 CSV,出现非 FS 类型时 自动升级为 [{id,type}] 数组),乐观更新 + 失败回滚 - i18n:新增 menu.addPredecessor/addSuccessor/removeDependency/noCandidates 与 linkType.fs/ss/ff/sf(默认英文 + demo 中文) - demo 工程示例接入 onDependencyDelete 并使 onDependencyCreate 按类型 upsert - 测试:GanttView 交互 8 例 + ObjectGantt 写回 6 例(共 151 通过),tsc 通过 - 浏览器留痕:verify-dep-edit.mjs 7/7,截图 38-41 * feat(gantt): 拖拽冲突校验 + 顺延确认 拖动任务条后回放依赖前向排程,若新位置违反 FS/SS/FF/SF 约束则弹出顺延 确认框:自动顺延按拓扑顺序重排受影响任务(汇总条固定),取消保留则保留 手动位置。dependenciesField 存在时默认启用,readOnly 下禁用。 - GanttView 新增 rescheduleOnConflict 属性、冲突检测与确认对话框 - ObjectGantt 在 dependenciesField 配置时自动开启 - i18n 新增 gantt.conflict.* (英文默认 + demo 中文) - 单测 +8 (GanttView 拖拽冲突 6 + ObjectGantt 接线 2),共 159 通过 - 浏览器留痕 scripts/verify-conflict.mjs,截图 42-44
1 parent e8ad3ba commit 0c5b468

94 files changed

Lines changed: 6480 additions & 193 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/i18n/src/locales/en.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,41 @@ const en = {
294294
today: 'Today',
295295
showTaskList: 'Show task list',
296296
hideTaskList: 'Hide task list',
297+
viewMode: 'Timeline granularity',
298+
enterFullscreen: 'Enter fullscreen',
299+
exitFullscreen: 'Exit fullscreen',
300+
criticalPath: 'Highlight critical path',
301+
autoSchedule: 'Auto-schedule dependencies',
302+
exportPng: 'Export as PNG',
303+
undo: 'Undo',
304+
redo: 'Redo',
305+
},
306+
viewMode: {
307+
day: 'Day',
308+
week: 'Week',
309+
month: 'Month',
310+
quarter: 'Quarter',
311+
},
312+
row: {
313+
expand: 'Expand',
314+
collapse: 'Collapse',
315+
},
316+
aria: {
317+
taskList: 'Task list',
318+
},
319+
tooltip: {
320+
days: 'd',
321+
},
322+
menu: {
323+
view: 'View details',
324+
edit: 'Edit inline',
325+
delete: 'Delete',
326+
},
327+
resource: {
328+
header: 'Resource',
329+
peak: 'Peak',
330+
over: 'overloaded',
331+
empty: 'No tasks to allocate.',
297332
},
298333
},
299334
view: {

packages/i18n/src/locales/zh.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -294,6 +294,41 @@ const zh = {
294294
today: '今天',
295295
showTaskList: '展开任务列表',
296296
hideTaskList: '收起任务列表',
297+
viewMode: '时间粒度',
298+
enterFullscreen: '进入全屏',
299+
exitFullscreen: '退出全屏',
300+
criticalPath: '高亮关键路径',
301+
autoSchedule: '自动排程依赖',
302+
exportPng: '导出 PNG',
303+
undo: '撤销',
304+
redo: '重做',
305+
},
306+
viewMode: {
307+
day: '日',
308+
week: '周',
309+
month: '月',
310+
quarter: '季度',
311+
},
312+
row: {
313+
expand: '展开',
314+
collapse: '收起',
315+
},
316+
aria: {
317+
taskList: '任务列表',
318+
},
319+
tooltip: {
320+
days: '天',
321+
},
322+
menu: {
323+
view: '查看详情',
324+
edit: '行内编辑',
325+
delete: '删除',
326+
},
327+
resource: {
328+
header: '资源',
329+
peak: '峰值',
330+
over: '超载',
331+
empty: '没有可分配的任务。',
297332
},
298333
},
299334
view: {

packages/plugin-gantt/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,7 @@ Display project timeline with tasks:
127127
viewMode?: 'day' | 'week' | 'month',
128128
onTaskClick?: (task) => void,
129129
onTaskUpdate?: (task) => void,
130+
readOnly?: boolean, // disable all editing (drag/resize/inline/delete/link/undo)
130131
className?: string
131132
}
132133
```

packages/plugin-gantt/ROADMAP.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -57,9 +57,9 @@ Today marker + jump-to-today, weekend highlighting, semantic color fallback, i18
5757

5858
## Phase 6 — Advanced (SVAR PRO territory, differentiators)
5959

60-
- [ ] Critical path computation + slack visualization
61-
- [ ] Baselines (planned vs actual bars)
62-
- [ ] Auto-scheduling: dependency-driven date shifting (forward, finish-to-start first)
63-
- [ ] Working calendar (skip weekends/holidays in duration math)
64-
- [ ] Undo/redo for drag/edit operations
65-
- [ ] Export: PNG/PDF (client-side), MS Project XML import/export
60+
- [x] Critical path computation + slack visualization — CPM forward/backward passes in `scheduling.ts` (zero-slack chain), toolbar toggle (Activity icon) highlights critical bars/milestones/summaries + joining link arrows in red; `criticalPathDefault` prop / `schema.criticalPath` start it on
61+
- [x] Baselines (planned vs actual bars)`baselineStart`/`baselineEnd` per task render a thin reference strip hugging each row's bottom (summary / task / milestone) on the shared ms→px mapping; `showBaselines` prop / `schema.showBaselines`, mapped in `ObjectGantt` via `baselineStartField` / `baselineEndField`
62+
- [x] Auto-scheduling: dependency-driven date shifting (forward, finish-to-start first)`computeProjectReschedule` (顺延: push successors later, durations preserved, never earlier; honors fs/ss/ff/sf; summaries fixed), toolbar button (Wand2) does a one-shot whole-project reschedule via `onTaskUpdate`; `autoSchedule` prop, auto-on in `ObjectGantt` when `dependenciesField` set
63+
- [x] Working calendar (skip weekends/holidays in duration math)`WorkingCalendar` (`skipWeekends`, `holidays` ISO-day Set) measures durations in working days; `computeCriticalPath` / `computeProjectReschedule` snap rescheduled tasks to working-day boundaries (fs/ss bump via `nextWorkingDay`, ff/sf back-derive via `subWorkingDays`); `workingCalendar` prop, derived in `ObjectGantt` from `schema.skipWeekends` / `schema.holidays`
64+
- [x] Undo/redo for drag/edit operations`commitTaskUpdates` records before/after field deltas per mutation (group-drag / auto-schedule batched into one entry) and replays through `onTaskUpdate` against `tasksRef`; toolbar Undo/Redo buttons + Ctrl/Cmd+Z, Ctrl/Cmd+Shift+Z, Ctrl/Cmd+Y; only shown when `onTaskUpdate` is wired
65+
- [x] Export PNG (client-side) — toolbar button (Download) rebuilds the whole chart (virtualization-independent) as a standalone SVG with concrete hex colors, rasterizes to a 2× PNG via canvas, downloads `gantt-<mode>.png`; zero dependencies. _(PDF / MS Project XML still pending.)_

0 commit comments

Comments
 (0)