@anchrd/intel-ui 0.18.0 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -9
- package/src/app/app-tree/app-tree.tsx +59 -147
- package/src/app/settings-dialog/settings-dialog.tsx +10 -32
- package/src/data/intel-data-provider/intel-data-provider.ts +0 -163
- package/src/data/intel-data-provider/intel-data-provider.types.ts +0 -100
- package/src/entry-picker/entry-picker.tsx +6 -6
- package/src/i18n/de.json +6 -217
- package/src/i18n/en.json +6 -217
- package/src/i18n/es.json +6 -217
- package/src/import-bundle/import-bundle.tsx +152 -0
- package/src/kind-icon.ts +1 -15
- package/src/main.tsx +7 -10
- package/src/node-table/node-table.tsx +2 -2
- package/src/nodes/nodes.tsx +0 -34
- package/src/resource-menu/resource-menu.tsx +42 -14
- package/src/section-hint/section-hint.tsx +2 -2
- package/src/title-row/title-row.tsx +5 -22
- package/src/tools/tools.tsx +0 -2
- package/vite.config.ts +0 -4
- package/src/agent/agent-avatar/agent-avatar.tsx +0 -34
- package/src/agent/agent-calendar/agent-calendar.tsx +0 -160
- package/src/agent/agent-chat/agent-chat.tsx +0 -118
- package/src/agent/agent-costs/agent-costs.ts +0 -95
- package/src/agent/agent-cron/agent-cron.ts +0 -68
- package/src/agent/agent-definition/agent-definition.ts +0 -96
- package/src/agent/agent-delegation-notice/agent-delegation-notice.ts +0 -48
- package/src/agent/agent-entry-title/agent-entry-title.ts +0 -72
- package/src/agent/agent-log/agent-log.tsx +0 -308
- package/src/agent/agent-models/agent-models.ts +0 -166
- package/src/agent/agent-models/agent-models.types.ts +0 -24
- package/src/agent/agent-profile/agent-profile.tsx +0 -1006
- package/src/agent/agent-state/agent-state.ts +0 -85
- package/src/agent/agent-status-dot/agent-status-dot.ts +0 -73
- package/src/agent/agent.tsx +0 -507
- package/src/board/board-calendar/board-calendar.tsx +0 -89
- package/src/board/board-card/board-card.tsx +0 -106
- package/src/board/board-data/board-data.ts +0 -241
- package/src/board/board-data/board-data.types.ts +0 -63
- package/src/board/board-detail/board-detail.tsx +0 -629
- package/src/board/board-gantt/board-gantt.ts +0 -545
- package/src/board/board-gantt/board-gantt.tsx +0 -286
- package/src/board/board-graph/board-graph.ts +0 -174
- package/src/board/board-graph/board-graph.tsx +0 -168
- package/src/board/board-items/board-items.ts +0 -183
- package/src/board/board-kanban/board-kanban.ts +0 -137
- package/src/board/board-kanban/board-kanban.tsx +0 -257
- package/src/board/board-status/board-status.ts +0 -59
- package/src/board/board-statuses/board-statuses.ts +0 -63
- package/src/board/board-statuses/board-statuses.tsx +0 -228
- package/src/board/board-table/board-table.ts +0 -33
- package/src/board/board-table/board-table.tsx +0 -413
- package/src/board/board-views/board-views.tsx +0 -68
- package/src/board/board-views/board-views.types.ts +0 -29
- package/src/board/board.tsx +0 -251
- package/src/components/ui/item-calendar.tsx +0 -181
- package/src/components/ui/item-gantt.tsx +0 -463
- package/src/components/ui/kanban.tsx +0 -282
- package/src/data/agent-runtime/agent-runtime.ts +0 -131
- package/src/hooks/use-capabilities.ts +0 -22
- package/src/hooks/use-model-catalog.ts +0 -26
- package/src/timezone/timezone-combobox/timezone-combobox.tsx +0 -149
- package/src/timezone/timezone-context.tsx +0 -65
- package/src/timezone/timezone.ts +0 -174
|
@@ -1,545 +0,0 @@
|
|
|
1
|
-
import type { BoardTask } from "@anchrd/intel-contract";
|
|
2
|
-
import type { UpdateTask } from "@/board/board-data/board-data.types.ts";
|
|
3
|
-
import {
|
|
4
|
-
addDays,
|
|
5
|
-
type BoardItem,
|
|
6
|
-
daysBetween,
|
|
7
|
-
dayValue,
|
|
8
|
-
rootedParents,
|
|
9
|
-
} from "@/board/board-items/board-items.ts";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Everything a Gantt of a board is, as arithmetic (anchrd/intel#293).
|
|
13
|
-
*
|
|
14
|
-
* ⚠️ The pixels are here and the DOM is next door. A timeline is one coordinate system — a day is an
|
|
15
|
-
* x, a row is a y, an arrow is a list of points between them — and keeping that in a component would
|
|
16
|
-
* make every one of the cases below only testable by rendering: a dependency that points backwards
|
|
17
|
-
* in time, a resize that would put the start after the due date, a drag that ended where it started.
|
|
18
|
-
* Those are the cases the picture is wrong about, and a picture is the one thing that cannot assert.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
// How wide a day is drawn, per scale. A board's dates are DAYS (contract), so pixels-per-day is the
|
|
22
|
-
// whole of the zoom: every offset and every width below is a day count times this number.
|
|
23
|
-
const DayWidth = { days: 34, weeks: 12, months: 4 } as const;
|
|
24
|
-
|
|
25
|
-
export type GanttScale = keyof typeof DayWidth;
|
|
26
|
-
export const GanttScales = ["days", "weeks", "months"] as const;
|
|
27
|
-
|
|
28
|
-
export function dayWidth(scale: GanttScale): number {
|
|
29
|
-
return DayWidth[scale];
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export interface GanttSpan {
|
|
33
|
-
startAt: Date;
|
|
34
|
-
endAt: Date;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* A tick on the time axis, measured in DAYS rather than pixels.
|
|
39
|
-
*
|
|
40
|
-
* ⚠️ Deliberately not a column of fixed width. A month is 28 to 31 days wide and a week that starts
|
|
41
|
-
* mid-month is however many days are left of it, so a header built from equal columns drifts away
|
|
42
|
-
* from the bars underneath it — slowly, which is the kind of wrong nobody notices until a bar sits
|
|
43
|
-
* under the wrong month.
|
|
44
|
-
*/
|
|
45
|
-
export interface GanttTick {
|
|
46
|
-
key: string;
|
|
47
|
-
label: string;
|
|
48
|
-
days: number;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
export interface GanttAxis {
|
|
52
|
-
// The first day drawn, always the first of a month so the header groups are whole.
|
|
53
|
-
start: Date;
|
|
54
|
-
days: number;
|
|
55
|
-
// The two header rows: the coarse one (months, or years at month scale) and the fine one.
|
|
56
|
-
groups: GanttTick[];
|
|
57
|
-
ticks: GanttTick[];
|
|
58
|
-
// Where today stands, as a day index, or `null` when the board is nowhere near it.
|
|
59
|
-
today: number | null;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function firstOfMonth(day: Date): Date {
|
|
63
|
-
return new Date(day.getFullYear(), day.getMonth(), 1);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* The window the chart draws, from the work that is on it.
|
|
68
|
-
*
|
|
69
|
-
* ⚠️ A month of air on each side, and today is always inside it. A window cut exactly to the first
|
|
70
|
-
* and last bar leaves nowhere to drag a bar TO — the reader would have to make a task later before
|
|
71
|
-
* they could make it later — and a board whose work is all in the past would open with no clue that
|
|
72
|
-
* it is in the past.
|
|
73
|
-
*/
|
|
74
|
-
export function ganttAxis(
|
|
75
|
-
spans: GanttSpan[],
|
|
76
|
-
today: Date,
|
|
77
|
-
scale: GanttScale,
|
|
78
|
-
locale: string,
|
|
79
|
-
): GanttAxis {
|
|
80
|
-
let min = today;
|
|
81
|
-
let max = today;
|
|
82
|
-
for (const span of spans) {
|
|
83
|
-
if (span.startAt < min) min = span.startAt;
|
|
84
|
-
if (span.endAt > max) max = span.endAt;
|
|
85
|
-
}
|
|
86
|
-
const first = firstOfMonth(min);
|
|
87
|
-
const last = firstOfMonth(max);
|
|
88
|
-
const start = new Date(first.getFullYear(), first.getMonth() - 1, 1);
|
|
89
|
-
const stop = new Date(last.getFullYear(), last.getMonth() + 2, 1);
|
|
90
|
-
const days = daysBetween(start, stop);
|
|
91
|
-
const todayIndex = daysBetween(start, today);
|
|
92
|
-
return {
|
|
93
|
-
start,
|
|
94
|
-
days,
|
|
95
|
-
groups: groupTicks(start, days, scale, locale),
|
|
96
|
-
ticks: fineTicks(start, days, scale, locale),
|
|
97
|
-
today: todayIndex >= 0 && todayIndex < days ? todayIndex : null,
|
|
98
|
-
};
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// The coarse row: months over days and weeks, years over months.
|
|
102
|
-
function groupTicks(start: Date, days: number, scale: GanttScale, locale: string): GanttTick[] {
|
|
103
|
-
const ticks: GanttTick[] = [];
|
|
104
|
-
if (scale === "months") {
|
|
105
|
-
const format = new Intl.DateTimeFormat(locale, { year: "numeric" });
|
|
106
|
-
for (let index = 0; index < days; ) {
|
|
107
|
-
const day = addDays(start, index);
|
|
108
|
-
const next = new Date(day.getFullYear() + 1, 0, 1);
|
|
109
|
-
const width = Math.min(daysBetween(day, next), days - index);
|
|
110
|
-
ticks.push({ key: `${day.getFullYear()}`, label: format.format(day), days: width });
|
|
111
|
-
index += width;
|
|
112
|
-
}
|
|
113
|
-
return ticks;
|
|
114
|
-
}
|
|
115
|
-
const format = new Intl.DateTimeFormat(locale, { month: "long", year: "numeric" });
|
|
116
|
-
for (let index = 0; index < days; ) {
|
|
117
|
-
const day = addDays(start, index);
|
|
118
|
-
const next = new Date(day.getFullYear(), day.getMonth() + 1, 1);
|
|
119
|
-
const width = Math.min(daysBetween(day, next), days - index);
|
|
120
|
-
ticks.push({
|
|
121
|
-
key: `${day.getFullYear()}-${day.getMonth()}`,
|
|
122
|
-
label: format.format(day),
|
|
123
|
-
days: width,
|
|
124
|
-
});
|
|
125
|
-
index += width;
|
|
126
|
-
}
|
|
127
|
-
return ticks;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* How many ticks may be drawn, whatever the dates say.
|
|
132
|
-
*
|
|
133
|
-
* ⚠️ The window comes from the board's own dates, and a `BoardTaskDate` is any ISO date — one task
|
|
134
|
-
* typed as `2226-08-07` (or written that way over MCP) makes a window of seventy thousand days. At
|
|
135
|
-
* day ticks that is seventy thousand header cells and as many grid lines, which is not a slow
|
|
136
|
-
* screen but a dead tab. The unit widens instead: the bars keep their exact day width, so the chart
|
|
137
|
-
* stays as precise as it was and only the ruler gets coarser.
|
|
138
|
-
*/
|
|
139
|
-
const MaxTicks = 1_200;
|
|
140
|
-
|
|
141
|
-
function fineTicks(start: Date, days: number, scale: GanttScale, locale: string): GanttTick[] {
|
|
142
|
-
const unit: GanttScale =
|
|
143
|
-
days <= MaxTicks ? scale : days <= MaxTicks * 7 && scale !== "months" ? "weeks" : "months";
|
|
144
|
-
if (unit !== scale) return fineTicks(start, days, unit, locale);
|
|
145
|
-
if (scale === "days") {
|
|
146
|
-
const format = new Intl.DateTimeFormat(locale, { day: "numeric" });
|
|
147
|
-
return Array.from({ length: days }, (_, index) => {
|
|
148
|
-
const day = addDays(start, index);
|
|
149
|
-
return { key: `d${index}`, label: format.format(day), days: 1 };
|
|
150
|
-
});
|
|
151
|
-
}
|
|
152
|
-
if (scale === "months") {
|
|
153
|
-
const format = new Intl.DateTimeFormat(locale, { month: "short" });
|
|
154
|
-
const ticks: GanttTick[] = [];
|
|
155
|
-
for (let index = 0; index < days; ) {
|
|
156
|
-
const day = addDays(start, index);
|
|
157
|
-
const next = new Date(day.getFullYear(), day.getMonth() + 1, 1);
|
|
158
|
-
const width = Math.min(daysBetween(day, next), days - index);
|
|
159
|
-
ticks.push({ key: `m${index}`, label: format.format(day), days: width });
|
|
160
|
-
index += width;
|
|
161
|
-
}
|
|
162
|
-
return ticks;
|
|
163
|
-
}
|
|
164
|
-
const format = new Intl.DateTimeFormat(locale, { day: "numeric", month: "short" });
|
|
165
|
-
const ticks: GanttTick[] = [];
|
|
166
|
-
for (let index = 0; index < days; index += 7) {
|
|
167
|
-
const day = addDays(start, index);
|
|
168
|
-
ticks.push({ key: `w${index}`, label: format.format(day), days: Math.min(7, days - index) });
|
|
169
|
-
}
|
|
170
|
-
return ticks;
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* A task with the tasks under it, and what the two of them span together.
|
|
175
|
-
*
|
|
176
|
-
* ⚠️ `span` is the subtree, `item` is the task's own dates. Keeping both is what lets an epic show a
|
|
177
|
-
* bar over its children without pretending those dates are its own — the derived one must not be
|
|
178
|
-
* dragged, because one bar cannot write five tasks.
|
|
179
|
-
*/
|
|
180
|
-
export interface GanttNode {
|
|
181
|
-
task: BoardTask;
|
|
182
|
-
item: BoardItem | null;
|
|
183
|
-
span: GanttSpan | null;
|
|
184
|
-
children: GanttNode[];
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
export interface GanttTree {
|
|
188
|
-
roots: GanttNode[];
|
|
189
|
-
/**
|
|
190
|
-
* The subtrees on which no date exists anywhere — the leftover column.
|
|
191
|
-
*
|
|
192
|
-
* ⚠️ Whole subtrees, not loose tasks. A dateless epic with two dateless children is three tasks
|
|
193
|
-
* the DoD says must not vanish, and listing only the epic would lose the other two just as
|
|
194
|
-
* quietly as dropping all three. A dateless task UNDER a dated one is not in here at all: it is a
|
|
195
|
-
* row in the chart with no bar, which is where it belongs.
|
|
196
|
-
*/
|
|
197
|
-
undated: GanttNode[];
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
/** The board as the tree the chart draws, over the one dated reading of it (`board-items.ts`). */
|
|
201
|
-
export function ganttTree(tasks: BoardTask[], items: Map<string, BoardItem>): GanttTree {
|
|
202
|
-
const parents = rootedParents(tasks);
|
|
203
|
-
const nodes = new Map<string, GanttNode>(
|
|
204
|
-
tasks.map((task) => [
|
|
205
|
-
task.id,
|
|
206
|
-
{ task, item: items.get(task.id) ?? null, span: null, children: [] },
|
|
207
|
-
]),
|
|
208
|
-
);
|
|
209
|
-
const roots: GanttNode[] = [];
|
|
210
|
-
for (const task of tasks) {
|
|
211
|
-
const node = nodes.get(task.id);
|
|
212
|
-
if (!node) continue;
|
|
213
|
-
const parent = parents.get(task.id) ?? null;
|
|
214
|
-
const target = parent === null ? undefined : nodes.get(parent);
|
|
215
|
-
if (target === undefined) roots.push(node);
|
|
216
|
-
else target.children.push(node);
|
|
217
|
-
}
|
|
218
|
-
for (const root of roots) fillSpan(root);
|
|
219
|
-
return {
|
|
220
|
-
roots: roots.filter((node) => node.span !== null),
|
|
221
|
-
undated: roots.filter((node) => node.span === null),
|
|
222
|
-
};
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// Bottom-up, so a parent's span is its own dates and everything under it. Depth is capped at five
|
|
226
|
-
// (#285), so the recursion is bounded, and `rootedParents` has already broken any cycle.
|
|
227
|
-
function fillSpan(node: GanttNode): GanttSpan | null {
|
|
228
|
-
let span: GanttSpan | null =
|
|
229
|
-
node.item === null ? null : { startAt: node.item.startAt, endAt: node.item.endAt };
|
|
230
|
-
for (const child of node.children) {
|
|
231
|
-
const below = fillSpan(child);
|
|
232
|
-
if (below === null) continue;
|
|
233
|
-
span =
|
|
234
|
-
span === null
|
|
235
|
-
? below
|
|
236
|
-
: {
|
|
237
|
-
startAt: below.startAt < span.startAt ? below.startAt : span.startAt,
|
|
238
|
-
endAt: below.endAt > span.endAt ? below.endAt : span.endAt,
|
|
239
|
-
};
|
|
240
|
-
}
|
|
241
|
-
node.span = span;
|
|
242
|
-
return span;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
export interface GanttRow {
|
|
246
|
-
task: BoardTask;
|
|
247
|
-
depth: number;
|
|
248
|
-
item: BoardItem | null;
|
|
249
|
-
hasChildren: boolean;
|
|
250
|
-
collapsed: boolean;
|
|
251
|
-
/**
|
|
252
|
-
* The bar this row actually draws.
|
|
253
|
-
*
|
|
254
|
-
* ⚠️ `derived` is the load-bearing half. A derived bar is the subtree rolled into one — an epic
|
|
255
|
-
* over its children, or a collapsed parent standing in for everything beneath it — and dragging it
|
|
256
|
-
* would have to write every task it covers. One gesture, five writes, five versions and no way to
|
|
257
|
-
* say which of them the reader meant. It is drawn and never dragged; the tasks under it are.
|
|
258
|
-
*/
|
|
259
|
-
bar: (GanttSpan & { derived: boolean }) | null;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/** The visible rows, in hierarchy order, with whatever is collapsed left out. */
|
|
263
|
-
export function ganttRows(nodes: GanttNode[], collapsed: ReadonlySet<string>): GanttRow[] {
|
|
264
|
-
const rows: GanttRow[] = [];
|
|
265
|
-
const walk = (list: GanttNode[], depth: number) => {
|
|
266
|
-
for (const node of list) {
|
|
267
|
-
const hasChildren = node.children.length > 0;
|
|
268
|
-
const shut = hasChildren && collapsed.has(node.task.id);
|
|
269
|
-
rows.push({
|
|
270
|
-
task: node.task,
|
|
271
|
-
depth,
|
|
272
|
-
item: node.item,
|
|
273
|
-
hasChildren,
|
|
274
|
-
collapsed: shut,
|
|
275
|
-
bar: barOf(node, shut, hasChildren),
|
|
276
|
-
});
|
|
277
|
-
if (hasChildren && !shut) walk(node.children, depth + 1);
|
|
278
|
-
}
|
|
279
|
-
};
|
|
280
|
-
walk(nodes, 0);
|
|
281
|
-
return rows;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
function barOf(
|
|
285
|
-
node: GanttNode,
|
|
286
|
-
shut: boolean,
|
|
287
|
-
hasChildren: boolean,
|
|
288
|
-
): (GanttSpan & { derived: boolean }) | null {
|
|
289
|
-
// Its own dates, whenever they are the whole truth about this row. A parent that is shut is
|
|
290
|
-
// standing in for its children as well, so its own bar is not that.
|
|
291
|
-
if (node.item !== null && !shut) {
|
|
292
|
-
return { startAt: node.item.startAt, endAt: node.item.endAt, derived: false };
|
|
293
|
-
}
|
|
294
|
-
// Otherwise the subtree, which is what an epic spanning its children is — the DoD's first reading
|
|
295
|
-
// of "the outline is visible", with collapsing as the second. Both are here rather than one.
|
|
296
|
-
if (hasChildren && node.span !== null) {
|
|
297
|
-
return { startAt: node.span.startAt, endAt: node.span.endAt, derived: true };
|
|
298
|
-
}
|
|
299
|
-
return null;
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
/**
|
|
303
|
-
* One `dependsOn` as an arrow — the point of the ticket.
|
|
304
|
-
*
|
|
305
|
-
* ⚠️ `blocking` is `board.blockedBy` and nothing else (anchrd/intel#311), so an arrow means exactly
|
|
306
|
-
* what the "Blocked" chip on a card and the thick edge in the graph mean. A second reading of "still
|
|
307
|
-
* open" here would be the third one on the same screen, and the day they disagree is the day the
|
|
308
|
-
* picture stops being evidence.
|
|
309
|
-
*/
|
|
310
|
-
export interface GanttLink {
|
|
311
|
-
id: string;
|
|
312
|
-
fromId: string;
|
|
313
|
-
toId: string;
|
|
314
|
-
blocking: boolean;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
export interface GanttLinks {
|
|
318
|
-
links: GanttLink[];
|
|
319
|
-
/**
|
|
320
|
-
* How many dependencies could not be drawn.
|
|
321
|
-
*
|
|
322
|
-
* ⚠️ Counted and said out loud, never dropped in silence. An arrow needs a bar at BOTH ends, and
|
|
323
|
-
* a dependency on a task with no date — or on one hidden inside a collapsed epic — has no second
|
|
324
|
-
* end to point at. Re-pointing it at the nearest visible ancestor would draw a relationship the
|
|
325
|
-
* board does not contain, which is worse than not drawing it; saying nothing would make a chart
|
|
326
|
-
* with three arrows look complete when the board has five.
|
|
327
|
-
*/
|
|
328
|
-
hidden: number;
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
export function ganttLinks(
|
|
332
|
-
rows: GanttRow[],
|
|
333
|
-
blockedBy: (task: BoardTask) => BoardTask[],
|
|
334
|
-
): GanttLinks {
|
|
335
|
-
const drawn = new Set(rows.filter((row) => row.bar !== null).map((row) => row.task.id));
|
|
336
|
-
const links: GanttLink[] = [];
|
|
337
|
-
let hidden = 0;
|
|
338
|
-
for (const row of rows) {
|
|
339
|
-
if (row.bar === null) {
|
|
340
|
-
// Its dependencies are not "hidden": this row has no bar of its own to point at either, so
|
|
341
|
-
// there was never an arrow to lose. Counting them would report a gap that does not exist.
|
|
342
|
-
continue;
|
|
343
|
-
}
|
|
344
|
-
const open = new Set(blockedBy(row.task).map((task) => task.id));
|
|
345
|
-
/**
|
|
346
|
-
* ⚠️ Once per pair, however often the list names it.
|
|
347
|
-
*
|
|
348
|
-
* Waiting twice for the same task is waiting for it once: two arrows on exactly the same path
|
|
349
|
-
* would draw nothing extra and hand React two children with one key.
|
|
350
|
-
*
|
|
351
|
-
* ⚠️ `dependsOn` carries a uniqueness rule since anchrd/intel#318 — the write path refuses a
|
|
352
|
-
* repeat and a stored one is folded away on read — so nothing arriving through Intel names a
|
|
353
|
-
* task twice any more. This stays for the same reason the graph view's copy of it does: a view
|
|
354
|
-
* takes whatever list it is handed, and it may not be the thing that turns odd data into a
|
|
355
|
-
* blank screen. Before that ticket the same duplicate made the graph THROW.
|
|
356
|
-
*/
|
|
357
|
-
const seen = new Set<string>();
|
|
358
|
-
for (const fromId of row.task.dependsOn) {
|
|
359
|
-
if (seen.has(fromId)) continue;
|
|
360
|
-
seen.add(fromId);
|
|
361
|
-
if (!drawn.has(fromId)) {
|
|
362
|
-
hidden += 1;
|
|
363
|
-
continue;
|
|
364
|
-
}
|
|
365
|
-
links.push({
|
|
366
|
-
id: `${fromId}->${row.task.id}`,
|
|
367
|
-
fromId,
|
|
368
|
-
toId: row.task.id,
|
|
369
|
-
blocking: open.has(fromId),
|
|
370
|
-
});
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
return { links, hidden };
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
export interface GanttPoint {
|
|
377
|
-
x: number;
|
|
378
|
-
y: number;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
/**
|
|
382
|
-
* The corners an arrow turns, from the end of what must finish to the start of what waits.
|
|
383
|
-
*
|
|
384
|
-
* ⚠️ Two shapes, because a dependency can point BACKWARDS in time — nothing stops a board from
|
|
385
|
-
* saying a task waits for something that ends after it starts, and that is precisely the state a
|
|
386
|
-
* reader needs to see. Drawn as the straight forward case it would be a line running right to left
|
|
387
|
-
* through both bars, which reads as a dependency in the other direction.
|
|
388
|
-
*/
|
|
389
|
-
export function linkPoints(
|
|
390
|
-
from: { x: number; y: number },
|
|
391
|
-
to: { x: number; y: number },
|
|
392
|
-
gap: number,
|
|
393
|
-
): GanttPoint[] {
|
|
394
|
-
if (to.x >= from.x + gap * 2) {
|
|
395
|
-
const turn = to.x - gap;
|
|
396
|
-
return [
|
|
397
|
-
{ x: from.x, y: from.y },
|
|
398
|
-
{ x: turn, y: from.y },
|
|
399
|
-
{ x: turn, y: to.y },
|
|
400
|
-
{ x: to.x, y: to.y },
|
|
401
|
-
];
|
|
402
|
-
}
|
|
403
|
-
// Backwards: out to the right of the predecessor, along the seam between the two rows, back to
|
|
404
|
-
// the left of the dependent, and into it from the left — so the arrowhead still says "this waits
|
|
405
|
-
// for that" rather than the reverse.
|
|
406
|
-
const seam = (from.y + to.y) / 2;
|
|
407
|
-
return [
|
|
408
|
-
{ x: from.x, y: from.y },
|
|
409
|
-
{ x: from.x + gap, y: from.y },
|
|
410
|
-
{ x: from.x + gap, y: seam },
|
|
411
|
-
{ x: to.x - gap, y: seam },
|
|
412
|
-
{ x: to.x - gap, y: to.y },
|
|
413
|
-
{ x: to.x, y: to.y },
|
|
414
|
-
];
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
export function pathOf(points: GanttPoint[]): string {
|
|
418
|
-
return points
|
|
419
|
-
.map(
|
|
420
|
-
(point, index) => `${index === 0 ? "M" : "L"}${Math.round(point.x)} ${Math.round(point.y)}`,
|
|
421
|
-
)
|
|
422
|
-
.join(" ");
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// How tall a row is drawn. Here rather than in the component because the arrows are measured in it.
|
|
426
|
-
export const GanttRowHeight = 34;
|
|
427
|
-
|
|
428
|
-
// How far an arrow stands clear of a bar before it turns. Also the depth of the detour a backwards
|
|
429
|
-
// dependency takes, so it cannot come out flush with the bar it points at.
|
|
430
|
-
const LinkGap = 9;
|
|
431
|
-
|
|
432
|
-
/** A span as the day index it starts on and the number of days it covers, ends included. */
|
|
433
|
-
export function barDays(span: GanttSpan, start: Date): { from: number; days: number } {
|
|
434
|
-
return {
|
|
435
|
-
from: daysBetween(start, span.startAt),
|
|
436
|
-
// Ends included: a task that starts and is due on the same day is one day wide, not none.
|
|
437
|
-
days: daysBetween(span.startAt, span.endAt) + 1,
|
|
438
|
-
};
|
|
439
|
-
}
|
|
440
|
-
|
|
441
|
-
/**
|
|
442
|
-
* Every arrow, as the path it is drawn along.
|
|
443
|
-
*
|
|
444
|
-
* ⚠️ It leaves the END of what has to finish and arrives at the START of what waits — finish to
|
|
445
|
-
* start, which is the only reading `dependsOn` has (#285: a task waits for another task, not for a
|
|
446
|
-
* date). Anchoring either end anywhere else would draw a relationship the contract cannot express.
|
|
447
|
-
*/
|
|
448
|
-
export function linkGeometry(
|
|
449
|
-
links: GanttLink[],
|
|
450
|
-
rows: GanttRow[],
|
|
451
|
-
axisStart: Date,
|
|
452
|
-
width: number,
|
|
453
|
-
rowHeight: number,
|
|
454
|
-
): { id: string; path: string; blocking: boolean }[] {
|
|
455
|
-
const placed = new Map<string, { index: number; bar: GanttSpan }>();
|
|
456
|
-
rows.forEach((row, index) => {
|
|
457
|
-
if (row.bar !== null) placed.set(row.task.id, { index, bar: row.bar });
|
|
458
|
-
});
|
|
459
|
-
const anchors = (id: string) => {
|
|
460
|
-
const found = placed.get(id);
|
|
461
|
-
if (found === undefined) return null;
|
|
462
|
-
const { from, days } = barDays(found.bar, axisStart);
|
|
463
|
-
return {
|
|
464
|
-
left: from * width,
|
|
465
|
-
right: (from + days) * width,
|
|
466
|
-
y: found.index * rowHeight + rowHeight / 2,
|
|
467
|
-
};
|
|
468
|
-
};
|
|
469
|
-
return links.flatMap((link) => {
|
|
470
|
-
const from = anchors(link.fromId);
|
|
471
|
-
const to = anchors(link.toId);
|
|
472
|
-
// `ganttLinks` has already counted anything that cannot be drawn; this is only the type saying
|
|
473
|
-
// the same thing again.
|
|
474
|
-
if (from === null || to === null) return [];
|
|
475
|
-
return [
|
|
476
|
-
{
|
|
477
|
-
id: link.id,
|
|
478
|
-
blocking: link.blocking,
|
|
479
|
-
path: pathOf(linkPoints({ x: from.right, y: from.y }, { x: to.left, y: to.y }, LinkGap)),
|
|
480
|
-
},
|
|
481
|
-
];
|
|
482
|
-
});
|
|
483
|
-
}
|
|
484
|
-
|
|
485
|
-
export type GanttEdit = "move" | "start" | "end";
|
|
486
|
-
|
|
487
|
-
/**
|
|
488
|
-
* What a finished drag is worth, as the one `board_task_update` it is — or as nothing.
|
|
489
|
-
*
|
|
490
|
-
* ⚠️ It answers `null` for a gesture that changed no date, and that is half of what it is for. A
|
|
491
|
-
* bar picked up and put back is the commonest drag there is; sent anyway it would mint a version,
|
|
492
|
-
* bump the board and stand in the audit as work that did not happen — the same rule `dropOutcome`
|
|
493
|
-
* holds for a card dropped on its own place.
|
|
494
|
-
*
|
|
495
|
-
* ⚠️ A move shifts the dates the task HAS. A task with only a due date is drawn as the single day it
|
|
496
|
-
* is due on, and dragging that bar must not invent a start date for it: the reader moved a task, they
|
|
497
|
-
* did not say when it begins. Widening the bar from its left edge is how a task gets one, and that is
|
|
498
|
-
* a different gesture with a different handle.
|
|
499
|
-
*
|
|
500
|
-
* ⚠️ Neither edge may cross the other. The adapter already refuses to draw a backwards bar — a start
|
|
501
|
-
* after the due date is a board somebody typed wrong — so a resize that would produce one is pinned
|
|
502
|
-
* at the day it collides with instead. Writing it would store a span this chart cannot draw, which
|
|
503
|
-
* is the shape of a bug that survives a reload.
|
|
504
|
-
*
|
|
505
|
-
* ⚠️ "Changed" is measured against the edge that was DRAWN (`item`), never against the field that
|
|
506
|
-
* was stored (`task`), and the difference between those two is a real defect this once had. A task
|
|
507
|
-
* with no start date is drawn as the one day it is due on, so `item.startAt` is that day while
|
|
508
|
-
* `task.startDate` is `null`. Dragging the left grip to the RIGHT then pins at the due date, and
|
|
509
|
-
* comparing the pinned day against `null` says "changed" — for a gesture during which the bar does
|
|
510
|
-
* not move by a pixel, because the preview clamps at one day. The reader drags, watches nothing
|
|
511
|
-
* happen, lets go, and the task quietly acquires a start date. The rule that closes it is the one
|
|
512
|
-
* the preview already follows: a bar that ends where it was drawn is worth nothing.
|
|
513
|
-
*/
|
|
514
|
-
export function ganttWrite(
|
|
515
|
-
task: BoardTask,
|
|
516
|
-
item: BoardItem,
|
|
517
|
-
edit: GanttEdit,
|
|
518
|
-
deltaDays: number,
|
|
519
|
-
): UpdateTask | null {
|
|
520
|
-
if (edit === "move") {
|
|
521
|
-
if (deltaDays === 0) return null;
|
|
522
|
-
const dueDate = dayValue(addDays(item.endAt, deltaDays));
|
|
523
|
-
// `startDate` travels only if there is one. `item.startAt` falls back to the due date, so
|
|
524
|
-
// sending it unconditionally would write a start date onto every task that never had one.
|
|
525
|
-
return task.startDate === null
|
|
526
|
-
? { taskId: task.id, dueDate }
|
|
527
|
-
: { taskId: task.id, startDate: dayValue(addDays(item.startAt, deltaDays)), dueDate };
|
|
528
|
-
}
|
|
529
|
-
if (edit === "start") {
|
|
530
|
-
const moved = addDays(item.startAt, deltaDays);
|
|
531
|
-
const startDate = dayValue(moved > item.endAt ? item.endAt : moved);
|
|
532
|
-
return startDate === dayValue(item.startAt) ? null : { taskId: task.id, startDate };
|
|
533
|
-
}
|
|
534
|
-
const moved = addDays(item.endAt, deltaDays);
|
|
535
|
-
// ⚠️ Pinned at the bar's own left edge, which for a task with no start date is the due date
|
|
536
|
-
// itself — so the shortest a bar can be dragged is the one day it already is, and the gesture
|
|
537
|
-
// answers `null` instead of moving a task that was not picked up. The clamp has to be the one the
|
|
538
|
-
// reader SAW: a preview that stopped at one day and a write that carried on past it would store a
|
|
539
|
-
// date nobody dragged to.
|
|
540
|
-
const dueDate = dayValue(moved < item.startAt ? item.startAt : moved);
|
|
541
|
-
// Against the drawn edge, as above. `item.endAt` IS `task.dueDate` for everything that reaches a
|
|
542
|
-
// bar, so this changes no answer — it states the rule once instead of twice, which is what kept
|
|
543
|
-
// the other branch wrong.
|
|
544
|
-
return dueDate === dayValue(item.endAt) ? null : { taskId: task.id, dueDate };
|
|
545
|
-
}
|