@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.
Files changed (39) hide show
  1. package/package.json +5 -5
  2. package/src/components/data-display/DataViews/DataViewsBoard.tsx +327 -0
  3. package/src/components/data-display/DataViews/DataViewsGrid.tsx +80 -77
  4. package/src/components/data-display/DataViews/DataViewsTableBase.tsx +178 -230
  5. package/src/components/data-display/DataViews/DeleteViewDialog.tsx +83 -0
  6. package/src/components/data-display/DataViews/SaveViewModal.tsx +140 -52
  7. package/src/components/data-display/DataViews/data-views-columns-tab.tsx +240 -0
  8. package/src/components/data-display/DataViews/data-views-columns.ts +129 -0
  9. package/src/components/data-display/DataViews/data-views-dirty.ts +62 -0
  10. package/src/components/data-display/DataViews/data-views-display-panel.tsx +328 -0
  11. package/src/components/data-display/DataViews/data-views-display-tabs.tsx +328 -0
  12. package/src/components/data-display/DataViews/data-views-empty.tsx +91 -0
  13. package/src/components/data-display/DataViews/data-views-export.tsx +260 -0
  14. package/src/components/data-display/DataViews/data-views-glyphs.tsx +103 -0
  15. package/src/components/data-display/DataViews/data-views-grid-bodies.tsx +340 -0
  16. package/src/components/data-display/DataViews/data-views-grid-parts.tsx +220 -242
  17. package/src/components/data-display/DataViews/data-views-inline-bar.tsx +187 -81
  18. package/src/components/data-display/DataViews/data-views-layout-context.tsx +215 -79
  19. package/src/components/data-display/DataViews/data-views-more-filters.tsx +300 -0
  20. package/src/components/data-display/DataViews/data-views-overflow.ts +302 -0
  21. package/src/components/data-display/DataViews/data-views-preview.ts +11 -3
  22. package/src/components/data-display/DataViews/data-views-range-pill.tsx +162 -92
  23. package/src/components/data-display/DataViews/data-views-range-values.ts +69 -0
  24. package/src/components/data-display/DataViews/data-views-scopes.tsx +356 -0
  25. package/src/components/data-display/DataViews/data-views-search.tsx +156 -0
  26. package/src/components/data-display/DataViews/data-views-select-all-strip.tsx +105 -0
  27. package/src/components/data-display/DataViews/data-views-selection.ts +50 -0
  28. package/src/components/data-display/DataViews/data-views-server-query.ts +75 -0
  29. package/src/components/data-display/DataViews/data-views-shell-toolbar.tsx +87 -0
  30. package/src/components/data-display/DataViews/data-views-table-parts.tsx +5 -0
  31. package/src/components/data-display/DataViews/data-views-toolbar.tsx +63 -48
  32. package/src/components/data-display/DataViews/data-views-types.ts +88 -9
  33. package/src/components/data-display/DataViews/data-views-view-nav.tsx +378 -0
  34. package/src/components/data-display/DataViews/index.ts +14 -1
  35. package/src/components/data-display/DataViews/use-data-views-state.ts +158 -134
  36. package/src/components/data-display/DataViews/use-saved-views-controller.ts +171 -0
  37. package/src/components/layout/ContentToolbar/ContentToolbar.tsx +86 -30
  38. package/src/components/layout/ContentToolbar/ContentToolbar.types.ts +25 -0
  39. package/src/components/layout/ContentToolbar/MultiSelectDropdown.tsx +6 -1
