fixes
This commit is contained in:
@@ -8,5 +8,7 @@ export { SseStreamView } from "./src/components/SseStreamView";
|
|||||||
export { SseConnectionStatus } from "./src/components/SseConnectionStatus";
|
export { SseConnectionStatus } from "./src/components/SseConnectionStatus";
|
||||||
export { getApi } from "./src/hooks/useApi";
|
export { getApi } from "./src/hooks/useApi";
|
||||||
export { useItemSse } from "./src/hooks/useItemSse";
|
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 { FilterComponentProps } from "./src/context/useResource";
|
||||||
export type { SpecConfiguration, ResourceConfig, FieldConfig, FKFieldConfig, ResourceRelationship } from "./src/types";
|
export type { SpecConfiguration, ResourceConfig, FieldConfig, FKFieldConfig, ResourceRelationship } from "./src/types";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
import { TextField } from "@mui/material";
|
import { TextField, Chip, Box } from "@mui/material";
|
||||||
import type { FieldConfig } from "../../types";
|
import type { FieldConfig } from "../../types";
|
||||||
import { StringField } from "./renderers/StringField";
|
import { StringField } from "./renderers/StringField";
|
||||||
import { NumberField } from "./renderers/NumberField";
|
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 (
|
||||||
|
<Box>
|
||||||
|
<Chip
|
||||||
|
label={`${field.label} (${Array.isArray(value) ? value.length : 0})`}
|
||||||
|
size="small"
|
||||||
|
color="default"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (field.type === "object" && !field.fk) {
|
||||||
|
return (
|
||||||
|
<Box>
|
||||||
|
<Chip
|
||||||
|
label={field.label}
|
||||||
|
size="small"
|
||||||
|
color="default"
|
||||||
|
variant="outlined"
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StringField
|
<StringField
|
||||||
field={field}
|
field={field}
|
||||||
|
|||||||
@@ -15,13 +15,21 @@ export function DiscriminatorField({ field, value, onChange, error }: Props) {
|
|||||||
const discProp = field.discriminatorProperty ?? "type";
|
const discProp = field.discriminatorProperty ?? "type";
|
||||||
const currentType = value?.[discProp] ?? "";
|
const currentType = value?.[discProp] ?? "";
|
||||||
|
|
||||||
|
function defaultFieldValue(field: FieldConfig): any {
|
||||||
|
if (field.enumValues) return field.enumValues[0];
|
||||||
|
if (field.isArray) return [];
|
||||||
|
if (field.type === "object") return {};
|
||||||
|
if (field.type === "number" || field.type === "integer") return 0;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const handleTypeChange = useCallback((e: any) => {
|
const handleTypeChange = useCallback((e: any) => {
|
||||||
const newType = e.target.value;
|
const newType = e.target.value;
|
||||||
const option = options.find((o) => o.value === newType);
|
const option = options.find((o) => o.value === newType);
|
||||||
const newValue: Record<string, any> = { [discProp]: newType };
|
const newValue: Record<string, any> = { [discProp]: newType };
|
||||||
if (option) {
|
if (option) {
|
||||||
for (const f of option.fields) {
|
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);
|
onChange(newValue);
|
||||||
|
|||||||
@@ -243,6 +243,10 @@ function buildDefaultShape(fields: FieldConfig[], schemas: Record<string, any>):
|
|||||||
const refSchemaObj = schemas[f.refSchema!];
|
const refSchemaObj = schemas[f.refSchema!];
|
||||||
const nestedFields = refSchemaObj ? extractFields(f.refSchema!, refSchemaObj, schemas) : [];
|
const nestedFields = refSchemaObj ? extractFields(f.refSchema!, refSchemaObj, schemas) : [];
|
||||||
shape[f.name] = f.isArray ? [] : buildDefaultShape(nestedFields, 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 {
|
} else {
|
||||||
shape[f.name] = null;
|
shape[f.name] = null;
|
||||||
}
|
}
|
||||||
@@ -254,7 +258,7 @@ function initEditValue(value: any, field: FieldConfig, schemas: Record<string, a
|
|||||||
if (field.isArray) {
|
if (field.isArray) {
|
||||||
return value ? value.map((item: any) => ({ ...item })) : [];
|
return value ? value.map((item: any) => ({ ...item })) : [];
|
||||||
}
|
}
|
||||||
if (value && typeof value === "object") {
|
if (value != null && value !== "" && typeof value === "object") {
|
||||||
return { ...value };
|
return { ...value };
|
||||||
}
|
}
|
||||||
return buildDefaultShape(
|
return buildDefaultShape(
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import DoneIcon from "@mui/icons-material/Done";
|
|||||||
import type { ResourceConfig, ParsedListResponse, FieldConfig } from "../types";
|
import type { ResourceConfig, ParsedListResponse, FieldConfig } from "../types";
|
||||||
import { useAppContext } from "./AppContext";
|
import { useAppContext } from "./AppContext";
|
||||||
import { getApi } from "../hooks/useApi";
|
import { getApi } from "../hooks/useApi";
|
||||||
|
import { sanitizePayload } from "../utils/sanitize-payload";
|
||||||
import { StringField } from "../components/fields/renderers/StringField";
|
import { StringField } from "../components/fields/renderers/StringField";
|
||||||
import { NumberField } from "../components/fields/renderers/NumberField";
|
import { NumberField } from "../components/fields/renderers/NumberField";
|
||||||
import { DateField } from "../components/fields/renderers/DateField";
|
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 {
|
export function useResource(resourceName: string): UseResourceReturn {
|
||||||
const { resources } = useAppContext();
|
const { resources, schemas } = useAppContext();
|
||||||
const resource = resources.find((r) => r.name === resourceName);
|
const resource = resources.find((r) => r.name === resourceName);
|
||||||
|
|
||||||
const [state, setState] = useState<ResourceState>({ loading: false, error: null });
|
const [state, setState] = useState<ResourceState>({ loading: false, error: null });
|
||||||
@@ -661,6 +662,18 @@ export function useResource(resourceName: string): UseResourceReturn {
|
|||||||
[rPath, setLoading, setError]
|
[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(
|
const create = useCallback(
|
||||||
async (data: any): Promise<any> => {
|
async (data: any): Promise<any> => {
|
||||||
if (!rPath) throw new Error(`Resource "${resourceName}" not found yet`);
|
if (!rPath) throw new Error(`Resource "${resourceName}" not found yet`);
|
||||||
@@ -668,7 +681,10 @@ export function useResource(resourceName: string): UseResourceReturn {
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const api = getApi();
|
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;
|
return res.data;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(parseError(e));
|
setError(parseError(e));
|
||||||
@@ -677,7 +693,7 @@ export function useResource(resourceName: string): UseResourceReturn {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[rPath, setLoading, setError]
|
[rPath, rFields, schemas, resolveFk, setLoading, setError]
|
||||||
);
|
);
|
||||||
|
|
||||||
const update = useCallback(
|
const update = useCallback(
|
||||||
@@ -687,8 +703,11 @@ export function useResource(resourceName: string): UseResourceReturn {
|
|||||||
setError(null);
|
setError(null);
|
||||||
try {
|
try {
|
||||||
const api = getApi();
|
const api = getApi();
|
||||||
|
const sanitized = rFields && schemas
|
||||||
|
? await sanitizePayload(data, rFields, schemas, resolveFk)
|
||||||
|
: data;
|
||||||
const method = rUpdateMethod ?? "put";
|
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;
|
return res.data;
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
setError(parseError(e));
|
setError(parseError(e));
|
||||||
@@ -697,7 +716,7 @@ export function useResource(resourceName: string): UseResourceReturn {
|
|||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[rPath, rUpdateMethod, setLoading, setError]
|
[rPath, rFields, schemas, resolveFk, rUpdateMethod, setLoading, setError]
|
||||||
);
|
);
|
||||||
|
|
||||||
const remove = useCallback(
|
const remove = useCallback(
|
||||||
|
|||||||
67
react-openapi/src/utils/sanitize-payload.ts
Normal file
67
react-openapi/src/utils/sanitize-payload.ts
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
import type { FieldConfig } from "../types";
|
||||||
|
import { extractFields } from "../transformers/field-config";
|
||||||
|
|
||||||
|
export type FkResolver = (resourceName: string, fkValue: any) => Promise<any>;
|
||||||
|
|
||||||
|
export async function sanitizePayload(
|
||||||
|
data: any,
|
||||||
|
fields: FieldConfig[],
|
||||||
|
schemas: Record<string, any>,
|
||||||
|
resolveFk?: FkResolver,
|
||||||
|
): Promise<any> {
|
||||||
|
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<string, any> = { ...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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user