@anchrd/intel-ui 0.38.0 → 0.40.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 +6 -2
- package/src/access-summary/access-summary.tsx +1 -11
- package/src/app/app-tree/app-tree.tsx +37 -0
- package/src/board/board-assignee/board-assignee.ts +18 -0
- package/src/board/board-data/board-data.ts +31 -5
- package/src/board/board-data/board-data.types.ts +10 -1
- package/src/board/board-kanban/board-kanban.ts +63 -9
- package/src/board/board-kanban/board-kanban.tsx +371 -63
- package/src/board/board-panel/board-panel.tsx +222 -31
- package/src/board/board-settings/board-settings.tsx +209 -0
- package/src/board/board-stripes/board-stripes.ts +128 -0
- package/src/board/board-table/board-table.ts +23 -105
- package/src/board/board-table/board-table.tsx +436 -135
- package/src/board/board-task/board-task.ts +105 -0
- package/src/board/board-task/board-task.tsx +364 -0
- package/src/frontmatter/frontmatter.tsx +11 -2
- package/src/i18n/de.json +44 -3
- package/src/i18n/en.json +44 -3
- package/src/i18n/es.json +44 -3
- package/src/router/selection-search.ts +17 -2
- package/src/styles.css +29 -25
- package/src/user-name/user-name.ts +17 -0
|
@@ -1,14 +1,6 @@
|
|
|
1
1
|
import type { BoardTask } from "@anchrd/intel-contract/board";
|
|
2
2
|
import type { ColumnWithCards } from "@/board/board-data/board-data.types.ts";
|
|
3
3
|
|
|
4
|
-
export type SortKey = "title" | "status" | "assignee" | "due" | "dependsOn";
|
|
5
|
-
export type GroupKey = "none" | "status" | "assignee";
|
|
6
|
-
|
|
7
|
-
export interface Sort {
|
|
8
|
-
key: SortKey;
|
|
9
|
-
direction: "asc" | "desc";
|
|
10
|
-
}
|
|
11
|
-
|
|
12
4
|
/**
|
|
13
5
|
* One line of the table: the card, plus the two things the card cannot answer about itself.
|
|
14
6
|
*
|
|
@@ -21,115 +13,41 @@ export interface Row {
|
|
|
21
13
|
task: BoardTask;
|
|
22
14
|
columnTitle: string;
|
|
23
15
|
blocking: string | null;
|
|
16
|
+
/** The subtasks under this one. Empty for a card nothing hangs beneath (#669). */
|
|
17
|
+
subRows: Row[];
|
|
24
18
|
}
|
|
25
19
|
|
|
20
|
+
/**
|
|
21
|
+
* The rows as a TREE: a subtask hangs under its parent (#669).
|
|
22
|
+
*
|
|
23
|
+
* ⚠️ **A subtask whose parent is not in the answer is drawn at the top level.** `board_get` returns
|
|
24
|
+
* what matches the filter, and a subtask can match while its parent does not — hung under a parent
|
|
25
|
+
* that is not there, the row would be lost from the table entirely. The contract says the same at
|
|
26
|
+
* `BoardTask.parentTaskId`; this is the reader that has to honour it.
|
|
27
|
+
*
|
|
28
|
+
* ⚠️ **The order inside a level is the board's**: columns in their configured order, cards by
|
|
29
|
+
* position. That is what a table without a sort shows, and it is the order somebody arranged by
|
|
30
|
+
* hand.
|
|
31
|
+
*/
|
|
26
32
|
export function rowsOf(columns: ColumnWithCards[]): Row[] {
|
|
27
33
|
const titles = new Map<string, string>();
|
|
28
34
|
for (const entry of columns) {
|
|
29
35
|
for (const card of entry.cards) titles.set(card.id, card.title);
|
|
30
36
|
}
|
|
31
|
-
|
|
37
|
+
const flat = columns.flatMap((entry) =>
|
|
32
38
|
entry.cards.map((task) => ({
|
|
33
39
|
task,
|
|
34
40
|
columnTitle: entry.column.title,
|
|
35
41
|
blocking: task.dependsOn === null ? null : (titles.get(task.dependsOn) ?? task.dependsOn),
|
|
42
|
+
subRows: [] as Row[],
|
|
36
43
|
})),
|
|
37
44
|
);
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
case "status":
|
|
45
|
-
return row.columnTitle;
|
|
46
|
-
case "assignee":
|
|
47
|
-
return row.task.assigneeId;
|
|
48
|
-
case "due":
|
|
49
|
-
return row.task.dueDate;
|
|
50
|
-
case "dependsOn":
|
|
51
|
-
return row.blocking;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* The table in one order.
|
|
57
|
-
*
|
|
58
|
-
* ⚠️ **An empty cell sorts last in BOTH directions, and that is not a preference.** A card without
|
|
59
|
-
* a due date is not the earliest one — it is one whose date nobody knows, and putting it first is a
|
|
60
|
-
* table that answers "what is due next" with the rows that have no answer. Comparing `null` with a
|
|
61
|
-
* string instead is worse than wrong: `null < "2026-08-19"` and `null > "2026-08-19"` are BOTH
|
|
62
|
-
* false, so the comparator claims equality between rows that are not equal, and the order then
|
|
63
|
-
* depends on where the dateless rows happened to sit.
|
|
64
|
-
*
|
|
65
|
-
* ⚠️ **The sort is stable and the incoming order is the tiebreaker.** Rows arrive in the board's
|
|
66
|
-
* own order — configured columns first, cards by position — so two cards with the same due date
|
|
67
|
-
* keep the order the board gives them rather than swapping on every render.
|
|
68
|
-
*/
|
|
69
|
-
export function sortRows(rows: Row[], sort: Sort | null): Row[] {
|
|
70
|
-
if (sort === null) return rows;
|
|
71
|
-
const factor = sort.direction === "asc" ? 1 : -1;
|
|
72
|
-
return [...rows].sort((left, right) => {
|
|
73
|
-
const a = cellOf(left, sort.key);
|
|
74
|
-
const b = cellOf(right, sort.key);
|
|
75
|
-
if (a === null && b === null) return 0;
|
|
76
|
-
if (a === null) return 1;
|
|
77
|
-
if (b === null) return -1;
|
|
78
|
-
// An ISO timestamp compares correctly as a string, so one comparison serves every column.
|
|
79
|
-
return factor * a.localeCompare(b, undefined, { numeric: true, sensitivity: "base" });
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export interface Group {
|
|
84
|
-
/**
|
|
85
|
-
* The VALUE the rows were grouped by, not the text to draw.
|
|
86
|
-
*
|
|
87
|
-
* ⚠️ The difference matters for the assignee: the value is a Gate principal id, and an id must
|
|
88
|
-
* never reach the screen (`user-name.ts`, #258). The view resolves it to a name or to the word
|
|
89
|
-
* for somebody it cannot name; this module has no business deciding that, and returning a
|
|
90
|
-
* ready-made label would have made the id the obvious thing to put there.
|
|
91
|
-
*
|
|
92
|
-
* `null` is the group the key does not answer for — unassigned, mostly.
|
|
93
|
-
*/
|
|
94
|
-
key: string | null;
|
|
95
|
-
rows: Row[];
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* The rows in groups, in the order the rows themselves arrive.
|
|
100
|
-
*
|
|
101
|
-
* ⚠️ **Not alphabetically.** Grouped by status, the groups are the board's columns, and their order
|
|
102
|
-
* carries meaning the alphabet destroys: `todo, doing, done` would be drawn `doing, done, todo`.
|
|
103
|
-
* Walking the rows and appending each new group as it first appears keeps the board's order for
|
|
104
|
-
* free — and keeps whatever `sortRows` decided when the grouping key is something else.
|
|
105
|
-
*/
|
|
106
|
-
export function groupRows(rows: Row[], key: GroupKey): Group[] {
|
|
107
|
-
if (key === "none") return [{ key: null, rows }];
|
|
108
|
-
const groups: Group[] = [];
|
|
109
|
-
for (const row of rows) {
|
|
110
|
-
const value = key === "status" ? row.columnTitle : row.task.assigneeId;
|
|
111
|
-
const existing = groups.find((group) => group.key === value);
|
|
112
|
-
if (existing === undefined) groups.push({ key: value, rows: [row] });
|
|
113
|
-
else existing.rows.push(row);
|
|
45
|
+
const byId = new Map(flat.map((row) => [row.task.id, row]));
|
|
46
|
+
const roots: Row[] = [];
|
|
47
|
+
for (const row of flat) {
|
|
48
|
+
const parent = row.task.parentTaskId === null ? undefined : byId.get(row.task.parentTaskId);
|
|
49
|
+
if (parent === undefined) roots.push(row);
|
|
50
|
+
else parent.subRows.push(row);
|
|
114
51
|
}
|
|
115
|
-
return
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** What the header of a sortable column has to say for a reader who cannot see the arrow. */
|
|
119
|
-
export function ariaSortOf(sort: Sort | null, key: SortKey): "ascending" | "descending" | "none" {
|
|
120
|
-
if (sort === null || sort.key !== key) return "none";
|
|
121
|
-
return sort.direction === "asc" ? "ascending" : "descending";
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* What a click on a header means.
|
|
126
|
-
*
|
|
127
|
-
* ⚠️ Three states, not two: ascending, descending, and back to the board's own order. Without the
|
|
128
|
-
* third there is no way back — once a table is sorted it stays sorted, and the order the board was
|
|
129
|
-
* arranged in by hand is unreachable for the rest of the session.
|
|
130
|
-
*/
|
|
131
|
-
export function nextSort(sort: Sort | null, key: SortKey): Sort | null {
|
|
132
|
-
if (sort === null || sort.key !== key) return { key, direction: "asc" };
|
|
133
|
-
if (sort.direction === "asc") return { key, direction: "desc" };
|
|
134
|
-
return null;
|
|
52
|
+
return roots;
|
|
135
53
|
}
|