Skip to content

Commit a5acc8d

Browse files
committed
feat: Embedded form
1 parent 31a3ad8 commit a5acc8d

18 files changed

Lines changed: 822 additions & 76 deletions

File tree

apps/docs/pages/docs/i18n.mdx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
import { Callout } from "nextra/components";
2-
import OptionsTable from "../../components/OptionsTable";
3-
41
# I18n
52

63
<Callout type="info">
@@ -146,6 +143,30 @@ The following keys are accepted:
146143
"The text displayed in the multiselect widget in list display mode to toggle the select dialog",
147144
defaultValue: '"Select items"',
148145
},
146+
{
147+
name: "form.widgets.multiselect.create",
148+
description:
149+
"The text displayed in the multiselect widget create button when allowCreate is enabled",
150+
defaultValue: '"Create item"',
151+
},
152+
{
153+
name: "form.widgets.select.create",
154+
description:
155+
"The text displayed in the select widget create button when allowCreate is enabled",
156+
defaultValue: '"Create item"',
157+
},
158+
{
159+
name: "form.widgets.multiselect.edit",
160+
description:
161+
"The text displayed in the multiselect widget edit button when allowEdit is enabled",
162+
defaultValue: '"Edit item"',
163+
},
164+
{
165+
name: "form.widgets.select.edit",
166+
description:
167+
"The text displayed in the select widget edit button when allowEdit is enabled",
168+
defaultValue: '"Edit item"',
169+
},
149170
{
150171
name: "form.widgets.scalar_array.add",
151172
description:

packages/examples-common/messages/fr.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ export default {
105105
},
106106
multiselect: {
107107
select: "Sélectionner",
108+
create: "Créer un élément",
109+
edit: "Modifier un élément",
110+
},
111+
select: {
112+
create: "Créer un élément",
113+
edit: "Modifier un élément",
108114
},
109115
},
110116
user: {

packages/examples-common/options.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ export const options: NextAdminOptions = {
111111
},
112112
posts: {
113113
display: "table",
114+
allowCreate: true,
114115
},
115116
avatar: {
116117
format: "file",
@@ -299,6 +300,7 @@ export const options: NextAdminOptions = {
299300
display: "list",
300301
orderField: "order",
301302
relationshipSearchField: "category",
303+
allowCreate: true,
302304
},
303305
images: {
304306
format: "file",
@@ -357,6 +359,8 @@ export const options: NextAdminOptions = {
357359
display: "list",
358360
relationshipSearchField: "post",
359361
orderField: "order",
362+
allowCreate: true,
363+
allowEdit: true,
360364
},
361365
},
362366
},

packages/examples-common/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
"scripts": {
55
"dev": "tsc -w",
66
"build": "tsc",
7-
"clean": "rm -rf dist"
7+
"clean": "rm -rf dist",
8+
"typecheck": "tsc --noEmit"
89
},
910
"exports": {
1011
"./components": "./dist/components/index.js",

packages/generator-prisma/src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
#!/usr/bin/env node
22
import { generatorHandler } from "@prisma/generator-helper";
33
import { parseEnvValue } from "@prisma/internals";
4-
import path from "path";
54
import fs from "fs/promises";
5+
import path from "path";
66
// @ts-expect-error
77
import { transformDMMF } from "prisma-json-schema-generator/dist/generator/transformDMMF";
88
import { insertDmmfData } from "./dmmf";

packages/next-admin/src/appHandler.ts

Lines changed: 90 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import cloneDeep from "lodash.clonedeep";
12
import { createEdgeRouter } from "next-connect";
23
import { HookError } from "./exceptions/HookError";
34
import { handleOptionsSearch } from "./handlers/options";
@@ -12,16 +13,18 @@ import {
1213
RequestContext,
1314
ServerAction,
1415
} from "./types";
16+
import { getSchema, initGlobals } from "./utils/globals";
17+
import { getSchemaForResource, getSchemas } from "./utils/jsonSchema";
1518
import { hasPermission } from "./utils/permissions";
16-
import { getRawData } from "./utils/prisma";
19+
import { getDataItem, getRawData } from "./utils/prisma";
1720
import {
1821
formatId,
1922
getFormValuesFromFormData,
2023
getModelIdProperty,
2124
getResourceFromParams,
2225
getResources,
26+
transformSchema,
2327
} from "./utils/server";
24-
import { getSchema, initGlobals } from "./utils/globals";
2528

2629
export const createHandler = <P extends string = "nextadmin">({
2730
apiBasePath,
@@ -51,6 +54,88 @@ export const createHandler = <P extends string = "nextadmin">({
5154
}
5255

5356
router
57+
.get(`${apiBasePath}/:model/schema/:id?`, async (req, ctx) => {
58+
try {
59+
const resources = getResources(options);
60+
const params = await ctx.params;
61+
const resource = getResourceFromParams(
62+
[params[paramKey][0]],
63+
resources
64+
);
65+
66+
if (!resource) {
67+
return Response.json(
68+
{ error: "Resource not found" },
69+
{ status: 404 }
70+
);
71+
}
72+
73+
const id =
74+
params[paramKey].length > 2 ? params[paramKey][2] : undefined;
75+
const edit = options?.model?.[resource]?.edit;
76+
77+
let deepCopySchema = await transformSchema(
78+
resource,
79+
//@ts-expect-error
80+
edit,
81+
options
82+
)(cloneDeep(getSchema()));
83+
84+
const resourceSchema = getSchemaForResource(deepCopySchema, resource);
85+
86+
if (id) {
87+
const formattedId = formatId(resource, id);
88+
89+
const { data, relationshipsRawData } = await getDataItem({
90+
prisma,
91+
resource,
92+
resourceId: formattedId,
93+
options,
94+
});
95+
96+
const { uiSchema, schema } = getSchemas(
97+
data,
98+
resourceSchema,
99+
edit?.fields as EditFieldsOptions<typeof resource>
100+
);
101+
102+
return Response.json({
103+
data,
104+
modelSchema: schema,
105+
uiSchema,
106+
relationshipsRawData,
107+
resource,
108+
});
109+
}
110+
111+
const relationshipsRawData = await getRawData({
112+
prisma,
113+
resource,
114+
resourceIds: [],
115+
maxDepth: 2,
116+
});
117+
118+
const { uiSchema, schema } = getSchemas(
119+
null,
120+
resourceSchema,
121+
edit?.fields as EditFieldsOptions<typeof resource>
122+
);
123+
124+
return Response.json({
125+
data: null,
126+
modelSchema: schema,
127+
uiSchema,
128+
relationshipsRawData,
129+
resource,
130+
});
131+
} catch (e) {
132+
console.error("Error in GET schema endpoint:", e);
133+
return Response.json(
134+
{ error: (e as Error)?.message || "Unknown error occurred" },
135+
{ status: 500 }
136+
);
137+
}
138+
})
54139
.get(`${apiBasePath}/:model/raw`, async (req, ctx) => {
55140
const resources = getResources(options);
56141
const params = await ctx.params;
@@ -88,6 +173,9 @@ export const createHandler = <P extends string = "nextadmin">({
88173

89174
return Response.json(data);
90175
})
176+
.get(`${apiBasePath}/:model/:id`, async (req, ctx) => {
177+
return Response.json({ error: "Not implemented" }, { status: 200 });
178+
})
91179
.post(`${apiBasePath}/:model/actions/:id`, async (req, ctx) => {
92180
const resources = getResources(options);
93181
const params = await ctx.params;

packages/next-admin/src/components/Form.tsx

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,8 @@ import {
44
InformationCircleIcon,
55
TrashIcon,
66
} from "@heroicons/react/24/outline";
7-
import RjsfForm from "@rjsf/core";
87
import type { FormProps as RjsfFormProps } from "@rjsf/core";
8+
import RjsfForm from "@rjsf/core";
99
import {
1010
BaseInputTemplateProps,
1111
ErrorSchema,
@@ -48,6 +48,7 @@ import {
4848
Permission,
4949
} from "../types";
5050
import { getSchemas } from "../utils/jsonSchema";
51+
import { getSubmitButtonOptions } from "../utils/rjsf";
5152
import { formatLabel, isFileUploadFormat, slugify } from "../utils/tools";
5253
import FormHeader from "./FormHeader";
5354
import ArrayField from "./inputs/ArrayField";
@@ -69,7 +70,6 @@ import {
6970
TooltipRoot,
7071
TooltipTrigger,
7172
} from "./radix/Tooltip";
72-
import { getSubmitButtonOptions } from "../utils/rjsf";
7373

7474
const RichTextField = lazy(() => import("./inputs/RichText/RichTextField"));
7575

@@ -82,12 +82,14 @@ const widgets: RjsfFormProps["widgets"] = {
8282
TextareaWidget: TextareaWidget,
8383
};
8484

85-
const Form = ({
85+
export const Form = ({
8686
data,
8787
schema,
8888
resource,
8989
validation: validationProp,
9090
customInputs,
91+
onSubmitCallback,
92+
isEmbedded,
9193
}: FormProps) => {
9294
const [validation, setValidation] = useState(validationProp);
9395
const { basePath, options, apiBasePath } = useConfig();
@@ -229,6 +231,7 @@ const Form = ({
229231
body: formData,
230232
}
231233
);
234+
debugger;
232235
const result = await response.json();
233236
if (result?.validation) {
234237
setValidation(result.validation);
@@ -240,6 +243,12 @@ const Form = ({
240243
cleanAll();
241244
}
242245
if (result?.deleted) {
246+
247+
if (onSubmitCallback) {
248+
onSubmitCallback(result);
249+
return;
250+
}
251+
243252
return router.replace({
244253
pathname: `${basePath}/${slugify(resource)}`,
245254
query: {
@@ -251,6 +260,12 @@ const Form = ({
251260
});
252261
}
253262
if (result?.created) {
263+
264+
if (onSubmitCallback) {
265+
onSubmitCallback(result);
266+
return;
267+
}
268+
254269
const pathname = result?.redirect
255270
? `${basePath}/${slugify(resource)}`
256271
: `${basePath}/${slugify(resource)}/${result.createdId}`;
@@ -266,6 +281,12 @@ const Form = ({
266281
});
267282
}
268283
if (result?.updated) {
284+
285+
if (onSubmitCallback) {
286+
onSubmitCallback(result);
287+
return;
288+
}
289+
269290
const pathname = result?.redirect
270291
? `${basePath}/${slugify(resource)}`
271292
: location.pathname;
@@ -306,9 +327,9 @@ const Form = ({
306327
const customInput = customInputs?.[props.name as Field<ModelName>];
307328
const improvedCustomInput = customInput
308329
? cloneElement(customInput, {
309-
...customInput.props,
310-
mode: edit ? "edit" : "create",
311-
})
330+
...customInput.props,
331+
mode: edit ? "edit" : "create",
332+
})
312333
: undefined;
313334
return <ArrayField {...props} customInput={improvedCustomInput} />;
314335
},
@@ -605,7 +626,7 @@ const Form = ({
605626
extraErrors={extraErrors}
606627
fields={fields}
607628
disabled={allDisabled}
608-
formContext={{ isPending, schema }}
629+
formContext={{ isPending, schema, parentId: id }}
609630
templates={templates}
610631
widgets={widgets}
611632
ref={ref}
@@ -618,9 +639,9 @@ const Form = ({
618639

619640
return (
620641
<div className="relative h-full">
621-
<div className="bg-nextadmin-background-default dark:bg-dark-nextadmin-background-default max-w-full p-4 align-middle sm:p-8">
642+
<div className={clsx(!isEmbedded && "bg-nextadmin-background-default dark:bg-dark-nextadmin-background-default p-4 sm:p-8", "max-w-full align-middle ")}>
622643
<Message className="-mt-2 mb-2 sm:-mt-4 sm:mb-4" />
623-
<div className="bg-nextadmin-background-default dark:bg-dark-nextadmin-background-emphasis border-nextadmin-border-default dark:border-dark-nextadmin-border-default max-w-screen-md rounded-lg border p-4 sm:p-8">
644+
<div className={clsx(!isEmbedded && "bg-nextadmin-background-default dark:bg-dark-nextadmin-background-emphasis border-nextadmin-border-default dark:border-dark-nextadmin-border-default rounded-lg border", "max-w-screen-md p-4 sm:p-8")}>
624645
<RjsfFormComponent ref={formRef} />
625646
</div>
626647
</div>
@@ -635,7 +656,7 @@ const FormWrapper = ({
635656
}: FormProps) => {
636657
return (
637658
<ClientActionDialogProvider componentsMap={clientActionsComponents}>
638-
<FormHeader {...props} />
659+
{props?.isEmbedded ? null : <FormHeader {...props} />}
639660
<FormDataProvider
640661
data={props.data}
641662
relationshipsRawData={relationshipsRawData}

packages/next-admin/src/components/inputs/ArrayField.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { FieldProps } from "@rjsf/utils";
22
import type { CustomInputProps, Enumeration, FormProps } from "../../types";
3+
import FileWidget from "./FileWidget/FileWidget";
34
import MultiSelectWidget from "./MultiSelect/MultiSelectWidget";
45
import ScalarArrayField from "./ScalarArray/ScalarArrayField";
5-
import FileWidget from "./FileWidget/FileWidget";
66

77
const ArrayField = (
88
props: FieldProps & { customInput?: React.ReactElement<CustomInputProps> }
@@ -22,7 +22,7 @@ const ArrayField = (
2222

2323
const field =
2424
resourceDefinition.properties[
25-
name as keyof typeof resourceDefinition.properties
25+
name as keyof typeof resourceDefinition.properties
2626
];
2727

2828
if (field?.__nextadmin?.kind === "scalar" && field?.__nextadmin?.isList) {
@@ -66,6 +66,7 @@ const ArrayField = (
6666
required={required}
6767
schema={schema}
6868
options={options}
69+
formContext={formContext}
6970
/>
7071
);
7172
};

0 commit comments

Comments
 (0)