import React, { useEffect, useState, useCallback } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { Box, Typography, Button, Paper, Grid, CircularProgress, Alert, Snackbar, } from "@mui/material"; import ArrowBackIcon from "@mui/icons-material/ArrowBack"; import SaveIcon from "@mui/icons-material/Save"; import type { ResourceConfig, FieldConfig } from "../types"; import { useResource } from "../context/useResource"; import { useAppContext } from "../context/AppContext"; import { getApi } from "../hooks/useApi"; import { FormFieldRenderer } from "./fields"; import { extractFields } from "../transformers/field-config"; interface ResourceFormProps { resource: ResourceConfig; basePath: string; mode: "create" | "edit"; } export function ResourceForm({ resource, basePath, mode }: ResourceFormProps) { const navigate = useNavigate(); const { id } = useParams(); const crud = useResource(resource); const { resources: allResources } = useAppContext(); const [formData, setFormData] = useState>({}); const [errors, setErrors] = useState>({}); const [saving, setSaving] = useState(false); const [snackbar, setSnackbar] = useState<{ open: boolean; message: string; severity: "success" | "error" }>({ open: false, message: "", severity: "success", }); const [fkOptions, setFkOptions] = useState>({}); const [fkLoading, setFkLoading] = useState>({}); const { schemas } = useAppContext(); useEffect(() => { console.log(`[ResourceForm] mounted resource="${resource.name}" mode=${mode} id=${id}`); console.log(`[ResourceForm] relationships:`, resource.relationships.map(r => `{field=${r.fieldName} target=${r.config.resource} prefetch=${r.config.prefetch}}`)); }, []); useEffect(() => { if (mode === "create") { const initial: Record = {}; resource.orderedFields.forEach((f) => { if (f.refSchema && !f.fk && formData[f.name] === undefined) { const refSchemaObj = schemas[f.refSchema!]; if (refSchemaObj) { const nestedFields = extractFields(f.refSchema!, refSchemaObj, schemas); initial[f.name] = f.isArray ? [] : buildInitialShape(nestedFields, schemas); } else { initial[f.name] = null; } } }); if (Object.keys(initial).length > 0) { setFormData((prev) => ({ ...prev, ...initial })); } } }, [mode, resource.name]); const loadFkOptions = useCallback(async (fieldName: string, fk: { resource: string; prefetch: boolean }) => { console.log(`[loadFkOptions] CALLED field="${fieldName}" resource="${fk.resource}" prefetch=${fk.prefetch}`); setFkLoading((prev) => ({ ...prev, [fieldName]: true })); try { const targetRes = allResources.find((r) => r.name === fk.resource); if (!targetRes) { console.log(`[loadFkOptions] targetRes NOT FOUND for "${fk.resource}"`); return; } console.log(`[loadFkOptions] targetRes found: path="${targetRes.path}" pagination=${!!targetRes.pagination}`); const api = getApi(); const params: Record = {}; if (targetRes.pagination) { params.limit = 0; } console.log(`[loadFkOptions] fetching GET ${targetRes.path}`, params); const res = await api.get(targetRes.path, { params }); console.log(`[loadFkOptions] response status=${res.status} data type=${typeof res.data} isArray=${Array.isArray(res.data)}`); let items: any[]; if (targetRes.pagination) { if (!res.data || typeof res.data !== "object" || !Array.isArray(res.data.items)) { console.log(`[loadFkOptions] paginated parse FAILED: data=`, res.data); throw new Error(`Expected paginated response from ${targetRes.path}`); } items = res.data.items; console.log(`[loadFkOptions] paginated: total=${res.data.total} items.length=${items.length}`); } else { if (!Array.isArray(res.data)) { console.log(`[loadFkOptions] non-paginated parse FAILED: data=`, res.data); throw new Error(`Expected array response from ${targetRes.path}`); } items = res.data; console.log(`[loadFkOptions] non-paginated: items.length=${items.length}`); } const opts = items.map((item: any) => ({ value: item[targetRes.primaryKey], label: applyFormat(item, targetRes.displayFormat), })); console.log(`[loadFkOptions] computed ${opts.length} options for field "${fieldName}"`, opts.slice(0, 3)); setFkOptions((prev) => ({ ...prev, [fieldName]: opts })); } catch (e) { console.log(`[loadFkOptions] ERROR field="${fieldName}":`, e); } finally { setFkLoading((prev) => ({ ...prev, [fieldName]: false })); } }, [allResources]); useEffect(() => { console.log(`[prefetch effect] ${resource.relationships.length} relationships, checking prefetch...`); resource.relationships.forEach((rel) => { console.log(`[prefetch effect] field="${rel.fieldName}" prefetch=${rel.config.prefetch} -> ${rel.config.prefetch ? "WILL FETCH" : "skipped (onFocus)"}`); if (rel.config.prefetch) { loadFkOptions(rel.fieldName, rel.config); } }); }, [resource.relationships, loadFkOptions]); useEffect(() => { if (mode === "edit" && id) { crud.get(id).then((data) => { const resolved = { ...(data ?? {}) }; resource.relationships.forEach((rel) => { const val = resolved[rel.fieldName]; if (val != null) { const targetRes = allResources.find((r) => r.name === rel.config.resource); if (targetRes) { if (Array.isArray(val)) { resolved[rel.fieldName] = val.map((item: any) => item[targetRes.primaryKey]); } else if (typeof val === "object") { resolved[rel.fieldName] = val[targetRes.primaryKey]; } } if (!rel.config.prefetch) { loadFkOptions(rel.fieldName, rel.config); } } }); setFormData(resolved); }); } }, [mode, id, loadFkOptions, resource.relationships]); const loadFkOnFocus = (fieldName: string) => { console.log(`[loadFkOnFocus] CALLED field="${fieldName}"`); const rel = resource.relationships.find((r) => r.fieldName === fieldName); if (rel) { console.log(`[loadFkOnFocus] found rel: prefetch=${rel.config.prefetch} fkOptions[${fieldName}]=${fkOptions[fieldName] ? "exists" : "undefined"}`); } else { console.log(`[loadFkOnFocus] NO RELATIONSHIP found for field="${fieldName}"`); } if (rel && !rel.config.prefetch && !fkOptions[fieldName]) { console.log(`[loadFkOnFocus] conditions met -> calling loadFkOptions`); loadFkOptions(fieldName, rel.config); } else { console.log(`[loadFkOnFocus] NOT calling loadFkOptions: rel=${!!rel} !prefetch=${rel && !rel.config.prefetch} !hasOptions=${!fkOptions[fieldName]}`); } }; const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); const validationErrors: Record = {}; resource.orderedFields .filter((f) => f.required && !f.readOnly && f.name !== resource.primaryKey) .forEach((f) => { if (formData[f.name] === undefined || formData[f.name] === null || formData[f.name] === "") { validationErrors[f.name] = `${f.label} is required`; } }); if (Object.keys(validationErrors).length > 0) { setErrors(validationErrors); return; } setErrors({}); setSaving(true); try { if (mode === "create") { await crud.create(formData); setSnackbar({ open: true, message: "Created successfully", severity: "success" }); navigate(`${basePath}/${resource.name}`); } else { await crud.update(id!, formData); setSnackbar({ open: true, message: "Updated successfully", severity: "success" }); navigate(`${basePath}/${resource.name}/${id}`); } } catch (e: any) { setSnackbar({ open: true, message: e.message ?? "Operation failed", severity: "error" }); } finally { setSaving(false); } }; const handleChange = (fieldName: string, value: any) => { setFormData((prev) => ({ ...prev, [fieldName]: value })); if (errors[fieldName]) { setErrors((prev) => { const copy = { ...prev }; delete copy[fieldName]; return copy; }); } }; const title = mode === "create" ? `Create ${resource.schemaName}` : `Edit ${resource.schemaName}`; return ( {title} {resource.orderedFields .filter((f) => !(f.name === resource.primaryKey && mode === "edit")) .map((field) => ( handleChange(field.name, val)} error={errors[field.name]} fkOptions={fkOptions[field.name]} fkLoading={fkLoading[field.name]} recordId={id} onFkOpen={loadFkOnFocus} /> ))} setSnackbar((s) => ({ ...s, open: false }))} > setSnackbar((s) => ({ ...s, open: false }))}> {snackbar.message} ); } function applyFormat(obj: any, format: string): string { if (!obj || typeof obj !== "object") return String(obj ?? ""); return format.replace(/\{(\w+)\}/g, (_, key) => String(obj[key] ?? "")); } function buildInitialShape(fields: FieldConfig[], schemas: Record): Record { const shape: Record = {}; for (const f of fields) { if (f.refSchema && !f.fk) { const refSchemaObj = schemas[f.refSchema!]; const nestedFields = refSchemaObj ? extractFields(f.refSchema!, refSchemaObj, schemas) : []; shape[f.name] = f.isArray ? [] : buildInitialShape(nestedFields, schemas); } else { shape[f.name] = null; } } return shape; }