@cosmicdrift/kumiko-renderer 0.386.0 → 0.388.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 +12 -4
- package/dist/app/action-form-shim.d.ts +8 -8
- package/dist/app/action-form-shim.js +20 -22
- package/dist/app/action-form-support.d.ts +1 -0
- package/dist/app/action-form-support.js +1 -1
- 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 +7 -3
- 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.d.ts +11 -0
- package/dist/app/entity-edit-prefill.js +18 -13
- 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 +29 -28
- package/dist/app/extension-sections.js +10 -10
- package/dist/app/feature-schema.js +11 -11
- package/dist/app/form-schema.js +2 -2
- package/dist/app/header-actions-bar.js +2 -2
- 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/edit-mounts.d.ts +2 -2
- package/dist/components/embedded-list-field.js +5 -5
- 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 +15 -2
- package/dist/components/render-edit-action-button.d.ts +1 -3
- package/dist/components/render-edit-action-button.js +2 -2
- 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 +74 -73
- package/dist/components/render-field.d.ts +7 -7
- package/dist/components/render-field.js +62 -61
- package/dist/components/render-list.d.ts +42 -43
- package/dist/components/render-list.js +93 -84
- 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/flow-diagram/render.js +9 -1
- package/dist/headless/apex/index.d.ts +1 -2
- package/dist/headless/apex/index.js +5 -6
- package/dist/headless/form/form-controller.js +14 -6
- package/dist/headless/form/types.d.ts +1 -0
- package/dist/headless/form/zod-bridge.js +2 -2
- package/dist/headless/format/index.js +89 -64
- package/dist/headless/format/money.js +1 -1
- package/dist/headless/index.js +2 -2
- package/dist/headless/view-model/edit.js +4 -2
- package/dist/headless/view-model/list.js +184 -150
- package/dist/headless/view-model/types.d.ts +6 -3
- package/dist/hooks/live-refetch-scheduler.js +4 -0
- 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.d.ts +9 -1
- package/dist/hooks/use-reference-lookup.js +32 -19
- package/dist/hooks/use-store.js +18 -22
- package/dist/hooks/use-stream-handler.js +4 -2
- package/dist/i18n-defaults.js +6 -8
- package/dist/i18n.d.ts +10 -10
- package/dist/i18n.js +25 -25
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -4
- package/dist/live/dispatcher-live.js +2 -2
- package/dist/primitives.d.ts +244 -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 +29 -25
- package/dist/web/app/dashboard-body.js +17 -1
- package/dist/web/app/dashboard-panels.js +12 -2
- 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/index.d.ts +1 -1
- package/dist/web/index.js +1 -1
- 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/shell-header.js +2 -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/uniform-bar-controls.d.ts +1 -0
- package/dist/web/layout/uniform-bar-controls.js +4 -0
- 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/lib/file-ref-download.js +4 -1
- 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/code-input.d.ts +6 -0
- package/dist/web/primitives/code-input.js +31 -0
- 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 +6 -6
- 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 +2 -2
- package/dist/web/primitives/field.js +9 -8
- package/dist/web/primitives/file-thumbnail.d.ts +3 -0
- package/dist/web/primitives/file-thumbnail.js +5 -0
- package/dist/web/primitives/form.d.ts +1 -1
- package/dist/web/primitives/form.js +6 -6
- package/dist/web/primitives/input.js +8 -3
- package/dist/web/primitives/layout-elements.js +7 -5
- 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/page-header.js +3 -1
- package/dist/web/primitives/pager.js +1 -1
- package/dist/web/primitives/row-actions.js +6 -7
- 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/primitives/use-focus-on-mount.d.ts +2 -0
- package/dist/web/primitives/use-focus-on-mount.js +9 -0
- 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/bounded-load-cache.d.ts +1 -1
- package/dist/web/widgets/bounded-load-cache.js +10 -4
- package/dist/web/widgets/charts.d.ts +10 -10
- package/dist/web/widgets/charts.js +8 -8
- package/dist/web/widgets/choice-cards.d.ts +17 -0
- package/dist/web/widgets/choice-cards.js +12 -0
- package/dist/web/widgets/collapsible-section.d.ts +3 -3
- package/dist/web/widgets/collapsible-section.js +7 -7
- package/dist/web/widgets/detail-list.d.ts +5 -3
- package/dist/web/widgets/detail-list.js +5 -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.d.ts +1 -0
- package/dist/web/widgets/index.js +5 -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/pdf-preview.js +1 -1
- package/dist/web/widgets/plan-card.d.ts +2 -0
- package/dist/web/widgets/plan-card.js +1 -1
- 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/upload-zone.js +13 -5
- package/dist/web/widgets/use-draft.d.ts +7 -7
- package/dist/web/widgets/use-draft.js +7 -7
- package/package.json +6 -6
- package/src/changes.json +49 -0
|
@@ -1,17 +1,16 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Reference lookup limits are centralized here.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// - Combobox
|
|
5
|
-
// typed-search
|
|
6
|
-
// -
|
|
7
|
-
//
|
|
3
|
+
// Two use cases have different default limits:
|
|
4
|
+
// - Combobox editing (a user picks one): 50 is sufficient because the
|
|
5
|
+
// typed-search fallback is active (REMOTE_SEARCH_DEBOUNCE_MS).
|
|
6
|
+
// - Bulk list display (rendering cells for many rows): 200 because each
|
|
7
|
+
// reference column is loaded once per page, not once per cell.
|
|
8
8
|
//
|
|
9
|
-
// Apps
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
// und use-reference-lookup.
|
|
9
|
+
// Apps can override the defaults by changing these constants, but there is
|
|
10
|
+
// currently no per-feature or per-app override API; adding one would require
|
|
11
|
+
// extending createKumikoApp.
|
|
12
|
+
// The constants live in their own module so future authors can find them in
|
|
13
|
+
// one place instead of searching through render-field and use-reference-lookup.
|
|
15
14
|
export const REFERENCE_COMBOBOX_LIMIT = 50;
|
|
16
15
|
export const REFERENCE_LIST_LOOKUP_LIMIT = 200;
|
|
17
16
|
export const REFERENCE_SEARCH_DEBOUNCE_MS = 300;
|
|
@@ -3,33 +3,33 @@ export declare const NO_FILTER_URL_VALUE = "~";
|
|
|
3
3
|
export type ListSortDir = DataTableSortDir;
|
|
4
4
|
export type ListSort = DataTableSort;
|
|
5
5
|
export type ListUrlState = {
|
|
6
|
-
/**
|
|
6
|
+
/** Active sort (or null for unsorted; the server supplies the default order). */
|
|
7
7
|
readonly sort: ListSort | null;
|
|
8
|
-
/** Search
|
|
8
|
+
/** Search term (empty when unset). */
|
|
9
9
|
readonly q: string;
|
|
10
|
-
/** 1-
|
|
11
|
-
*
|
|
10
|
+
/** 1-based page number. For pagination="infinite" or false, this value is
|
|
11
|
+
* ignored; callers read it only when relevant. */
|
|
12
12
|
readonly page: number;
|
|
13
13
|
/** User-chosen page size from `PAGE_SIZE_OPTIONS`; undefined = screen default. */
|
|
14
14
|
readonly pageSize: number | undefined;
|
|
15
|
-
/**
|
|
16
|
-
*
|
|
15
|
+
/** Active faceted filters: field → selected values (strings; callers coerce
|
|
16
|
+
* them according to the field type). Empty when no filter is set. */
|
|
17
17
|
readonly filters: Readonly<Record<string, readonly string[]>>;
|
|
18
18
|
};
|
|
19
19
|
export type ListUrlStateApi = ListUrlState & {
|
|
20
|
-
/**
|
|
20
|
+
/** Atomically sets sort + dir. null means unsorted and clears both keys. */
|
|
21
21
|
readonly setSort: (next: ListSort | null) => void;
|
|
22
|
-
/**
|
|
23
|
-
*
|
|
22
|
+
/** Sets the search term. An empty string clears the key. Callers must debounce
|
|
23
|
+
* this themselves (e.g. with useDebouncedCallback in the search input component). */
|
|
24
24
|
readonly setQ: (next: string) => void;
|
|
25
|
-
/**
|
|
25
|
+
/** Sets the page. 1 or less clears the key and restores the default page. */
|
|
26
26
|
readonly setPage: (next: number) => void;
|
|
27
27
|
/** Sets the page size and resets the page; undefined clears the key. */
|
|
28
28
|
readonly setPageSize: (next: number | undefined) => void;
|
|
29
|
-
/**
|
|
30
|
-
*
|
|
29
|
+
/** Sets the selected values for a facet field. An empty array clears the key
|
|
30
|
+
* and resets the page, like sort/search changes. */
|
|
31
31
|
readonly setFilter: (field: string, values: readonly string[]) => void;
|
|
32
|
-
/**
|
|
32
|
+
/** Clears all active faceted filters (for a reset button). */
|
|
33
33
|
readonly clearFilters: () => void;
|
|
34
34
|
/** Sets both bounds of a dateRange facet in ONE params update — two
|
|
35
35
|
* setFilter calls would overwrite each other, since the second still sees
|
|
@@ -1,28 +1,29 @@
|
|
|
1
|
-
// useListUrlState —
|
|
2
|
-
// per-screen-id namespaced URL
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// useListUrlState — combines an entityList's sort/dir/page/q into a
|
|
2
|
+
// per-screen-id namespaced URL state. The screen-ID prefix prevents two
|
|
3
|
+
// lists on the same route (e.g. a dashboard with Orders + Incidents)
|
|
4
|
+
// from competing over the same query keys.
|
|
5
5
|
//
|
|
6
|
-
//
|
|
6
|
+
// Parameter schema per list:
|
|
7
7
|
// <screenId>.sort — field name (string)
|
|
8
8
|
// <screenId>.dir — "asc" | "desc"
|
|
9
9
|
// <screenId>.q — search term (URL-encoded)
|
|
10
|
-
// <screenId>.page — 1-based page number (
|
|
11
|
-
// <screenId>.f.<field> —
|
|
12
|
-
// NO_FILTER_URL_VALUE = "no filter", chosen
|
|
10
|
+
// <screenId>.page — 1-based page number (only for pagination="pages")
|
|
11
|
+
// <screenId>.f.<field> — faceted filter: comma-joined selected values;
|
|
12
|
+
// NO_FILTER_URL_VALUE = "no filter", chosen deliberately
|
|
13
13
|
// for a field that has a declared default
|
|
14
14
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
15
|
+
// Uses setSearchParams (replaceState, not push) so sort/filter toggles do not
|
|
16
|
+
// flood the browser history.
|
|
17
17
|
import { useCallback, useMemo } from "react";
|
|
18
18
|
import { useNav } from "../app/nav.js";
|
|
19
19
|
// A facet with a `defaultFilters` entry falls back to that default whenever
|
|
20
20
|
// its URL key is absent. Clearing it therefore has to leave a marker, or the
|
|
21
21
|
// default would come straight back.
|
|
22
22
|
export const NO_FILTER_URL_VALUE = "~";
|
|
23
|
-
// `.`
|
|
24
|
-
//
|
|
25
|
-
//
|
|
23
|
+
// `.` is used as a separator because it is readable (`?orders.sort=name`)
|
|
24
|
+
// and does not conflict with conventional field names (kebab- or camelCase
|
|
25
|
+
// without dots). The boot validator requires screen.id to contain no dots;
|
|
26
|
+
// see the boot-validator entityList section.
|
|
26
27
|
export const PAGE_SIZE_OPTIONS = [25, 50, 100];
|
|
27
28
|
function parsePageSize(value) {
|
|
28
29
|
if (value === undefined)
|
|
@@ -68,8 +69,8 @@ export function useListUrlState(screenId, defaultFilters) {
|
|
|
68
69
|
const field = k.slice(filterPrefix.length);
|
|
69
70
|
if (field === "")
|
|
70
71
|
continue;
|
|
71
|
-
// ponytail: comma-join —
|
|
72
|
-
//
|
|
72
|
+
// ponytail: comma-join — facet values are select options/booleans without
|
|
73
|
+
// commas; use repeated keys if comma-containing values are ever needed.
|
|
73
74
|
out[field] = v === NO_FILTER_URL_VALUE ? [] : v.split(",").filter((s) => s !== "");
|
|
74
75
|
}
|
|
75
76
|
for (const [field, values] of Object.entries(defaultFilters ?? {})) {
|
|
@@ -82,10 +83,9 @@ export function useListUrlState(screenId, defaultFilters) {
|
|
|
82
83
|
? NO_FILTER_URL_VALUE
|
|
83
84
|
: null, [defaultFilters]);
|
|
84
85
|
const setSort = useCallback((next) => {
|
|
85
|
-
//
|
|
86
|
-
//
|
|
87
|
-
//
|
|
88
|
-
// null — gleiche Logik.
|
|
86
|
+
// Atomically reset the page on every sort change; otherwise the user could
|
|
87
|
+
// land on page 5 of the old ordering, which is visually confusing. The
|
|
88
|
+
// page reset also applies when next is null for the same reason.
|
|
89
89
|
nav.setSearchParams({
|
|
90
90
|
[key(screenId, "sort")]: next === null ? null : next.field,
|
|
91
91
|
[key(screenId, "dir")]: next === null ? null : next.dir,
|
|
@@ -93,9 +93,8 @@ export function useListUrlState(screenId, defaultFilters) {
|
|
|
93
93
|
});
|
|
94
94
|
}, [nav, screenId]);
|
|
95
95
|
const setQ = useCallback((next) => {
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
// soll die Sortierung nicht zerlegen. Nur Page wird gereset.
|
|
96
|
+
// A search change resets the page but preserves the sort, so users can search
|
|
97
|
+
// while keeping the active ordering.
|
|
99
98
|
nav.setSearchParams({
|
|
100
99
|
[key(screenId, "q")]: next === "" ? null : next,
|
|
101
100
|
[key(screenId, "page")]: null,
|
package/dist/hooks/use-query.js
CHANGED
|
@@ -114,9 +114,9 @@ export function useQuery(type, payload, options = {}) {
|
|
|
114
114
|
}, refetchIntervalMs);
|
|
115
115
|
return () => clearInterval(timer);
|
|
116
116
|
}, [enabled, refetchIntervalMs, runFetch]);
|
|
117
|
-
// Live
|
|
118
|
-
//
|
|
119
|
-
//
|
|
117
|
+
// Live mode: listen for SSE events for the query entity and refetch.
|
|
118
|
+
// A separate effect ensures changes to `live` or `type` update the
|
|
119
|
+
// subscription lifecycle exactly once.
|
|
120
120
|
const subscribeLive = useLiveEvents();
|
|
121
121
|
useEffect(() => {
|
|
122
122
|
// skip: live mode or query disabled, no SSE subscription needed
|
|
@@ -13,10 +13,18 @@ export declare function referenceOptionSource(field: {
|
|
|
13
13
|
readonly queryQn: string;
|
|
14
14
|
readonly labelKey: string;
|
|
15
15
|
};
|
|
16
|
-
|
|
16
|
+
export declare function isCompleteRowPage(data: {
|
|
17
|
+
readonly nextCursor?: string | null;
|
|
18
|
+
} | null): boolean;
|
|
19
|
+
/** `map` holds the resolved labels; while the query loads or has failed an id is
|
|
20
|
+
* simply not in it. Callers decide the fallback: the raw id, or the "Unknown"
|
|
21
|
+
* label for entities listed in `UNRESOLVED_REFERENCE_LABEL_KEYS` once the
|
|
22
|
+
* lookup is complete.
|
|
17
23
|
* `featureName` is the reference's target feature, not the screen's. */
|
|
18
24
|
export declare function useReferenceLookup(featureName: string, refEntity: string, labelField: string): {
|
|
19
25
|
readonly map: ReferenceLookupMap;
|
|
20
26
|
readonly loading: boolean;
|
|
21
27
|
readonly failed: boolean;
|
|
28
|
+
/** False when the lookup page was capped: absent ids may still exist. */
|
|
29
|
+
readonly complete: boolean;
|
|
22
30
|
};
|
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
//
|
|
1
|
+
// Renderer-side eager load for reference fields.
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
// <refEntity>:list` (limit:200)
|
|
5
|
-
//
|
|
3
|
+
// Each reference column of a list calls `<feature>:query:
|
|
4
|
+
// <refEntity>:list` (limit:200) once, builds a Map<uuid, displayValue>,
|
|
5
|
+
// and the renderer uses it as the cell display renderer.
|
|
6
6
|
//
|
|
7
|
-
// Strategy-
|
|
8
|
-
// - Server-side Drizzle
|
|
9
|
-
// N+1)
|
|
10
|
-
//
|
|
11
|
-
// -
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
7
|
+
// Strategy trade-offs:
|
|
8
|
+
// - Server-side Drizzle joins would be more efficient (one query instead
|
|
9
|
+
// of N+1) but would have to extend the executor API with lookupTables.
|
|
10
|
+
// The renderer-side path wins for the MVP.
|
|
11
|
+
// - limit:200 is a hard UX limit: with more entries in the referenced
|
|
12
|
+
// entity, the last rows fall back to the raw id (or "Unknown" for
|
|
13
|
+
// opt-in entities once the lookup is complete). A searchable combobox plus a
|
|
14
|
+
// server-side eager load with an ID whitelist will lift that later.
|
|
15
15
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
16
|
+
// The hook is call-stable: useQuery's internal cache shares the lookup map
|
|
17
|
+
// between list and edit form for the same entity, and live updates arrive
|
|
18
|
+
// via SSE (use-query-live).
|
|
19
19
|
import { REFERENCE_LOOKUP_SOURCES, SYSTEM_REFERENCE_LABELS, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
20
20
|
import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-types/kms-adapter-types";
|
|
21
21
|
import { useMemo } from "react";
|
|
@@ -37,15 +37,23 @@ export function referenceLookupSource(refFeature, refEntity, labelField) {
|
|
|
37
37
|
}
|
|
38
38
|
// Where a reference field's option rows come from, and which of their keys
|
|
39
39
|
// holds the display text — the entity's own list handler plus `refLabelField`
|
|
40
|
-
// by default, or an author-supplied handler whose rows carry `label
|
|
41
|
-
//
|
|
40
|
+
// by default, or an author-supplied handler whose rows carry `label`,
|
|
41
|
+
// for targets with no readable column of their own.
|
|
42
42
|
export function referenceOptionSource(field, refFeature, refEntity) {
|
|
43
43
|
if (field.refOptionsQuery !== undefined) {
|
|
44
44
|
return { queryQn: field.refOptionsQuery, labelKey: "label" };
|
|
45
45
|
}
|
|
46
46
|
return referenceLookupSource(refFeature, refEntity, field.refLabelField ?? "id");
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
// A page with a continuation cursor is only a prefix of the referenced rows,
|
|
49
|
+
// so a missing id in it proves nothing about the row's existence.
|
|
50
|
+
export function isCompleteRowPage(data) {
|
|
51
|
+
return data !== null && (data.nextCursor ?? null) === null;
|
|
52
|
+
}
|
|
53
|
+
/** `map` holds the resolved labels; while the query loads or has failed an id is
|
|
54
|
+
* simply not in it. Callers decide the fallback: the raw id, or the "Unknown"
|
|
55
|
+
* label for entities listed in `UNRESOLVED_REFERENCE_LABEL_KEYS` once the
|
|
56
|
+
* lookup is complete.
|
|
49
57
|
* `featureName` is the reference's target feature, not the screen's. */
|
|
50
58
|
export function useReferenceLookup(featureName, refEntity, labelField) {
|
|
51
59
|
const { queryQn, labelKey } = referenceLookupSource(featureName, refEntity, labelField);
|
|
@@ -73,5 +81,10 @@ export function useReferenceLookup(featureName, refEntity, labelField) {
|
|
|
73
81
|
}
|
|
74
82
|
return out;
|
|
75
83
|
}, [result.data, labelKey, systemLabel, translate]);
|
|
76
|
-
return {
|
|
84
|
+
return {
|
|
85
|
+
map,
|
|
86
|
+
loading: result.loading,
|
|
87
|
+
failed: result.error !== null,
|
|
88
|
+
complete: isCompleteRowPage(result.data),
|
|
89
|
+
};
|
|
77
90
|
}
|
package/dist/hooks/use-store.js
CHANGED
|
@@ -1,37 +1,33 @@
|
|
|
1
1
|
import { useRef, useSyncExternalStore } from "react";
|
|
2
2
|
// React bindings for Kumiko's store primitive (`createStore` from
|
|
3
3
|
// @cosmicdrift/kumiko-renderer/headless). Both hooks are thin wrappers around React's
|
|
4
|
-
// useSyncExternalStore — the
|
|
5
|
-
// store itself
|
|
4
|
+
// useSyncExternalStore — the entire subscribe/notify mechanism lives in the
|
|
5
|
+
// store itself; only React wiring happens here.
|
|
6
6
|
//
|
|
7
|
-
// useStore: returns the whole snapshot
|
|
8
|
-
// its identity. The Object.is gate in the store
|
|
9
|
-
// updates.
|
|
7
|
+
// useStore: returns the whole snapshot and re-renders when the store changes
|
|
8
|
+
// its identity. The Object.is gate in the store prevents no-op updates.
|
|
10
9
|
//
|
|
11
|
-
// useStoreSelector:
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
// Identität zurück, kein Re-Render.
|
|
10
|
+
// useStoreSelector: returns a derived view. Selectors that return object or
|
|
11
|
+
// array literals (e.g. `s => ({ a, b })`) create a new identity on every
|
|
12
|
+
// render — useSyncExternalStore would interpret this as a change and enter
|
|
13
|
+
// a re-render loop. The `equals` argument handles this case: when the
|
|
14
|
+
// selection is equal, the cached identity is returned and no re-render occurs.
|
|
17
15
|
// @wrapper-known semantic-alias
|
|
18
16
|
export function useStore(store) {
|
|
19
17
|
return useSyncExternalStore(store.subscribe, store.getSnapshot, store.getSnapshot);
|
|
20
18
|
}
|
|
21
19
|
export function useStoreSelector(store, select, equals = Object.is) {
|
|
22
|
-
// Cache
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
const lastSelected = useRef(
|
|
27
|
-
const initialized = useRef(false);
|
|
20
|
+
// Cache of the last selection. On the next getSnapshot call, the wrapper
|
|
21
|
+
// compares the newly computed selection with the cache and returns the
|
|
22
|
+
// cached identity when they are equal. This keeps selector results such as
|
|
23
|
+
// `s => ({ a, b })` stable.
|
|
24
|
+
const lastSelected = useRef(null);
|
|
28
25
|
const getSelectedSnapshot = () => {
|
|
29
26
|
const next = select(store.getSnapshot());
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
lastSelected.current = next;
|
|
34
|
-
initialized.current = true;
|
|
27
|
+
const cached = lastSelected.current;
|
|
28
|
+
if (cached !== null && equals(cached.value, next))
|
|
29
|
+
return cached.value;
|
|
30
|
+
lastSelected.current = { value: next };
|
|
35
31
|
return next;
|
|
36
32
|
};
|
|
37
33
|
return useSyncExternalStore(store.subscribe, getSelectedSnapshot, getSelectedSnapshot);
|
|
@@ -80,10 +80,12 @@ export function useStreamHandler(type, payload = {}, options = {}) {
|
|
|
80
80
|
}, []);
|
|
81
81
|
return { chunks, status, error, start, abort, reset };
|
|
82
82
|
}
|
|
83
|
+
function isDispatcherError(e) {
|
|
84
|
+
return !!e && typeof e === "object" && "code" in e && "message" in e && "i18nKey" in e;
|
|
85
|
+
}
|
|
83
86
|
function asDispatcherError(e) {
|
|
84
|
-
if (e
|
|
87
|
+
if (isDispatcherError(e))
|
|
85
88
|
return e;
|
|
86
|
-
}
|
|
87
89
|
return {
|
|
88
90
|
code: "stream_error",
|
|
89
91
|
httpStatus: 0,
|
package/dist/i18n-defaults.js
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
|
-
// Framework
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// einzelne Keys via clientFeatures.translations überschreiben.
|
|
1
|
+
// Framework default bundle. These strings are required by renderer components (Save/Cancel/Delete
|
|
2
|
+
// buttons, empty states, search placeholder, navigation-toggle ARIA labels, and validation
|
|
3
|
+
// reasons). createKumikoApp adds this as the last fallback in LocaleProvider, while apps can
|
|
4
|
+
// override individual keys via clientFeatures.translations.
|
|
6
5
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// list / nav / form / validation).
|
|
6
|
+
// All keys use the `kumiko.` prefix to avoid collisions with app keys. Subpaths group keys by area
|
|
7
|
+
// (actions / list / nav / form / validation).
|
|
10
8
|
export const kumikoDefaultTranslations = {
|
|
11
9
|
en: {
|
|
12
10
|
"kumiko.actions.save": "Save",
|
package/dist/i18n.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ import type { LocaleResolver } from "./headless/index.ts";
|
|
|
5
5
|
* placeholders (same semantics as i18next t); plural entries resolve via
|
|
6
6
|
* `resolveTranslationValue`. */
|
|
7
7
|
export type TranslationBundle = Readonly<Record<string, TranslationValue>>;
|
|
8
|
-
/** Map
|
|
8
|
+
/** Map from locale code (BCP-47, e.g. `"de"`, `"en-US"`) → bundle. */
|
|
9
9
|
export type TranslationsByLocale = Readonly<Record<string, TranslationBundle>>;
|
|
10
10
|
/** Key-first shape for `r.translations({ keys })` — each key maps locale → TranslationValue. */
|
|
11
11
|
export type TranslationsByKey = Readonly<Record<string, Readonly<Record<string, TranslationValue>>>>;
|
|
@@ -14,9 +14,9 @@ export declare function translationsByLocaleFromKeys(source: TranslationsByKey):
|
|
|
14
14
|
/** Inverse of `translationsByLocaleFromKeys`: pivot locale-first bundles to the key-first
|
|
15
15
|
* shape `r.translations({ keys })` expects, so one source feeds server and client. */
|
|
16
16
|
export declare function translationsByKeyFromLocales(source: TranslationsByLocale): TranslationsByKey;
|
|
17
|
-
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
/** Merges two TranslationsByLocale maps — the override wins per key,
|
|
18
|
+
* and the locales are combined. Standard building block for client
|
|
19
|
+
* plugins that layer app overrides over their default bundles. */
|
|
20
20
|
export declare function mergeTranslations(base: TranslationsByLocale, override: TranslationsByLocale): TranslationsByLocale;
|
|
21
21
|
export type LocaleProviderProps = {
|
|
22
22
|
readonly resolver: LocaleResolver;
|
|
@@ -31,9 +31,9 @@ export type LocaleProviderProps = {
|
|
|
31
31
|
readonly children: ReactNode;
|
|
32
32
|
};
|
|
33
33
|
export declare function LocaleProvider({ resolver, fallbackBundles, fallbackLocale, children, }: LocaleProviderProps): ReactNode;
|
|
34
|
-
/**
|
|
35
|
-
*
|
|
36
|
-
*
|
|
34
|
+
/** Provides the current LocaleResolver and automatically subscribes to
|
|
35
|
+
* locale changes — the calling component re-renders when the language
|
|
36
|
+
* changes. Throws when there is no provider in the tree. */
|
|
37
37
|
export declare function useLocale(): LocaleResolver;
|
|
38
38
|
/** `t("key", params)` resolves in this order:
|
|
39
39
|
* 1. App resolver (e.g. i18next)
|
|
@@ -58,9 +58,9 @@ export declare function useOptionalLocale(): string | undefined;
|
|
|
58
58
|
export declare function useOptionalTimeZone(): string;
|
|
59
59
|
/** Non-throwing translate — undefined outside LocaleProvider. */
|
|
60
60
|
export declare function useOptionalTranslation(): ((key: string, params?: Readonly<Record<string, unknown>>) => string) | undefined;
|
|
61
|
-
/** Default
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
/** Default resolver for apps without their own i18n layer. Returns every key
|
|
62
|
+
* unchanged — the plugin fallback bundles then provide the actual
|
|
63
|
+
* translation. Also useful as a basis for tests. */
|
|
64
64
|
export declare function createStaticLocaleResolver(options?: {
|
|
65
65
|
readonly locale?: string;
|
|
66
66
|
readonly timeZone?: string;
|
package/dist/i18n.js
CHANGED
|
@@ -39,9 +39,9 @@ export function translationsByKeyFromLocales(source) {
|
|
|
39
39
|
}
|
|
40
40
|
return out;
|
|
41
41
|
}
|
|
42
|
-
/**
|
|
43
|
-
*
|
|
44
|
-
*
|
|
42
|
+
/** Merges two TranslationsByLocale maps — the override wins per key,
|
|
43
|
+
* and the locales are combined. Standard building block for client
|
|
44
|
+
* plugins that layer app overrides over their default bundles. */
|
|
45
45
|
export function mergeTranslations(base, override) {
|
|
46
46
|
const locales = new Set([...Object.keys(base), ...Object.keys(override)]);
|
|
47
47
|
const merged = {};
|
|
@@ -52,10 +52,10 @@ export function mergeTranslations(base, override) {
|
|
|
52
52
|
}
|
|
53
53
|
const LocaleContext = createContext(undefined);
|
|
54
54
|
const FormalityContext = createContext("informal");
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
// useMemo
|
|
58
|
-
//
|
|
55
|
+
// Stable reference instead of `fallbackBundles = []` as a default parameter:
|
|
56
|
+
// a literal default is allocated on EVERY call and would invalidate the
|
|
57
|
+
// useMemo reference check in the provider below whenever the caller omits
|
|
58
|
+
// fallbackBundles.
|
|
59
59
|
const EMPTY_FALLBACK_BUNDLES = [];
|
|
60
60
|
export function LocaleProvider({ resolver, fallbackBundles = EMPTY_FALLBACK_BUNDLES, fallbackLocale = "en", children, }) {
|
|
61
61
|
// Without memoization every re-render of the provider (e.g. because an
|
|
@@ -67,16 +67,16 @@ export function LocaleProvider({ resolver, fallbackBundles = EMPTY_FALLBACK_BUND
|
|
|
67
67
|
const value = useMemo(() => ({ resolver, fallbackBundles, fallbackLocale }), [resolver, fallbackBundles, fallbackLocale]);
|
|
68
68
|
return _jsx(LocaleContext.Provider, { value: value, children: children });
|
|
69
69
|
}
|
|
70
|
-
/**
|
|
71
|
-
*
|
|
72
|
-
*
|
|
70
|
+
/** Provides the current LocaleResolver and automatically subscribes to
|
|
71
|
+
* locale changes — the calling component re-renders when the language
|
|
72
|
+
* changes. Throws when there is no provider in the tree. */
|
|
73
73
|
export function useLocale() {
|
|
74
74
|
const ctx = useContext(LocaleContext);
|
|
75
75
|
if (ctx === undefined) {
|
|
76
76
|
throw new Error("useLocale must be used inside <LocaleProvider>");
|
|
77
77
|
}
|
|
78
|
-
// Subscribe + current locale
|
|
79
|
-
//
|
|
78
|
+
// Subscribe + current locale snapshot. We do not need the return value
|
|
79
|
+
// itself — only the re-render trigger matters.
|
|
80
80
|
const getLocaleSnapshot = () => ctx.resolver.locale();
|
|
81
81
|
useSyncExternalStore(ctx.resolver.subscribe, getLocaleSnapshot, getLocaleSnapshot);
|
|
82
82
|
return ctx.resolver;
|
|
@@ -93,16 +93,16 @@ export function useTranslation() {
|
|
|
93
93
|
if (ctx === undefined) {
|
|
94
94
|
throw new Error("useTranslation must be used inside <LocaleProvider>");
|
|
95
95
|
}
|
|
96
|
-
// Re-
|
|
97
|
-
//
|
|
98
|
-
//
|
|
96
|
+
// Re-render on language changes. `ctx.resolver.subscribe` is already
|
|
97
|
+
// a stable reference from the resolver, so no separate subscription
|
|
98
|
+
// callback memoization is needed here.
|
|
99
99
|
const getLocaleSnapshot = () => ctx.resolver.locale();
|
|
100
100
|
const locale = useSyncExternalStore(ctx.resolver.subscribe, getLocaleSnapshot, getLocaleSnapshot);
|
|
101
|
-
// `t`
|
|
102
|
-
//
|
|
103
|
-
// (
|
|
104
|
-
//
|
|
105
|
-
//
|
|
101
|
+
// `t` MUST remain referentially stable while the resolver/bundles/locale
|
|
102
|
+
// do not change — consumers regularly use `t` in useEffect dependencies
|
|
103
|
+
// (e.g. to reload queries when the language changes). Creating a new `t`
|
|
104
|
+
// on every render would otherwise cause a render/effect infinite loop
|
|
105
|
+
// (see the admin-shell Overview screens, production incident).
|
|
106
106
|
const formality = useContext(FormalityContext);
|
|
107
107
|
return useCallback((key, params) => translateWithFallbacks(ctx, locale, formality, key, params), [ctx, locale, formality]);
|
|
108
108
|
}
|
|
@@ -165,9 +165,9 @@ export function useOptionalTranslation() {
|
|
|
165
165
|
const t = useCallback((key, params) => ctx === undefined ? key : translateWithFallbacks(ctx, locale, formality, key, params), [ctx, locale, formality]);
|
|
166
166
|
return ctx === undefined ? undefined : t;
|
|
167
167
|
}
|
|
168
|
-
/** Default
|
|
169
|
-
*
|
|
170
|
-
*
|
|
168
|
+
/** Default resolver for apps without their own i18n layer. Returns every key
|
|
169
|
+
* unchanged — the plugin fallback bundles then provide the actual
|
|
170
|
+
* translation. Also useful as a basis for tests. */
|
|
171
171
|
export function createStaticLocaleResolver(options = {}) {
|
|
172
172
|
const locale = options.locale ?? "en";
|
|
173
173
|
const timeZone = options.timeZone ?? "UTC";
|
|
@@ -175,8 +175,8 @@ export function createStaticLocaleResolver(options = {}) {
|
|
|
175
175
|
translate: (key) => key,
|
|
176
176
|
locale: () => locale,
|
|
177
177
|
timeZone: () => timeZone,
|
|
178
|
-
// No-op subscribe:
|
|
179
|
-
//
|
|
178
|
+
// No-op subscribe: our locale is static, so no change event ever occurs.
|
|
179
|
+
// Unsubscribe is also a no-op.
|
|
180
180
|
subscribe: () => () => { },
|
|
181
181
|
};
|
|
182
182
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,7 @@ export type { CustomScreensMap, CustomScreensProviderProps } from "./app/custom-
|
|
|
14
14
|
export { CustomScreensProvider, useCustomScreenComponent } from "./app/custom-screens.tsx";
|
|
15
15
|
export type { DashboardBodyProps, DashboardBodyProviderProps } from "./app/dashboard-body.tsx";
|
|
16
16
|
export { DashboardBodyProvider, useDashboardBody } from "./app/dashboard-body.tsx";
|
|
17
|
+
export { type TenantCurrencyResolution, useTenantCurrency } from "./app/entity-edit-prefill.ts";
|
|
17
18
|
export { type ErrorNotice, ErrorReference, errorNoticeOf } from "./app/error-reference.tsx";
|
|
18
19
|
export type { ExtensionFormRegistry, ExtensionFormSubmitHandler, ExtensionSubmitContext, ExtensionSubmitResult, } from "./app/extension-form-submit.tsx";
|
|
19
20
|
export { ExtensionFormRegistryProvider, useExtensionFormSubmit, } from "./app/extension-form-submit.tsx";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
// Platform-agnostic React renderer for Kumiko screens.
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Platform-agnostic React renderer for Kumiko screens. This is the
|
|
2
|
+
// shared layer for components, hooks, contexts, and types. Web DOM
|
|
3
|
+
// implementations live under ./web and provide their concrete
|
|
4
|
+
// primitives, navigation, and SSE implementations through providers
|
|
5
|
+
// in this tree.
|
|
5
6
|
//
|
|
6
7
|
// Direct consumers of this layer: platform bindings
|
|
7
8
|
// (@cosmicdrift/kumiko-renderer/web) or an app writing its own bootstrap.
|
|
@@ -16,6 +17,7 @@ export { CONTENT_EDITOR_ELEMENT_ID, ContentEditorsProvider, TextareaContentEdito
|
|
|
16
17
|
export { ContentPreview, substituteVariables } from "./app/content-preview.js";
|
|
17
18
|
export { CustomScreensProvider, useCustomScreenComponent } from "./app/custom-screens.js";
|
|
18
19
|
export { DashboardBodyProvider, useDashboardBody } from "./app/dashboard-body.js";
|
|
20
|
+
export { useTenantCurrency } from "./app/entity-edit-prefill.js";
|
|
19
21
|
export { ErrorReference, errorNoticeOf } from "./app/error-reference.js";
|
|
20
22
|
export { ExtensionFormRegistryProvider, useExtensionFormSubmit, } from "./app/extension-form-submit.js";
|
|
21
23
|
export { ExtensionSectionsProvider, extensionSectionName, useExtensionSectionComponent, } from "./app/extension-sections.js";
|
|
@@ -89,7 +89,7 @@ export function createLiveDispatcher(options = {}) {
|
|
|
89
89
|
return {
|
|
90
90
|
async write(type, payload, opts) {
|
|
91
91
|
const body = { type, payload };
|
|
92
|
-
// Idempotency by default
|
|
92
|
+
// Idempotency by default: without a requestId the server-side
|
|
93
93
|
// dedup never engages and a transport-level double-send duplicates
|
|
94
94
|
// events. Callers with a logical-submit id (savable queue, form
|
|
95
95
|
// controllers) pass their own and keep it across their retries.
|
|
@@ -106,7 +106,7 @@ export function createLiveDispatcher(options = {}) {
|
|
|
106
106
|
const body = { commands };
|
|
107
107
|
// One id for the whole batch — the server caches the BatchResult
|
|
108
108
|
// under it, so a retried batch returns the cached outcome instead of
|
|
109
|
-
// re-executing the commands
|
|
109
|
+
// re-executing the commands.
|
|
110
110
|
body["requestId"] = opts?.requestId ?? generateRequestId();
|
|
111
111
|
const call = await callJson(PATH_BATCH, body, opts?.signal);
|
|
112
112
|
return normalizeBatchResponse(call);
|