@cosmicdrift/kumiko-renderer 0.386.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 +1 -1
- 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 +42 -43
- 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
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Currency decimal places — covers the world's zero- and three-decimal
|
|
2
2
|
// outliers, defaults to 2 for everything else. Shared between renderer/web's
|
|
3
3
|
// MoneyInput (minor-unit display factor) and RenderField's minor/major-unit
|
|
4
|
-
// conversion
|
|
4
|
+
// conversion — a single source keeps both sides
|
|
5
5
|
// scaling the same currency the same way.
|
|
6
6
|
export function currencyDecimals(code) {
|
|
7
7
|
if (code === "JPY" || code === "KRW" || code === "VND" || code === "ISK")
|
package/dist/headless/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
// Re-
|
|
2
|
-
//
|
|
1
|
+
// Re-exports from framework/ui-types so renderer code uses the same
|
|
2
|
+
// parser as the server boot validator (cross-feature references).
|
|
3
3
|
export { parseRefTarget } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
4
4
|
export { generateRequestId, StreamFrame } from "./dispatcher/index.js";
|
|
5
5
|
export { createFormController, groupIssuesByPath, I18N_KEY_PARAM, relevantFieldIssues, zodErrorToFieldIssues, } from "./form/index.js";
|
|
@@ -52,7 +52,7 @@ function wasEmptiedByWriteExclusion(declaredFieldSpecs, remainingFields) {
|
|
|
52
52
|
return declaredFieldSpecs.length > 0 && remainingFields.length === 0;
|
|
53
53
|
}
|
|
54
54
|
// relatedList runs its own query — nothing here to resolve against
|
|
55
|
-
// entity/values, the spec passes through verbatim
|
|
55
|
+
// entity/values, the spec passes through verbatim.
|
|
56
56
|
export function computeRelatedListSectionViewModel(sectionSpec, translate) {
|
|
57
57
|
return {
|
|
58
58
|
kind: "relatedList",
|
|
@@ -461,7 +461,7 @@ export function computeEditViewModel(input) {
|
|
|
461
461
|
const visible = hasVisibleContent(fields, sectionSpec.actions, values);
|
|
462
462
|
// Tabs mode renders this section as its own panel, where two columns reads
|
|
463
463
|
// better than the single-column default a stacked form keeps. Applies to
|
|
464
|
-
// entityEdit tabs as well
|
|
464
|
+
// entityEdit tabs as well, not just projectionDetail.
|
|
465
465
|
const defaultColumns = screen.layout.mode === "tabs" ? 2 : 1;
|
|
466
466
|
const groups = ("groups" in sectionSpec ? sectionSpec.groups : undefined)
|
|
467
467
|
?.map((group) => ({ ...group, fields: group.fields.filter((f) => !isHiddenByExclusion(f)) }))
|
|
@@ -108,10 +108,9 @@ export function computeListViewModel(input) {
|
|
|
108
108
|
continue;
|
|
109
109
|
}
|
|
110
110
|
const label = translate(normalized.label ?? fieldLabelKey(featureName, screen.entity, normalized.field));
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
//
|
|
114
|
-
// Renderer baut die Lookup-QN als
|
|
111
|
+
// Reference fields may target the same feature ("user") or another feature
|
|
112
|
+
// ("users:user"). parseRefTarget returns the feature and entity names, which
|
|
113
|
+
// the renderer uses to build the lookup query as
|
|
115
114
|
// `<refFeature>:query:<refEntity>:list`.
|
|
116
115
|
const refRaw = fieldDef.type === "reference" ? fieldDef.entity : undefined;
|
|
117
116
|
const refTarget = refRaw !== undefined ? parseRefTarget(refRaw, featureName) : undefined;
|
|
@@ -191,7 +191,7 @@ export type EditFieldViewModel = {
|
|
|
191
191
|
* entity appears as the display label in the dropdown. Default: "id". */
|
|
192
192
|
readonly refLabelField?: string;
|
|
193
193
|
/** Only for `type: "reference"` — query handler QN the picker sources its
|
|
194
|
-
* options from
|
|
194
|
+
* options from, replacing the referenced entity's list handler.
|
|
195
195
|
* Rows carry `{ id, label }`; `refLabelField` is then unused for the
|
|
196
196
|
* picker but still drives list cells. */
|
|
197
197
|
readonly refOptionsQuery?: string;
|
|
@@ -268,7 +268,7 @@ export type EditFieldViewModel = {
|
|
|
268
268
|
/** Only set for a plain (non-list, non-embedded) `type: "money"` field —
|
|
269
269
|
* same source as embeddedListCurrency (EntityDefinition.defaultCurrency,
|
|
270
270
|
* falls back to "EUR"). The renderer needs this to build the payload's
|
|
271
|
-
* `{amount, currency}` shape
|
|
271
|
+
* `{amount, currency}` shape. */
|
|
272
272
|
readonly currency?: string;
|
|
273
273
|
/** Only set for `type: "number"` when NumberFieldDef.unit is set — a static
|
|
274
274
|
* suffix string, or a sibling-field reference the renderer resolves against
|
|
@@ -292,7 +292,7 @@ export type EditFieldsSectionViewModel = {
|
|
|
292
292
|
* `excludeFields`. Unlike a declared-empty section (review step), the
|
|
293
293
|
* renderer must not give it a tab or wizard step. */
|
|
294
294
|
readonly emptiedByWriteExclusion?: boolean;
|
|
295
|
-
/** Optional —
|
|
295
|
+
/** Optional — a titleless section renders only its fields (flat form). */
|
|
296
296
|
readonly title?: string;
|
|
297
297
|
/** Translated help text under the block heading, from
|
|
298
298
|
* `EditFieldsSection.description`. Renders even without `title`. */
|
|
@@ -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
|
|
@@ -1,21 +1,20 @@
|
|
|
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
|
-
// ID-Whitelist heben das später auf.
|
|
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 show only UUIDs. A searchable combobox plus a
|
|
13
|
+
// server-side eager load with an ID whitelist will lift that later.
|
|
15
14
|
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
15
|
+
// The hook is call-stable: useQuery's internal cache shares the lookup map
|
|
16
|
+
// between list and edit form for the same entity, and live updates arrive
|
|
17
|
+
// via SSE (use-query-live).
|
|
19
18
|
import { REFERENCE_LOOKUP_SOURCES, SYSTEM_REFERENCE_LABELS, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
20
19
|
import { PII_ERASED_SENTINEL } from "@cosmicdrift/kumiko-types/kms-adapter-types";
|
|
21
20
|
import { useMemo } from "react";
|
|
@@ -37,8 +36,8 @@ export function referenceLookupSource(refFeature, refEntity, labelField) {
|
|
|
37
36
|
}
|
|
38
37
|
// Where a reference field's option rows come from, and which of their keys
|
|
39
38
|
// 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
|
-
//
|
|
39
|
+
// by default, or an author-supplied handler whose rows carry `label`,
|
|
40
|
+
// for targets with no readable column of their own.
|
|
42
41
|
export function referenceOptionSource(field, refFeature, refEntity) {
|
|
43
42
|
if (field.refOptionsQuery !== undefined) {
|
|
44
43
|
return { queryQn: field.refOptionsQuery, labelKey: "label" };
|
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,6 @@
|
|
|
1
|
-
// Framework
|
|
2
|
-
// brauchen (Save/Cancel/Delete-Buttons, Empty-States, Search-Placeholder,
|
|
3
|
-
// Nav-Toggle-aria-Labels, Validation-Reasons). createKumikoApp hängt das
|
|
4
|
-
// als ALLERLETZTEN Fallback in den LocaleProvider — Apps können
|
|
5
|
-
// einzelne Keys via clientFeatures.translations überschreiben.
|
|
1
|
+
// Framework default bundle. These strings are required by renderer components (Save/Cancel/Delete buttons, empty states, search placeholder, navigation-toggle ARIA labels, and validation reasons). createKumikoApp adds this as the last fallback in LocaleProvider, while apps can override individual keys via clientFeatures.translations.
|
|
6
2
|
//
|
|
7
|
-
//
|
|
8
|
-
// App-Keys kollidieren. Sub-Pfade gruppieren nach Bereich (actions /
|
|
9
|
-
// list / nav / form / validation).
|
|
3
|
+
// All keys use the `kumiko.` prefix to avoid collisions with app keys. Subpaths group keys by area (actions / list / nav / form / validation).
|
|
10
4
|
export const kumikoDefaultTranslations = {
|
|
11
5
|
en: {
|
|
12
6
|
"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.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.
|
|
@@ -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);
|