@@ -0,0 +1,378 @@
1
+ "use client";
2
+
3
+ /**
4
+ * The saved views, rendered INSIDE the Exibir panel rather than in a dropdown
5
+ * of their own.
6
+ *
7
+ * Two levels, both of them replacing the panel's BODY (the tabs stay put):
8
+ * the list of views, and one view's actions. It is a drill-down and not a
9
+ * nested menu on purpose — an absolutely positioned popover inside the panel's
10
+ * own scroll container gets clipped by it, which is how "Excluir visão" ends
11
+ * up unreachable. One level, no clipping, and it works on touch.
12
+ */
13
+ import ChevronLeftRoundedIcon from "@mui/icons-material/ChevronLeftRounded";
14
+ import CheckRoundedIcon from "@mui/icons-material/CheckRounded";
15
+ import DeleteOutlineRoundedIcon from "@mui/icons-material/DeleteOutlineRounded";
16
+ import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
17
+ import GroupOutlinedIcon from "@mui/icons-material/GroupOutlined";
18
+ import MoreVertRoundedIcon from "@mui/icons-material/MoreVertRounded";
19
+ import PushPinOutlinedIcon from "@mui/icons-material/PushPinOutlined";
20
+ import StarOutlineRoundedIcon from "@mui/icons-material/StarOutlined";
21
+ import StarOutlineRoundedIcon2 from "@mui/icons-material/StarOutlineRounded";
22
+ import { IconButton } from "@mui/material";
23
+
24
+ import KeyboardArrowDownRoundedIcon from "@mui/icons-material/KeyboardArrowDownRounded";
25
+ import RestartAltRoundedIcon from "@mui/icons-material/RestartAltRounded";
26
+ import SaveOutlinedIcon from "@mui/icons-material/SaveOutlined";
27
+
28
+ import { Button } from "../../form/Button";
29
+ import { Box } from "../../../mui/Box";
30
+ import { Text } from "../../typography/Text";
31
+
32
+ import type { DisplayPanelView } from "./data-views-display-panel";
33
+ import type { SavedViewSummary } from "./data-views-types";
34
+
35
+ /** What the panel needs to drive the views without owning any of them. */
36
+ export interface ViewNavHandlers {
37
+ views: SavedViewSummary[];
38
+ activeViewId?: string | null;
39
+ /** `null` selects the built-in "Visão principal". */
40
+ onSelectView: (id: string | null) => void;
41
+ onEditView: (view: SavedViewSummary) => void;
42
+ onPatchView: (
43
+ view: SavedViewSummary,
44
+ changes: Partial<Pick<SavedViewSummary, "isDefault" | "pinned" | "shared">>,
45
+ ) => void;
46
+ onDeleteView: (view: SavedViewSummary) => void;
47
+ }
48
+
49
+ const rowSx = {
50
+ display: "flex",
51
+ alignItems: "center",
52
+ gap: 1,
53
+ width: "100%",
54
+ px: 1.25,
55
+ py: 1,
56
+ border: 0,
57
+ borderRadius: 1,
58
+ bgcolor: "transparent",
59
+ cursor: "pointer",
60
+ font: "inherit",
61
+ fontSize: "0.8125rem",
62
+ textAlign: "left",
63
+ color: "text.primary",
64
+ "&:hover": { bgcolor: "action.hover" },
65
+ } as const;
66
+
67
+ /** The list: the built-in view, then each saved one with its own "⋮". */
68
+ export function ViewsList({
69
+ handlers,
70
+ onOpenActions,
71
+ testIdPrefix,
72
+ }: {
73
+ handlers: ViewNavHandlers;
74
+ onOpenActions: (view: SavedViewSummary) => void;
75
+ testIdPrefix: string;
76
+ }): React.JSX.Element {
77
+ const { views, activeViewId, onSelectView } = handlers;
78
+ const onMain = !activeViewId;
79
+ return (
80
+ <Box sx={{ p: 0.75 }} data-testid={`${testIdPrefix}-views-list`}>
81
+ <Box
82
+ component="button"
83
+ type="button"
84
+ onClick={() => onSelectView(null)}
85
+ data-testid={`${testIdPrefix}-view-main`}
86
+ sx={{ ...rowSx, color: onMain ? "primary.main" : "text.primary", fontWeight: onMain ? 600 : 400 }}
87
+ >
88
+ <Box component="span" sx={{ flex: 1 }}>
89
+ Visão principal
90
+ </Box>
91
+ {onMain && <CheckRoundedIcon fontSize="small" />}
92
+ </Box>
93
+
94
+ {views.map((view) => {
95
+ const active = view.id === activeViewId;
96
+ return (
97
+ <Box key={view.id} sx={{ display: "flex", alignItems: "center" }}>
98
+ <Box
99
+ component="button"
100
+ type="button"
101
+ onClick={() => onSelectView(view.id)}
102
+ data-testid={`${testIdPrefix}-view-${view.id}`}
103
+ sx={{ ...rowSx, minWidth: 0, color: active ? "primary.main" : "text.primary", fontWeight: active ? 600 : 400 }}
104
+ >
105
+ <Box component="span" sx={{ flex: 1, minWidth: 0, overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap" }}>
106
+ {view.name}
107
+ </Box>
108
+ {/* What the row-menu toggles DID, readable without opening it:
109
+ a view set as default, pinned or shared says so here. */}
110
+ {view.isDefault && (
111
+ <StarOutlineRoundedIcon fontSize="small" sx={{ color: "warning.main" }} titleAccess="Padrão" />
112
+ )}
113
+ {view.pinned && (
114
+ <PushPinOutlinedIcon fontSize="small" sx={{ color: "text.disabled" }} titleAccess="Fixada" />
115
+ )}
116
+ {view.shared && (
117
+ <GroupOutlinedIcon fontSize="small" sx={{ color: "text.disabled" }} titleAccess="Compartilhada" />
118
+ )}
119
+ {active && <CheckRoundedIcon fontSize="small" />}
120
+ </Box>
121
+ <IconButton
122
+ size="small"
123
+ onClick={() => onOpenActions(view)}
124
+ aria-label={`Opções de ${view.name}`}
125
+ data-testid={`${testIdPrefix}-view-${view.id}-menu`}
126
+ >
127
+ <MoreVertRoundedIcon fontSize="small" />
128
+ </IconButton>
129
+ </Box>
130
+ );
131
+ })}
132
+
133
+ {views.length === 0 && (
134
+ <Text variant="caption" as="p">
135
+ <Box component="span" sx={{ display: "block", px: 1.25, py: 1.5, color: "text.secondary", lineHeight: 1.6 }}>
136
+ Nenhuma visão salva. Ajuste filtros, colunas ou ordenação e salve abaixo.
137
+ </Box>
138
+ </Text>
139
+ )}
140
+ </Box>
141
+ );
142
+ }
143
+
144
+ /** One view's actions, reached from its "⋮" and left by the back row. */
145
+ export function ViewActions({
146
+ view,
147
+ handlers,
148
+ onBack,
149
+ testIdPrefix,
150
+ }: {
151
+ view: SavedViewSummary;
152
+ handlers: ViewNavHandlers;
153
+ onBack: () => void;
154
+ testIdPrefix: string;
155
+ }): React.JSX.Element {
156
+ const { onEditView, onPatchView, onDeleteView } = handlers;
157
+ const action = (
158
+ key: string,
159
+ Icon: typeof StarOutlineRoundedIcon2,
160
+ label: string,
161
+ onClick: () => void,
162
+ opts: { on?: boolean; danger?: boolean } = {},
163
+ ): React.JSX.Element => (
164
+ <Box
165
+ component="button"
166
+ type="button"
167
+ onClick={onClick}
168
+ data-testid={`${testIdPrefix}-view-action-${key}`}
169
+ sx={{
170
+ ...rowSx,
171
+ color: opts.danger ? "error.main" : "text.primary",
172
+ "&:hover": { bgcolor: opts.danger ? "error.lighter" : "action.hover" },
173
+ }}
174
+ >
175
+ <Icon fontSize="small" sx={{ color: opts.danger ? "inherit" : "text.disabled" }} />
176
+ <Box component="span" sx={{ flex: 1 }}>
177
+ {label}
178
+ </Box>
179
+ {opts.on && <CheckRoundedIcon fontSize="small" color="primary" />}
180
+ </Box>
181
+ );
182
+
183
+ return (
184
+ <Box sx={{ p: 0.75 }} data-testid={`${testIdPrefix}-view-actions`}>
185
+ <Box
186
+ component="button"
187
+ type="button"
188
+ onClick={onBack}
189
+ data-testid={`${testIdPrefix}-view-actions-back`}
190
+ sx={{ ...rowSx, color: "text.secondary", fontSize: "0.75rem" }}
191
+ >
192
+ <ChevronLeftRoundedIcon fontSize="small" />
193
+ <Box component="span" sx={{ flex: 1, fontWeight: 600, color: "text.primary" }}>
194
+ {view.name}
195
+ </Box>
196
+ </Box>
197
+ <Box sx={{ my: 0.5, borderTop: 1, borderColor: "divider" }} />
198
+ {action("edit", EditOutlinedIcon, "Renomear e editar", () => onEditView(view))}
199
+ {action("default", StarOutlineRoundedIcon2, "Definir como padrão", () => onPatchView(view, { isDefault: !view.isDefault }), { on: view.isDefault })}
200
+ {action("pin", PushPinOutlinedIcon, "Fixar na barra lateral", () => onPatchView(view, { pinned: !view.pinned }), { on: view.pinned })}
201
+ {action("share", GroupOutlinedIcon, "Compartilhar com a equipe", () => onPatchView(view, { shared: !view.shared }), { on: view.shared })}
202
+ <Box sx={{ my: 0.5, borderTop: 1, borderColor: "divider" }} />
203
+ {action("delete", DeleteOutlineRoundedIcon, "Excluir visão", () => onDeleteView(view), { danger: true })}
204
+ </Box>
205
+ );
206
+ }
207
+
208
+ /** Name, unsaved dot, chevron — the parts, so the toggle has one decision. */
209
+ function ToggleContents({
210
+ view,
211
+ open,
212
+ interactive,
213
+ testIdPrefix,
214
+ }: {
215
+ view: DisplayPanelView;
216
+ open: boolean;
217
+ interactive: boolean;
218
+ testIdPrefix: string;
219
+ }): React.JSX.Element {
220
+ return (
221
+ <>
222
+ <Box component="span" sx={{ fontWeight: 600 }} data-testid={`${testIdPrefix}-display-view-name`}>
223
+ {view.activeViewName ?? "Visão principal"}
224
+ </Box>
225
+ {view.dirty && (
226
+ <Box
227
+ component="span"
228
+ aria-label="Alterações não salvas"
229
+ data-testid={`${testIdPrefix}-display-dirty`}
230
+ sx={{ height: 6, width: 6, borderRadius: "50%", bgcolor: "primary.main" }}
231
+ />
232
+ )}
233
+ {interactive && (
234
+ <KeyboardArrowDownRoundedIcon
235
+ fontSize="small"
236
+ sx={{ color: "text.disabled", transform: open ? "rotate(180deg)" : "none" }}
237
+ />
238
+ )}
239
+ </>
240
+ );
241
+ }
242
+
243
+ /** The name + unsaved dot; a button only when there are views to open. */
244
+ function ViewToggle({
245
+ view,
246
+ open,
247
+ onToggle,
248
+ testIdPrefix,
249
+ }: {
250
+ view: DisplayPanelView;
251
+ open: boolean;
252
+ onToggle: () => void;
253
+ testIdPrefix: string;
254
+ }): React.JSX.Element {
255
+ const interactive = Boolean(view.nav);
256
+ // One decision, not eight: a panel with no views renders the same row as
257
+ // plain text rather than as a button that opens nothing.
258
+ const affordance = interactive
259
+ ? { px: 0.75, py: 0.25, cursor: "pointer", "&:hover": { bgcolor: "action.hover" } }
260
+ : { px: 0, py: 0, cursor: "default" };
261
+ return (
262
+ <Box
263
+ component={interactive ? "button" : "div"}
264
+ type={interactive ? "button" : undefined}
265
+ onClick={interactive ? onToggle : undefined}
266
+ aria-expanded={interactive ? open : undefined}
267
+ data-testid={`${testIdPrefix}-display-view-toggle`}
268
+ sx={{
269
+ ml: "auto",
270
+ display: "flex",
271
+ alignItems: "center",
272
+ gap: 0.75,
273
+ minWidth: 0,
274
+ border: 0,
275
+ borderRadius: 1,
276
+ font: "inherit",
277
+ fontSize: "0.75rem",
278
+ bgcolor: open ? "action.selected" : "transparent",
279
+ ...affordance,
280
+ }}
281
+ >
282
+ <ToggleContents view={view} open={open} interactive={interactive} testIdPrefix={testIdPrefix} />
283
+ </Box>
284
+ );
285
+ }
286
+
287
+ /** The VISÃO row above the tabs. */
288
+ export function ViewHeader(props: {
289
+ view: DisplayPanelView;
290
+ open: boolean;
291
+ onToggle: () => void;
292
+ testIdPrefix: string;
293
+ }): React.JSX.Element {
294
+ return (
295
+ <Box sx={{ display: "flex", alignItems: "center", gap: 1, px: 1.5, py: 1, borderBottom: 1, borderColor: "divider" }}>
296
+ <Text variant="caption" as="span">
297
+ <Box component="span" sx={{ textTransform: "uppercase", letterSpacing: 0.5, color: "text.disabled" }}>
298
+ Visão
299
+ </Box>
300
+ </Text>
301
+ <ViewToggle {...props} />
302
+ </Box>
303
+ );
304
+ }
305
+
306
+ /** Redefinir / Salvar como nova / Atualizar, pinned below the tabs. */
307
+ export function ViewFooter({
308
+ view,
309
+ onDone,
310
+ testIdPrefix,
311
+ }: {
312
+ view: DisplayPanelView;
313
+ onDone: () => void;
314
+ testIdPrefix: string;
315
+ }): React.JSX.Element {
316
+ return (
317
+ <Box
318
+ sx={{
319
+ display: "flex",
320
+ alignItems: "center",
321
+ justifyContent: "space-between",
322
+ gap: 1,
323
+ px: 1.5,
324
+ py: 1,
325
+ borderTop: 1,
326
+ borderColor: "divider",
327
+ bgcolor: "action.hover",
328
+ }}
329
+ >
330
+ <Button
331
+ variant="text"
332
+ size="sm"
333
+ color="neutral"
334
+ disabled={!view.dirty}
335
+ startIcon={<RestartAltRoundedIcon fontSize="small" />}
336
+ onClick={view.onReset}
337
+ dataTestId={`${testIdPrefix}-display-reset`}
338
+ >
339
+ Redefinir
340
+ </Button>
341
+ <Box sx={{ display: "flex", alignItems: "center", gap: 1 }}>
342
+ {/* Only meaningful with a view APPLIED: on Visão principal the single
343
+ "Salvar visão" already is "save as new". */}
344
+ {view.onUpdate && (
345
+ <Button
346
+ variant="text"
347
+ size="sm"
348
+ color="neutral"
349
+ onClick={() => {
350
+ view.onSaveAs();
351
+ onDone();
352
+ }}
353
+ dataTestId={`${testIdPrefix}-display-save-as`}
354
+ >
355
+ Salvar como nova
356
+ </Button>
357
+ )}
358
+ {/* Enabled only when something in Ordenar / Colunas / Exibição (or the
359
+ filters the view also stores) differs from the applied view: the
360
+ filled button is a STATE — "there is something to save" — not a
361
+ permanent fixture. */}
362
+ <Button
363
+ size="sm"
364
+ disabled={!view.dirty}
365
+ startIcon={<SaveOutlinedIcon fontSize="small" />}
366
+ onClick={() => {
367
+ (view.onUpdate ?? view.onSaveAs)();
368
+ onDone();
369
+ }}
370
+ dataTestId={`${testIdPrefix}-display-save`}
371
+ >
372
+ {view.onUpdate ? "Atualizar visão" : "Salvar visão"}
373
+ </Button>
374
+ </Box>
375
+ </Box>
376
+ );
377
+ }
378
+
@@ -8,8 +8,20 @@
8
8
  export { BaseCard } from "./base-card";
9
9
  export { DataViewsGrid } from "./DataViewsGrid";
10
10
  export { DataViewsTableBase, type DataViewsTableBaseProps } from "./DataViewsTableBase";
11
- export { toSavedViewSummary, CARD_ASPECT_RATIOS } from "./data-views-types";
11
+ export { toSavedViewSummary, CARD_ASPECT_RATIOS, DATA_VIEWS_LAYOUTS } from "./data-views-types";
12
12
  export { cardScaleForZoom, cardMinWidthForZoom } from "./data-views-layout-context";
13
+ export { DataViewsBoard, type BoardConfig, type BoardGroup } from "./DataViewsBoard";
14
+ export type {
15
+ DataViewExport,
16
+ DataViewExportFormat,
17
+ DataViewExportRequest,
18
+ } from "./data-views-export";
19
+ export {
20
+ DataViewsScopeTabs,
21
+ resolveScope,
22
+ assertNoScopePillOverlap,
23
+ type ScopeConfig,
24
+ } from "./data-views-scopes";
13
25
  export type {
14
26
  RowAction,
15
27
  DataViewCardSelection,
@@ -17,6 +29,7 @@ export type {
17
29
  SavedViewSummary,
18
30
  DataViewState,
19
31
  DataViewSyncState,
32
+ DataViewsLayout,
20
33
  DataViewColumn,
21
34
  FilterFieldConfig,
22
35
  RangeFieldConfig,