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; +}