@12-apps/ui 1.5.2 → 1.6.1
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 +4 -4
- 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
- package/src/components/utility/InstallPrompt/InstallPrompt.earlyCapture.ts +117 -0
- package/src/components/utility/InstallPrompt/InstallPrompt.md +52 -4
- package/src/components/utility/InstallPrompt/InstallPrompt.types.ts +19 -0
- package/src/components/utility/InstallPrompt/index.ts +8 -0
- package/src/components/utility/InstallPrompt/usePwaInstall.ts +39 -11
|
@@ -1,31 +1,28 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import { useState } from "react";
|
|
3
|
+
import { useEffect, useState } from "react";
|
|
5
4
|
|
|
6
|
-
import {
|
|
7
|
-
DataGrid,
|
|
8
|
-
type GridColumn,
|
|
9
|
-
type GridSort,
|
|
10
|
-
} from "../DataGrid";
|
|
11
|
-
import {
|
|
12
|
-
type ColumnVisibilityOption,
|
|
13
|
-
} from "../../layout/ContentToolbar";
|
|
14
5
|
import { TableFilter } from "../../layout/TableFilter";
|
|
6
|
+
import { Text } from "../../typography/Text";
|
|
15
7
|
import { Box } from "../../../mui/Box";
|
|
16
8
|
import { Stack } from "../../../mui/Stack";
|
|
17
9
|
|
|
18
10
|
import {
|
|
19
|
-
cardMinWidthForZoom,
|
|
20
|
-
cardScaleForZoom,
|
|
21
11
|
DataViewsLayoutProvider,
|
|
22
12
|
useDataViewsLayout,
|
|
23
13
|
type DataViewsLayout,
|
|
24
14
|
} from "./data-views-layout-context";
|
|
25
15
|
import { FilterDialog, GridFilterPanel } from "./data-views-filter-panel";
|
|
26
|
-
import {
|
|
27
|
-
import {
|
|
16
|
+
import { GridMain } from "./data-views-grid-bodies";
|
|
17
|
+
import { InlineFilterChips, InlineFilterControls } from "./data-views-inline-bar";
|
|
18
|
+
import { toOverflowFields, useFilterOverflow, type OverflowSplit } from "./data-views-overflow";
|
|
19
|
+
import { ShellToolbar } from "./data-views-shell-toolbar";
|
|
20
|
+
import type { DisplayPanelView } from "./data-views-display-panel";
|
|
21
|
+
import type { DataViewExport } from "./data-views-export";
|
|
22
|
+
import { DataViewsEmpty } from "./data-views-empty";
|
|
28
23
|
import { DataViewsPagination } from "./data-views-pagination";
|
|
24
|
+
import type { BoardConfig } from "./DataViewsBoard";
|
|
25
|
+
import { DataViewsScopeTabs, type ScopeConfig } from "./data-views-scopes";
|
|
29
26
|
import { togglePillValues } from "./data-views-grid-helpers";
|
|
30
27
|
import type {
|
|
31
28
|
DataViewCardSelection,
|
|
@@ -36,181 +33,82 @@ import type {
|
|
|
36
33
|
} from "./data-views-types";
|
|
37
34
|
import type { DataViewsController } from "./use-data-views-state";
|
|
38
35
|
|
|
39
|
-
/* ──
|
|
40
|
-
|
|
41
|
-
interface GridBodyProps<T extends Record<string, unknown>> {
|
|
42
|
-
rows: T[];
|
|
43
|
-
columns: GridColumn<T>[];
|
|
44
|
-
getRowId: (row: T) => string | number;
|
|
45
|
-
selectedIds: Array<string | number>;
|
|
46
|
-
onChangeSelected: (ids: Array<string | number>) => void;
|
|
47
|
-
sortBy: GridSort[];
|
|
48
|
-
onChangeSortBy: (next: GridSort[]) => void;
|
|
49
|
-
/** "server" defers ordering to the backend (rows render as-is); "client" sorts in-grid. */
|
|
50
|
-
sortMode: "client" | "server";
|
|
51
|
-
dataTestId?: string;
|
|
52
|
-
emptyState?: React.ReactNode;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/** The dense DataGrid with multi-select, wrapped in the scrollable table region. */
|
|
56
|
-
function GridBody<T extends Record<string, unknown>>({
|
|
57
|
-
rows,
|
|
58
|
-
columns,
|
|
59
|
-
getRowId,
|
|
60
|
-
selectedIds,
|
|
61
|
-
onChangeSelected,
|
|
62
|
-
sortBy,
|
|
63
|
-
onChangeSortBy,
|
|
64
|
-
sortMode,
|
|
65
|
-
dataTestId,
|
|
66
|
-
emptyState,
|
|
67
|
-
}: GridBodyProps<T>): React.JSX.Element {
|
|
68
|
-
return (
|
|
69
|
-
<Box
|
|
70
|
-
sx={{
|
|
71
|
-
width: "100%",
|
|
72
|
-
overflowX: "auto",
|
|
73
|
-
mt: 1.5,
|
|
74
|
-
// Tabwoah-style dense rows: MUI's default TableCell padding keeps rows
|
|
75
|
-
// tall regardless of rowHeight, so trim it here.
|
|
76
|
-
"& .MuiTableCell-root": { py: 0.25, fontSize: "0.8125rem" },
|
|
77
|
-
"& .MuiTableCell-head": { py: 0.5, fontSize: "0.75rem" },
|
|
78
|
-
}}
|
|
79
|
-
>
|
|
80
|
-
<DataGrid<T>
|
|
81
|
-
rows={rows}
|
|
82
|
-
columns={columns}
|
|
83
|
-
getRowId={(row) => getRowId(row)}
|
|
84
|
-
virtualizeRows={false}
|
|
85
|
-
density="compact"
|
|
86
|
-
rowHeight={36}
|
|
87
|
-
headerHeight={36}
|
|
88
|
-
selection={{ mode: "multi", selectedRowIds: selectedIds, onChangeSelected }}
|
|
89
|
-
sorting={{ mode: sortMode, sortBy, onChangeSortBy }}
|
|
90
|
-
data-testid={dataTestId}
|
|
91
|
-
emptyState={emptyState}
|
|
92
|
-
/>
|
|
93
|
-
</Box>
|
|
94
|
-
);
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
/* ── Body (cards) ────────────────────────────────────────────────────────── */
|
|
98
|
-
|
|
99
|
-
interface CardBodyProps<T extends Record<string, unknown>> {
|
|
100
|
-
rows: T[];
|
|
101
|
-
renderCard: (row: T, selection: DataViewCardSelection) => React.ReactNode;
|
|
102
|
-
getRowId: (row: T) => string | number;
|
|
103
|
-
selectedIds: Set<string | number>;
|
|
104
|
-
onToggleId: (id: string | number) => void;
|
|
105
|
-
minCardWidth: number;
|
|
106
|
-
/** Content scale (padding + type) handed to each card, from the zoom slider. */
|
|
107
|
-
cardScale: number;
|
|
108
|
-
dataTestId?: string;
|
|
109
|
-
emptyState?: React.ReactNode;
|
|
110
|
-
}
|
|
36
|
+
/* ── Header row ──────────────────────────────────────────────────────────── */
|
|
111
37
|
|
|
112
38
|
/**
|
|
113
|
-
* The
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
39
|
+
* The grid's own header row: the page title on the left, the primary page
|
|
40
|
+
* actions on the right.
|
|
41
|
+
*
|
|
42
|
+
* `title` and `headerActions` were DECLARED on `DataViewsGridProps` and
|
|
43
|
+
* `DataViewsTableBaseProps` but never destructured or forwarded — dead props
|
|
44
|
+
* whose types lied to every caller that set them. Wired here rather than
|
|
45
|
+
* deleted: a table that carries its own title keeps the title, the scope tabs
|
|
46
|
+
* and the toolbar as one block, instead of the page having to space them.
|
|
47
|
+
*
|
|
48
|
+
* Renders NOTHING when neither is supplied, so no existing table gains a row.
|
|
118
49
|
*/
|
|
119
|
-
function
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
}: CardBodyProps<T>): React.JSX.Element {
|
|
130
|
-
if (rows.length === 0) {
|
|
131
|
-
return <Box sx={{ mt: 1.5 }}>{emptyState}</Box>;
|
|
132
|
-
}
|
|
50
|
+
function GridHeaderRow({
|
|
51
|
+
title,
|
|
52
|
+
headerActions,
|
|
53
|
+
testIdPrefix,
|
|
54
|
+
}: {
|
|
55
|
+
title?: string;
|
|
56
|
+
headerActions?: React.ReactNode;
|
|
57
|
+
testIdPrefix: string;
|
|
58
|
+
}): React.JSX.Element | null {
|
|
59
|
+
if (!title && !headerActions) return null;
|
|
133
60
|
return (
|
|
134
61
|
<Box
|
|
62
|
+
data-testid={`${testIdPrefix}-header`}
|
|
135
63
|
sx={{
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
64
|
+
display: "flex",
|
|
65
|
+
alignItems: "center",
|
|
66
|
+
justifyContent: "space-between",
|
|
67
|
+
gap: 2,
|
|
68
|
+
flexWrap: "wrap",
|
|
69
|
+
pb: 1.5,
|
|
142
70
|
}}
|
|
143
|
-
data-testid={dataTestId ? `${dataTestId}-cards` : "data-views-cards"}
|
|
144
71
|
>
|
|
145
|
-
{
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
})}
|
|
154
|
-
</Box>
|
|
155
|
-
);
|
|
156
|
-
})}
|
|
72
|
+
{title ? (
|
|
73
|
+
<Text variant="heading" size="lg" as="h2">
|
|
74
|
+
{title}
|
|
75
|
+
</Text>
|
|
76
|
+
) : (
|
|
77
|
+
<Box />
|
|
78
|
+
)}
|
|
79
|
+
{headerActions && <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>{headerActions}</Box>}
|
|
157
80
|
</Box>
|
|
158
81
|
);
|
|
159
82
|
}
|
|
160
83
|
|
|
161
|
-
/* ──
|
|
84
|
+
/* ── Layout ⇄ view-state mirror ──────────────────────────────────────────── */
|
|
162
85
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
86
|
+
/**
|
|
87
|
+
* Mirrors the LIVE layout back into the view state, so saving a view captures
|
|
88
|
+
* the layout the user is actually looking at.
|
|
89
|
+
*
|
|
90
|
+
* A component rather than a callback prop because only something INSIDE the
|
|
91
|
+
* provider can read the layout, and only the controller can write the state.
|
|
92
|
+
* The `stored !== layout` guard is what makes it idempotent: `c` is rebuilt on
|
|
93
|
+
* every render, so an unguarded effect here would patch → re-render → patch.
|
|
94
|
+
*
|
|
95
|
+
* It re-fires when `stored` changes too, which is the case that matters after
|
|
96
|
+
* applying a saved view: `appliedState` REPLACES the state, so a view saved
|
|
97
|
+
* before layouts were captured clears `state.layout`, and the mirror puts the
|
|
98
|
+
* current one back rather than leaving the next save with nothing to store.
|
|
99
|
+
*/
|
|
100
|
+
function LayoutStateSync<T extends Record<string, unknown>>({
|
|
173
101
|
c,
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
renderCard={renderCard}
|
|
185
|
-
getRowId={getRowId}
|
|
186
|
-
selectedIds={c.selectedIds}
|
|
187
|
-
onToggleId={(id) => {
|
|
188
|
-
const next = new Set(c.selectedIds);
|
|
189
|
-
if (next.has(id)) next.delete(id);
|
|
190
|
-
else next.add(id);
|
|
191
|
-
c.setSelectedIds(next);
|
|
192
|
-
}}
|
|
193
|
-
minCardWidth={cardMinWidthForZoom(zoom)}
|
|
194
|
-
cardScale={cardScaleForZoom(zoom)}
|
|
195
|
-
dataTestId={dataTestId}
|
|
196
|
-
emptyState={emptyState}
|
|
197
|
-
/>
|
|
198
|
-
);
|
|
199
|
-
}
|
|
200
|
-
return (
|
|
201
|
-
<GridBody
|
|
202
|
-
rows={c.matched}
|
|
203
|
-
columns={c.gridColumns}
|
|
204
|
-
getRowId={getRowId}
|
|
205
|
-
selectedIds={[...c.selectedIds]}
|
|
206
|
-
onChangeSelected={(ids) => c.setSelectedIds(new Set(ids))}
|
|
207
|
-
sortBy={c.state.sortBy}
|
|
208
|
-
onChangeSortBy={(next: GridSort[]) => c.patch({ sortBy: next })}
|
|
209
|
-
sortMode={c.serverMode ? "server" : "client"}
|
|
210
|
-
dataTestId={dataTestId}
|
|
211
|
-
emptyState={emptyState}
|
|
212
|
-
/>
|
|
213
|
-
);
|
|
102
|
+
}: {
|
|
103
|
+
c: DataViewsController<T>;
|
|
104
|
+
}): null {
|
|
105
|
+
const { layout } = useDataViewsLayout();
|
|
106
|
+
const stored = c.state.layout;
|
|
107
|
+
useEffect(() => {
|
|
108
|
+
if (stored !== layout) c.patch({ layout });
|
|
109
|
+
// `c` is intentionally out of the deps — see the guard above.
|
|
110
|
+
}, [layout, stored]);
|
|
111
|
+
return null;
|
|
214
112
|
}
|
|
215
113
|
|
|
216
114
|
/* ── Shell (composes toolbar + body + panel) ─────────────────────────────── */
|
|
@@ -256,22 +154,36 @@ function useGridShellFilters<T extends Record<string, unknown>>({
|
|
|
256
154
|
showInline: boolean;
|
|
257
155
|
useModal: boolean;
|
|
258
156
|
inlineVisible: boolean;
|
|
259
|
-
filtersHidden: boolean;
|
|
260
|
-
toggleFilters: () => void;
|
|
261
157
|
filterProps: FilterSurfaceProps<T>;
|
|
158
|
+
split: OverflowSplit<T>;
|
|
159
|
+
onControlOpenChange: (open: boolean) => void;
|
|
262
160
|
} {
|
|
263
161
|
const { state } = c;
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
const
|
|
271
|
-
|
|
162
|
+
// How many filter controls are open. A COUNT, not a flag: closing one popover
|
|
163
|
+
// by opening another overlaps, and a boolean would unfreeze mid-handover.
|
|
164
|
+
const [openControls, setOpenControls] = useState(0);
|
|
165
|
+
// ONE measurement for the whole shell: the filter row decides which controls
|
|
166
|
+
// it can keep, and the TOOLBAR needs the same answer to drop its labels.
|
|
167
|
+
// Measuring twice would let the two disagree at the crossover width.
|
|
168
|
+
const split = useFilterOverflow(
|
|
169
|
+
toOverflowFields(fields, rangeFields),
|
|
170
|
+
state.pills,
|
|
171
|
+
c.ranges,
|
|
172
|
+
openControls > 0,
|
|
173
|
+
);
|
|
174
|
+
// THE BAR RENDERS AT EVERY WIDTH. It used to be swapped for a full-screen
|
|
175
|
+
// filter MODAL below `lg`, which was the responsive strategy before the
|
|
176
|
+
// measured ladder existed — and the two now do the same job, badly together:
|
|
177
|
+
// the modal took the bar away at 1199px, and with it the very degradation
|
|
178
|
+
// (labels off, search collapsed, filters into "Mais") that exists to make a
|
|
179
|
+
// narrow bar work. So a 900px window got no bar and no ladder, and the
|
|
180
|
+
// operator lost the search entirely rather than gaining a compact one.
|
|
181
|
+
//
|
|
182
|
+
// The ladder is measured, so it already covers every width the modal did.
|
|
183
|
+
const showInline = inlineFilters;
|
|
184
|
+
const useModal = false;
|
|
272
185
|
const inlineVisible =
|
|
273
186
|
showInline &&
|
|
274
|
-
!filtersHidden &&
|
|
275
187
|
(alwaysShowSearch || fields.length > 0 || rangeFields.length > 0 || state.search !== "");
|
|
276
188
|
const filterProps: FilterSurfaceProps<T> = {
|
|
277
189
|
testIdPrefix,
|
|
@@ -287,7 +199,15 @@ function useGridShellFilters<T extends Record<string, unknown>>({
|
|
|
287
199
|
onClearField: (fieldId) => c.patch({ pills: { ...state.pills, [fieldId]: [] } }),
|
|
288
200
|
onClearAll: () => c.patch({ search: "", pills: {}, ranges: {} }),
|
|
289
201
|
};
|
|
290
|
-
return {
|
|
202
|
+
return {
|
|
203
|
+
showInline,
|
|
204
|
+
useModal,
|
|
205
|
+
inlineVisible,
|
|
206
|
+
filterProps,
|
|
207
|
+
split,
|
|
208
|
+
onControlOpenChange: (open) =>
|
|
209
|
+
setOpenControls((count) => Math.max(0, count + (open ? 1 : -1))),
|
|
210
|
+
};
|
|
291
211
|
}
|
|
292
212
|
|
|
293
213
|
interface GridShellProps<T extends Record<string, unknown>> {
|
|
@@ -305,8 +225,30 @@ interface GridShellProps<T extends Record<string, unknown>> {
|
|
|
305
225
|
/** Opt-in "Grade" (cards) layout: renders each row as an entity-supplied card.
|
|
306
226
|
* Omit ⇒ table only (no layout toggle). */
|
|
307
227
|
renderCard?: (row: T, selection: DataViewCardSelection) => React.ReactNode;
|
|
308
|
-
/**
|
|
228
|
+
/**
|
|
229
|
+
* Opt-in "Quadro" (board) layout. Needs `renderCard` too — the board reuses
|
|
230
|
+
* the entity's card, so a board config without one simply offers no board
|
|
231
|
+
* rather than failing: that is a config gap, not a runtime error.
|
|
232
|
+
*/
|
|
233
|
+
board?: BoardConfig<T>;
|
|
234
|
+
/** Opt-in "Lista" layout: one full-width, entity-rendered row per record. */
|
|
235
|
+
renderListRow?: (row: T, selection: DataViewCardSelection) => React.ReactNode;
|
|
236
|
+
/** The page-level partition rendered as tabs above the toolbar. */
|
|
237
|
+
scopes?: ScopeConfig[];
|
|
238
|
+
/** Per-sort-field value kind, so directions read in the column's own terms. */
|
|
239
|
+
sortKinds?: Record<string, string>;
|
|
240
|
+
/** The saved-view chrome bracketing the Exibir panel. */
|
|
241
|
+
displayView?: DisplayPanelView;
|
|
242
|
+
/** Injected export — the host re-queries; the grid never fetches. */
|
|
243
|
+
exportConfig?: DataViewExport;
|
|
244
|
+
/** Page title in the grid's own header row, above the scopes + toolbar. */
|
|
245
|
+
title?: string;
|
|
246
|
+
/** Primary page actions rendered at the header row's right. */
|
|
247
|
+
headerActions?: React.ReactNode;
|
|
248
|
+
/** Which layout to show first when the user has expressed no preference (default "table"). */
|
|
309
249
|
defaultLayout?: DataViewsLayout;
|
|
250
|
+
/** Pin to `defaultLayout`, ignoring the remembered preference (stories/docs). */
|
|
251
|
+
ignoreStoredLayout?: boolean;
|
|
310
252
|
/**
|
|
311
253
|
* Opt into the responsive inline filter UX: a collapsible filter row below the
|
|
312
254
|
* toolbar on wide screens and a modal on narrow ones. When false (default),
|
|
@@ -323,78 +265,114 @@ interface GridShellProps<T extends Record<string, unknown>> {
|
|
|
323
265
|
alwaysShowSearch?: boolean;
|
|
324
266
|
}
|
|
325
267
|
|
|
326
|
-
/**
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
dataTestId,
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
}
|
|
344
|
-
|
|
345
|
-
const { showInline, useModal, inlineVisible, filtersHidden, toggleFilters, filterProps } = useGridShellFilters(
|
|
346
|
-
{ c, inlineFilters, fields, rangeFields, testIdPrefix, alwaysShowSearch },
|
|
268
|
+
/** The scrollable content region: scope tabs, toolbar, filter bar, body, pager. */
|
|
269
|
+
function ShellStack<T extends Record<string, unknown>>({
|
|
270
|
+
props,
|
|
271
|
+
filters,
|
|
272
|
+
}: {
|
|
273
|
+
props: GridShellProps<T>;
|
|
274
|
+
filters: ReturnType<typeof useGridShellFilters<T>>;
|
|
275
|
+
}): React.JSX.Element {
|
|
276
|
+
const { c, testIdPrefix, dataTestId, scopes = [], emptyState, inlineFilters = false } = props;
|
|
277
|
+
const { showInline, useModal, inlineVisible, filterProps, split, onControlOpenChange } = filters;
|
|
278
|
+
// The grid renders the FILTERED empty state itself — it is the only party
|
|
279
|
+
// that knows a filter is applied. See {@link DataViewsEmpty}.
|
|
280
|
+
const body = (
|
|
281
|
+
<DataViewsEmpty
|
|
282
|
+
filtered={c.activeFilterCount > 0 || c.state.search !== ""}
|
|
283
|
+
onClearFilters={() => c.patch({ search: "", pills: {}, ranges: {} })}
|
|
284
|
+
emptyState={emptyState}
|
|
285
|
+
testIdPrefix={testIdPrefix}
|
|
286
|
+
/>
|
|
347
287
|
);
|
|
348
|
-
const columnOptions: ColumnVisibilityOption[] = c.hideableColumns.map((col) => ({
|
|
349
|
-
id: col.id,
|
|
350
|
-
label: col.label,
|
|
351
|
-
visible: state.visibleColumns.includes(col.id),
|
|
352
|
-
}));
|
|
353
288
|
return (
|
|
354
|
-
<DataViewsLayoutProvider canUseCards={Boolean(renderCard)} defaultLayout={defaultLayout}>
|
|
355
289
|
<TableFilter open={c.filterOpen} onOpenChange={c.setFilterOpen} hasActiveFilters={c.activeFilterCount > 0}>
|
|
356
290
|
<Stack spacing={0} data-testid={dataTestId ? `${dataTestId}-container` : undefined}>
|
|
357
|
-
<
|
|
291
|
+
<GridHeaderRow title={props.title} headerActions={props.headerActions} testIdPrefix={testIdPrefix} />
|
|
292
|
+
{/* Renders nothing (and reserves nothing) for an empty scope list. */}
|
|
293
|
+
<DataViewsScopeTabs
|
|
294
|
+
scopes={scopes}
|
|
295
|
+
value={c.scope}
|
|
296
|
+
onChange={c.setScope}
|
|
297
|
+
counts={c.scopeCounts}
|
|
298
|
+
testIdPrefix={testIdPrefix}
|
|
299
|
+
/>
|
|
300
|
+
<ShellToolbar
|
|
301
|
+
c={c}
|
|
302
|
+
rows={props.rows}
|
|
303
|
+
sortKinds={props.sortKinds}
|
|
304
|
+
displayView={props.displayView}
|
|
305
|
+
exportConfig={props.exportConfig}
|
|
306
|
+
filterControls={
|
|
307
|
+
inlineVisible ? (
|
|
308
|
+
<InlineFilterControls
|
|
309
|
+
{...filterProps}
|
|
310
|
+
split={split}
|
|
311
|
+
onControlOpenChange={onControlOpenChange}
|
|
312
|
+
/>
|
|
313
|
+
) : undefined
|
|
314
|
+
}
|
|
315
|
+
barRef={split.barRef}
|
|
358
316
|
testIdPrefix={testIdPrefix}
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
sortFields={c.resolvedSortFields}
|
|
365
|
-
activeSortField={c.activeSortField}
|
|
366
|
-
activeSortOrder={c.activeSortOrder}
|
|
367
|
-
onChangeSort={(field, order) => c.patch({ sortBy: [{ id: field, dir: order }] })}
|
|
368
|
-
matchedCount={c.matched.length}
|
|
369
|
-
totalCount={c.serverMode ? c.serverTotalCount : rows.length}
|
|
370
|
-
toolbarRightSlot={toolbarRightSlot}
|
|
371
|
-
columnOptions={columnOptions}
|
|
372
|
-
onToggleColumn={c.toggleColumn}
|
|
373
|
-
filterOpen={c.filterOpen}
|
|
374
|
-
setFilterOpen={c.setFilterOpen}
|
|
375
|
-
activeFilterCount={c.activeFilterCount}
|
|
376
|
-
showFilterTrigger={!showInline}
|
|
377
|
-
showFiltersToggle={showInline}
|
|
378
|
-
filtersHidden={filtersHidden}
|
|
379
|
-
onToggleFilters={toggleFilters}
|
|
317
|
+
rowActions={props.rowActions}
|
|
318
|
+
bulkActions={props.bulkActions}
|
|
319
|
+
toolbarRightSlot={props.toolbarRightSlot}
|
|
320
|
+
compactControls={showInline && split.compactControls}
|
|
321
|
+
showInline={showInline}
|
|
380
322
|
/>
|
|
381
|
-
{
|
|
323
|
+
{/* Only the applied-filter chips live under the toolbar now — the
|
|
324
|
+
controls themselves ride on the toolbar line above. */}
|
|
325
|
+
{inlineVisible && <InlineFilterChips {...filterProps} />}
|
|
382
326
|
<TableFilter.Layout>
|
|
383
327
|
<TableFilter.Main>
|
|
384
328
|
<GridMain
|
|
385
329
|
c={c}
|
|
386
|
-
getRowId={getRowId}
|
|
387
|
-
renderCard={renderCard}
|
|
330
|
+
getRowId={props.getRowId}
|
|
331
|
+
renderCard={props.renderCard}
|
|
332
|
+
renderListRow={props.renderListRow}
|
|
333
|
+
board={props.board}
|
|
388
334
|
dataTestId={dataTestId}
|
|
389
|
-
|
|
335
|
+
testIdPrefix={testIdPrefix}
|
|
336
|
+
emptyState={body}
|
|
390
337
|
/>
|
|
391
338
|
<DataViewsPagination c={c} testIdPrefix={testIdPrefix} />
|
|
392
339
|
</TableFilter.Main>
|
|
393
|
-
{
|
|
340
|
+
{inlineFilters ? null : <GridFilterPanel {...filterProps} />}
|
|
394
341
|
</TableFilter.Layout>
|
|
395
342
|
{useModal && <FilterDialog open={c.filterOpen} onClose={() => c.setFilterOpen(false)} {...filterProps} />}
|
|
396
343
|
</Stack>
|
|
397
344
|
</TableFilter>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export function GridShell<T extends Record<string, unknown>>(props: GridShellProps<T>): React.JSX.Element {
|
|
349
|
+
const {
|
|
350
|
+
c,
|
|
351
|
+
fields,
|
|
352
|
+
rangeFields,
|
|
353
|
+
testIdPrefix,
|
|
354
|
+
renderCard,
|
|
355
|
+
renderListRow,
|
|
356
|
+
board,
|
|
357
|
+
defaultLayout,
|
|
358
|
+
ignoreStoredLayout,
|
|
359
|
+
inlineFilters = false,
|
|
360
|
+
alwaysShowSearch = false,
|
|
361
|
+
} = props;
|
|
362
|
+
const filters = useGridShellFilters({ c, inlineFilters, fields, rangeFields, testIdPrefix, alwaysShowSearch });
|
|
363
|
+
return (
|
|
364
|
+
<DataViewsLayoutProvider
|
|
365
|
+
canUseCards={Boolean(renderCard)}
|
|
366
|
+
canUseList={Boolean(renderListRow)}
|
|
367
|
+
// The board reuses the entity's card, so it needs BOTH — a `board` with no
|
|
368
|
+
// `renderCard` offers no board rather than throwing.
|
|
369
|
+
canUseBoard={Boolean(board && renderCard)}
|
|
370
|
+
defaultLayout={defaultLayout}
|
|
371
|
+
ignoreStoredLayout={ignoreStoredLayout}
|
|
372
|
+
viewLayout={c.state.layout}
|
|
373
|
+
>
|
|
374
|
+
<LayoutStateSync c={c} />
|
|
375
|
+
<ShellStack props={props} filters={filters} />
|
|
398
376
|
</DataViewsLayoutProvider>
|
|
399
377
|
);
|
|
400
378
|
}
|