@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
|
@@ -1,15 +1,23 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import type { SortFieldDefinition } from "../../layout/ContentToolbar";
|
|
4
|
-
import { useMemo, useRef, useState } from "react";
|
|
4
|
+
import { useEffect, useMemo, useRef, useState } from "react";
|
|
5
5
|
|
|
6
6
|
import { DataViewsGrid } from "./DataViewsGrid";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
7
|
+
import type { DisplayPanelView } from "./data-views-display-panel";
|
|
8
|
+
import { viewStateKey } from "./data-views-dirty";
|
|
9
|
+
import { DeleteViewDialog } from "./DeleteViewDialog";
|
|
10
|
+
import type { BoardConfig } from "./DataViewsBoard";
|
|
11
|
+
import type { DataViewExport } from "./data-views-export";
|
|
12
|
+
import type { ScopeConfig } from "./data-views-scopes";
|
|
13
|
+
import { ViewDialogs, ViewMutationErrorAlert } from "./data-views-table-parts";
|
|
14
|
+
import {
|
|
15
|
+
useSavedViewsController,
|
|
16
|
+
type SavedViewsController,
|
|
17
|
+
} from "./use-saved-views-controller";
|
|
9
18
|
import {
|
|
10
|
-
coerceViewState,
|
|
11
|
-
emptyViewState,
|
|
12
19
|
type DataViewColumn,
|
|
20
|
+
type DataViewsLayout,
|
|
13
21
|
type DataViewServer,
|
|
14
22
|
type DataViewState,
|
|
15
23
|
type DataViewSyncState,
|
|
@@ -21,7 +29,6 @@ import {
|
|
|
21
29
|
type RowAction,
|
|
22
30
|
type SavedViewSummary,
|
|
23
31
|
} from "./data-views-types";
|
|
24
|
-
import { type SaveViewPayload } from "./SaveViewModal";
|
|
25
32
|
|
|
26
33
|
export interface DataViewsTableBaseProps<T extends Record<string, unknown>> {
|
|
27
34
|
/** The applied-view id read from the URL (`?view=`) on load, or null. */
|
|
@@ -48,6 +55,18 @@ export interface DataViewsTableBaseProps<T extends Record<string, unknown>> {
|
|
|
48
55
|
headerActions?: React.ReactNode;
|
|
49
56
|
/** Sort options for the toolbar's "Sort By" dropdown (defaults to sortable columns). */
|
|
50
57
|
sortFields?: SortFieldDefinition[];
|
|
58
|
+
/**
|
|
59
|
+
* Per-sort-field VALUE KIND (`"currency" | "number" | "date" | "text"`), so the
|
|
60
|
+
* Exibir panel phrases the direction in that column's own terms. "Crescente"
|
|
61
|
+
* on a currency column is a puzzle; "Menor → maior" is not. Defaults to text.
|
|
62
|
+
*/
|
|
63
|
+
sortKinds?: Record<string, string>;
|
|
64
|
+
/**
|
|
65
|
+
* Opt-in "Exportar" control. The grid hands the host the current query,
|
|
66
|
+
* UNPAGINATED, and the host re-queries — the grid never fetches, and an
|
|
67
|
+
* export therefore follows the filters rather than the loaded page.
|
|
68
|
+
*/
|
|
69
|
+
exportConfig?: DataViewExport;
|
|
51
70
|
/** Notified with the currently-visible (filtered) rows, so Export matches the view. */
|
|
52
71
|
onVisibleRowsChange?: (rows: T[]) => void;
|
|
53
72
|
/** Reusable row actions driving both the row "⋮" kebab and the bulk menu. */
|
|
@@ -60,8 +79,22 @@ export interface DataViewsTableBaseProps<T extends Record<string, unknown>> {
|
|
|
60
79
|
renderRowMenu?: (row: T) => React.ReactNode;
|
|
61
80
|
/** Opt-in "Grade" (cards) layout — a card renderer per row; adds a Grade/Tabela toggle. */
|
|
62
81
|
renderCard?: (row: T, selection: DataViewCardSelection) => React.ReactNode;
|
|
63
|
-
/**
|
|
64
|
-
|
|
82
|
+
/** Opt-in "Quadro" (board) layout — the loaded page as columns of one field. Needs `renderCard`. */
|
|
83
|
+
board?: BoardConfig<T>;
|
|
84
|
+
/** Opt-in "Lista" layout — one full-width, entity-rendered row per record (FUT-733). */
|
|
85
|
+
renderListRow?: (row: T, selection: DataViewCardSelection) => React.ReactNode;
|
|
86
|
+
/** The page-level partition rendered as tabs above the toolbar (server mode only). */
|
|
87
|
+
scopes?: ScopeConfig[];
|
|
88
|
+
/** The row field the scopes partition by — used only to reject a pill over the same field. */
|
|
89
|
+
scopeFieldId?: string;
|
|
90
|
+
/** Which layout to show first when the user has expressed no preference (default "table"). */
|
|
91
|
+
defaultLayout?: DataViewsLayout;
|
|
92
|
+
/**
|
|
93
|
+
* Ignore the remembered cross-screen layout preference and pin to
|
|
94
|
+
* `defaultLayout`. For tables that exist to SHOW a layout — stories, docs,
|
|
95
|
+
* screenshots — not for real screens.
|
|
96
|
+
*/
|
|
97
|
+
ignoreStoredLayout?: boolean;
|
|
65
98
|
/** Opt into the responsive inline filter UX (collapsible row / modal). Default keeps the panel. */
|
|
66
99
|
inlineFilters?: boolean;
|
|
67
100
|
/** Keep the inline search visible with no filter fields and no active search. */
|
|
@@ -92,6 +125,12 @@ function useUrlSyncState(
|
|
|
92
125
|
pills: initialState.pills,
|
|
93
126
|
ranges: initialState.ranges,
|
|
94
127
|
sortBy: initialState.sortBy,
|
|
128
|
+
// The scope belongs in the URL-driven slice alongside them: a
|
|
129
|
+
// `?view=recusados`-style deep link and browser back/forward must
|
|
130
|
+
// move the tab strip, and they must do it WITHOUT resetting the
|
|
131
|
+
// user's hidden columns — which is the whole reason this is a
|
|
132
|
+
// merging `syncState` and not a replacing `appliedState`.
|
|
133
|
+
scope: initialState.scope,
|
|
95
134
|
}
|
|
96
135
|
: undefined,
|
|
97
136
|
[isServerMode, initialState],
|
|
@@ -110,188 +149,96 @@ function resolveInitialView(
|
|
|
110
149
|
return requested ?? views.find((view) => view.isDefault);
|
|
111
150
|
}
|
|
112
151
|
|
|
113
|
-
interface SavedViewsController {
|
|
114
|
-
applied: DataViewState;
|
|
115
|
-
/** Name of the currently-applied saved view, if any (drives the trigger label). */
|
|
116
|
-
activeViewName?: string;
|
|
117
|
-
/** Reset to the built-in no-filter "Main vision" default (clears the URL param). */
|
|
118
|
-
selectMain: () => void;
|
|
119
|
-
currentRef: React.MutableRefObject<DataViewState>;
|
|
120
|
-
saveOpen: boolean;
|
|
121
|
-
editing: SavedViewSummary | null;
|
|
122
|
-
manageOpen: boolean;
|
|
123
|
-
setSaveOpen: (open: boolean) => void;
|
|
124
|
-
setEditing: (view: SavedViewSummary | null) => void;
|
|
125
|
-
setManageOpen: (open: boolean) => void;
|
|
126
|
-
applyView: (view: SavedViewSummary) => void;
|
|
127
|
-
openCreate: () => void;
|
|
128
|
-
openEdit: (view: SavedViewSummary) => void;
|
|
129
|
-
/** Close the save modal and clear the editing target. */
|
|
130
|
-
closeSave: () => void;
|
|
131
|
-
/** Close the manage dialog and open the edit modal for `view`. */
|
|
132
|
-
editFromManage: (view: SavedViewSummary) => void;
|
|
133
|
-
patchView: ViewMutations["patchView"];
|
|
134
|
-
handleDelete: ViewMutations["handleDelete"];
|
|
135
|
-
handleSave: (payload: SaveViewPayload) => Promise<void>;
|
|
136
|
-
/** Last pin/share/default/delete failure — shown as an Alert, never silent. */
|
|
137
|
-
mutationError: string | null;
|
|
138
|
-
clearMutationError: () => void;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
interface AppliedView {
|
|
142
|
-
applied: DataViewState;
|
|
143
|
-
currentRef: React.MutableRefObject<DataViewState>;
|
|
144
|
-
activeViewName?: string;
|
|
145
|
-
applyView: (view: SavedViewSummary) => void;
|
|
146
|
-
selectMain: () => void;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
152
|
/**
|
|
150
|
-
* The
|
|
151
|
-
*
|
|
153
|
+
* The Exibir panel's view chrome, assembled from the saved-views controller.
|
|
154
|
+
* Extracted so {@link DataViewsTableBase} stays inside its size budget.
|
|
152
155
|
*/
|
|
153
|
-
function
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
156
|
+
function buildDisplayView({
|
|
157
|
+
ctl,
|
|
158
|
+
views,
|
|
159
|
+
activeView,
|
|
160
|
+
dirty,
|
|
161
|
+
onRequestDelete,
|
|
162
|
+
}: {
|
|
163
|
+
ctl: SavedViewsController;
|
|
164
|
+
views: SavedViewSummary[];
|
|
165
|
+
activeView: SavedViewSummary | undefined;
|
|
166
|
+
dirty: boolean;
|
|
167
|
+
/** Opens the confirmation; the delete itself happens on confirm. */
|
|
168
|
+
onRequestDelete: (view: SavedViewSummary) => void;
|
|
169
|
+
}): DisplayPanelView {
|
|
170
|
+
return {
|
|
171
|
+
activeViewName: ctl.activeViewName,
|
|
172
|
+
dirty,
|
|
173
|
+
onReset: () => (activeView ? ctl.applyView(activeView) : ctl.selectMain()),
|
|
174
|
+
// Only offer "Atualizar" when there IS a view to update — otherwise the
|
|
175
|
+
// single footer button is a plain "Salvar visão".
|
|
176
|
+
onUpdate: activeView ? () => ctl.openEdit(activeView) : undefined,
|
|
177
|
+
onSaveAs: ctl.openCreate,
|
|
178
|
+
// The views themselves, not a dropdown: the panel renders them in its own
|
|
179
|
+
// body (see `data-views-view-nav`), so there is no second popover to clip
|
|
180
|
+
// the row actions.
|
|
181
|
+
nav: {
|
|
182
|
+
views,
|
|
183
|
+
activeViewId: activeView?.id ?? null,
|
|
184
|
+
onSelectView: (id) => {
|
|
185
|
+
const next = id === null ? null : views.find((view) => view.id === id);
|
|
186
|
+
if (next) ctl.applyView(next);
|
|
187
|
+
else ctl.selectMain();
|
|
188
|
+
},
|
|
189
|
+
onEditView: ctl.openEdit,
|
|
190
|
+
onPatchView: (view, changes) => void ctl.patchView(view, changes),
|
|
191
|
+
onDeleteView: onRequestDelete,
|
|
192
|
+
},
|
|
181
193
|
};
|
|
182
|
-
const activeViewName = views.find((view) => view.id === appliedViewId)?.name;
|
|
183
|
-
return { applied, currentRef, activeViewName, applyView, selectMain };
|
|
184
194
|
}
|
|
185
195
|
|
|
186
196
|
/**
|
|
187
|
-
*
|
|
188
|
-
*
|
|
189
|
-
*
|
|
197
|
+
* "Are there unsaved changes?" — the dot on Exibir, and whether Redefinir and
|
|
198
|
+
* Salvar visão are live.
|
|
199
|
+
*
|
|
200
|
+
* Two things it gets right that the inline version did not. It compares
|
|
201
|
+
* NORMALISED states (`viewStateKey`), so a filter applied and then removed is
|
|
202
|
+
* clean again rather than clean-looking but flagged; and it re-baselines after
|
|
203
|
+
* a SAVE, because the state just written is the clean one from that point.
|
|
204
|
+
*
|
|
205
|
+
* The baseline is the first state the grid emits for a view, not the view
|
|
206
|
+
* itself: the grid normalises what it is handed — resolving visible columns,
|
|
207
|
+
* filling in layout — so the applied view is never byte-identical to the state
|
|
208
|
+
* it produces.
|
|
190
209
|
*/
|
|
191
|
-
function
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
const
|
|
200
|
-
useAppliedView(router, columnIds, views, initialView, initialState);
|
|
201
|
-
const [saveOpen, setSaveOpen] = useState(false);
|
|
202
|
-
const [editing, setEditing] = useState<SavedViewSummary | null>(null);
|
|
203
|
-
const [manageOpen, setManageOpen] = useState(false);
|
|
204
|
-
|
|
205
|
-
const openCreate = (): void => {
|
|
206
|
-
setEditing(null);
|
|
207
|
-
setSaveOpen(true);
|
|
208
|
-
};
|
|
209
|
-
const openEdit = (view: SavedViewSummary): void => {
|
|
210
|
-
setEditing(view);
|
|
211
|
-
setSaveOpen(true);
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
const [mutationError, setMutationError] = useState<string | null>(null);
|
|
215
|
-
const refreshAfter = (ok: boolean, error?: string): void => {
|
|
216
|
-
if (ok) {
|
|
217
|
-
setMutationError(null);
|
|
218
|
-
router.refresh();
|
|
219
|
-
} else {
|
|
220
|
-
// A rejected pin/share/default/delete must be visible (FUT-100) — the
|
|
221
|
-
// menus close on click, so an inline Alert is the only surviving signal.
|
|
222
|
-
setMutationError(error ?? "Não foi possível salvar a alteração da visão.");
|
|
223
|
-
}
|
|
224
|
-
};
|
|
210
|
+
function useDirtyTracking(ctl: SavedViewsController): {
|
|
211
|
+
dirty: boolean;
|
|
212
|
+
noteState: (state: DataViewState) => void;
|
|
213
|
+
markSaved: () => void;
|
|
214
|
+
} {
|
|
215
|
+
const [dirty, setDirty] = useState(false);
|
|
216
|
+
const [savedAt, setSavedAt] = useState(0);
|
|
217
|
+
const baseline = useRef<string | null>(null);
|
|
218
|
+
const appliedKey = JSON.stringify(ctl.applied ?? null);
|
|
225
219
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
refreshAfter,
|
|
231
|
-
);
|
|
232
|
-
|
|
233
|
-
const handleSave = async (payload: SaveViewPayload): Promise<void> => {
|
|
234
|
-
await saveView(payload);
|
|
235
|
-
setSaveOpen(false);
|
|
236
|
-
setEditing(null);
|
|
237
|
-
};
|
|
220
|
+
useEffect(() => {
|
|
221
|
+
baseline.current = null;
|
|
222
|
+
setDirty(false);
|
|
223
|
+
}, [appliedKey, savedAt]);
|
|
238
224
|
|
|
239
225
|
return {
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
openCreate,
|
|
252
|
-
openEdit,
|
|
253
|
-
closeSave: () => {
|
|
254
|
-
setSaveOpen(false);
|
|
255
|
-
setEditing(null);
|
|
256
|
-
},
|
|
257
|
-
editFromManage: (view: SavedViewSummary) => {
|
|
258
|
-
setManageOpen(false);
|
|
259
|
-
openEdit(view);
|
|
226
|
+
dirty,
|
|
227
|
+
noteState: (state) => {
|
|
228
|
+
ctl.currentRef.current = state;
|
|
229
|
+
const key = viewStateKey(state);
|
|
230
|
+
if (baseline.current === null) {
|
|
231
|
+
baseline.current = key;
|
|
232
|
+
setDirty(false);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
const next = key !== baseline.current;
|
|
236
|
+
setDirty((prev) => (prev === next ? prev : next));
|
|
260
237
|
},
|
|
261
|
-
|
|
262
|
-
handleDelete,
|
|
263
|
-
handleSave,
|
|
264
|
-
mutationError,
|
|
265
|
-
clearMutationError: () => setMutationError(null),
|
|
238
|
+
markSaved: () => setSavedAt((tick) => tick + 1),
|
|
266
239
|
};
|
|
267
240
|
}
|
|
268
241
|
|
|
269
|
-
/**
|
|
270
|
-
* The saved-views dropdown wired to the controller — the toolbar's right slot.
|
|
271
|
-
* Extracted so the {@link DataViewsTable} render stays within the size budget.
|
|
272
|
-
*/
|
|
273
|
-
function ViewsMenu(props: {
|
|
274
|
-
views: SavedViewSummary[];
|
|
275
|
-
ctl: SavedViewsController;
|
|
276
|
-
testIdPrefix: string;
|
|
277
|
-
}): React.JSX.Element {
|
|
278
|
-
const { views, ctl, testIdPrefix } = props;
|
|
279
|
-
return (
|
|
280
|
-
<ViewsMenuSlot
|
|
281
|
-
views={views}
|
|
282
|
-
activeViewName={ctl.activeViewName}
|
|
283
|
-
applyView={ctl.applyView}
|
|
284
|
-
selectMain={ctl.selectMain}
|
|
285
|
-
openCreate={ctl.openCreate}
|
|
286
|
-
openEdit={ctl.openEdit}
|
|
287
|
-
handleDelete={ctl.handleDelete}
|
|
288
|
-
patchView={ctl.patchView}
|
|
289
|
-
onManageAll={() => ctl.setManageOpen(true)}
|
|
290
|
-
testIdPrefix={testIdPrefix}
|
|
291
|
-
/>
|
|
292
|
-
);
|
|
293
|
-
}
|
|
294
|
-
|
|
295
242
|
/**
|
|
296
243
|
* The reusable admin table (FUT-89): the DataViews grid plus the full saved-views
|
|
297
244
|
* experience — a views dropdown, a save/edit modal (with preview), and a manage
|
|
@@ -301,38 +248,43 @@ function ViewsMenu(props: {
|
|
|
301
248
|
* a view restores its filters + columns + sort. Each table supplies its own columns
|
|
302
249
|
* + filter fields; everything else is shared.
|
|
303
250
|
*/
|
|
304
|
-
export function DataViewsTableBase<T extends Record<string, unknown>>(
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
rowActions,
|
|
323
|
-
rowActionsLeading,
|
|
324
|
-
bulkActions,
|
|
325
|
-
renderRowMenu,
|
|
326
|
-
renderCard,
|
|
327
|
-
defaultLayout,
|
|
328
|
-
inlineFilters,
|
|
329
|
-
alwaysShowSearch,
|
|
330
|
-
server,
|
|
331
|
-
initialState,
|
|
332
|
-
}: DataViewsTableBaseProps<T>): React.JSX.Element {
|
|
251
|
+
export function DataViewsTableBase<T extends Record<string, unknown>>(
|
|
252
|
+
props: DataViewsTableBaseProps<T>,
|
|
253
|
+
): React.JSX.Element {
|
|
254
|
+
// Forwarded as a whole rather than restated name-by-name twice (as in
|
|
255
|
+
// DataViewsGrid): this component only ADDS the saved-views chrome, and a
|
|
256
|
+
// 30-name destructure repeated in the JSX is exactly where a newly added prop
|
|
257
|
+
// silently stops being passed on.
|
|
258
|
+
const {
|
|
259
|
+
initialViewId = null,
|
|
260
|
+
persistence,
|
|
261
|
+
router,
|
|
262
|
+
columns,
|
|
263
|
+
fields,
|
|
264
|
+
testIdPrefix = "table",
|
|
265
|
+
views,
|
|
266
|
+
server,
|
|
267
|
+
initialState,
|
|
268
|
+
} = props;
|
|
333
269
|
const initialView = resolveInitialView(views, initialViewId);
|
|
334
270
|
const columnIds = columns.map((col) => col.id);
|
|
335
271
|
const ctl = useSavedViewsController(persistence, router, columnIds, views, initialView, initialState);
|
|
272
|
+
// Unsaved-changes flag for the Exibir panel. Set from `onStateChange` rather
|
|
273
|
+
// than derived in render because the live state lives in a REF (the grid owns
|
|
274
|
+
// it); the equality bail-out in `setDirty` is what stops the update loop.
|
|
275
|
+
const { dirty, noteState, markSaved } = useDirtyTracking(ctl);
|
|
276
|
+
// Deleting a saved view is confirmed, not immediate — see `DeleteViewDialog`.
|
|
277
|
+
const [pendingDelete, setPendingDelete] = useState<SavedViewSummary | null>(null);
|
|
278
|
+
// The view the panel's "Redefinir" restores: the applied one, or the built-in
|
|
279
|
+
// "Visão principal" when none is.
|
|
280
|
+
const activeView = views.find((view) => view.name === ctl.activeViewName);
|
|
281
|
+
const displayView = buildDisplayView({
|
|
282
|
+
ctl,
|
|
283
|
+
views,
|
|
284
|
+
activeView,
|
|
285
|
+
dirty,
|
|
286
|
+
onRequestDelete: setPendingDelete,
|
|
287
|
+
});
|
|
336
288
|
// Server-mode lists re-apply URL-derived controls on back/forward (not just the mount seed).
|
|
337
289
|
const syncState = useUrlSyncState(server, initialState);
|
|
338
290
|
|
|
@@ -340,34 +292,25 @@ export function DataViewsTableBase<T extends Record<string, unknown>>({
|
|
|
340
292
|
<>
|
|
341
293
|
<ViewMutationErrorAlert error={ctl.mutationError} onClose={ctl.clearMutationError} testIdPrefix={testIdPrefix} />
|
|
342
294
|
<DataViewsGrid<T>
|
|
343
|
-
|
|
344
|
-
columns={columns}
|
|
345
|
-
fields={fields}
|
|
346
|
-
rangeFields={rangeFields}
|
|
347
|
-
getRowId={getRowId}
|
|
348
|
-
onRowClick={onRowClick}
|
|
349
|
-
emptyState={emptyState}
|
|
350
|
-
dataTestId={dataTestId}
|
|
295
|
+
{...props}
|
|
351
296
|
testIdPrefix={testIdPrefix}
|
|
352
|
-
title={title}
|
|
353
|
-
headerActions={headerActions}
|
|
354
|
-
sortFields={sortFields}
|
|
355
|
-
rowActions={rowActions}
|
|
356
|
-
rowActionsLeading={rowActionsLeading}
|
|
357
|
-
bulkActions={bulkActions}
|
|
358
|
-
renderRowMenu={renderRowMenu}
|
|
359
|
-
renderCard={renderCard}
|
|
360
|
-
defaultLayout={defaultLayout}
|
|
361
|
-
inlineFilters={inlineFilters}
|
|
362
|
-
alwaysShowSearch={alwaysShowSearch}
|
|
363
|
-
server={server}
|
|
364
297
|
appliedState={ctl.applied}
|
|
365
298
|
syncState={syncState}
|
|
366
|
-
onStateChange={
|
|
367
|
-
|
|
368
|
-
|
|
299
|
+
onStateChange={noteState}
|
|
300
|
+
// The saved-views menu is NOT a toolbar control any more: it lives in
|
|
301
|
+
// the Exibir panel's VISÃO header, passed through `displayView`.
|
|
302
|
+
displayView={displayView}
|
|
303
|
+
/>
|
|
304
|
+
{renderViewDialogs(ctl, fields, columns, views, testIdPrefix, markSaved)}
|
|
305
|
+
<DeleteViewDialog
|
|
306
|
+
view={pendingDelete}
|
|
307
|
+
onClose={() => setPendingDelete(null)}
|
|
308
|
+
onConfirm={(view) => {
|
|
309
|
+
void ctl.handleDelete(view);
|
|
310
|
+
setPendingDelete(null);
|
|
311
|
+
}}
|
|
312
|
+
testIdPrefix={testIdPrefix}
|
|
369
313
|
/>
|
|
370
|
-
{renderViewDialogs(ctl, fields, columns, views, testIdPrefix)}
|
|
371
314
|
</>
|
|
372
315
|
);
|
|
373
316
|
}
|
|
@@ -379,6 +322,7 @@ function renderViewDialogs<T extends Record<string, unknown>>(
|
|
|
379
322
|
columns: DataViewsTableBaseProps<T>["columns"],
|
|
380
323
|
views: SavedViewSummary[],
|
|
381
324
|
testIdPrefix: string,
|
|
325
|
+
onSaved: () => void,
|
|
382
326
|
): React.JSX.Element {
|
|
383
327
|
return (
|
|
384
328
|
<ViewDialogs<T>
|
|
@@ -388,7 +332,11 @@ function renderViewDialogs<T extends Record<string, unknown>>(
|
|
|
388
332
|
fields={fields}
|
|
389
333
|
columns={columns}
|
|
390
334
|
editing={ctl.editing}
|
|
391
|
-
onSave={
|
|
335
|
+
onSave={async (payload) => {
|
|
336
|
+
await ctl.handleSave(payload);
|
|
337
|
+
// The saved state is the clean one from here on.
|
|
338
|
+
onSaved();
|
|
339
|
+
}}
|
|
392
340
|
manageOpen={ctl.manageOpen}
|
|
393
341
|
onCloseManage={() => ctl.setManageOpen(false)}
|
|
394
342
|
views={views}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The confirmation in front of "Excluir visão".
|
|
5
|
+
*
|
|
6
|
+
* Deleting was immediate: one click in a row menu, on a control sitting
|
|
7
|
+
* directly under four harmless toggles, and the view was gone. A saved view is
|
|
8
|
+
* somebody's filters, columns and sort — cheap to rebuild only if you remember
|
|
9
|
+
* what they were.
|
|
10
|
+
*
|
|
11
|
+
* It names the view rather than saying "this view", says plainly that the
|
|
12
|
+
* records are untouched, and warns separately when the view is SHARED, because
|
|
13
|
+
* that deletion takes it from everyone rather than from one person.
|
|
14
|
+
*/
|
|
15
|
+
import { Dialog, DialogContent } from "../../feedback/Dialog";
|
|
16
|
+
import { Button } from "../../form/Button";
|
|
17
|
+
import { Stack } from "../../../mui/Stack";
|
|
18
|
+
import { Text } from "../../typography/Text";
|
|
19
|
+
|
|
20
|
+
import type { SavedViewSummary } from "./data-views-types";
|
|
21
|
+
|
|
22
|
+
export function DeleteViewDialog({
|
|
23
|
+
view,
|
|
24
|
+
onClose,
|
|
25
|
+
onConfirm,
|
|
26
|
+
/** What this table holds, for the "your records are safe" line. */
|
|
27
|
+
entityLabel = "Os registros",
|
|
28
|
+
testIdPrefix = "view",
|
|
29
|
+
}: {
|
|
30
|
+
view: SavedViewSummary | null;
|
|
31
|
+
onClose: () => void;
|
|
32
|
+
onConfirm: (view: SavedViewSummary) => void;
|
|
33
|
+
entityLabel?: string;
|
|
34
|
+
testIdPrefix?: string;
|
|
35
|
+
}): React.JSX.Element | null {
|
|
36
|
+
if (!view) return null;
|
|
37
|
+
return (
|
|
38
|
+
<Dialog
|
|
39
|
+
open
|
|
40
|
+
onClose={onClose}
|
|
41
|
+
title="Excluir visão"
|
|
42
|
+
size="sm"
|
|
43
|
+
showCloseButton
|
|
44
|
+
dataTestId={`${testIdPrefix}-delete-modal`}
|
|
45
|
+
>
|
|
46
|
+
<DialogContent>
|
|
47
|
+
<Stack spacing={1.5}>
|
|
48
|
+
<Text variant="body" as="p" data-testid={`${testIdPrefix}-delete-target`}>
|
|
49
|
+
Excluir <strong>{view.name}</strong>?
|
|
50
|
+
</Text>
|
|
51
|
+
|
|
52
|
+
{view.shared && (
|
|
53
|
+
<Text variant="body" color="warning" as="p" data-testid={`${testIdPrefix}-delete-shared`}>
|
|
54
|
+
Esta visão está compartilhada — a equipe perderá o acesso a ela.
|
|
55
|
+
</Text>
|
|
56
|
+
)}
|
|
57
|
+
|
|
58
|
+
<Text variant="body" color="secondary" as="p">
|
|
59
|
+
{entityLabel} não são afetados; apenas o recorte salvo é removido.
|
|
60
|
+
</Text>
|
|
61
|
+
|
|
62
|
+
<Stack direction="row" spacing={1.5} sx={{ justifyContent: "flex-end", pt: 0.5 }}>
|
|
63
|
+
<Button
|
|
64
|
+
variant="text"
|
|
65
|
+
color="neutral"
|
|
66
|
+
onClick={onClose}
|
|
67
|
+
dataTestId={`${testIdPrefix}-delete-cancel`}
|
|
68
|
+
>
|
|
69
|
+
Cancelar
|
|
70
|
+
</Button>
|
|
71
|
+
<Button
|
|
72
|
+
color="danger"
|
|
73
|
+
onClick={() => onConfirm(view)}
|
|
74
|
+
dataTestId={`${testIdPrefix}-delete-confirm`}
|
|
75
|
+
>
|
|
76
|
+
Excluir visão
|
|
77
|
+
</Button>
|
|
78
|
+
</Stack>
|
|
79
|
+
</Stack>
|
|
80
|
+
</DialogContent>
|
|
81
|
+
</Dialog>
|
|
82
|
+
);
|
|
83
|
+
}
|