@cosmicdrift/kumiko-bundled-features 0.34.0 → 0.34.2
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
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cosmicdrift/kumiko-bundled-features",
|
|
3
|
-
"version": "0.34.
|
|
3
|
+
"version": "0.34.2",
|
|
4
4
|
"description": "Built-in features — tenant, user, auth, delivery. The stuff you'd rewrite anyway, already typed.",
|
|
5
5
|
"license": "BUSL-1.1",
|
|
6
6
|
"author": "Marc Frost <marc@cosmicdriftgamestudio.com>",
|
|
@@ -110,6 +110,63 @@ describe("CustomFieldsFormSection", () => {
|
|
|
110
110
|
});
|
|
111
111
|
});
|
|
112
112
|
|
|
113
|
+
test("pre-fills inputs from initialValues (Edit zeigt den Bestand, nicht write-only)", async () => {
|
|
114
|
+
mockedQueryRows = [
|
|
115
|
+
{
|
|
116
|
+
id: "f1",
|
|
117
|
+
entityName: "component",
|
|
118
|
+
fieldKey: "vendor",
|
|
119
|
+
type: "text",
|
|
120
|
+
required: false,
|
|
121
|
+
displayOrder: 1,
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "f2",
|
|
125
|
+
entityName: "component",
|
|
126
|
+
fieldKey: "tier",
|
|
127
|
+
type: "number",
|
|
128
|
+
required: false,
|
|
129
|
+
displayOrder: 2,
|
|
130
|
+
},
|
|
131
|
+
];
|
|
132
|
+
dispatchSpy.mockClear();
|
|
133
|
+
|
|
134
|
+
render(
|
|
135
|
+
<Wrapper>
|
|
136
|
+
<CustomFieldsFormSection
|
|
137
|
+
entityName="component"
|
|
138
|
+
entityId="row-42"
|
|
139
|
+
initialValues={{ vendor: "Hetzner", tier: 3 }}
|
|
140
|
+
/>
|
|
141
|
+
</Wrapper>,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
// Gespeicherte Werte sind in den Inputs sichtbar (kein write-only mehr).
|
|
145
|
+
const vendorInput = document.getElementById("custom-field-vendor") as HTMLInputElement;
|
|
146
|
+
const tierInput = document.getElementById("custom-field-tier") as HTMLInputElement;
|
|
147
|
+
expect(vendorInput.value).toBe("Hetzner");
|
|
148
|
+
expect(tierInput.value).toBe("3");
|
|
149
|
+
|
|
150
|
+
// Save bleibt disabled bis zur ersten Änderung (pending trackt nur Edits) —
|
|
151
|
+
// sonst würde jeder Mount alle Werte sinnlos neu schreiben.
|
|
152
|
+
const saveBtn = screen.getByTestId("custom-fields-form-save") as HTMLButtonElement;
|
|
153
|
+
expect(saveBtn.disabled).toBe(true);
|
|
154
|
+
|
|
155
|
+
// Nur das geänderte Feld wird geschrieben, nicht der unveränderte Bestand.
|
|
156
|
+
fireEvent.change(vendorInput, { target: { value: "Netcup" } });
|
|
157
|
+
expect(saveBtn.disabled).toBe(false);
|
|
158
|
+
fireEvent.click(saveBtn);
|
|
159
|
+
await Promise.resolve();
|
|
160
|
+
await Promise.resolve();
|
|
161
|
+
expect(dispatchSpy).toHaveBeenCalledTimes(1);
|
|
162
|
+
expect(dispatchSpy).toHaveBeenCalledWith("custom-fields:write:set-custom-field", {
|
|
163
|
+
entityName: "component",
|
|
164
|
+
entityId: "row-42",
|
|
165
|
+
fieldKey: "vendor",
|
|
166
|
+
value: "Netcup",
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
113
170
|
test("shows create-mode banner when entityId is null and skips the fieldDefinition query", () => {
|
|
114
171
|
mockedQueryRows = [];
|
|
115
172
|
useQuerySpy.mockClear();
|
|
@@ -35,9 +35,15 @@ type FieldDefinitionListResponse = {
|
|
|
35
35
|
export function CustomFieldsFormSection({
|
|
36
36
|
entityName,
|
|
37
37
|
entityId,
|
|
38
|
+
initialValues,
|
|
38
39
|
}: {
|
|
39
40
|
readonly entityName: string;
|
|
40
41
|
readonly entityId: string | null;
|
|
42
|
+
/** Bereits gespeicherte customField-Werte der Entity (aus der detail-
|
|
43
|
+
* row durchgereicht). Ohne sie wäre die Section write-only — die Inputs
|
|
44
|
+
* zeigen den Bestand beim Edit. `pending` trackt nur Änderungen, also
|
|
45
|
+
* bleibt der Save-Button bis zur ersten Eingabe disabled. */
|
|
46
|
+
readonly initialValues?: Readonly<Record<string, unknown>>;
|
|
41
47
|
}): ReactNode {
|
|
42
48
|
const { Banner, Button, Field, Input, Text } = usePrimitives();
|
|
43
49
|
const t = useTranslation();
|
|
@@ -122,8 +128,10 @@ export function CustomFieldsFormSection({
|
|
|
122
128
|
label={field.fieldKey}
|
|
123
129
|
required={field.required}
|
|
124
130
|
>
|
|
125
|
-
{renderInputFor(
|
|
126
|
-
|
|
131
|
+
{renderInputFor(
|
|
132
|
+
field,
|
|
133
|
+
pending[field.fieldKey] ?? displayValue(field.type, initialValues?.[field.fieldKey]),
|
|
134
|
+
(v) => setPending((p) => ({ ...p, [field.fieldKey]: v })),
|
|
127
135
|
)}
|
|
128
136
|
</Field>
|
|
129
137
|
))}
|
|
@@ -189,3 +197,13 @@ function coerceValue(type: string, raw: string): unknown {
|
|
|
189
197
|
if (type === "boolean") return raw === "true";
|
|
190
198
|
return raw;
|
|
191
199
|
}
|
|
200
|
+
|
|
201
|
+
// Umkehrung von coerceValue: gespeicherter jsonb-Wert → string-Form für den
|
|
202
|
+
// Input. number/boolean/date/text werden so dargestellt, wie der Input sie
|
|
203
|
+
// erwartet; fehlende Werte werden zu "".
|
|
204
|
+
function displayValue(type: string, value: unknown): string {
|
|
205
|
+
if (value === undefined || value === null) return "";
|
|
206
|
+
if (type === "boolean") return value === true ? "true" : "false";
|
|
207
|
+
if (type === "number") return typeof value === "number" ? String(value) : String(value);
|
|
208
|
+
return String(value);
|
|
209
|
+
}
|