@cosmicdrift/kumiko-renderer 0.328.0 → 0.328.1
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/dist/app/action-form-shim.d.ts +14 -0
- package/{src/app/action-form-shim.ts → dist/app/action-form-shim.js} +26 -46
- package/dist/app/app-features-context.d.ts +8 -0
- package/dist/app/app-features-context.js +16 -0
- package/dist/app/column-renderers.d.ts +30 -0
- package/dist/app/column-renderers.js +34 -0
- package/dist/app/config-edit-shim.d.ts +11 -0
- package/{src/app/config-edit-shim.ts → dist/app/config-edit-shim.js} +12 -25
- package/dist/app/content-editors.d.ts +38 -0
- package/dist/app/content-editors.js +26 -0
- package/dist/app/content-preview.d.ts +11 -0
- package/dist/app/content-preview.js +30 -0
- package/dist/app/custom-screens.d.ts +11 -0
- package/dist/app/custom-screens.js +20 -0
- package/dist/app/dashboard-body.d.ts +16 -0
- package/dist/app/dashboard-body.js +11 -0
- package/dist/app/extension-form-submit.d.ts +32 -0
- package/dist/app/extension-form-submit.js +73 -0
- package/dist/app/extension-sections.d.ts +72 -0
- package/dist/app/extension-sections.js +34 -0
- package/dist/app/feature-schema.d.ts +4 -0
- package/{src/app/feature-schema.ts → dist/app/feature-schema.js} +12 -22
- package/dist/app/form-schema.d.ts +4 -0
- package/dist/app/form-schema.js +85 -0
- package/dist/app/kumiko-screen.d.ts +43 -0
- package/dist/app/kumiko-screen.js +2206 -0
- package/dist/app/layout-fields.d.ts +3 -0
- package/dist/app/layout-fields.js +20 -0
- package/dist/app/list-facets.d.ts +48 -0
- package/dist/app/list-facets.js +185 -0
- package/dist/app/nav.d.ts +61 -0
- package/dist/app/nav.js +139 -0
- package/dist/app/projection-detail-shim.d.ts +8 -0
- package/dist/app/projection-detail-shim.js +71 -0
- package/dist/app/projection-list-shim.d.ts +13 -0
- package/{src/app/projection-list-shim.ts → dist/app/projection-list-shim.js} +26 -41
- package/dist/app/qn.d.ts +7 -0
- package/{src/app/qn.ts → dist/app/qn.js} +9 -11
- package/dist/app/qualify-screen-id.d.ts +2 -0
- package/{src/app/qualify-screen-id.ts → dist/app/qualify-screen-id.js} +5 -6
- package/dist/app/reference-facet-bridge.d.ts +10 -0
- package/dist/app/reference-facet-bridge.js +22 -0
- package/dist/app/return-to.d.ts +31 -0
- package/dist/app/return-to.js +188 -0
- package/dist/app/row-actions.d.ts +77 -0
- package/dist/app/row-actions.js +416 -0
- package/dist/app/screen-access.d.ts +8 -0
- package/dist/app/screen-access.js +48 -0
- package/dist/app/secret-mint-body.d.ts +10 -0
- package/dist/app/secret-mint-body.js +140 -0
- package/dist/app/secrets-edit-body.d.ts +8 -0
- package/dist/app/secrets-edit-body.js +76 -0
- package/dist/app/use-embedded-screen.d.ts +6 -0
- package/dist/app/use-embedded-screen.js +19 -0
- package/dist/app/variable-chips.d.ts +7 -0
- package/dist/app/variable-chips.js +16 -0
- package/dist/app/write-failed-error.d.ts +6 -0
- package/dist/app/write-failed-error.js +21 -0
- package/dist/components/embedded-list-field.d.ts +10 -0
- package/dist/components/embedded-list-field.js +259 -0
- package/dist/components/grid-cell-for-field.d.ts +25 -0
- package/dist/components/grid-cell-for-field.js +31 -0
- package/dist/components/multi-select-checkboxes.d.ts +14 -0
- package/dist/components/multi-select-checkboxes.js +36 -0
- package/dist/components/reference-create-dialog.d.ts +13 -0
- package/dist/components/reference-create-dialog.js +46 -0
- package/dist/components/related-list-section.d.ts +25 -0
- package/dist/components/related-list-section.js +273 -0
- package/dist/components/render-edit-action-button.d.ts +22 -0
- package/dist/components/render-edit-action-button.js +43 -0
- package/dist/components/render-edit-logic.d.ts +6 -0
- package/dist/components/render-edit-logic.js +63 -0
- package/dist/components/render-edit-types.d.ts +209 -0
- package/dist/components/render-edit-types.js +1 -0
- package/dist/components/render-edit.d.ts +6 -0
- package/dist/components/render-edit.js +1169 -0
- package/dist/components/render-field.d.ts +38 -0
- package/dist/components/render-field.js +544 -0
- package/dist/components/render-list.d.ts +117 -0
- package/dist/components/render-list.js +232 -0
- package/dist/components/write-form-section.d.ts +17 -0
- package/dist/components/write-form-section.js +104 -0
- package/dist/context/dispatcher-context.d.ts +10 -0
- package/{src/context/dispatcher-context.tsx → dist/context/dispatcher-context.js} +16 -29
- package/dist/context/draft-storage-context.d.ts +12 -0
- package/dist/context/draft-storage-context.js +18 -0
- package/dist/context/user-roles-context.d.ts +7 -0
- package/{src/context/user-roles-context.tsx → dist/context/user-roles-context.js} +7 -15
- package/dist/format-when.d.ts +1 -0
- package/{src/format-when.ts → dist/format-when.js} +7 -7
- package/dist/hooks/reference-limits.d.ts +3 -0
- package/{src/hooks/reference-limits.ts → dist/hooks/reference-limits.js} +0 -1
- package/dist/hooks/use-ai-text.d.ts +52 -0
- package/dist/hooks/use-ai-text.js +90 -0
- package/dist/hooks/use-disclosure.d.ts +7 -0
- package/dist/hooks/use-disclosure.js +8 -0
- package/dist/hooks/use-form.d.ts +7 -0
- package/dist/hooks/use-form.js +36 -0
- package/dist/hooks/use-list-url-state.d.ts +43 -0
- package/dist/hooks/use-list-url-state.js +131 -0
- package/dist/hooks/use-mutation.d.ts +9 -0
- package/dist/hooks/use-mutation.js +37 -0
- package/dist/hooks/use-query.d.ts +14 -0
- package/dist/hooks/use-query.js +118 -0
- package/dist/hooks/use-reference-lookup.d.ts +14 -0
- package/dist/hooks/use-reference-lookup.js +63 -0
- package/dist/hooks/use-store.d.ts +3 -0
- package/dist/hooks/use-store.js +38 -0
- package/dist/hooks/use-stream-handler.d.ts +14 -0
- package/dist/hooks/use-stream-handler.js +93 -0
- package/dist/i18n-defaults.d.ts +2 -0
- package/dist/i18n-defaults.js +201 -0
- package/dist/i18n.d.ts +64 -0
- package/dist/i18n.js +166 -0
- package/dist/index.d.ts +77 -0
- package/dist/index.js +55 -0
- package/dist/inside-drawer.d.ts +2 -0
- package/{src/inside-drawer.ts → dist/inside-drawer.js} +2 -5
- package/dist/page-header-slot.d.ts +2 -0
- package/{src/page-header-slot.ts → dist/page-header-slot.js} +2 -5
- package/dist/primitives.d.ts +1351 -0
- package/dist/primitives.js +73 -0
- package/dist/screen-fills-height.d.ts +3 -0
- package/dist/screen-fills-height.js +3 -0
- package/dist/share-links.d.ts +4 -0
- package/dist/share-links.js +11 -0
- package/dist/sort-by-accessor.d.ts +5 -0
- package/dist/sort-by-accessor.js +16 -0
- package/dist/sse/live-events.d.ts +23 -0
- package/dist/sse/live-events.js +28 -0
- package/dist/tokens.d.ts +83 -0
- package/dist/tokens.js +80 -0
- package/package.json +16 -9
- package/src/__tests__/error-i18n-defaults.test.ts +0 -82
- package/src/__tests__/format-when.test.ts +0 -12
- package/src/__tests__/i18n.test.tsx +0 -283
- package/src/__tests__/merge-search-params-into-initial.test.ts +0 -525
- package/src/__tests__/qn.test.ts +0 -83
- package/src/__tests__/share-links.test.ts +0 -26
- package/src/__tests__/sort-by-accessor.test.ts +0 -48
- package/src/__tests__/stringify-nav-params.test.ts +0 -33
- package/src/__tests__/use-list-url-state.test.tsx +0 -250
- package/src/app/__tests__/action-form-field-labels.test.tsx +0 -92
- package/src/app/__tests__/action-form-shim.test.ts +0 -137
- package/src/app/__tests__/build-list-query-payload.test.ts +0 -107
- package/src/app/__tests__/config-edit-shim.test.ts +0 -57
- package/src/app/__tests__/content-editors.test.tsx +0 -128
- package/src/app/__tests__/content-preview.test.tsx +0 -184
- package/src/app/__tests__/entity-edit-create-search-params.test.tsx +0 -236
- package/src/app/__tests__/entity-edit-fill-height.test.tsx +0 -118
- package/src/app/__tests__/entity-edit-jsonb-embedded-submit.test.tsx +0 -239
- package/src/app/__tests__/entity-list-create-singleton.test.tsx +0 -128
- package/src/app/__tests__/entity-list-row-action-entity-target.test.tsx +0 -424
- package/src/app/__tests__/entity-list-row-action-icon.test.tsx +0 -248
- package/src/app/__tests__/entity-list-row-action-kumiko-actions-view.test.tsx +0 -162
- package/src/app/__tests__/entity-list-row-action-refetch.test.tsx +0 -401
- package/src/app/__tests__/entity-list-screen-padding.test.tsx +0 -128
- package/src/app/__tests__/feature-schema.test.ts +0 -40
- package/src/app/__tests__/form-schema.test.ts +0 -247
- package/src/app/__tests__/list-daterange-facet.test.ts +0 -165
- package/src/app/__tests__/list-filter-facets.test.tsx +0 -521
- package/src/app/__tests__/nav.test.ts +0 -198
- package/src/app/__tests__/projection-detail-actions.test.tsx +0 -719
- package/src/app/__tests__/projection-detail-reference-field.test.tsx +0 -165
- package/src/app/__tests__/projection-detail-shim.test.ts +0 -188
- package/src/app/__tests__/projection-detail-singleton.test.tsx +0 -191
- package/src/app/__tests__/projection-list-bad-shape.test.tsx +0 -152
- package/src/app/__tests__/projection-list-reference-column.test.tsx +0 -219
- package/src/app/__tests__/projection-list-search-sort.test.tsx +0 -212
- package/src/app/__tests__/qualified-names.test.ts +0 -37
- package/src/app/__tests__/return-to.test.ts +0 -396
- package/src/app/__tests__/row-actions-navigate-tab.test.ts +0 -223
- package/src/app/__tests__/row-default-edit-action.test.tsx +0 -324
- package/src/app/__tests__/screen-access-allows.test.ts +0 -34
- package/src/app/__tests__/secret-mint-body.test.tsx +0 -404
- package/src/app/__tests__/variable-chips.test.tsx +0 -70
- package/src/app/app-features-context.tsx +0 -25
- package/src/app/column-renderers.tsx +0 -68
- package/src/app/content-editors.tsx +0 -85
- package/src/app/content-preview.tsx +0 -67
- package/src/app/custom-screens.tsx +0 -29
- package/src/app/dashboard-body.tsx +0 -33
- package/src/app/extension-form-submit.tsx +0 -123
- package/src/app/extension-sections.tsx +0 -111
- package/src/app/form-schema.ts +0 -91
- package/src/app/kumiko-screen.tsx +0 -3801
- package/src/app/layout-fields.ts +0 -31
- package/src/app/list-facets.ts +0 -259
- package/src/app/nav.tsx +0 -258
- package/src/app/projection-detail-shim.ts +0 -86
- package/src/app/reference-facet-bridge.tsx +0 -58
- package/src/app/return-to.tsx +0 -234
- package/src/app/row-actions.ts +0 -640
- package/src/app/screen-access.ts +0 -63
- package/src/app/secret-mint-body.tsx +0 -259
- package/src/app/secrets-edit-body.tsx +0 -194
- package/src/app/use-embedded-screen.ts +0 -27
- package/src/app/variable-chips.tsx +0 -39
- package/src/app/write-failed-error.ts +0 -27
- package/src/components/__tests__/embedded-list-field.test.tsx +0 -531
- package/src/components/__tests__/multi-select-checkboxes.test.tsx +0 -162
- package/src/components/__tests__/related-list-section-empty-state.test.tsx +0 -143
- package/src/components/__tests__/related-list-section.test.tsx +0 -1733
- package/src/components/__tests__/render-edit-action-button.test.tsx +0 -205
- package/src/components/__tests__/render-edit-hidden-section.test.tsx +0 -128
- package/src/components/__tests__/render-edit-logic.test.ts +0 -314
- package/src/components/__tests__/render-edit-screen-description.test.tsx +0 -152
- package/src/components/__tests__/render-edit-submit-actions.test.tsx +0 -460
- package/src/components/__tests__/render-field-app-locale.test.tsx +0 -142
- package/src/components/__tests__/render-field-enum-option.test.tsx +0 -99
- package/src/components/__tests__/render-field-icon-derivation.test.tsx +0 -119
- package/src/components/__tests__/render-field-json-format.test.tsx +0 -164
- package/src/components/__tests__/render-field-money-embedded-list-consistency.test.tsx +0 -212
- package/src/components/__tests__/render-field-money-roundtrip.test.tsx +0 -217
- package/src/components/__tests__/render-field-number-grouping.test.tsx +0 -76
- package/src/components/__tests__/render-field-number-unit.test.tsx +0 -227
- package/src/components/__tests__/render-field-password-format.test.tsx +0 -101
- package/src/components/__tests__/render-field-select-display.test.tsx +0 -134
- package/src/components/__tests__/render-field-unit-format.test.tsx +0 -107
- package/src/components/__tests__/render-field-unsupported-types.test.tsx +0 -195
- package/src/components/__tests__/render-list-toolbar-icon-only.test.tsx +0 -146
- package/src/components/__tests__/write-form-section.test.tsx +0 -375
- package/src/components/embedded-list-field.tsx +0 -354
- package/src/components/grid-cell-for-field.tsx +0 -88
- package/src/components/multi-select-checkboxes.tsx +0 -93
- package/src/components/reference-create-dialog.tsx +0 -91
- package/src/components/related-list-section.tsx +0 -491
- package/src/components/render-edit-action-button.tsx +0 -109
- package/src/components/render-edit-logic.ts +0 -85
- package/src/components/render-edit-types.ts +0 -226
- package/src/components/render-edit.tsx +0 -1871
- package/src/components/render-field.tsx +0 -951
- package/src/components/render-list.tsx +0 -593
- package/src/components/write-form-section.tsx +0 -198
- package/src/context/draft-storage-context.tsx +0 -39
- package/src/hooks/__tests__/use-ai-text.test.tsx +0 -177
- package/src/hooks/__tests__/use-disclosure.test.tsx +0 -25
- package/src/hooks/__tests__/use-form.test.tsx +0 -65
- package/src/hooks/__tests__/use-mutation.test.tsx +0 -77
- package/src/hooks/__tests__/use-query.test.tsx +0 -149
- package/src/hooks/__tests__/use-stream-handler.test.tsx +0 -107
- package/src/hooks/use-ai-text.ts +0 -172
- package/src/hooks/use-disclosure.ts +0 -20
- package/src/hooks/use-form.ts +0 -78
- package/src/hooks/use-list-url-state.ts +0 -207
- package/src/hooks/use-mutation.ts +0 -61
- package/src/hooks/use-query.ts +0 -166
- package/src/hooks/use-reference-lookup.ts +0 -76
- package/src/hooks/use-store.ts +0 -48
- package/src/hooks/use-stream-handler.ts +0 -134
- package/src/i18n-defaults.ts +0 -240
- package/src/i18n.tsx +0 -286
- package/src/index.ts +0 -279
- package/src/primitives.tsx +0 -1567
- package/src/screen-fills-height.ts +0 -3
- package/src/share-links.ts +0 -12
- package/src/sort-by-accessor.ts +0 -20
- package/src/sse/live-events.tsx +0 -56
- package/src/tokens.tsx +0 -170
|
@@ -1,354 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
EditFieldViewModel,
|
|
3
|
-
EmbeddedListCellViewModel,
|
|
4
|
-
FieldIssue,
|
|
5
|
-
} from "@cosmicdrift/kumiko-headless";
|
|
6
|
-
import {
|
|
7
|
-
computeDerivedCellValue,
|
|
8
|
-
currencyDecimals,
|
|
9
|
-
groupEmbeddedListIssues,
|
|
10
|
-
roundDerivedCellValue,
|
|
11
|
-
sumEmbeddedListColumn,
|
|
12
|
-
} from "@cosmicdrift/kumiko-headless";
|
|
13
|
-
import { type ReactNode, useState } from "react";
|
|
14
|
-
import { REFERENCE_COMBOBOX_LIMIT } from "../hooks/reference-limits";
|
|
15
|
-
import { useQuery } from "../hooks/use-query";
|
|
16
|
-
import { referenceLookupSource } from "../hooks/use-reference-lookup";
|
|
17
|
-
import { useTranslation } from "../i18n";
|
|
18
|
-
import type { EmbeddedListColumn, EmbeddedListTotal } from "../primitives";
|
|
19
|
-
import { usePrimitives } from "../primitives";
|
|
20
|
-
|
|
21
|
-
export type EmbeddedListFieldProps = {
|
|
22
|
-
readonly field: EditFieldViewModel;
|
|
23
|
-
readonly id: string;
|
|
24
|
-
readonly onChange: (value: unknown) => void;
|
|
25
|
-
readonly allIssues: Readonly<Record<string, readonly FieldIssue[]>>;
|
|
26
|
-
readonly featureName: string;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
type EmbeddedRow = Readonly<Record<string, unknown>>;
|
|
30
|
-
|
|
31
|
-
function withRecomputedDerived(
|
|
32
|
-
row: EmbeddedRow,
|
|
33
|
-
derived: EditFieldViewModel["embeddedListDerived"],
|
|
34
|
-
cells: readonly EmbeddedListCellViewModel[],
|
|
35
|
-
): EmbeddedRow {
|
|
36
|
-
if (derived === undefined) return row;
|
|
37
|
-
// Read sources from an unchanged snapshot, never from `result` (which this
|
|
38
|
-
// loop mutates) — mirrors withDerivedCells server-side so chained derived
|
|
39
|
-
// cells (e.g. net=qty*price, then gross=sum(net,tax)) can't drift between
|
|
40
|
-
// client preview and server depending on Object.entries(derived) order.
|
|
41
|
-
const source = { ...row };
|
|
42
|
-
const result: Record<string, unknown> = { ...row };
|
|
43
|
-
for (const [derivedField, def] of Object.entries(derived)) {
|
|
44
|
-
const values = def.from.map((src) => {
|
|
45
|
-
const v = source[src];
|
|
46
|
-
return typeof v === "number" ? v : undefined;
|
|
47
|
-
});
|
|
48
|
-
const computed = computeDerivedCellValue(def.op, values);
|
|
49
|
-
const target = cells.find((c) => c.field === derivedField);
|
|
50
|
-
result[derivedField] =
|
|
51
|
-
computed !== undefined && target !== undefined
|
|
52
|
-
? roundDerivedCellValue(computed, target)
|
|
53
|
-
: computed;
|
|
54
|
-
}
|
|
55
|
-
return result;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Locale-aware decimal parse for pasted text: strips space/NBSP thousands
|
|
59
|
-
// separators outright, then treats whichever of "," or "." appears LAST in
|
|
60
|
-
// the string as the decimal separator and strips the other one — handles
|
|
61
|
-
// both "1.234,56" (de) and "1,234.56" (en-US) pastes. Returns undefined for
|
|
62
|
-
// anything that still doesn't parse as a finite number.
|
|
63
|
-
function parseLocaleAwareDecimal(raw: string): number | undefined {
|
|
64
|
-
const stripped = raw.replace(/[\s ]/g, "");
|
|
65
|
-
if (stripped === "") return undefined;
|
|
66
|
-
const lastComma = stripped.lastIndexOf(",");
|
|
67
|
-
const lastDot = stripped.lastIndexOf(".");
|
|
68
|
-
const decimalIndex = Math.max(lastComma, lastDot);
|
|
69
|
-
if (decimalIndex === -1) {
|
|
70
|
-
const n = Number(stripped);
|
|
71
|
-
return Number.isFinite(n) ? n : undefined;
|
|
72
|
-
}
|
|
73
|
-
const integerPart = stripped.slice(0, decimalIndex).replace(/[.,]/g, "");
|
|
74
|
-
const fractionPart = stripped.slice(decimalIndex + 1);
|
|
75
|
-
if (!/^\d*$/.test(integerPart) || !/^\d*$/.test(fractionPart)) return undefined;
|
|
76
|
-
const n = Number(
|
|
77
|
-
`${integerPart === "" ? "0" : integerPart}.${fractionPart === "" ? "0" : fractionPart}`,
|
|
78
|
-
);
|
|
79
|
-
return Number.isFinite(n) ? n : undefined;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function coerceCellValue(column: EmbeddedListColumn, text: string, currency: string): unknown {
|
|
83
|
-
switch (column.type) {
|
|
84
|
-
case "text":
|
|
85
|
-
return text;
|
|
86
|
-
case "number":
|
|
87
|
-
case "decimal": {
|
|
88
|
-
if (text.trim() === "") return undefined;
|
|
89
|
-
const n = Number(text);
|
|
90
|
-
return Number.isFinite(n) ? n : undefined;
|
|
91
|
-
}
|
|
92
|
-
case "money": {
|
|
93
|
-
// Money cells are minor-unit integers (cents) in storage — paste
|
|
94
|
-
// arrives as a major-unit decimal string ("12,99"/"12.99"), so scale
|
|
95
|
-
// by the currency's decimal places. Must agree with the typed-in path
|
|
96
|
-
// (renderCellControl's MoneyInput, which scales by the same
|
|
97
|
-
// currencyDecimals) — a hardcoded ×100 here diverged for zero-/three-
|
|
98
|
-
// decimal currencies (JPY, BHD, ...), landing a pasted value 100x off
|
|
99
|
-
// from the same value typed by hand (kumiko-framework#1972).
|
|
100
|
-
// parseLocaleAwareDecimal handles both "1.234,56" (de) and
|
|
101
|
-
// "1,234.56" (en-US) thousands/decimal conventions — a naive
|
|
102
|
-
// `replace(",", ".")` mangled DE-locale pastes with thousands
|
|
103
|
-
// separators into NaN (kumiko-framework#1838).
|
|
104
|
-
if (text.trim() === "") return undefined;
|
|
105
|
-
const n = parseLocaleAwareDecimal(text);
|
|
106
|
-
return n === undefined ? undefined : Math.round(n * 10 ** currencyDecimals(currency));
|
|
107
|
-
}
|
|
108
|
-
case "boolean":
|
|
109
|
-
return ["true", "1", "yes", "y", "ja"].includes(text.trim().toLowerCase());
|
|
110
|
-
case "date":
|
|
111
|
-
case "timestamp":
|
|
112
|
-
return text.trim();
|
|
113
|
-
case "select": {
|
|
114
|
-
const match = (column.options ?? []).find(
|
|
115
|
-
(opt) => opt === text || column.optionLabels?.[opt] === text,
|
|
116
|
-
);
|
|
117
|
-
return match;
|
|
118
|
-
}
|
|
119
|
-
case "reference": {
|
|
120
|
-
const match = (column.referenceOptions ?? []).find(
|
|
121
|
-
(o) => o.label === text || o.value === text,
|
|
122
|
-
);
|
|
123
|
-
return match?.value;
|
|
124
|
-
}
|
|
125
|
-
default: {
|
|
126
|
-
const exhaustiveCheck: never = column.type;
|
|
127
|
-
return exhaustiveCheck;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
export function EmbeddedListField({
|
|
133
|
-
field,
|
|
134
|
-
id,
|
|
135
|
-
onChange,
|
|
136
|
-
allIssues,
|
|
137
|
-
featureName,
|
|
138
|
-
}: EmbeddedListFieldProps): ReactNode {
|
|
139
|
-
const { EmbeddedListInput } = usePrimitives();
|
|
140
|
-
const t = useTranslation();
|
|
141
|
-
const [pasteWarning, setPasteWarning] = useState<{
|
|
142
|
-
readonly droppedRows: number;
|
|
143
|
-
readonly unmatchedCells: number;
|
|
144
|
-
} | null>(null);
|
|
145
|
-
|
|
146
|
-
const cells = field.embeddedListCells ?? [];
|
|
147
|
-
const rows = Array.isArray(field.value) ? (field.value as readonly EmbeddedRow[]) : [];
|
|
148
|
-
const derived = field.embeddedListDerived;
|
|
149
|
-
|
|
150
|
-
const referenceCells = cells.filter((c) => c.type === "reference");
|
|
151
|
-
const referenceSources = referenceCells.map((cell) =>
|
|
152
|
-
cell.refOptionsQuery !== undefined
|
|
153
|
-
? { queryQn: cell.refOptionsQuery, labelKey: "label" }
|
|
154
|
-
: referenceLookupSource(
|
|
155
|
-
cell.refFeature ?? featureName,
|
|
156
|
-
cell.refEntity ?? "",
|
|
157
|
-
cell.refLabelField ?? "id",
|
|
158
|
-
),
|
|
159
|
-
);
|
|
160
|
-
const referenceQueries = referenceSources.map((source) =>
|
|
161
|
-
// biome-ignore lint/correctness/useHookAtTopLevel: referenceCells comes from the entity-schema definition — fixed for the screen's lifetime, not a real conditional-hook risk.
|
|
162
|
-
useQuery<{ rows: ReadonlyArray<Record<string, unknown>> }>(source.queryQn, {
|
|
163
|
-
limit: REFERENCE_COMBOBOX_LIMIT,
|
|
164
|
-
}),
|
|
165
|
-
);
|
|
166
|
-
|
|
167
|
-
if (EmbeddedListInput === undefined) return null;
|
|
168
|
-
|
|
169
|
-
const columns: EmbeddedListColumn[] = cells.map((cell) => {
|
|
170
|
-
const isDerived = derived?.[cell.field] !== undefined;
|
|
171
|
-
if (cell.type === "reference") {
|
|
172
|
-
const idx = referenceCells.indexOf(cell);
|
|
173
|
-
const query = referenceQueries[idx];
|
|
174
|
-
const labelKey = referenceSources[idx]?.labelKey ?? "id";
|
|
175
|
-
const referenceOptions = (query?.data?.rows ?? []).map((row) => ({
|
|
176
|
-
value: String(row["id"] ?? ""),
|
|
177
|
-
label: String(row[labelKey] ?? row["id"] ?? ""),
|
|
178
|
-
}));
|
|
179
|
-
return {
|
|
180
|
-
field: cell.field,
|
|
181
|
-
label: cell.label,
|
|
182
|
-
type: cell.type,
|
|
183
|
-
required: cell.required,
|
|
184
|
-
derived: isDerived,
|
|
185
|
-
referenceOptions,
|
|
186
|
-
referenceLoading: query?.loading ?? false,
|
|
187
|
-
};
|
|
188
|
-
}
|
|
189
|
-
return {
|
|
190
|
-
field: cell.field,
|
|
191
|
-
label: cell.label,
|
|
192
|
-
type: cell.type,
|
|
193
|
-
required: cell.required,
|
|
194
|
-
derived: isDerived,
|
|
195
|
-
...(cell.options !== undefined && { options: cell.options }),
|
|
196
|
-
...(cell.optionLabels !== undefined && { optionLabels: cell.optionLabels }),
|
|
197
|
-
};
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
const totals: EmbeddedListTotal[] = (field.embeddedListTotals ?? []).map((subFieldName) => {
|
|
201
|
-
const cell = cells.find((c) => c.field === subFieldName);
|
|
202
|
-
return {
|
|
203
|
-
field: subFieldName,
|
|
204
|
-
label: cell?.label ?? subFieldName,
|
|
205
|
-
value: sumEmbeddedListColumn(rows, subFieldName),
|
|
206
|
-
};
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const { listIssues, rowIssues, cellIssues } = groupEmbeddedListIssues(allIssues, field.field);
|
|
210
|
-
const combinedListIssues: readonly FieldIssue[] =
|
|
211
|
-
pasteWarning === null
|
|
212
|
-
? listIssues
|
|
213
|
-
: [
|
|
214
|
-
...listIssues,
|
|
215
|
-
...(pasteWarning.droppedRows > 0
|
|
216
|
-
? [
|
|
217
|
-
{
|
|
218
|
-
path: field.field,
|
|
219
|
-
code: "paste-rows-truncated",
|
|
220
|
-
i18nKey: "kumiko.field.embedded-list.paste-rows-truncated",
|
|
221
|
-
params: { count: pasteWarning.droppedRows },
|
|
222
|
-
},
|
|
223
|
-
]
|
|
224
|
-
: []),
|
|
225
|
-
...(pasteWarning.unmatchedCells > 0
|
|
226
|
-
? [
|
|
227
|
-
{
|
|
228
|
-
path: field.field,
|
|
229
|
-
code: "paste-cells-unmatched",
|
|
230
|
-
i18nKey: "kumiko.field.embedded-list.paste-cells-unmatched",
|
|
231
|
-
params: { count: pasteWarning.unmatchedCells },
|
|
232
|
-
},
|
|
233
|
-
]
|
|
234
|
-
: []),
|
|
235
|
-
];
|
|
236
|
-
|
|
237
|
-
function replaceRow(rowIndex: number, updater: (row: EmbeddedRow) => EmbeddedRow): void {
|
|
238
|
-
const nextRows = rows.map((row, i) => (i === rowIndex ? updater(row) : row));
|
|
239
|
-
onChange(nextRows);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function handleCellChange(rowIndex: number, cellField: string, value: unknown): void {
|
|
243
|
-
replaceRow(rowIndex, (row) =>
|
|
244
|
-
withRecomputedDerived({ ...row, [cellField]: value }, derived, cells),
|
|
245
|
-
);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
function handleAddRow(): void {
|
|
249
|
-
onChange([...rows, withRecomputedDerived({}, derived, cells)]);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
function handleRemoveRow(rowIndex: number): void {
|
|
253
|
-
onChange(rows.filter((_, i) => i !== rowIndex));
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function handleDuplicateRow(rowIndex: number): void {
|
|
257
|
-
const source = rows[rowIndex];
|
|
258
|
-
if (source === undefined) return;
|
|
259
|
-
const next = [...rows];
|
|
260
|
-
next.splice(rowIndex + 1, 0, { ...source });
|
|
261
|
-
onChange(next);
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
function handleMoveRow(fromIndex: number, toIndex: number): void {
|
|
265
|
-
if (toIndex < 0 || toIndex >= rows.length) return;
|
|
266
|
-
const next = [...rows];
|
|
267
|
-
const [moved] = next.splice(fromIndex, 1);
|
|
268
|
-
if (moved === undefined) return;
|
|
269
|
-
next.splice(toIndex, 0, moved);
|
|
270
|
-
onChange(next);
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
function handlePasteCells(
|
|
274
|
-
rowIndex: number,
|
|
275
|
-
columnIndex: number,
|
|
276
|
-
grid: readonly (readonly string[])[],
|
|
277
|
-
): void {
|
|
278
|
-
const maxItems = field.embeddedListMaxItems;
|
|
279
|
-
const nextRows = [...rows];
|
|
280
|
-
const touchedIndices = new Set<number>();
|
|
281
|
-
let droppedRows = 0;
|
|
282
|
-
let unmatchedCells = 0;
|
|
283
|
-
|
|
284
|
-
grid.forEach((gridRow, gridRowOffset) => {
|
|
285
|
-
const targetRowIndex = rowIndex + gridRowOffset;
|
|
286
|
-
if (targetRowIndex >= nextRows.length) {
|
|
287
|
-
if (maxItems !== undefined && nextRows.length >= maxItems) {
|
|
288
|
-
droppedRows += 1;
|
|
289
|
-
return;
|
|
290
|
-
}
|
|
291
|
-
nextRows.push({});
|
|
292
|
-
}
|
|
293
|
-
const targetRow = nextRows[targetRowIndex];
|
|
294
|
-
if (targetRow === undefined) return;
|
|
295
|
-
let updatedRow: Record<string, unknown> = { ...targetRow };
|
|
296
|
-
gridRow.forEach((text, gridColOffset) => {
|
|
297
|
-
const column = columns[columnIndex + gridColOffset];
|
|
298
|
-
if (column === undefined) return;
|
|
299
|
-
const coerced = coerceCellValue(column, text, field.embeddedListCurrency ?? "EUR");
|
|
300
|
-
// Also covers an unparseable money paste (e.g. mangled thousands
|
|
301
|
-
// separators) — the cell must stay unchanged instead of silently
|
|
302
|
-
// clearing to undefined (kumiko-framework#1838).
|
|
303
|
-
const isUnmatchedOrUnparseable =
|
|
304
|
-
(column.type === "select" || column.type === "reference" || column.type === "money") &&
|
|
305
|
-
coerced === undefined &&
|
|
306
|
-
text.trim() !== "";
|
|
307
|
-
if (isUnmatchedOrUnparseable) {
|
|
308
|
-
unmatchedCells += 1;
|
|
309
|
-
return;
|
|
310
|
-
}
|
|
311
|
-
updatedRow = { ...updatedRow, [column.field]: coerced };
|
|
312
|
-
});
|
|
313
|
-
nextRows[targetRowIndex] = updatedRow;
|
|
314
|
-
touchedIndices.add(targetRowIndex);
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
setPasteWarning(droppedRows > 0 || unmatchedCells > 0 ? { droppedRows, unmatchedCells } : null);
|
|
318
|
-
|
|
319
|
-
const recomputed = nextRows.map((row, i) =>
|
|
320
|
-
touchedIndices.has(i) ? withRecomputedDerived(row, derived, cells) : row,
|
|
321
|
-
);
|
|
322
|
-
onChange(recomputed);
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
return (
|
|
326
|
-
<EmbeddedListInput
|
|
327
|
-
id={id}
|
|
328
|
-
columns={columns}
|
|
329
|
-
rows={rows}
|
|
330
|
-
totals={totals}
|
|
331
|
-
currency={field.embeddedListCurrency}
|
|
332
|
-
disabled={field.readOnly}
|
|
333
|
-
minItems={field.embeddedListMinItems}
|
|
334
|
-
maxItems={field.embeddedListMaxItems}
|
|
335
|
-
listIssues={combinedListIssues}
|
|
336
|
-
rowIssues={rowIssues}
|
|
337
|
-
cellIssues={cellIssues}
|
|
338
|
-
onCellChange={handleCellChange}
|
|
339
|
-
onAddRow={handleAddRow}
|
|
340
|
-
onRemoveRow={handleRemoveRow}
|
|
341
|
-
onDuplicateRow={handleDuplicateRow}
|
|
342
|
-
onMoveRow={handleMoveRow}
|
|
343
|
-
onPasteCells={handlePasteCells}
|
|
344
|
-
addLabel={t("kumiko.field.embedded-list.add-row")}
|
|
345
|
-
removeLabel={t("kumiko.field.embedded-list.remove-row")}
|
|
346
|
-
duplicateLabel={t("kumiko.field.embedded-list.duplicate-row")}
|
|
347
|
-
moveUpLabel={t("kumiko.field.embedded-list.move-up")}
|
|
348
|
-
moveDownLabel={t("kumiko.field.embedded-list.move-down")}
|
|
349
|
-
emptyLabel={t("kumiko.field.embedded-list.empty")}
|
|
350
|
-
emptyCtaLabel={t("kumiko.field.embedded-list.empty-cta")}
|
|
351
|
-
testId={id}
|
|
352
|
-
/>
|
|
353
|
-
);
|
|
354
|
-
}
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
import type { EditFieldViewModel, FieldIssue } from "@cosmicdrift/kumiko-headless";
|
|
2
|
-
import type { ReactNode } from "react";
|
|
3
|
-
import type { FieldCellWidth, usePrimitives } from "../primitives";
|
|
4
|
-
import { RenderField } from "./render-field";
|
|
5
|
-
|
|
6
|
-
// Extracted out of render-edit.tsx so write-form-section.tsx can reuse it
|
|
7
|
-
// without a circular import between the two components.
|
|
8
|
-
export type GridCellForFieldProps = {
|
|
9
|
-
readonly field: EditFieldViewModel;
|
|
10
|
-
readonly columns: number;
|
|
11
|
-
readonly issues: readonly FieldIssue[] | undefined;
|
|
12
|
-
readonly onChange: (value: unknown) => void;
|
|
13
|
-
readonly GridCell: ReturnType<typeof usePrimitives>["GridCell"];
|
|
14
|
-
/** Tier 2.7e-3: passed through so Reference fields can build the correct
|
|
15
|
-
* lookup query QN (`<feature>:query:<refEntity>:list`). */
|
|
16
|
-
readonly featureName: string;
|
|
17
|
-
readonly labelAppendix?: ReactNode;
|
|
18
|
-
readonly fieldAppendix?: ReactNode;
|
|
19
|
-
/** Full issues-by-path map (FormSnapshot.errors) — passed through for
|
|
20
|
-
* embedded-list fields, which bucket row-/cell-level issues themselves. */
|
|
21
|
-
readonly allIssues: Readonly<Record<string, readonly FieldIssue[]>>;
|
|
22
|
-
/** Passed through to RenderField unchanged — see RenderEditProps.valueDisplay. */
|
|
23
|
-
readonly valueDisplay: "form" | "text";
|
|
24
|
-
/** Passed through to RenderField as `row` — see RenderFieldProps.row. */
|
|
25
|
-
readonly row: Readonly<Record<string, unknown>>;
|
|
26
|
-
readonly changed?: boolean;
|
|
27
|
-
readonly flow?: boolean;
|
|
28
|
-
};
|
|
29
|
-
|
|
30
|
-
const FIELD_CELL_WIDTH_BY_TYPE: Readonly<Record<string, FieldCellWidth>> = {
|
|
31
|
-
number: "number",
|
|
32
|
-
decimal: "number",
|
|
33
|
-
bigInt: "number",
|
|
34
|
-
money: "money",
|
|
35
|
-
date: "date",
|
|
36
|
-
select: "select",
|
|
37
|
-
multiSelect: "select",
|
|
38
|
-
boolean: "auto",
|
|
39
|
-
longText: "full",
|
|
40
|
-
embedded: "full",
|
|
41
|
-
jsonb: "full",
|
|
42
|
-
file: "full",
|
|
43
|
-
files: "full",
|
|
44
|
-
image: "full",
|
|
45
|
-
images: "full",
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
function fieldCellWidth(field: EditFieldViewModel): FieldCellWidth {
|
|
49
|
-
if (field.span !== undefined || (field.type === "text" && field.multiline)) return "full";
|
|
50
|
-
return FIELD_CELL_WIDTH_BY_TYPE[field.type] ?? "text";
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
export function GridCellForField({
|
|
54
|
-
field,
|
|
55
|
-
columns,
|
|
56
|
-
issues,
|
|
57
|
-
onChange,
|
|
58
|
-
GridCell,
|
|
59
|
-
featureName,
|
|
60
|
-
labelAppendix,
|
|
61
|
-
fieldAppendix,
|
|
62
|
-
allIssues,
|
|
63
|
-
valueDisplay,
|
|
64
|
-
row,
|
|
65
|
-
changed,
|
|
66
|
-
flow,
|
|
67
|
-
}: GridCellForFieldProps): ReactNode {
|
|
68
|
-
// RenderField renders nothing for a hidden field, but the GridCell around it still claims the row.
|
|
69
|
-
if (!field.visible) return null;
|
|
70
|
-
|
|
71
|
-
const effectiveSpan = field.span !== undefined ? Math.min(field.span, columns) : 1;
|
|
72
|
-
return (
|
|
73
|
-
<GridCell span={effectiveSpan} {...(flow === true && { width: fieldCellWidth(field) })}>
|
|
74
|
-
<RenderField
|
|
75
|
-
field={field}
|
|
76
|
-
{...(issues !== undefined && { issues })}
|
|
77
|
-
onChange={onChange}
|
|
78
|
-
featureName={featureName}
|
|
79
|
-
{...(labelAppendix !== undefined && { labelAppendix })}
|
|
80
|
-
{...(fieldAppendix !== undefined && { fieldAppendix })}
|
|
81
|
-
allIssues={allIssues}
|
|
82
|
-
valueDisplay={valueDisplay}
|
|
83
|
-
row={row}
|
|
84
|
-
{...(changed === true && { changed })}
|
|
85
|
-
/>
|
|
86
|
-
</GridCell>
|
|
87
|
-
);
|
|
88
|
-
}
|
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
import type { EditFieldViewModel } from "@cosmicdrift/kumiko-headless";
|
|
2
|
-
import type { ReactNode } from "react";
|
|
3
|
-
import { useTranslation } from "../i18n";
|
|
4
|
-
import { usePrimitives } from "../primitives";
|
|
5
|
-
|
|
6
|
-
export type MultiSelectCheckboxOption = {
|
|
7
|
-
readonly value: string;
|
|
8
|
-
readonly label: string;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export type MultiSelectCheckboxesProps = {
|
|
12
|
-
readonly field: EditFieldViewModel;
|
|
13
|
-
readonly id: string;
|
|
14
|
-
readonly options: readonly MultiSelectCheckboxOption[];
|
|
15
|
-
readonly value: readonly string[];
|
|
16
|
-
readonly onChange: (value: readonly string[]) => void;
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
// Renders MultiSelectFieldDef's `display: "checkboxes"` mode — every option
|
|
20
|
-
// as a visible checkbox plus a select-all toggle, built purely from the
|
|
21
|
-
// platform-neutral Primitives contract (Grid/GridCell/Field/Input kind
|
|
22
|
-
// "boolean", the same primitive `case "boolean"` uses in render-field.tsx).
|
|
23
|
-
// No renderer-web import here — this file must stay usable from Expo too.
|
|
24
|
-
export function MultiSelectCheckboxes({
|
|
25
|
-
field,
|
|
26
|
-
id,
|
|
27
|
-
options,
|
|
28
|
-
value,
|
|
29
|
-
onChange,
|
|
30
|
-
}: MultiSelectCheckboxesProps): ReactNode {
|
|
31
|
-
const { Grid, GridCell, Field, Input, Button } = usePrimitives();
|
|
32
|
-
const t = useTranslation();
|
|
33
|
-
const disabled = field.readOnly;
|
|
34
|
-
const selected = new Set(value);
|
|
35
|
-
const allSelected = options.length > 0 && options.every((opt) => selected.has(opt.value));
|
|
36
|
-
|
|
37
|
-
// Always emits in `field.options` order (MultiSelectFieldDef's documented
|
|
38
|
-
// ordering guarantee), never in click/selection order.
|
|
39
|
-
const emitOrdered = (next: ReadonlySet<string>): void => {
|
|
40
|
-
onChange(options.filter((opt) => next.has(opt.value)).map((opt) => opt.value));
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
const toggleOption = (optionValue: string, checked: boolean): void => {
|
|
44
|
-
const next = new Set(selected);
|
|
45
|
-
if (checked) {
|
|
46
|
-
next.add(optionValue);
|
|
47
|
-
} else {
|
|
48
|
-
next.delete(optionValue);
|
|
49
|
-
}
|
|
50
|
-
emitOrdered(next);
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const toggleAll = (): void => {
|
|
54
|
-
emitOrdered(allSelected ? new Set() : new Set(options.map((opt) => opt.value)));
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
return (
|
|
58
|
-
<>
|
|
59
|
-
<Button
|
|
60
|
-
type="button"
|
|
61
|
-
variant="secondary"
|
|
62
|
-
size="sm"
|
|
63
|
-
disabled={disabled}
|
|
64
|
-
onClick={toggleAll}
|
|
65
|
-
testId={`${id}-select-all`}
|
|
66
|
-
>
|
|
67
|
-
{allSelected
|
|
68
|
-
? t("kumiko.field.multiSelect.deselect-all")
|
|
69
|
-
: t("kumiko.field.multiSelect.select-all")}
|
|
70
|
-
</Button>
|
|
71
|
-
<Grid
|
|
72
|
-
columns={field.columns ?? 2}
|
|
73
|
-
testId={`${id}-checkboxes`}
|
|
74
|
-
{...(field.maxRows !== undefined && { maxRows: field.maxRows })}
|
|
75
|
-
>
|
|
76
|
-
{options.map((opt) => (
|
|
77
|
-
<GridCell key={opt.value}>
|
|
78
|
-
<Field id={`${id}-${opt.value}`} label={opt.label} layout="inline">
|
|
79
|
-
<Input
|
|
80
|
-
kind="boolean"
|
|
81
|
-
id={`${id}-${opt.value}`}
|
|
82
|
-
name={`${id}-${opt.value}`}
|
|
83
|
-
value={selected.has(opt.value)}
|
|
84
|
-
onChange={(checked) => toggleOption(opt.value, checked)}
|
|
85
|
-
disabled={disabled}
|
|
86
|
-
/>
|
|
87
|
-
</Field>
|
|
88
|
-
</GridCell>
|
|
89
|
-
))}
|
|
90
|
-
</Grid>
|
|
91
|
-
</>
|
|
92
|
-
);
|
|
93
|
-
}
|
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
EntityDefinition,
|
|
3
|
-
EntityEditScreenDefinition,
|
|
4
|
-
} from "@cosmicdrift/kumiko-framework/ui-types";
|
|
5
|
-
import type { FormValues, SubmitResult } from "@cosmicdrift/kumiko-headless";
|
|
6
|
-
import { type ReactNode, useMemo } from "react";
|
|
7
|
-
import { buildInitialValues } from "../app/kumiko-screen";
|
|
8
|
-
import { toKebab } from "../app/qn";
|
|
9
|
-
import { useTranslation } from "../i18n";
|
|
10
|
-
import { usePrimitives } from "../primitives";
|
|
11
|
-
import { RenderEdit } from "./render-edit";
|
|
12
|
-
|
|
13
|
-
// Hosts a target entity's create-form inside a bare Modal so a reference
|
|
14
|
-
// field can create a missing record without leaving the current form
|
|
15
|
-
// (kumiko-framework#1681) — same create wiring as EntityEditCreateBody
|
|
16
|
-
// (kumiko-screen.tsx), but reports the new id back via a callback instead
|
|
17
|
-
// of navigating to the entity's list screen.
|
|
18
|
-
|
|
19
|
-
function entityWriteCommand(featureName: string, entity: string): string {
|
|
20
|
-
return `${toKebab(featureName)}:write:${toKebab(entity)}:create`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// The create handler's success payload is `{ kind: "save", id, ... }`
|
|
24
|
-
// (see event-store-executor-write.ts) but RenderEdit's onSubmit only
|
|
25
|
-
// types it as `unknown` — narrow defensively instead of casting through it.
|
|
26
|
-
// `field` is author-declared (ActionFormRedirect.idFrom, fw#2670), never
|
|
27
|
-
// user input; the own-property check keeps a prototype key like
|
|
28
|
-
// "constructor" from resolving to anything.
|
|
29
|
-
export function extractIdField(data: unknown, field: string): string | undefined {
|
|
30
|
-
if (typeof data !== "object" || data === null) return undefined;
|
|
31
|
-
if (!Object.hasOwn(data, field)) return undefined;
|
|
32
|
-
const value = (data as Record<string, unknown>)[field];
|
|
33
|
-
return typeof value === "string" ? value : undefined;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function extractCreatedId(data: unknown): string | undefined {
|
|
37
|
-
return extractIdField(data, "id");
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export type ReferenceCreateDialogProps = {
|
|
41
|
-
readonly open: boolean;
|
|
42
|
-
readonly onClose: () => void;
|
|
43
|
-
// id is undefined when the record was created but the write-handler's
|
|
44
|
-
// success payload carried no `id` (custom create-handler variant) — the
|
|
45
|
-
// record already exists server-side, so this is still a success signal
|
|
46
|
-
// to the caller, just one it can't auto-select from (#1694).
|
|
47
|
-
readonly onCreated: (id: string | undefined) => void;
|
|
48
|
-
readonly featureName: string;
|
|
49
|
-
readonly screen: EntityEditScreenDefinition;
|
|
50
|
-
readonly entity: EntityDefinition;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
export function ReferenceCreateDialog({
|
|
54
|
-
open,
|
|
55
|
-
onClose,
|
|
56
|
-
onCreated,
|
|
57
|
-
featureName,
|
|
58
|
-
screen,
|
|
59
|
-
entity,
|
|
60
|
-
}: ReferenceCreateDialogProps): ReactNode {
|
|
61
|
-
const { Modal } = usePrimitives();
|
|
62
|
-
const t = useTranslation();
|
|
63
|
-
const initial = useMemo(
|
|
64
|
-
() => buildInitialValues(entity.fields, entity.defaultCurrency ?? "EUR") as FormValues,
|
|
65
|
-
[entity.fields, entity.defaultCurrency],
|
|
66
|
-
);
|
|
67
|
-
const writeCommand = entityWriteCommand(featureName, screen.entity);
|
|
68
|
-
const handleSubmitted = (result: SubmitResult<unknown>): void => {
|
|
69
|
-
if (result.validationBlocked || !result.isSuccess) return;
|
|
70
|
-
onCreated(extractCreatedId(result.data));
|
|
71
|
-
};
|
|
72
|
-
if (!open) return null;
|
|
73
|
-
return (
|
|
74
|
-
<Modal
|
|
75
|
-
open={open}
|
|
76
|
-
onOpenChange={(next) => !next && onClose()}
|
|
77
|
-
title={t("kumiko.actions.create")}
|
|
78
|
-
>
|
|
79
|
-
<RenderEdit
|
|
80
|
-
screen={screen}
|
|
81
|
-
entity={entity}
|
|
82
|
-
featureName={featureName}
|
|
83
|
-
initial={initial}
|
|
84
|
-
writeCommand={writeCommand}
|
|
85
|
-
onSubmit={handleSubmitted}
|
|
86
|
-
onCancel={onClose}
|
|
87
|
-
{...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel })}
|
|
88
|
-
/>
|
|
89
|
-
</Modal>
|
|
90
|
-
);
|
|
91
|
-
}
|