@cosmicdrift/kumiko-renderer 0.388.0 → 0.389.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 (161) hide show
  1. package/dist/app/base-path-context.d.ts +9 -0
  2. package/dist/app/base-path-context.js +13 -0
  3. package/dist/app/base-path.d.ts +3 -0
  4. package/dist/app/base-path.js +27 -0
  5. package/dist/app/config-edit-body.d.ts +1 -1
  6. package/dist/app/config-edit-body.js +15 -6
  7. package/dist/app/config-edit-without-secret-fields.d.ts +2 -0
  8. package/dist/app/config-edit-without-secret-fields.js +28 -0
  9. package/dist/app/entity-list-facet-specs.d.ts +3 -0
  10. package/dist/app/entity-list-facet-specs.js +65 -0
  11. package/dist/app/entity-list-screen.js +74 -206
  12. package/dist/app/kumiko-screen.js +6 -14
  13. package/dist/app/list-bulk-actions.d.ts +18 -0
  14. package/dist/app/list-bulk-actions.js +54 -0
  15. package/dist/app/locked-screen-header.d.ts +9 -0
  16. package/dist/app/locked-screen-header.js +33 -0
  17. package/dist/app/projection-list-body.js +51 -46
  18. package/dist/app/row-actions.d.ts +3 -0
  19. package/dist/app/row-actions.js +10 -5
  20. package/dist/app/screen-shared.js +9 -2
  21. package/dist/app/use-entity-range-facets.d.ts +24 -0
  22. package/dist/app/use-entity-range-facets.js +64 -0
  23. package/dist/app/use-projection-date-range.d.ts +12 -0
  24. package/dist/app/use-projection-date-range.js +21 -0
  25. package/dist/app/use-screen-list-rows.d.ts +37 -0
  26. package/dist/app/use-screen-list-rows.js +104 -0
  27. package/dist/app/use-screen-list-table.d.ts +24 -0
  28. package/dist/app/use-screen-list-table.js +46 -0
  29. package/dist/components/list-group-page-bridges.d.ts +3 -0
  30. package/dist/components/list-group-page-bridges.js +31 -0
  31. package/dist/components/list-state-store.d.ts +16 -0
  32. package/dist/components/list-state-store.js +31 -0
  33. package/dist/components/related-list-section.js +37 -89
  34. package/dist/components/render-field.js +6 -5
  35. package/dist/components/render-list-toolbar-action.d.ts +27 -0
  36. package/dist/components/render-list-toolbar-action.js +40 -0
  37. package/dist/components/render-list.d.ts +13 -29
  38. package/dist/components/render-list.js +42 -50
  39. package/dist/current-user.d.ts +2 -0
  40. package/dist/data-table-aggregate-types.d.ts +47 -0
  41. package/dist/data-table-aggregate-types.js +1 -0
  42. package/dist/data-table-bulk-types.d.ts +27 -0
  43. package/dist/data-table-bulk-types.js +1 -0
  44. package/dist/data-table-facet-types.d.ts +36 -0
  45. package/dist/data-table-facet-types.js +1 -0
  46. package/dist/data-table-view-types.d.ts +73 -0
  47. package/dist/data-table-view-types.js +1 -0
  48. package/dist/headless/view-model/edit.js +3 -1
  49. package/dist/headless/view-model/types.d.ts +4 -2
  50. package/dist/hooks/list-column-layout.d.ts +19 -0
  51. package/dist/hooks/list-column-layout.js +36 -0
  52. package/dist/hooks/list-group-pages.d.ts +26 -0
  53. package/dist/hooks/list-group-pages.js +37 -0
  54. package/dist/hooks/list-reset.d.ts +8 -0
  55. package/dist/hooks/list-reset.js +15 -0
  56. package/dist/hooks/list-server-groups.d.ts +39 -0
  57. package/dist/hooks/list-server-groups.js +84 -0
  58. package/dist/hooks/list-state-schema.d.ts +28 -0
  59. package/dist/hooks/list-state-schema.js +76 -0
  60. package/dist/hooks/use-bulk-run.d.ts +41 -0
  61. package/dist/hooks/use-bulk-run.js +65 -0
  62. package/dist/hooks/use-group-load-state.d.ts +11 -0
  63. package/dist/hooks/use-group-load-state.js +31 -0
  64. package/dist/hooks/use-list-aggregates.d.ts +38 -0
  65. package/dist/hooks/use-list-aggregates.js +107 -0
  66. package/dist/hooks/use-list-table.d.ts +38 -0
  67. package/dist/hooks/use-list-table.js +91 -0
  68. package/dist/hooks/use-list-view-state.d.ts +43 -0
  69. package/dist/hooks/use-list-view-state.js +161 -0
  70. package/dist/hooks/use-related-list-rows.d.ts +27 -0
  71. package/dist/hooks/use-related-list-rows.js +81 -0
  72. package/dist/hooks/use-related-list-state.d.ts +15 -0
  73. package/dist/hooks/use-related-list-state.js +31 -0
  74. package/dist/hooks/use-remembered-list-params.d.ts +13 -0
  75. package/dist/hooks/use-remembered-list-params.js +53 -0
  76. package/dist/hooks/use-remembered-local-list-state.d.ts +26 -0
  77. package/dist/hooks/use-remembered-local-list-state.js +63 -0
  78. package/dist/i18n-defaults.js +53 -0
  79. package/dist/index.d.ts +3 -1
  80. package/dist/index.js +2 -0
  81. package/dist/primitives.d.ts +11 -47
  82. package/dist/web/app/create-app.d.ts +5 -0
  83. package/dist/web/app/create-app.js +7 -4
  84. package/dist/web/app/history-leave-guard.d.ts +6 -0
  85. package/dist/web/app/history-leave-guard.js +115 -0
  86. package/dist/web/app/list-state-store-boot.d.ts +6 -0
  87. package/dist/web/app/list-state-store-boot.js +15 -0
  88. package/dist/web/app/nav.js +7 -54
  89. package/dist/web/app/session-list-state-store.d.ts +15 -0
  90. package/dist/web/app/session-list-state-store.js +117 -0
  91. package/dist/web/index.d.ts +1 -0
  92. package/dist/web/index.js +1 -0
  93. package/dist/web/lib/export-xlsx-request.d.ts +11 -0
  94. package/dist/web/lib/export-xlsx-request.js +46 -0
  95. package/dist/web/primitives/data-table/actions-column.d.ts +16 -0
  96. package/dist/web/primitives/data-table/actions-column.js +34 -0
  97. package/dist/web/primitives/data-table/bulk-selection-bar.d.ts +20 -0
  98. package/dist/web/primitives/data-table/bulk-selection-bar.js +37 -0
  99. package/dist/web/primitives/data-table/card-sort-select.d.ts +14 -0
  100. package/dist/web/primitives/data-table/card-sort-select.js +40 -0
  101. package/dist/web/primitives/data-table/column-layout-mode.d.ts +5 -0
  102. package/dist/web/primitives/data-table/column-layout-mode.js +10 -0
  103. package/dist/web/primitives/data-table/column-picker-section.d.ts +7 -0
  104. package/dist/web/primitives/data-table/column-picker-section.js +35 -0
  105. package/dist/web/primitives/data-table/column-resize-handle.d.ts +14 -0
  106. package/dist/web/primitives/data-table/column-resize-handle.js +46 -0
  107. package/dist/web/primitives/data-table/density.d.ts +14 -0
  108. package/dist/web/primitives/data-table/density.js +20 -0
  109. package/dist/web/primitives/data-table/export-button.d.ts +5 -0
  110. package/dist/web/primitives/data-table/export-button.js +37 -0
  111. package/dist/web/primitives/data-table/facet-cluster.d.ts +8 -0
  112. package/dist/web/primitives/data-table/facet-cluster.js +37 -0
  113. package/dist/web/primitives/data-table/group-band.d.ts +28 -0
  114. package/dist/web/primitives/data-table/group-band.js +53 -0
  115. package/dist/web/primitives/data-table/measure-value.d.ts +10 -0
  116. package/dist/web/primitives/data-table/measure-value.js +28 -0
  117. package/dist/web/primitives/data-table/pin-section.d.ts +8 -0
  118. package/dist/web/primitives/data-table/pin-section.js +20 -0
  119. package/dist/web/primitives/data-table/reset-all-button.d.ts +4 -0
  120. package/dist/web/primitives/data-table/reset-all-button.js +8 -0
  121. package/dist/web/primitives/data-table/server-group-header.d.ts +11 -0
  122. package/dist/web/primitives/data-table/server-group-header.js +51 -0
  123. package/dist/web/primitives/data-table/sortable-header.d.ts +17 -0
  124. package/dist/web/primitives/data-table/sortable-header.js +41 -0
  125. package/dist/web/primitives/data-table/table-chrome.d.ts +13 -0
  126. package/dist/web/primitives/data-table/table-chrome.js +30 -0
  127. package/dist/web/primitives/data-table/table-footer.d.ts +7 -0
  128. package/dist/web/primitives/data-table/table-footer.js +20 -0
  129. package/dist/web/primitives/data-table/toolbar-end-cluster.d.ts +10 -0
  130. package/dist/web/primitives/data-table/toolbar-end-cluster.js +13 -0
  131. package/dist/web/primitives/data-table/totals-footer.d.ts +24 -0
  132. package/dist/web/primitives/data-table/totals-footer.js +48 -0
  133. package/dist/web/primitives/data-table/use-bulk-selection.d.ts +21 -0
  134. package/dist/web/primitives/data-table/use-bulk-selection.js +66 -0
  135. package/dist/web/primitives/data-table/use-column-pins.d.ts +32 -0
  136. package/dist/web/primitives/data-table/use-column-pins.js +117 -0
  137. package/dist/web/primitives/data-table/use-column-sizing.d.ts +22 -0
  138. package/dist/web/primitives/data-table/use-column-sizing.js +44 -0
  139. package/dist/web/primitives/data-table/use-measured-column-widths.d.ts +15 -0
  140. package/dist/web/primitives/data-table/use-measured-column-widths.js +83 -0
  141. package/dist/web/primitives/data-table/use-row-keyboard-navigation.d.ts +22 -0
  142. package/dist/web/primitives/data-table/use-row-keyboard-navigation.js +65 -0
  143. package/dist/web/primitives/data-table/use-row-selection.d.ts +14 -0
  144. package/dist/web/primitives/data-table/use-row-selection.js +48 -0
  145. package/dist/web/primitives/data-table/use-view-columns.d.ts +6 -0
  146. package/dist/web/primitives/data-table/use-view-columns.js +17 -0
  147. package/dist/web/primitives/data-table/view-menu.d.ts +8 -0
  148. package/dist/web/primitives/data-table/view-menu.js +45 -0
  149. package/dist/web/primitives/data-table-cell.d.ts +0 -22
  150. package/dist/web/primitives/data-table-cell.js +1 -58
  151. package/dist/web/primitives/data-table.d.ts +1 -1
  152. package/dist/web/primitives/data-table.js +122 -144
  153. package/dist/web/primitives/file-value-list.d.ts +1 -1
  154. package/dist/web/primitives/file-value-list.js +5 -5
  155. package/dist/web/primitives/number-range-facet.d.ts +7 -0
  156. package/dist/web/primitives/number-range-facet.js +33 -0
  157. package/dist/web/widgets/bounded-load-cache.d.ts +5 -1
  158. package/dist/web/widgets/bounded-load-cache.js +48 -26
  159. package/dist/web/widgets/pdf-preview.js +9 -4
  160. package/package.json +5 -5
  161. package/src/changes.json +109 -0
