diff --git a/react-openapi/index.ts b/react-openapi/index.ts
index 3b5f543..fb73fa2 100644
--- a/react-openapi/index.ts
+++ b/react-openapi/index.ts
@@ -8,5 +8,7 @@ export { SseStreamView } from "./src/components/SseStreamView";
export { SseConnectionStatus } from "./src/components/SseConnectionStatus";
export { getApi } from "./src/hooks/useApi";
export { useItemSse } from "./src/hooks/useItemSse";
+export { sanitizePayload } from "./src/utils/sanitize-payload";
+export type { FkResolver } from "./src/utils/sanitize-payload";
export type { FilterComponentProps } from "./src/context/useResource";
export type { SpecConfiguration, ResourceConfig, FieldConfig, FKFieldConfig, ResourceRelationship } from "./src/types";
diff --git a/react-openapi/src/components/fields/FormFieldRenderer.tsx b/react-openapi/src/components/fields/FormFieldRenderer.tsx
index 425c3f9..6d9616e 100644
--- a/react-openapi/src/components/fields/FormFieldRenderer.tsx
+++ b/react-openapi/src/components/fields/FormFieldRenderer.tsx
@@ -1,5 +1,5 @@
import React from "react";
-import { TextField } from "@mui/material";
+import { TextField, Chip, Box } from "@mui/material";
import type { FieldConfig } from "../../types";
import { StringField } from "./renderers/StringField";
import { NumberField } from "./renderers/NumberField";
@@ -139,6 +139,32 @@ export function FormFieldRenderer({ field, value, onChange, error, fkOptions, fk
);
}
+ if (field.type === "array" && !field.fk) {
+ return (
+
+
+
+ );
+ }
+
+ if (field.type === "object" && !field.fk) {
+ return (
+
+
+
+ );
+ }
+
return (
{
const newType = e.target.value;
const option = options.find((o) => o.value === newType);
const newValue: Record = { [discProp]: newType };
if (option) {
for (const f of option.fields) {
- newValue[f.name] = f.enumValues?.[0] ?? f.type === "number" ? 0 : f.type === "integer" ? 0 : "";
+ newValue[f.name] = defaultFieldValue(f);
}
}
onChange(newValue);
diff --git a/react-openapi/src/components/fields/renderers/JsonField.tsx b/react-openapi/src/components/fields/renderers/JsonField.tsx
index 8fd34a9..14aefb9 100644
--- a/react-openapi/src/components/fields/renderers/JsonField.tsx
+++ b/react-openapi/src/components/fields/renderers/JsonField.tsx
@@ -243,6 +243,10 @@ function buildDefaultShape(fields: FieldConfig[], schemas: Record):
const refSchemaObj = schemas[f.refSchema!];
const nestedFields = refSchemaObj ? extractFields(f.refSchema!, refSchemaObj, schemas) : [];
shape[f.name] = f.isArray ? [] : buildDefaultShape(nestedFields, schemas);
+ } else if (f.isArray) {
+ shape[f.name] = [];
+ } else if (f.type === "object") {
+ shape[f.name] = {};
} else {
shape[f.name] = null;
}
@@ -254,7 +258,7 @@ function initEditValue(value: any, field: FieldConfig, schemas: Record ({ ...item })) : [];
}
- if (value && typeof value === "object") {
+ if (value != null && value !== "" && typeof value === "object") {
return { ...value };
}
return buildDefaultShape(
diff --git a/react-openapi/src/context/useResource.tsx b/react-openapi/src/context/useResource.tsx
index 67b29e7..4e9d7de 100644
--- a/react-openapi/src/context/useResource.tsx
+++ b/react-openapi/src/context/useResource.tsx
@@ -4,6 +4,7 @@ import DoneIcon from "@mui/icons-material/Done";
import type { ResourceConfig, ParsedListResponse, FieldConfig } from "../types";
import { useAppContext } from "./AppContext";
import { getApi } from "../hooks/useApi";
+import { sanitizePayload } from "../utils/sanitize-payload";
import { StringField } from "../components/fields/renderers/StringField";
import { NumberField } from "../components/fields/renderers/NumberField";
import { DateField } from "../components/fields/renderers/DateField";
@@ -588,7 +589,7 @@ function buildFilterComponent(field: FieldConfig, resourceName: string): React.F
}
export function useResource(resourceName: string): UseResourceReturn {
- const { resources } = useAppContext();
+ const { resources, schemas } = useAppContext();
const resource = resources.find((r) => r.name === resourceName);
const [state, setState] = useState({ loading: false, error: null });
@@ -661,6 +662,18 @@ export function useResource(resourceName: string): UseResourceReturn {
[rPath, setLoading, setError]
);
+ const resolveFk = useCallback(async (resourceName: string, id: any) => {
+ const targetRes = resources.find((r) => r.name === resourceName);
+ if (!targetRes) return id;
+ try {
+ const api = getApi();
+ const res = await api.get(`${targetRes.path}/${id}`);
+ return res.data;
+ } catch {
+ return id;
+ }
+ }, [resources]);
+
const create = useCallback(
async (data: any): Promise => {
if (!rPath) throw new Error(`Resource "${resourceName}" not found yet`);
@@ -668,7 +681,10 @@ export function useResource(resourceName: string): UseResourceReturn {
setError(null);
try {
const api = getApi();
- const res = await api.post(rPath, data);
+ const sanitized = rFields && schemas
+ ? await sanitizePayload(data, rFields, schemas, resolveFk)
+ : data;
+ const res = await api.post(rPath, sanitized);
return res.data;
} catch (e: any) {
setError(parseError(e));
@@ -677,7 +693,7 @@ export function useResource(resourceName: string): UseResourceReturn {
setLoading(false);
}
},
- [rPath, setLoading, setError]
+ [rPath, rFields, schemas, resolveFk, setLoading, setError]
);
const update = useCallback(
@@ -687,8 +703,11 @@ export function useResource(resourceName: string): UseResourceReturn {
setError(null);
try {
const api = getApi();
+ const sanitized = rFields && schemas
+ ? await sanitizePayload(data, rFields, schemas, resolveFk)
+ : data;
const method = rUpdateMethod ?? "put";
- const res = await (method === "patch" ? api.patch : api.put)(`${rPath}/${id}`, data);
+ const res = await (method === "patch" ? api.patch : api.put)(`${rPath}/${id}`, sanitized);
return res.data;
} catch (e: any) {
setError(parseError(e));
@@ -697,7 +716,7 @@ export function useResource(resourceName: string): UseResourceReturn {
setLoading(false);
}
},
- [rPath, rUpdateMethod, setLoading, setError]
+ [rPath, rFields, schemas, resolveFk, rUpdateMethod, setLoading, setError]
);
const remove = useCallback(
diff --git a/react-openapi/src/utils/sanitize-payload.ts b/react-openapi/src/utils/sanitize-payload.ts
new file mode 100644
index 0000000..653ced5
--- /dev/null
+++ b/react-openapi/src/utils/sanitize-payload.ts
@@ -0,0 +1,67 @@
+import type { FieldConfig } from "../types";
+import { extractFields } from "../transformers/field-config";
+
+export type FkResolver = (resourceName: string, fkValue: any) => Promise;
+
+export async function sanitizePayload(
+ data: any,
+ fields: FieldConfig[],
+ schemas: Record,
+ resolveFk?: FkResolver,
+): Promise {
+ if (data == null || typeof data !== "object") return data;
+
+ if (Array.isArray(data)) {
+ return Promise.all(data.map((item) => {
+ if (item != null && typeof item === "object") {
+ return sanitizePayload(item, fields, schemas, resolveFk);
+ }
+ return item;
+ }));
+ }
+
+ const result: Record = { ...data };
+ const fieldMap = new Map(fields.map((f) => [f.name, f]));
+
+ for (const key of Object.keys(result)) {
+ const field = fieldMap.get(key);
+ if (!field) continue;
+
+ const val = result[key];
+
+ if (field.fk) {
+ if (val == null || val === "") {
+ result[key] = null;
+ } else if (resolveFk) {
+ if (field.isArray && Array.isArray(val)) {
+ result[key] = await Promise.all(val.map((v: any) => resolveFk(field.fk!.resource, v)));
+ } else {
+ result[key] = await resolveFk(field.fk!.resource, val);
+ }
+ }
+ continue;
+ }
+
+ if (field.refSchema && !field.fk) {
+ if (val == null || val === "") {
+ result[key] = field.isArray ? [] : {};
+ } else if (typeof val === "object") {
+ const refSchemaObj = schemas[field.refSchema!];
+ const nestedFields = refSchemaObj
+ ? extractFields(field.refSchema!, refSchemaObj, schemas)
+ : [];
+ result[key] = await sanitizePayload(val, nestedFields, schemas, resolveFk);
+ }
+ } else if (field.isArray) {
+ if (val == null || val === "") {
+ result[key] = [];
+ }
+ } else if (field.type === "object") {
+ if (val == null || val === "") {
+ result[key] = {};
+ }
+ }
+ }
+
+ return result;
+}