@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,413 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
type ColumnFiltersState,
|
|
3
|
-
type ColumnVisibilityState,
|
|
4
|
-
columnFilteringFeature,
|
|
5
|
-
columnGroupingFeature,
|
|
6
|
-
columnVisibilityFeature,
|
|
7
|
-
createColumnHelper,
|
|
8
|
-
createExpandedRowModel,
|
|
9
|
-
createFilteredRowModel,
|
|
10
|
-
createGroupedRowModel,
|
|
11
|
-
createSortedRowModel,
|
|
12
|
-
type ExpandedState,
|
|
13
|
-
filterFn_includesString,
|
|
14
|
-
type GroupingState,
|
|
15
|
-
rowExpandingFeature,
|
|
16
|
-
rowSortingFeature,
|
|
17
|
-
type SortingState,
|
|
18
|
-
sortFn_alphanumeric,
|
|
19
|
-
sortFn_basic,
|
|
20
|
-
tableFeatures,
|
|
21
|
-
useTable,
|
|
22
|
-
} from "@tanstack/react-table";
|
|
23
|
-
import { ChevronDown, ChevronRight, Columns3, Search } from "lucide-react";
|
|
24
|
-
import { useMemo, useState } from "react";
|
|
25
|
-
import { BoardAssigneeLabel } from "@/board/board-card/board-card.tsx";
|
|
26
|
-
import { shownTasks } from "@/board/board-data/board-data.ts";
|
|
27
|
-
import { labelOf, toneOf } from "@/board/board-status/board-status.ts";
|
|
28
|
-
import type { BoardViewProps } from "@/board/board-views/board-views.types.ts";
|
|
29
|
-
import {
|
|
30
|
-
DropdownMenu,
|
|
31
|
-
DropdownMenuCheckboxItem,
|
|
32
|
-
DropdownMenuContent,
|
|
33
|
-
DropdownMenuTrigger,
|
|
34
|
-
} from "@/components/ui/dropdown-menu";
|
|
35
|
-
import { useI18n } from "@/i18n/i18n-context.tsx";
|
|
36
|
-
import { type BoardRow, boardRows } from "./board-table.ts";
|
|
37
|
-
|
|
38
|
-
// ⚠️ Registered one by one rather than through `stockFeatures`. Every feature the board's table does
|
|
39
|
-
// not use — pagination, selection, pinning, resizing, faceting — would otherwise be in the bundle
|
|
40
|
-
// of a screen that never calls it, and the list below is also the honest answer to "what can this
|
|
41
|
-
// table do".
|
|
42
|
-
const features = tableFeatures({
|
|
43
|
-
columnFilteringFeature,
|
|
44
|
-
columnGroupingFeature,
|
|
45
|
-
columnVisibilityFeature,
|
|
46
|
-
rowExpandingFeature,
|
|
47
|
-
rowSortingFeature,
|
|
48
|
-
// ⚠️ A row-model slot has to stand AFTER the feature it needs. Filtering before grouping and
|
|
49
|
-
// expanding last is the order the rows actually pass through.
|
|
50
|
-
filteredRowModel: createFilteredRowModel(),
|
|
51
|
-
sortedRowModel: createSortedRowModel(),
|
|
52
|
-
groupedRowModel: createGroupedRowModel(),
|
|
53
|
-
expandedRowModel: createExpandedRowModel(),
|
|
54
|
-
filterFns: { includesString: filterFn_includesString },
|
|
55
|
-
sortFns: { alphanumeric: sortFn_alphanumeric, basic: sortFn_basic },
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const helper = createColumnHelper<typeof features, BoardRow>();
|
|
59
|
-
|
|
60
|
-
// The one place a grouping is offered. Not free-form: a board has fixed fields (#285) and there is
|
|
61
|
-
// no UI to add one, so grouping by anything else would be grouping by something nobody can create.
|
|
62
|
-
const groupBy = ["none", "status", "assignee"] as const;
|
|
63
|
-
type GroupBy = (typeof groupBy)[number];
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* The board as a table (anchrd/intel#286).
|
|
67
|
-
*
|
|
68
|
-
* ⚠️ The hierarchy is `getSubRows` and nothing else. Epic, task and subtask are a DEPTH in this
|
|
69
|
-
* schema and not three types (#285), so the table has one row shape and indents it by
|
|
70
|
-
* `row.depth` — there is no second column saying which of the three a row is, because the schema
|
|
71
|
-
* has no such fact to show.
|
|
72
|
-
*
|
|
73
|
-
* ⚠️ The columns are fixed and there is no UI to add one. A board is not a table node (`kind:
|
|
74
|
-
* "table"`), and a custom-field editor here would be a second, weaker version of the thing Intel
|
|
75
|
-
* already has for exactly that.
|
|
76
|
-
*/
|
|
77
|
-
export function BoardTable({ board, select, showArchived }: BoardViewProps) {
|
|
78
|
-
const i18n = useI18n();
|
|
79
|
-
const [sorting, setSorting] = useState<SortingState>([]);
|
|
80
|
-
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
|
81
|
-
const [columnVisibility, setColumnVisibility] = useState<ColumnVisibilityState>({});
|
|
82
|
-
const [expanded, setExpanded] = useState<ExpandedState>(true);
|
|
83
|
-
const [grouping, setGrouping] = useState<GroupingState>([]);
|
|
84
|
-
|
|
85
|
-
const data = useMemo(
|
|
86
|
-
() => boardRows(shownTasks(board.tasks, showArchived)),
|
|
87
|
-
[board.tasks, showArchived],
|
|
88
|
-
);
|
|
89
|
-
|
|
90
|
-
const columns = useMemo(
|
|
91
|
-
() =>
|
|
92
|
-
helper.columns([
|
|
93
|
-
helper.accessor("title", {
|
|
94
|
-
id: "title",
|
|
95
|
-
header: i18n.t("common.title"),
|
|
96
|
-
filterFn: "includesString",
|
|
97
|
-
sortFn: "alphanumeric",
|
|
98
|
-
// Grouping by the title would make one group per row, which is the table again with an
|
|
99
|
-
// extra click in front of it.
|
|
100
|
-
enableGrouping: false,
|
|
101
|
-
}),
|
|
102
|
-
helper.accessor((task) => labelOf(task.status, board.statuses), {
|
|
103
|
-
id: "status",
|
|
104
|
-
header: i18n.t("board.column.status"),
|
|
105
|
-
sortFn: "alphanumeric",
|
|
106
|
-
}),
|
|
107
|
-
helper.accessor(
|
|
108
|
-
// ⚠️ Grouped by a STABLE key, never by the rendered name. Most person ids cannot be
|
|
109
|
-
// resolved to a name in this browser (`useUserName`), so grouping by what is drawn would
|
|
110
|
-
// put every unnamed person into one group called "assigned".
|
|
111
|
-
(task) =>
|
|
112
|
-
task.assignee === null
|
|
113
|
-
? ""
|
|
114
|
-
: task.assignee.type === "agent"
|
|
115
|
-
? `agent:${task.assignee.nodeId}`
|
|
116
|
-
: `user:${task.assignee.id}`,
|
|
117
|
-
{
|
|
118
|
-
id: "assignee",
|
|
119
|
-
header: i18n.t("board.column.assignee"),
|
|
120
|
-
sortFn: "alphanumeric",
|
|
121
|
-
cell: ({ row }) =>
|
|
122
|
-
row.original.assignee === null ? (
|
|
123
|
-
<span className="text-muted-foreground">{i18n.t("board.unassigned")}</span>
|
|
124
|
-
) : (
|
|
125
|
-
<BoardAssigneeLabel assignee={row.original.assignee} />
|
|
126
|
-
),
|
|
127
|
-
},
|
|
128
|
-
),
|
|
129
|
-
/**
|
|
130
|
-
* ⚠️ `undefined` for a task with no due date, and never `null`.
|
|
131
|
-
*
|
|
132
|
-
* `sortUndefined` only fires on `undefined` (`createSortedRowModel`), so with `null` the
|
|
133
|
-
* dateless rows fall through to the sort function — where `null < "2026-08-19"` and
|
|
134
|
-
* `null > "2026-08-19"` are both false. That makes the comparator report "equal" for pairs
|
|
135
|
-
* that are not, which is not a comparator at all: the sort then produces an order that
|
|
136
|
-
* depends on where the dateless rows happened to sit. Observed as a "sorted" column reading
|
|
137
|
-
* 8/6, 8/19, —, 8/14.
|
|
138
|
-
*
|
|
139
|
-
* An ISO date sorts correctly as a string, so `basic` is the whole of the comparison.
|
|
140
|
-
*/
|
|
141
|
-
helper.accessor((task) => task.dueDate ?? undefined, {
|
|
142
|
-
id: "dueDate",
|
|
143
|
-
header: i18n.t("board.column.dueDate"),
|
|
144
|
-
sortFn: "basic",
|
|
145
|
-
sortUndefined: "last",
|
|
146
|
-
enableGrouping: false,
|
|
147
|
-
cell: ({ row }) => {
|
|
148
|
-
const value = row.original.dueDate;
|
|
149
|
-
return value === null ? (
|
|
150
|
-
<span className="text-muted-foreground">—</span>
|
|
151
|
-
) : (
|
|
152
|
-
<time dateTime={value} className="tabular-nums">
|
|
153
|
-
{new Date(`${value}T00:00:00`).toLocaleDateString(i18n.locale)}
|
|
154
|
-
</time>
|
|
155
|
-
);
|
|
156
|
-
},
|
|
157
|
-
}),
|
|
158
|
-
helper.accessor((task) => task.labels.join(", "), {
|
|
159
|
-
id: "labels",
|
|
160
|
-
header: i18n.t("board.column.labels"),
|
|
161
|
-
filterFn: "includesString",
|
|
162
|
-
enableGrouping: false,
|
|
163
|
-
enableSorting: false,
|
|
164
|
-
}),
|
|
165
|
-
]),
|
|
166
|
-
[i18n, board.statuses],
|
|
167
|
-
);
|
|
168
|
-
|
|
169
|
-
const table = useTable({
|
|
170
|
-
features,
|
|
171
|
-
columns,
|
|
172
|
-
data,
|
|
173
|
-
// The hierarchy the DoD asks for, in one option. Everything else about depth — the indent, the
|
|
174
|
-
// chevron, which rows exist at all — follows from it.
|
|
175
|
-
getSubRows: (row: BoardRow) => row.subRows,
|
|
176
|
-
getRowId: (row: BoardRow) => row.id,
|
|
177
|
-
state: { sorting, columnFilters, columnVisibility, expanded, grouping },
|
|
178
|
-
onSortingChange: setSorting,
|
|
179
|
-
onColumnFiltersChange: setColumnFilters,
|
|
180
|
-
onColumnVisibilityChange: setColumnVisibility,
|
|
181
|
-
onExpandedChange: setExpanded,
|
|
182
|
-
onGroupingChange: setGrouping,
|
|
183
|
-
/**
|
|
184
|
-
* ⚠️ Off, and it has to be off.
|
|
185
|
-
*
|
|
186
|
-
* TanStack resets the expanded state whenever the row structure changes, and on a board the row
|
|
187
|
-
* structure changes constantly: every card edit, every drag and every archived-switch flip
|
|
188
|
-
* hands `data` a new array. With the reset on, opening an epic and then renaming anything on the
|
|
189
|
-
* board closes it again — and turning grouping on collapsed every group the moment it appeared,
|
|
190
|
-
* which is how this was found (the groups in the first screenshot were shut).
|
|
191
|
-
*/
|
|
192
|
-
autoResetExpanded: false,
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const currentGrouping: GroupBy = (grouping[0] as GroupBy) ?? "none";
|
|
196
|
-
const titleFilter = String(table.getColumn("title")?.getFilterValue() ?? "");
|
|
197
|
-
|
|
198
|
-
return (
|
|
199
|
-
<div className="flex min-h-0 flex-1 flex-col gap-3 p-6">
|
|
200
|
-
<div className="flex flex-wrap items-center gap-2">
|
|
201
|
-
<label className="relative">
|
|
202
|
-
<span className="sr-only">{i18n.t("board.filterTitle")}</span>
|
|
203
|
-
<Search
|
|
204
|
-
aria-hidden="true"
|
|
205
|
-
className="pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground"
|
|
206
|
-
/>
|
|
207
|
-
<input
|
|
208
|
-
type="search"
|
|
209
|
-
value={titleFilter}
|
|
210
|
-
placeholder={i18n.t("board.filterTitle")}
|
|
211
|
-
onChange={(event) =>
|
|
212
|
-
table.getColumn("title")?.setFilterValue(event.currentTarget.value || undefined)
|
|
213
|
-
}
|
|
214
|
-
className="h-8 w-56 rounded-md border bg-background pr-3 pl-8 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
215
|
-
/>
|
|
216
|
-
</label>
|
|
217
|
-
<label className="flex items-center gap-2 text-sm">
|
|
218
|
-
<span className="text-muted-foreground">{i18n.t("board.groupBy")}</span>
|
|
219
|
-
<select
|
|
220
|
-
value={currentGrouping}
|
|
221
|
-
onChange={(event) => {
|
|
222
|
-
const next = event.currentTarget.value as GroupBy;
|
|
223
|
-
setGrouping(next === "none" ? [] : [next]);
|
|
224
|
-
// A group that opens closed is a screen with nothing on it. Regrouping therefore
|
|
225
|
-
// expands again, which is also the state the table starts in.
|
|
226
|
-
setExpanded(true);
|
|
227
|
-
}}
|
|
228
|
-
className="h-8 rounded-md border bg-background px-2 text-sm outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
229
|
-
>
|
|
230
|
-
{groupBy.map((option) => (
|
|
231
|
-
<option key={option} value={option}>
|
|
232
|
-
{i18n.t(`board.groupBy.${option}`)}
|
|
233
|
-
</option>
|
|
234
|
-
))}
|
|
235
|
-
</select>
|
|
236
|
-
</label>
|
|
237
|
-
<DropdownMenu>
|
|
238
|
-
<DropdownMenuTrigger className="ml-auto inline-flex h-8 items-center gap-2 rounded-md border px-3 text-sm outline-none hover:bg-muted focus-visible:ring-2 focus-visible:ring-ring">
|
|
239
|
-
<Columns3 aria-hidden="true" className="size-4" />
|
|
240
|
-
{i18n.t("board.columns")}
|
|
241
|
-
</DropdownMenuTrigger>
|
|
242
|
-
<DropdownMenuContent align="end">
|
|
243
|
-
{table.getAllLeafColumns().map((column) => (
|
|
244
|
-
<DropdownMenuCheckboxItem
|
|
245
|
-
key={column.id}
|
|
246
|
-
checked={column.getIsVisible()}
|
|
247
|
-
onCheckedChange={(value) => column.toggleVisibility(Boolean(value))}
|
|
248
|
-
// The last visible column cannot be hidden: an empty table is a screen that has
|
|
249
|
-
// lost its data as far as anybody looking at it can tell.
|
|
250
|
-
disabled={column.getIsVisible() && table.getVisibleLeafColumns().length === 1}
|
|
251
|
-
>
|
|
252
|
-
{i18n.t(`board.column.${column.id === "title" ? "title" : column.id}`)}
|
|
253
|
-
</DropdownMenuCheckboxItem>
|
|
254
|
-
))}
|
|
255
|
-
</DropdownMenuContent>
|
|
256
|
-
</DropdownMenu>
|
|
257
|
-
</div>
|
|
258
|
-
<div className="min-h-0 flex-1 overflow-auto rounded-lg border">
|
|
259
|
-
<table className="w-full border-collapse text-sm">
|
|
260
|
-
<thead className="sticky top-0 z-10 bg-card">
|
|
261
|
-
{table.getHeaderGroups().map((group) => (
|
|
262
|
-
<tr key={group.id}>
|
|
263
|
-
{group.headers.map((header) => (
|
|
264
|
-
<th
|
|
265
|
-
key={header.id}
|
|
266
|
-
scope="col"
|
|
267
|
-
aria-sort={
|
|
268
|
-
header.column.getIsSorted() === "asc"
|
|
269
|
-
? "ascending"
|
|
270
|
-
: header.column.getIsSorted() === "desc"
|
|
271
|
-
? "descending"
|
|
272
|
-
: "none"
|
|
273
|
-
}
|
|
274
|
-
className="border-b px-3 py-2 text-left font-medium whitespace-nowrap"
|
|
275
|
-
>
|
|
276
|
-
{header.isPlaceholder ? null : header.column.getCanSort() ? (
|
|
277
|
-
<button
|
|
278
|
-
type="button"
|
|
279
|
-
onClick={header.column.getToggleSortingHandler()}
|
|
280
|
-
className="inline-flex items-center gap-1 rounded outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring"
|
|
281
|
-
>
|
|
282
|
-
<table.FlexRender header={header} />
|
|
283
|
-
<span aria-hidden="true" className="text-muted-foreground">
|
|
284
|
-
{header.column.getIsSorted() === "asc"
|
|
285
|
-
? "↑"
|
|
286
|
-
: header.column.getIsSorted() === "desc"
|
|
287
|
-
? "↓"
|
|
288
|
-
: ""}
|
|
289
|
-
</span>
|
|
290
|
-
</button>
|
|
291
|
-
) : (
|
|
292
|
-
<table.FlexRender header={header} />
|
|
293
|
-
)}
|
|
294
|
-
</th>
|
|
295
|
-
))}
|
|
296
|
-
</tr>
|
|
297
|
-
))}
|
|
298
|
-
</thead>
|
|
299
|
-
<tbody>
|
|
300
|
-
{table.getRowModel().rows.map((row) => (
|
|
301
|
-
<tr key={row.id} className="border-b last:border-b-0 hover:bg-muted/60">
|
|
302
|
-
{row.getVisibleCells().map((cell) => (
|
|
303
|
-
<td key={cell.id} className="px-3 py-2 align-top">
|
|
304
|
-
{/* ⚠️ A group row carries the grouped value and NOTHING else. Left to render
|
|
305
|
-
itself, every other cell of a group row falls back to the first task in the
|
|
306
|
-
group — so the heading "Backlog 3" came with one arbitrary member's title
|
|
307
|
-
and its date beside it, which reads as a row of its own and is a lie about
|
|
308
|
-
the two below it. */}
|
|
309
|
-
{row.getIsGrouped() && !cell.getIsGrouped() ? null : cell.getIsGrouped() ? (
|
|
310
|
-
<button
|
|
311
|
-
type="button"
|
|
312
|
-
onClick={row.getToggleExpandedHandler()}
|
|
313
|
-
aria-expanded={row.getIsExpanded()}
|
|
314
|
-
className="inline-flex items-center gap-2 rounded font-medium outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
315
|
-
>
|
|
316
|
-
{row.getIsExpanded() ? (
|
|
317
|
-
<ChevronDown aria-hidden="true" className="size-4" />
|
|
318
|
-
) : (
|
|
319
|
-
<ChevronRight aria-hidden="true" className="size-4" />
|
|
320
|
-
)}
|
|
321
|
-
<GroupLabel row={row} columnId={cell.column.id} value={cell.getValue()} />
|
|
322
|
-
<span className="text-xs text-muted-foreground tabular-nums">
|
|
323
|
-
{row.subRows.length}
|
|
324
|
-
</span>
|
|
325
|
-
</button>
|
|
326
|
-
) : cell.getIsPlaceholder() ? null : cell.column.id === "title" ? (
|
|
327
|
-
<div
|
|
328
|
-
className="flex min-w-0 items-start gap-1.5"
|
|
329
|
-
// ⚠️ The indent is a style and not a nested table. A row nested in markup
|
|
330
|
-
// could not be sorted or grouped with its siblings, and a screen reader
|
|
331
|
-
// would read four tables where there is one.
|
|
332
|
-
style={{ paddingInlineStart: `${row.depth * 1.25}rem` }}
|
|
333
|
-
>
|
|
334
|
-
{row.getCanExpand() ? (
|
|
335
|
-
<button
|
|
336
|
-
type="button"
|
|
337
|
-
onClick={row.getToggleExpandedHandler()}
|
|
338
|
-
aria-expanded={row.getIsExpanded()}
|
|
339
|
-
aria-label={i18n.t("board.toggleSubtasks", {
|
|
340
|
-
task: row.original.title,
|
|
341
|
-
})}
|
|
342
|
-
className="mt-0.5 rounded outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
343
|
-
>
|
|
344
|
-
{row.getIsExpanded() ? (
|
|
345
|
-
<ChevronDown aria-hidden="true" className="size-4" />
|
|
346
|
-
) : (
|
|
347
|
-
<ChevronRight aria-hidden="true" className="size-4" />
|
|
348
|
-
)}
|
|
349
|
-
</button>
|
|
350
|
-
) : (
|
|
351
|
-
<span aria-hidden="true" className="size-4 shrink-0" />
|
|
352
|
-
)}
|
|
353
|
-
<button
|
|
354
|
-
type="button"
|
|
355
|
-
onClick={() => select(row.original.id)}
|
|
356
|
-
className="min-w-0 rounded text-left outline-none hover:underline focus-visible:ring-2 focus-visible:ring-ring"
|
|
357
|
-
>
|
|
358
|
-
{row.original.title}
|
|
359
|
-
</button>
|
|
360
|
-
{board.blockedBy(row.original).length > 0 ? (
|
|
361
|
-
<span className="shrink-0 rounded-md border border-destructive px-1 text-xs text-destructive">
|
|
362
|
-
{i18n.t("board.blocked")}
|
|
363
|
-
</span>
|
|
364
|
-
) : null}
|
|
365
|
-
</div>
|
|
366
|
-
) : cell.column.id === "status" ? (
|
|
367
|
-
<span className="inline-flex items-center gap-1.5 whitespace-nowrap">
|
|
368
|
-
<span
|
|
369
|
-
aria-hidden="true"
|
|
370
|
-
className={`size-2 rounded-full ${toneOf(row.original.status, board.statuses).dot}`}
|
|
371
|
-
/>
|
|
372
|
-
<table.FlexRender cell={cell} />
|
|
373
|
-
</span>
|
|
374
|
-
) : (
|
|
375
|
-
<table.FlexRender cell={cell} />
|
|
376
|
-
)}
|
|
377
|
-
</td>
|
|
378
|
-
))}
|
|
379
|
-
</tr>
|
|
380
|
-
))}
|
|
381
|
-
</tbody>
|
|
382
|
-
</table>
|
|
383
|
-
{table.getRowModel().rows.length === 0 ? (
|
|
384
|
-
<p className="p-6 text-sm text-muted-foreground">{i18n.t("board.noTasks")}</p>
|
|
385
|
-
) : null}
|
|
386
|
-
</div>
|
|
387
|
-
</div>
|
|
388
|
-
);
|
|
389
|
-
}
|
|
390
|
-
|
|
391
|
-
/**
|
|
392
|
-
* The value a group was formed on, said in words.
|
|
393
|
-
*
|
|
394
|
-
* ⚠️ The assignee column groups on a STABLE key (`user:…`, `agent:…`) and that key is not a name.
|
|
395
|
-
* The heading is therefore read off the first row of the group rather than off the key — that is
|
|
396
|
-
* the only place an agent's title can be resolved, and the only place a person who cannot be named
|
|
397
|
-
* is named honestly rather than by their id.
|
|
398
|
-
*/
|
|
399
|
-
function GroupLabel({
|
|
400
|
-
row,
|
|
401
|
-
columnId,
|
|
402
|
-
value,
|
|
403
|
-
}: {
|
|
404
|
-
row: { subRows: { original: BoardRow }[] };
|
|
405
|
-
columnId: string;
|
|
406
|
-
value: unknown;
|
|
407
|
-
}) {
|
|
408
|
-
const i18n = useI18n();
|
|
409
|
-
if (columnId !== "assignee") return <span>{String(value ?? "")}</span>;
|
|
410
|
-
const assignee = row.subRows[0]?.original.assignee ?? null;
|
|
411
|
-
if (assignee === null) return <span>{i18n.t("board.unassigned")}</span>;
|
|
412
|
-
return <BoardAssigneeLabel assignee={assignee} />;
|
|
413
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { CalendarDays, ChartGantt, KanbanSquare, Share2, Table2 } from "lucide-react";
|
|
2
|
-
import { lazy, Suspense } from "react";
|
|
3
|
-
import { BoardCalendar } from "@/board/board-calendar/board-calendar.tsx";
|
|
4
|
-
import { BoardGantt } from "@/board/board-gantt/board-gantt.tsx";
|
|
5
|
-
import { BoardKanban } from "@/board/board-kanban/board-kanban.tsx";
|
|
6
|
-
import { BoardTable } from "@/board/board-table/board-table.tsx";
|
|
7
|
-
import type { BoardView, BoardViewProps } from "./board-views.types.ts";
|
|
8
|
-
|
|
9
|
-
// Sigma and graphology are the heaviest thing on this screen and only one view needs them, the
|
|
10
|
-
// same arrangement `graph-pane` already has for the relation graph.
|
|
11
|
-
const BoardGraph = lazy(async () => ({
|
|
12
|
-
default: (await import("@/board/board-graph/board-graph.tsx")).BoardGraph,
|
|
13
|
-
}));
|
|
14
|
-
|
|
15
|
-
/**
|
|
16
|
-
* Which views a board has (anchrd/intel#286).
|
|
17
|
-
*
|
|
18
|
-
* ⚠️ A LIST, and the switch above it renders whatever is in the list — there is deliberately no
|
|
19
|
-
* `switch (view)` with one case per view anywhere. Gantt (anchrd/intel#293) proved it: it is one
|
|
20
|
-
* more entry here and nothing else, reading the same `BoardHandle` and the same dated items
|
|
21
|
-
* (`board-items.ts`), so the work it cost was a renderer. Hard-wired cases would have made it a
|
|
22
|
-
* change to the screen, the switch, the URL parser and the tests instead.
|
|
23
|
-
*
|
|
24
|
-
* ⚠️ The order is the order the switch draws. Kanban first because it is what a board is opened
|
|
25
|
-
* for; the graph last because it answers a question one has after looking, not before.
|
|
26
|
-
*/
|
|
27
|
-
export const boardViews: readonly BoardView[] = [
|
|
28
|
-
{
|
|
29
|
-
id: "kanban",
|
|
30
|
-
labelKey: "board.view.kanban",
|
|
31
|
-
icon: KanbanSquare,
|
|
32
|
-
render: (props: BoardViewProps) => <BoardKanban {...props} />,
|
|
33
|
-
},
|
|
34
|
-
{
|
|
35
|
-
id: "table",
|
|
36
|
-
labelKey: "board.view.table",
|
|
37
|
-
icon: Table2,
|
|
38
|
-
render: (props: BoardViewProps) => <BoardTable {...props} />,
|
|
39
|
-
},
|
|
40
|
-
{
|
|
41
|
-
id: "calendar",
|
|
42
|
-
labelKey: "board.view.calendar",
|
|
43
|
-
icon: CalendarDays,
|
|
44
|
-
render: (props: BoardViewProps) => <BoardCalendar {...props} />,
|
|
45
|
-
},
|
|
46
|
-
{
|
|
47
|
-
id: "gantt",
|
|
48
|
-
labelKey: "board.view.gantt",
|
|
49
|
-
icon: ChartGantt,
|
|
50
|
-
render: (props: BoardViewProps) => <BoardGantt {...props} />,
|
|
51
|
-
},
|
|
52
|
-
{
|
|
53
|
-
id: "graph",
|
|
54
|
-
labelKey: "board.view.graph",
|
|
55
|
-
icon: Share2,
|
|
56
|
-
render: (props: BoardViewProps) => (
|
|
57
|
-
<Suspense fallback={null}>
|
|
58
|
-
<BoardGraph {...props} />
|
|
59
|
-
</Suspense>
|
|
60
|
-
),
|
|
61
|
-
},
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
export function boardViewById(id: string | null): BoardView {
|
|
65
|
-
// The first entry is the fallback rather than a named one: a view removed from the list must not
|
|
66
|
-
// leave a screen pointing at nothing, and an unknown id in a bookmarked URL is the same case.
|
|
67
|
-
return boardViews.find((view) => view.id === id) ?? (boardViews[0] as BoardView);
|
|
68
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { LucideIcon } from "lucide-react";
|
|
2
|
-
import type { ReactNode } from "react";
|
|
3
|
-
import type { BoardHandle } from "@/board/board-data/board-data.types.ts";
|
|
4
|
-
|
|
5
|
-
/**
|
|
6
|
-
* Everything a view of a board gets, and the whole of it.
|
|
7
|
-
*
|
|
8
|
-
* ⚠️ It carries no loader and no writer of its own — `board` is the one data layer, and a view that
|
|
9
|
-
* reached past it would be a second cache entry for the same board.
|
|
10
|
-
*/
|
|
11
|
-
export interface BoardViewProps {
|
|
12
|
-
board: BoardHandle;
|
|
13
|
-
// How a view asks for the detail panel. `null` closes it. There is deliberately no "which task is
|
|
14
|
-
// open" alongside it: no view highlights the selection today, and a prop threaded through four
|
|
15
|
-
// renderers that none of them reads is a prop that will be wrong before it is used.
|
|
16
|
-
select(taskId: string | null): void;
|
|
17
|
-
// Whether the shelf is on screen. One switch above every view rather than one per view: it is a
|
|
18
|
-
// question about the board, not about how the board is drawn.
|
|
19
|
-
showArchived: boolean;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface BoardView {
|
|
23
|
-
// What stands in the URL and in the switch. A view keeps its id forever; a bookmarked board must
|
|
24
|
-
// not open on a different view because a label was rewritten.
|
|
25
|
-
id: string;
|
|
26
|
-
labelKey: string;
|
|
27
|
-
icon: LucideIcon;
|
|
28
|
-
render(props: BoardViewProps): ReactNode;
|
|
29
|
-
}
|