@cosmicdrift/kumiko-renderer 0.187.0 → 0.189.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +5 -4
- package/src/__tests__/merge-search-params-into-initial.test.ts +13 -1
- package/src/app/__tests__/form-schema.test.ts +227 -0
- package/src/app/extension-sections.tsx +17 -0
- package/src/app/form-schema.ts +90 -0
- package/src/app/kumiko-screen.tsx +182 -42
- package/src/app/layout-fields.ts +27 -0
- package/src/components/__tests__/render-edit-logic.test.ts +48 -0
- package/src/components/__tests__/render-field-money-roundtrip.test.tsx +189 -0
- package/src/components/__tests__/render-field-unsupported-types.test.tsx +66 -2
- package/src/components/reference-create-dialog.tsx +4 -1
- package/src/components/render-edit-logic.ts +27 -0
- package/src/components/render-edit.tsx +579 -58
- package/src/components/render-field.tsx +86 -14
- package/src/context/draft-storage-context.tsx +39 -0
- package/src/i18n-defaults.ts +15 -0
- package/src/index.ts +8 -1
- package/src/primitives.tsx +35 -11
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
// Regression #1834: field types without a dedicated widget (embedded
|
|
2
|
-
// without embeddedListCells, jsonb,
|
|
2
|
+
// without embeddedListCells, jsonb, files, images) must render read-only
|
|
3
3
|
// instead of falling back to a text input. The old default-branch
|
|
4
4
|
// fallback ran the value through stringValue() — an object/array turned
|
|
5
5
|
// into "[object Object]"/"a,b", and saving that overwrote the real data.
|
|
6
|
+
// #1925 gave multiSelect a real combobox widget (so it dropped out of this
|
|
7
|
+
// list) and moved files/images in (no multi-upload widget yet, deliberately
|
|
8
|
+
// deferred — they used to hit the same text-input corruption via the
|
|
9
|
+
// default branch).
|
|
6
10
|
//
|
|
7
11
|
// Capture-Input/Banner instead of real primitives, same pattern as
|
|
8
12
|
// render-field-app-locale.test.tsx.
|
|
@@ -86,7 +90,8 @@ function renderField(field: EditFieldViewModel): void {
|
|
|
86
90
|
describe.each([
|
|
87
91
|
["embedded (kein embeddedListCells)", baseField({ type: "embedded", value: { note: "x" } })],
|
|
88
92
|
["jsonb", baseField({ type: "jsonb", value: { a: 1 } })],
|
|
89
|
-
["
|
|
93
|
+
["files", baseField({ type: "files", value: ["11111111-1111-1111-1111-111111111111"] })],
|
|
94
|
+
["images", baseField({ type: "images", value: ["11111111-1111-1111-1111-111111111111"] })],
|
|
90
95
|
])("RenderField — %s ohne Widget", (_name, field) => {
|
|
91
96
|
test("rendert einen schreibgeschützten Banner statt eines editierbaren Text-Inputs", () => {
|
|
92
97
|
renderField(field);
|
|
@@ -108,3 +113,62 @@ describe("RenderField — text bleibt weiterhin editierbar", () => {
|
|
|
108
113
|
expect(capturedInput?.kind).toBe("text");
|
|
109
114
|
});
|
|
110
115
|
});
|
|
116
|
+
|
|
117
|
+
// #1925: field types that gained a real widget on the auto-wired
|
|
118
|
+
// entityEdit path (previously either no widget at all or a type-mismatched
|
|
119
|
+
// text-input fallback).
|
|
120
|
+
describe("RenderField — #1925 neue Widgets", () => {
|
|
121
|
+
test("multiSelect rendert eine Multi-Combobox statt eines Banners", () => {
|
|
122
|
+
renderField(
|
|
123
|
+
baseField({
|
|
124
|
+
type: "multiSelect",
|
|
125
|
+
value: ["a"],
|
|
126
|
+
options: ["a", "b"],
|
|
127
|
+
}),
|
|
128
|
+
);
|
|
129
|
+
expect(capturedBanner).toBeUndefined();
|
|
130
|
+
expect(capturedInput).toBeDefined();
|
|
131
|
+
expect(capturedInput?.kind).toBe("combobox");
|
|
132
|
+
if (capturedInput?.kind !== "combobox") return;
|
|
133
|
+
expect(capturedInput.multiple).toBe(true);
|
|
134
|
+
expect(capturedInput.value).toEqual(["a"]);
|
|
135
|
+
expect(capturedInput.options).toEqual([
|
|
136
|
+
{ value: "a", label: "a" },
|
|
137
|
+
{ value: "b", label: "b" },
|
|
138
|
+
]);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test("decimal rendert ein number-Input statt Text (kein String-Fallback mehr)", () => {
|
|
142
|
+
renderField(baseField({ type: "decimal", value: 12.5 }));
|
|
143
|
+
expect(capturedBanner).toBeUndefined();
|
|
144
|
+
expect(capturedInput).toBeDefined();
|
|
145
|
+
expect(capturedInput?.kind).toBe("number");
|
|
146
|
+
if (capturedInput?.kind !== "number") return;
|
|
147
|
+
expect(capturedInput.value).toBe(12.5);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
test("bigInt rendert ein number-Input statt Text (kein String-Fallback mehr)", () => {
|
|
151
|
+
renderField(baseField({ type: "bigInt", value: 42 }));
|
|
152
|
+
expect(capturedBanner).toBeUndefined();
|
|
153
|
+
expect(capturedInput).toBeDefined();
|
|
154
|
+
expect(capturedInput?.kind).toBe("number");
|
|
155
|
+
if (capturedInput?.kind !== "number") return;
|
|
156
|
+
expect(capturedInput.value).toBe(42);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
test("tz rendert ein dediziertes tz-Input statt Text", () => {
|
|
160
|
+
renderField(baseField({ type: "tz", value: "Europe/Berlin" }));
|
|
161
|
+
expect(capturedBanner).toBeUndefined();
|
|
162
|
+
expect(capturedInput).toBeDefined();
|
|
163
|
+
expect(capturedInput?.kind).toBe("tz");
|
|
164
|
+
if (capturedInput?.kind !== "tz") return;
|
|
165
|
+
expect(capturedInput.value).toBe("Europe/Berlin");
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
test("longText rendert immer ein textarea, auch ohne explizites multiline-Flag", () => {
|
|
169
|
+
renderField(baseField({ type: "longText", value: "lange Notiz" }));
|
|
170
|
+
expect(capturedBanner).toBeUndefined();
|
|
171
|
+
expect(capturedInput).toBeDefined();
|
|
172
|
+
expect(capturedInput?.kind).toBe("textarea");
|
|
173
|
+
});
|
|
174
|
+
});
|
|
@@ -52,7 +52,10 @@ export function ReferenceCreateDialog({
|
|
|
52
52
|
}: ReferenceCreateDialogProps): ReactNode {
|
|
53
53
|
const { Modal } = usePrimitives();
|
|
54
54
|
const t = useTranslation();
|
|
55
|
-
const initial = useMemo(
|
|
55
|
+
const initial = useMemo(
|
|
56
|
+
() => buildInitialValues(entity.fields, entity.defaultCurrency ?? "EUR") as FormValues,
|
|
57
|
+
[entity.fields, entity.defaultCurrency],
|
|
58
|
+
);
|
|
56
59
|
const writeCommand = entityWriteCommand(featureName, screen.entity);
|
|
57
60
|
const handleSubmitted = (result: SubmitResult<unknown>): void => {
|
|
58
61
|
if (result.validationBlocked || !result.isSuccess) return;
|
|
@@ -39,3 +39,30 @@ export function shouldNotifyCaller(
|
|
|
39
39
|
): boolean {
|
|
40
40
|
return !(result.isSuccess && !extensionsPersisted);
|
|
41
41
|
}
|
|
42
|
+
|
|
43
|
+
// Restricts rendered sections to the caller's `fields` filter, keeping
|
|
44
|
+
// section order/title/visibility from the layout unchanged. A `fields`
|
|
45
|
+
// section whose filtered field list is empty is dropped entirely — an empty
|
|
46
|
+
// section container would read as a broken layout, not "nothing to show
|
|
47
|
+
// here". Extension sections are never filtered — they carry their own field
|
|
48
|
+
// set, unrelated to the `field`-name filter. `fieldsFilter === undefined`
|
|
49
|
+
// (no prop passed) returns the same array reference, so callers that skip
|
|
50
|
+
// the prop keep unchanged render behavior.
|
|
51
|
+
export function filterEditSections(
|
|
52
|
+
sections: readonly EditSectionViewModel[],
|
|
53
|
+
fieldsFilter: readonly string[] | undefined,
|
|
54
|
+
): readonly EditSectionViewModel[] {
|
|
55
|
+
if (fieldsFilter === undefined) return sections;
|
|
56
|
+
const filterSet = new Set(fieldsFilter);
|
|
57
|
+
const result: EditSectionViewModel[] = [];
|
|
58
|
+
for (const section of sections) {
|
|
59
|
+
if (section.kind === "extension") {
|
|
60
|
+
result.push(section);
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const fields = section.fields.filter((f) => filterSet.has(f.field));
|
|
64
|
+
if (fields.length === 0) continue;
|
|
65
|
+
result.push({ ...section, fields });
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|