@cosmicdrift/kumiko-renderer 0.327.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 -44
- 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 -38
- 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/dist/inside-drawer.js +9 -0
- package/dist/page-header-slot.d.ts +2 -0
- package/dist/page-header-slot.js +9 -0
- 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/changes.json +52 -0
- 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-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 -111
- 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 -3614
- 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 -85
- package/src/app/reference-facet-bridge.tsx +0 -58
- package/src/app/return-to.tsx +0 -234
- package/src/app/row-actions.ts +0 -639
- 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 -1669
- 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 -60
- 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 -467
- package/src/components/render-edit-action-button.tsx +0 -106
- package/src/components/render-edit-logic.ts +0 -85
- package/src/components/render-edit-types.ts +0 -216
- package/src/components/render-edit.tsx +0 -1700
- package/src/components/render-field.tsx +0 -942
- package/src/components/render-list.tsx +0 -561
- package/src/components/write-form-section.tsx +0 -199
- 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-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 -171
- package/src/hooks/use-mutation.ts +0 -61
- package/src/hooks/use-query.ts +0 -132
- 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 -220
- package/src/i18n.tsx +0 -286
- package/src/index.ts +0 -272
- package/src/primitives.tsx +0 -1446
- 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
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useCallback, useMemo, useState } from "react";
|
|
3
|
+
import { useDispatcher } from "../context/dispatcher-context.js";
|
|
4
|
+
import { useQuery } from "../hooks/use-query.js";
|
|
5
|
+
import { useTranslation } from "../i18n.js";
|
|
6
|
+
import { usePrimitives } from "../primitives.js";
|
|
7
|
+
import { dispatcherErrorText } from "./write-failed-error.js";
|
|
8
|
+
// secrets:query:list never returns plaintext (only a redacted preview), so
|
|
9
|
+
// unlike ConfigEditBody there is no server value to pre-fill a draft with —
|
|
10
|
+
// every input starts at "" and stays that way unless the user types into it.
|
|
11
|
+
export function SecretsEditBody({ screen, translate }) {
|
|
12
|
+
const { Banner, Form, Section, Field, Input, Button, Text } = usePrimitives();
|
|
13
|
+
const t = useTranslation();
|
|
14
|
+
const effectiveTranslate = translate ?? t;
|
|
15
|
+
const dispatcher = useDispatcher();
|
|
16
|
+
const listQuery = useQuery("secrets:query:list", {});
|
|
17
|
+
const [drafts, setDrafts] = useState({});
|
|
18
|
+
const [submitting, setSubmitting] = useState(false);
|
|
19
|
+
const [submitError, setSubmitError] = useState(null);
|
|
20
|
+
const rowsByQualifiedKey = useMemo(() => {
|
|
21
|
+
const out = new Map();
|
|
22
|
+
for (const row of listQuery.data ?? [])
|
|
23
|
+
out.set(row.key, row);
|
|
24
|
+
return out;
|
|
25
|
+
}, [listQuery.data]);
|
|
26
|
+
const setDraft = useCallback((fieldId, value) => {
|
|
27
|
+
setDrafts((prev) => ({ ...prev, [fieldId]: value }));
|
|
28
|
+
}, []);
|
|
29
|
+
const handleSubmit = useCallback(async () => {
|
|
30
|
+
const commands = Object.entries(screen.secretKeys).flatMap(([fieldId, qualified]) => {
|
|
31
|
+
const value = drafts[fieldId]?.trim();
|
|
32
|
+
return value ? [{ type: "secrets:write:set", payload: { key: qualified, value } }] : [];
|
|
33
|
+
});
|
|
34
|
+
if (commands.length === 0)
|
|
35
|
+
return;
|
|
36
|
+
setSubmitting(true);
|
|
37
|
+
setSubmitError(null);
|
|
38
|
+
const result = await dispatcher.batch(commands);
|
|
39
|
+
setSubmitting(false);
|
|
40
|
+
if (!result.isSuccess) {
|
|
41
|
+
setSubmitError(dispatcherErrorText(result.error, effectiveTranslate));
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
setDrafts({});
|
|
45
|
+
await listQuery.refetch();
|
|
46
|
+
}, [dispatcher, drafts, screen.secretKeys, listQuery.refetch, effectiveTranslate]);
|
|
47
|
+
const handleDelete = useCallback(async (qualified) => {
|
|
48
|
+
const result = await dispatcher.write("secrets:write:delete", { key: qualified });
|
|
49
|
+
if (!result.isSuccess) {
|
|
50
|
+
setSubmitError(dispatcherErrorText(result.error, effectiveTranslate));
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
await listQuery.refetch();
|
|
54
|
+
}, [dispatcher, listQuery.refetch, effectiveTranslate]);
|
|
55
|
+
if (listQuery.loading && listQuery.data === null) {
|
|
56
|
+
return (_jsx(Banner, { padded: true, variant: "loading", testId: "kumiko-screen-loading", children: "Loading\u2026" }));
|
|
57
|
+
}
|
|
58
|
+
if (listQuery.error) {
|
|
59
|
+
return (_jsx(Banner, { padded: true, variant: "error", testId: "kumiko-screen-error", children: dispatcherErrorText(listQuery.error, effectiveTranslate) }));
|
|
60
|
+
}
|
|
61
|
+
return (_jsxs(Form, { onSubmit: () => {
|
|
62
|
+
void handleSubmit();
|
|
63
|
+
}, testId: "secrets-edit-form", ...(screen.description !== undefined && {
|
|
64
|
+
subtitle: effectiveTranslate(screen.description),
|
|
65
|
+
}), actions: _jsx(Button, { type: "submit", variant: "primary", loading: submitting, disabled: submitting, testId: "secrets-edit-submit", children: effectiveTranslate("kumiko.actions.save") }), children: [submitError !== null && (_jsx(Banner, { variant: "error", testId: "secrets-edit-error", children: submitError })), screen.sections.map((section, index) => (_jsx(Section, { ...(section.title !== undefined && { title: effectiveTranslate(section.title) }), children: section.fields.map((fieldId) => {
|
|
66
|
+
const qualified = screen.secretKeys[fieldId];
|
|
67
|
+
if (qualified === undefined)
|
|
68
|
+
return null;
|
|
69
|
+
const row = rowsByQualifiedKey.get(qualified);
|
|
70
|
+
const hintKey = screen.fieldHints?.[fieldId];
|
|
71
|
+
const isRequired = screen.requiredFields?.includes(fieldId) ?? false;
|
|
72
|
+
return (_jsxs(Field, { id: fieldId, label: effectiveTranslate(screen.fieldLabels[fieldId] ?? fieldId), required: isRequired, testId: `field-${fieldId}`, fieldAppendix: _jsxs(_Fragment, { children: [hintKey !== undefined && (_jsx(Text, { variant: "small", children: effectiveTranslate(hintKey) })), row !== undefined ? (_jsxs(_Fragment, { children: [_jsx(Text, { variant: "small", testId: `secret-preview-${fieldId}`, children: row.redactedPreview ?? effectiveTranslate("config.secrets.set") }), _jsx(Button, { type: "button", variant: "danger-ghost", size: "sm", onClick: () => handleDelete(qualified), testId: `secret-delete-${fieldId}`, children: effectiveTranslate("config.secrets.delete") })] })) : (_jsxs(_Fragment, { children: [_jsx(Text, { variant: "small", testId: `secret-not-set-${fieldId}`, children: effectiveTranslate("config.secrets.notSet") }), isRequired && (_jsx(Text, { variant: "small", testId: `required-marker-${fieldId}`, children: effectiveTranslate("config.secrets.required") }))] }))] }), children: [_jsx(Text, { variant: "small", children: effectiveTranslate(row !== undefined
|
|
73
|
+
? "config.secrets.replacePlaceholder"
|
|
74
|
+
: "config.secrets.placeholder") }), _jsx(Input, { kind: "password", id: fieldId, name: fieldId, value: drafts[fieldId] ?? "", onChange: (v) => setDraft(fieldId, v), autoComplete: "new-password", disabled: submitting, testId: `secret-input-${fieldId}` })] }, fieldId));
|
|
75
|
+
}) }, section.title ?? `section-${index}`)))] }));
|
|
76
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { FeatureSchema } from "./feature-schema.js";
|
|
2
|
+
export type EmbeddedScreenTarget = {
|
|
3
|
+
readonly schema: FeatureSchema;
|
|
4
|
+
readonly qn: string;
|
|
5
|
+
};
|
|
6
|
+
export declare function useEmbeddedScreen(hostFeatureName: string, screen: string): EmbeddedScreenTarget | undefined;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useUserRoles } from "../context/user-roles-context.js";
|
|
2
|
+
import { useAppFeatures } from "./app-features-context.js";
|
|
3
|
+
import { featureNameFromQualifiedScreenId, qualifyScreenId } from "./qualify-screen-id.js";
|
|
4
|
+
import { screenAccessAllows } from "./screen-access.js";
|
|
5
|
+
// Undefined (not KumikoScreen's access-denied banner) so the host can drop the whole tile.
|
|
6
|
+
export function useEmbeddedScreen(hostFeatureName, screen) {
|
|
7
|
+
const features = useAppFeatures();
|
|
8
|
+
const userRoles = useUserRoles();
|
|
9
|
+
const crossFeatureName = featureNameFromQualifiedScreenId(screen);
|
|
10
|
+
const targetFeatureName = crossFeatureName ?? hostFeatureName;
|
|
11
|
+
const qn = crossFeatureName !== undefined ? screen : qualifyScreenId(hostFeatureName, screen);
|
|
12
|
+
const schema = features.find((f) => f.featureName === targetFeatureName);
|
|
13
|
+
const target = schema?.screens.find((s) => qualifyScreenId(targetFeatureName, s.id) === qn);
|
|
14
|
+
if (schema === undefined || target === undefined)
|
|
15
|
+
return undefined;
|
|
16
|
+
if (!screenAccessAllows(target.access, userRoles))
|
|
17
|
+
return undefined;
|
|
18
|
+
return { schema, qn };
|
|
19
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
|
+
export type VariableChipsProps = {
|
|
3
|
+
readonly variables: readonly string[];
|
|
4
|
+
readonly onInsert: (name: string) => void;
|
|
5
|
+
readonly disabled?: boolean;
|
|
6
|
+
};
|
|
7
|
+
export declare function VariableChips({ variables, onInsert, disabled }: VariableChipsProps): ReactNode;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
// Editor-agnostic chip bar for a content editor's declared variables
|
|
3
|
+
// (ContentCollectionDefinition.variableSchema). Renders next to the text
|
|
4
|
+
// surface, never inside it — step 3 (rich/tiptap) reuses this same bar over
|
|
5
|
+
// a different editor, so it must not assume a textarea underneath.
|
|
6
|
+
import { Fragment } from "react";
|
|
7
|
+
import { useTranslation } from "../i18n.js";
|
|
8
|
+
import { usePrimitives } from "../primitives.js";
|
|
9
|
+
// No wrapping element: @cosmicdrift/kumiko-renderer is DOM-free (native impls
|
|
10
|
+
// have no <div>), and layout is a platform concern anyway — the caller
|
|
11
|
+
// (PlainContentEditor et al.) places these buttons in its own container.
|
|
12
|
+
export function VariableChips({ variables, onInsert, disabled }) {
|
|
13
|
+
const { Button } = usePrimitives();
|
|
14
|
+
const t = useTranslation();
|
|
15
|
+
return (_jsx(Fragment, { children: variables.map((name) => (_jsx(Button, { type: "button", variant: "secondary", size: "sm", disabled: disabled, onClick: () => onInsert(name), ariaLabel: t("kumiko.contentEditor.insertVariable", { name }), children: `{{${name}}}` }, name))) }));
|
|
16
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { DispatcherError } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
export declare class WriteFailedError extends Error {
|
|
3
|
+
readonly dispatcherError: DispatcherError;
|
|
4
|
+
constructor(error: DispatcherError, message: string);
|
|
5
|
+
}
|
|
6
|
+
export declare function dispatcherErrorText(error: DispatcherError, translate: (key: string, params?: Readonly<Record<string, unknown>>) => string): string;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
// dispatcher.write wirft bei Server-Fehlern NICHT — es returnt
|
|
2
|
+
// { isSuccess: false, error }. Action-Wiring das das Result verwirft,
|
|
3
|
+
// macht Fehler unsichtbar ("Klick tut nichts"-Prod-Bug 2026-06-07).
|
|
4
|
+
// Diese Klasse trägt den strukturierten DispatcherError dahin, wo eine
|
|
5
|
+
// UI ihn anzeigen kann (Toast mit docsUrl).
|
|
6
|
+
export class WriteFailedError extends Error {
|
|
7
|
+
dispatcherError;
|
|
8
|
+
constructor(error, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.name = "WriteFailedError";
|
|
11
|
+
this.dispatcherError = error;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
// i18nKey gewinnt wenn das Bundle ihn kennt (translate returnt den Key
|
|
15
|
+
// unverändert wenn nicht — Renderer-Convention), sonst message, zuletzt code.
|
|
16
|
+
export function dispatcherErrorText(error, translate) {
|
|
17
|
+
const translated = translate(error.i18nKey, error.i18nParams);
|
|
18
|
+
if (translated !== error.i18nKey)
|
|
19
|
+
return translated;
|
|
20
|
+
return error.message !== "" ? error.message : error.code;
|
|
21
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { EditFieldViewModel, FieldIssue } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
export type EmbeddedListFieldProps = {
|
|
4
|
+
readonly field: EditFieldViewModel;
|
|
5
|
+
readonly id: string;
|
|
6
|
+
readonly onChange: (value: unknown) => void;
|
|
7
|
+
readonly allIssues: Readonly<Record<string, readonly FieldIssue[]>>;
|
|
8
|
+
readonly featureName: string;
|
|
9
|
+
};
|
|
10
|
+
export declare function EmbeddedListField({ field, id, onChange, allIssues, featureName, }: EmbeddedListFieldProps): ReactNode;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { computeDerivedCellValue, currencyDecimals, groupEmbeddedListIssues, roundDerivedCellValue, sumEmbeddedListColumn, } from "@cosmicdrift/kumiko-headless";
|
|
3
|
+
import { useState } from "react";
|
|
4
|
+
import { REFERENCE_COMBOBOX_LIMIT } from "../hooks/reference-limits.js";
|
|
5
|
+
import { useQuery } from "../hooks/use-query.js";
|
|
6
|
+
import { referenceLookupSource } from "../hooks/use-reference-lookup.js";
|
|
7
|
+
import { useTranslation } from "../i18n.js";
|
|
8
|
+
import { usePrimitives } from "../primitives.js";
|
|
9
|
+
function withRecomputedDerived(row, derived, cells) {
|
|
10
|
+
if (derived === undefined)
|
|
11
|
+
return row;
|
|
12
|
+
// Read sources from an unchanged snapshot, never from `result` (which this
|
|
13
|
+
// loop mutates) — mirrors withDerivedCells server-side so chained derived
|
|
14
|
+
// cells (e.g. net=qty*price, then gross=sum(net,tax)) can't drift between
|
|
15
|
+
// client preview and server depending on Object.entries(derived) order.
|
|
16
|
+
const source = { ...row };
|
|
17
|
+
const result = { ...row };
|
|
18
|
+
for (const [derivedField, def] of Object.entries(derived)) {
|
|
19
|
+
const values = def.from.map((src) => {
|
|
20
|
+
const v = source[src];
|
|
21
|
+
return typeof v === "number" ? v : undefined;
|
|
22
|
+
});
|
|
23
|
+
const computed = computeDerivedCellValue(def.op, values);
|
|
24
|
+
const target = cells.find((c) => c.field === derivedField);
|
|
25
|
+
result[derivedField] =
|
|
26
|
+
computed !== undefined && target !== undefined
|
|
27
|
+
? roundDerivedCellValue(computed, target)
|
|
28
|
+
: computed;
|
|
29
|
+
}
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
// Locale-aware decimal parse for pasted text: strips space/NBSP thousands
|
|
33
|
+
// separators outright, then treats whichever of "," or "." appears LAST in
|
|
34
|
+
// the string as the decimal separator and strips the other one — handles
|
|
35
|
+
// both "1.234,56" (de) and "1,234.56" (en-US) pastes. Returns undefined for
|
|
36
|
+
// anything that still doesn't parse as a finite number.
|
|
37
|
+
function parseLocaleAwareDecimal(raw) {
|
|
38
|
+
const stripped = raw.replace(/[\s ]/g, "");
|
|
39
|
+
if (stripped === "")
|
|
40
|
+
return undefined;
|
|
41
|
+
const lastComma = stripped.lastIndexOf(",");
|
|
42
|
+
const lastDot = stripped.lastIndexOf(".");
|
|
43
|
+
const decimalIndex = Math.max(lastComma, lastDot);
|
|
44
|
+
if (decimalIndex === -1) {
|
|
45
|
+
const n = Number(stripped);
|
|
46
|
+
return Number.isFinite(n) ? n : undefined;
|
|
47
|
+
}
|
|
48
|
+
const integerPart = stripped.slice(0, decimalIndex).replace(/[.,]/g, "");
|
|
49
|
+
const fractionPart = stripped.slice(decimalIndex + 1);
|
|
50
|
+
if (!/^\d*$/.test(integerPart) || !/^\d*$/.test(fractionPart))
|
|
51
|
+
return undefined;
|
|
52
|
+
const n = Number(`${integerPart === "" ? "0" : integerPart}.${fractionPart === "" ? "0" : fractionPart}`);
|
|
53
|
+
return Number.isFinite(n) ? n : undefined;
|
|
54
|
+
}
|
|
55
|
+
function coerceCellValue(column, text, currency) {
|
|
56
|
+
switch (column.type) {
|
|
57
|
+
case "text":
|
|
58
|
+
return text;
|
|
59
|
+
case "number":
|
|
60
|
+
case "decimal": {
|
|
61
|
+
if (text.trim() === "")
|
|
62
|
+
return undefined;
|
|
63
|
+
const n = Number(text);
|
|
64
|
+
return Number.isFinite(n) ? n : undefined;
|
|
65
|
+
}
|
|
66
|
+
case "money": {
|
|
67
|
+
// Money cells are minor-unit integers (cents) in storage — paste
|
|
68
|
+
// arrives as a major-unit decimal string ("12,99"/"12.99"), so scale
|
|
69
|
+
// by the currency's decimal places. Must agree with the typed-in path
|
|
70
|
+
// (renderCellControl's MoneyInput, which scales by the same
|
|
71
|
+
// currencyDecimals) — a hardcoded ×100 here diverged for zero-/three-
|
|
72
|
+
// decimal currencies (JPY, BHD, ...), landing a pasted value 100x off
|
|
73
|
+
// from the same value typed by hand (kumiko-framework#1972).
|
|
74
|
+
// parseLocaleAwareDecimal handles both "1.234,56" (de) and
|
|
75
|
+
// "1,234.56" (en-US) thousands/decimal conventions — a naive
|
|
76
|
+
// `replace(",", ".")` mangled DE-locale pastes with thousands
|
|
77
|
+
// separators into NaN (kumiko-framework#1838).
|
|
78
|
+
if (text.trim() === "")
|
|
79
|
+
return undefined;
|
|
80
|
+
const n = parseLocaleAwareDecimal(text);
|
|
81
|
+
return n === undefined ? undefined : Math.round(n * 10 ** currencyDecimals(currency));
|
|
82
|
+
}
|
|
83
|
+
case "boolean":
|
|
84
|
+
return ["true", "1", "yes", "y", "ja"].includes(text.trim().toLowerCase());
|
|
85
|
+
case "date":
|
|
86
|
+
case "timestamp":
|
|
87
|
+
return text.trim();
|
|
88
|
+
case "select": {
|
|
89
|
+
const match = (column.options ?? []).find((opt) => opt === text || column.optionLabels?.[opt] === text);
|
|
90
|
+
return match;
|
|
91
|
+
}
|
|
92
|
+
case "reference": {
|
|
93
|
+
const match = (column.referenceOptions ?? []).find((o) => o.label === text || o.value === text);
|
|
94
|
+
return match?.value;
|
|
95
|
+
}
|
|
96
|
+
default: {
|
|
97
|
+
const exhaustiveCheck = column.type;
|
|
98
|
+
return exhaustiveCheck;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
export function EmbeddedListField({ field, id, onChange, allIssues, featureName, }) {
|
|
103
|
+
const { EmbeddedListInput } = usePrimitives();
|
|
104
|
+
const t = useTranslation();
|
|
105
|
+
const [pasteWarning, setPasteWarning] = useState(null);
|
|
106
|
+
const cells = field.embeddedListCells ?? [];
|
|
107
|
+
const rows = Array.isArray(field.value) ? field.value : [];
|
|
108
|
+
const derived = field.embeddedListDerived;
|
|
109
|
+
const referenceCells = cells.filter((c) => c.type === "reference");
|
|
110
|
+
const referenceSources = referenceCells.map((cell) => cell.refOptionsQuery !== undefined
|
|
111
|
+
? { queryQn: cell.refOptionsQuery, labelKey: "label" }
|
|
112
|
+
: referenceLookupSource(cell.refFeature ?? featureName, cell.refEntity ?? "", cell.refLabelField ?? "id"));
|
|
113
|
+
const referenceQueries = referenceSources.map((source) =>
|
|
114
|
+
// biome-ignore lint/correctness/useHookAtTopLevel: referenceCells comes from the entity-schema definition — fixed for the screen's lifetime, not a real conditional-hook risk.
|
|
115
|
+
useQuery(source.queryQn, {
|
|
116
|
+
limit: REFERENCE_COMBOBOX_LIMIT,
|
|
117
|
+
}));
|
|
118
|
+
if (EmbeddedListInput === undefined)
|
|
119
|
+
return null;
|
|
120
|
+
const columns = cells.map((cell) => {
|
|
121
|
+
const isDerived = derived?.[cell.field] !== undefined;
|
|
122
|
+
if (cell.type === "reference") {
|
|
123
|
+
const idx = referenceCells.indexOf(cell);
|
|
124
|
+
const query = referenceQueries[idx];
|
|
125
|
+
const labelKey = referenceSources[idx]?.labelKey ?? "id";
|
|
126
|
+
const referenceOptions = (query?.data?.rows ?? []).map((row) => ({
|
|
127
|
+
value: String(row["id"] ?? ""),
|
|
128
|
+
label: String(row[labelKey] ?? row["id"] ?? ""),
|
|
129
|
+
}));
|
|
130
|
+
return {
|
|
131
|
+
field: cell.field,
|
|
132
|
+
label: cell.label,
|
|
133
|
+
type: cell.type,
|
|
134
|
+
required: cell.required,
|
|
135
|
+
derived: isDerived,
|
|
136
|
+
referenceOptions,
|
|
137
|
+
referenceLoading: query?.loading ?? false,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
field: cell.field,
|
|
142
|
+
label: cell.label,
|
|
143
|
+
type: cell.type,
|
|
144
|
+
required: cell.required,
|
|
145
|
+
derived: isDerived,
|
|
146
|
+
...(cell.options !== undefined && { options: cell.options }),
|
|
147
|
+
...(cell.optionLabels !== undefined && { optionLabels: cell.optionLabels }),
|
|
148
|
+
};
|
|
149
|
+
});
|
|
150
|
+
const totals = (field.embeddedListTotals ?? []).map((subFieldName) => {
|
|
151
|
+
const cell = cells.find((c) => c.field === subFieldName);
|
|
152
|
+
return {
|
|
153
|
+
field: subFieldName,
|
|
154
|
+
label: cell?.label ?? subFieldName,
|
|
155
|
+
value: sumEmbeddedListColumn(rows, subFieldName),
|
|
156
|
+
};
|
|
157
|
+
});
|
|
158
|
+
const { listIssues, rowIssues, cellIssues } = groupEmbeddedListIssues(allIssues, field.field);
|
|
159
|
+
const combinedListIssues = pasteWarning === null
|
|
160
|
+
? listIssues
|
|
161
|
+
: [
|
|
162
|
+
...listIssues,
|
|
163
|
+
...(pasteWarning.droppedRows > 0
|
|
164
|
+
? [
|
|
165
|
+
{
|
|
166
|
+
path: field.field,
|
|
167
|
+
code: "paste-rows-truncated",
|
|
168
|
+
i18nKey: "kumiko.field.embedded-list.paste-rows-truncated",
|
|
169
|
+
params: { count: pasteWarning.droppedRows },
|
|
170
|
+
},
|
|
171
|
+
]
|
|
172
|
+
: []),
|
|
173
|
+
...(pasteWarning.unmatchedCells > 0
|
|
174
|
+
? [
|
|
175
|
+
{
|
|
176
|
+
path: field.field,
|
|
177
|
+
code: "paste-cells-unmatched",
|
|
178
|
+
i18nKey: "kumiko.field.embedded-list.paste-cells-unmatched",
|
|
179
|
+
params: { count: pasteWarning.unmatchedCells },
|
|
180
|
+
},
|
|
181
|
+
]
|
|
182
|
+
: []),
|
|
183
|
+
];
|
|
184
|
+
function replaceRow(rowIndex, updater) {
|
|
185
|
+
const nextRows = rows.map((row, i) => (i === rowIndex ? updater(row) : row));
|
|
186
|
+
onChange(nextRows);
|
|
187
|
+
}
|
|
188
|
+
function handleCellChange(rowIndex, cellField, value) {
|
|
189
|
+
replaceRow(rowIndex, (row) => withRecomputedDerived({ ...row, [cellField]: value }, derived, cells));
|
|
190
|
+
}
|
|
191
|
+
function handleAddRow() {
|
|
192
|
+
onChange([...rows, withRecomputedDerived({}, derived, cells)]);
|
|
193
|
+
}
|
|
194
|
+
function handleRemoveRow(rowIndex) {
|
|
195
|
+
onChange(rows.filter((_, i) => i !== rowIndex));
|
|
196
|
+
}
|
|
197
|
+
function handleDuplicateRow(rowIndex) {
|
|
198
|
+
const source = rows[rowIndex];
|
|
199
|
+
if (source === undefined)
|
|
200
|
+
return;
|
|
201
|
+
const next = [...rows];
|
|
202
|
+
next.splice(rowIndex + 1, 0, { ...source });
|
|
203
|
+
onChange(next);
|
|
204
|
+
}
|
|
205
|
+
function handleMoveRow(fromIndex, toIndex) {
|
|
206
|
+
if (toIndex < 0 || toIndex >= rows.length)
|
|
207
|
+
return;
|
|
208
|
+
const next = [...rows];
|
|
209
|
+
const [moved] = next.splice(fromIndex, 1);
|
|
210
|
+
if (moved === undefined)
|
|
211
|
+
return;
|
|
212
|
+
next.splice(toIndex, 0, moved);
|
|
213
|
+
onChange(next);
|
|
214
|
+
}
|
|
215
|
+
function handlePasteCells(rowIndex, columnIndex, grid) {
|
|
216
|
+
const maxItems = field.embeddedListMaxItems;
|
|
217
|
+
const nextRows = [...rows];
|
|
218
|
+
const touchedIndices = new Set();
|
|
219
|
+
let droppedRows = 0;
|
|
220
|
+
let unmatchedCells = 0;
|
|
221
|
+
grid.forEach((gridRow, gridRowOffset) => {
|
|
222
|
+
const targetRowIndex = rowIndex + gridRowOffset;
|
|
223
|
+
if (targetRowIndex >= nextRows.length) {
|
|
224
|
+
if (maxItems !== undefined && nextRows.length >= maxItems) {
|
|
225
|
+
droppedRows += 1;
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
nextRows.push({});
|
|
229
|
+
}
|
|
230
|
+
const targetRow = nextRows[targetRowIndex];
|
|
231
|
+
if (targetRow === undefined)
|
|
232
|
+
return;
|
|
233
|
+
let updatedRow = { ...targetRow };
|
|
234
|
+
gridRow.forEach((text, gridColOffset) => {
|
|
235
|
+
const column = columns[columnIndex + gridColOffset];
|
|
236
|
+
if (column === undefined)
|
|
237
|
+
return;
|
|
238
|
+
const coerced = coerceCellValue(column, text, field.embeddedListCurrency ?? "EUR");
|
|
239
|
+
// Also covers an unparseable money paste (e.g. mangled thousands
|
|
240
|
+
// separators) — the cell must stay unchanged instead of silently
|
|
241
|
+
// clearing to undefined (kumiko-framework#1838).
|
|
242
|
+
const isUnmatchedOrUnparseable = (column.type === "select" || column.type === "reference" || column.type === "money") &&
|
|
243
|
+
coerced === undefined &&
|
|
244
|
+
text.trim() !== "";
|
|
245
|
+
if (isUnmatchedOrUnparseable) {
|
|
246
|
+
unmatchedCells += 1;
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
updatedRow = { ...updatedRow, [column.field]: coerced };
|
|
250
|
+
});
|
|
251
|
+
nextRows[targetRowIndex] = updatedRow;
|
|
252
|
+
touchedIndices.add(targetRowIndex);
|
|
253
|
+
});
|
|
254
|
+
setPasteWarning(droppedRows > 0 || unmatchedCells > 0 ? { droppedRows, unmatchedCells } : null);
|
|
255
|
+
const recomputed = nextRows.map((row, i) => touchedIndices.has(i) ? withRecomputedDerived(row, derived, cells) : row);
|
|
256
|
+
onChange(recomputed);
|
|
257
|
+
}
|
|
258
|
+
return (_jsx(EmbeddedListInput, { id: id, columns: columns, rows: rows, totals: totals, currency: field.embeddedListCurrency, disabled: field.readOnly, minItems: field.embeddedListMinItems, maxItems: field.embeddedListMaxItems, listIssues: combinedListIssues, rowIssues: rowIssues, cellIssues: cellIssues, onCellChange: handleCellChange, onAddRow: handleAddRow, onRemoveRow: handleRemoveRow, onDuplicateRow: handleDuplicateRow, onMoveRow: handleMoveRow, onPasteCells: handlePasteCells, addLabel: t("kumiko.field.embedded-list.add-row"), removeLabel: t("kumiko.field.embedded-list.remove-row"), duplicateLabel: t("kumiko.field.embedded-list.duplicate-row"), moveUpLabel: t("kumiko.field.embedded-list.move-up"), moveDownLabel: t("kumiko.field.embedded-list.move-down"), emptyLabel: t("kumiko.field.embedded-list.empty"), emptyCtaLabel: t("kumiko.field.embedded-list.empty-cta"), testId: id }));
|
|
259
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EditFieldViewModel, FieldIssue } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
import type { usePrimitives } from "../primitives.js";
|
|
4
|
+
export type GridCellForFieldProps = {
|
|
5
|
+
readonly field: EditFieldViewModel;
|
|
6
|
+
readonly columns: number;
|
|
7
|
+
readonly issues: readonly FieldIssue[] | undefined;
|
|
8
|
+
readonly onChange: (value: unknown) => void;
|
|
9
|
+
readonly GridCell: ReturnType<typeof usePrimitives>["GridCell"];
|
|
10
|
+
/** Tier 2.7e-3: passed through so Reference fields can build the correct
|
|
11
|
+
* lookup query QN (`<feature>:query:<refEntity>:list`). */
|
|
12
|
+
readonly featureName: string;
|
|
13
|
+
readonly labelAppendix?: ReactNode;
|
|
14
|
+
readonly fieldAppendix?: ReactNode;
|
|
15
|
+
/** Full issues-by-path map (FormSnapshot.errors) — passed through for
|
|
16
|
+
* embedded-list fields, which bucket row-/cell-level issues themselves. */
|
|
17
|
+
readonly allIssues: Readonly<Record<string, readonly FieldIssue[]>>;
|
|
18
|
+
/** Passed through to RenderField unchanged — see RenderEditProps.valueDisplay. */
|
|
19
|
+
readonly valueDisplay: "form" | "text";
|
|
20
|
+
/** Passed through to RenderField as `row` — see RenderFieldProps.row. */
|
|
21
|
+
readonly row: Readonly<Record<string, unknown>>;
|
|
22
|
+
readonly changed?: boolean;
|
|
23
|
+
readonly flow?: boolean;
|
|
24
|
+
};
|
|
25
|
+
export declare function GridCellForField({ field, columns, issues, onChange, GridCell, featureName, labelAppendix, fieldAppendix, allIssues, valueDisplay, row, changed, flow, }: GridCellForFieldProps): ReactNode;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { RenderField } from "./render-field.js";
|
|
3
|
+
const FIELD_CELL_WIDTH_BY_TYPE = {
|
|
4
|
+
number: "number",
|
|
5
|
+
decimal: "number",
|
|
6
|
+
bigInt: "number",
|
|
7
|
+
money: "money",
|
|
8
|
+
date: "date",
|
|
9
|
+
select: "select",
|
|
10
|
+
multiSelect: "select",
|
|
11
|
+
boolean: "auto",
|
|
12
|
+
longText: "full",
|
|
13
|
+
embedded: "full",
|
|
14
|
+
jsonb: "full",
|
|
15
|
+
file: "full",
|
|
16
|
+
files: "full",
|
|
17
|
+
image: "full",
|
|
18
|
+
images: "full",
|
|
19
|
+
};
|
|
20
|
+
function fieldCellWidth(field) {
|
|
21
|
+
if (field.span !== undefined || (field.type === "text" && field.multiline))
|
|
22
|
+
return "full";
|
|
23
|
+
return FIELD_CELL_WIDTH_BY_TYPE[field.type] ?? "text";
|
|
24
|
+
}
|
|
25
|
+
export function GridCellForField({ field, columns, issues, onChange, GridCell, featureName, labelAppendix, fieldAppendix, allIssues, valueDisplay, row, changed, flow, }) {
|
|
26
|
+
// RenderField renders nothing for a hidden field, but the GridCell around it still claims the row.
|
|
27
|
+
if (!field.visible)
|
|
28
|
+
return null;
|
|
29
|
+
const effectiveSpan = field.span !== undefined ? Math.min(field.span, columns) : 1;
|
|
30
|
+
return (_jsx(GridCell, { span: effectiveSpan, ...(flow === true && { width: fieldCellWidth(field) }), children: _jsx(RenderField, { field: field, ...(issues !== undefined && { issues }), onChange: onChange, featureName: featureName, ...(labelAppendix !== undefined && { labelAppendix }), ...(fieldAppendix !== undefined && { fieldAppendix }), allIssues: allIssues, valueDisplay: valueDisplay, row: row, ...(changed === true && { changed }) }) }));
|
|
31
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { EditFieldViewModel } from "@cosmicdrift/kumiko-headless";
|
|
2
|
+
import type { ReactNode } from "react";
|
|
3
|
+
export type MultiSelectCheckboxOption = {
|
|
4
|
+
readonly value: string;
|
|
5
|
+
readonly label: string;
|
|
6
|
+
};
|
|
7
|
+
export type MultiSelectCheckboxesProps = {
|
|
8
|
+
readonly field: EditFieldViewModel;
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly options: readonly MultiSelectCheckboxOption[];
|
|
11
|
+
readonly value: readonly string[];
|
|
12
|
+
readonly onChange: (value: readonly string[]) => void;
|
|
13
|
+
};
|
|
14
|
+
export declare function MultiSelectCheckboxes({ field, id, options, value, onChange, }: MultiSelectCheckboxesProps): ReactNode;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useTranslation } from "../i18n.js";
|
|
3
|
+
import { usePrimitives } from "../primitives.js";
|
|
4
|
+
// Renders MultiSelectFieldDef's `display: "checkboxes"` mode — every option
|
|
5
|
+
// as a visible checkbox plus a select-all toggle, built purely from the
|
|
6
|
+
// platform-neutral Primitives contract (Grid/GridCell/Field/Input kind
|
|
7
|
+
// "boolean", the same primitive `case "boolean"` uses in render-field.tsx).
|
|
8
|
+
// No renderer-web import here — this file must stay usable from Expo too.
|
|
9
|
+
export function MultiSelectCheckboxes({ field, id, options, value, onChange, }) {
|
|
10
|
+
const { Grid, GridCell, Field, Input, Button } = usePrimitives();
|
|
11
|
+
const t = useTranslation();
|
|
12
|
+
const disabled = field.readOnly;
|
|
13
|
+
const selected = new Set(value);
|
|
14
|
+
const allSelected = options.length > 0 && options.every((opt) => selected.has(opt.value));
|
|
15
|
+
// Always emits in `field.options` order (MultiSelectFieldDef's documented
|
|
16
|
+
// ordering guarantee), never in click/selection order.
|
|
17
|
+
const emitOrdered = (next) => {
|
|
18
|
+
onChange(options.filter((opt) => next.has(opt.value)).map((opt) => opt.value));
|
|
19
|
+
};
|
|
20
|
+
const toggleOption = (optionValue, checked) => {
|
|
21
|
+
const next = new Set(selected);
|
|
22
|
+
if (checked) {
|
|
23
|
+
next.add(optionValue);
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
next.delete(optionValue);
|
|
27
|
+
}
|
|
28
|
+
emitOrdered(next);
|
|
29
|
+
};
|
|
30
|
+
const toggleAll = () => {
|
|
31
|
+
emitOrdered(allSelected ? new Set() : new Set(options.map((opt) => opt.value)));
|
|
32
|
+
};
|
|
33
|
+
return (_jsxs(_Fragment, { children: [_jsx(Button, { type: "button", variant: "secondary", size: "sm", disabled: disabled, onClick: toggleAll, testId: `${id}-select-all`, children: allSelected
|
|
34
|
+
? t("kumiko.field.multiSelect.deselect-all")
|
|
35
|
+
: t("kumiko.field.multiSelect.select-all") }), _jsx(Grid, { columns: field.columns ?? 2, testId: `${id}-checkboxes`, ...(field.maxRows !== undefined && { maxRows: field.maxRows }), children: options.map((opt) => (_jsx(GridCell, { children: _jsx(Field, { id: `${id}-${opt.value}`, label: opt.label, layout: "inline", children: _jsx(Input, { kind: "boolean", id: `${id}-${opt.value}`, name: `${id}-${opt.value}`, value: selected.has(opt.value), onChange: (checked) => toggleOption(opt.value, checked), disabled: disabled }) }) }, opt.value))) })] }));
|
|
36
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { EntityDefinition, EntityEditScreenDefinition } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import { type ReactNode } from "react";
|
|
3
|
+
export declare function extractIdField(data: unknown, field: string): string | undefined;
|
|
4
|
+
export declare function extractCreatedId(data: unknown): string | undefined;
|
|
5
|
+
export type ReferenceCreateDialogProps = {
|
|
6
|
+
readonly open: boolean;
|
|
7
|
+
readonly onClose: () => void;
|
|
8
|
+
readonly onCreated: (id: string | undefined) => void;
|
|
9
|
+
readonly featureName: string;
|
|
10
|
+
readonly screen: EntityEditScreenDefinition;
|
|
11
|
+
readonly entity: EntityDefinition;
|
|
12
|
+
};
|
|
13
|
+
export declare function ReferenceCreateDialog({ open, onClose, onCreated, featureName, screen, entity, }: ReferenceCreateDialogProps): ReactNode;
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useMemo } from "react";
|
|
3
|
+
import { buildInitialValues } from "../app/kumiko-screen.js";
|
|
4
|
+
import { toKebab } from "../app/qn.js";
|
|
5
|
+
import { useTranslation } from "../i18n.js";
|
|
6
|
+
import { usePrimitives } from "../primitives.js";
|
|
7
|
+
import { RenderEdit } from "./render-edit.js";
|
|
8
|
+
// Hosts a target entity's create-form inside a bare Modal so a reference
|
|
9
|
+
// field can create a missing record without leaving the current form
|
|
10
|
+
// (kumiko-framework#1681) — same create wiring as EntityEditCreateBody
|
|
11
|
+
// (kumiko-screen.tsx), but reports the new id back via a callback instead
|
|
12
|
+
// of navigating to the entity's list screen.
|
|
13
|
+
function entityWriteCommand(featureName, entity) {
|
|
14
|
+
return `${toKebab(featureName)}:write:${toKebab(entity)}:create`;
|
|
15
|
+
}
|
|
16
|
+
// The create handler's success payload is `{ kind: "save", id, ... }`
|
|
17
|
+
// (see event-store-executor-write.ts) but RenderEdit's onSubmit only
|
|
18
|
+
// types it as `unknown` — narrow defensively instead of casting through it.
|
|
19
|
+
// `field` is author-declared (ActionFormRedirect.idFrom, fw#2670), never
|
|
20
|
+
// user input; the own-property check keeps a prototype key like
|
|
21
|
+
// "constructor" from resolving to anything.
|
|
22
|
+
export function extractIdField(data, field) {
|
|
23
|
+
if (typeof data !== "object" || data === null)
|
|
24
|
+
return undefined;
|
|
25
|
+
if (!Object.hasOwn(data, field))
|
|
26
|
+
return undefined;
|
|
27
|
+
const value = data[field];
|
|
28
|
+
return typeof value === "string" ? value : undefined;
|
|
29
|
+
}
|
|
30
|
+
export function extractCreatedId(data) {
|
|
31
|
+
return extractIdField(data, "id");
|
|
32
|
+
}
|
|
33
|
+
export function ReferenceCreateDialog({ open, onClose, onCreated, featureName, screen, entity, }) {
|
|
34
|
+
const { Modal } = usePrimitives();
|
|
35
|
+
const t = useTranslation();
|
|
36
|
+
const initial = useMemo(() => buildInitialValues(entity.fields, entity.defaultCurrency ?? "EUR"), [entity.fields, entity.defaultCurrency]);
|
|
37
|
+
const writeCommand = entityWriteCommand(featureName, screen.entity);
|
|
38
|
+
const handleSubmitted = (result) => {
|
|
39
|
+
if (result.validationBlocked || !result.isSuccess)
|
|
40
|
+
return;
|
|
41
|
+
onCreated(extractCreatedId(result.data));
|
|
42
|
+
};
|
|
43
|
+
if (!open)
|
|
44
|
+
return null;
|
|
45
|
+
return (_jsx(Modal, { open: open, onOpenChange: (next) => !next && onClose(), title: t("kumiko.actions.create"), children: _jsx(RenderEdit, { screen: screen, entity: entity, featureName: featureName, initial: initial, writeCommand: writeCommand, onSubmit: handleSubmitted, onCancel: onClose, ...(screen.submitLabel !== undefined && { submitLabel: screen.submitLabel }) }) }));
|
|
46
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { RowActionDrawer } from "@cosmicdrift/kumiko-framework/ui-types";
|
|
2
|
+
import type { EditRelatedListSectionViewModel, Translate } from "@cosmicdrift/kumiko-headless";
|
|
3
|
+
import { type ReactNode } from "react";
|
|
4
|
+
export declare function RelatedListSection({ section, parentId, record, featureName, translate, hideTitle, grow, onOpenDrawer, actions, }: {
|
|
5
|
+
readonly section: EditRelatedListSectionViewModel;
|
|
6
|
+
readonly parentId: string;
|
|
7
|
+
/** The enclosing projectionDetail's own record (the "Akte") — evaluates
|
|
8
|
+
* toolbarActions' `visible`/`params` (RelatedListToolbarAction), the same
|
|
9
|
+
* record header actions already use. */
|
|
10
|
+
readonly record: Readonly<Record<string, unknown>>;
|
|
11
|
+
readonly featureName: string;
|
|
12
|
+
readonly translate?: Translate;
|
|
13
|
+
readonly hideTitle?: boolean;
|
|
14
|
+
/** Under a fixed-height screen the tab panel chain stretches so the count footer sits at the bottom. */
|
|
15
|
+
readonly grow?: boolean;
|
|
16
|
+
/** Opens a drawer-kind rowAction (fw#2710). Supplied by the parent
|
|
17
|
+
* (ProjectionDetailBody), which owns schema + the actual Drawer render —
|
|
18
|
+
* this component only ever invokes the callback. */
|
|
19
|
+
readonly onOpenDrawer?: (action: RowActionDrawer, initialValues: Readonly<Record<string, unknown>> | undefined) => void;
|
|
20
|
+
/** section.actions, already resolved into buttons by the caller —
|
|
21
|
+
* rendered in the Section's title row. Only reachable in the standalone
|
|
22
|
+
* (non-`hideTitle`) branch below — `hideTitle` uses `FillContainer`, which
|
|
23
|
+
* has no title row to render actions into (see the comment on that branch). */
|
|
24
|
+
readonly actions?: ReactNode;
|
|
25
|
+
}): ReactNode;
|