diff --git a/src/generators/NextGenerator.js b/src/generators/NextGenerator.js index 809f2c60..0c307818 100644 --- a/src/generators/NextGenerator.js +++ b/src/generators/NextGenerator.js @@ -11,7 +11,7 @@ export default class NextGenerator extends BaseGenerator { this.routeAddedtoServer = false; this.registerTemplates(`next/`, [ // components - "components/common/Layout.tsx", + "components/common/Wrapper.tsx", "components/common/Pagination.tsx", "components/common/ReferenceLinks.tsx", "components/foo/List.tsx", @@ -25,12 +25,12 @@ export default class NextGenerator extends BaseGenerator { "types/item.ts", // pages - "pages/foos/[id]/index.tsx", - "pages/foos/[id]/edit.tsx", - "pages/foos/page/[page].tsx", - "pages/foos/index.tsx", - "pages/foos/create.tsx", - "pages/_app.tsx", + "app/foos/[id]/page.tsx", + "app/foos/[id]/edit/page.tsx", + "app/foos/page/[page]/page.tsx", + "app/foos/page.tsx", + "app/foos/create/page.tsx", + "app/layout.tsx", // utils "utils/dataAccess.ts", @@ -81,9 +81,12 @@ export default class NextGenerator extends BaseGenerator { // Copy with patterned name this.createDir(`${dir}/components/${context.lc}`); - this.createDir(`${dir}/pages/${context.lc}s`); - this.createDir(`${dir}/pages/${context.lc}s/[id]`); - this.createDir(`${dir}/pages/${context.lc}s/page`); + this.createDir(`${dir}/app/${context.lc}s`); + this.createDir(`${dir}/app/${context.lc}s/[id]`); + this.createDir(`${dir}/app/${context.lc}s/create`); + this.createDir(`${dir}/app/${context.lc}s/[id]/edit`); + this.createDir(`${dir}/app/${context.lc}s/page`); + this.createDir(`${dir}/app/${context.lc}s/page/[page]`); [ // components "components/%s/List.tsx", @@ -92,11 +95,11 @@ export default class NextGenerator extends BaseGenerator { "components/%s/Form.tsx", // pages - "pages/%ss/[id]/index.tsx", - "pages/%ss/[id]/edit.tsx", - "pages/%ss/page/[page].tsx", - "pages/%ss/index.tsx", - "pages/%ss/create.tsx", + "app/%ss/[id]/page.tsx", + "app/%ss/[id]/edit/page.tsx", + "app/%ss/page/[page]/page.tsx", + "app/%ss/page.tsx", + "app/%ss/create/page.tsx", ].forEach((pattern) => this.createFileFromPattern(pattern, dir, [context.lc], context) ); @@ -107,7 +110,7 @@ export default class NextGenerator extends BaseGenerator { // copy with regular name [ // components - "components/common/Layout.tsx", + "components/common/Wrapper.tsx", "components/common/Pagination.tsx", "components/common/ReferenceLinks.tsx", @@ -116,7 +119,7 @@ export default class NextGenerator extends BaseGenerator { "types/item.ts", // pages - "pages/_app.tsx", + "app/layout.tsx", // utils "utils/dataAccess.ts", diff --git a/src/generators/NextGenerator.test.js b/src/generators/NextGenerator.test.js index 8b05ecf0..da29cb6e 100644 --- a/src/generators/NextGenerator.test.js +++ b/src/generators/NextGenerator.test.js @@ -48,18 +48,18 @@ describe("generate", () => { "/components/abc/PageList.tsx", "/components/abc/Show.tsx", "/components/abc/Form.tsx", - "/components/common/Layout.tsx", + "/components/common/Wrapper.tsx", "/components/common/ReferenceLinks.tsx", "/components/common/Pagination.tsx", "/types/Abc.ts", "/types/collection.ts", "/types/item.ts", - "/pages/abcs/[id]/index.tsx", - "/pages/abcs/[id]/edit.tsx", - "/pages/abcs/page/[page].tsx", - "/pages/abcs/index.tsx", - "/pages/abcs/create.tsx", - "/pages/_app.tsx", + "/app/abcs/[id]/index.tsx", + "/app/abcs/[id]/edit.tsx", + "/app/abcs/page/[page].tsx", + "/app/abcs/index.tsx", + "/app/abcs/create.tsx", + "/app/_app.tsx", "/utils/dataAccess.ts", "/utils/mercure.ts", ].forEach((file) => expect(fs.existsSync(tmpobj.name + file)).toBe(true)); diff --git a/src/index.js b/src/index.js old mode 100755 new mode 100644 diff --git a/templates/next/app/foos/[id]/edit/page.tsx b/templates/next/app/foos/[id]/edit/page.tsx new file mode 100644 index 00000000..07e16c6f --- /dev/null +++ b/templates/next/app/foos/[id]/edit/page.tsx @@ -0,0 +1,32 @@ +"use client"; + +import type { Metadata } from "next"; +import { useQuery } from "react-query"; + +import { Form } from "../../../../components/{{{lc}}}/Form"; +import { {{{ucf}}} } from "../../../../types/{{{ucf}}}"; +import { customFetch, FetchResponse } from "../../../../utils/dataAccess"; + + +const get{{{ucf}}} = async (id: string|string[]|undefined) => id ? await customFetch<{{{ucf}}}>(`/{{{name}}}/${id}`) : Promise.resolve(undefined); + +type Props = { + params: { id: string }; +}; + +export default function Page({ params }: Props) { + const { id } = params; + + const { data: { data: {{lc}} } = {} } = useQuery | undefined>(['{{{lc}}}', id], () => get{{{ucf}}}(id)); + + if (!{{{lc}}}) { + return null; + } + + return ( +
+ {`Edit {{{ucf}}} ${ {{{lc}}}["@id"]}`} +
+
+ ); +}; diff --git a/templates/next/app/foos/[id]/page.tsx b/templates/next/app/foos/[id]/page.tsx new file mode 100644 index 00000000..1b2a08b8 --- /dev/null +++ b/templates/next/app/foos/[id]/page.tsx @@ -0,0 +1,34 @@ +"use client"; + +import { useQuery } from "react-query"; + +import { Show } from "../../../components/{{{lc}}}/Show"; +import { {{{ucf}}} } from "../../../types/{{{ucf}}}"; +import { customFetch, FetchResponse } from "../../../utils/dataAccess"; +import { useMercure } from "../../../utils/mercure"; + +const get{{{ucf}}} = async (id: string|string[]|undefined) => id ? await customFetch<{{{ucf}}}>(`/{{{name}}}/${id}`) : Promise.resolve(undefined); + +type Props = { + params: { id: string }; +}; + +export default function Page({ params }: Props) { + const { id } = params; + + + const { data: { data: {{lc}}, hubURL } = { hubURL: null, text: '' } } = + useQuery | undefined>(['{{{lc}}}', id], () => get{{{ucf}}}(id)); + const {{{lc}}}Data = useMercure({{lc}}, hubURL); + + if (!{{{lc}}}Data) { + return null; + } + + return ( +
+ {`Show {{{ucf}}} ${ {{{lc}}}Data["@id"]}`} + +
+ ); +}; \ No newline at end of file diff --git a/templates/next/app/foos/create.tsx b/templates/next/app/foos/create.tsx new file mode 100644 index 00000000..be42218d --- /dev/null +++ b/templates/next/app/foos/create.tsx @@ -0,0 +1,17 @@ +import { NextComponentType, NextPageContext } from "next"; +import Head from "next/head"; + +import { Form } from "../../components/{{{lc}}}/Form"; + +const Page: NextComponentType = () => ( +
+
+ + Create {{{ucf}}} + +
+ +
+); + +export default Page; diff --git a/templates/next/app/foos/create/page.tsx b/templates/next/app/foos/create/page.tsx new file mode 100644 index 00000000..9cc9a5df --- /dev/null +++ b/templates/next/app/foos/create/page.tsx @@ -0,0 +1,14 @@ +import type { Metadata } from "next"; +import { Form } from "../../../components/{{{lc}}}/Form"; + +export const metadata: Metadata = { + title: "Create {{{ucf}}}", +}; + +export default function Page() { + return ( +
+ +
+ ); +} diff --git a/templates/next/app/foos/page.tsx b/templates/next/app/foos/page.tsx new file mode 100644 index 00000000..8cc1cb4d --- /dev/null +++ b/templates/next/app/foos/page.tsx @@ -0,0 +1,8 @@ +import type { Metadata } from "next"; +import PageList from "../../components/{{{lc}}}/PageList"; + +export const metadata: Metadata = { + title: "{{{ucf}}} List", +}; + +export default PageList; diff --git a/templates/next/app/foos/page/[page]/page.tsx b/templates/next/app/foos/page/[page]/page.tsx new file mode 100644 index 00000000..e90132fb --- /dev/null +++ b/templates/next/app/foos/page/[page]/page.tsx @@ -0,0 +1,3 @@ +import PageList from "../../../../components/{{{lc}}}/PageList"; + +export default PageList; diff --git a/templates/next/app/layout.tsx b/templates/next/app/layout.tsx new file mode 100644 index 00000000..ce907223 --- /dev/null +++ b/templates/next/app/layout.tsx @@ -0,0 +1,24 @@ +import React from "react"; +import type { Metadata } from "next"; +import Wrapper from "../components/common/Wrapper"; +import "../styles/style.css"; + +export const metadata: Metadata = { + title: "API Platform Create Client", + description: + "Create REST and GraphQL APIs, scaffold Jamstack webapps, stream changes in real-time.", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} diff --git a/templates/next/components/common/Wrapper.tsx b/templates/next/components/common/Wrapper.tsx new file mode 100644 index 00000000..b895f7df --- /dev/null +++ b/templates/next/components/common/Wrapper.tsx @@ -0,0 +1,14 @@ +"use client"; + +import { ReactNode, useState } from "react"; +import { QueryClient, QueryClientProvider } from "react-query"; + +const Wrapper = ({ children }: { children: ReactNode }) => { + const [queryClient] = useState(() => new QueryClient()); + + return ( + {children} + ); +}; + +export default Wrapper; \ No newline at end of file diff --git a/templates/next/components/foo/Form.tsx b/templates/next/components/foo/Form.tsx index 3612d618..449b77a9 100644 --- a/templates/next/components/foo/Form.tsx +++ b/templates/next/components/foo/Form.tsx @@ -1,10 +1,11 @@ import { FunctionComponent, useState } from "react"; import Link from "next/link"; -import { useRouter } from "next/router"; -import { ErrorMessage{{#if hasManyRelations}}, Field, FieldArray{{/if}}, Formik } from "formik"; +import { useRouter } from "next/navigation"; +import { useForm{{#if hasManyRelations}}, useFieldArray{{/if}} } from "react-hook-form"; + import { useMutation } from "react-query"; -import { fetch, FetchError, FetchResponse } from "../../utils/dataAccess"; +import { customFetch, FetchError, FetchResponse } from "../../utils/dataAccess"; import { {{{ucf}}} } from '../../types/{{{ucf}}}'; interface Props { @@ -20,17 +21,52 @@ interface DeleteParams { } const save{{{ucf}}} = async ({ values }: SaveParams) => - await fetch<{{ucf}}>(!values["@id"] ? "/{{{name}}}" : values["@id"], { + await customFetch<{{ucf}}>(!values["@id"] ? "/{{{name}}}" : values["@id"], { method: !values["@id"] ? "POST" : "PUT", body: JSON.stringify(values), }); -const delete{{{ucf}}} = async (id: string) => await fetch<{{ucf}}>(id, { method: "DELETE" }); +const delete{{{ucf}}} = async (id: string) => await customFetch<{{ucf}}>(id, { method: "DELETE" }); + + +const formatDefaultValue = ({{lc}} : {{{ucf}}} | undefined)=> { + return {{lc}} ? + { + ...{{lc}}, + {{#each fields}} + {{#if isEmbeddeds}} + {{name}}: {{../lc}}["{{name}}"]?.map((emb: any) => emb['@id']) ?? [], + {{else if embedded}} + {{name}}: {{../lc}}["{{name}}"]?.['@id'] ?? "", + {{/if}} + {{/each}} + } : + new {{{ucf}}}() +} + export const Form: FunctionComponent = ({ {{{lc}}} }) => { - const [, setError] = useState(null); + const [message, setMessage] = useState(null); const router = useRouter(); + const { + handleSubmit, + register, + setError, + control, + formState: { errors, isSubmitting, dirtyFields, isValid }, + } = useForm<{{{ucf}}}>({ + defaultValues: formatDefaultValue({{lc}}), + }); + + {{#each formFields}} + {{#if isRelations}} + const { fields: {{{name}}}, append: append{{{name}}}, remove: remove{{{name}}} } = useFieldArray({ + control, + name: "{{{name}}}", + }); + {{/if}} + {{/each}} const saveMutation = useMutation | undefined, Error|FetchError, SaveParams>((saveParams) => save{{{ucf}}}(saveParams)); const deleteMutation = useMutation | undefined, Error|FetchError, DeleteParams>(({ id }) => delete{{{ucf}}}(id), { @@ -38,17 +74,42 @@ export const Form: FunctionComponent = ({ {{{lc}}} }) => { router.push("/{{{lc}}}s"); }, onError: (error)=> { - setError(`Error when deleting the resource: ${error}`); + setMessage(`Error when deleting the resource: ${error}`); console.error(error); } }); const handleDelete = () => { + setMessage(null); if (!{{lc}} || !{{lc}}["@id"]) return; if (!window.confirm("Are you sure you want to delete this item?")) return; deleteMutation.mutate({ id: {{lc}}["@id"] }); }; + const onSubmit = async (values: {{{ucf}}}) => { + const isCreation = !values["@id"]; + setMessage(null); + + saveMutation.mutate( + { values }, + { + onSuccess: () => { + setMessage(`Element ${isCreation ? "created" : "updated"}.`); + router.push("/{{{name}}}"); + }, + onError: (error) => { + setMessage(error.message); + if ("fields" in error) { + error.fields.map( + ({ field, message }: { field: string; message: string }) => + setError(field, { type: "custom", message }) + ); + } + } + } + ); + } + return (
= ({ {{{lc}}} }) => {

{ {{{lc}}} ? `Edit {{{ucf}}} ${ {{~lc}}['@id']}` : `Create {{{ucf}}}` }

- emb['@id']) ?? [], - {{else if embedded}} - {{name}}: {{../lc}}["{{name}}"]?.['@id'] ?? "", - {{/if}} - {{/each}} - } : - new {{{ucf}}}() - } - validate={() => { - const errors = {}; - // add your validation logic here - return errors; - }} - onSubmit={(values, { setSubmitting, setStatus, setErrors }) => { - const isCreation = !values["@id"]; - saveMutation.mutate( - { values }, - { - onSuccess: () => { - setStatus({ - isValid: true, - msg: `Element ${isCreation ? "created" : "updated"}.`, - }); - router.push("/{{{name}}}"); - }, - onError: (error) => { - setStatus({ - isValid: false, - msg: `${error.message}`, - }); - if ("fields" in error) { - setErrors(error.fields); - } - }, - onSettled: ()=> { - setSubmitting(false); - } - } - ); - }} - > - {({ - values, - status, - errors, - touched, - handleChange, - handleBlur, - handleSubmit, - isSubmitting, - }) => ( - - {{#each formFields}} + + {{#each formFields}}
{{#if isRelations}}
{{name}}
- ( -
- {values.{{name}} && values.{{name}}.length > 0 ? ( - values.{{name}}.map((item: any, index: number) => ( -
- - - -
- )) - ) : ( - - )} -
- )} - /> +
    + { {{name}}.map((item, index) => ( +
  • + + +
  • + ))} +
+ {{else}} - + {errors?.{{name}} && ( +

{errors?.{{name}}?.message}

+ )} {{/if}}
{{/each}} - {status && status.msg && ( + {message && (
- {status.msg} + {message}
)} - - )} -
+
{ {{{lc}}} && (