@@ -0,0 +1,43 @@
1
+ import type { ListColumnPin, ListColumnSpec, ListTableOptions } from "@cosmicdrift/kumiko-types/screen";
2
+ import type { ListStateStore } from "../components/list-state-store.tsx";
3
+ import type { DataTableColumnPins, DataTableColumnWidths, DataTableDensity, DataTableViewColumn } from "../data-table-view-types.ts";
4
+ export type ListViewSchemaColumn = {
5
+ readonly field: string;
6
+ readonly hidden?: boolean;
7
+ readonly pinned?: ListColumnPin;
8
+ readonly width?: number;
9
+ readonly minWidth?: number;
10
+ };
11
+ export type ListViewState = {
12
+ /** Undefined unless `table.columnPicker` is on. */
13
+ readonly columns: readonly DataTableViewColumn[] | undefined;
14
+ readonly density: DataTableDensity;
15
+ readonly userChoosesDensity: boolean;
16
+ readonly setColumns: (next: readonly DataTableViewColumn[]) => void;
17
+ readonly setDensity: (next: DataTableDensity) => void;
18
+ /** Undefined unless `table.pinning` is on. */
19
+ readonly pins: DataTableColumnPins | undefined;
20
+ readonly setPins: (next: DataTableColumnPins) => void;
21
+ /** Undefined unless `table.resizing` is on. */
22
+ readonly widths: DataTableColumnWidths | undefined;
23
+ readonly minWidths: DataTableColumnWidths | undefined;
24
+ readonly setWidth: (field: string, width: number) => void;
25
+ /** Active group field, "" = not grouped. */
26
+ readonly groupBy: string;
27
+ /** Undefined unless `table.grouping.userChoice` is on. */
28
+ readonly groupByFields: readonly string[] | undefined;
29
+ readonly setGroupBy: (next: string) => void;
30
+ readonly resetView: () => void;
31
+ readonly viewIsDefault: boolean;
32
+ };
33
+ export declare function listViewSchemaColumns(columns: readonly ListColumnSpec[]): ListViewSchemaColumn[];
34
+ type ViewStateArgs = {
35
+ readonly storeKey: string;
36
+ readonly store: ListStateStore;
37
+ readonly table: ListTableOptions;
38
+ readonly schemaColumns: readonly ListViewSchemaColumn[];
39
+ };
40
+ /** Column choice, order and density of one list. Lives in the list-state store
41
+ * (never in the URL) and is independent of `rememberState`. */
42
+ export declare function useListViewState(args: ViewStateArgs): ListViewState;
43
+ export {};
@@ -0,0 +1,161 @@
1
+ import { normalizeListColumn } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import { useCallback, useMemo, useState } from "react";
3
+ import { clampWidth, DEFAULT_MIN_COLUMN_WIDTH_PX, defaultPins, defaultWidths, mergeSavedWidths, minWidthsOf, sameRecord, } from "./list-column-layout.js";
4
+ import { sanitizeStoredListState } from "./list-state-schema.js";
5
+ export function listViewSchemaColumns(columns) {
6
+ return columns.map((column) => {
7
+ const { field, hidden, pinned, width, minWidth } = normalizeListColumn(column);
8
+ return {
9
+ field,
10
+ ...(hidden === true && { hidden }),
11
+ ...(pinned !== undefined && { pinned }),
12
+ ...(width !== undefined && { width }),
13
+ ...(minWidth !== undefined && { minWidth }),
14
+ };
15
+ });
16
+ }
17
+ function schemaDefaultColumns(schema) {
18
+ return schema.map((column) => ({ field: column.field, visible: column.hidden !== true }));
19
+ }
20
+ /** Saved order wins; a column the saved view has never seen takes its schema
21
+ * position (after its schema predecessor) with its schema default. */
22
+ function mergeSavedColumns(schema, saved) {
23
+ const merged = [...saved];
24
+ const present = new Set(saved.map((column) => column.field));
25
+ schema.forEach((column, index) => {
26
+ // skip: column already saved
27
+ if (present.has(column.field))
28
+ return;
29
+ const predecessor = schema[index - 1];
30
+ const predecessorAt = predecessor === undefined ? -1 : merged.findIndex((m) => m.field === predecessor.field);
31
+ merged.splice(predecessorAt + 1, 0, {
32
+ field: column.field,
33
+ visible: column.hidden !== true,
34
+ });
35
+ present.add(column.field);
36
+ });
37
+ return merged;
38
+ }
39
+ function sameColumns(a, b) {
40
+ return (a.length === b.length &&
41
+ a.every((c, i) => c.field === b[i]?.field && c.visible === b[i]?.visible));
42
+ }
43
+ function hasVisibleColumn(columns) {
44
+ return columns.some((column) => column.visible);
45
+ }
46
+ function groupByChoices(grouping) {
47
+ if (grouping?.userChoice !== true)
48
+ return undefined;
49
+ const offered = [
50
+ ...(grouping.field !== undefined ? [grouping.field] : []),
51
+ ...(grouping.fields ?? []),
52
+ ];
53
+ return [...new Set(offered)];
54
+ }
55
+ function isOfferedGroupBy(grouping, saved) {
56
+ if (saved === undefined)
57
+ return false;
58
+ const choices = groupByChoices(grouping);
59
+ return choices !== undefined && (saved === "" || choices.includes(saved));
60
+ }
61
+ function readInitialView(args, defaults) {
62
+ const saved = sanitizeStoredListState(args.store.read(args.storeKey), {
63
+ columnFields: args.schemaColumns.map((column) => column.field),
64
+ filterFields: [],
65
+ })?.view;
66
+ const savedGroupBy = saved?.groupBy;
67
+ const merged = args.table.columnPicker === true && saved?.columns !== undefined
68
+ ? mergeSavedColumns(args.schemaColumns, saved.columns)
69
+ : defaults.columns;
70
+ return {
71
+ columns: hasVisibleColumn(merged) ? merged : defaults.columns,
72
+ density: args.table.density?.userChoice === true && saved?.density ? saved.density : defaults.density,
73
+ // Saved pins replace the schema pins entirely, so the user can release a schema pin.
74
+ pins: args.table.pinning === true && saved?.pins !== undefined ? saved.pins : defaults.pins,
75
+ widths: args.table.resizing === true && saved?.widths !== undefined
76
+ ? mergeSavedWidths(defaults.widths, saved.widths, minWidthsOf(args.schemaColumns))
77
+ : defaults.widths,
78
+ groupBy: isOfferedGroupBy(args.table.grouping, savedGroupBy) ? savedGroupBy : defaults.groupBy,
79
+ };
80
+ }
81
+ /** Column choice, order and density of one list. Lives in the list-state store
82
+ * (never in the URL) and is independent of `rememberState`. */
83
+ export function useListViewState(args) {
84
+ const { storeKey, store, table, schemaColumns } = args;
85
+ const columnPicker = table.columnPicker === true;
86
+ const userChoosesDensity = table.density?.userChoice === true;
87
+ const pinning = table.pinning === true;
88
+ const resizing = table.resizing === true;
89
+ const groupByFields = useMemo(() => groupByChoices(table.grouping), [table.grouping]);
90
+ const userChoosesGroupBy = groupByFields !== undefined;
91
+ const minWidths = useMemo(() => minWidthsOf(schemaColumns), [schemaColumns]);
92
+ const defaults = useMemo(() => ({
93
+ columns: schemaDefaultColumns(schemaColumns),
94
+ density: table.density?.default ?? "comfortable",
95
+ pins: pinning ? defaultPins(schemaColumns) : {},
96
+ widths: resizing ? defaultWidths(schemaColumns, minWidths) : {},
97
+ groupBy: table.grouping?.field ?? "",
98
+ }), [schemaColumns, table.density?.default, pinning, resizing, minWidths, table.grouping?.field]);
99
+ const [seeded, setSeeded] = useState(() => ({
100
+ storeKey,
101
+ view: readInitialView(args, defaults),
102
+ }));
103
+ const view = seeded.storeKey === storeKey ? seeded.view : readInitialView(args, defaults);
104
+ if (seeded.storeKey !== storeKey)
105
+ setSeeded({ storeKey, view });
106
+ const commit = useCallback((next) => {
107
+ setSeeded({ storeKey, view: next });
108
+ store.write(storeKey, {
109
+ ...store.read(storeKey),
110
+ view: {
111
+ ...(columnPicker && { columns: next.columns }),
112
+ ...(userChoosesDensity && { density: next.density }),
113
+ ...(pinning && { pins: next.pins }),
114
+ ...(resizing && { widths: next.widths }),
115
+ ...(userChoosesGroupBy && { groupBy: next.groupBy }),
116
+ },
117
+ });
118
+ }, [store, storeKey, columnPicker, userChoosesDensity, pinning, resizing, userChoosesGroupBy]);
119
+ const setColumns = useCallback((next) => {
120
+ // skip: a view must keep at least one visible column
121
+ if (!hasVisibleColumn(next))
122
+ return;
123
+ commit({ ...view, columns: next });
124
+ }, [commit, view]);
125
+ const setDensity = useCallback((next) => commit({ ...view, density: next }), [commit, view]);
126
+ const setPins = useCallback((next) => commit({ ...view, pins: next }), [commit, view]);
127
+ const setWidth = useCallback((field, width) => commit({
128
+ ...view,
129
+ widths: {
130
+ ...view.widths,
131
+ [field]: clampWidth(width, minWidths[field] ?? DEFAULT_MIN_COLUMN_WIDTH_PX),
132
+ },
133
+ }), [commit, view, minWidths]);
134
+ const setGroupBy = useCallback((next) => commit({ ...view, groupBy: next }), [commit, view]);
135
+ const resetView = useCallback(() => {
136
+ setSeeded({ storeKey, view: defaults });
137
+ const { view: _dropped, ...rest } = store.read(storeKey) ?? {};
138
+ store.write(storeKey, rest);
139
+ }, [store, storeKey, defaults]);
140
+ return {
141
+ columns: columnPicker ? view.columns : undefined,
142
+ density: view.density,
143
+ userChoosesDensity,
144
+ setColumns,
145
+ setDensity,
146
+ pins: pinning ? view.pins : undefined,
147
+ setPins,
148
+ widths: resizing ? view.widths : undefined,
149
+ minWidths: resizing ? minWidths : undefined,
150
+ setWidth,
151
+ groupBy: view.groupBy,
152
+ groupByFields,
153
+ setGroupBy,
154
+ resetView,
155
+ viewIsDefault: view.density === defaults.density &&
156
+ (!columnPicker || sameColumns(view.columns, defaults.columns)) &&
157
+ (!pinning || sameRecord(view.pins, defaults.pins)) &&
158
+ (!resizing || sameRecord(view.widths, defaults.widths)) &&
159
+ view.groupBy === defaults.groupBy,
160
+ };
161
+ }
@@ -0,0 +1,27 @@
1
+ import type { EditRelatedListSectionViewModel, Translate } from "../headless/index.ts";
2
+ import type { DataTableSort } from "../primitives.tsx";
3
+ import { type ListAggregates } from "./use-list-aggregates.ts";
4
+ type Row = Readonly<Record<string, unknown>>;
5
+ export type RelatedListRows = {
6
+ readonly rows: readonly Row[];
7
+ readonly firstLoad: boolean;
8
+ readonly error: ListAggregates["error"];
9
+ readonly hasData: boolean;
10
+ /** The one-shot fetch stopped early, so the client-side sort covers part of the set. */
11
+ readonly truncated: boolean;
12
+ readonly aggregates: ListAggregates;
13
+ readonly refetchSelfAndHost: () => Promise<void>;
14
+ };
15
+ /** Rows of a relatedList section: one fetch sorted on the client, or the loaded
16
+ * rows of its server groups in server order when `table.grouping` is active. */
17
+ export declare function useRelatedListRows(args: {
18
+ readonly section: EditRelatedListSectionViewModel;
19
+ readonly payload: Readonly<Record<string, unknown>>;
20
+ readonly groupField: string | undefined;
21
+ readonly sort: DataTableSort | null;
22
+ readonly locale: string | undefined;
23
+ readonly refreshToken: unknown;
24
+ readonly onAfterWrite: (() => Promise<void> | void) | undefined;
25
+ readonly translate: Translate;
26
+ }): RelatedListRows;
27
+ export {};
@@ -0,0 +1,81 @@
1
+ import { normalizeListColumn } from "@cosmicdrift/kumiko-framework/ui-types";
2
+ import { useCallback, useEffect, useMemo, useRef } from "react";
3
+ import { sortByAccessor } from "../sort-by-accessor.js";
4
+ import { useListAggregates } from "./use-list-aggregates.js";
5
+ import { useQuery } from "./use-query.js";
6
+ const DEFAULT_GROUP_PAGE_SIZE = 50;
7
+ // A handler that omits `nextCursor` (or reports a `total` the fetched rows
8
+ // already cover) has no further rows; only a real cursor marks truncation.
9
+ function isTruncatedFetch(data) {
10
+ return (data !== null &&
11
+ typeof data.nextCursor === "string" &&
12
+ (data.total === undefined || data.total > data.rows.length));
13
+ }
14
+ /** Rows of a relatedList section: one fetch sorted on the client, or the loaded
15
+ * rows of its server groups in server order when `table.grouping` is active. */
16
+ export function useRelatedListRows(args) {
17
+ const { section, payload, groupField, sort, locale, refreshToken, onAfterWrite } = args;
18
+ const aggregates = useListAggregates({
19
+ queryType: section.query,
20
+ payload,
21
+ table: section.table,
22
+ groupField,
23
+ pageSize: section.pageSize ?? DEFAULT_GROUP_PAGE_SIZE,
24
+ paging: "cursor",
25
+ translate: args.translate,
26
+ });
27
+ const { grouped, refresh: refreshAggregates } = aggregates;
28
+ const rowsQuery = useQuery(section.query, payload, { enabled: !grouped });
29
+ const ownRefetch = rowsQuery.refetch;
30
+ const refetchOwn = useCallback(async () => {
31
+ refreshAggregates();
32
+ if (!grouped)
33
+ await ownRefetch();
34
+ }, [refreshAggregates, grouped, ownRefetch]);
35
+ const lastRefreshTokenRef = useRef(refreshToken);
36
+ useEffect(() => {
37
+ // skip: the token has not changed since the last refetch
38
+ if (lastRefreshTokenRef.current === refreshToken)
39
+ return;
40
+ lastRefreshTokenRef.current = refreshToken;
41
+ void refetchOwn();
42
+ }, [refreshToken, refetchOwn]);
43
+ const refetchSelfAndHost = useCallback(async () => {
44
+ await refetchOwn();
45
+ await onAfterWrite?.();
46
+ }, [refetchOwn, onAfterWrite]);
47
+ const sortAccessors = useMemo(() => {
48
+ const accessors = {};
49
+ for (const col of section.columns) {
50
+ const field = normalizeListColumn(col).field;
51
+ accessors[field] = (row) => {
52
+ const value = row[field];
53
+ if (value === null || value === undefined)
54
+ return null;
55
+ return typeof value === "number" ? value : String(value);
56
+ };
57
+ }
58
+ return accessors;
59
+ }, [section.columns]);
60
+ const sortedRows = useMemo(() => sortByAccessor(rowsQuery.data?.rows ?? [], sort, sortAccessors, locale), [rowsQuery.data, sort, sortAccessors, locale]);
61
+ if (grouped) {
62
+ return {
63
+ rows: aggregates.rows,
64
+ firstLoad: aggregates.loading,
65
+ error: aggregates.error,
66
+ hasData: aggregates.serverGroups !== undefined,
67
+ truncated: false,
68
+ aggregates,
69
+ refetchSelfAndHost,
70
+ };
71
+ }
72
+ return {
73
+ rows: sortedRows,
74
+ firstLoad: rowsQuery.loading && rowsQuery.data === null,
75
+ error: rowsQuery.error,
76
+ hasData: rowsQuery.data !== null,
77
+ truncated: isTruncatedFetch(rowsQuery.data),
78
+ aggregates,
79
+ refetchSelfAndHost,
80
+ };
81
+ }
@@ -0,0 +1,15 @@
1
+ import type { EditRelatedListSectionViewModel } from "../headless/index.ts";
2
+ /** Search, filters and sort of a relatedList section (component-local: it has
3
+ * no URL namespace) plus the DataTable props of its `table` block. */
4
+ export declare function useRelatedListState(section: EditRelatedListSectionViewModel, parentScreenId: string | undefined): {
5
+ search: string;
6
+ filters: Readonly<Record<string, readonly string[]>>;
7
+ sort: import("./use-list-url-state.ts").ListSort | null;
8
+ setSearch: (search: string) => void;
9
+ setFilters: import("react").Dispatch<import("react").SetStateAction<Readonly<Record<string, readonly string[]>>>>;
10
+ setSort: (sort: import("./use-list-url-state.ts").ListSort | null) => void;
11
+ reset: () => void;
12
+ atDefault: boolean;
13
+ listTable: import("./use-list-table.ts").ListTableProps;
14
+ groupField: string | undefined;
15
+ };
@@ -0,0 +1,31 @@
1
+ import { useMemo } from "react";
2
+ import { useListStateStore } from "../components/list-state-store.js";
3
+ import { useListTable } from "./use-list-table.js";
4
+ import { listViewSchemaColumns } from "./use-list-view-state.js";
5
+ import { useRememberedLocalListState } from "./use-remembered-local-list-state.js";
6
+ /** Search, filters and sort of a relatedList section (component-local: it has
7
+ * no URL namespace) plus the DataTable props of its `table` block. */
8
+ export function useRelatedListState(section, parentScreenId) {
9
+ const store = useListStateStore();
10
+ const storeKey = `${parentScreenId ?? ""}#${section.id ?? section.query}`;
11
+ const defaultSort = useMemo(() => section.defaultSort ?? null, [section.defaultSort]);
12
+ const schemaColumns = useMemo(() => listViewSchemaColumns(section.columns), [section.columns]);
13
+ const known = useMemo(() => ({
14
+ columnFields: schemaColumns.map((column) => column.field),
15
+ filterFields: (section.facets ?? []).map((facet) => facet.field),
16
+ }), [schemaColumns, section.facets]);
17
+ const local = useRememberedLocalListState({
18
+ storeKey,
19
+ store,
20
+ remember: section.table?.rememberState === true,
21
+ defaultSort,
22
+ known,
23
+ });
24
+ const { props: listTable, groupField } = useListTable({
25
+ storeKey,
26
+ table: section.table,
27
+ schemaColumns,
28
+ local: { atDefault: local.atDefault, reset: local.reset },
29
+ });
30
+ return { ...local, listTable, groupField };
31
+ }
@@ -0,0 +1,13 @@
1
+ import type { ListStateStore } from "../components/list-state-store.tsx";
2
+ import { type KnownListFields } from "./list-state-schema.ts";
3
+ type RememberedListParamsArgs = {
4
+ readonly screenId: string;
5
+ readonly enabled: boolean;
6
+ readonly store: ListStateStore;
7
+ readonly known: KnownListFields;
8
+ };
9
+ /** URL stays the source of truth: the stored list params are copied into the
10
+ * URL once when the screen opens without any, and every later URL change is
11
+ * written back. A link that already carries list params is never overridden. */
12
+ export declare function useRememberedListParams({ screenId, enabled, store, known, }: RememberedListParamsArgs): void;
13
+ export {};
@@ -0,0 +1,53 @@
1
+ import { useEffect, useMemo, useRef } from "react";
2
+ import { useNav } from "../app/nav.js";
3
+ import { listParamKeys } from "./list-reset.js";
4
+ import { sanitizeStoredListState } from "./list-state-schema.js";
5
+ function stripScreenPrefix(params, screenId) {
6
+ const prefix = `${screenId}.`;
7
+ return Object.fromEntries(listParamKeys(params, screenId).flatMap((key) => {
8
+ const value = params[key];
9
+ return value === undefined ? [] : [[key.slice(prefix.length), value]];
10
+ }));
11
+ }
12
+ function sameParams(a, b) {
13
+ const keys = Object.keys(a);
14
+ return keys.length === Object.keys(b).length && keys.every((key) => a[key] === b[key]);
15
+ }
16
+ /** URL stays the source of truth: the stored list params are copied into the
17
+ * URL once when the screen opens without any, and every later URL change is
18
+ * written back. A link that already carries list params is never overridden. */
19
+ export function useRememberedListParams({ screenId, enabled, store, known, }) {
20
+ const nav = useNav();
21
+ const current = useMemo(() => stripScreenPrefix(nav.searchParams, screenId), [nav.searchParams, screenId]);
22
+ const phaseRef = useRef({ screenId, phase: "pending" });
23
+ useEffect(() => {
24
+ // skip: remembering is not enabled
25
+ if (!enabled)
26
+ return;
27
+ if (phaseRef.current.screenId !== screenId)
28
+ phaseRef.current = { screenId, phase: "pending" };
29
+ const state = phaseRef.current;
30
+ const currentIsEmpty = Object.keys(current).length === 0;
31
+ if (state.phase === "pending" && currentIsEmpty) {
32
+ const stored = sanitizeStoredListState(store.read(screenId), known)?.params;
33
+ if (stored !== undefined && Object.keys(stored).length > 0) {
34
+ state.phase = "restoring";
35
+ nav.setSearchParams(Object.fromEntries(Object.entries(stored).map(([k, v]) => [`${screenId}.${k}`, v])));
36
+ // skip: restore started, effect reruns with the restored URL
37
+ return;
38
+ }
39
+ }
40
+ // skip: waiting for the restored URL to arrive
41
+ if (state.phase === "restoring" && currentIsEmpty)
42
+ return;
43
+ state.phase = "live";
44
+ const existing = store.read(screenId);
45
+ // skip: nothing stored and nothing to store
46
+ if (existing?.params === undefined && currentIsEmpty)
47
+ return;
48
+ // skip: stored state already matches the URL
49
+ if (existing?.params !== undefined && sameParams(existing.params, current))
50
+ return;
51
+ store.write(screenId, { ...existing, params: current });
52
+ }, [enabled, screenId, current, known, store, nav]);
53
+ }
@@ -0,0 +1,26 @@
1
+ import { type Dispatch, type SetStateAction } from "react";
2
+ import type { ListStateStore } from "../components/list-state-store.tsx";
3
+ import { type KnownListFields } from "./list-state-schema.ts";
4
+ import type { ListSort } from "./use-list-url-state.ts";
5
+ type LocalFilters = Readonly<Record<string, readonly string[]>>;
6
+ type LocalListStateArgs = {
7
+ readonly storeKey: string;
8
+ readonly store: ListStateStore;
9
+ /** `table.rememberState`: seeds from the store and writes every change back. */
10
+ readonly remember: boolean;
11
+ readonly defaultSort: ListSort | null;
12
+ readonly known: KnownListFields;
13
+ };
14
+ /** Search, facet filters and sort of a list whose state is component-local
15
+ * (relatedList has no URL namespace). Same stored `params` format as the URL lists. */
16
+ export declare function useRememberedLocalListState({ storeKey, store, remember, defaultSort, known, }: LocalListStateArgs): {
17
+ search: string;
18
+ filters: LocalFilters;
19
+ sort: ListSort | null;
20
+ setSearch: (search: string) => void;
21
+ setFilters: Dispatch<SetStateAction<Readonly<Record<string, readonly string[]>>>>;
22
+ setSort: (sort: ListSort | null) => void;
23
+ reset: () => void;
24
+ atDefault: boolean;
25
+ };
26
+ export {};
@@ -0,0 +1,63 @@
1
+ import { useCallback, useEffect, useState } from "react";
2
+ import { sanitizeStoredListState } from "./list-state-schema.js";
3
+ const FILTER_PREFIX = "f.";
4
+ function sameSort(a, b) {
5
+ return a === b || (a !== null && b !== null && a.field === b.field && a.dir === b.dir);
6
+ }
7
+ function toParams(state, defaultSort) {
8
+ const params = {};
9
+ if (state.search !== "")
10
+ params["q"] = state.search;
11
+ if (!sameSort(state.sort, defaultSort) && state.sort !== null) {
12
+ params["sort"] = state.sort.field;
13
+ params["dir"] = state.sort.dir;
14
+ }
15
+ for (const [field, values] of Object.entries(state.filters)) {
16
+ if (values.length > 0)
17
+ params[`${FILTER_PREFIX}${field}`] = values.join(",");
18
+ }
19
+ return params;
20
+ }
21
+ function fromParams(params, defaultSort) {
22
+ const sortField = params?.["sort"];
23
+ const dir = params?.["dir"];
24
+ const sort = sortField !== undefined && (dir === "asc" || dir === "desc")
25
+ ? { field: sortField, dir }
26
+ : defaultSort;
27
+ const filters = {};
28
+ for (const [key, value] of Object.entries(params ?? {})) {
29
+ if (key.startsWith(FILTER_PREFIX)) {
30
+ filters[key.slice(FILTER_PREFIX.length)] = value.split(",").filter((v) => v !== "");
31
+ }
32
+ }
33
+ return { search: params?.["q"] ?? "", filters, sort };
34
+ }
35
+ /** Search, facet filters and sort of a list whose state is component-local
36
+ * (relatedList has no URL namespace). Same stored `params` format as the URL lists. */
37
+ export function useRememberedLocalListState({ storeKey, store, remember, defaultSort, known, }) {
38
+ const [state, setState] = useState(() => remember
39
+ ? fromParams(sanitizeStoredListState(store.read(storeKey), known)?.params, defaultSort)
40
+ : { search: "", filters: {}, sort: defaultSort });
41
+ useEffect(() => {
42
+ // skip: remembering is not enabled
43
+ if (!remember)
44
+ return;
45
+ const params = toParams(state, defaultSort);
46
+ const existing = store.read(storeKey);
47
+ // skip: nothing stored and nothing to store
48
+ if (existing?.params === undefined && Object.keys(params).length === 0)
49
+ return;
50
+ store.write(storeKey, { ...existing, params });
51
+ }, [remember, state, defaultSort, store, storeKey]);
52
+ const setSearch = useCallback((search) => setState((prev) => ({ ...prev, search })), []);
53
+ const setFilters = useCallback((next) => setState((prev) => ({
54
+ ...prev,
55
+ filters: typeof next === "function" ? next(prev.filters) : next,
56
+ })), []);
57
+ const setSort = useCallback((sort) => setState((prev) => ({ ...prev, sort })), []);
58
+ const reset = useCallback(() => setState({ search: "", filters: {}, sort: defaultSort }), [defaultSort]);
59
+ const atDefault = state.search === "" &&
60
+ Object.values(state.filters).every((values) => values.length === 0) &&
61
+ sameSort(state.sort, defaultSort);
62
+ return { ...state, setSearch, setFilters, setSort, reset, atDefault };
63
+ }
@@ -86,6 +86,44 @@ export const kumikoDefaultTranslations = {
86
86
  "kumiko.list.sort.unsorted": "Unsorted",
87
87
  "kumiko.list.filter.reset": "Reset",
88
88
  "kumiko.list.filter.toggle": "Filters",
89
+ "kumiko.list.reset-all": "Reset",
90
+ "kumiko.list.export": "Export",
91
+ "kumiko.list.export-busy": "Exporting…",
92
+ "kumiko.list.export-too-many": "More than {max} rows. Narrow the list with a filter and export again.",
93
+ "kumiko.list.export-failed": "The export failed. Please try again.",
94
+ "kumiko.list.view.menu": "View",
95
+ "kumiko.list.view.columns": "Columns",
96
+ "kumiko.list.view.density": "Density",
97
+ "kumiko.list.view.density.comfortable": "Comfortable",
98
+ "kumiko.list.view.density.compact": "Compact",
99
+ "kumiko.list.view.reset": "Reset view",
100
+ "kumiko.list.group.by": "Group by",
101
+ "kumiko.list.group.none": "None",
102
+ "kumiko.list.group.empty": "(empty)",
103
+ "kumiko.list.group.load-more": "Load more",
104
+ "kumiko.list.group.loading": "Loading…",
105
+ "kumiko.list.group.loaded": "{loaded} of {count}",
106
+ "kumiko.list.measure.sum": "Sum of {label}",
107
+ "kumiko.list.measure.avg": "Average {label}",
108
+ "kumiko.list.measure.count": "Count",
109
+ "kumiko.list.measure.min": "Minimum {label}",
110
+ "kumiko.list.measure.max": "Maximum {label}",
111
+ "kumiko.list.measure.kind.sum": "Sum",
112
+ "kumiko.list.measure.kind.avg": "Average",
113
+ "kumiko.list.measure.kind.count": "Count",
114
+ "kumiko.list.measure.kind.min": "Minimum",
115
+ "kumiko.list.measure.kind.max": "Maximum",
116
+ "kumiko.list.totals.label": "Total",
117
+ "kumiko.list.range.min": "From",
118
+ "kumiko.list.range.max": "To",
119
+ "kumiko.list.notice.truncated": "Not every group is shown. Please narrow the list with a filter.",
120
+ "kumiko.list.notice.searchTruncated": "The search matched too many entries; groups and totals cover only part of them. Please filter further.",
121
+ "kumiko.list.view.column.move-up": "Move {column} up",
122
+ "kumiko.list.view.pinning": "Pinned columns",
123
+ "kumiko.list.view.pin-left": "Pin {column} left",
124
+ "kumiko.list.view.pin-right": "Pin {column} right",
125
+ "kumiko.list.column.resize": "Resize {column}",
126
+ "kumiko.list.view.column.move-down": "Move {column} down",
89
127
  "kumiko.list.related-list-truncated": "Showing the first {count} entries. More are available but not loaded — this list does not paginate.",
90
128
  "kumiko.reference.system-tenant": "System",
91
129
  "kumiko.reference.system-user": "System",
@@ -111,6 +149,21 @@ export const kumikoDefaultTranslations = {
111
149
  "kumiko.theme.light": "Light theme",
112
150
  "kumiko.list.row.collapse": "Collapse {title}",
113
151
  "kumiko.list.row.expand": "Expand {title}",
152
+ "kumiko.list.bulk.selected": "{count} selected",
153
+ "kumiko.list.bulk.clear": "Clear selection",
154
+ "kumiko.list.bulk.limit": "At most {max} rows per selection",
155
+ "kumiko.list.bulk.select-all": "Select all rows on this page",
156
+ "kumiko.list.bulk.select-row": "Select {title}",
157
+ "kumiko.list.bulk.progress": "{settled} of {total} done",
158
+ "kumiko.list.bulk.cancel": "Cancel",
159
+ "kumiko.list.bulk.cancelling": "Cancelling…",
160
+ "kumiko.list.bulk.confirm": "Run “{action}” on the selection ({count})?",
161
+ "kumiko.list.bulk.result.title": "“{action}” finished",
162
+ "kumiko.list.bulk.result.succeeded": "Succeeded: {count}",
163
+ "kumiko.list.bulk.result.failed": "Failed: {count}",
164
+ "kumiko.list.bulk.result.skipped": "Skipped: {count}",
165
+ "kumiko.list.bulk.result.not-started": "Not run: {count}",
166
+ "kumiko.list.bulk.result.dismiss": "Dismiss",
114
167
  "kumiko.list.sort.by": "Sorted by {column}",
115
168
  "kumiko.form.changed": "changed",
116
169
  "kumiko.form.on-this-page": "On this page",
package/dist/index.d.ts CHANGED
@@ -2,6 +2,8 @@ export { type Formality, formalLocaleTag } from "@cosmicdrift/kumiko-framework/u
2
2
  export { synthesizeActionFormEntity, synthesizeActionFormScreen } from "./app/action-form-shim.ts";
3
3
  export type { AppFeaturesProviderProps } from "./app/app-features-context.tsx";
4
4
  export { AppFeaturesProvider, useAppFeatures } from "./app/app-features-context.tsx";
5
+ export { normalizeBasePath, prependBasePath, stripBasePath } from "./app/base-path.ts";
6
+ export { BasePathProvider, useBasePath } from "./app/base-path-context.tsx";
5
7
  export type { ActionNotice, ActionNotifier, ClientActionHandler, ClientActionOutcome, ClientActionRuntime, ClientActionsMap, } from "./app/client-actions.tsx";
6
8
  export { ActionNoticeProvider, ClientActionsProvider, runClientAction, useClientActionHandler, useClientActionRuntime, } from "./app/client-actions.tsx";
7
9
  export type { ColumnRendererComponent, ColumnRendererProps, ColumnRenderersMap, ColumnRenderersProviderProps, } from "./app/column-renderers.tsx";
@@ -86,7 +88,7 @@ export { InsideDrawerProvider, useInsideDrawer } from "./inside-drawer.ts";
86
88
  export { hasUnsavedInput, registerUnsavedInputSource, requestLeave, setLeaveConfirmer, useUnsavedInputSource, } from "./leave-guard.ts";
87
89
  export { LeaveGuardDialog } from "./leave-guard-dialog.tsx";
88
90
  export { PageHeaderCompactProvider, PageHeaderSlotAvailableProvider, usePageHeaderCompact, usePageHeaderSlotAvailable, } from "./page-header-slot.ts";
89
- export type { ActionMenuItemSpec, ActionOverflowMenuProps, AppPrimitives, BannerProps, ButtonProps, CardOptions, CardProps, CardSlots, CopyButtonProps, CorePrimitives, DataTableDateRangeFacet, DataTableFacet, DataTableFacetChip, DataTableProps, DataTableRowAction, DataTableRowActionMode, DataTableRowGrouping, DataTableSort, DataTableSortDir, DialogProps, DrawerProps, EmbeddedListCellType, EmbeddedListColumn, EmbeddedListInputProps, EmbeddedListTotal, FieldCellWidth, FieldProps, FileValueListProps, FillContainerProps, FooterActionMarker, FooterActionRole, FormProps, FormSectionNavItem, FormWidth, GridCellProps, GridProps, HeadingProps, InputFillAction, InputProps, JsonViewProps, LightboxImage, LightboxProps, LinkProps, MetricBandProps, MetricProps, ModalProps, PageHeaderProps, PickedFile, PrimitivesProviderProps, PrimitivesRegistry, ProgressProps, ProgressTone, PromoPanelAction, PromoPanelProps, RuntimeRenderer, SecretRevealProps, SecretRevealValue, SectionProps, ShareButtonProps, StatusBadgeProps, StatusTone, StepBarProps, StickyActionBarProps, StickyPrimaryActionMarker, TabsProps, TextProps, TrailingInputAction, WizardStepGroupProps, } from "./primitives.tsx";
91
+ export type { ActionMenuItemSpec, ActionOverflowMenuProps, AppPrimitives, BannerProps, BulkRowOutcome, ButtonProps, CardOptions, CardProps, CardSlots, CopyButtonProps, CorePrimitives, DataTableBulk, DataTableBulkAction, DataTableDateRangeFacet, DataTableFacet, DataTableFacetChip, DataTableProps, DataTableRowAction, DataTableRowActionMode, DataTableSort, DataTableSortDir, DialogProps, DrawerProps, EmbeddedListCellType, EmbeddedListColumn, EmbeddedListInputProps, EmbeddedListTotal, FieldCellWidth, FieldProps, FileValueListProps, FillContainerProps, FooterActionMarker, FooterActionRole, FormProps, FormSectionNavItem, FormWidth, GridCellProps, GridProps, HeadingProps, InputFillAction, InputProps, JsonViewProps, LightboxImage, LightboxProps, LinkProps, MetricBandProps, MetricProps, ModalProps, PageHeaderProps, PickedFile, PrimitivesProviderProps, PrimitivesRegistry, ProgressProps, ProgressTone, PromoPanelAction, PromoPanelProps, RuntimeRenderer, SecretRevealProps, SecretRevealValue, SectionProps, ShareButtonProps, StatusBadgeProps, StatusTone, StepBarProps, StickyActionBarProps, StickyPrimaryActionMarker, TabsProps, TextProps, TrailingInputAction, WizardStepGroupProps, } from "./primitives.tsx";
90
92
  export { FOOTER_ACTION_ROLE_PROP, NARROW_LABEL_PROP, PrimitivesProvider, STICKY_PRIMARY_ACTION_PROP, shouldRenderActionsIconOnly, statusToneForOptionTone, statusToneForValue, useOptionalPrimitives, usePrimitives, } from "./primitives.tsx";
91
93
  export type { SessionEndedSignal, SessionEndedSignalProviderProps, } from "./session/session-ended.tsx";
92
94
  export { createSessionEndedSignal, isSessionEndedError, SESSION_ENDED_ERROR_CODES, SessionEndedSignalProvider, useSessionEndedSignal, withSessionEndedDetection, } from "./session/session-ended.tsx";
package/dist/index.js CHANGED
@@ -11,6 +11,8 @@
11
11
  export { formalLocaleTag } from "@cosmicdrift/kumiko-framework/ui-types";
12
12
  export { synthesizeActionFormEntity, synthesizeActionFormScreen } from "./app/action-form-shim.js";
13
13
  export { AppFeaturesProvider, useAppFeatures } from "./app/app-features-context.js";
14
+ export { normalizeBasePath, prependBasePath, stripBasePath } from "./app/base-path.js";
15
+ export { BasePathProvider, useBasePath } from "./app/base-path-context.js";
14
16
  export { ActionNoticeProvider, ClientActionsProvider, runClientAction, useClientActionHandler, useClientActionRuntime, } from "./app/client-actions.js";
15
17
  export { ColumnRenderersProvider, useColumnRenderer } from "./app/column-renderers.js";
16
18
  export { CONTENT_EDITOR_ELEMENT_ID, ContentEditorsProvider, TextareaContentEditor, useContentEditor, } from "./app/content-editors.js";