@12-apps/ui 6.57.0 → 6.59.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/dist/_internal/components/data-display/Avatar/Avatar.js +2 -2
- package/dist/_internal/components/data-display/Badge/Badge.js +2 -2
- package/dist/_internal/components/data-display/Carousel/Carousel.js +2 -2
- package/dist/_internal/components/data-display/Chart/Chart.js +4 -4
- package/dist/_internal/components/data-display/Chart/SpecChart.js +7 -7
- package/dist/_internal/components/data-display/Chart/chart-context.js +2 -2
- package/dist/_internal/components/data-display/Chart/chart-format.js +5 -1
- package/dist/_internal/components/data-display/Chart/chart-renderers.js +3 -3
- package/dist/_internal/components/data-display/Chart/chart-spec.js +2 -2
- package/dist/_internal/components/data-display/DataGrid/DataGrid.js +2 -2
- package/dist/_internal/components/data-display/DataViews/DataViewsGrid.js +18 -18
- package/dist/_internal/components/data-display/DataViews/DataViewsTableBase.js +20 -19
- package/dist/_internal/components/data-display/DataViews/base-list-card-geometry.js +5 -3
- package/dist/_internal/components/data-display/DataViews/base-list-card-layout.js +45 -0
- package/dist/_internal/components/data-display/DataViews/base-list-card-layout.js.map +1 -0
- package/dist/_internal/components/data-display/DataViews/base-list-card-slots.js +2 -2
- package/dist/_internal/components/data-display/DataViews/base-list-card.js +5 -4
- package/dist/_internal/components/data-display/DataViews/data-views-category-pill.js +1 -1
- package/dist/_internal/components/data-display/DataViews/data-views-filter-panel.js +4 -4
- package/dist/_internal/components/data-display/DataViews/data-views-grid-bodies.js +4 -4
- package/dist/_internal/components/data-display/DataViews/data-views-grid-parts.js +17 -17
- package/dist/_internal/components/data-display/DataViews/data-views-inline-bar.js +6 -6
- package/dist/_internal/components/data-display/DataViews/data-views-more-fields.js +4 -4
- package/dist/_internal/components/data-display/DataViews/data-views-more-filters.js +5 -5
- package/dist/_internal/components/data-display/DataViews/data-views-range-pill.js +2 -2
- package/dist/_internal/components/data-display/DataViews/data-views-selection-extra.js +3 -1
- package/dist/_internal/components/data-display/DataViews/data-views-shell-toolbar.js +3 -3
- package/dist/_internal/components/data-display/DataViews/data-views-table-parts.js +1 -1
- package/dist/_internal/components/data-display/DataViews/data-views-toolbar.js +2 -2
- package/dist/_internal/components/data-display/DataViews/data-views-url-echo.js +13 -0
- package/dist/_internal/components/data-display/DataViews/data-views-url-echo.js.map +1 -0
- package/dist/_internal/components/data-display/DataViews/list-card-rails.js +1 -1
- package/dist/_internal/components/data-display/FleetMap/FleetMap.fixtures.js +1 -1
- package/dist/_internal/components/data-display/index.js +16 -16
- package/dist/_internal/components/index.js +24 -24
- package/dist/_internal/components/layout/ContentToolbar/ContentToolbar.js +1 -1
- package/dist/_internal/components/layout/index.js +1 -1
- package/dist/_internal/components/navigation/SectionNav/SectionNav.bar.js +6 -6
- package/dist/_internal/components/navigation/SectionNav/SectionNav.js +9 -9
- package/dist/_internal/components/navigation/SectionNav/SectionNav.parts.js +3 -3
- package/dist/_internal/components/navigation/SectionNav/SectionNav.primary.js +4 -4
- package/dist/_internal/components/navigation/SectionNav/SectionNav.rail.js +5 -5
- package/dist/_internal/components/navigation/SectionNav/SectionNav.railCreate.js +4 -4
- package/dist/_internal/components/navigation/SectionNav/SectionNav.sheet.js +4 -4
- package/dist/_internal/components/navigation/index.js +9 -9
- package/dist/_internal/en-US.data-display.js +1 -1
- package/dist/_internal/pt-BR.data-display.js +1 -1
- package/dist/_internal/user-avatar/user-avatar.js +3 -3
- package/dist/charts.js +8 -8
- package/dist/{chunk-RBHBJDOZ.js → chunk-3FMWQQW6.js} +5 -5
- package/dist/chunk-3FMWQQW6.js.map +1 -0
- package/dist/{chunk-MKAA3YIJ.js → chunk-3K7WN2PO.js} +3 -3
- package/dist/{chunk-GI24X7XA.js → chunk-4PPNCZWY.js} +3 -3
- package/dist/{chunk-573XC4HK.js → chunk-4QKS5Y35.js} +2 -2
- package/dist/{chunk-KX75HM5U.js → chunk-5YDS6HO3.js} +7 -2
- package/dist/{chunk-KX75HM5U.js.map → chunk-5YDS6HO3.js.map} +1 -1
- package/dist/{chunk-N4MR4EMC.js → chunk-6D2PFC5C.js} +2 -2
- package/dist/{chunk-QVWNK4JO.js → chunk-6GPNZSAE.js} +19 -2
- package/dist/chunk-6GPNZSAE.js.map +1 -0
- package/dist/{chunk-LJKWWXZD.js → chunk-6OSUK2C7.js} +28 -22
- package/dist/chunk-6OSUK2C7.js.map +1 -0
- package/dist/{chunk-ICSXPZXT.js → chunk-6QBT5TPS.js} +2 -2
- package/dist/{chunk-A6EUQDRK.js → chunk-ARJIKW56.js} +4 -2
- package/dist/chunk-ARJIKW56.js.map +1 -0
- package/dist/{chunk-53ACGKBW.js → chunk-EDBP62WG.js} +3 -3
- package/dist/{chunk-YXIVQFNW.js → chunk-EWIKSTJV.js} +4 -4
- package/dist/{chunk-LWZ2K225.js → chunk-FXZUVZON.js} +6 -6
- package/dist/chunk-G6EJLEVV.js +75 -0
- package/dist/chunk-G6EJLEVV.js.map +1 -0
- package/dist/{chunk-IK2ZVV3M.js → chunk-GK7JNSMK.js} +2 -2
- package/dist/{chunk-HFBQCJNP.js → chunk-HCYRCU4W.js} +2 -2
- package/dist/{chunk-BJMNAPKH.js → chunk-HH7PM5HT.js} +10 -22
- package/dist/chunk-HH7PM5HT.js.map +1 -0
- package/dist/{chunk-7AAJYWC3.js → chunk-HPO7PB4F.js} +2 -2
- package/dist/{chunk-WF3P7GE5.js → chunk-I3CQBEKS.js} +2 -2
- package/dist/{chunk-RREWYOKS.js → chunk-IL4FCC54.js} +2 -2
- package/dist/{chunk-7M3DRP7Q.js → chunk-ILNO4ZJ6.js} +3 -3
- package/dist/{chunk-R6SBP5M5.js → chunk-JVOEVTCZ.js} +4 -2
- package/dist/chunk-JVOEVTCZ.js.map +1 -0
- package/dist/{chunk-2CJZLL5Y.js → chunk-NHVL2QPG.js} +2 -1
- package/dist/{chunk-2CJZLL5Y.js.map → chunk-NHVL2QPG.js.map} +1 -1
- package/dist/{chunk-45PGSVOH.js → chunk-NTJUAPJM.js} +10 -8
- package/dist/chunk-NTJUAPJM.js.map +1 -0
- package/dist/{chunk-EUPUEMFX.js → chunk-PC4H5LVB.js} +7 -7
- package/dist/{chunk-KEKOPD43.js → chunk-PQ7RWH7L.js} +4 -2
- package/dist/chunk-PQ7RWH7L.js.map +1 -0
- package/dist/{chunk-HRMB72E4.js → chunk-QHP5PZNC.js} +2 -1
- package/dist/{chunk-HRMB72E4.js.map → chunk-QHP5PZNC.js.map} +1 -1
- package/dist/{chunk-GNBCSTXF.js → chunk-QKAYXAQE.js} +6 -6
- package/dist/{chunk-GNBCSTXF.js.map → chunk-QKAYXAQE.js.map} +1 -1
- package/dist/{chunk-TZFBJ47T.js → chunk-SFLNHCA2.js} +39 -15
- package/dist/chunk-SFLNHCA2.js.map +1 -0
- package/dist/{chunk-OBMPE6PW.js → chunk-SSDYEOU4.js} +5 -5
- package/dist/{chunk-I6ESCBY3.js → chunk-TF27Z27K.js} +2 -2
- package/dist/{chunk-JF5LQOGB.js → chunk-U47KRLEW.js} +57 -45
- package/dist/chunk-U47KRLEW.js.map +1 -0
- package/dist/chunk-V4V2N6YJ.js +55 -0
- package/dist/chunk-V4V2N6YJ.js.map +1 -0
- package/dist/{chunk-C67FDYG7.js → chunk-W7N4ORCV.js} +16 -6
- package/dist/chunk-W7N4ORCV.js.map +1 -0
- package/dist/{chunk-JL77BSOL.js → chunk-WBDQSBLV.js} +2 -2
- package/dist/{chunk-G76BNXWE.js → chunk-XW5SEW3H.js} +6 -6
- package/dist/{chunk-46ZATLQV.js → chunk-YIKFYWKD.js} +3 -3
- package/dist/{chunk-4JBS6NOT.js → chunk-ZWBGQYVO.js} +35 -6
- package/dist/chunk-ZWBGQYVO.js.map +1 -0
- package/dist/{chunk-VWW2EXH7.js → chunk-ZYJZHXFU.js} +4 -4
- package/dist/data-display/Avatar.js +2 -2
- package/dist/data-display/Badge.js +2 -2
- package/dist/data-display/CardKit.js +10 -10
- package/dist/data-display/Carousel.js +2 -2
- package/dist/data-display/Chart.js +8 -8
- package/dist/data-display/DataGrid.js +2 -2
- package/dist/data-display/DataViews.js +25 -23
- package/dist/en-US.js +1 -1
- package/dist/layout/ContentToolbar.js +1 -1
- package/dist/locales.js +2 -2
- package/dist/navigation/SectionNav.js +9 -9
- package/dist/pt-BR.js +1 -1
- package/dist/types/components/data-display/Chart/Chart.types.d.ts +5 -0
- package/dist/types/components/data-display/Chart/Chart.types.d.ts.map +1 -1
- package/dist/types/components/data-display/Chart/chart-context.d.ts.map +1 -1
- package/dist/types/components/data-display/Chart/chart-format.d.ts +14 -0
- package/dist/types/components/data-display/Chart/chart-format.d.ts.map +1 -1
- package/dist/types/components/data-display/Chart/chart-spec.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/DataViewsTableBase.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/base-list-card-geometry.d.ts +29 -1
- package/dist/types/components/data-display/DataViews/base-list-card-geometry.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/base-list-card-layout.d.ts +46 -0
- package/dist/types/components/data-display/DataViews/base-list-card-layout.d.ts.map +1 -0
- package/dist/types/components/data-display/DataViews/base-list-card-slots.d.ts +5 -2
- package/dist/types/components/data-display/DataViews/base-list-card-slots.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/base-list-card.d.ts +10 -1
- package/dist/types/components/data-display/DataViews/base-list-card.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-copy.d.ts +7 -0
- package/dist/types/components/data-display/DataViews/data-views-copy.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-grid-parts.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-selection-extra.d.ts +2 -0
- package/dist/types/components/data-display/DataViews/data-views-selection-extra.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-shell-toolbar.d.ts +3 -1
- package/dist/types/components/data-display/DataViews/data-views-shell-toolbar.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-toolbar.d.ts +2 -0
- package/dist/types/components/data-display/DataViews/data-views-toolbar.d.ts.map +1 -1
- package/dist/types/components/data-display/DataViews/data-views-url-echo.d.ts +47 -0
- package/dist/types/components/data-display/DataViews/data-views-url-echo.d.ts.map +1 -0
- package/dist/types/components/data-display/DataViews/list-card-rails.d.ts +17 -2
- package/dist/types/components/data-display/DataViews/list-card-rails.d.ts.map +1 -1
- package/dist/types/components/layout/ContentToolbar/ContentToolbar.d.ts +3 -2
- package/dist/types/components/layout/ContentToolbar/ContentToolbar.d.ts.map +1 -1
- package/dist/types/components/layout/ContentToolbar/ContentToolbar.types.d.ts +14 -0
- package/dist/types/components/layout/ContentToolbar/ContentToolbar.types.d.ts.map +1 -1
- package/dist/types/en-US.data-display.d.ts.map +1 -1
- package/dist/types/pt-BR.data-display.d.ts.map +1 -1
- package/dist/user-avatar.js +3 -3
- package/package.json +1 -1
- package/src/components/data-display/Chart/Chart.md +5 -1
- package/src/components/data-display/Chart/Chart.types.ts +5 -0
- package/src/components/data-display/Chart/chart-context.ts +21 -1
- package/src/components/data-display/Chart/chart-format.ts +31 -0
- package/src/components/data-display/Chart/chart-renderers.tsx +1 -1
- package/src/components/data-display/Chart/chart-spec.ts +2 -1
- package/src/components/data-display/DataViews/DataViewsTableBase.tsx +7 -36
- package/src/components/data-display/DataViews/base-list-card-geometry.tsx +114 -40
- package/src/components/data-display/DataViews/base-list-card-layout.tsx +104 -0
- package/src/components/data-display/DataViews/base-list-card-slots.tsx +47 -3
- package/src/components/data-display/DataViews/base-list-card.tsx +65 -68
- package/src/components/data-display/DataViews/data-views-copy.ts +7 -0
- package/src/components/data-display/DataViews/data-views-grid-parts.tsx +2 -1
- package/src/components/data-display/DataViews/data-views-selection-extra.ts +10 -1
- package/src/components/data-display/DataViews/data-views-shell-toolbar.tsx +4 -0
- package/src/components/data-display/DataViews/data-views-toolbar.tsx +4 -0
- package/src/components/data-display/DataViews/data-views-url-echo.ts +134 -0
- package/src/components/data-display/DataViews/list-card-rails.tsx +22 -6
- package/src/components/layout/ContentToolbar/ContentToolbar.md +5 -2
- package/src/components/layout/ContentToolbar/ContentToolbar.tsx +51 -10
- package/src/components/layout/ContentToolbar/ContentToolbar.types.ts +14 -0
- package/src/en-US.data-display.ts +1 -0
- package/src/pt-BR.data-display.ts +1 -0
- package/dist/chunk-45PGSVOH.js.map +0 -1
- package/dist/chunk-4JBS6NOT.js.map +0 -1
- package/dist/chunk-A6EUQDRK.js.map +0 -1
- package/dist/chunk-BJMNAPKH.js.map +0 -1
- package/dist/chunk-C67FDYG7.js.map +0 -1
- package/dist/chunk-JF5LQOGB.js.map +0 -1
- package/dist/chunk-KEKOPD43.js.map +0 -1
- package/dist/chunk-LJKWWXZD.js.map +0 -1
- package/dist/chunk-QVWNK4JO.js.map +0 -1
- package/dist/chunk-R6SBP5M5.js.map +0 -1
- package/dist/chunk-RBHBJDOZ.js.map +0 -1
- package/dist/chunk-TZFBJ47T.js.map +0 -1
- /package/dist/{chunk-MKAA3YIJ.js.map → chunk-3K7WN2PO.js.map} +0 -0
- /package/dist/{chunk-GI24X7XA.js.map → chunk-4PPNCZWY.js.map} +0 -0
- /package/dist/{chunk-573XC4HK.js.map → chunk-4QKS5Y35.js.map} +0 -0
- /package/dist/{chunk-N4MR4EMC.js.map → chunk-6D2PFC5C.js.map} +0 -0
- /package/dist/{chunk-ICSXPZXT.js.map → chunk-6QBT5TPS.js.map} +0 -0
- /package/dist/{chunk-53ACGKBW.js.map → chunk-EDBP62WG.js.map} +0 -0
- /package/dist/{chunk-YXIVQFNW.js.map → chunk-EWIKSTJV.js.map} +0 -0
- /package/dist/{chunk-LWZ2K225.js.map → chunk-FXZUVZON.js.map} +0 -0
- /package/dist/{chunk-IK2ZVV3M.js.map → chunk-GK7JNSMK.js.map} +0 -0
- /package/dist/{chunk-HFBQCJNP.js.map → chunk-HCYRCU4W.js.map} +0 -0
- /package/dist/{chunk-7AAJYWC3.js.map → chunk-HPO7PB4F.js.map} +0 -0
- /package/dist/{chunk-WF3P7GE5.js.map → chunk-I3CQBEKS.js.map} +0 -0
- /package/dist/{chunk-RREWYOKS.js.map → chunk-IL4FCC54.js.map} +0 -0
- /package/dist/{chunk-7M3DRP7Q.js.map → chunk-ILNO4ZJ6.js.map} +0 -0
- /package/dist/{chunk-EUPUEMFX.js.map → chunk-PC4H5LVB.js.map} +0 -0
- /package/dist/{chunk-OBMPE6PW.js.map → chunk-SSDYEOU4.js.map} +0 -0
- /package/dist/{chunk-I6ESCBY3.js.map → chunk-TF27Z27K.js.map} +0 -0
- /package/dist/{chunk-JL77BSOL.js.map → chunk-WBDQSBLV.js.map} +0 -0
- /package/dist/{chunk-G76BNXWE.js.map → chunk-XW5SEW3H.js.map} +0 -0
- /package/dist/{chunk-46ZATLQV.js.map → chunk-YIKFYWKD.js.map} +0 -0
- /package/dist/{chunk-VWW2EXH7.js.map → chunk-ZYJZHXFU.js.map} +0 -0
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
|
|
5
|
+
import { Box } from "../../../mui/Box";
|
|
6
|
+
import type { BaseListCardProps } from "./base-list-card";
|
|
7
|
+
import { rowGutters, type StackPlacement, type useRowShell } from "./base-list-card-geometry";
|
|
8
|
+
import { cellRailsTemplate } from "./list-card-rails";
|
|
9
|
+
import { cellTracks, useCellConfig, type ListCardCellConfig } from "./list-card-cells";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* WHAT SHAPE A ROW TAKES, resolved once: its cells and their template, whether
|
|
13
|
+
* it ends in the "this opens" chevron, and what its two-line form keeps.
|
|
14
|
+
*
|
|
15
|
+
* Split from the card at the file-size gate. Everything here decides; nothing
|
|
16
|
+
* here renders but the standalone container.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Which cell config applies, and whether this row resolves its own tracks.
|
|
21
|
+
*
|
|
22
|
+
* The list's config wins over the card's: one declaration of the list's shape is
|
|
23
|
+
* the point, so a row inside a group cannot introduce a column of its own. And
|
|
24
|
+
* the template is standalone-only — inside a group the GROUP owns it and the row
|
|
25
|
+
* is subgrid over it, because a row resolving its own tracks is exactly what
|
|
26
|
+
* stops a list from lining up.
|
|
27
|
+
*/
|
|
28
|
+
function useResolvedCells(
|
|
29
|
+
props: BaseListCardProps,
|
|
30
|
+
inGroup: boolean,
|
|
31
|
+
gutters: { disclose: boolean; drag: boolean; select: boolean },
|
|
32
|
+
): {
|
|
33
|
+
cells: readonly ListCardCellConfig<never>[] | null;
|
|
34
|
+
configured: boolean;
|
|
35
|
+
cellTemplate: string | null;
|
|
36
|
+
} {
|
|
37
|
+
const groupCells = useCellConfig();
|
|
38
|
+
const cells = groupCells ?? props.cells ?? null;
|
|
39
|
+
const configured = cells != null && cells.length > 0 && props.row != null;
|
|
40
|
+
const cellTemplate = configured && !inGroup ? cellRailsTemplate(cellTracks(cells), gutters) : null;
|
|
41
|
+
return { cells, configured, cellTemplate };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Whether the row ends in the "this opens" chevron: it is live and leads
|
|
46
|
+
* somewhere it can actually go — a click handler, or a link the row renders.
|
|
47
|
+
* Only a named-slot row with a title renders an anchor for `href` (the caption
|
|
48
|
+
* wraps the title), so an `href` alone does not count elsewhere: a chevron on
|
|
49
|
+
* a row that cannot open is the promise the disclosure chevron's docblock
|
|
50
|
+
* warns about. Not when the row expands either:
|
|
51
|
+
* the disclosure chevron already speaks, and a second arrow meaning something
|
|
52
|
+
* else beside it would contradict it.
|
|
53
|
+
*/
|
|
54
|
+
function rowOpens(
|
|
55
|
+
props: BaseListCardProps,
|
|
56
|
+
shell: ReturnType<typeof useRowShell>,
|
|
57
|
+
configured: boolean,
|
|
58
|
+
expandable: boolean,
|
|
59
|
+
): boolean {
|
|
60
|
+
if (expandable) return false;
|
|
61
|
+
// The named-slot caption renders its anchor only around a title.
|
|
62
|
+
return shell.acts || (shell.actionable && props.href != null && props.title != null && !configured);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* The cell that takes the VALUE's place when the row stacks: the one the config
|
|
67
|
+
* marks `strong` ("the figure the row is really about"), else the last — as
|
|
68
|
+
* long as it is not the first cell, which already holds the title's place.
|
|
69
|
+
*/
|
|
70
|
+
function valueCellId(cells: readonly ListCardCellConfig<never>[]): string | null {
|
|
71
|
+
const first = cells[0];
|
|
72
|
+
const candidate = cells.find((cell) => cell.strong === true && cell !== first) ?? cells[cells.length - 1];
|
|
73
|
+
return candidate != null && candidate !== first ? candidate.id : null;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** Everything the row's shape depends on, resolved once. */
|
|
77
|
+
export function useRowLayout(
|
|
78
|
+
props: BaseListCardProps,
|
|
79
|
+
shell: ReturnType<typeof useRowShell>,
|
|
80
|
+
expandable: boolean,
|
|
81
|
+
): ReturnType<typeof useResolvedCells> & { opens: boolean; stack: StackPlacement } {
|
|
82
|
+
const resolved = useResolvedCells(props, shell.group != null, rowGutters(props, shell));
|
|
83
|
+
const cells = resolved.configured ? resolved.cells : null;
|
|
84
|
+
return {
|
|
85
|
+
...resolved,
|
|
86
|
+
opens: rowOpens(props, shell, resolved.configured, expandable),
|
|
87
|
+
stack: {
|
|
88
|
+
firstCell: cells?.[0]?.id ?? null,
|
|
89
|
+
valueCell: cells == null ? null : valueCellId(cells),
|
|
90
|
+
expandable,
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* A standalone row's QUERY CONTAINER. A container query matches an ancestor,
|
|
97
|
+
* never the element itself, so the row cannot be the container its own
|
|
98
|
+
* two-line template asks about. In a group the group is the container, and the
|
|
99
|
+
* row must stay its direct child for subgrid, so no wrapper there.
|
|
100
|
+
*/
|
|
101
|
+
export function StandaloneContainer({ inGroup, children }: { inGroup: boolean; children: ReactNode }): React.JSX.Element {
|
|
102
|
+
if (inGroup) return <>{children}</>;
|
|
103
|
+
return <Box sx={{ containerType: "inline-size", width: "100%", minWidth: 0 }}>{children}</Box>;
|
|
104
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
|
+
import KeyboardArrowRightIcon from "@mui/icons-material/KeyboardArrowRight";
|
|
3
4
|
import { Fragment, type ReactNode } from "react";
|
|
4
5
|
|
|
5
6
|
import { DescriptionItem, type DescriptionItemProps } from "../DescriptionItem";
|
|
@@ -242,19 +243,34 @@ export function ListCardActions({
|
|
|
242
243
|
actions,
|
|
243
244
|
alwaysVisible,
|
|
244
245
|
menu,
|
|
246
|
+
opens = false,
|
|
245
247
|
testId,
|
|
246
248
|
}: {
|
|
247
249
|
actions?: ReactNode;
|
|
248
250
|
alwaysVisible?: boolean;
|
|
249
251
|
menu?: ReactNode;
|
|
252
|
+
/** End the row with the "this opens" chevron. */
|
|
253
|
+
opens?: boolean;
|
|
250
254
|
testId: (slot: string) => string | undefined;
|
|
251
255
|
}): React.JSX.Element | null {
|
|
252
|
-
if (actions == null && menu == null) return null;
|
|
256
|
+
if (actions == null && menu == null && !opens) return null;
|
|
253
257
|
return (
|
|
254
|
-
// z-index 1: above the stretched link, so these stay clickable.
|
|
258
|
+
// z-index 1: above the stretched link, so these stay clickable. The box
|
|
259
|
+
// itself lets clicks through (`pointer-events: none`, re-enabled on its
|
|
260
|
+
// children): only the controls inside it are meant to catch one, and the
|
|
261
|
+
// chevron, which re-disables it, must hand its click to the row or link
|
|
262
|
+
// underneath rather than swallow it.
|
|
255
263
|
<Box
|
|
256
264
|
data-slot="actions"
|
|
257
|
-
sx={{
|
|
265
|
+
sx={{
|
|
266
|
+
display: "flex",
|
|
267
|
+
alignItems: "center",
|
|
268
|
+
gap: 0.5,
|
|
269
|
+
position: "relative",
|
|
270
|
+
zIndex: 1,
|
|
271
|
+
pointerEvents: "none",
|
|
272
|
+
"& > *": { pointerEvents: "auto" },
|
|
273
|
+
}}
|
|
258
274
|
>
|
|
259
275
|
{actions && (
|
|
260
276
|
<Box
|
|
@@ -282,6 +298,31 @@ export function ListCardActions({
|
|
|
282
298
|
{menu}
|
|
283
299
|
</Box>
|
|
284
300
|
)}
|
|
301
|
+
{opens && <OpenChevron testId={testId("open")} />}
|
|
302
|
+
</Box>
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* THE "THIS OPENS" CHEVRON, at the end of a row that leads somewhere.
|
|
308
|
+
*
|
|
309
|
+
* A row that navigates or opens a record looked exactly like a row that does
|
|
310
|
+
* nothing until a pointer hovered it, and a phone has no hover. Always visible —
|
|
311
|
+
* unlike `actions`, it is a statement about the row, not a control to reveal.
|
|
312
|
+
*
|
|
313
|
+
* Decorative to assistive tech (`aria-hidden`): the row itself is the button or
|
|
314
|
+
* the link, and already says so. It takes no click of its own for the same
|
|
315
|
+
* reason; the click falls through to the row.
|
|
316
|
+
*/
|
|
317
|
+
function OpenChevron({ testId }: { testId?: string }): React.JSX.Element {
|
|
318
|
+
return (
|
|
319
|
+
<Box
|
|
320
|
+
component="span"
|
|
321
|
+
aria-hidden
|
|
322
|
+
data-testid={testId}
|
|
323
|
+
sx={{ display: "flex", alignItems: "center", color: "text.secondary", pointerEvents: "none !important" }}
|
|
324
|
+
>
|
|
325
|
+
<KeyboardArrowRightIcon fontSize="small" />
|
|
285
326
|
</Box>
|
|
286
327
|
);
|
|
287
328
|
}
|
|
@@ -293,6 +334,7 @@ export function ListCardTail({
|
|
|
293
334
|
actions,
|
|
294
335
|
actionsAlwaysVisible,
|
|
295
336
|
menu,
|
|
337
|
+
opens,
|
|
296
338
|
testId,
|
|
297
339
|
}: {
|
|
298
340
|
value?: ReactNode;
|
|
@@ -301,6 +343,7 @@ export function ListCardTail({
|
|
|
301
343
|
actions?: ReactNode;
|
|
302
344
|
actionsAlwaysVisible?: boolean;
|
|
303
345
|
menu?: ReactNode;
|
|
346
|
+
opens?: boolean;
|
|
304
347
|
testId: (slot: string) => string | undefined;
|
|
305
348
|
}): React.JSX.Element {
|
|
306
349
|
return (
|
|
@@ -319,6 +362,7 @@ export function ListCardTail({
|
|
|
319
362
|
actions={actions}
|
|
320
363
|
alwaysVisible={actionsAlwaysVisible}
|
|
321
364
|
menu={menu}
|
|
365
|
+
opens={opens}
|
|
322
366
|
testId={testId}
|
|
323
367
|
/>
|
|
324
368
|
</>
|
|
@@ -11,10 +11,10 @@ import { type CardSurfaceProps } from "./card-surface";
|
|
|
11
11
|
import { DropIndicator } from "./data-views-drag";
|
|
12
12
|
import { useDisclosure } from "./base-list-card-disclosure";
|
|
13
13
|
import { actionProps, metaShape, rowStyles, useRowShell } from "./base-list-card-geometry";
|
|
14
|
+
import { StandaloneContainer, useRowLayout } from "./base-list-card-layout";
|
|
14
15
|
import { DiscloseSlot, DragSlot, SelectSlot } from "./base-list-card-gutters";
|
|
15
16
|
import { type DataViewsDensity } from "./data-views-layout-context";
|
|
16
|
-
import {
|
|
17
|
-
import { ListCardCells, cellTracks, useCellConfig, type ListCardCellConfig } from "./list-card-cells";
|
|
17
|
+
import { ListCardCells, type ListCardCellConfig } from "./list-card-cells";
|
|
18
18
|
import {
|
|
19
19
|
ListCardActions,
|
|
20
20
|
ListCardCaption,
|
|
@@ -41,7 +41,16 @@ import {
|
|
|
41
41
|
* same row renders full-bleed on a phone, in a 300px board column, and beside a
|
|
42
42
|
* filter panel that opens and closes. Below `STACK_BREAK` it leaves the shared
|
|
43
43
|
* rails entirely and goes two-line, which is the standard mobile transaction
|
|
44
|
-
* row and beats truncating everything: title
|
|
44
|
+
* row and beats truncating everything: the title (with its subtitle) on line
|
|
45
|
+
* 1, the value under it at the right edge.
|
|
46
|
+
* A CONFIGURED row keeps the same two facts — its first cell in the title's
|
|
47
|
+
* place, its `strong` (else last) cell in the value's — and drops the rest
|
|
48
|
+
* into the expandable body's job. Standalone, the card renders inside a
|
|
49
|
+
* wrapper that is the query container, since a row cannot query itself.
|
|
50
|
+
*
|
|
51
|
+
* A row that OPENS something (a click handler, or a link it renders) ends in a
|
|
52
|
+
* chevron saying so, at every width — unless it expands, where the disclosure
|
|
53
|
+
* chevron is the only arrow it carries.
|
|
45
54
|
*/
|
|
46
55
|
|
|
47
56
|
export interface BaseListCardProps extends CardSurfaceProps {
|
|
@@ -145,33 +154,6 @@ export interface BaseListCardProps extends CardSurfaceProps {
|
|
|
145
154
|
}
|
|
146
155
|
|
|
147
156
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
/**
|
|
151
|
-
* Which cell config applies, and whether this row resolves its own tracks.
|
|
152
|
-
*
|
|
153
|
-
* The list's config wins over the card's: one declaration of the list's shape is
|
|
154
|
-
* the point, so a row inside a group cannot introduce a column of its own. And
|
|
155
|
-
* the template is standalone-only — inside a group the GROUP owns it and the row
|
|
156
|
-
* is subgrid over it, because a row resolving its own tracks is exactly what
|
|
157
|
-
* stops a list from lining up.
|
|
158
|
-
*/
|
|
159
|
-
function useResolvedCells(
|
|
160
|
-
props: BaseListCardProps,
|
|
161
|
-
inGroup: boolean,
|
|
162
|
-
): {
|
|
163
|
-
cells: readonly ListCardCellConfig<never>[] | null;
|
|
164
|
-
configured: boolean;
|
|
165
|
-
cellTemplate: string | null;
|
|
166
|
-
} {
|
|
167
|
-
const groupCells = useCellConfig();
|
|
168
|
-
const cells = groupCells ?? props.cells ?? null;
|
|
169
|
-
const configured = cells != null && cells.length > 0 && props.row != null;
|
|
170
|
-
const cellTemplate = configured && !inGroup ? cellRailsTemplate(cellTracks(cells)) : null;
|
|
171
|
-
return { cells, configured, cellTemplate };
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
|
|
175
157
|
/**
|
|
176
158
|
* The row's middle: either the list's configured cells, or the named slots.
|
|
177
159
|
*
|
|
@@ -185,6 +167,7 @@ function RowContent({
|
|
|
185
167
|
slot,
|
|
186
168
|
actionable,
|
|
187
169
|
meta,
|
|
170
|
+
opens,
|
|
188
171
|
}: {
|
|
189
172
|
card: BaseListCardProps;
|
|
190
173
|
cells: readonly ListCardCellConfig<never>[] | null;
|
|
@@ -192,6 +175,8 @@ function RowContent({
|
|
|
192
175
|
slot: (name: string) => string | undefined;
|
|
193
176
|
actionable: boolean;
|
|
194
177
|
meta: { columns: number; present: boolean };
|
|
178
|
+
/** Whether the row ends in the "this opens" chevron. */
|
|
179
|
+
opens: boolean;
|
|
195
180
|
}): React.JSX.Element {
|
|
196
181
|
if (configured && cells != null) {
|
|
197
182
|
return (
|
|
@@ -206,6 +191,7 @@ function RowContent({
|
|
|
206
191
|
actions={actionable ? card.actions : undefined}
|
|
207
192
|
alwaysVisible={card.actionsAlwaysVisible}
|
|
208
193
|
menu={card.menu}
|
|
194
|
+
opens={opens}
|
|
209
195
|
testId={slot}
|
|
210
196
|
/>
|
|
211
197
|
</>
|
|
@@ -227,6 +213,7 @@ function RowContent({
|
|
|
227
213
|
actions={actionable ? card.actions : undefined}
|
|
228
214
|
actionsAlwaysVisible={card.actionsAlwaysVisible}
|
|
229
215
|
menu={card.menu}
|
|
216
|
+
opens={opens}
|
|
230
217
|
testId={slot}
|
|
231
218
|
/>
|
|
232
219
|
</>
|
|
@@ -294,46 +281,56 @@ export function BaseListCard(props: BaseListCardProps): React.JSX.Element {
|
|
|
294
281
|
const { actionable, selectable, slot, drag, reserve, pad, padY, acts } = shell;
|
|
295
282
|
const meta = metaShape(props);
|
|
296
283
|
const disclosure = useDisclosure(props);
|
|
297
|
-
const { cells, configured, cellTemplate } =
|
|
284
|
+
const { cells, configured, cellTemplate, opens, stack } = useRowLayout(props, shell, disclosure.expandable);
|
|
298
285
|
|
|
299
286
|
return (
|
|
300
|
-
<
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
{
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
287
|
+
<StandaloneContainer inGroup={shell.group != null}>
|
|
288
|
+
<Card
|
|
289
|
+
variant="outlined"
|
|
290
|
+
className={props.className}
|
|
291
|
+
dataTestId={props.testId}
|
|
292
|
+
aria-label={props["aria-label"]}
|
|
293
|
+
aria-disabled={actionable ? undefined : true}
|
|
294
|
+
onContextMenu={props.onContextMenu}
|
|
295
|
+
{...(acts ? actionProps(onClick) : {})}
|
|
296
|
+
{...drag.itemProps}
|
|
297
|
+
// A PLAIN OBJECT: `Card` merges by spreading, so a function sx vanishes.
|
|
298
|
+
sx={rowStyles(props, shell, cellTemplate, stack)}
|
|
299
|
+
>
|
|
300
|
+
{drag.dropEdge != null && <DropIndicator edge={drag.dropEdge} />}
|
|
301
|
+
<DiscloseSlot
|
|
302
|
+
expandable={disclosure.expandable}
|
|
303
|
+
expanded={disclosure.expanded}
|
|
304
|
+
reserve={reserve}
|
|
305
|
+
onToggle={disclosure.toggle}
|
|
306
|
+
controls={disclosure.regionId}
|
|
307
|
+
testId={slot("disclose")}
|
|
308
|
+
/>
|
|
309
|
+
<DragSlot drag={drag} reserve={reserve} testId={slot("drag")} />
|
|
310
|
+
<SelectSlot
|
|
311
|
+
selectable={selectable}
|
|
312
|
+
selected={selected}
|
|
313
|
+
reserve={reserve}
|
|
314
|
+
onToggleSelect={props.onToggleSelect}
|
|
315
|
+
testId={slot("checkbox")}
|
|
316
|
+
/>
|
|
317
|
+
<Box data-slot="leading" sx={{ display: "flex", alignItems: "center" }}>
|
|
318
|
+
{props.leading}
|
|
319
|
+
</Box>
|
|
320
|
+
<RowContent
|
|
321
|
+
card={props}
|
|
322
|
+
cells={cells}
|
|
323
|
+
configured={configured}
|
|
324
|
+
slot={slot}
|
|
325
|
+
actionable={actionable}
|
|
326
|
+
meta={meta}
|
|
327
|
+
opens={opens}
|
|
328
|
+
/>
|
|
329
|
+
<ExpandableBody disclosure={disclosure} padY={padY} pad={pad}>
|
|
330
|
+
{props.children}
|
|
331
|
+
</ExpandableBody>
|
|
332
|
+
</Card>
|
|
333
|
+
</StandaloneContainer>
|
|
337
334
|
);
|
|
338
335
|
}
|
|
339
336
|
|
|
@@ -225,6 +225,13 @@ export interface DataViewsSelectionCopy {
|
|
|
225
225
|
clearSelection: string;
|
|
226
226
|
selectAll: string;
|
|
227
227
|
onThisPage(count: number): string;
|
|
228
|
+
/**
|
|
229
|
+
* The toolbar's count while rows are ticked: "1 selecionado". A plural, so a
|
|
230
|
+
* function. Keep it SHORT: at 320 it shares line 1 with the clear button and
|
|
231
|
+
* has about 118px — "20 itens selecionados" (135px) wrapped and stranded the
|
|
232
|
+
* divider before it; "20 selecionados" fits.
|
|
233
|
+
*/
|
|
234
|
+
selectedCount(count: number): string;
|
|
228
235
|
/** One row's checkbox, which carries no visible label of its own. */
|
|
229
236
|
selectRow: string;
|
|
230
237
|
/**
|
|
@@ -16,7 +16,7 @@ import type { ListGroupConfig } from "./list-card-rails";
|
|
|
16
16
|
import { InlineFilterControls } from "./data-views-inline-bar";
|
|
17
17
|
import { toOverflowFields, useFilterOverflow, type OverflowSplit } from "./data-views-overflow";
|
|
18
18
|
import { GridHeaderRow } from "./data-views-header-row";
|
|
19
|
-
import { resolveSelectionExtra, type SelectionExtraRender } from "./data-views-selection-extra";
|
|
19
|
+
import { allOnPageSelected, resolveSelectionExtra, type SelectionExtraRender } from "./data-views-selection-extra";
|
|
20
20
|
import { ShellToolbar } from "./data-views-shell-toolbar";
|
|
21
21
|
import type { DisplayPanelView } from "./data-views-display-panel";
|
|
22
22
|
import type { DataViewExport } from "./data-views-export";
|
|
@@ -284,6 +284,7 @@ function ShellStack<T extends Record<string, unknown>>({
|
|
|
284
284
|
rowActions={props.rowActions}
|
|
285
285
|
bulkActions={props.bulkActions}
|
|
286
286
|
selectionExtra={resolveSelectionExtra(props)}
|
|
287
|
+
allOnPageSelected={allOnPageSelected(c, props.getRowId)}
|
|
287
288
|
toolbarRightSlot={props.toolbarRightSlot}
|
|
288
289
|
compactControls={showInline && split.compactControls}
|
|
289
290
|
counterHidden={showInline && split.counterHidden}
|
|
@@ -47,6 +47,15 @@ export type SelectionExtraRender<T extends Record<string, unknown>> = (
|
|
|
47
47
|
* Returns `undefined` with nothing selected, so the toolbar renders no empty
|
|
48
48
|
* slot: a widening with nothing to widen is not a control.
|
|
49
49
|
*/
|
|
50
|
+
/** Every row the page is rendering is selected — and the page is not empty. */
|
|
51
|
+
export function allOnPageSelected<T extends Record<string, unknown>>(
|
|
52
|
+
c: DataViewsController<T>,
|
|
53
|
+
getRowId: (row: T) => string | number,
|
|
54
|
+
): boolean {
|
|
55
|
+
const page = c.matched;
|
|
56
|
+
return page.length > 0 && page.every((row) => c.selectedIds.has(getRowId(row)));
|
|
57
|
+
}
|
|
58
|
+
|
|
50
59
|
export function resolveSelectionExtra<T extends Record<string, unknown>>({
|
|
51
60
|
c,
|
|
52
61
|
getRowId,
|
|
@@ -61,7 +70,7 @@ export function resolveSelectionExtra<T extends Record<string, unknown>>({
|
|
|
61
70
|
return selectionExtra({
|
|
62
71
|
selectedRows: c.selectedRows,
|
|
63
72
|
clearSelection: c.clearSelection,
|
|
64
|
-
allOnPageSelected:
|
|
73
|
+
allOnPageSelected: allOnPageSelected(c, getRowId),
|
|
65
74
|
pageRowCount: page.length,
|
|
66
75
|
});
|
|
67
76
|
}
|
|
@@ -23,6 +23,7 @@ export function ShellToolbar<T extends Record<string, unknown>>({
|
|
|
23
23
|
rowActions,
|
|
24
24
|
bulkActions,
|
|
25
25
|
selectionExtra,
|
|
26
|
+
allOnPageSelected,
|
|
26
27
|
toolbarRightSlot,
|
|
27
28
|
showInline,
|
|
28
29
|
compactControls,
|
|
@@ -43,6 +44,8 @@ export function ShellToolbar<T extends Record<string, unknown>>({
|
|
|
43
44
|
bulkActions?: (selectedRows: T[], clearSelection: () => void) => React.ReactNode;
|
|
44
45
|
/** Already resolved by the shell, which is the party that knows the page. */
|
|
45
46
|
selectionExtra?: React.ReactNode;
|
|
47
|
+
/** Every row on the page is ticked: the select-all box reads checked, else partial. */
|
|
48
|
+
allOnPageSelected?: boolean;
|
|
46
49
|
toolbarRightSlot?: React.ReactNode;
|
|
47
50
|
/** Search + filters, rendered on the toolbar line (see `GridToolbar`). */
|
|
48
51
|
filterControls?: React.ReactNode;
|
|
@@ -75,6 +78,7 @@ export function ShellToolbar<T extends Record<string, unknown>>({
|
|
|
75
78
|
rowActions={rowActions}
|
|
76
79
|
bulkActions={bulkActions}
|
|
77
80
|
selectionExtra={selectionExtra}
|
|
81
|
+
allOnPageSelected={allOnPageSelected}
|
|
78
82
|
sortFields={c.resolvedSortFields}
|
|
79
83
|
activeSortField={c.activeSortField}
|
|
80
84
|
activeSortOrder={c.activeSortOrder}
|
|
@@ -28,6 +28,8 @@ export interface GridToolbarProps<T extends Record<string, unknown>> {
|
|
|
28
28
|
bulkActions?: (selectedRows: T[], clearSelection: () => void) => React.ReactNode;
|
|
29
29
|
/** A control between the count and the actions menu — see `selectionExtra`. */
|
|
30
30
|
selectionExtra?: React.ReactNode;
|
|
31
|
+
/** Every row on the page is ticked: the select-all box reads checked, else partial. */
|
|
32
|
+
allOnPageSelected?: boolean;
|
|
31
33
|
sortFields: SortFieldDefinition[];
|
|
32
34
|
activeSortField: string;
|
|
33
35
|
activeSortOrder: "asc" | "desc";
|
|
@@ -160,11 +162,13 @@ export function GridToolbar<T extends Record<string, unknown>>(props: GridToolba
|
|
|
160
162
|
clearAllText={copy.filters.clearAll}
|
|
161
163
|
hasSelection={selectedRows.length > 0}
|
|
162
164
|
selectedCount={selectedRows.length}
|
|
165
|
+
selectedCountLabel={copy.selection.selectedCount}
|
|
163
166
|
selectAll={props.selectAll}
|
|
164
167
|
clearSelection={props.clearSelection}
|
|
165
168
|
selectAllTestId={`${testIdPrefix}-select-all`}
|
|
166
169
|
clearAllTestId={`${testIdPrefix}-clear-all`}
|
|
167
170
|
selectionExtra={props.selectionExtra}
|
|
171
|
+
allSelected={props.allOnPageSelected}
|
|
168
172
|
actions={renderBulkActions({
|
|
169
173
|
rowActions: props.rowActions,
|
|
170
174
|
bulkActions: props.bulkActions,
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { useEffect, useRef, useState } from "react";
|
|
4
|
+
|
|
5
|
+
import type { RangeValue } from "./data-views-range-types";
|
|
6
|
+
import type { DataViewServer, DataViewState, DataViewSyncState } from "./data-views-types";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* A server-mode grid's URL round trip, and the echo it must not re-apply.
|
|
10
|
+
*
|
|
11
|
+
* A server-mode host mirrors the grid's query into its URL and hands it back as
|
|
12
|
+
* `initialState`, which the base turns into `syncState` and MERGES over the live
|
|
13
|
+
* state, so browser back/forward moves the controls. But the browser shows the
|
|
14
|
+
* new URL before React renders it. An edit made in that window, such as
|
|
15
|
+
* clearing a date bound right after typing it, is overwritten when the render
|
|
16
|
+
* carrying the PREVIOUS query lands. The grid then re-emits that stale query,
|
|
17
|
+
* and the cleared filter comes back and sticks in the URL (FUT-3182, the
|
|
18
|
+
* audit trail's "De" bound).
|
|
19
|
+
*
|
|
20
|
+
* So the base records every query it emits. An `initialState` matching one
|
|
21
|
+
* still pending is the host echoing it back, not a navigation, and it is not
|
|
22
|
+
* merged. Anything else (back/forward, a deep link, a host-side reset) still is.
|
|
23
|
+
* A host whose round trip changes the slice's shape never matches, and keeps
|
|
24
|
+
* the old behaviour rather than losing a navigation.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
/** The URL-carried part of a grid state or query. */
|
|
28
|
+
type UrlSlice = Pick<DataViewState, "search" | "pills" | "ranges" | "sortBy" | "scope">;
|
|
29
|
+
|
|
30
|
+
/** Hosts drop unset bounds on the way through the URL, so the key drops them too. */
|
|
31
|
+
function bounds(range: RangeValue): RangeValue | null {
|
|
32
|
+
const set = (value: number | string | undefined): boolean => value !== undefined && value !== "";
|
|
33
|
+
const kept: RangeValue = {
|
|
34
|
+
...(set(range.min) ? { min: range.min } : {}),
|
|
35
|
+
...(set(range.max) ? { max: range.max } : {}),
|
|
36
|
+
};
|
|
37
|
+
return Object.keys(kept).length > 0 ? kept : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const byKey = ([a]: [string, unknown], [b]: [string, unknown]): number => a.localeCompare(b);
|
|
41
|
+
|
|
42
|
+
/** One comparable string for a URL slice: empty pills and unbounded ranges left out. */
|
|
43
|
+
export function urlSliceKey(slice: UrlSlice): string {
|
|
44
|
+
const pills = Object.entries(slice.pills ?? {})
|
|
45
|
+
.filter(([, values]) => values.length > 0)
|
|
46
|
+
.sort(byKey);
|
|
47
|
+
const ranges = Object.entries(slice.ranges ?? {})
|
|
48
|
+
.map(([id, range]): [string, RangeValue | null] => [id, bounds(range)])
|
|
49
|
+
.filter(([, range]) => range !== null)
|
|
50
|
+
.sort(byKey);
|
|
51
|
+
return JSON.stringify({
|
|
52
|
+
q: slice.search ?? "",
|
|
53
|
+
pills,
|
|
54
|
+
ranges,
|
|
55
|
+
sort: slice.sortBy ?? [],
|
|
56
|
+
scope: slice.scope ?? null,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Enough to cover a burst of edits; older entries can no longer echo. */
|
|
61
|
+
const MAX_PENDING = 20;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Record what the grid emits, and recognise it coming back.
|
|
65
|
+
*
|
|
66
|
+
* `consumeEcho` answers whether a URL slice is one of the queries still in
|
|
67
|
+
* flight. A match drops it and every older one, since the host has caught up to
|
|
68
|
+
* at least that far; a miss is a navigation and clears the list.
|
|
69
|
+
*/
|
|
70
|
+
export function useUrlEchoGuard(server: DataViewServer | undefined): {
|
|
71
|
+
server: DataViewServer | undefined;
|
|
72
|
+
consumeEcho: (slice: UrlSlice) => boolean;
|
|
73
|
+
} {
|
|
74
|
+
const pending = useRef<string[]>([]);
|
|
75
|
+
const recorded: DataViewServer | undefined = server && {
|
|
76
|
+
...server,
|
|
77
|
+
onQueryChange: (query) => {
|
|
78
|
+
pending.current = [...pending.current, urlSliceKey(query)].slice(-MAX_PENDING);
|
|
79
|
+
server.onQueryChange(query);
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
const consumeEcho = (slice: UrlSlice): boolean => {
|
|
83
|
+
const at = pending.current.lastIndexOf(urlSliceKey(slice));
|
|
84
|
+
pending.current = at === -1 ? [] : pending.current.slice(at + 1);
|
|
85
|
+
return at !== -1;
|
|
86
|
+
};
|
|
87
|
+
return { server: recorded, consumeEcho };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function syncFrom(serverMode: boolean, initialState: DataViewState | undefined): DataViewSyncState | undefined {
|
|
91
|
+
if (!serverMode || !initialState) return undefined;
|
|
92
|
+
return {
|
|
93
|
+
search: initialState.search,
|
|
94
|
+
pills: initialState.pills,
|
|
95
|
+
ranges: initialState.ranges,
|
|
96
|
+
sortBy: initialState.sortBy,
|
|
97
|
+
// The scope belongs in the URL-driven slice alongside them: a
|
|
98
|
+
// `?view=recusados`-style deep link and browser back/forward must move the
|
|
99
|
+
// tab strip, and they must do it WITHOUT resetting the user's hidden
|
|
100
|
+
// columns — which is the whole reason this is a merging `syncState` and
|
|
101
|
+
// not a replacing `appliedState`.
|
|
102
|
+
scope: initialState.scope,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Derive the grid's reactive `syncState` from the URL-seeded `initialState` in
|
|
108
|
+
* server mode, so browser back/forward RE-APPLIES the search/pills/sort controls
|
|
109
|
+
* (merging over live state, preserving hidden columns — see
|
|
110
|
+
* {@link DataViewSyncState}). Client-mode tables (no `server`) get `undefined`.
|
|
111
|
+
*
|
|
112
|
+
* Seeded on the first render, so a deep link shows on mount. After that a new
|
|
113
|
+
* `initialState` reference is re-applied unless it is the grid's own query
|
|
114
|
+
* coming back (`consumeEcho`), which would overwrite an edit made since.
|
|
115
|
+
*/
|
|
116
|
+
export function useUrlSyncState(
|
|
117
|
+
server: DataViewServer | undefined,
|
|
118
|
+
initialState: DataViewState | undefined,
|
|
119
|
+
consumeEcho: (slice: UrlSlice) => boolean,
|
|
120
|
+
): DataViewSyncState | undefined {
|
|
121
|
+
const serverMode = server !== undefined;
|
|
122
|
+
const [syncState, setSyncState] = useState(() => syncFrom(serverMode, initialState));
|
|
123
|
+
const seen = useRef({ serverMode, initialState });
|
|
124
|
+
useEffect(() => {
|
|
125
|
+
const last = seen.current;
|
|
126
|
+
if (last.serverMode === serverMode && last.initialState === initialState) return;
|
|
127
|
+
seen.current = { serverMode, initialState };
|
|
128
|
+
const next = syncFrom(serverMode, initialState);
|
|
129
|
+
if (next && consumeEcho(next)) return;
|
|
130
|
+
setSyncState(next);
|
|
131
|
+
// `consumeEcho` reads a ref; only a new URL seed or a mode change re-runs this.
|
|
132
|
+
}, [serverMode, initialState]);
|
|
133
|
+
return syncState;
|
|
134
|
+
}
|