@12-apps/ui 1.5.1 → 1.6.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 +5 -5
- package/src/components/data-display/DataViews/DataViewsBoard.tsx +327 -0
- package/src/components/data-display/DataViews/DataViewsGrid.tsx +80 -77
- package/src/components/data-display/DataViews/DataViewsTableBase.tsx +178 -230
- package/src/components/data-display/DataViews/DeleteViewDialog.tsx +83 -0
- package/src/components/data-display/DataViews/SaveViewModal.tsx +140 -52
- package/src/components/data-display/DataViews/data-views-columns-tab.tsx +240 -0
- package/src/components/data-display/DataViews/data-views-columns.ts +129 -0
- package/src/components/data-display/DataViews/data-views-dirty.ts +62 -0
- package/src/components/data-display/DataViews/data-views-display-panel.tsx +328 -0
- package/src/components/data-display/DataViews/data-views-display-tabs.tsx +328 -0
- package/src/components/data-display/DataViews/data-views-empty.tsx +91 -0
- package/src/components/data-display/DataViews/data-views-export.tsx +260 -0
- package/src/components/data-display/DataViews/data-views-glyphs.tsx +103 -0
- package/src/components/data-display/DataViews/data-views-grid-bodies.tsx +340 -0
- package/src/components/data-display/DataViews/data-views-grid-parts.tsx +220 -242
- package/src/components/data-display/DataViews/data-views-inline-bar.tsx +187 -81
- package/src/components/data-display/DataViews/data-views-layout-context.tsx +215 -79
- package/src/components/data-display/DataViews/data-views-more-filters.tsx +300 -0
- package/src/components/data-display/DataViews/data-views-overflow.ts +302 -0
- package/src/components/data-display/DataViews/data-views-preview.ts +11 -3
- package/src/components/data-display/DataViews/data-views-range-pill.tsx +162 -92
- package/src/components/data-display/DataViews/data-views-range-values.ts +69 -0
- package/src/components/data-display/DataViews/data-views-scopes.tsx +356 -0
- package/src/components/data-display/DataViews/data-views-search.tsx +156 -0
- package/src/components/data-display/DataViews/data-views-select-all-strip.tsx +105 -0
- package/src/components/data-display/DataViews/data-views-selection.ts +50 -0
- package/src/components/data-display/DataViews/data-views-server-query.ts +75 -0
- package/src/components/data-display/DataViews/data-views-shell-toolbar.tsx +87 -0
- package/src/components/data-display/DataViews/data-views-table-parts.tsx +5 -0
- package/src/components/data-display/DataViews/data-views-toolbar.tsx +63 -48
- package/src/components/data-display/DataViews/data-views-types.ts +88 -9
- package/src/components/data-display/DataViews/data-views-view-nav.tsx +378 -0
- package/src/components/data-display/DataViews/index.ts +14 -1
- package/src/components/data-display/DataViews/use-data-views-state.ts +158 -134
- package/src/components/data-display/DataViews/use-saved-views-controller.ts +171 -0
- package/src/components/layout/ContentToolbar/ContentToolbar.tsx +86 -30
- package/src/components/layout/ContentToolbar/ContentToolbar.types.ts +25 -0
- package/src/components/layout/ContentToolbar/MultiSelectDropdown.tsx +6 -1
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { DataGrid, type GridColumn, type GridSort } from "../DataGrid";
|
|
4
|
+
import { Box } from "../../../mui/Box";
|
|
5
|
+
|
|
6
|
+
import {
|
|
7
|
+
cardMinWidthForZoom,
|
|
8
|
+
cardScaleForZoom,
|
|
9
|
+
DENSITY_CARD_COLUMNS,
|
|
10
|
+
DENSITY_ROW_PADDING,
|
|
11
|
+
useDataViewsLayout,
|
|
12
|
+
} from "./data-views-layout-context";
|
|
13
|
+
import { DataViewsBoard, type BoardConfig } from "./DataViewsBoard";
|
|
14
|
+
import { SelectAllStrip } from "./data-views-select-all-strip";
|
|
15
|
+
import type { DataViewCardSelection } from "./data-views-types";
|
|
16
|
+
import type { DataViewsController } from "./use-data-views-state";
|
|
17
|
+
|
|
18
|
+
/* ── Body (grid) ─────────────────────────────────────────────────────────── */
|
|
19
|
+
|
|
20
|
+
interface GridBodyProps<T extends Record<string, unknown>> {
|
|
21
|
+
rows: T[];
|
|
22
|
+
columns: GridColumn<T>[];
|
|
23
|
+
getRowId: (row: T) => string | number;
|
|
24
|
+
selectedIds: Array<string | number>;
|
|
25
|
+
onChangeSelected: (ids: Array<string | number>) => void;
|
|
26
|
+
sortBy: GridSort[];
|
|
27
|
+
onChangeSortBy: (next: GridSort[]) => void;
|
|
28
|
+
/** "server" defers ordering to the backend (rows render as-is); "client" sorts in-grid. */
|
|
29
|
+
sortMode: "client" | "server";
|
|
30
|
+
/** Vertical cell padding, from the density preference. */
|
|
31
|
+
rowPadding: number;
|
|
32
|
+
dataTestId?: string;
|
|
33
|
+
emptyState?: React.ReactNode;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** The dense DataGrid with multi-select, wrapped in the scrollable table region. */
|
|
37
|
+
function GridBody<T extends Record<string, unknown>>({
|
|
38
|
+
rows,
|
|
39
|
+
columns,
|
|
40
|
+
getRowId,
|
|
41
|
+
selectedIds,
|
|
42
|
+
onChangeSelected,
|
|
43
|
+
sortBy,
|
|
44
|
+
onChangeSortBy,
|
|
45
|
+
sortMode,
|
|
46
|
+
rowPadding,
|
|
47
|
+
dataTestId,
|
|
48
|
+
emptyState,
|
|
49
|
+
}: GridBodyProps<T>): React.JSX.Element {
|
|
50
|
+
return (
|
|
51
|
+
<Box
|
|
52
|
+
sx={{
|
|
53
|
+
width: "100%",
|
|
54
|
+
overflowX: "auto",
|
|
55
|
+
mt: 1.5,
|
|
56
|
+
// Tabwoah-style dense rows: MUI's default TableCell padding keeps rows
|
|
57
|
+
// tall regardless of rowHeight, so the DENSITY preference is applied
|
|
58
|
+
// here rather than through `rowHeight`, which it would fight.
|
|
59
|
+
"& .MuiTableCell-root": { py: rowPadding, fontSize: "0.8125rem" },
|
|
60
|
+
"& .MuiTableCell-head": { py: 0.5, fontSize: "0.75rem" },
|
|
61
|
+
}}
|
|
62
|
+
>
|
|
63
|
+
<DataGrid<T>
|
|
64
|
+
rows={rows}
|
|
65
|
+
columns={columns}
|
|
66
|
+
getRowId={(row) => getRowId(row)}
|
|
67
|
+
virtualizeRows={false}
|
|
68
|
+
density="compact"
|
|
69
|
+
rowHeight={36}
|
|
70
|
+
headerHeight={36}
|
|
71
|
+
selection={{ mode: "multi", selectedRowIds: selectedIds, onChangeSelected }}
|
|
72
|
+
sorting={{ mode: sortMode, sortBy, onChangeSortBy }}
|
|
73
|
+
data-testid={dataTestId}
|
|
74
|
+
emptyState={emptyState}
|
|
75
|
+
/>
|
|
76
|
+
</Box>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/* ── Body (cards) ────────────────────────────────────────────────────────── */
|
|
81
|
+
|
|
82
|
+
interface CardBodyProps<T extends Record<string, unknown>> {
|
|
83
|
+
rows: T[];
|
|
84
|
+
renderCard: (row: T, selection: DataViewCardSelection) => React.ReactNode;
|
|
85
|
+
getRowId: (row: T) => string | number;
|
|
86
|
+
selectedIds: Set<string | number>;
|
|
87
|
+
onToggleId: (id: string | number) => void;
|
|
88
|
+
minCardWidth: number;
|
|
89
|
+
/** Cap on cards per row, from the density preference. */
|
|
90
|
+
maxColumns: number;
|
|
91
|
+
/** Content scale (padding + type) handed to each card, from the zoom slider. */
|
|
92
|
+
cardScale: number;
|
|
93
|
+
dataTestId?: string;
|
|
94
|
+
emptyState?: React.ReactNode;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* The "Grade" layout: the filtered/sorted rows rendered as an auto-filling grid
|
|
99
|
+
* of entity-supplied cards. Reuses `rows` (= `c.matched`), so search/filter/sort
|
|
100
|
+
* apply; the column width comes from the zoom slider, and each card is handed its
|
|
101
|
+
* selection state so it can drive its own checkbox (BaseCard) — the same
|
|
102
|
+
* selection model as the table.
|
|
103
|
+
*/
|
|
104
|
+
function CardBody<T extends Record<string, unknown>>({
|
|
105
|
+
rows,
|
|
106
|
+
renderCard,
|
|
107
|
+
getRowId,
|
|
108
|
+
selectedIds,
|
|
109
|
+
onToggleId,
|
|
110
|
+
minCardWidth,
|
|
111
|
+
maxColumns,
|
|
112
|
+
cardScale,
|
|
113
|
+
dataTestId,
|
|
114
|
+
emptyState,
|
|
115
|
+
}: CardBodyProps<T>): React.JSX.Element {
|
|
116
|
+
if (rows.length === 0) {
|
|
117
|
+
return <Box sx={{ mt: 1.5 }}>{emptyState}</Box>;
|
|
118
|
+
}
|
|
119
|
+
return (
|
|
120
|
+
<Box
|
|
121
|
+
sx={{
|
|
122
|
+
mt: 1.5,
|
|
123
|
+
display: "grid",
|
|
124
|
+
// Fixed inter-card gap — deliberately NOT scaled by the zoom slider, so
|
|
125
|
+
// only the cards grow while the space between them stays constant.
|
|
126
|
+
gap: 1.5,
|
|
127
|
+
// `auto-fill` still decides how many FIT; density caps how many are
|
|
128
|
+
// ALLOWED, so "Poucos" reads as bigger cards rather than more of them.
|
|
129
|
+
gridTemplateColumns: {
|
|
130
|
+
xs: "1fr",
|
|
131
|
+
sm: `repeat(auto-fill, minmax(max(${minCardWidth}px, calc((100% - ${(maxColumns - 1) * 12}px) / ${maxColumns})), 1fr))`,
|
|
132
|
+
},
|
|
133
|
+
}}
|
|
134
|
+
data-testid={dataTestId ? `${dataTestId}-cards` : "data-views-cards"}
|
|
135
|
+
>
|
|
136
|
+
{rows.map((row) => {
|
|
137
|
+
const id = getRowId(row);
|
|
138
|
+
return (
|
|
139
|
+
<Box key={id}>
|
|
140
|
+
{renderCard(row, {
|
|
141
|
+
selected: selectedIds.has(id),
|
|
142
|
+
onToggleSelect: () => onToggleId(id),
|
|
143
|
+
scale: cardScale,
|
|
144
|
+
})}
|
|
145
|
+
</Box>
|
|
146
|
+
);
|
|
147
|
+
})}
|
|
148
|
+
</Box>
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/* ── Body (list) ─────────────────────────────────────────────────────────── */
|
|
153
|
+
|
|
154
|
+
interface ListBodyProps<T extends Record<string, unknown>> {
|
|
155
|
+
rows: T[];
|
|
156
|
+
renderListRow: (row: T, selection: DataViewCardSelection) => React.ReactNode;
|
|
157
|
+
getRowId: (row: T) => string | number;
|
|
158
|
+
selectedIds: Set<string | number>;
|
|
159
|
+
onToggleId: (id: string | number) => void;
|
|
160
|
+
/** Gap between rows, from the density preference. */
|
|
161
|
+
rowGap: number;
|
|
162
|
+
dataTestId?: string;
|
|
163
|
+
emptyState?: React.ReactNode;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* The "Lista" layout: one FULL-WIDTH row per record, rendered by the entity — a
|
|
168
|
+
* marker, a title, a subtitle and a value on the right is the shape it was
|
|
169
|
+
* designed for (FUT-733).
|
|
170
|
+
*
|
|
171
|
+
* It sits between the table and the cards rather than replacing either: the
|
|
172
|
+
* table is for comparing many columns, the cards are for browsing, and the list
|
|
173
|
+
* is for scanning a queue on a narrow screen. Selection is the SAME model as
|
|
174
|
+
* both — each row receives its selection state, so it can drive its own
|
|
175
|
+
* checkbox and the bulk menu behaves identically in every layout.
|
|
176
|
+
*
|
|
177
|
+
* `scale` is handed over as 1 rather than the zoom multiplier, and the zoom
|
|
178
|
+
* slider is hidden in this layout: a full-width row has no card size to
|
|
179
|
+
* multiply. See {@link DataViewsZoomSlider}.
|
|
180
|
+
*/
|
|
181
|
+
function ListBody<T extends Record<string, unknown>>({
|
|
182
|
+
rows,
|
|
183
|
+
renderListRow,
|
|
184
|
+
getRowId,
|
|
185
|
+
selectedIds,
|
|
186
|
+
onToggleId,
|
|
187
|
+
rowGap,
|
|
188
|
+
dataTestId,
|
|
189
|
+
emptyState,
|
|
190
|
+
}: ListBodyProps<T>): React.JSX.Element {
|
|
191
|
+
if (rows.length === 0) {
|
|
192
|
+
return <Box sx={{ mt: 1.5 }}>{emptyState}</Box>;
|
|
193
|
+
}
|
|
194
|
+
return (
|
|
195
|
+
<Box
|
|
196
|
+
sx={{ mt: 1.5, display: "flex", flexDirection: "column", gap: rowGap }}
|
|
197
|
+
data-testid={dataTestId ? `${dataTestId}-list` : "data-views-list"}
|
|
198
|
+
>
|
|
199
|
+
{rows.map((row) => {
|
|
200
|
+
const id = getRowId(row);
|
|
201
|
+
return (
|
|
202
|
+
<Box key={id}>
|
|
203
|
+
{renderListRow(row, {
|
|
204
|
+
selected: selectedIds.has(id),
|
|
205
|
+
onToggleSelect: () => onToggleId(id),
|
|
206
|
+
scale: 1,
|
|
207
|
+
})}
|
|
208
|
+
</Box>
|
|
209
|
+
);
|
|
210
|
+
})}
|
|
211
|
+
</Box>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/* ── Body selector (board vs list vs cards vs table, from context) ───────── */
|
|
216
|
+
|
|
217
|
+
interface GridMainProps<T extends Record<string, unknown>> {
|
|
218
|
+
c: DataViewsController<T>;
|
|
219
|
+
getRowId: (row: T) => string | number;
|
|
220
|
+
renderCard?: (row: T, selection: DataViewCardSelection) => React.ReactNode;
|
|
221
|
+
/** Opt-in "Lista" layout — one full-width row per record. */
|
|
222
|
+
renderListRow?: (row: T, selection: DataViewCardSelection) => React.ReactNode;
|
|
223
|
+
/** Opt-in "Quadro" (board) layout — needs `renderCard`, since it reuses the card. */
|
|
224
|
+
board?: BoardConfig<T>;
|
|
225
|
+
dataTestId?: string;
|
|
226
|
+
emptyState?: React.ReactNode;
|
|
227
|
+
testIdPrefix: string;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* The headerless layouts, each preceded by its own select-all.
|
|
232
|
+
*
|
|
233
|
+
* The strip is rendered HERE rather than inside each body so all three agree on
|
|
234
|
+
* where it sits and what it says — and so the table, which already has one in
|
|
235
|
+
* its `<thead>`, never gets a second.
|
|
236
|
+
*/
|
|
237
|
+
function Headerless<T extends Record<string, unknown>>({
|
|
238
|
+
c,
|
|
239
|
+
getRowId,
|
|
240
|
+
testIdPrefix,
|
|
241
|
+
children,
|
|
242
|
+
}: {
|
|
243
|
+
c: DataViewsController<T>;
|
|
244
|
+
getRowId: (row: T) => string | number;
|
|
245
|
+
testIdPrefix: string;
|
|
246
|
+
children: React.ReactNode;
|
|
247
|
+
}): React.JSX.Element {
|
|
248
|
+
return (
|
|
249
|
+
<>
|
|
250
|
+
<SelectAllStrip
|
|
251
|
+
rows={c.matched}
|
|
252
|
+
getRowId={getRowId}
|
|
253
|
+
selectedIds={c.selectedIds}
|
|
254
|
+
onChange={(next) => c.setSelectedIds(next)}
|
|
255
|
+
testIdPrefix={testIdPrefix}
|
|
256
|
+
/>
|
|
257
|
+
{children}
|
|
258
|
+
</>
|
|
259
|
+
);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Picks the body from the layout context: board, list, cards, or the dense grid. */
|
|
263
|
+
export function GridMain<T extends Record<string, unknown>>({
|
|
264
|
+
c,
|
|
265
|
+
getRowId,
|
|
266
|
+
renderCard,
|
|
267
|
+
renderListRow,
|
|
268
|
+
board,
|
|
269
|
+
dataTestId,
|
|
270
|
+
emptyState,
|
|
271
|
+
testIdPrefix,
|
|
272
|
+
}: GridMainProps<T>): React.JSX.Element {
|
|
273
|
+
const { layout, zoom, density } = useDataViewsLayout();
|
|
274
|
+
if (layout === "list" && renderListRow) {
|
|
275
|
+
return (
|
|
276
|
+
<Headerless c={c} getRowId={getRowId} testIdPrefix={testIdPrefix}>
|
|
277
|
+
<ListBody
|
|
278
|
+
rows={c.matched}
|
|
279
|
+
renderListRow={renderListRow}
|
|
280
|
+
getRowId={getRowId}
|
|
281
|
+
selectedIds={c.selectedIds}
|
|
282
|
+
onToggleId={c.toggleId}
|
|
283
|
+
rowGap={DENSITY_ROW_PADDING[density]}
|
|
284
|
+
dataTestId={dataTestId}
|
|
285
|
+
emptyState={emptyState}
|
|
286
|
+
/>
|
|
287
|
+
</Headerless>
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
if (layout === "board" && board && renderCard) {
|
|
291
|
+
return (
|
|
292
|
+
<Headerless c={c} getRowId={getRowId} testIdPrefix={testIdPrefix}>
|
|
293
|
+
<DataViewsBoard
|
|
294
|
+
rows={c.matched}
|
|
295
|
+
board={board}
|
|
296
|
+
getRowId={getRowId}
|
|
297
|
+
renderCard={renderCard}
|
|
298
|
+
selectedIds={c.selectedIds}
|
|
299
|
+
onToggleId={c.toggleId}
|
|
300
|
+
cardScale={cardScaleForZoom(zoom)}
|
|
301
|
+
dataTestId={dataTestId}
|
|
302
|
+
/>
|
|
303
|
+
</Headerless>
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
if (layout === "cards" && renderCard) {
|
|
307
|
+
return (
|
|
308
|
+
<Headerless c={c} getRowId={getRowId} testIdPrefix={testIdPrefix}>
|
|
309
|
+
<CardBody
|
|
310
|
+
rows={c.matched}
|
|
311
|
+
renderCard={renderCard}
|
|
312
|
+
getRowId={getRowId}
|
|
313
|
+
selectedIds={c.selectedIds}
|
|
314
|
+
onToggleId={c.toggleId}
|
|
315
|
+
minCardWidth={cardMinWidthForZoom(zoom)}
|
|
316
|
+
maxColumns={DENSITY_CARD_COLUMNS[density]}
|
|
317
|
+
cardScale={cardScaleForZoom(zoom)}
|
|
318
|
+
dataTestId={dataTestId}
|
|
319
|
+
emptyState={emptyState}
|
|
320
|
+
/>
|
|
321
|
+
</Headerless>
|
|
322
|
+
);
|
|
323
|
+
}
|
|
324
|
+
return (
|
|
325
|
+
<GridBody
|
|
326
|
+
rows={c.matched}
|
|
327
|
+
columns={c.gridColumns}
|
|
328
|
+
getRowId={getRowId}
|
|
329
|
+
selectedIds={[...c.selectedIds]}
|
|
330
|
+
onChangeSelected={(ids) => c.setSelectedIds(new Set(ids))}
|
|
331
|
+
sortBy={c.state.sortBy}
|
|
332
|
+
onChangeSortBy={(next: GridSort[]) => c.patch({ sortBy: next })}
|
|
333
|
+
sortMode={c.serverMode ? "server" : "client"}
|
|
334
|
+
rowPadding={DENSITY_ROW_PADDING[density]}
|
|
335
|
+
dataTestId={dataTestId}
|
|
336
|
+
emptyState={emptyState}
|
|
337
|
+
/>
|
|
338
|
+
);
|
|
339
|
+
}
|
|
340
|
+
|