@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.
- package/dist/app/base-path-context.d.ts +9 -0
- package/dist/app/base-path-context.js +13 -0
- package/dist/app/base-path.d.ts +3 -0
- package/dist/app/base-path.js +27 -0
- package/dist/app/config-edit-body.d.ts +1 -1
- package/dist/app/config-edit-body.js +15 -6
- package/dist/app/config-edit-without-secret-fields.d.ts +2 -0
- package/dist/app/config-edit-without-secret-fields.js +28 -0
- package/dist/app/entity-list-facet-specs.d.ts +3 -0
- package/dist/app/entity-list-facet-specs.js +65 -0
- package/dist/app/entity-list-screen.js +74 -206
- package/dist/app/kumiko-screen.js +6 -14
- package/dist/app/list-bulk-actions.d.ts +18 -0
- package/dist/app/list-bulk-actions.js +54 -0
- package/dist/app/locked-screen-header.d.ts +9 -0
- package/dist/app/locked-screen-header.js +33 -0
- package/dist/app/projection-list-body.js +51 -46
- package/dist/app/row-actions.d.ts +3 -0
- package/dist/app/row-actions.js +10 -5
- package/dist/app/screen-shared.js +9 -2
- package/dist/app/use-entity-range-facets.d.ts +24 -0
- package/dist/app/use-entity-range-facets.js +64 -0
- package/dist/app/use-projection-date-range.d.ts +12 -0
- package/dist/app/use-projection-date-range.js +21 -0
- package/dist/app/use-screen-list-rows.d.ts +37 -0
- package/dist/app/use-screen-list-rows.js +104 -0
- package/dist/app/use-screen-list-table.d.ts +24 -0
- package/dist/app/use-screen-list-table.js +46 -0
- package/dist/components/list-group-page-bridges.d.ts +3 -0
- package/dist/components/list-group-page-bridges.js +31 -0
- package/dist/components/list-state-store.d.ts +16 -0
- package/dist/components/list-state-store.js +31 -0
- package/dist/components/related-list-section.js +37 -89
- package/dist/components/render-field.js +6 -5
- package/dist/components/render-list-toolbar-action.d.ts +27 -0
- package/dist/components/render-list-toolbar-action.js +40 -0
- package/dist/components/render-list.d.ts +13 -29
- package/dist/components/render-list.js +42 -50
- package/dist/current-user.d.ts +2 -0
- package/dist/data-table-aggregate-types.d.ts +47 -0
- package/dist/data-table-aggregate-types.js +1 -0
- package/dist/data-table-bulk-types.d.ts +27 -0
- package/dist/data-table-bulk-types.js +1 -0
- package/dist/data-table-facet-types.d.ts +36 -0
- package/dist/data-table-facet-types.js +1 -0
- package/dist/data-table-view-types.d.ts +73 -0
- package/dist/data-table-view-types.js +1 -0
- package/dist/headless/view-model/edit.js +3 -1
- package/dist/headless/view-model/types.d.ts +4 -2
- package/dist/hooks/list-column-layout.d.ts +19 -0
- package/dist/hooks/list-column-layout.js +36 -0
- package/dist/hooks/list-group-pages.d.ts +26 -0
- package/dist/hooks/list-group-pages.js +37 -0
- package/dist/hooks/list-reset.d.ts +8 -0
- package/dist/hooks/list-reset.js +15 -0
- package/dist/hooks/list-server-groups.d.ts +39 -0
- package/dist/hooks/list-server-groups.js +84 -0
- package/dist/hooks/list-state-schema.d.ts +28 -0
- package/dist/hooks/list-state-schema.js +76 -0
- package/dist/hooks/use-bulk-run.d.ts +41 -0
- package/dist/hooks/use-bulk-run.js +65 -0
- package/dist/hooks/use-group-load-state.d.ts +11 -0
- package/dist/hooks/use-group-load-state.js +31 -0
- package/dist/hooks/use-list-aggregates.d.ts +38 -0
- package/dist/hooks/use-list-aggregates.js +107 -0
- package/dist/hooks/use-list-table.d.ts +38 -0
- package/dist/hooks/use-list-table.js +91 -0
- package/dist/hooks/use-list-view-state.d.ts +43 -0
- package/dist/hooks/use-list-view-state.js +161 -0
- package/dist/hooks/use-related-list-rows.d.ts +27 -0
- package/dist/hooks/use-related-list-rows.js +81 -0
- package/dist/hooks/use-related-list-state.d.ts +15 -0
- package/dist/hooks/use-related-list-state.js +31 -0
- package/dist/hooks/use-remembered-list-params.d.ts +13 -0
- package/dist/hooks/use-remembered-list-params.js +53 -0
- package/dist/hooks/use-remembered-local-list-state.d.ts +26 -0
- package/dist/hooks/use-remembered-local-list-state.js +63 -0
- package/dist/i18n-defaults.js +53 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +2 -0
- package/dist/primitives.d.ts +11 -47
- package/dist/web/app/create-app.d.ts +5 -0
- package/dist/web/app/create-app.js +7 -4
- package/dist/web/app/history-leave-guard.d.ts +6 -0
- package/dist/web/app/history-leave-guard.js +115 -0
- package/dist/web/app/list-state-store-boot.d.ts +6 -0
- package/dist/web/app/list-state-store-boot.js +15 -0
- package/dist/web/app/nav.js +7 -54
- package/dist/web/app/session-list-state-store.d.ts +15 -0
- package/dist/web/app/session-list-state-store.js +117 -0
- package/dist/web/index.d.ts +1 -0
- package/dist/web/index.js +1 -0
- package/dist/web/lib/export-xlsx-request.d.ts +11 -0
- package/dist/web/lib/export-xlsx-request.js +46 -0
- package/dist/web/primitives/data-table/actions-column.d.ts +16 -0
- package/dist/web/primitives/data-table/actions-column.js +34 -0
- package/dist/web/primitives/data-table/bulk-selection-bar.d.ts +20 -0
- package/dist/web/primitives/data-table/bulk-selection-bar.js +37 -0
- package/dist/web/primitives/data-table/card-sort-select.d.ts +14 -0
- package/dist/web/primitives/data-table/card-sort-select.js +40 -0
- package/dist/web/primitives/data-table/column-layout-mode.d.ts +5 -0
- package/dist/web/primitives/data-table/column-layout-mode.js +10 -0
- package/dist/web/primitives/data-table/column-picker-section.d.ts +7 -0
- package/dist/web/primitives/data-table/column-picker-section.js +35 -0
- package/dist/web/primitives/data-table/column-resize-handle.d.ts +14 -0
- package/dist/web/primitives/data-table/column-resize-handle.js +46 -0
- package/dist/web/primitives/data-table/density.d.ts +14 -0
- package/dist/web/primitives/data-table/density.js +20 -0
- package/dist/web/primitives/data-table/export-button.d.ts +5 -0
- package/dist/web/primitives/data-table/export-button.js +37 -0
- package/dist/web/primitives/data-table/facet-cluster.d.ts +8 -0
- package/dist/web/primitives/data-table/facet-cluster.js +37 -0
- package/dist/web/primitives/data-table/group-band.d.ts +28 -0
- package/dist/web/primitives/data-table/group-band.js +53 -0
- package/dist/web/primitives/data-table/measure-value.d.ts +10 -0
- package/dist/web/primitives/data-table/measure-value.js +28 -0
- package/dist/web/primitives/data-table/pin-section.d.ts +8 -0
- package/dist/web/primitives/data-table/pin-section.js +20 -0
- package/dist/web/primitives/data-table/reset-all-button.d.ts +4 -0
- package/dist/web/primitives/data-table/reset-all-button.js +8 -0
- package/dist/web/primitives/data-table/server-group-header.d.ts +11 -0
- package/dist/web/primitives/data-table/server-group-header.js +51 -0
- package/dist/web/primitives/data-table/sortable-header.d.ts +17 -0
- package/dist/web/primitives/data-table/sortable-header.js +41 -0
- package/dist/web/primitives/data-table/table-chrome.d.ts +13 -0
- package/dist/web/primitives/data-table/table-chrome.js +30 -0
- package/dist/web/primitives/data-table/table-footer.d.ts +7 -0
- package/dist/web/primitives/data-table/table-footer.js +20 -0
- package/dist/web/primitives/data-table/toolbar-end-cluster.d.ts +10 -0
- package/dist/web/primitives/data-table/toolbar-end-cluster.js +13 -0
- package/dist/web/primitives/data-table/totals-footer.d.ts +24 -0
- package/dist/web/primitives/data-table/totals-footer.js +48 -0
- package/dist/web/primitives/data-table/use-bulk-selection.d.ts +21 -0
- package/dist/web/primitives/data-table/use-bulk-selection.js +66 -0
- package/dist/web/primitives/data-table/use-column-pins.d.ts +32 -0
- package/dist/web/primitives/data-table/use-column-pins.js +117 -0
- package/dist/web/primitives/data-table/use-column-sizing.d.ts +22 -0
- package/dist/web/primitives/data-table/use-column-sizing.js +44 -0
- package/dist/web/primitives/data-table/use-measured-column-widths.d.ts +15 -0
- package/dist/web/primitives/data-table/use-measured-column-widths.js +83 -0
- package/dist/web/primitives/data-table/use-row-keyboard-navigation.d.ts +22 -0
- package/dist/web/primitives/data-table/use-row-keyboard-navigation.js +65 -0
- package/dist/web/primitives/data-table/use-row-selection.d.ts +14 -0
- package/dist/web/primitives/data-table/use-row-selection.js +48 -0
- package/dist/web/primitives/data-table/use-view-columns.d.ts +6 -0
- package/dist/web/primitives/data-table/use-view-columns.js +17 -0
- package/dist/web/primitives/data-table/view-menu.d.ts +8 -0
- package/dist/web/primitives/data-table/view-menu.js +45 -0
- package/dist/web/primitives/data-table-cell.d.ts +0 -22
- package/dist/web/primitives/data-table-cell.js +1 -58
- package/dist/web/primitives/data-table.d.ts +1 -1
- package/dist/web/primitives/data-table.js +122 -144
- package/dist/web/primitives/file-value-list.d.ts +1 -1
- package/dist/web/primitives/file-value-list.js +5 -5
- package/dist/web/primitives/number-range-facet.d.ts +7 -0
- package/dist/web/primitives/number-range-facet.js +33 -0
- package/dist/web/widgets/bounded-load-cache.d.ts +5 -1
- package/dist/web/widgets/bounded-load-cache.js +48 -26
- package/dist/web/widgets/pdf-preview.js +9 -4
- package/package.json +5 -5
- package/src/changes.json +109 -0
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import { type EntityDefinition, type EntityListScreenDefinition
|
|
1
|
+
import { type EntityDefinition, type EntityListScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
import { type ReactNode } from "react";
|
|
3
3
|
import type { ListRowViewModel, Translate } from "../headless/index.ts";
|
|
4
4
|
import type { ListSort } from "../hooks/use-list-url-state.ts";
|
|
5
|
-
import { type DataTableDateRangeFacet, type DataTableFacet, type DataTableProps, type DataTableRowAction, type DataTableRowActionMode, type
|
|
5
|
+
import { type DataTableDateRangeFacet, type DataTableFacet, type DataTableProps, type DataTableRowAction, type DataTableRowActionMode, type DataTableTableBlockProps } from "../primitives.tsx";
|
|
6
6
|
import { type ListEmptyStateContent } from "./list-empty-state.tsx";
|
|
7
|
+
import { type ToolbarActionButton } from "./render-list-toolbar-action.tsx";
|
|
8
|
+
export type { ToolbarActionButton };
|
|
7
9
|
export type RenderListProps = {
|
|
8
10
|
readonly screen: EntityListScreenDefinition;
|
|
9
11
|
readonly entity: Pick<EntityDefinition, "fields" | "derivedFields">;
|
|
@@ -67,12 +69,11 @@ export type RenderListProps = {
|
|
|
67
69
|
* EntityListScreenDefinition.rowActions: handler-QN becomes a dispatcher
|
|
68
70
|
* call, and i18n keys become translated strings. */
|
|
69
71
|
readonly rowActions?: readonly DataTableRowAction[];
|
|
70
|
-
/** How the row-action column renders (see `DataTableProps.rowActionMode`).
|
|
71
|
-
*
|
|
72
|
-
* DataTable falls back to its adaptive default. */
|
|
72
|
+
/** How the row-action column renders (see `DataTableProps.rowActionMode`). KumikoScreen
|
|
73
|
+
* derives it from the actions; without it the DataTable uses its adaptive default. */
|
|
73
74
|
readonly rowActionMode?: DataTableRowActionMode;
|
|
74
|
-
/** Forwarded to `DataTableProps.
|
|
75
|
-
readonly
|
|
75
|
+
/** Forwarded to `DataTableProps.bulk` / `rowTone`. */
|
|
76
|
+
readonly bulk?: DataTableProps["bulk"];
|
|
76
77
|
readonly rowTone?: DataTableProps["rowTone"];
|
|
77
78
|
/** Toolbar actions in the list header in resolved form. KumikoScreen
|
|
78
79
|
* builds them from EntityListScreenDefinition.toolbarActions:
|
|
@@ -93,13 +94,11 @@ export type RenderListProps = {
|
|
|
93
94
|
readonly dateRangeFacets?: readonly DataTableDateRangeFacet[];
|
|
94
95
|
/** One bound changed (set URL state + refetch). */
|
|
95
96
|
readonly onDateRangeChange?: (field: string, bound: "from" | "to", value: string) => void;
|
|
96
|
-
/** Forwarded to `DataTableProps.chromeless` — drops the table's own card
|
|
97
|
-
*
|
|
98
|
-
* section). Default false. */
|
|
97
|
+
/** Forwarded to `DataTableProps.chromeless` — drops the table's own card frame for a
|
|
98
|
+
* host that already provides one (relatedList in a tabs-mode section). Default false. */
|
|
99
99
|
readonly chromeless?: boolean;
|
|
100
|
-
/** Forwarded to `DataTableProps.scrollBody` — bounds the table height and
|
|
101
|
-
*
|
|
102
|
-
* tabs-mode section). Default false. */
|
|
100
|
+
/** Forwarded to `DataTableProps.scrollBody` — bounds the table height and scrolls rows
|
|
101
|
+
* internally instead of growing the page (relatedList in a tabs-mode section). Default false. */
|
|
103
102
|
readonly scrollBody?: boolean;
|
|
104
103
|
/** Forwarded to `DataTableProps.screenPadding` — `true`: the table carries
|
|
105
104
|
* the shared screen padding because it is the screen body (entityList/
|
|
@@ -112,20 +111,5 @@ export type RenderListProps = {
|
|
|
112
111
|
readonly expandedRowIds?: ReadonlySet<string>;
|
|
113
112
|
readonly onToggleRowExpanded?: (rowId: string) => void;
|
|
114
113
|
readonly renderExpandedRow?: (row: ListRowViewModel) => ReactNode;
|
|
115
|
-
};
|
|
116
|
-
export type ToolbarActionButton = {
|
|
117
|
-
readonly id: string;
|
|
118
|
-
readonly label: string;
|
|
119
|
-
readonly style?: "primary" | "secondary" | "danger";
|
|
120
|
-
readonly confirm?: string;
|
|
121
|
-
readonly confirmLabel?: string;
|
|
122
|
-
/** Overrides the default "danger implies a confirm dialog" rule. Schema-driven
|
|
123
|
-
* navigate/drawer actions set it to false: the colour marks the action as
|
|
124
|
-
* destructive, but the target form is the confirmation. */
|
|
125
|
-
readonly confirmRequired?: boolean;
|
|
126
|
-
readonly onTrigger: () => Promise<void> | void;
|
|
127
|
-
/** Id-derived default icon (ACTION_ICON_BY_ID in kumiko-screen.tsx) —
|
|
128
|
-
* ToolbarAction has no author-declared icon field, unlike RowAction. */
|
|
129
|
-
readonly icon?: IconKey;
|
|
130
|
-
};
|
|
114
|
+
} & DataTableTableBlockProps;
|
|
131
115
|
export declare function RenderList(props: RenderListProps): ReactNode;
|
|
@@ -11,6 +11,7 @@ import { usePageHeaderCompact, usePageHeaderSlotAvailable } from "../page-header
|
|
|
11
11
|
import { shouldRenderActionsIconOnly, usePrimitives, } from "../primitives.js";
|
|
12
12
|
import { ListEmptyStateBody } from "./list-empty-state.js";
|
|
13
13
|
import { needsActionConfirm } from "./render-edit-action-button.js";
|
|
14
|
+
import { ToolbarActionView } from "./render-list-toolbar-action.js";
|
|
14
15
|
const SEARCH_DEBOUNCE_MS = 300;
|
|
15
16
|
// Icon-less actions are promoted too; on phones the call site falls back to a
|
|
16
17
|
// "plus" icon, on desktop the label carries the action.
|
|
@@ -18,7 +19,7 @@ function isHeaderPromotableToolbarAction(action) {
|
|
|
18
19
|
return action.style === "primary" && !needsActionConfirm(action);
|
|
19
20
|
}
|
|
20
21
|
export function RenderList(props) {
|
|
21
|
-
const { screen, entity, rows, featureName, translate: translateProp, onRowClick, emptyState, onCreate, createLabel, searchable = false, searchPlaceholder, description, emptyContent, narrowed = false, defaultFilterActive: defaultFilterActiveProp = false, itemNounKey, searchValue, onSearchChange, sort, onSortChange, pager, onReachEnd, loadingMore, hasMore, rowActions, rowActionMode,
|
|
22
|
+
const { screen, entity, rows, featureName, translate: translateProp, onRowClick, emptyState, onCreate, createLabel, searchable = false, searchPlaceholder, description, emptyContent, narrowed = false, defaultFilterActive: defaultFilterActiveProp = false, itemNounKey, searchValue, onSearchChange, sort, onSortChange, pager, onReachEnd, loadingMore, hasMore, rowActions, rowActionMode, bulk, rowTone, toolbarActions, filterFacets, filterValues, onFilterChange, onFilterReset, dateRangeFacets, onDateRangeChange, chromeless, scrollBody, screenPadding, expandedRowIds, onToggleRowExpanded, renderExpandedRow, ...tableBlockProps } = props;
|
|
22
23
|
// As in RenderEdit, fall back to the i18next context for translation;
|
|
23
24
|
// otherwise column headers would display raw i18n keys.
|
|
24
25
|
const t = useTranslation();
|
|
@@ -27,6 +28,10 @@ export function RenderList(props) {
|
|
|
27
28
|
const pageHeaderSlotAvailable = usePageHeaderSlotAvailable();
|
|
28
29
|
const pageHeaderCompact = usePageHeaderCompact();
|
|
29
30
|
const createInPageHeader = onCreate !== undefined && PageHeader !== undefined && pageHeaderSlotAvailable;
|
|
31
|
+
// Promoted actions that moved into the phone header's overflow menu have no button of
|
|
32
|
+
// their own, so their busy and error state lives here.
|
|
33
|
+
const [menuActionBusyId, setMenuActionBusyId] = useState(null);
|
|
34
|
+
const [menuActionError, setMenuActionError] = useState(null);
|
|
30
35
|
// Local search buffer + debounce. External changes (browser back,
|
|
31
36
|
// cross-component reset) are mirrored back; typing only fires
|
|
32
37
|
// onSearchChange after 300ms without further keystrokes.
|
|
@@ -214,16 +219,41 @@ export function RenderList(props) {
|
|
|
214
219
|
const hasHeaderSlot = screen.slots?.header !== undefined;
|
|
215
220
|
const toolbarIconOnly = hasToolbarActions && shouldRenderActionsIconOnly(toolbarActions);
|
|
216
221
|
const createButton = onCreate !== undefined && createInPageHeader && pageHeaderCompact ? (_jsx(Button, { variant: "primary", icon: "plus", size: "icon", ariaLabel: effectiveCreateLabel, title: effectiveCreateLabel, onClick: onCreate, testId: "render-list-create" })) : onCreate !== undefined ? (_jsx(Button, { variant: "primary", icon: "plus", onClick: onCreate, testId: "render-list-create", className: "px-3.5", children: effectiveCreateLabel })) : undefined;
|
|
217
|
-
//
|
|
218
|
-
//
|
|
219
|
-
//
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
const
|
|
222
|
+
// Every primary toolbar action takes the page-header slot, left of the create
|
|
223
|
+
// button (which stays the rightmost primary action): labelled on desktop, so
|
|
224
|
+
// projection lists look like entity lists. On phones an icon button, or, next to
|
|
225
|
+
// the create button, an entry in the header's overflow menu: the header has no
|
|
226
|
+
// room for a labelled action beside it.
|
|
227
|
+
const promotedToolbarActions = PageHeader !== undefined && pageHeaderSlotAvailable
|
|
228
|
+
? (toolbarActions?.filter(isHeaderPromotableToolbarAction) ?? [])
|
|
229
|
+
: [];
|
|
230
|
+
const promotedInOverflowMenu = pageHeaderCompact && createInPageHeader && promotedToolbarActions.length > 0;
|
|
231
|
+
const promotedMenuItems = promotedInOverflowMenu
|
|
232
|
+
? promotedToolbarActions.map((a) => ({
|
|
233
|
+
id: a.id,
|
|
234
|
+
label: a.label,
|
|
235
|
+
...(a.icon !== undefined && { icon: a.icon }),
|
|
236
|
+
...(menuActionBusyId !== null && { disabled: true }),
|
|
237
|
+
testId: `render-list-toolbar-action-${a.id}`,
|
|
238
|
+
onSelect: () => {
|
|
239
|
+
setMenuActionBusyId(a.id);
|
|
240
|
+
setMenuActionError(null);
|
|
241
|
+
void (async () => {
|
|
242
|
+
try {
|
|
243
|
+
await a.onTrigger();
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
setMenuActionError(e instanceof Error ? e.message : String(e));
|
|
247
|
+
}
|
|
248
|
+
finally {
|
|
249
|
+
setMenuActionBusyId(null);
|
|
250
|
+
}
|
|
251
|
+
})();
|
|
252
|
+
},
|
|
253
|
+
}))
|
|
254
|
+
: [];
|
|
255
|
+
const headerActions = promotedToolbarActions.length > 0 && !promotedInOverflowMenu ? (_jsxs(_Fragment, { children: [promotedToolbarActions.map((a) => (_jsx(ToolbarActionView, { action: pageHeaderCompact ? { ...a, icon: a.icon ?? "plus" } : a, iconOnly: pageHeaderCompact, Button: Button, Dialog: Dialog, Banner: Banner }, a.id))), createInPageHeader && createButton] })) : createInPageHeader ? (createButton) : undefined;
|
|
256
|
+
const toolbarActionsInToolbar = toolbarActions?.filter((a) => !promotedToolbarActions.includes(a)) ?? [];
|
|
227
257
|
const hasToolbarActionsInToolbar = toolbarActionsInToolbar.length > 0;
|
|
228
258
|
const toolbarEnd = hasHeaderSlot ||
|
|
229
259
|
hasToolbarActionsInToolbar ||
|
|
@@ -242,8 +272,7 @@ export function RenderList(props) {
|
|
|
242
272
|
const titleKey = `screen:${screen.id}.title`;
|
|
243
273
|
const resolvedTitle = translate(titleKey);
|
|
244
274
|
const toolbarTitle = resolvedTitle === titleKey ? screen.id : resolvedTitle;
|
|
245
|
-
|
|
246
|
-
return (_jsxs(_Fragment, { children: [createInPageHeader && PageHeader !== undefined && _jsx(PageHeader, { actions: createButton }), promotedActionButton !== undefined && PageHeader !== undefined && (_jsx(PageHeader, { actions: promotedActionButton })), referenceColumns.map((rc) => (_jsx(ReferenceLookupBridge, { field: rc.field, refEntity: rc.refEntity, labelField: rc.labelField, featureName: rc.refFeature, onMap: handleLookupMap }, rc.field))), _jsx(DataTable, { columns: enrichedVm.columns, rows: enrichedVm.rows, toolbarTitle: toolbarTitle, ...(onRowClick !== undefined && { onRowClick }), ...(composedEmptyState !== undefined && { emptyState: composedEmptyState }), ...(toolbarStart !== undefined && { toolbarStart }), ...(description !== undefined && { toolbarDescription: description }), ...(toolbarEnd !== undefined && { toolbarEnd }), ...(sort !== undefined && { sort }), ...(onSortChange !== undefined && { onSortChange }), ...(pager !== undefined && { pager }), ...(onReachEnd !== undefined && { onReachEnd }), ...(loadingMore !== undefined && { loadingMore }), ...(hasMore !== undefined && { hasMore }), ...(itemNoun !== undefined && { itemNoun }), ...(rowActions !== undefined && { rowActions }), ...(rowActionMode !== undefined && { rowActionMode }), ...(rowGrouping !== undefined && { rowGrouping }), ...(rowTone !== undefined && { rowTone }), ...(filterFacets !== undefined && { filterFacets }), ...(filterValues !== undefined && { filterValues }), ...(onFilterChange !== undefined && { onFilterChange }), ...(onFilterReset !== undefined && { onFilterReset }), ...(dateRangeFacets !== undefined && { dateRangeFacets }), ...(onDateRangeChange !== undefined && { onDateRangeChange }), ...(chromeless !== undefined && { chromeless }), ...(scrollBody !== undefined && { scrollBody }), ...(screenPadding !== undefined && { screenPadding }), ...(expandedRowIds !== undefined && { expandedRowIds }), ...(onToggleRowExpanded !== undefined && { onToggleRowExpanded }), ...(renderExpandedRow !== undefined && { renderExpandedRow }), testId: "render-list-table" })] }));
|
|
275
|
+
return (_jsxs(_Fragment, { children: [headerActions !== undefined && PageHeader !== undefined && (_jsx(PageHeader, { actions: headerActions, ...(promotedInOverflowMenu && { overflowItems: promotedMenuItems }) })), menuActionError !== null && (_jsx(Banner, { variant: "error", testId: "render-list-toolbar-action-error", children: menuActionError })), referenceColumns.map((rc) => (_jsx(ReferenceLookupBridge, { field: rc.field, refEntity: rc.refEntity, labelField: rc.labelField, featureName: rc.refFeature, onMap: handleLookupMap }, rc.field))), _jsx(DataTable, { columns: enrichedVm.columns, rows: enrichedVm.rows, toolbarTitle: toolbarTitle, ...(onRowClick !== undefined && { onRowClick }), ...(composedEmptyState !== undefined && { emptyState: composedEmptyState }), ...(toolbarStart !== undefined && { toolbarStart }), ...(description !== undefined && { toolbarDescription: description }), ...(toolbarEnd !== undefined && { toolbarEnd }), ...(sort !== undefined && { sort }), ...(onSortChange !== undefined && { onSortChange }), ...(pager !== undefined && { pager }), ...(onReachEnd !== undefined && { onReachEnd }), ...(loadingMore !== undefined && { loadingMore }), ...(hasMore !== undefined && { hasMore }), ...(itemNoun !== undefined && { itemNoun }), ...(rowActions !== undefined && { rowActions }), ...(rowActionMode !== undefined && { rowActionMode }), ...(bulk !== undefined && { bulk }), ...(rowTone !== undefined && { rowTone }), ...(filterFacets !== undefined && { filterFacets }), ...(filterValues !== undefined && { filterValues }), ...(onFilterChange !== undefined && { onFilterChange }), ...(onFilterReset !== undefined && { onFilterReset }), ...(dateRangeFacets !== undefined && { dateRangeFacets }), ...(onDateRangeChange !== undefined && { onDateRangeChange }), ...(chromeless !== undefined && { chromeless }), ...(scrollBody !== undefined && { scrollBody }), ...(screenPadding !== undefined && { screenPadding }), ...(expandedRowIds !== undefined && { expandedRowIds }), ...(onToggleRowExpanded !== undefined && { onToggleRowExpanded }), ...(renderExpandedRow !== undefined && { renderExpandedRow }), ...tableBlockProps, testId: "render-list-table" })] }));
|
|
247
276
|
}
|
|
248
277
|
// Header slot in the list toolbar (toolbarEnd): `screen.slots.header` is
|
|
249
278
|
// a PlatformComponent (`{ react: { __component: "X" } }`) resolved through
|
|
@@ -290,43 +319,6 @@ function ReferenceLookupBridge({ field, refEntity, labelField, featureName, onMa
|
|
|
290
319
|
}, [field, lookup.map, lookup.complete, resolved, onMap]);
|
|
291
320
|
return null;
|
|
292
321
|
}
|
|
293
|
-
// ToolbarActionView renders one button per toolbar action, with a confirm
|
|
294
|
-
// dialog when required. It follows the inline RowAction pattern but has no
|
|
295
|
-
// row context. The busy state covers async onTrigger; the dialog opens before
|
|
296
|
-
// the trigger when confirmation is required for confirm/danger actions.
|
|
297
|
-
function ToolbarActionView({ action, iconOnly = false, Button, Dialog, Banner, }) {
|
|
298
|
-
const [busy, setBusy] = useState(false);
|
|
299
|
-
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
300
|
-
// Toolbar actions do not have access to the toast primitive, unlike
|
|
301
|
-
// rowActions routed through the DataTable primitive, so an inline error
|
|
302
|
-
// banner is used to surface failures. Without it, a swallowed failure result
|
|
303
|
-
// would look to the user as though nothing happened.
|
|
304
|
-
const [errorText, setErrorText] = useState(null);
|
|
305
|
-
const trigger = async () => {
|
|
306
|
-
setBusy(true);
|
|
307
|
-
setErrorText(null);
|
|
308
|
-
try {
|
|
309
|
-
await action.onTrigger();
|
|
310
|
-
}
|
|
311
|
-
catch (e) {
|
|
312
|
-
setErrorText(e instanceof Error ? e.message : String(e));
|
|
313
|
-
}
|
|
314
|
-
finally {
|
|
315
|
-
setBusy(false);
|
|
316
|
-
}
|
|
317
|
-
};
|
|
318
|
-
const variant = action.style ?? "secondary";
|
|
319
|
-
const needsConfirm = needsActionConfirm(action);
|
|
320
|
-
const showIconOnly = iconOnly && action.icon !== undefined;
|
|
321
|
-
return (_jsxs(_Fragment, { children: [_jsx(Button, { variant: variant, loading: busy, ...(action.icon !== undefined && { icon: action.icon }), size: showIconOnly ? "icon" : "md", ...(showIconOnly && { ariaLabel: action.label, title: action.label }), onClick: () => {
|
|
322
|
-
if (needsConfirm) {
|
|
323
|
-
setConfirmOpen(true);
|
|
324
|
-
}
|
|
325
|
-
else {
|
|
326
|
-
void trigger();
|
|
327
|
-
}
|
|
328
|
-
}, testId: `render-list-toolbar-action-${action.id}`, children: showIconOnly ? null : action.label }), _jsx(Dialog, { open: confirmOpen, onOpenChange: setConfirmOpen, title: action.label, ...(action.confirm !== undefined && { description: action.confirm }), confirmLabel: action.confirmLabel ?? action.label, ...(action.style === "danger" && { variant: "danger" }), onConfirm: trigger, testId: `render-list-toolbar-action-${action.id}-dialog` }), errorText !== null && (_jsx(Banner, { variant: "error", testId: `render-list-toolbar-action-${action.id}-error`, children: errorText }))] }));
|
|
329
|
-
}
|
|
330
322
|
function isPlainRecord(value) {
|
|
331
323
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
332
324
|
}
|
package/dist/current-user.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export type CurrentUser = {
|
|
|
7
7
|
readonly displayName: string;
|
|
8
8
|
/** Effective IANA zone of the user (override, else tenant). */
|
|
9
9
|
readonly timeZone?: string;
|
|
10
|
+
/** Active tenant; scopes per-user browser state such as remembered list views. */
|
|
11
|
+
readonly tenantId?: string;
|
|
10
12
|
};
|
|
11
13
|
export declare function CurrentUserProvider({ user, children, }: {
|
|
12
14
|
readonly user: CurrentUser | undefined;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { AggregateKey } from "@cosmicdrift/kumiko-types/aggregate-types";
|
|
2
|
+
import type { ListAggregateMeasureKind, ListAggregateValue } from "@cosmicdrift/kumiko-types/list-aggregate-types";
|
|
3
|
+
/** One server-computed measure; a money measure holds one minor-unit value per currency. */
|
|
4
|
+
export type DataTableMeasure = {
|
|
5
|
+
readonly field: string;
|
|
6
|
+
readonly kind: ListAggregateMeasureKind;
|
|
7
|
+
readonly value: ListAggregateValue;
|
|
8
|
+
};
|
|
9
|
+
export type DataTableServerGroup = {
|
|
10
|
+
/** `relatedListGroupKey` of the group value; stable across reloads. */
|
|
11
|
+
readonly key: string;
|
|
12
|
+
/** Raw group value, rendered through the group column when `label` is absent. */
|
|
13
|
+
readonly value: AggregateKey;
|
|
14
|
+
readonly label?: string;
|
|
15
|
+
/** Rows of the whole group on the server, not the loaded ones. */
|
|
16
|
+
readonly count: number;
|
|
17
|
+
readonly measures: readonly DataTableMeasure[];
|
|
18
|
+
readonly collapsed: boolean;
|
|
19
|
+
/** How many of the table's `rows` belong to this group; rows arrive in group order. */
|
|
20
|
+
readonly rowCount: number;
|
|
21
|
+
readonly hasMore: boolean;
|
|
22
|
+
readonly loading: boolean;
|
|
23
|
+
};
|
|
24
|
+
/** Server-side grouping: groups are the load unit, each loads its own rows. */
|
|
25
|
+
export type DataTableServerGroups = {
|
|
26
|
+
readonly field: string;
|
|
27
|
+
readonly groups: readonly DataTableServerGroup[];
|
|
28
|
+
readonly totalCount: number;
|
|
29
|
+
readonly onToggle: (key: string) => void;
|
|
30
|
+
readonly onLoadMore: (key: string) => void;
|
|
31
|
+
readonly onHeaderVisible: (key: string) => void;
|
|
32
|
+
};
|
|
33
|
+
export type DataTableTotals = {
|
|
34
|
+
readonly measures: readonly DataTableMeasure[];
|
|
35
|
+
};
|
|
36
|
+
/** `truncated`: more groups than the server answers. `searchTruncated`: the search hit its cap. */
|
|
37
|
+
export type DataTableListNotice = "truncated" | "searchTruncated";
|
|
38
|
+
export type DataTableRangeBound = "min" | "max";
|
|
39
|
+
/** "From/to" inputs on a number field. Values are the typed strings, "" = open bound. */
|
|
40
|
+
export type DataTableRangeFacet = {
|
|
41
|
+
readonly field: string;
|
|
42
|
+
readonly label: string;
|
|
43
|
+
readonly min: string;
|
|
44
|
+
readonly max: string;
|
|
45
|
+
/** Shown after the inputs, e.g. the money field's currency. */
|
|
46
|
+
readonly unit?: string;
|
|
47
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { IconKey } from "@cosmicdrift/kumiko-types/nav-icon";
|
|
2
|
+
import type { ListRowViewModel } from "./headless/index.ts";
|
|
3
|
+
export type BulkRowOutcome = {
|
|
4
|
+
readonly kind: "succeeded";
|
|
5
|
+
} | {
|
|
6
|
+
readonly kind: "failed";
|
|
7
|
+
readonly message: string;
|
|
8
|
+
};
|
|
9
|
+
export type DataTableBulkAction = {
|
|
10
|
+
readonly id: string;
|
|
11
|
+
/** Translated label of the underlying row action. */
|
|
12
|
+
readonly label: string;
|
|
13
|
+
readonly style?: "primary" | "secondary" | "danger";
|
|
14
|
+
readonly icon?: IconKey;
|
|
15
|
+
/** Present: one confirm dialog for the whole selection, with this text for `count` rows. */
|
|
16
|
+
readonly confirmText?: (count: number) => string;
|
|
17
|
+
readonly confirmLabel?: string;
|
|
18
|
+
/** False: the row action's `visible` hides it for this row; the run skips the row. */
|
|
19
|
+
readonly appliesTo: (row: ListRowViewModel) => boolean;
|
|
20
|
+
/** Exactly one server write for one row. Never throws for a rejected write. */
|
|
21
|
+
readonly writeRow: (row: ListRowViewModel) => Promise<BulkRowOutcome>;
|
|
22
|
+
};
|
|
23
|
+
export type DataTableBulk = {
|
|
24
|
+
readonly actions: readonly DataTableBulkAction[];
|
|
25
|
+
/** Reloads the list once after a run. */
|
|
26
|
+
readonly reload: () => Promise<void>;
|
|
27
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type DataTableFacet = {
|
|
2
|
+
/** Entity-Feldname (= URL-State-Key + payload.filters[].field). */
|
|
3
|
+
readonly field: string;
|
|
4
|
+
/** Translated Button-Label ("Status", "Active"). */
|
|
5
|
+
readonly label: string;
|
|
6
|
+
/** Selectable values with display labels. `value` is the raw filter value
|
|
7
|
+
* (a select option or `"true"`/`"false"`), transported as a string. */
|
|
8
|
+
readonly options: readonly {
|
|
9
|
+
readonly value: string;
|
|
10
|
+
readonly label: string;
|
|
11
|
+
}[];
|
|
12
|
+
/** Present: render the facet as a row of single-choice toggle chips instead
|
|
13
|
+
* of a dropdown. A chip is pressed while the selection equals its values. */
|
|
14
|
+
readonly chips?: readonly DataTableFacetChip[];
|
|
15
|
+
/** Chips only: show each chip's `count` next to its label. */
|
|
16
|
+
readonly showCounts?: boolean;
|
|
17
|
+
/** Chips only: hide chips whose `count` is 0 (unless pressed). */
|
|
18
|
+
readonly hideEmpty?: boolean;
|
|
19
|
+
};
|
|
20
|
+
export type DataTableFacetChip = {
|
|
21
|
+
readonly id: string;
|
|
22
|
+
/** Translated chip label. */
|
|
23
|
+
readonly label: string;
|
|
24
|
+
/** Selection a click sets; `[]` clears the facet. */
|
|
25
|
+
readonly values: readonly string[];
|
|
26
|
+
/** Matching row count, undefined while unknown. */
|
|
27
|
+
readonly count?: number;
|
|
28
|
+
};
|
|
29
|
+
export type DataTableDateRangeFacet = {
|
|
30
|
+
/** The facet's namespace (its date field) — a key, not a filter value. */
|
|
31
|
+
readonly field: string;
|
|
32
|
+
/** Translated label ("When"). */
|
|
33
|
+
readonly label: string;
|
|
34
|
+
readonly from: string;
|
|
35
|
+
readonly to: string;
|
|
36
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import type { ListColumnPin, ListTableDensity } from "@cosmicdrift/kumiko-types/screen";
|
|
2
|
+
import type { DataTableListNotice, DataTableRangeBound, DataTableRangeFacet, DataTableServerGroups, DataTableTotals } from "./data-table-aggregate-types.ts";
|
|
3
|
+
export type DataTableDensity = ListTableDensity;
|
|
4
|
+
export type DataTableColumnPin = ListColumnPin;
|
|
5
|
+
export type DataTableColumnPins = Readonly<Record<string, DataTableColumnPin>>;
|
|
6
|
+
export type DataTableColumnWidths = Readonly<Record<string, number>>;
|
|
7
|
+
/** One column in the view menu. The label comes from the DataTable's own
|
|
8
|
+
* column of the same `field`. */
|
|
9
|
+
export type DataTableViewColumn = {
|
|
10
|
+
readonly field: string;
|
|
11
|
+
readonly visible: boolean;
|
|
12
|
+
};
|
|
13
|
+
export type DataTableViewOptions = {
|
|
14
|
+
/** Present: the view menu has a column section (visibility + order). The
|
|
15
|
+
* order lists every column; hidden ones stay in the list. */
|
|
16
|
+
readonly columns?: readonly DataTableViewColumn[];
|
|
17
|
+
readonly onColumnsChange?: (next: readonly DataTableViewColumn[]) => void;
|
|
18
|
+
readonly density: DataTableDensity;
|
|
19
|
+
/** Present: the view menu has a density section. */
|
|
20
|
+
readonly onDensityChange?: (next: DataTableDensity) => void;
|
|
21
|
+
/** Present: pinning is on and the view menu has a pin section. Unpinned columns have no entry. */
|
|
22
|
+
readonly pins?: DataTableColumnPins;
|
|
23
|
+
readonly onPinsChange?: (next: DataTableColumnPins) => void;
|
|
24
|
+
/** Present: resizing is on; the table switches to table-layout fixed. Every column has an entry (px). */
|
|
25
|
+
readonly widths?: DataTableColumnWidths;
|
|
26
|
+
/** Present together with `widths`; every column has an entry (px). */
|
|
27
|
+
readonly minWidths?: DataTableColumnWidths;
|
|
28
|
+
readonly onWidthChange?: (field: string, width: number) => void;
|
|
29
|
+
readonly keyboardNavigation?: boolean;
|
|
30
|
+
/** Present: the view menu has a "group by" section. */
|
|
31
|
+
readonly groupBy?: DataTableGroupByChoice;
|
|
32
|
+
/** False once columns, order or density differ from the screen default. */
|
|
33
|
+
readonly viewIsDefault: boolean;
|
|
34
|
+
readonly onViewReset: () => void;
|
|
35
|
+
};
|
|
36
|
+
export type DataTableGroupByChoice = {
|
|
37
|
+
/** Active group field, "" = not grouped. */
|
|
38
|
+
readonly value: string;
|
|
39
|
+
readonly fields: readonly string[];
|
|
40
|
+
readonly onChange: (next: string) => void;
|
|
41
|
+
};
|
|
42
|
+
export type DataTableResetAll = {
|
|
43
|
+
/** True while something deviates from the screen default. */
|
|
44
|
+
readonly active: boolean;
|
|
45
|
+
readonly onReset: () => void;
|
|
46
|
+
};
|
|
47
|
+
/** What the export button sends to POST /api/export/xlsx (xlsx is the only format). */
|
|
48
|
+
export type DataTableExport = {
|
|
49
|
+
/** Screen QN `<feature>:screen:<id>`. */
|
|
50
|
+
readonly screen: string;
|
|
51
|
+
readonly screenShortId: string;
|
|
52
|
+
/** The list payload without paging keys; the server reads the whole filtered list. */
|
|
53
|
+
readonly payload: Readonly<Record<string, unknown>>;
|
|
54
|
+
/** Visible column ids in display order. */
|
|
55
|
+
readonly columns: readonly string[];
|
|
56
|
+
};
|
|
57
|
+
/** The DataTable props a screen's `table` block feeds. */
|
|
58
|
+
export type DataTableTableBlock = {
|
|
59
|
+
/** View menu (column picker, density); absent = no menu. */
|
|
60
|
+
readonly view?: DataTableViewOptions;
|
|
61
|
+
/** Replaces the facet reset with one that also covers sort, search and page size. */
|
|
62
|
+
readonly resetAll?: DataTableResetAll;
|
|
63
|
+
/** Present: the toolbar has an export button (web only). */
|
|
64
|
+
readonly exportList?: DataTableExport;
|
|
65
|
+
/** Present: the rows are grouped on the server; replaces pager and infinite scroll. */
|
|
66
|
+
readonly serverGroups?: DataTableServerGroups;
|
|
67
|
+
/** Present: a footer row with the server-computed totals of the filtered list. */
|
|
68
|
+
readonly totals?: DataTableTotals;
|
|
69
|
+
/** Present: the toolbar says the answer is incomplete and asks to narrow the list. */
|
|
70
|
+
readonly notice?: DataTableListNotice;
|
|
71
|
+
readonly rangeFacets?: readonly DataTableRangeFacet[];
|
|
72
|
+
readonly onRangeChange?: (field: string, bound: DataTableRangeBound, value: string) => void;
|
|
73
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -56,6 +56,7 @@ function wasEmptiedByWriteExclusion(declaredFieldSpecs, remainingFields) {
|
|
|
56
56
|
export function computeRelatedListSectionViewModel(sectionSpec, translate) {
|
|
57
57
|
return {
|
|
58
58
|
kind: "relatedList",
|
|
59
|
+
...(sectionSpec.id !== undefined && { id: sectionSpec.id }),
|
|
59
60
|
title: translate(sectionSpec.title),
|
|
60
61
|
query: sectionSpec.query,
|
|
61
62
|
...(sectionSpec.parentParam !== undefined && { parentParam: sectionSpec.parentParam }),
|
|
@@ -67,13 +68,14 @@ export function computeRelatedListSectionViewModel(sectionSpec, translate) {
|
|
|
67
68
|
}),
|
|
68
69
|
...(sectionSpec.itemNoun !== undefined && { itemNoun: sectionSpec.itemNoun }),
|
|
69
70
|
...(sectionSpec.pageSize !== undefined && { pageSize: sectionSpec.pageSize }),
|
|
71
|
+
...(sectionSpec.table !== undefined && { table: sectionSpec.table }),
|
|
70
72
|
...(sectionSpec.defaultSort !== undefined && { defaultSort: sectionSpec.defaultSort }),
|
|
71
73
|
...(sectionSpec.searchable !== undefined && { searchable: sectionSpec.searchable }),
|
|
72
74
|
...(sectionSpec.facets !== undefined && { facets: sectionSpec.facets }),
|
|
73
75
|
...(sectionSpec.rowClick !== undefined && { rowClick: sectionSpec.rowClick }),
|
|
74
76
|
...(sectionSpec.rowActions !== undefined && { rowActions: sectionSpec.rowActions }),
|
|
75
77
|
...(sectionSpec.rowActionMode !== undefined && { rowActionMode: sectionSpec.rowActionMode }),
|
|
76
|
-
...(sectionSpec.
|
|
78
|
+
...(sectionSpec.bulkActions !== undefined && { bulkActions: sectionSpec.bulkActions }),
|
|
77
79
|
...(sectionSpec.rowTone !== undefined && { rowTone: sectionSpec.rowTone }),
|
|
78
80
|
...(sectionSpec.toolbarActions !== undefined && { toolbarActions: sectionSpec.toolbarActions }),
|
|
79
81
|
...(sectionSpec.actions !== undefined && { actions: sectionSpec.actions }),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { EditFieldSpec, EditSectionSpec, FieldAffix, FieldFillAction, FieldIconKey, FieldRenderer, IconKey, ListColumnSpec, ListFacetSpec, ListSortSpec, OptionsQueryPayload, PlatformComponent,
|
|
1
|
+
import type { EditFieldSpec, EditSectionSpec, FieldAffix, FieldFillAction, FieldIconKey, FieldRenderer, IconKey, ListBulkAction, ListColumnSpec, ListFacetSpec, ListSortSpec, ListTableOptions, OptionsQueryPayload, PlatformComponent, RelatedListRowTone, RelatedListToolbarAction, RowAction, RowActionMode, ScreenSlots, SelectOptionTone } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
export type RuntimeRenderer = (value: unknown, row?: Readonly<Record<string, unknown>>) => string;
|
|
3
3
|
/** Where a cell's text links to. `href` comes from NavApi.hrefFor (new-tab and
|
|
4
4
|
* copy-link work); `onPress` is the in-app navigation for a plain click. */
|
|
@@ -333,6 +333,7 @@ export type EditExtensionSectionViewModel = {
|
|
|
333
333
|
};
|
|
334
334
|
export type EditRelatedListSectionViewModel = {
|
|
335
335
|
readonly kind: "relatedList";
|
|
336
|
+
readonly id?: string;
|
|
336
337
|
readonly title: string;
|
|
337
338
|
readonly query: string;
|
|
338
339
|
readonly parentParam?: string;
|
|
@@ -345,6 +346,7 @@ export type EditRelatedListSectionViewModel = {
|
|
|
345
346
|
/** Untranslated plural-forms key, resolved with the row count by the list footer. */
|
|
346
347
|
readonly itemNoun?: string;
|
|
347
348
|
readonly pageSize?: number;
|
|
349
|
+
readonly table?: ListTableOptions;
|
|
348
350
|
readonly defaultSort?: ListSortSpec;
|
|
349
351
|
readonly searchable?: boolean;
|
|
350
352
|
readonly facets?: readonly ListFacetSpec[];
|
|
@@ -354,7 +356,7 @@ export type EditRelatedListSectionViewModel = {
|
|
|
354
356
|
};
|
|
355
357
|
readonly rowActions?: readonly RowAction[];
|
|
356
358
|
readonly rowActionMode?: RowActionMode;
|
|
357
|
-
readonly
|
|
359
|
+
readonly bulkActions?: readonly ListBulkAction[];
|
|
358
360
|
readonly rowTone?: RelatedListRowTone;
|
|
359
361
|
readonly toolbarActions?: readonly RelatedListToolbarAction[];
|
|
360
362
|
/** From `EditRelatedListSection.actions` — rendered in the
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { DataTableColumnPin, DataTableColumnPins, DataTableColumnWidths } from "../data-table-view-types.ts";
|
|
2
|
+
export declare const DEFAULT_COLUMN_WIDTH_PX = 160;
|
|
3
|
+
export declare const DEFAULT_MIN_COLUMN_WIDTH_PX = 64;
|
|
4
|
+
/** Upper bound so a tampered stored width cannot blow the table up. */
|
|
5
|
+
export declare const MAX_COLUMN_WIDTH_PX = 2000;
|
|
6
|
+
type LayoutSchemaColumn = {
|
|
7
|
+
readonly field: string;
|
|
8
|
+
readonly pinned?: DataTableColumnPin;
|
|
9
|
+
readonly width?: number;
|
|
10
|
+
readonly minWidth?: number;
|
|
11
|
+
};
|
|
12
|
+
export declare function defaultPins(schema: readonly LayoutSchemaColumn[]): DataTableColumnPins;
|
|
13
|
+
export declare function minWidthsOf(schema: readonly LayoutSchemaColumn[]): DataTableColumnWidths;
|
|
14
|
+
export declare function clampWidth(width: number, min: number): number;
|
|
15
|
+
export declare function defaultWidths(schema: readonly LayoutSchemaColumn[], minWidths: DataTableColumnWidths): DataTableColumnWidths;
|
|
16
|
+
/** A saved width wins per field; a column the saved view has never seen keeps its default. */
|
|
17
|
+
export declare function mergeSavedWidths(defaults: DataTableColumnWidths, saved: Readonly<Record<string, number>>, minWidths: DataTableColumnWidths): DataTableColumnWidths;
|
|
18
|
+
export declare function sameRecord<T>(a: Readonly<Record<string, T>>, b: Readonly<Record<string, T>>): boolean;
|
|
19
|
+
export {};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export const DEFAULT_COLUMN_WIDTH_PX = 160;
|
|
2
|
+
export const DEFAULT_MIN_COLUMN_WIDTH_PX = 64;
|
|
3
|
+
/** Upper bound so a tampered stored width cannot blow the table up. */
|
|
4
|
+
export const MAX_COLUMN_WIDTH_PX = 2000;
|
|
5
|
+
export function defaultPins(schema) {
|
|
6
|
+
const pins = {};
|
|
7
|
+
for (const column of schema) {
|
|
8
|
+
if (column.pinned !== undefined)
|
|
9
|
+
pins[column.field] = column.pinned;
|
|
10
|
+
}
|
|
11
|
+
return pins;
|
|
12
|
+
}
|
|
13
|
+
export function minWidthsOf(schema) {
|
|
14
|
+
return Object.fromEntries(schema.map((column) => [column.field, column.minWidth ?? DEFAULT_MIN_COLUMN_WIDTH_PX]));
|
|
15
|
+
}
|
|
16
|
+
export function clampWidth(width, min) {
|
|
17
|
+
return Math.min(MAX_COLUMN_WIDTH_PX, Math.max(min, Math.round(width)));
|
|
18
|
+
}
|
|
19
|
+
export function defaultWidths(schema, minWidths) {
|
|
20
|
+
return Object.fromEntries(schema.map((column) => [
|
|
21
|
+
column.field,
|
|
22
|
+
clampWidth(column.width ?? DEFAULT_COLUMN_WIDTH_PX, minWidths[column.field] ?? DEFAULT_MIN_COLUMN_WIDTH_PX),
|
|
23
|
+
]));
|
|
24
|
+
}
|
|
25
|
+
/** A saved width wins per field; a column the saved view has never seen keeps its default. */
|
|
26
|
+
export function mergeSavedWidths(defaults, saved, minWidths) {
|
|
27
|
+
return Object.fromEntries(Object.entries(defaults).map(([field, fallback]) => {
|
|
28
|
+
const savedWidth = saved[field];
|
|
29
|
+
const min = minWidths[field] ?? DEFAULT_MIN_COLUMN_WIDTH_PX;
|
|
30
|
+
return [field, savedWidth === undefined ? fallback : clampWidth(savedWidth, min)];
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
export function sameRecord(a, b) {
|
|
34
|
+
const keys = Object.keys(a);
|
|
35
|
+
return keys.length === Object.keys(b).length && keys.every((key) => a[key] === b[key]);
|
|
36
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { AggregateKey } from "@cosmicdrift/kumiko-types/aggregate-types";
|
|
2
|
+
/** How a group pages through its rows: offset for `pagination: "pages"`, cursor for `"infinite"`. */
|
|
3
|
+
export type ListGroupPaging = "offset" | "cursor";
|
|
4
|
+
export type ListGroupPageResult = {
|
|
5
|
+
readonly rows: readonly Readonly<Record<string, unknown>>[];
|
|
6
|
+
readonly nextCursor: string | null;
|
|
7
|
+
readonly loading: boolean;
|
|
8
|
+
};
|
|
9
|
+
export type ListGroupPageRequest = {
|
|
10
|
+
readonly pageKey: string;
|
|
11
|
+
readonly payload: Readonly<Record<string, unknown>>;
|
|
12
|
+
};
|
|
13
|
+
/** The list payload as the aggregate sees it: filters, search and screen filter, no paging. */
|
|
14
|
+
export declare function withoutListPaging(payload: Readonly<Record<string, unknown>>): Record<string, unknown>;
|
|
15
|
+
export declare function groupPageKey(groupKey: string, pageIndex: number): string;
|
|
16
|
+
export declare function groupPagePayload(args: {
|
|
17
|
+
readonly base: Readonly<Record<string, unknown>>;
|
|
18
|
+
readonly groupField: string;
|
|
19
|
+
readonly groupValue: AggregateKey;
|
|
20
|
+
readonly pageSize: number;
|
|
21
|
+
readonly pageIndex: number;
|
|
22
|
+
readonly paging: ListGroupPaging;
|
|
23
|
+
readonly cursor: string | undefined;
|
|
24
|
+
}): Record<string, unknown>;
|
|
25
|
+
/** Loaded pages of one group in order, up to the first page that has not answered yet. */
|
|
26
|
+
export declare function loadedGroupPages(results: ReadonlyMap<string, ListGroupPageResult>, groupKey: string, requestedPages: number): ListGroupPageResult[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
const PAGING_KEYS = ["limit", "offset", "cursor", "totalCount"];
|
|
2
|
+
/** The list payload as the aggregate sees it: filters, search and screen filter, no paging. */
|
|
3
|
+
export function withoutListPaging(payload) {
|
|
4
|
+
const kept = { ...payload };
|
|
5
|
+
for (const key of PAGING_KEYS)
|
|
6
|
+
delete kept[key];
|
|
7
|
+
return kept;
|
|
8
|
+
}
|
|
9
|
+
export function groupPageKey(groupKey, pageIndex) {
|
|
10
|
+
return `${groupKey}#${pageIndex}`;
|
|
11
|
+
}
|
|
12
|
+
function existingFilters(base) {
|
|
13
|
+
const filters = base["filters"];
|
|
14
|
+
return Array.isArray(filters) ? filters : [];
|
|
15
|
+
}
|
|
16
|
+
export function groupPagePayload(args) {
|
|
17
|
+
const { base, groupField, groupValue, pageSize, pageIndex, paging, cursor } = args;
|
|
18
|
+
const offset = pageIndex * pageSize;
|
|
19
|
+
return {
|
|
20
|
+
...base,
|
|
21
|
+
filters: [...existingFilters(base), { field: groupField, op: "eq", value: groupValue }],
|
|
22
|
+
limit: pageSize,
|
|
23
|
+
...(paging === "offset" && offset > 0 && { offset }),
|
|
24
|
+
...(paging === "cursor" && cursor !== undefined && { cursor }),
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
/** Loaded pages of one group in order, up to the first page that has not answered yet. */
|
|
28
|
+
export function loadedGroupPages(results, groupKey, requestedPages) {
|
|
29
|
+
const pages = [];
|
|
30
|
+
for (let pageIndex = 0; pageIndex < requestedPages; pageIndex++) {
|
|
31
|
+
const page = results.get(groupPageKey(groupKey, pageIndex));
|
|
32
|
+
if (page === undefined)
|
|
33
|
+
break;
|
|
34
|
+
pages.push(page);
|
|
35
|
+
}
|
|
36
|
+
return pages;
|
|
37
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
type UrlParams = Readonly<Record<string, string | undefined>>;
|
|
2
|
+
/** Exact `${screenId}.` prefix: `orders` must not match `orders-archive.q`. */
|
|
3
|
+
export declare function listParamKeys(params: UrlParams, screenId: string): string[];
|
|
4
|
+
/** The page alone is navigation, not a deviation from the default view. */
|
|
5
|
+
export declare function isListStateAtDefault(params: UrlParams, screenId: string): boolean;
|
|
6
|
+
/** Nulling every list key lets defaultSort/defaultFilters apply again, and drops the `~` "no filter" markers. */
|
|
7
|
+
export declare function listResetUpdates(params: UrlParams, screenId: string): Record<string, null>;
|
|
8
|
+
export {};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
const PAGE_SUFFIX = "page";
|
|
2
|
+
/** Exact `${screenId}.` prefix: `orders` must not match `orders-archive.q`. */
|
|
3
|
+
export function listParamKeys(params, screenId) {
|
|
4
|
+
const prefix = `${screenId}.`;
|
|
5
|
+
return Object.keys(params).filter((key) => key.startsWith(prefix));
|
|
6
|
+
}
|
|
7
|
+
/** The page alone is navigation, not a deviation from the default view. */
|
|
8
|
+
export function isListStateAtDefault(params, screenId) {
|
|
9
|
+
const pageKey = `${screenId}.${PAGE_SUFFIX}`;
|
|
10
|
+
return listParamKeys(params, screenId).every((key) => key === pageKey);
|
|
11
|
+
}
|
|
12
|
+
/** Nulling every list key lets defaultSort/defaultFilters apply again, and drops the `~` "no filter" markers. */
|
|
13
|
+
export function listResetUpdates(params, screenId) {
|
|
14
|
+
return Object.fromEntries(listParamKeys(params, screenId).map((key) => [key, null]));
|
|
15
|
+
}
|