@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
|
@@ -5,8 +5,8 @@ import { useMemo } from "react";
|
|
|
5
5
|
import { rendererWarn } from "../dev-console.js";
|
|
6
6
|
import { useQuery } from "../hooks/use-query.js";
|
|
7
7
|
// `defaultCurrency` is entityEdit-only — it enables the `{amount, currency}`
|
|
8
|
-
// write shape
|
|
9
|
-
// `moneyCurrencyOverrides`
|
|
8
|
+
// write shape; config-edit's plain-number contract needs bare `0`.
|
|
9
|
+
// `moneyCurrencyOverrides` replaces `defaultCurrency` per field name
|
|
10
10
|
// — used for a `money` field that declares `currency: { kind: "tenant" }`, so
|
|
11
11
|
// its empty initial value carries the tenant's own currency instead of the
|
|
12
12
|
// entity-wide default.
|
|
@@ -51,7 +51,7 @@ export function buildInitialValues(fields, defaultCurrency, moneyCurrencyOverrid
|
|
|
51
51
|
return out;
|
|
52
52
|
}
|
|
53
53
|
// A `money` field opts a currently-empty value into the tenant-settings
|
|
54
|
-
// bundle's per-tenant currency
|
|
54
|
+
// bundle's per-tenant currency via `currency: { kind: "tenant" }`
|
|
55
55
|
// on its FieldDefinition. Read structurally — renderer only depends on the
|
|
56
56
|
// client-safe FieldDefinition subset, not the concrete MoneyFieldDef/
|
|
57
57
|
// MoneyCurrencySource types, same idiom as the other field-shape narrowings
|
|
@@ -68,11 +68,11 @@ export function tenantCurrencyMoneyFieldNames(fields) {
|
|
|
68
68
|
}
|
|
69
69
|
// Resolves what a tenant-declared money field's currency should be, from the
|
|
70
70
|
// same `config:query:values` query ConfigEditBody already uses. Never falls
|
|
71
|
-
// back to "EUR" while the query is genuinely still in flight
|
|
71
|
+
// back to "EUR" while the query is genuinely still in flight — a
|
|
72
72
|
// screen with no tenant-declared money field, or a query that errored or
|
|
73
73
|
// returned no value for the key, resolves immediately instead of blocking.
|
|
74
|
-
function resolveTenantCurrency(
|
|
75
|
-
if (
|
|
74
|
+
function resolveTenantCurrency(needed, query, fallback) {
|
|
75
|
+
if (!needed)
|
|
76
76
|
return { status: "not-needed" };
|
|
77
77
|
if (query.error) {
|
|
78
78
|
return fallback === undefined
|
|
@@ -88,8 +88,14 @@ function resolveTenantCurrency(fieldNames, query, fallback) {
|
|
|
88
88
|
? { status: "unresolved" }
|
|
89
89
|
: { status: "ready", currency: fallback };
|
|
90
90
|
}
|
|
91
|
-
//
|
|
92
|
-
//
|
|
91
|
+
// `needed = false` skips the config query entirely; an errored query or a missing value
|
|
92
|
+
// resolves to `fallback` (or `unresolved` without one).
|
|
93
|
+
export function useTenantCurrency(needed, fallback) {
|
|
94
|
+
const query = useQuery("config:query:values", { keys: [TENANT_CURRENCY_CONFIG_KEY] }, { enabled: needed });
|
|
95
|
+
return resolveTenantCurrency(needed, query, fallback);
|
|
96
|
+
}
|
|
97
|
+
// A money field naming a fixed ISO code (`currency: { kind: "literal", code }`)
|
|
98
|
+
// resolves with no query at all. Read structurally, same idiom as
|
|
93
99
|
// tenantCurrencyMoneyFieldNames above.
|
|
94
100
|
export function literalCurrencyOverrides(fields) {
|
|
95
101
|
const out = {};
|
|
@@ -105,9 +111,9 @@ export function literalCurrencyOverrides(fields) {
|
|
|
105
111
|
return out;
|
|
106
112
|
}
|
|
107
113
|
// Per-field currency for the money fields that declare their own source: a
|
|
108
|
-
// literal code resolves synchronously
|
|
114
|
+
// literal code resolves synchronously, a `{ kind: "tenant" }` field
|
|
109
115
|
// needs the tenant-settings config query and makes the caller hold the form
|
|
110
|
-
// until it lands
|
|
116
|
+
// until it lands — seeding the fallback meanwhile would render, and
|
|
111
117
|
// on a fast click submit, the wrong currency. Callers pass the tenant field
|
|
112
118
|
// names themselves because an edit form only needs the fetch for fields the
|
|
113
119
|
// record has no stored value for.
|
|
@@ -115,8 +121,7 @@ export function useMoneyCurrencyOverrides(fields, tenantFieldNames,
|
|
|
115
121
|
// `undefined` = no safe last resort (entity-less forms): an unresolvable
|
|
116
122
|
// tenant currency is reported via `unresolved` instead of guessing a code.
|
|
117
123
|
fallback) {
|
|
118
|
-
const
|
|
119
|
-
const resolution = resolveTenantCurrency(tenantFieldNames, query, fallback);
|
|
124
|
+
const resolution = useTenantCurrency(tenantFieldNames.length > 0, fallback);
|
|
120
125
|
const resolvedTenantCurrency = resolution.status === "ready" ? resolution.currency : undefined;
|
|
121
126
|
const overrides = useMemo(() => {
|
|
122
127
|
const out = { ...literalCurrencyOverrides(fields) };
|
|
@@ -164,7 +169,7 @@ function warnPrefillParam(message) {
|
|
|
164
169
|
rendererWarn(`[kumiko] ${message}`);
|
|
165
170
|
}
|
|
166
171
|
// An actionForm has no entity and thus no defaultCurrency, so a bare-number
|
|
167
|
-
// prefill would stay a number the handler's zod schema rejects
|
|
172
|
+
// prefill would stay a number the handler's zod schema rejects.
|
|
168
173
|
function coerceMoneyValue(raw, fieldName, defaultCurrency) {
|
|
169
174
|
if (raw === null || raw === undefined)
|
|
170
175
|
return undefined;
|
|
@@ -52,7 +52,7 @@ function EntityEditCreateBody({ schema, screen, entity, translate, onSaved, }) {
|
|
|
52
52
|
// returnTo would fight that.
|
|
53
53
|
const returnTarget = screen.singleton === true ? undefined : returnTargetParam;
|
|
54
54
|
const entityDefaultCurrency = entity.defaultCurrency ?? "EUR";
|
|
55
|
-
// A money field declaring `currency: { kind: "tenant" }`
|
|
55
|
+
// A money field declaring `currency: { kind: "tenant" }` resolves
|
|
56
56
|
// its empty-value currency from the tenant-settings config key instead of
|
|
57
57
|
// entityDefaultCurrency — `enabled` keeps the query a no-op for entities
|
|
58
58
|
// without any such field.
|
|
@@ -117,7 +117,7 @@ function EntityEditCreateBody({ schema, screen, entity, translate, onSaved, }) {
|
|
|
117
117
|
}
|
|
118
118
|
}, [nav, screen.redirect, schema, appFeatures, navigateToList, onSaved, returnTarget]);
|
|
119
119
|
// Never seed a tenant-declared money field with entityDefaultCurrency while
|
|
120
|
-
// its real tenant currency is still in flight
|
|
120
|
+
// its real tenant currency is still in flight — wait instead.
|
|
121
121
|
if (currencyLoading) {
|
|
122
122
|
return _jsx(QueryLoadingBanner, {});
|
|
123
123
|
}
|
|
@@ -172,7 +172,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
172
172
|
// error and the user reloads.
|
|
173
173
|
const recordVersion = versionOf(record);
|
|
174
174
|
const entityDefaultCurrency = entity.defaultCurrency ?? "EUR";
|
|
175
|
-
// A money field declaring `currency: { kind: "tenant" }`
|
|
175
|
+
// A money field declaring `currency: { kind: "tenant" }` resolves
|
|
176
176
|
// its empty-value currency from the tenant-settings config key instead of
|
|
177
177
|
// entityDefaultCurrency — `enabled` keeps the query a no-op for entities
|
|
178
178
|
// without any such field. A record's own stored value (below) always wins
|
|
@@ -333,7 +333,7 @@ function EntityEditUpdateForm({ schema, screen, entity, entityId, record, onRelo
|
|
|
333
333
|
return undefined;
|
|
334
334
|
}, [dispatcher, deleteCommand, entityId, navigateToList, onDeleted, returnTarget, nav]);
|
|
335
335
|
// Never seed a tenant-declared money field with entityDefaultCurrency while
|
|
336
|
-
// its real tenant currency is still in flight
|
|
336
|
+
// its real tenant currency is still in flight — wait instead. A
|
|
337
337
|
// stored (non-empty) value is unaffected — it never reaches this branch's
|
|
338
338
|
// fallback because `initial` above already prefers `record[name]`.
|
|
339
339
|
if (currencyLoading) {
|
|
@@ -298,7 +298,7 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
298
298
|
// drift apart.
|
|
299
299
|
const runNavigate = useCallback((action, row) => {
|
|
300
300
|
if (action.entity !== undefined) {
|
|
301
|
-
// Entity targets
|
|
301
|
+
// Entity targets only resolve in the NavApi implementation
|
|
302
302
|
// against ALL features (resolveTarget in renderer/web/nav.tsx); this
|
|
303
303
|
// package only knows `schema` (the one calling feature) and cannot look up
|
|
304
304
|
// detailFor across features itself.
|
|
@@ -499,8 +499,8 @@ function EntityListBody({ schema, screen, entity, translate, onRowClick, }) {
|
|
|
499
499
|
// the entity has searchable fields (dead toolbar slot otherwise — the
|
|
500
500
|
// server search index has nothing to filter). Gated on
|
|
501
501
|
// schema.searchAdapterMissing !== true regardless of source (explicit or
|
|
502
|
-
// auto) — a search bar the server can't serve would 422 at request-time
|
|
503
|
-
//
|
|
502
|
+
// auto) — a search bar the server can't serve would 422 at request-time;
|
|
503
|
+
// matches the boot-time check in api/server.ts.
|
|
504
504
|
const searchable = schema.searchAdapterMissing !== true &&
|
|
505
505
|
(screen.searchable ??
|
|
506
506
|
Object.values(entity.fields).some((f) => "searchable" in f && f.searchable === true));
|
|
@@ -3,7 +3,7 @@ export type ExtensionSubmitContext = {
|
|
|
3
3
|
};
|
|
4
4
|
export type ExtensionSubmitResult = {
|
|
5
5
|
readonly isSuccess: boolean;
|
|
6
|
-
/** i18n
|
|
6
|
+
/** i18n key for the error message that the form displays as a banner. */
|
|
7
7
|
readonly errorKey?: string;
|
|
8
8
|
};
|
|
9
9
|
export type ExtensionFormSubmitHandler = (ctx: ExtensionSubmitContext) => Promise<ExtensionSubmitResult>;
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
// @runtime client
|
|
2
|
-
// Extension-Form-Submit-Registry —
|
|
3
|
-
// Custom-Fields)
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
// mit der entityId.
|
|
2
|
+
// Extension-Form-Submit-Registry — lets an extension section (e.g.
|
|
3
|
+
// Custom-Fields) write during submission of the surrounding entityEdit form
|
|
4
|
+
// instead of maintaining its own Save button ("composed form, one save").
|
|
5
|
+
// The section reports (a) a submit handler and (b) its dirty state to the form;
|
|
6
|
+
// the form enables its Save button even when only one section is dirty and,
|
|
7
|
+
// after the entity write, calls all section handlers with the entityId.
|
|
9
8
|
//
|
|
10
|
-
//
|
|
11
|
-
// Save
|
|
9
|
+
// Without a surrounding form (Context === null), the section falls back to its
|
|
10
|
+
// own Save button (Standalone-Backward-Compat).
|
|
12
11
|
import { createContext, useContext, useEffect, useId, useMemo, useRef } from "react";
|
|
13
12
|
const ExtensionFormRegistryContext = createContext(null);
|
|
14
13
|
export const ExtensionFormRegistryProvider = ExtensionFormRegistryContext.Provider;
|
|
@@ -48,15 +47,15 @@ export function useExtensionFormHost(onStatusChange) {
|
|
|
48
47
|
}, []);
|
|
49
48
|
return { registry, runAll };
|
|
50
49
|
}
|
|
51
|
-
// Section
|
|
52
|
-
//
|
|
53
|
-
// Save
|
|
50
|
+
// Section side: reports the handler and dirty state to the surrounding form.
|
|
51
|
+
// Returns true when a composed form is present, so the section hides its own
|
|
52
|
+
// Save button; false means standalone, so its own Save button remains.
|
|
54
53
|
export function useExtensionFormSubmit(opts) {
|
|
55
54
|
const registry = useContext(ExtensionFormRegistryContext);
|
|
56
55
|
const key = useId();
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
56
|
+
// The ref prevents re-registration whenever the handler identity changes
|
|
57
|
+
// (only dirty triggers it); the handler always reads the latest closure state
|
|
58
|
+
// (pending) when invoked.
|
|
60
59
|
const handlerRef = useRef(opts.onSubmit);
|
|
61
60
|
handlerRef.current = opts.onSubmit;
|
|
62
61
|
useEffect(() => {
|
|
@@ -1,31 +1,30 @@
|
|
|
1
1
|
import type { PlatformComponent } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
import { type ComponentType, type ReactNode } from "react";
|
|
3
|
-
/**
|
|
4
|
-
* react
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
/** Extracts the `__component` name from a PlatformComponent. Reads the
|
|
4
|
+
* react and native branches (the app author registers both under the same
|
|
5
|
+
* name); returns the first string found. Pure narrowing via `in` — no cast
|
|
6
|
+
* or assertion. */
|
|
7
7
|
export declare function extensionSectionName(component: PlatformComponent): string | undefined;
|
|
8
8
|
export type ExtensionSectionProps = {
|
|
9
9
|
readonly entityName: string;
|
|
10
10
|
readonly entityId: string | null;
|
|
11
|
-
/**
|
|
12
|
-
* detail
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
/** Previously saved extension values for the entity (passed through from
|
|
12
|
+
* the loaded detail row). `undefined` in create mode or when the host
|
|
13
|
+
* screen provides no values. Allows the section to display existing
|
|
14
|
+
* values while editing instead of being write-only. */
|
|
15
15
|
readonly initialValues?: Readonly<Record<string, unknown>>;
|
|
16
|
-
/**
|
|
17
|
-
*
|
|
18
|
-
* (useListUrlState)
|
|
19
|
-
*
|
|
16
|
+
/** Set only in the list-header slot (`entityId` is null): the list's
|
|
17
|
+
* `screen.id`, so a header control can access the per-screen URL filter
|
|
18
|
+
* state (`useListUrlState`) — for example, a tag filter that narrows the
|
|
19
|
+
* list to a set of IDs. `undefined` in entityEdit sections. */
|
|
20
20
|
readonly screenId?: string;
|
|
21
|
-
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
/** Set only in a Dashboard-`custom` panel: the currently selected value
|
|
22
|
+
* of the screen filter (see DashboardFilterDefinition), merged as with any
|
|
23
|
+
* other panel query. `undefined` in all other mount locations. Dashboard
|
|
24
|
+
* panels have no entity — `entityName`/`entityId` contain the `screen.id`
|
|
25
|
+
* and `null`, respectively; see CustomPanelBody in dashboard-body.tsx. */
|
|
26
26
|
readonly filterParams?: Readonly<Record<string, unknown>>;
|
|
27
|
-
/** Current form values of the host RenderEdit (controlled mode
|
|
28
|
-
* issue #1887/#1888) — same snapshot reference as
|
|
27
|
+
/** Current form values of the host RenderEdit (controlled mode) — same snapshot reference as
|
|
29
28
|
* `RenderEditChangeState.values`. Lets the section read the host
|
|
30
29
|
* form's live state (e.g. for a review step). Only set in
|
|
31
30
|
* entityEdit sections; undefined in list-header and dashboard
|
|
@@ -44,10 +43,12 @@ export type ExtensionSectionProps = {
|
|
|
44
43
|
* `snapshot.errors` on the field instead of a collective message.
|
|
45
44
|
* Undefined outside entityEdit sections. */
|
|
46
45
|
readonly validate?: () => boolean;
|
|
47
|
-
/** Marks
|
|
48
|
-
*
|
|
46
|
+
/** Marks host form values as saved after the section wrote them itself, so
|
|
47
|
+
* a following navigation does not ask to discard. Pass the field names the
|
|
48
|
+
* section persisted: other host fields keep their unsaved state. Without
|
|
49
|
+
* `fields`, every current host value counts as saved.
|
|
49
50
|
* Undefined outside entityEdit sections and the entityEdit footer slot. */
|
|
50
|
-
readonly markSaved?: () => void;
|
|
51
|
+
readonly markSaved?: (fields?: readonly string[]) => void;
|
|
51
52
|
/** Whether the host form has unsaved changes. Only set in the entityEdit
|
|
52
53
|
* footer slot (`screen.slots.footer`); undefined in all other mounts. */
|
|
53
54
|
readonly hasUnsavedChanges?: boolean;
|
|
@@ -73,10 +74,10 @@ export type ExtensionSectionsProviderProps = {
|
|
|
73
74
|
readonly value: ExtensionSectionsMap;
|
|
74
75
|
};
|
|
75
76
|
export declare function ExtensionSectionsProvider({ children, value, }: ExtensionSectionsProviderProps): ReactNode;
|
|
76
|
-
/**
|
|
77
|
-
* undefined
|
|
78
|
-
*
|
|
79
|
-
*
|
|
80
|
-
*
|
|
81
|
-
*
|
|
77
|
+
/** Looks up the component for an extension-section name. Returns
|
|
78
|
+
* `undefined` when no provider is mounted or the name is not registered in
|
|
79
|
+
* the map — the caller (RenderEdit) then displays its placeholder banner.
|
|
80
|
+
* `name` is optional so callers can invoke the hook unconditionally
|
|
81
|
+
* (Rules-of-Hooks) without supplying a stub key such as `""`, analogous to
|
|
82
|
+
* `useColumnRenderer`. */
|
|
82
83
|
export declare function useExtensionSectionComponent(name?: string): ExtensionSectionComponent | undefined;
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useContext } from "react";
|
|
3
|
-
/**
|
|
4
|
-
* react
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
/** Extracts the `__component` name from a PlatformComponent. Reads the
|
|
4
|
+
* react and native branches (the app author registers both under the same
|
|
5
|
+
* name); returns the first string found. Pure narrowing via `in` — no cast
|
|
6
|
+
* or assertion. */
|
|
7
7
|
export function extensionSectionName(component) {
|
|
8
8
|
for (const branch of [component.react, component.native]) {
|
|
9
9
|
if (branch !== null && typeof branch === "object" && "__component" in branch) {
|
|
@@ -18,12 +18,12 @@ const ExtensionSectionsContext = createContext(undefined);
|
|
|
18
18
|
export function ExtensionSectionsProvider({ children, value, }) {
|
|
19
19
|
return (_jsx(ExtensionSectionsContext.Provider, { value: value, children: children }));
|
|
20
20
|
}
|
|
21
|
-
/**
|
|
22
|
-
* undefined
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
21
|
+
/** Looks up the component for an extension-section name. Returns
|
|
22
|
+
* `undefined` when no provider is mounted or the name is not registered in
|
|
23
|
+
* the map — the caller (RenderEdit) then displays its placeholder banner.
|
|
24
|
+
* `name` is optional so callers can invoke the hook unconditionally
|
|
25
|
+
* (Rules-of-Hooks) without supplying a stub key such as `""`, analogous to
|
|
26
|
+
* `useColumnRenderer`. */
|
|
27
27
|
export function useExtensionSectionComponent(name) {
|
|
28
28
|
const map = useContext(ExtensionSectionsContext);
|
|
29
29
|
if (name === undefined || name === "")
|
|
@@ -4,10 +4,10 @@
|
|
|
4
4
|
// file that exports a schema can be imported from the browser bundle
|
|
5
5
|
// without dragging in Node-only framework internals.
|
|
6
6
|
//
|
|
7
|
-
// Types
|
|
8
|
-
// (buildAppSchema, dev-server)
|
|
9
|
-
//
|
|
10
|
-
// (toAppSchema, isAppSchema)
|
|
7
|
+
// Types live in `framework/ui-types/app-schema.ts` so the server
|
|
8
|
+
// (buildAppSchema, dev-server) can produce them without pulling in
|
|
9
|
+
// renderer as a dependency. Only re-exports and runtime helpers
|
|
10
|
+
// (toAppSchema, isAppSchema) needed by client code at runtime belong here.
|
|
11
11
|
//
|
|
12
12
|
// Typical layout on the feature author's side:
|
|
13
13
|
//
|
|
@@ -28,10 +28,10 @@
|
|
|
28
28
|
// r.screen(editScreen);
|
|
29
29
|
// ...
|
|
30
30
|
// });
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
34
|
-
//
|
|
31
|
+
// Normalizes FeatureSchema → AppSchema and is idempotent for AppSchema.
|
|
32
|
+
// Promotes a feature-local `workspaces` list (legacy) to the app level
|
|
33
|
+
// so all layouts can use the new form without requiring old clientSchemas
|
|
34
|
+
// to be migrated.
|
|
35
35
|
export function toAppSchema(input) {
|
|
36
36
|
if ("features" in input)
|
|
37
37
|
return input;
|
|
@@ -42,9 +42,9 @@ export function toAppSchema(input) {
|
|
|
42
42
|
...(workspaces !== undefined && { workspaces }),
|
|
43
43
|
};
|
|
44
44
|
}
|
|
45
|
-
//
|
|
46
|
-
// (
|
|
47
|
-
//
|
|
45
|
+
// Type guard for callers that need to distinguish the types at runtime
|
|
46
|
+
// (rare; toAppSchema is usually sufficient). Do not use `"features" in x`
|
|
47
|
+
// inline: TS then narrows to the join type instead of the actual distinction.
|
|
48
48
|
export function isAppSchema(input) {
|
|
49
49
|
return "features" in input;
|
|
50
50
|
}
|
package/dist/app/form-schema.js
CHANGED
|
@@ -26,10 +26,10 @@ function isRequiredValueMissing(spec, field, record, unchangedFrom) {
|
|
|
26
26
|
if (!isRequired)
|
|
27
27
|
return false;
|
|
28
28
|
// Embedded LIST fields get their own EmbeddedListField grid widget
|
|
29
|
-
//
|
|
29
|
+
// — they're fillable, so NO_WIDGET_FIELD_TYPES's "embedded"
|
|
30
30
|
// entry must not exempt them. A statically-`required: true` field
|
|
31
31
|
// with no bound widget is also caught at boot
|
|
32
|
-
// (validateNoWidgetRequiredField in boot-validator/screens.ts).
|
|
32
|
+
// (validateNoWidgetRequiredField in boot-validator/screens-forms.ts).
|
|
33
33
|
if (NO_WIDGET_FIELD_TYPES.includes(field.type) && !isEmbeddedListField(field))
|
|
34
34
|
return false;
|
|
35
35
|
if (isPresent(record[spec.field]))
|
|
@@ -3,7 +3,7 @@ import { RenderEditActionButton } from "../components/render-edit-action-button.
|
|
|
3
3
|
import { useActionMenuItems } from "../components/use-action-menu-items.js";
|
|
4
4
|
import { useTranslation } from "../i18n.js";
|
|
5
5
|
import { compactHeaderPrimary, orderPlainHeaderActions, primaryHeaderAction, } from "./header-actions.js";
|
|
6
|
-
// Header actions bar
|
|
6
|
+
// Header actions bar: <=2 actions render as plain buttons; >2 with an
|
|
7
7
|
// `ActionOverflowMenu` primitive collapse to the primary button plus a menu
|
|
8
8
|
// for the rest — same rule as RowActionsCell for table rows. Owns confirm
|
|
9
9
|
// state for menu items: RenderEditActionButton already owns a per-button
|
|
@@ -18,7 +18,7 @@ export function HeaderActionsBar({ actions, Button, Dialog, ActionOverflowMenu,
|
|
|
18
18
|
if (compact && PageHeader !== undefined) {
|
|
19
19
|
const compactPrimary = compactHeaderPrimary(actions, preferredPrimaryId);
|
|
20
20
|
const overflowActions = actions.filter((a) => a !== compactPrimary?.action);
|
|
21
|
-
return (_jsxs(_Fragment, { children: [compactPrimary !== undefined && (_jsx(RenderEditActionButton, { action: compactPrimary.action, Button: Button, Dialog: Dialog, iconOnly: compactPrimary.iconOnly,
|
|
21
|
+
return (_jsxs(_Fragment, { children: [compactPrimary !== undefined && (_jsx(RenderEditActionButton, { action: compactPrimary.action, Button: Button, Dialog: Dialog, iconOnly: compactPrimary.iconOnly, onError: onError }, compactPrimary.action.id)), overflowActions.length > 0 && (_jsx(PageHeader, { overflowItems: overflowActions.map(toMenuItem) })), pendingActionDialog] }));
|
|
22
22
|
}
|
|
23
23
|
const maxPlainButtons = collapseAfterPrimary ? 1 : 2;
|
|
24
24
|
if (actions.length <= maxPlainButtons || ActionOverflowMenu === undefined) {
|
|
@@ -130,9 +130,9 @@ function renderScreenBody({ schema, screen, translate, entityId, onRowClick, onC
|
|
|
130
130
|
return _jsx(CustomScreenBody, { screenId: screen.id, featureName: schema.featureName });
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
|
-
//
|
|
134
|
-
//
|
|
135
|
-
//
|
|
133
|
+
// The dashboard panel implementation (StatCard, charts) is platform-specific and
|
|
134
|
+
// comes from the DashboardBody context; renderer/web registers the web
|
|
135
|
+
// implementation in createKumikoApp.
|
|
136
136
|
function DashboardScreenBody({ featureName, screen, translate, }) {
|
|
137
137
|
const { Banner, Text } = usePrimitives();
|
|
138
138
|
const Body = useDashboardBody();
|
|
@@ -141,13 +141,11 @@ function DashboardScreenBody({ featureName, screen, translate, }) {
|
|
|
141
141
|
}
|
|
142
142
|
return (_jsx(Body, { featureName: featureName, screen: screen, ...(translate !== undefined && { translate }) }));
|
|
143
143
|
}
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
// gemountet ist, unabhängig davon ob ein Client-Plugin existiert
|
|
150
|
-
// (kumiko-framework#2025).
|
|
144
|
+
// Looks up the component from the CustomScreens context, populated by
|
|
145
|
+
// clientFeatures.components in createKumikoApp. If no provider is mounted or
|
|
146
|
+
// the screenId is unregistered, an error banner is rendered instead of an
|
|
147
|
+
// empty screen. The screen remains reachable by URL when the server feature is
|
|
148
|
+
// mounted, even without a client plugin.
|
|
151
149
|
function CustomScreenBody({ screenId, featureName, }) {
|
|
152
150
|
const { Banner, Text } = usePrimitives();
|
|
153
151
|
const Component = useCustomScreenComponent(screenId);
|
|
@@ -3,7 +3,7 @@ import { isFieldsEditSection, normalizeEditField, sectionFieldSpecs, } from "@co
|
|
|
3
3
|
// (and relatedList) sections skipped. Both this and `layoutFieldNames` key
|
|
4
4
|
// off "rendered by the layout" for the same reason: a field the user never
|
|
5
5
|
// sees gets no chance to review/correct a value nor to fix a presence error
|
|
6
|
-
// (search-param merge
|
|
6
|
+
// (search-param merge; presence schema in form-schema.ts).
|
|
7
7
|
export function layoutEditFields(screen) {
|
|
8
8
|
const specs = [];
|
|
9
9
|
for (const section of screen.layout.sections) {
|
package/dist/app/list-facets.js
CHANGED
|
@@ -94,7 +94,7 @@ export function normalizeDefaultFilters(defaults) {
|
|
|
94
94
|
// hand-crafted URL param for an undeclared field never reaches the
|
|
95
95
|
// server). Deliberately NOT gated on the field being a *facet* — entityList
|
|
96
96
|
// passes through any field present in entity.fields, matching its
|
|
97
|
-
//
|
|
97
|
+
// earlier behavior; only the boolean-coercion branch cares about type.
|
|
98
98
|
function isFacetI18nKey(label) {
|
|
99
99
|
return !/\s/.test(label) && (label.includes(".") || label.includes(":"));
|
|
100
100
|
}
|
|
@@ -119,8 +119,8 @@ export function buildFilterPayload(urlFilters, typeOf) {
|
|
|
119
119
|
return out;
|
|
120
120
|
}
|
|
121
121
|
// projectionList/relatedList adapter — neither has an entity/i18n convention
|
|
122
|
-
// to derive labels from, so ListFacetSpec carries every label explicitly
|
|
123
|
-
//
|
|
122
|
+
// to derive labels from, so ListFacetSpec carries every label explicitly.
|
|
123
|
+
// Labels may be raw display strings or i18n keys; run them
|
|
124
124
|
// through translate like entity facets (passthrough when the key is missing).
|
|
125
125
|
// `featureName` resolves a reference facet's `entity` target the same way
|
|
126
126
|
// ListColumnSpec.refEntity does (same-feature short name, or "feature:entity").
|
package/dist/app/nav.d.ts
CHANGED
|
@@ -40,16 +40,16 @@ export type NavApi = {
|
|
|
40
40
|
* platform-specific Link-Komponenten (Web: `<a href>`; Native
|
|
41
41
|
* typically doesn't need this). */
|
|
42
42
|
readonly hrefFor: (target: NavTarget, options?: NavigateOptions) => string;
|
|
43
|
-
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
43
|
+
/** Snapshot of the current search params (browser: `?key=value` pairs
|
|
44
|
+
* after the path). Native implementations without a URL concept return
|
|
45
|
+
* an empty object. The value is a plain record, not a Map, so React
|
|
46
|
+
* subscribers can compare it shallowly. */
|
|
47
47
|
readonly searchParams: Readonly<Record<string, string>>;
|
|
48
|
-
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
48
|
+
/** Merges updates into the current search params. A `null` value deletes
|
|
49
|
+
* the key. The path is unchanged. Platform implementations use
|
|
50
|
+
* `history.replaceState` rather than a push so sort/filter toggles do not
|
|
51
|
+
* flood back navigation. Native implementations may no-op when the
|
|
52
|
+
* concept does not exist. */
|
|
53
53
|
readonly setSearchParams: (updates: Readonly<Record<string, string | null>>) => void;
|
|
54
54
|
};
|
|
55
55
|
export declare function parsePath(pathname: string, hasWorkspaces?: boolean): NavRoute | undefined;
|
|
@@ -60,7 +60,7 @@ export declare function findDetailForScreen(features: readonly FeatureSchema[],
|
|
|
60
60
|
readonly screen: FeatureSchema["screens"][number];
|
|
61
61
|
} | undefined;
|
|
62
62
|
/** Non-throwing existence check for findDetailForScreen — lets a caller pick
|
|
63
|
-
* a different default (
|
|
63
|
+
* a different default (row-click fallback) instead of resolving. */
|
|
64
64
|
export declare function hasDetailScreen(features: readonly FeatureSchema[], entity: string): boolean;
|
|
65
65
|
export declare function resolveTarget(features: readonly FeatureSchema[], target: NavTarget): ScreenTarget;
|
|
66
66
|
export type NavProviderProps = {
|
package/dist/app/nav.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useCallback, useContext, useEffect, useState } from "react";
|
|
3
3
|
import { lastSegment } from "./qn.js";
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
4
|
+
// Path format:
|
|
5
|
+
// without workspaces: `/task-list`, `/task-edit/abc-123`, `/` → undefined
|
|
6
|
+
// with workspaces: `/admin/task-list`, `/admin/task-edit/abc`,
|
|
7
|
+
// `/admin` → workspace-only (no screen yet),
|
|
8
|
+
// `/` → undefined.
|
|
9
|
+
// Everything after the first 2/3 segments is ignored because Kumiko's
|
|
10
|
+
// navigation grammar does not support further nesting. Nested routes would
|
|
11
|
+
// be a spec change, not an accidental URL shape.
|
|
12
12
|
function decodeSegments(segments) {
|
|
13
13
|
try {
|
|
14
14
|
return segments.map((segment) => decodeURIComponent(segment));
|
|
@@ -57,7 +57,7 @@ export function formatPath(target) {
|
|
|
57
57
|
return `/${segments.map((segment) => encodeURIComponent(segment)).join("/")}`;
|
|
58
58
|
}
|
|
59
59
|
// Shared entity→detailFor-screen lookup — resolveTarget's ObjectTarget
|
|
60
|
-
// branch and hasDetailScreen (create-app's row-click default
|
|
60
|
+
// branch and hasDetailScreen (create-app's row-click default) both
|
|
61
61
|
// need "is there a detail screen for this entity", one to resolve it, the
|
|
62
62
|
// other to just check before falling back to a different default.
|
|
63
63
|
export function findDetailForScreen(features, entity) {
|
|
@@ -70,7 +70,7 @@ export function findDetailForScreen(features, entity) {
|
|
|
70
70
|
return undefined;
|
|
71
71
|
}
|
|
72
72
|
/** Non-throwing existence check for findDetailForScreen — lets a caller pick
|
|
73
|
-
* a different default (
|
|
73
|
+
* a different default (row-click fallback) instead of resolving. */
|
|
74
74
|
export function hasDetailScreen(features, entity) {
|
|
75
75
|
return findDetailForScreen(features, entity) !== undefined;
|
|
76
76
|
}
|
|
@@ -93,9 +93,9 @@ export function resolveTarget(features, target) {
|
|
|
93
93
|
}
|
|
94
94
|
throw new Error(`resolveTarget: no detail screen for entity "${target.entity}". Add detailFor: "${target.entity}" to the screen that shows it.`);
|
|
95
95
|
}
|
|
96
|
-
// Context
|
|
97
|
-
//
|
|
98
|
-
//
|
|
96
|
+
// Context and hook. The default is `undefined` so a missing provider fails
|
|
97
|
+
// loudly instead of exposing a silent no-op NavApi with dead `navigate()`
|
|
98
|
+
// calls.
|
|
99
99
|
const NavContext = createContext(undefined);
|
|
100
100
|
const InitialValuesHandoffContext = createContext(undefined);
|
|
101
101
|
export function NavProvider({ children, value }) {
|
|
@@ -188,11 +188,11 @@ export function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
188
188
|
await refetchAfterWrite(detailQuery.refetch);
|
|
189
189
|
setReloadNonce((n) => n + 1);
|
|
190
190
|
}, [detailQuery.refetch]);
|
|
191
|
-
// Header actions render directly via RenderEditActionButton
|
|
192
|
-
// headerActions moved out of RenderEdit's footer
|
|
191
|
+
// Header actions render directly via RenderEditActionButton
|
|
192
|
+
// headerActions moved out of RenderEdit's footer — this component owns
|
|
193
193
|
// the failure-surfacing state RenderEdit used to own for them.
|
|
194
194
|
const [actionError, setActionError] = useState(null);
|
|
195
|
-
// Default edit action
|
|
195
|
+
// Default edit action: resolved cross-feature over ALL mounted
|
|
196
196
|
// features, not just this feature's own schema — detailFor itself is
|
|
197
197
|
// resolved cross-feature by the boot-validator (detail-screens.ts), and
|
|
198
198
|
// the motivating case is a projectionDetail whose query belongs to one
|
|
@@ -383,7 +383,7 @@ export function ProjectionDetailBody({ schema, screen, translate, entityId, }) {
|
|
|
383
383
|
}),
|
|
384
384
|
}) }));
|
|
385
385
|
}
|
|
386
|
-
// Grouped into the head Card alongside title/status/metrics
|
|
386
|
+
// Grouped into the head Card alongside title/status/metrics:
|
|
387
387
|
// these are actions on the record the head shows, not on whichever tab is
|
|
388
388
|
// open, so they must stay in place across tab switches instead of
|
|
389
389
|
// trailing the active tab's content in the card footer.
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { EditLayout, EntityDefinition, EntityEditScreenDefinition, ProjectionDetailScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
-
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
2
|
+
/** Minimal EntityDefinition built from the layout fields: every field is a text
|
|
3
|
+
* field — computeEditViewModel only reads `fields[<f>].type`, and text is
|
|
4
|
+
* sufficient for display-only rendering (no Select/Number-specific rendering
|
|
5
|
+
* is needed). */
|
|
5
6
|
export declare function synthesizeProjectionDetailEntity(layout: EditLayout): Pick<EntityDefinition, "fields">;
|
|
6
|
-
/**
|
|
7
|
-
*
|
|
7
|
+
/** Converts a ProjectionDetailScreenDefinition to the EntityEditScreen shape
|
|
8
|
+
* expected by RenderEdit, with every field forcibly set to readOnly:true. */
|
|
8
9
|
export declare function synthesizeProjectionDetailScreen(screen: ProjectionDetailScreenDefinition): EntityEditScreenDefinition;
|