@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,29 +1,30 @@
|
|
|
1
|
-
// Shim
|
|
1
|
+
// Shim for the ProjectionDetail renderer (analogous to projection-list-shim.ts /
|
|
2
2
|
// config-edit-shim.ts).
|
|
3
3
|
//
|
|
4
|
-
// RenderEdit
|
|
5
|
-
// `screen: EntityEditScreenDefinition
|
|
6
|
-
// `entity.fields` (
|
|
7
|
-
// (
|
|
8
|
-
// query-
|
|
9
|
-
//
|
|
4
|
+
// RenderEdit requires an `entity: EntityDefinition` +
|
|
5
|
+
// `screen: EntityEditScreenDefinition` pair, but only uses
|
|
6
|
+
// `entity.fields` (field type for the input renderer) and `screen.layout`
|
|
7
|
+
// (which fields belong in which sections). A projectionDetail screen is
|
|
8
|
+
// query-driven and has NO Entity — these two helpers shape the input ad hoc
|
|
9
|
+
// so RenderEdit can be reused.
|
|
10
10
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
11
|
+
// The structural read-only guarantee is established in
|
|
12
|
+
// synthesizeProjectionDetailScreen: EVERY field is forced to readOnly:true
|
|
13
|
+
// (not just fields the author marked in the layout) — hasEditableSection()
|
|
14
|
+
// reads exactly this flag and hides the Save button when no field is
|
|
15
|
+
// editable. The author cannot accidentally bypass this guarantee.
|
|
16
16
|
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
// cross-
|
|
20
|
-
//
|
|
21
|
-
//
|
|
17
|
+
// Same debt reservation as in the other shims: if RenderEdit later accesses
|
|
18
|
+
// additional EntityDefinition fields (transitions, idType), or a boot
|
|
19
|
+
// validator cross-references the schema.entities map, these type lies will
|
|
20
|
+
// fail silently — then it is time for a real RenderProjectionDetail
|
|
21
|
+
// component.
|
|
22
22
|
import { isFieldsEditSection, normalizeEditField, PROJECTION_DETAIL_ENTITY as PROJECTION_DETAIL_PSEUDO_ENTITY, sectionFieldSpecs, } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
23
23
|
import { SYNTHESIZED_FIELD_PII_REASON } from "./projection-list-shim.js";
|
|
24
|
-
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
24
|
+
/** Minimal EntityDefinition built from the layout fields: every field is a text
|
|
25
|
+
* field — computeEditViewModel only reads `fields[<f>].type`, and text is
|
|
26
|
+
* sufficient for display-only rendering (no Select/Number-specific rendering
|
|
27
|
+
* is needed). */
|
|
27
28
|
export function synthesizeProjectionDetailEntity(layout) {
|
|
28
29
|
const fields = {};
|
|
29
30
|
for (const section of layout.sections) {
|
|
@@ -39,8 +40,8 @@ export function synthesizeProjectionDetailEntity(layout) {
|
|
|
39
40
|
}
|
|
40
41
|
return { fields };
|
|
41
42
|
}
|
|
42
|
-
/**
|
|
43
|
-
*
|
|
43
|
+
/** Converts a ProjectionDetailScreenDefinition to the EntityEditScreen shape
|
|
44
|
+
* expected by RenderEdit, with every field forcibly set to readOnly:true. */
|
|
44
45
|
export function synthesizeProjectionDetailScreen(screen) {
|
|
45
46
|
const forceReadOnly = (spec) => ({ ...normalizeEditField(spec), readOnly: true });
|
|
46
47
|
const sections = screen.layout.sections.map((section) => {
|
|
@@ -27,7 +27,7 @@ import { buildListQueryPayload, pageSizeOptionsFor } from "./screen-shared.js";
|
|
|
27
27
|
// (instead of being derived from an entity) — cross-feature capable.
|
|
28
28
|
// search/sort/pagination reuse the same URL-state + buildListQueryPayload
|
|
29
29
|
// wiring as EntityListBody; which of them are actually offered is derived
|
|
30
|
-
// from the query handler's Zod schema at buildAppSchema time
|
|
30
|
+
// from the query handler's Zod schema at buildAppSchema time, not
|
|
31
31
|
// declared here. Pages-mode pagination only — infinite-scroll accumulation
|
|
32
32
|
// isn't wired for projectionList.
|
|
33
33
|
export function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
@@ -49,7 +49,7 @@ export function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
49
49
|
return findEditScreenFor(detailFor, appFeatures, userRoles);
|
|
50
50
|
}, [appFeatures, screen.detailFor, userRoles]);
|
|
51
51
|
// searchable/sortable/paginated are derived at buildAppSchema time from the
|
|
52
|
-
// query handler's Zod schema
|
|
52
|
+
// query handler's Zod schema — not authored on the screen.
|
|
53
53
|
const searchable = screen.searchable ?? false;
|
|
54
54
|
const sortable = screen.sortable ?? false;
|
|
55
55
|
const paginated = screen.paginated ?? false;
|
|
@@ -72,7 +72,7 @@ export function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
72
72
|
// Pages-mode only (see header comment) — an author-set pagination:
|
|
73
73
|
// "infinite" on a paginated projectionList is silently a no-op today.
|
|
74
74
|
const usePager = paginated && (screen.pagination ?? "pages") === "pages";
|
|
75
|
-
// Facets
|
|
75
|
+
// Facets — a projectionList has no entity, so screen.facets is
|
|
76
76
|
// the only field inventory; resolveProjectionFacetSpecs reshapes +
|
|
77
77
|
// translates its explicit labels into the same ResolvedFacetSpec the
|
|
78
78
|
// entityList adapter produces, so both feed the same
|
|
@@ -82,7 +82,7 @@ export function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
82
82
|
const handleProjectionFacetOptions = useCallback((field, options) => setProjectionReferenceFacetOptions((prev) => prev[field] === options ? prev : { ...prev, [field]: options }), []);
|
|
83
83
|
const resolvedFacetSpecs = useMemo(() => mergeReferenceFacetOptions(facetSpecs, projectionReferenceFacetOptions), [facetSpecs, projectionReferenceFacetOptions]);
|
|
84
84
|
const filterPayload = useMemo(() => buildFilterPayload(urlState.filters, (field) => facetSpecs.find((spec) => spec.field === field)?.type), [urlState.filters, facetSpecs]);
|
|
85
|
-
// dateRange facets
|
|
85
|
+
// dateRange facets bypass `filters` entirely — their two bounds
|
|
86
86
|
// become the top-level query params the facet declares, resolved from a
|
|
87
87
|
// calendar date to an instant in the viewer's zone.
|
|
88
88
|
const timeZone = useOptionalTimeZone();
|
|
@@ -101,7 +101,7 @@ export function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
101
101
|
// Gated on screen.filter/screen.facets being declared (not just on
|
|
102
102
|
// urlState/filterPayload happening to carry a value) — same rule as
|
|
103
103
|
// activeSearch/activeSort above: a stale URL must not smuggle a param
|
|
104
|
-
// the bound query's Zod schema doesn't accept
|
|
104
|
+
// the bound query's Zod schema doesn't accept.
|
|
105
105
|
if (screen.filter !== undefined) {
|
|
106
106
|
payload["filter"] = screen.filter;
|
|
107
107
|
}
|
|
@@ -131,8 +131,8 @@ export function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
131
131
|
const handleDateRangeChange = useCallback((field, bound, value) => {
|
|
132
132
|
urlState.setDateRange(field, clampDateRange(readDateRange(urlState.filters, field), bound, value));
|
|
133
133
|
}, [urlState.setDateRange, urlState.filters]);
|
|
134
|
-
// Entity
|
|
135
|
-
//
|
|
134
|
+
// Entity targets: see EntityListBody.runNavigate for why the resolution happens
|
|
135
|
+
// in the NavApi impl. Unlike there: NO row["id"]
|
|
136
136
|
// fallback — projectionList rows come from an arbitrary query projection
|
|
137
137
|
// with no guaranteed "id" field. The boot validator therefore enforces an
|
|
138
138
|
// explicit entityId for projectionList entity targets. Same helper
|
|
@@ -198,8 +198,8 @@ export function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
198
198
|
// A failed refetch keeps the last good data on screen.
|
|
199
199
|
const refetchErrorBanner = rowsQuery.error ? (_jsx(QueryErrorBanner, { error: rowsQuery.error, translate: effectiveTranslate, onRetry: rowsQuery.refetch })) : null;
|
|
200
200
|
// rowsQuery.data is typed as PagedRows but the query handler is free to
|
|
201
|
-
// return anything at runtime (
|
|
202
|
-
//
|
|
201
|
+
// return anything at runtime (a bare array silently rendered an empty
|
|
202
|
+
// table instead of surfacing the mismatch), so the shape is checked.
|
|
203
203
|
if (rowsQuery.data !== null && !Array.isArray(rowsQuery.data.rows)) {
|
|
204
204
|
return (_jsxs(Banner, { padded: true, variant: "error", testId: "kumiko-screen-projection-list-bad-shape", children: ["Screen ", _jsx(Text, { variant: "code", children: screen.id }), " query", " ", _jsx(Text, { variant: "code", children: screen.query }), " did not return a PagedRows envelope; a projectionList query must return ", _jsx(Text, { variant: "code", children: "{ rows, nextCursor }" }), "."] }));
|
|
205
205
|
}
|
|
@@ -213,7 +213,7 @@ export function ProjectionListBody({ schema, screen, translate, onRowClick, }) {
|
|
|
213
213
|
: undefined;
|
|
214
214
|
// Same pager-construction as EntityListBody: no Pager UI until the
|
|
215
215
|
// server-provided `total` arrives (guards pagination="pages" without
|
|
216
|
-
// totalCount support
|
|
216
|
+
// totalCount support).
|
|
217
217
|
const total = rowsQuery.data?.total;
|
|
218
218
|
const pager = usePager && total !== undefined
|
|
219
219
|
? {
|
|
@@ -2,14 +2,14 @@ import type { EntityDefinition, EntityListScreenDefinition, ListColumnSpec, Proj
|
|
|
2
2
|
export declare const SYNTHESIZED_FIELD_PII_REASON = "synthesized display field, not stored entity data";
|
|
3
3
|
/** Minimal EntityDefinition from the column list: every field is a text
|
|
4
4
|
* field. `sortable` applies to all columns — buildAppSchema derives it from
|
|
5
|
-
* the query's Zod schema (`screen.sortable
|
|
5
|
+
* the query's Zod schema (`screen.sortable`); the query itself has
|
|
6
6
|
* no per-field server-sort guarantee, so a column opts out with
|
|
7
7
|
* `sortable: false` when the handler cannot order by it.
|
|
8
8
|
* computeListViewModel only reads `fields[<col>].type` — text is enough,
|
|
9
9
|
* presentation comes from the column renderer + explicit label. */
|
|
10
10
|
export declare function synthesizeProjectionEntity(columns: readonly ListColumnSpec[], sortable: boolean): Pick<EntityDefinition, "fields">;
|
|
11
|
-
/**
|
|
12
|
-
* RenderList
|
|
13
|
-
*
|
|
14
|
-
*
|
|
11
|
+
/** Converts a ProjectionListScreenDefinition to the EntityListScreen shape
|
|
12
|
+
* expected by RenderList. `type` = "entityList" plus a pseudo-entity satisfy
|
|
13
|
+
* the type constraint; RenderList does not branch on `type` and reads `entity`
|
|
14
|
+
* only for error messages. The list fields are passed through unchanged. */
|
|
15
15
|
export declare function synthesizeProjectionScreen(screen: ProjectionListScreenDefinition): EntityListScreenDefinition;
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
// Shim
|
|
1
|
+
// Shim for the ProjectionList renderer (analogous to action-form-shim.ts / config-edit-shim.ts).
|
|
2
2
|
//
|
|
3
|
-
// RenderList + computeListViewModel
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
// hierüber aufgelöst (der Body nimmt `screen.query` direkt).
|
|
3
|
+
// RenderList + computeListViewModel require an `entity: EntityDefinition` +
|
|
4
|
+
// `screen: EntityListScreenDefinition` pair, but only use `entity.fields`
|
|
5
|
+
// (column type/label) and the screen's list fields (columns/rowActions/…). A
|
|
6
|
+
// projectionList screen is query-driven and has NO entity, so these two helpers
|
|
7
|
+
// reshape the input ad hoc to reuse the existing list machinery. The query is
|
|
8
|
+
// NOT resolved here (the body reads `screen.query` directly).
|
|
10
9
|
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
// derivedFields)
|
|
14
|
-
//
|
|
15
|
-
// query-native ListView
|
|
10
|
+
// This preserves the same debt reservation as the other shims: if the list
|
|
11
|
+
// machinery later accesses additional EntityDefinition fields (transitions,
|
|
12
|
+
// idType, derivedFields), or a boot validator cross-references the
|
|
13
|
+
// schema.entities map, these type lies will fail silently. At that point, a
|
|
14
|
+
// query-native ListView component is needed.
|
|
16
15
|
import { normalizeListColumn } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
17
16
|
const PROJECTION_PSEUDO_ENTITY = "__projection__";
|
|
18
17
|
// Satisfies the text-field PII stance of the entity field type; these synthesized
|
|
@@ -20,7 +19,7 @@ const PROJECTION_PSEUDO_ENTITY = "__projection__";
|
|
|
20
19
|
export const SYNTHESIZED_FIELD_PII_REASON = "synthesized display field, not stored entity data";
|
|
21
20
|
/** Minimal EntityDefinition from the column list: every field is a text
|
|
22
21
|
* field. `sortable` applies to all columns — buildAppSchema derives it from
|
|
23
|
-
* the query's Zod schema (`screen.sortable
|
|
22
|
+
* the query's Zod schema (`screen.sortable`); the query itself has
|
|
24
23
|
* no per-field server-sort guarantee, so a column opts out with
|
|
25
24
|
* `sortable: false` when the handler cannot order by it.
|
|
26
25
|
* computeListViewModel only reads `fields[<col>].type` — text is enough,
|
|
@@ -37,10 +36,10 @@ export function synthesizeProjectionEntity(columns, sortable) {
|
|
|
37
36
|
}
|
|
38
37
|
return { fields };
|
|
39
38
|
}
|
|
40
|
-
/**
|
|
41
|
-
* RenderList
|
|
42
|
-
*
|
|
43
|
-
*
|
|
39
|
+
/** Converts a ProjectionListScreenDefinition to the EntityListScreen shape
|
|
40
|
+
* expected by RenderList. `type` = "entityList" plus a pseudo-entity satisfy
|
|
41
|
+
* the type constraint; RenderList does not branch on `type` and reads `entity`
|
|
42
|
+
* only for error messages. The list fields are passed through unchanged. */
|
|
44
43
|
export function synthesizeProjectionScreen(screen) {
|
|
45
44
|
return {
|
|
46
45
|
id: screen.id,
|
package/dist/app/qn.js
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
|
-
// Qualified-
|
|
2
|
-
// Feature-Prefix auf jede Screen/Nav/Workspace-id ein:
|
|
1
|
+
// Qualified-name helper. At boot, Kumiko's Registry adds the feature prefix to every Screen/Nav/Workspace ID:
|
|
3
2
|
//
|
|
4
3
|
// r.screen({ id: "task-list", ... })
|
|
5
4
|
// → registry-stored.id === "tasks:screen:task-list"
|
|
6
|
-
// → schema.screens (
|
|
5
|
+
// → schema.screens (sent to the browser) has id === "tasks:screen:task-list"
|
|
7
6
|
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
// ("/tasks:screen:task-list" +
|
|
7
|
+
// The renderer uses short-form IDs: formatPath writes them to the URL unchanged,
|
|
8
|
+
// and parsePath reads them back unchanged. When converting from the schema (QN form)
|
|
9
|
+
// to nav.navigate (short form), the prefix must be removed; otherwise the URL becomes
|
|
10
|
+
// doubly qualified
|
|
11
|
+
// ("/tasks:screen:task-list" + re-lookup → "tasks:screen:tasks:screen:
|
|
13
12
|
// task-list" → 404).
|
|
14
13
|
//
|
|
15
|
-
// `lastSegment`
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// ids in eigenen Stellen weiter passt).
|
|
14
|
+
// `lastSegment` is the inverse of qualifyScreenId/Nav/Workspace: it takes the final
|
|
15
|
+
// ":"-separated part. Strings without ":" are returned unchanged so app-author code
|
|
16
|
+
// using short-form IDs continues to work.
|
|
19
17
|
export function lastSegment(qn) {
|
|
20
18
|
const idx = qn.lastIndexOf(":");
|
|
21
19
|
return idx < 0 ? qn : qn.slice(idx + 1);
|
|
@@ -7,7 +7,7 @@ export function qualifyScreenId(featureName, screenId) {
|
|
|
7
7
|
// Inverse of qualifyScreenId. A short id ("task-list") has no feature
|
|
8
8
|
// segment and returns undefined — callers need that to tell "this redirect
|
|
9
9
|
// names a feature" apart from "this redirect is feature-less, resolve by
|
|
10
|
-
// short id alone"
|
|
10
|
+
// short id alone".
|
|
11
11
|
export function featureNameFromQualifiedScreenId(id) {
|
|
12
12
|
const parts = id.split(":");
|
|
13
13
|
return parts.length === 3 && parts[1] === "screen" ? parts[0] : undefined;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isUiAccessGranted } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
2
|
import { findDetailForScreen } from "./nav.js";
|
|
3
3
|
import { lastSegment } from "./qn.js";
|
|
4
|
-
// Shared UI-visibility predicate for the screen-render path
|
|
4
|
+
// Shared UI-visibility predicate for the screen-render path — nav
|
|
5
5
|
// filtering via filterByAccess in workspace-shell.tsx hid role-gated
|
|
6
6
|
// screens from the menu, but a direct URL/screenQn hit reached
|
|
7
7
|
// KumikoScreen unchecked). Public name kept as an alias of the shared
|
|
@@ -9,7 +9,7 @@ import { lastSegment } from "./qn.js";
|
|
|
9
9
|
export { isUiAccessGranted as screenAccessAllows };
|
|
10
10
|
// A navigate target (metric click) declares no access rule of its own — the
|
|
11
11
|
// destination screen already does, so the jump is offered exactly when the
|
|
12
|
-
// user may open where it lands
|
|
12
|
+
// user may open where it lands. No second copy of the rule to
|
|
13
13
|
// drift from the screen's own.
|
|
14
14
|
export function navigateTargetAllows(navigate, appFeatures, userRoles) {
|
|
15
15
|
const { screen, entity } = navigate;
|
|
@@ -166,7 +166,7 @@ export function entityQueryCommand(featureName, entity, verb) {
|
|
|
166
166
|
// search/sort/sortDirection/limit + offset/totalCount for pager mode OR
|
|
167
167
|
// cursor for infinite scroll. Shared by EntityListBody and
|
|
168
168
|
// ProjectionListBody so the two branches can't drift on this shape
|
|
169
|
-
//
|
|
169
|
+
// — entity-only additions (screen.filter, faceted filters) are
|
|
170
170
|
// layered on top by the caller instead of living in here.
|
|
171
171
|
export function buildListQueryPayload(state) {
|
|
172
172
|
const payload = { limit: state.limit };
|
|
@@ -40,10 +40,10 @@ function isBlank(value) {
|
|
|
40
40
|
// Mint form → one-time reveal → optional confirm → done. The revealed values
|
|
41
41
|
// live ONLY in this component's `revealed` state — never in the URL, a query
|
|
42
42
|
// cache, or nav — the write-handler's success payload is the only place the
|
|
43
|
-
// secret ever exists
|
|
43
|
+
// secret ever exists. onSubmit copies exclusively the fields
|
|
44
44
|
// declared in `screen.reveal.fields` out of that payload (a whitelist, never
|
|
45
45
|
// the payload as a whole) so an unrelated field (e.g. an internal "id") can
|
|
46
|
-
// never leak into the reveal. `screen.confirm` (
|
|
46
|
+
// never leak into the reveal. `screen.confirm` (e.g. TOTP enroll:
|
|
47
47
|
// scan the code, then enter one) works the same way for its own carried
|
|
48
48
|
// mint-payload fields — those live only in `carriedRef`, never in React state
|
|
49
49
|
// that gets rendered, never in the confirm form's own values.
|
|
@@ -63,7 +63,7 @@ export function SecretMintBody({ schema, screen, translate }) {
|
|
|
63
63
|
urlPrefillFields: screen.urlPrefillFields,
|
|
64
64
|
renderableFields: layoutFieldNames(synthScreen),
|
|
65
65
|
// A mint form has no entity either, so its money fields name their own
|
|
66
|
-
// currency source
|
|
66
|
+
// currency source.
|
|
67
67
|
...(mintCurrencyOverrides !== undefined && {
|
|
68
68
|
moneyCurrencyOverrides: mintCurrencyOverrides,
|
|
69
69
|
}),
|
|
@@ -188,7 +188,7 @@ export function SecretMintBody({ schema, screen, translate }) {
|
|
|
188
188
|
// submit state) across phases.
|
|
189
189
|
if (confirm !== undefined && confirmEntity !== undefined && confirmScreen !== undefined) {
|
|
190
190
|
// Hold until a tenant-declared currency lands, else the form would seed (and
|
|
191
|
-
// on a fast click submit) the fallback currency
|
|
191
|
+
// on a fast click submit) the fallback currency.
|
|
192
192
|
if (confirmCurrencyLoading) {
|
|
193
193
|
return _jsx(QueryLoadingBanner, {});
|
|
194
194
|
}
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
// dispatcher.write
|
|
2
|
-
// { isSuccess: false, error }. Action
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// UI
|
|
1
|
+
// dispatcher.write does NOT throw on server errors; it returns
|
|
2
|
+
// { isSuccess: false, error }. Action wiring that discards the result
|
|
3
|
+
// makes failures invisible ("click does nothing" in production).
|
|
4
|
+
// This class carries the structured DispatcherError to a layer that can
|
|
5
|
+
// display it in the UI, such as a toast with docsUrl.
|
|
6
6
|
export class WriteFailedError extends Error {
|
|
7
7
|
dispatcherError;
|
|
8
8
|
constructor(error, message) {
|
|
@@ -11,8 +11,9 @@ export class WriteFailedError extends Error {
|
|
|
11
11
|
this.dispatcherError = error;
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
|
-
// i18nKey
|
|
15
|
-
//
|
|
14
|
+
// i18nKey takes precedence when the bundle knows it (translate returns
|
|
15
|
+
// the key unchanged when it does not, per the renderer convention),
|
|
16
|
+
// followed by message and then code.
|
|
16
17
|
export function dispatcherErrorText(error, translate) {
|
|
17
18
|
const translated = translate(error.i18nKey, error.i18nParams);
|
|
18
19
|
if (translated !== error.i18nKey)
|
|
@@ -10,7 +10,7 @@ export declare function ExtensionSectionMount({ section, entityName: hostEntityN
|
|
|
10
10
|
readonly values?: Readonly<Record<string, unknown>>;
|
|
11
11
|
readonly patch?: (partial: Readonly<Record<string, unknown>>) => void;
|
|
12
12
|
readonly validate?: () => boolean;
|
|
13
|
-
readonly markSaved?: () => void;
|
|
13
|
+
readonly markSaved?: (fields?: readonly string[]) => void;
|
|
14
14
|
readonly hideTitle?: boolean;
|
|
15
15
|
/** section.actions, already resolved into buttons by the caller. */
|
|
16
16
|
readonly actions?: ReactNode;
|
|
@@ -24,7 +24,7 @@ export declare function EditSlotMount({ slot, slotName, screenId, entityName, en
|
|
|
24
24
|
readonly entityName: string;
|
|
25
25
|
readonly entityId: string | null;
|
|
26
26
|
readonly values: Readonly<Record<string, unknown>>;
|
|
27
|
-
readonly markSaved?: () => void;
|
|
27
|
+
readonly markSaved?: (fields?: readonly string[]) => void;
|
|
28
28
|
readonly hasUnsavedChanges?: boolean;
|
|
29
29
|
readonly wizardStep?: {
|
|
30
30
|
readonly index: number;
|
|
@@ -71,11 +71,11 @@ function coerceCellValue(column, text, currency) {
|
|
|
71
71
|
// (renderCellControl's MoneyInput, which scales by the same
|
|
72
72
|
// currencyDecimals) — a hardcoded ×100 here diverged for zero-/three-
|
|
73
73
|
// decimal currencies (JPY, BHD, ...), landing a pasted value 100x off
|
|
74
|
-
// from the same value typed by hand
|
|
74
|
+
// from the same value typed by hand.
|
|
75
75
|
// parseLocaleAwareDecimal handles both "1.234,56" (de) and
|
|
76
76
|
// "1,234.56" (en-US) thousands/decimal conventions — a naive
|
|
77
|
-
// `replace(",", ".")` mangled
|
|
78
|
-
// separators into NaN
|
|
77
|
+
// `replace(",", ".")` mangled German-locale pastes with thousands
|
|
78
|
+
// separators into NaN.
|
|
79
79
|
if (text.trim() === "")
|
|
80
80
|
return undefined;
|
|
81
81
|
const n = parseLocaleAwareDecimal(text);
|
|
@@ -169,7 +169,7 @@ function EmbeddedListFieldInner({ field, id, onChange, allIssues, featureName, }
|
|
|
169
169
|
};
|
|
170
170
|
}
|
|
171
171
|
return {
|
|
172
|
-
field: entry.from.join("+")
|
|
172
|
+
field: `${entry.from.join("+")}:${entry.label}`,
|
|
173
173
|
label: t(entry.label),
|
|
174
174
|
value: entry.from.reduce((sum, name) => sum + sumEmbeddedListColumn(rows, name), 0),
|
|
175
175
|
money: entry.from.every((name) => cells.find((c) => c.field === name)?.type === "money"),
|
|
@@ -259,7 +259,7 @@ function EmbeddedListFieldInner({ field, id, onChange, allIssues, featureName, }
|
|
|
259
259
|
const coerced = coerceCellValue(column, text, field.embeddedListCurrency ?? "EUR");
|
|
260
260
|
// Also covers an unparseable money paste (e.g. mangled thousands
|
|
261
261
|
// separators) — the cell must stay unchanged instead of silently
|
|
262
|
-
// clearing to undefined
|
|
262
|
+
// clearing to undefined.
|
|
263
263
|
const isUnmatchedOrUnparseable = (column.type === "select" || column.type === "reference" || column.type === "money") &&
|
|
264
264
|
coerced === undefined &&
|
|
265
265
|
text.trim() !== "";
|
|
@@ -7,7 +7,7 @@ export type GridCellForFieldProps = {
|
|
|
7
7
|
readonly issues: readonly FieldIssue[] | undefined;
|
|
8
8
|
readonly onChange: (value: unknown) => void;
|
|
9
9
|
readonly GridCell: ReturnType<typeof usePrimitives>["GridCell"];
|
|
10
|
-
/**
|
|
10
|
+
/** Passed through so Reference fields can build the correct
|
|
11
11
|
* lookup query QN (`<feature>:query:<refEntity>:list`). */
|
|
12
12
|
readonly featureName: string;
|
|
13
13
|
readonly labelAppendix?: ReactNode;
|
|
@@ -8,7 +8,7 @@ import { usePrimitives } from "../primitives.js";
|
|
|
8
8
|
import { RenderEdit } from "./render-edit.js";
|
|
9
9
|
// Hosts a target entity's create-form inside a bare Modal so a reference
|
|
10
10
|
// field can create a missing record without leaving the current form
|
|
11
|
-
//
|
|
11
|
+
// — same create wiring as EntityEditCreateBody
|
|
12
12
|
// (kumiko-screen.tsx), but reports the new id back via a callback instead
|
|
13
13
|
// of navigating to the entity's list screen.
|
|
14
14
|
function entityWriteCommand(featureName, entity) {
|
|
@@ -17,7 +17,7 @@ function entityWriteCommand(featureName, entity) {
|
|
|
17
17
|
// The create handler's success payload is `{ kind: "save", id, ... }`
|
|
18
18
|
// (see event-store-executor-write.ts) but RenderEdit's onSubmit only
|
|
19
19
|
// types it as `unknown` — narrow defensively instead of casting through it.
|
|
20
|
-
// `field` is author-declared (ActionFormRedirect.idFrom
|
|
20
|
+
// `field` is author-declared (ActionFormRedirect.idFrom), never
|
|
21
21
|
// user input; the own-property check keeps a prototype key like
|
|
22
22
|
// "constructor" from resolving to anything.
|
|
23
23
|
export function extractIdField(data, field) {
|
|
@@ -36,7 +36,7 @@ export function ReferenceCreateDialog({ open, onClose, onCreated, featureName, s
|
|
|
36
36
|
const t = useTranslation();
|
|
37
37
|
const entityDefaultCurrency = entity.defaultCurrency ?? "EUR";
|
|
38
38
|
const tenantCurrencyFieldNames = useMemo(() => (open ? tenantCurrencyMoneyFieldNames(entity.fields) : []), [open, entity.fields]);
|
|
39
|
-
// Same tenant-currency resolution as the entityEdit create screen
|
|
39
|
+
// Same tenant-currency resolution as the entityEdit create screen,
|
|
40
40
|
// so a `currency: { kind: "tenant" }` field seeds identically via this dialog.
|
|
41
41
|
const { overrides: moneyCurrencyOverrides, loading: currencyLoading } = useMoneyCurrencyOverrides(entity.fields, tenantCurrencyFieldNames, entityDefaultCurrency);
|
|
42
42
|
const initial = useMemo(() => buildInitialValues(entity.fields, entityDefaultCurrency, moneyCurrencyOverrides), [entity.fields, entityDefaultCurrency, moneyCurrencyOverrides]);
|
|
@@ -13,7 +13,7 @@ export declare function RelatedListSection({ section, parentId, record, featureN
|
|
|
13
13
|
readonly hideTitle?: boolean;
|
|
14
14
|
/** Under a fixed-height screen the tab panel chain stretches so the count footer sits at the bottom. */
|
|
15
15
|
readonly grow?: boolean;
|
|
16
|
-
/** Opens a drawer-kind rowAction
|
|
16
|
+
/** Opens a drawer-kind rowAction. Supplied by the parent
|
|
17
17
|
* (ProjectionDetailBody), which owns schema + the actual Drawer render —
|
|
18
18
|
* this component only ever invokes the callback. */
|
|
19
19
|
readonly onOpenDrawer?: (action: RowActionDrawer, initialValues: Readonly<Record<string, unknown>> | undefined) => void;
|
|
@@ -22,8 +22,21 @@ import { usePrimitives, } from "../primitives.js";
|
|
|
22
22
|
import { sortByAccessor } from "../sort-by-accessor.js";
|
|
23
23
|
import { RenderEditActionButton } from "./render-edit-action-button.js";
|
|
24
24
|
import { RenderList } from "./render-list.js";
|
|
25
|
+
// Keyed on the type, not on key presence: most factories omit `sortable`
|
|
26
|
+
// from the definition, so `"sortable" in field` would drop the column flag.
|
|
27
|
+
const NON_SORTABLE_FIELD_TYPES = new Set([
|
|
28
|
+
"longText",
|
|
29
|
+
"multiSelect",
|
|
30
|
+
"embedded",
|
|
31
|
+
"jsonb",
|
|
32
|
+
"tz",
|
|
33
|
+
"file",
|
|
34
|
+
"image",
|
|
35
|
+
"files",
|
|
36
|
+
"images",
|
|
37
|
+
]);
|
|
25
38
|
function isSortableCapable(field) {
|
|
26
|
-
return
|
|
39
|
+
return !NON_SORTABLE_FIELD_TYPES.has(field.type);
|
|
27
40
|
}
|
|
28
41
|
const RELATED_LIST_PSEUDO_ENTITY = "__related-list__";
|
|
29
42
|
// Minimal EntityDefinition from the section's own columns — same shape as
|
|
@@ -338,7 +351,7 @@ export function RelatedListSection({ section, parentId, record, featureName, tra
|
|
|
338
351
|
// A truncated fetch means `sortedRows` is a sort of a partial set, not of
|
|
339
352
|
// the full related-row set — the client-side sort above (or even plain
|
|
340
353
|
// unsorted display) would silently claim "these are the top N" when they
|
|
341
|
-
// are really just "the first N in the server's own order"
|
|
354
|
+
// are really just "the first N in the server's own order".
|
|
342
355
|
// `nextCursor` is exactly how the paged envelope marks that: non-null means
|
|
343
356
|
// more rows exist server-side beyond what was fetched.
|
|
344
357
|
// A handler that omits `nextCursor` (or reports a `total` the fetched rows
|
|
@@ -10,15 +10,13 @@ export declare function RenderEditActionConfirmDialog({ action, open, onOpenChan
|
|
|
10
10
|
readonly onConfirm: () => void | Promise<void>;
|
|
11
11
|
readonly Dialog: ReturnType<typeof usePrimitives>["Dialog"];
|
|
12
12
|
}): ReactNode;
|
|
13
|
-
export declare function RenderEditActionButton({ action, iconOnly, hideIcon,
|
|
13
|
+
export declare function RenderEditActionButton({ action, iconOnly, hideIcon, Button, Dialog, onError, }: {
|
|
14
14
|
readonly action: RenderEditAction;
|
|
15
15
|
/** Group-level collapse (see `shouldRenderActionsIconOnly`) — only takes
|
|
16
16
|
* effect when this action actually resolved an icon. */
|
|
17
17
|
readonly iconOnly?: boolean;
|
|
18
18
|
/** Record-header actions are text-only; the icon still shows when collapsed to icon-only. */
|
|
19
19
|
readonly hideIcon?: boolean;
|
|
20
|
-
/** Short labelled button for a phone header, where a bare icon would hide what the action does. */
|
|
21
|
-
readonly compactLabel?: boolean;
|
|
22
20
|
readonly Button: ReturnType<typeof usePrimitives>["Button"];
|
|
23
21
|
readonly Dialog: ReturnType<typeof usePrimitives>["Dialog"];
|
|
24
22
|
readonly onError: (notice: ErrorNotice | null) => void;
|
|
@@ -15,7 +15,7 @@ export function RenderEditActionConfirmDialog({ action, open, onOpenChange, onCo
|
|
|
15
15
|
// render-list.tsx's ToolbarActionView (each RenderEditAction is
|
|
16
16
|
// independently bound by the caller, there is no shared trigger pipeline
|
|
17
17
|
// to hook into like the built-in onDelete/onSubmit paths have).
|
|
18
|
-
export function RenderEditActionButton({ action, iconOnly = false, hideIcon = false,
|
|
18
|
+
export function RenderEditActionButton({ action, iconOnly = false, hideIcon = false, Button, Dialog, onError, }) {
|
|
19
19
|
const [busy, setBusy] = useState(false);
|
|
20
20
|
const [confirmOpen, setConfirmOpen] = useState(false);
|
|
21
21
|
const trigger = async () => {
|
|
@@ -33,7 +33,7 @@ export function RenderEditActionButton({ action, iconOnly = false, hideIcon = fa
|
|
|
33
33
|
};
|
|
34
34
|
const variant = action.style ?? "secondary";
|
|
35
35
|
const showIconOnly = iconOnly && action.icon !== undefined;
|
|
36
|
-
return (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", variant: variant, loading: busy, ...(action.icon !== undefined && (!hideIcon || showIconOnly) && { icon: action.icon }), ...(
|
|
36
|
+
return (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", variant: variant, loading: busy, ...(action.icon !== undefined && (!hideIcon || showIconOnly) && { icon: action.icon }), ...(showIconOnly && {
|
|
37
37
|
size: "icon",
|
|
38
38
|
ariaLabel: action.label,
|
|
39
39
|
title: action.label,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Hides the Save button when no field is editable and no extension section
|
|
2
2
|
// contributes to the composed form submit. Extensions that persist via their
|
|
3
|
-
// own dispatcher writes must not opt in
|
|
3
|
+
// own dispatcher writes must not opt in.
|
|
4
4
|
// Explicit-positive per kind (653/1), not `s.kind !== "fields"` — a future
|
|
5
5
|
// third EditSectionViewModel member would otherwise default to "editable"
|
|
6
6
|
// without anyone deciding that on purpose.
|
|
@@ -56,7 +56,7 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
|
|
|
56
56
|
/** Fires when the form gains or loses unsaved input (field changes or a
|
|
57
57
|
* dirty extension section). Lets a host such as a drawer guard closing. */
|
|
58
58
|
readonly onDirtyChange?: (dirty: boolean) => void;
|
|
59
|
-
/** Copy-link action
|
|
59
|
+
/** Copy-link action — only set in update mode (create mode
|
|
60
60
|
* has no entity id yet, hence no permalink). The callback is already fully
|
|
61
61
|
* bound (URL building + clipboard happen outside, in
|
|
62
62
|
* `@cosmicdrift/kumiko-renderer/web`'s RoutedScreen — this platform-neutral
|
|
@@ -74,7 +74,7 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
|
|
|
74
74
|
* header menu (drawer, card form, shell without a header slot) they fall
|
|
75
75
|
* back to buttons next to `actions`. */
|
|
76
76
|
readonly menuActions?: readonly RenderEditAction[];
|
|
77
|
-
/** Opens a relatedList section row's drawer-kind action
|
|
77
|
+
/** Opens a relatedList section row's drawer-kind action.
|
|
78
78
|
* RenderEdit has no `schema` to resolve the target actionForm itself —
|
|
79
79
|
* the caller (ProjectionDetailBody, which does have schema) supplies the
|
|
80
80
|
* opener and owns the actual Drawer state/rendering. Only projectionDetail
|
|
@@ -117,7 +117,7 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
|
|
|
117
117
|
/** Edit-mode "changed" markers also for screens without an entity id
|
|
118
118
|
* (configEdit loads its values from a query, not a record). */
|
|
119
119
|
readonly markChangedFields?: boolean;
|
|
120
|
-
/** Controlled mode
|
|
120
|
+
/** Controlled mode: fires on every values-snapshot change
|
|
121
121
|
* (typing, `patch(...)` from outside) with the current values. `changes`
|
|
122
122
|
* is the delta against the initial values — same semantics as
|
|
123
123
|
* `payloadMode: "changes"` — so a caller never overwrites unseen fields.
|
|
@@ -134,7 +134,7 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
|
|
|
134
134
|
* happens, so a caller patching dependent fields from inside `onChange`
|
|
135
135
|
* never has to guard against `controls` being undefined. */
|
|
136
136
|
readonly onChange?: (state: RenderEditChangeState<TValues>) => void;
|
|
137
|
-
/** Controlled mode
|
|
137
|
+
/** Controlled mode: called once after mount, hands the
|
|
138
138
|
* caller `patch`/`validate`/`getValues` bound to this RenderEdit
|
|
139
139
|
* instance — addressable from outside without a remount. `patch` merges
|
|
140
140
|
* only the given keys (existing `controller.setValues` semantics),
|
|
@@ -153,7 +153,7 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
|
|
|
153
153
|
* validation scope (the underlying `useForm` controller is mount-lived);
|
|
154
154
|
* rendering and `controls.validate()` do stay reactive to later changes. */
|
|
155
155
|
readonly fields?: readonly string[];
|
|
156
|
-
/** Locked state
|
|
156
|
+
/** Locked state: every rendered field and the submit button
|
|
157
157
|
* go visibly inactive, no write possible. For cases where input becomes
|
|
158
158
|
* moot — e.g. Solon's editor pointing at an existing record instead of
|
|
159
159
|
* creating a new one. Extension sections are out of scope: RenderEdit has
|
|
@@ -177,7 +177,7 @@ export type RenderEditProps<TValues extends FormValues, TCtx = unknown> = {
|
|
|
177
177
|
/** "form" (default) — every field renders as its Input widget, disabled
|
|
178
178
|
* when `field.readOnly`, unchanged behavior. "text" renders a
|
|
179
179
|
* `field.readOnly` field as plain text instead of a disabled Input
|
|
180
|
-
* (ProjectionDetailBody's read view
|
|
180
|
+
* (ProjectionDetailBody's read view) — editable fields are
|
|
181
181
|
* unaffected either way, so this only changes forms that already have
|
|
182
182
|
* readOnly fields. */
|
|
183
183
|
readonly valueDisplay?: "form" | "text";
|