@cosmicdrift/kumiko-renderer 0.385.0 → 0.387.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/LICENSE +21 -57
- package/dist/app/action-form-body.d.ts +2 -2
- package/dist/app/action-form-body.js +2 -2
- package/dist/app/action-form-shim.d.ts +8 -8
- package/dist/app/action-form-shim.js +20 -22
- package/dist/app/app-features-context.js +4 -4
- package/dist/app/column-renderers.d.ts +7 -9
- package/dist/app/column-renderers.js +15 -18
- package/dist/app/config-edit-body.js +1 -1
- package/dist/app/config-edit-shim.d.ts +7 -8
- package/dist/app/config-edit-shim.js +18 -20
- package/dist/app/custom-screens.d.ts +3 -3
- package/dist/app/custom-screens.js +8 -8
- package/dist/app/dashboard-body.d.ts +2 -2
- package/dist/app/dashboard-body.js +2 -2
- package/dist/app/entity-edit-prefill.js +9 -9
- package/dist/app/entity-edit-screen.js +4 -4
- package/dist/app/entity-list-screen.js +3 -3
- package/dist/app/extension-form-submit.d.ts +1 -1
- package/dist/app/extension-form-submit.js +14 -15
- package/dist/app/extension-sections.d.ts +24 -25
- package/dist/app/extension-sections.js +10 -10
- package/dist/app/feature-schema.js +11 -11
- package/dist/app/form-schema.js +1 -1
- package/dist/app/header-actions-bar.js +1 -1
- package/dist/app/kumiko-screen.js +8 -10
- package/dist/app/layout-fields.js +1 -1
- package/dist/app/list-facets.js +3 -3
- package/dist/app/nav.d.ts +10 -10
- package/dist/app/nav.js +13 -13
- package/dist/app/projection-detail-body.js +4 -4
- package/dist/app/projection-detail-shim.d.ts +6 -5
- package/dist/app/projection-detail-shim.js +23 -22
- package/dist/app/projection-list-body.js +10 -10
- package/dist/app/projection-list-shim.d.ts +5 -5
- package/dist/app/projection-list-shim.js +17 -18
- package/dist/app/qn.js +10 -12
- package/dist/app/qualify-screen-id.js +1 -1
- package/dist/app/screen-access.js +2 -2
- package/dist/app/screen-shared.js +1 -1
- package/dist/app/secret-mint-body.js +4 -4
- package/dist/app/write-failed-error.js +8 -7
- package/dist/components/embedded-list-field.js +4 -4
- package/dist/components/grid-cell-for-field.d.ts +1 -1
- package/dist/components/reference-create-dialog.js +3 -3
- package/dist/components/related-list-section.d.ts +1 -1
- package/dist/components/related-list-section.js +28 -5
- package/dist/components/render-edit-logic.js +1 -1
- package/dist/components/render-edit-types.d.ts +6 -6
- package/dist/components/render-edit.js +63 -65
- package/dist/components/render-field.d.ts +7 -7
- package/dist/components/render-field.js +57 -59
- package/dist/components/render-list.d.ts +43 -44
- package/dist/components/render-list.js +63 -67
- package/dist/components/use-wizard-form-draft.js +10 -10
- package/dist/context/dispatcher-context.js +5 -5
- package/dist/context/user-roles-context.js +2 -2
- package/dist/headless/apex/index.d.ts +1 -2
- package/dist/headless/apex/index.js +5 -6
- package/dist/headless/form/form-controller.js +6 -6
- package/dist/headless/form/zod-bridge.js +2 -2
- package/dist/headless/format/money.js +1 -1
- package/dist/headless/index.js +2 -2
- package/dist/headless/view-model/edit.js +2 -2
- package/dist/headless/view-model/list.js +3 -4
- package/dist/headless/view-model/types.d.ts +3 -3
- package/dist/hooks/reference-limits.js +11 -12
- package/dist/hooks/use-list-url-state.d.ts +13 -13
- package/dist/hooks/use-list-url-state.js +21 -22
- package/dist/hooks/use-query.js +3 -3
- package/dist/hooks/use-reference-lookup.js +16 -17
- package/dist/hooks/use-store.js +18 -22
- package/dist/hooks/use-stream-handler.js +4 -2
- package/dist/i18n-defaults.js +2 -8
- package/dist/i18n.d.ts +10 -10
- package/dist/i18n.js +25 -25
- package/dist/index.js +5 -4
- package/dist/live/dispatcher-live.js +2 -2
- package/dist/primitives.d.ts +225 -221
- package/dist/primitives.js +1 -1
- package/dist/sse/live-events.d.ts +5 -5
- package/dist/sse/live-events.js +17 -17
- package/dist/tokens.d.ts +8 -8
- package/dist/tokens.js +16 -17
- package/dist/web/app/browser-locale.d.ts +9 -9
- package/dist/web/app/browser-locale.js +19 -19
- package/dist/web/app/client-plugin.d.ts +44 -52
- package/dist/web/app/client-plugin.js +4 -5
- package/dist/web/app/create-app.d.ts +28 -31
- package/dist/web/app/create-app.js +21 -22
- package/dist/web/app/document-lang-sync.js +1 -1
- package/dist/web/app/draft-storage.js +1 -1
- package/dist/web/app/nav-providers-context.d.ts +3 -3
- package/dist/web/app/nav-providers-context.js +3 -3
- package/dist/web/app/nav.d.ts +18 -17
- package/dist/web/app/nav.js +66 -64
- package/dist/web/app/rich-content-editor.js +1 -1
- package/dist/web/components/config-cascade.js +17 -18
- package/dist/web/components/config-source-badge.js +4 -4
- package/dist/web/layout/app-layout.d.ts +9 -9
- package/dist/web/layout/app-layout.js +2 -2
- package/dist/web/layout/avatar.d.ts +4 -3
- package/dist/web/layout/avatar.js +15 -17
- package/dist/web/layout/default-app-shell.d.ts +22 -23
- package/dist/web/layout/editor-panel.js +3 -3
- package/dist/web/layout/nav-tree.js +86 -84
- package/dist/web/layout/profile-menu.d.ts +1 -1
- package/dist/web/layout/sidebar-brand.d.ts +7 -7
- package/dist/web/layout/sidebar-user.d.ts +1 -1
- package/dist/web/layout/sidebar-user.js +4 -4
- package/dist/web/layout/sidebar.d.ts +8 -8
- package/dist/web/layout/sidebar.js +7 -7
- package/dist/web/layout/target-resolver-stub.d.ts +5 -5
- package/dist/web/layout/target-resolver-stub.js +20 -20
- package/dist/web/layout/target-url.d.ts +7 -7
- package/dist/web/layout/target-url.js +22 -22
- package/dist/web/layout/theme-toggle.js +4 -5
- package/dist/web/layout/workspace-shell.d.ts +21 -21
- package/dist/web/layout/workspace-shell.js +29 -30
- package/dist/web/lib/cn.d.ts +4 -4
- package/dist/web/lib/cn.js +4 -4
- package/dist/web/primitives/action-menu.d.ts +8 -8
- package/dist/web/primitives/action-menu.js +14 -13
- package/dist/web/primitives/calendar-popover.d.ts +1 -1
- package/dist/web/primitives/calendar-popover.js +10 -10
- package/dist/web/primitives/card-styles.js +1 -1
- package/dist/web/primitives/card.js +2 -2
- package/dist/web/primitives/combobox.d.ts +12 -12
- package/dist/web/primitives/combobox.js +34 -35
- package/dist/web/primitives/data-table-cell.js +2 -2
- package/dist/web/primitives/data-table.js +2 -3
- package/dist/web/primitives/date-field.js +2 -2
- package/dist/web/primitives/date-input.js +4 -4
- package/dist/web/primitives/default-form.js +2 -2
- package/dist/web/primitives/dropdown-menu.js +14 -15
- package/dist/web/primitives/embedded-list-input.js +3 -3
- package/dist/web/primitives/facet-filter.js +1 -1
- package/dist/web/primitives/field.js +9 -8
- package/dist/web/primitives/form.d.ts +1 -1
- package/dist/web/primitives/form.js +6 -6
- package/dist/web/primitives/input.js +2 -2
- package/dist/web/primitives/layout-elements.js +1 -1
- package/dist/web/primitives/layout.js +2 -2
- package/dist/web/primitives/located-timestamp-input.js +4 -4
- package/dist/web/primitives/money-input.js +13 -17
- package/dist/web/primitives/pager.js +1 -1
- package/dist/web/primitives/row-actions.js +3 -4
- package/dist/web/primitives/secret-reveal.js +1 -1
- package/dist/web/primitives/timestamp-input.d.ts +9 -9
- package/dist/web/primitives/timestamp-input.js +21 -21
- package/dist/web/primitives/toast.js +12 -13
- package/dist/web/sse/live-events.d.ts +6 -6
- package/dist/web/sse/live-events.js +3 -3
- package/dist/web/tokens.d.ts +11 -12
- package/dist/web/tokens.js +35 -37
- package/dist/web/version/update-checker.js +14 -13
- package/dist/web/widgets/ai-text-field.js +1 -1
- package/dist/web/widgets/charts.d.ts +10 -10
- package/dist/web/widgets/charts.js +8 -8
- package/dist/web/widgets/collapsible-section.d.ts +3 -3
- package/dist/web/widgets/collapsible-section.js +6 -6
- package/dist/web/widgets/detail-list.d.ts +1 -3
- package/dist/web/widgets/detail-list.js +1 -3
- package/dist/web/widgets/feed-list.d.ts +2 -2
- package/dist/web/widgets/feed-list.js +2 -2
- package/dist/web/widgets/form-fields.d.ts +14 -14
- package/dist/web/widgets/form-fields.js +14 -13
- package/dist/web/widgets/index.js +4 -4
- package/dist/web/widgets/infinity-list.js +3 -3
- package/dist/web/widgets/mode-switch.d.ts +2 -2
- package/dist/web/widgets/mode-switch.js +2 -2
- package/dist/web/widgets/progress-list.d.ts +1 -2
- package/dist/web/widgets/progress-list.js +1 -2
- package/dist/web/widgets/query-table.d.ts +6 -6
- package/dist/web/widgets/result-panel.d.ts +16 -16
- package/dist/web/widgets/result-panel.js +10 -10
- package/dist/web/widgets/section-card.d.ts +3 -3
- package/dist/web/widgets/section-card.js +3 -3
- package/dist/web/widgets/stat.d.ts +4 -4
- package/dist/web/widgets/stat.js +6 -6
- package/dist/web/widgets/states.d.ts +4 -4
- package/dist/web/widgets/states.js +4 -4
- package/dist/web/widgets/use-draft.d.ts +7 -7
- package/dist/web/widgets/use-draft.js +7 -7
- package/package.json +6 -6
|
@@ -6,27 +6,26 @@ import { type DataTableDateRangeFacet, type DataTableFacet, type DataTableProps,
|
|
|
6
6
|
import { type ListEmptyStateContent } from "./list-empty-state.tsx";
|
|
7
7
|
export type RenderListProps = {
|
|
8
8
|
readonly screen: EntityListScreenDefinition;
|
|
9
|
-
readonly entity: EntityDefinition
|
|
9
|
+
readonly entity: Pick<EntityDefinition, "fields" | "derivedFields">;
|
|
10
10
|
readonly rows: readonly Readonly<Record<string, unknown>>[];
|
|
11
11
|
readonly featureName: string;
|
|
12
12
|
readonly translate?: Translate;
|
|
13
13
|
readonly onRowClick?: (row: ListRowViewModel) => void;
|
|
14
|
-
/**
|
|
15
|
-
*
|
|
14
|
+
/** Overrides the default empty box. When set, the automatic CTA via
|
|
15
|
+
* `onCreate` is ignored; the caller's content takes precedence. */
|
|
16
16
|
readonly emptyState?: ReactNode;
|
|
17
|
-
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
/** Adds a "+ New" button to the toolbar and uses it as the CTA in the
|
|
18
|
+
* default empty box. The caller provides the action (for example,
|
|
19
|
+
* navigation to the edit screen). */
|
|
20
20
|
readonly onCreate?: () => void;
|
|
21
|
-
/** Label
|
|
22
|
-
* (`kumiko.actions.create`).
|
|
23
|
-
* überschreiben. */
|
|
21
|
+
/** Label for the + New button. Defaults to the i18n bundle key
|
|
22
|
+
* (`kumiko.actions.create`). The caller can override it. */
|
|
24
23
|
readonly createLabel?: string;
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
24
|
+
/** Enables a search input in the toolbar. Filtering is server-side via
|
|
25
|
+
* the caller's onSearchChange; RenderList does not filter locally. */
|
|
27
26
|
readonly searchable?: boolean;
|
|
28
|
-
/** Placeholder
|
|
29
|
-
*
|
|
27
|
+
/** Placeholder for the search input. Defaults to the i18n bundle key
|
|
28
|
+
* (`kumiko.list.search-placeholder`). */
|
|
30
29
|
readonly searchPlaceholder?: string;
|
|
31
30
|
/** Hint at the left of the toolbar (already translated). */
|
|
32
31
|
readonly description?: string;
|
|
@@ -38,35 +37,35 @@ export type RenderListProps = {
|
|
|
38
37
|
readonly defaultFilterActive?: boolean;
|
|
39
38
|
/** Plural-forms key for the footer count noun; default is the entity's `:noun` convention key. */
|
|
40
39
|
readonly itemNounKey?: string;
|
|
41
|
-
/**
|
|
42
|
-
*
|
|
43
|
-
*
|
|
40
|
+
/** Current search term (from URL state / the parent). RenderList buffers
|
|
41
|
+
* typing locally with a 300ms debounce before calling onSearchChange;
|
|
42
|
+
* otherwise every keystroke would trigger a server round trip. */
|
|
44
43
|
readonly searchValue?: string;
|
|
45
|
-
/**
|
|
46
|
-
*
|
|
44
|
+
/** Called when the debounced search term changes. The caller uses it to
|
|
45
|
+
* set URL state (?<id>.q=…) and trigger a refetch. */
|
|
47
46
|
readonly onSearchChange?: (next: string) => void;
|
|
48
|
-
/**
|
|
47
|
+
/** Current sorting, or null for the server's default order. */
|
|
49
48
|
readonly sort?: ListSort | null;
|
|
50
|
-
/**
|
|
49
|
+
/** Called with the next sort state after a header click. */
|
|
51
50
|
readonly onSortChange?: (next: ListSort | null) => void;
|
|
52
|
-
/** Pager
|
|
53
|
-
*
|
|
54
|
-
* screen.pagination. */
|
|
51
|
+
/** Pager props for pagination="pages". If undefined, no pager UI is
|
|
52
|
+
* rendered; the default or infinite mode is used. KumikoScreen supplies
|
|
53
|
+
* this according to screen.pagination. */
|
|
55
54
|
readonly pager?: {
|
|
56
55
|
readonly page: number;
|
|
57
56
|
readonly limit: number;
|
|
58
57
|
readonly total: number;
|
|
59
58
|
readonly onPageChange: (next: number) => void;
|
|
60
59
|
};
|
|
61
|
-
/** Infinite-
|
|
62
|
-
*
|
|
63
|
-
*
|
|
60
|
+
/** Infinite-scroll wiring for pagination="infinite". KumikoScreen keeps
|
|
61
|
+
* the accumulated rows and cursor; RenderList passes the callbacks
|
|
62
|
+
* through to DataTable. */
|
|
64
63
|
readonly onReachEnd?: () => void;
|
|
65
64
|
readonly loadingMore?: boolean;
|
|
66
65
|
readonly hasMore?: boolean;
|
|
67
|
-
/**
|
|
68
|
-
* EntityListScreenDefinition.rowActions: handler-QN
|
|
69
|
-
* i18n
|
|
66
|
+
/** Per-row actions in resolved form. KumikoScreen builds them from
|
|
67
|
+
* EntityListScreenDefinition.rowActions: handler-QN becomes a dispatcher
|
|
68
|
+
* call, and i18n keys become translated strings. */
|
|
70
69
|
readonly rowActions?: readonly DataTableRowAction[];
|
|
71
70
|
/** How the row-action column renders (see `DataTableProps.rowActionMode`).
|
|
72
71
|
* KumikoScreen derives it from the actions themselves; without it the
|
|
@@ -75,38 +74,38 @@ export type RenderListProps = {
|
|
|
75
74
|
/** Forwarded to `DataTableProps.rowGrouping` / `rowTone`. */
|
|
76
75
|
readonly rowGrouping?: DataTableRowGrouping;
|
|
77
76
|
readonly rowTone?: DataTableProps["rowTone"];
|
|
78
|
-
/** Toolbar
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
77
|
+
/** Toolbar actions in the list header in resolved form. KumikoScreen
|
|
78
|
+
* builds them from EntityListScreenDefinition.toolbarActions:
|
|
79
|
+
* navigate targets use useNav and handler-QN uses a dispatcher call.
|
|
80
|
+
* RenderList renders the buttons on the right side of the toolbar,
|
|
81
|
+
* before "+ New". */
|
|
82
82
|
readonly toolbarActions?: readonly ToolbarActionButton[];
|
|
83
|
-
/** Faceted
|
|
84
|
-
* select/boolean
|
|
83
|
+
/** Faceted filter dropdowns. KumikoScreen builds them from filterable
|
|
84
|
+
* select/boolean fields; RenderList passes them through to DataTable. */
|
|
85
85
|
readonly filterFacets?: readonly DataTableFacet[];
|
|
86
|
-
/**
|
|
86
|
+
/** Currently selected facet values by field (from URL state). */
|
|
87
87
|
readonly filterValues?: Readonly<Record<string, readonly string[]>>;
|
|
88
|
-
/**
|
|
88
|
+
/** Toggles a facet value (sets URL state and refetches). */
|
|
89
89
|
readonly onFilterChange?: (field: string, values: readonly string[]) => void;
|
|
90
90
|
/** Reset aller aktiven Facets. */
|
|
91
91
|
readonly onFilterReset?: () => void;
|
|
92
|
-
/** Time-range filters
|
|
92
|
+
/** Time-range filters — forwarded to the DataTable. */
|
|
93
93
|
readonly dateRangeFacets?: readonly DataTableDateRangeFacet[];
|
|
94
94
|
/** One bound changed (set URL state + refetch). */
|
|
95
95
|
readonly onDateRangeChange?: (field: string, bound: "from" | "to", value: string) => void;
|
|
96
96
|
/** Forwarded to `DataTableProps.chromeless` — drops the table's own card
|
|
97
97
|
* frame for a host that already provides one (relatedList in a tabs-mode
|
|
98
|
-
* section
|
|
98
|
+
* section). Default false. */
|
|
99
99
|
readonly chromeless?: boolean;
|
|
100
100
|
/** Forwarded to `DataTableProps.scrollBody` — bounds the table height and
|
|
101
101
|
* scrolls rows internally instead of growing the page (relatedList in a
|
|
102
|
-
* tabs-mode section
|
|
102
|
+
* tabs-mode section). Default false. */
|
|
103
103
|
readonly scrollBody?: boolean;
|
|
104
104
|
/** Forwarded to `DataTableProps.screenPadding` — `true`: the table carries
|
|
105
105
|
* the shared screen padding because it is the screen body (entityList/
|
|
106
|
-
* projectionList
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
* padded host. */
|
|
106
|
+
* projectionList). Explicit `false`: no padding at all — the host already
|
|
107
|
+
* pads its own children (relatedList in a tabs-mode Card). Omitted: legacy
|
|
108
|
+
* `p-6` fallback for a standalone table with no padded host. */
|
|
110
109
|
readonly screenPadding?: boolean;
|
|
111
110
|
/** Forwarded to `DataTableProps.expandedRowIds` / `onToggleRowExpanded` /
|
|
112
111
|
* `renderExpandedRow` (entityList `expandableRow`). */
|
|
@@ -18,8 +18,8 @@ function isHeaderPromotableToolbarAction(action) {
|
|
|
18
18
|
}
|
|
19
19
|
export function RenderList(props) {
|
|
20
20
|
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, rowGrouping, rowTone, toolbarActions, filterFacets, filterValues, onFilterChange, onFilterReset, dateRangeFacets, onDateRangeChange, chromeless, scrollBody, screenPadding, expandedRowIds, onToggleRowExpanded, renderExpandedRow, } = props;
|
|
21
|
-
//
|
|
22
|
-
//
|
|
21
|
+
// As in RenderEdit, fall back to the i18next context for translation;
|
|
22
|
+
// otherwise column headers would display raw i18n keys.
|
|
23
23
|
const t = useTranslation();
|
|
24
24
|
const translate = translateProp ?? t;
|
|
25
25
|
const { DataTable, Button, Dialog, Input, Banner, PageHeader } = usePrimitives();
|
|
@@ -62,30 +62,28 @@ export function RenderList(props) {
|
|
|
62
62
|
return () => clearTimeout(timer);
|
|
63
63
|
}, [localQ, searchValue, onSearchChange]);
|
|
64
64
|
const vm = useMemo(() => computeListViewModel({ screen, entity, rows, translate, featureName }), [screen, entity, rows, translate, featureName]);
|
|
65
|
-
//
|
|
66
|
-
// Hooks
|
|
67
|
-
//
|
|
68
|
-
// ReferenceLookupBridge
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
// React verwaltet die mounted/unmounted Lifecycles beim Schema-
|
|
72
|
-
// Wechsel über die key-Property automatisch.
|
|
65
|
+
// Reference-field eager loading through bridge components.
|
|
66
|
+
// The Hooks rule requires useReferenceLookup for each reference column,
|
|
67
|
+
// but the number of columns varies with the schema. Each column therefore
|
|
68
|
+
// mounts a ReferenceLookupBridge that publishes its map to local state
|
|
69
|
+
// through onMap. Each bridge makes a constant number of hook calls (one),
|
|
70
|
+
// and React manages its lifecycle when the schema changes via key.
|
|
73
71
|
const referenceColumns = useMemo(() => vm.columns
|
|
74
72
|
.filter((c) => c.type === "reference" && c.refEntity !== undefined)
|
|
75
73
|
.map((c) => ({
|
|
76
74
|
field: c.field,
|
|
77
75
|
refEntity: c.refEntity ?? "",
|
|
78
|
-
//
|
|
79
|
-
//
|
|
76
|
+
// refFeature comes from parseRefTarget in the ViewModel
|
|
77
|
+
// (default = the current featureName).
|
|
80
78
|
refFeature: c.refFeature ?? featureName,
|
|
81
79
|
labelField: c.refLabelField ?? "id",
|
|
82
80
|
})), [vm.columns, featureName]);
|
|
83
81
|
const [referenceLookups, setReferenceLookups] = useState({});
|
|
84
82
|
const handleLookupMap = useCallback((field, map) => {
|
|
85
83
|
setReferenceLookups((prev) => {
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
//
|
|
84
|
+
// Skip the update when the map is identical. Otherwise the bridge
|
|
85
|
+
// would re-render and send the same map back until React entered
|
|
86
|
+
// an infinite loop.
|
|
89
87
|
if (prev[field] === map)
|
|
90
88
|
return prev;
|
|
91
89
|
return { ...prev, [field]: map };
|
|
@@ -98,8 +96,8 @@ export function RenderList(props) {
|
|
|
98
96
|
return vm.columns.map((col) => {
|
|
99
97
|
if (col.type !== "reference")
|
|
100
98
|
return col;
|
|
101
|
-
//
|
|
102
|
-
//
|
|
99
|
+
// An author-declared renderer always takes precedence;
|
|
100
|
+
// the default is used only when none is set.
|
|
103
101
|
if (col.renderer !== undefined)
|
|
104
102
|
return col;
|
|
105
103
|
const map = referenceLookups[col.field];
|
|
@@ -145,11 +143,11 @@ export function RenderList(props) {
|
|
|
145
143
|
});
|
|
146
144
|
};
|
|
147
145
|
const renderer = (value, row) => {
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
152
|
-
//
|
|
146
|
+
// Server eager loading: when the server includes _refs, read the display
|
|
147
|
+
// value directly from the resolved row. This avoids a round trip
|
|
148
|
+
// through the bridge map and the limit:200 constraint. The
|
|
149
|
+
// EagerloadedRow type from @cosmicdrift/kumiko-framework/db defines
|
|
150
|
+
// the shape of _refs.
|
|
153
151
|
const resolved = eagerloadedRef(row, col.field);
|
|
154
152
|
if (Array.isArray(resolved) && resolved.length > 0) {
|
|
155
153
|
return resolved.map((r) => String(r[labelField] ?? r["id"] ?? "")).join(", ");
|
|
@@ -157,7 +155,7 @@ export function RenderList(props) {
|
|
|
157
155
|
if (isPlainRecord(resolved)) {
|
|
158
156
|
return String(resolved[labelField] ?? resolved["id"] ?? "");
|
|
159
157
|
}
|
|
160
|
-
// Renderer-
|
|
158
|
+
// Renderer-side fallback when server eager loading is not active.
|
|
161
159
|
if (Array.isArray(value)) {
|
|
162
160
|
if (value.length === 0)
|
|
163
161
|
return EMPTY_VALUE_PLACEHOLDER;
|
|
@@ -182,9 +180,9 @@ export function RenderList(props) {
|
|
|
182
180
|
...vm,
|
|
183
181
|
columns: enrichedColumns,
|
|
184
182
|
}), [vm, enrichedColumns]);
|
|
185
|
-
// i18n
|
|
186
|
-
//
|
|
187
|
-
// (kumiko.actions.create, kumiko.list.search-placeholder, …).
|
|
183
|
+
// i18n defaults for toolbar and empty-state strings. The caller can
|
|
184
|
+
// override each one through a prop; otherwise the framework bundles provide
|
|
185
|
+
// them (kumiko.actions.create, kumiko.list.search-placeholder, …).
|
|
188
186
|
const effectiveCreateLabel = translate(createLabel ?? screen.createLabel ?? "kumiko.actions.create");
|
|
189
187
|
const effectiveSearchPlaceholder = translate(searchPlaceholder ?? screen.searchPlaceholder ?? "kumiko.list.search-placeholder");
|
|
190
188
|
const nounKey = itemNounKey ?? `${featureName}:entity:${screen.entity}:noun`;
|
|
@@ -193,12 +191,12 @@ export function RenderList(props) {
|
|
|
193
191
|
? (count) => translate(nounKey, { count })
|
|
194
192
|
: undefined;
|
|
195
193
|
const toolbarStart = searchable ? (_jsx(Input, { kind: "text", id: "render-list-search", name: "search", value: localQ, onChange: setLocalQ, placeholder: effectiveSearchPlaceholder })) : undefined;
|
|
196
|
-
// Toolbar-
|
|
197
|
-
//
|
|
198
|
-
//
|
|
199
|
-
//
|
|
200
|
-
//
|
|
201
|
-
//
|
|
194
|
+
// Toolbar-end slot: an optional header slot (for example, a cap counter,
|
|
195
|
+
// at the left of the right-hand cluster), toolbar actions from the schema,
|
|
196
|
+
// and an optional "+ New" button at the right edge. The header slot lives
|
|
197
|
+
// inside the toolbar row, with the same card frame, rather than as loose
|
|
198
|
+
// text above the screen title. Items retain array order, and "+ New" comes
|
|
199
|
+
// last because it is the most common and prominent CTA.
|
|
202
200
|
const hasToolbarActions = toolbarActions !== undefined && toolbarActions.length > 0;
|
|
203
201
|
const hasHeaderSlot = screen.slots?.header !== undefined;
|
|
204
202
|
const toolbarIconOnly = hasToolbarActions && shouldRenderActionsIconOnly(toolbarActions);
|
|
@@ -218,30 +216,29 @@ export function RenderList(props) {
|
|
|
218
216
|
hasToolbarActionsInToolbar ||
|
|
219
217
|
(onCreate !== undefined && !createInPageHeader) ? (_jsxs(_Fragment, { children: [hasHeaderSlot && _jsx(ListHeaderSlotMount, { screen: screen }), hasToolbarActionsInToolbar &&
|
|
220
218
|
toolbarActionsInToolbar.map((a) => (_jsx(ToolbarActionView, { action: a, iconOnly: toolbarIconOnly, Button: Button, Dialog: Dialog, Banner: Banner }, a.id))), !createInPageHeader && createButton] })) : undefined;
|
|
221
|
-
// Empty
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
//
|
|
219
|
+
// Empty state: by default, show a heading, description, and optional CTA
|
|
220
|
+
// button. The caller can replace it completely through emptyState.
|
|
221
|
+
// Without onCreate or an override, DataTable uses its default
|
|
222
|
+
// (`kumiko.list.no-entries`).
|
|
225
223
|
const defaultFilterActive = defaultFilterActiveProp && onFilterReset !== undefined;
|
|
226
224
|
const composedEmptyState = emptyState ??
|
|
227
225
|
(onCreate !== undefined || emptyContent !== undefined || narrowed || defaultFilterActive ? (_jsx(ListEmptyStateBody, { content: emptyContent, narrowed: narrowed, defaultFilterActive: defaultFilterActive, ...(onFilterReset !== undefined && { onShowAll: onFilterReset }), translate: translate, createButton: onCreate !== undefined && !narrowed && !defaultFilterActive && !createInPageHeader ? (_jsx(Button, { variant: "primary", icon: "plus", onClick: onCreate, testId: "render-list-empty-create", children: effectiveCreateLabel })) : undefined })) : undefined);
|
|
228
|
-
// Title
|
|
229
|
-
//
|
|
230
|
-
// screenId
|
|
231
|
-
// dann dass die Übersetzung fehlt).
|
|
226
|
+
// Title resolution uses the conventional i18n key `screen:<id>.title`.
|
|
227
|
+
// If the bundle knows the key, use its translated title; otherwise use
|
|
228
|
+
// screenId so the app developer can see that the translation is missing.
|
|
232
229
|
const titleKey = `screen:${screen.id}.title`;
|
|
233
230
|
const resolvedTitle = translate(titleKey);
|
|
234
231
|
const toolbarTitle = resolvedTitle === titleKey ? screen.id : resolvedTitle;
|
|
235
|
-
// ListSort = DataTableSort (use-list-url-state
|
|
232
|
+
// ListSort = DataTableSort (aliased in use-list-url-state), so no cast is needed.
|
|
236
233
|
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" })] }));
|
|
237
234
|
}
|
|
238
|
-
// Header
|
|
239
|
-
//
|
|
240
|
-
//
|
|
241
|
-
// entityId
|
|
242
|
-
//
|
|
243
|
-
//
|
|
244
|
-
//
|
|
235
|
+
// Header slot in the list toolbar (toolbarEnd): `screen.slots.header` is
|
|
236
|
+
// a PlatformComponent (`{ react: { __component: "X" } }`) resolved through
|
|
237
|
+
// the same ExtensionSectionsProvider registry as entityEdit sections.
|
|
238
|
+
// entityId is null in the list context because there is no row; the component
|
|
239
|
+
// loads its own data (for example, a cap counter from a usage query). A
|
|
240
|
+
// separate component is required by the Rules of Hooks because
|
|
241
|
+
// useExtensionSectionComponent must not run conditionally in RenderList.
|
|
245
242
|
function ListHeaderSlotMount({ screen, }) {
|
|
246
243
|
const header = screen.slots?.header;
|
|
247
244
|
const name = header !== undefined ? extensionSectionName(header) : undefined;
|
|
@@ -261,18 +258,18 @@ function ListHeaderSlotMount({ screen, }) {
|
|
|
261
258
|
// (useListUrlState) — e.g. a tag filter narrowing the list to an id-set.
|
|
262
259
|
return _jsx(Component, { entityName: screen.entity, entityId: null, screenId: screen.id });
|
|
263
260
|
}
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
268
|
-
// key-Prop im map-Loop des Parents.
|
|
261
|
+
// Bridge component for each reference column. It is mounted once per
|
|
262
|
+
// column, calls useReferenceLookup unconditionally, and publishes the map
|
|
263
|
+
// to the parent through onMap. React manages mounting and unmounting when
|
|
264
|
+
// the schema changes through the key prop in the parent's map loop.
|
|
269
265
|
function ReferenceLookupBridge({ field, refEntity, labelField, featureName, onMap, }) {
|
|
270
266
|
const lookup = useReferenceLookup(featureName, refEntity, labelField);
|
|
271
|
-
//
|
|
272
|
-
//
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
// "not resolved", an empty published map
|
|
267
|
+
// Use an effect rather than calling directly so setState runs outside the
|
|
268
|
+
// render path; otherwise React warns about updating one component while
|
|
269
|
+
// rendering another.
|
|
270
|
+
// Do not publish while loading or after a failed lookup: a missing map means
|
|
271
|
+
// "not resolved", whereas an empty published map means "nothing found".
|
|
272
|
+
// Only the latter may display "Unknown".
|
|
276
273
|
const resolved = !lookup.loading && !lookup.failed;
|
|
277
274
|
useEffect(() => {
|
|
278
275
|
if (resolved)
|
|
@@ -280,18 +277,17 @@ function ReferenceLookupBridge({ field, refEntity, labelField, featureName, onMa
|
|
|
280
277
|
}, [field, lookup.map, resolved, onMap]);
|
|
281
278
|
return null;
|
|
282
279
|
}
|
|
283
|
-
// ToolbarActionView
|
|
284
|
-
//
|
|
285
|
-
//
|
|
286
|
-
//
|
|
280
|
+
// ToolbarActionView renders one button per toolbar action, with a confirm
|
|
281
|
+
// dialog when required. It follows the inline RowAction pattern but has no
|
|
282
|
+
// row context. The busy state covers async onTrigger; the dialog opens before
|
|
283
|
+
// the trigger when confirmation is required for confirm/danger actions.
|
|
287
284
|
function ToolbarActionView({ action, iconOnly = false, Button, Dialog, Banner, }) {
|
|
288
285
|
const [busy, setBusy] = useState(false);
|
|
289
286
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
290
|
-
// Toolbar
|
|
291
|
-
//
|
|
292
|
-
//
|
|
293
|
-
//
|
|
294
|
-
// sieht für den User wie "nichts passiert" aus.
|
|
287
|
+
// Toolbar actions do not have access to the toast primitive, unlike
|
|
288
|
+
// rowActions routed through the DataTable primitive, so an inline error
|
|
289
|
+
// banner is used to surface failures. Without it, a swallowed failure result
|
|
290
|
+
// would look to the user as though nothing happened.
|
|
295
291
|
const [errorText, setErrorText] = useState(null);
|
|
296
292
|
const trigger = async () => {
|
|
297
293
|
setBusy(true);
|
|
@@ -12,10 +12,10 @@ const FORM_DRAFT_LIST = "form-draft:query:list";
|
|
|
12
12
|
// Mirrors bundled-features/src/form-draft/web/constants.ts's FORM_DRAFT_KEY_MAX_LENGTH
|
|
13
13
|
// — same hardcoding rationale as the QNs above (no dependency on that package).
|
|
14
14
|
const FORM_DRAFT_KEY_MAX_LENGTH = 256;
|
|
15
|
-
// Trailing-edge debounce for patch()-triggered draft saves
|
|
15
|
+
// Trailing-edge debounce for patch()-triggered draft saves. A single
|
|
16
16
|
// patch() call (VIN-decode, an extension section) should not save immediately
|
|
17
17
|
// per call, but patch() can also fire from inside onChange on every keystroke
|
|
18
|
-
// (the
|
|
18
|
+
// (the derived-field shape) — 500ms collapses a typing burst into one
|
|
19
19
|
// save instead of one per keystroke, while still saving well before a user
|
|
20
20
|
// abandons the tab.
|
|
21
21
|
export const PATCH_DRAFT_SAVE_DEBOUNCE_MS = 500;
|
|
@@ -40,7 +40,7 @@ function mintDraftId() {
|
|
|
40
40
|
return `draft-${Math.random().toString(36).slice(2)}${Math.random().toString(36).slice(2)}`;
|
|
41
41
|
}
|
|
42
42
|
export function useWizardFormDraftState({ screenId, entityId, isCreateMode, draftStorage, }) {
|
|
43
|
-
// Create-mode draftId
|
|
43
|
+
// Create-mode draftId — resumed from `sessionStorage` (web)
|
|
44
44
|
// on mount so a same-tab reload finds the right one of several parallel
|
|
45
45
|
// create-sessions on this screen; `null` until the first step change
|
|
46
46
|
// mints one (saveDraft), or the mount-time `form-draft:query:list`
|
|
@@ -55,7 +55,7 @@ export function useWizardFormDraftState({ screenId, entityId, isCreateMode, draf
|
|
|
55
55
|
// once for this mount. Without this, a create-mode discard resets `draftId`
|
|
56
56
|
// to `null` (see discardDraft below) and would re-arm the list effect —
|
|
57
57
|
// silently adopting an unrelated parallel draft on the same screen right
|
|
58
|
-
// after the user just submitted
|
|
58
|
+
// after the user just submitted.
|
|
59
59
|
const didListRef = useRef(false);
|
|
60
60
|
const [draftCandidates, setDraftCandidates] = useState(null);
|
|
61
61
|
// Derived from the screen id + the host entity id only — never from
|
|
@@ -64,7 +64,7 @@ export function useWizardFormDraftState({ screenId, entityId, isCreateMode, draf
|
|
|
64
64
|
// unchanged. Create-mode: `${screenId}:new:${draftId}` once a draftId
|
|
65
65
|
// exists (resumed, adopted, or minted), `undefined` before that — two
|
|
66
66
|
// parallel create sessions on the same screen must never collapse onto
|
|
67
|
-
// the same key
|
|
67
|
+
// the same key.
|
|
68
68
|
const draftKey = useMemo(() => isCreateMode
|
|
69
69
|
? draftId !== null
|
|
70
70
|
? `${screenId}:new:${draftId}`
|
|
@@ -133,7 +133,7 @@ export function useWizardFormDraftRestore({ screenId, isCreateMode, draftEnabled
|
|
|
133
133
|
hasNavigatedRef,
|
|
134
134
|
setRawStep,
|
|
135
135
|
]);
|
|
136
|
-
// Mount-time fallback
|
|
136
|
+
// Mount-time fallback for create-mode when no draftId
|
|
137
137
|
// survived in storage (new tab, cleared storage): ask the server for this
|
|
138
138
|
// screen's open drafts and let the user choose via a picker (below) —
|
|
139
139
|
// even for exactly one candidate. Auto-adopting a lone candidate would
|
|
@@ -186,7 +186,7 @@ export function buildWizardDraftActions({ screenId, isCreateMode, draftEnabled,
|
|
|
186
186
|
// (see draftCandidates below) — adopt it the same way a single
|
|
187
187
|
// auto-adopted candidate would be.
|
|
188
188
|
function adoptDraft(candidate) {
|
|
189
|
-
// Locked state
|
|
189
|
+
// Locked state: `disabled` means "no write possible" —
|
|
190
190
|
// repointing draftKey and patching in the candidate's values is exactly
|
|
191
191
|
// that, so a locked form must not adopt a draft.
|
|
192
192
|
// skip: a locked form keeps its current draft (reason above).
|
|
@@ -202,8 +202,8 @@ export function buildWizardDraftActions({ screenId, isCreateMode, draftEnabled,
|
|
|
202
202
|
//
|
|
203
203
|
// Create-mode, first draft save (step change OR a debounced patch() from
|
|
204
204
|
// controlled mode / extension sections, see patchAndScheduleDraftSave
|
|
205
|
-
// above — both call this on step 0 too): mints the draftId here
|
|
206
|
-
//
|
|
205
|
+
// above — both call this on step 0 too): mints the draftId here
|
|
206
|
+
// rather than at mount, so a form nobody ever interacted with
|
|
207
207
|
// never claims a draftId or writes a row. `draftKey`/`draftId` state
|
|
208
208
|
// won't reflect the mint until the next render, so the just-minted key is
|
|
209
209
|
// computed inline instead of read from the memoized `draftKey`.
|
|
@@ -220,7 +220,7 @@ export function buildWizardDraftActions({ screenId, isCreateMode, draftEnabled,
|
|
|
220
220
|
key = `${screenId}:new:${mintedId}`;
|
|
221
221
|
// A stale picker from the mount-time list fallback must not survive a
|
|
222
222
|
// mint — picking a candidate afterwards would repoint draftKey at an
|
|
223
|
-
// unrelated draft mid-edit and overwrite it
|
|
223
|
+
// unrelated draft mid-edit and overwrite it.
|
|
224
224
|
setDraftCandidates(null);
|
|
225
225
|
}
|
|
226
226
|
// skip: create-mode with a draftId that failed to mint — unreachable
|
|
@@ -32,11 +32,11 @@ export function useDispatcher() {
|
|
|
32
32
|
}
|
|
33
33
|
return dispatcher;
|
|
34
34
|
}
|
|
35
|
-
// Soft
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
35
|
+
// Soft variant: returns null when no provider is mounted instead of throwing. For
|
|
36
|
+
// components that optionally need a Dispatcher (for example, KumikoScreen renders
|
|
37
|
+
// with or without rowActions; the test suite does not mount a Dispatcher when the
|
|
38
|
+
// screen triggers no mutation). Callers must provide a sensible fallback for null
|
|
39
|
+
// (typically, no action wiring).
|
|
40
40
|
export function useOptionalDispatcher() {
|
|
41
41
|
return useContext(DispatcherContext) ?? undefined;
|
|
42
42
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useContext } from "react";
|
|
3
3
|
// Threads the current user's roles through the tree so KumikoScreen can
|
|
4
|
-
// gate role-restricted screens at render time
|
|
5
|
-
//
|
|
4
|
+
// gate role-restricted screens at render time — nav filtering alone
|
|
5
|
+
// doesn't stop a direct-URL/screenQn hit on a role-gated screen).
|
|
6
6
|
// Apps wire this from their `shell` render-prop, the same place they
|
|
7
7
|
// already pass `user` to WorkspaceShell for nav filtering.
|
|
8
8
|
//
|
|
@@ -231,6 +231,5 @@ export declare function injectPageHead(html: string, tagsHtml: string): string;
|
|
|
231
231
|
* the shared timeout. Never throws and never changes `html` on a resolver
|
|
232
232
|
* error, a `null` resolve, or a timeout — the caller's shell always ships
|
|
233
233
|
* unchanged. runProdApp and runDevApp both call this single function, so a
|
|
234
|
-
* future change to timeout or fallback semantics has one place to land
|
|
235
|
-
* (kumiko-framework#3026). */
|
|
234
|
+
* future change to timeout or fallback semantics has one place to land. */
|
|
236
235
|
export declare function resolveAndInjectPageHead(html: string, resolvePageHead: PageHeadResolver, input: PageHeadResolveInput): Promise<string>;
|
|
@@ -8,8 +8,8 @@ import { APEX_STRUCTURAL_CSS } from "./css.js";
|
|
|
8
8
|
import { APEX_LIGHTBOX_HTML, APEX_LIGHTBOX_SCRIPT } from "./lightbox.js";
|
|
9
9
|
export { APEX_NAV_MENU_CSS, APEX_NAV_TOGGLE_RESPONSIVE_CSS, APEX_STRUCTURAL_CSS } from "./css.js";
|
|
10
10
|
export { APEX_LIGHTBOX_HTML, APEX_LIGHTBOX_SCRIPT, APEX_LIGHTBOX_SCRIPT_CSP_HASH, } from "./lightbox.js";
|
|
11
|
-
// JSON in <script
|
|
12
|
-
// `</script>`
|
|
11
|
+
// JSON in <script> context: serialize `<` as < so neither
|
|
12
|
+
// `</script>` nor `<!--` can escape the block (JSON remains valid).
|
|
13
13
|
function scriptSafeJsonHtml(value) {
|
|
14
14
|
return JSON.stringify(value).replace(/</g, "\\u003c");
|
|
15
15
|
}
|
|
@@ -265,7 +265,7 @@ export function renderApexHeadTags(head) {
|
|
|
265
265
|
export function renderApexPage(page) {
|
|
266
266
|
const { head, brand } = page;
|
|
267
267
|
const theme = page.theme ?? "light";
|
|
268
|
-
// Brand
|
|
268
|
+
// Brand CSS is app-authored (see the file header for the trust boundary), not tenant input.
|
|
269
269
|
const cssHtml = (brand.fontFaceCss ?? "") + brand.tokensCss + APEX_STRUCTURAL_CSS;
|
|
270
270
|
const sectionsHtml = page.sections.map(renderSection).join("\n\n ");
|
|
271
271
|
// interactive-widget is ignored by iOS Safari; apex pages have no fixed bottom bars, so
|
|
@@ -308,7 +308,7 @@ export function injectPageHead(html, tagsHtml) {
|
|
|
308
308
|
// Resolver runs alongside the request, never gates it: a slow, throwing, or
|
|
309
309
|
// null-returning resolver must never turn a 200 shell into a 500 or a
|
|
310
310
|
// stall. One constant shared by runProdApp and runDevApp so both time out
|
|
311
|
-
// on the same value
|
|
311
|
+
// on the same value.
|
|
312
312
|
const PAGE_HEAD_TIMEOUT_MS = 300;
|
|
313
313
|
async function resolvePageHeadWithTimeout(resolvePageHead, input) {
|
|
314
314
|
let timer;
|
|
@@ -367,8 +367,7 @@ function toApexHead(meta) {
|
|
|
367
367
|
* the shared timeout. Never throws and never changes `html` on a resolver
|
|
368
368
|
* error, a `null` resolve, or a timeout — the caller's shell always ships
|
|
369
369
|
* unchanged. runProdApp and runDevApp both call this single function, so a
|
|
370
|
-
* future change to timeout or fallback semantics has one place to land
|
|
371
|
-
* (kumiko-framework#3026). */
|
|
370
|
+
* future change to timeout or fallback semantics has one place to land. */
|
|
372
371
|
export async function resolveAndInjectPageHead(html, resolvePageHead, input) {
|
|
373
372
|
const meta = await resolvePageHeadWithTimeout(resolvePageHead, input);
|
|
374
373
|
if (!meta)
|
|
@@ -68,7 +68,7 @@ function stripUntouchedEmptyStrings(values, initial, forceIncludeEmpty) {
|
|
|
68
68
|
let out;
|
|
69
69
|
for (const key of Object.keys(v)) {
|
|
70
70
|
// Value-based seed strip, except keys previously submitted as "" after a
|
|
71
|
-
// real clear — those must survive post-rebase submits
|
|
71
|
+
// real clear — those must survive post-rebase submits.
|
|
72
72
|
if (!forceIncludeEmpty.has(key) && v[key] === "" && ini[key] === "") {
|
|
73
73
|
out ??= { ...v };
|
|
74
74
|
delete out[key];
|
|
@@ -105,7 +105,7 @@ export function createFormController(options) {
|
|
|
105
105
|
let initial = { ...options.initial };
|
|
106
106
|
let errors = Object.freeze({});
|
|
107
107
|
// Keys last submitted as "" (intentional clear). Survives rebase so a later
|
|
108
|
-
// submit of other fields does not drop them
|
|
108
|
+
// submit of other fields does not drop them.
|
|
109
109
|
const forceIncludeEmpty = new Set();
|
|
110
110
|
// ctx is cast-held as TCtx; `undefined` is valid when callers don't
|
|
111
111
|
// declare conditional predicates that depend on it.
|
|
@@ -166,7 +166,7 @@ export function createFormController(options) {
|
|
|
166
166
|
function runValidate(scope, opts) {
|
|
167
167
|
// See the validate() doc comment in types.ts for the scope contract.
|
|
168
168
|
// A scope entry matches its exact path and everything below it, but not
|
|
169
|
-
// siblings: `address.city` must not pull in `address.zip
|
|
169
|
+
// siblings: `address.city` must not pull in `address.zip`.
|
|
170
170
|
const isInScope = scope === undefined
|
|
171
171
|
? undefined
|
|
172
172
|
: (path) => scope.some((entry) => path === entry || path.startsWith(`${entry}.`));
|
|
@@ -193,8 +193,8 @@ export function createFormController(options) {
|
|
|
193
193
|
hiddenFields.add(fieldKey);
|
|
194
194
|
}
|
|
195
195
|
// Root-level .refine() issues use path "(root)". Wizard step validate()
|
|
196
|
-
// still drops them
|
|
197
|
-
// fields={…} cannot silently skip cross-field rules
|
|
196
|
+
// still drops them; submit() passes includeRoot so RenderEdit
|
|
197
|
+
// fields={…} cannot silently skip cross-field rules.
|
|
198
198
|
const relevantIssues = relevantFieldIssues(zodErrorToFieldIssues(parsed.error), {
|
|
199
199
|
hiddenFields,
|
|
200
200
|
...(scope !== undefined && { scope }),
|
|
@@ -382,7 +382,7 @@ export function createFormController(options) {
|
|
|
382
382
|
const result = await dispatcher.write(submitCfg.type, payload, { requestId });
|
|
383
383
|
unansweredAttempt = isTransportFailure(result) ? { requestId, fingerprint } : null;
|
|
384
384
|
if (result.isSuccess) {
|
|
385
|
-
// Remember intentional empty clears so post-rebase submits keep them
|
|
385
|
+
// Remember intentional empty clears so post-rebase submits keep them.
|
|
386
386
|
if (payload && typeof payload === "object" && !Array.isArray(payload)) {
|
|
387
387
|
for (const [k, v] of Object.entries(payload)) {
|
|
388
388
|
if (v === "")
|
|
@@ -46,8 +46,8 @@ export function zodErrorToFieldIssues(error) {
|
|
|
46
46
|
}
|
|
47
47
|
// The issues that count for validity: hidden fields never block, and a
|
|
48
48
|
// scope narrows to the listed paths (an entry matches itself and everything
|
|
49
|
-
// below it, not siblings
|
|
50
|
-
// inside a scope (submit passes it
|
|
49
|
+
// below it, not siblings). `includeRoot` keeps "(root)" refine issues
|
|
50
|
+
// inside a scope (submit passes it). Shared by the controller's
|
|
51
51
|
// validate() and RenderEdit's `valid` dry-run so the two cannot drift.
|
|
52
52
|
export function relevantFieldIssues(issues, opts) {
|
|
53
53
|
const { hiddenFields, scope, includeRoot } = opts;
|