From 001d7f3e00e7d24b8700c39726f340d8d69dfb31 Mon Sep 17 00:00:00 2001 From: Alan Poulain Date: Fri, 20 Jan 2023 11:13:32 +0100 Subject: [PATCH] fix(next): add static generation for the list (#359) --- src/generators/NextGenerator.js | 5 +++ src/generators/NextGenerator.test.js | 2 + .../next/components/common/Pagination.tsx | 12 +++--- templates/next/components/foo/List.tsx | 16 +++---- templates/next/components/foo/PageList.tsx | 36 ++++++++++++++++ templates/next/components/foo/Show.tsx | 12 +++--- templates/next/pages/foos/[id]/edit.tsx | 4 +- templates/next/pages/foos/[id]/index.tsx | 4 +- templates/next/pages/foos/index.tsx | 43 ++++--------------- templates/next/pages/foos/page/[page].tsx | 31 +++++++++++++ templates/next/utils/dataAccess.ts | 40 +++++++++++------ 11 files changed, 135 insertions(+), 70 deletions(-) create mode 100644 templates/next/components/foo/PageList.tsx create mode 100644 templates/next/pages/foos/page/[page].tsx diff --git a/src/generators/NextGenerator.js b/src/generators/NextGenerator.js index 802004c5..809f2c60 100644 --- a/src/generators/NextGenerator.js +++ b/src/generators/NextGenerator.js @@ -15,6 +15,7 @@ export default class NextGenerator extends BaseGenerator { "components/common/Pagination.tsx", "components/common/ReferenceLinks.tsx", "components/foo/List.tsx", + "components/foo/PageList.tsx", "components/foo/Show.tsx", "components/foo/Form.tsx", @@ -26,6 +27,7 @@ export default class NextGenerator extends BaseGenerator { // 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", @@ -81,15 +83,18 @@ export default class NextGenerator extends BaseGenerator { 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`); [ // components "components/%s/List.tsx", + "components/%s/PageList.tsx", "components/%s/Show.tsx", "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", ].forEach((pattern) => diff --git a/src/generators/NextGenerator.test.js b/src/generators/NextGenerator.test.js index 6a6ae52d..8b05ecf0 100644 --- a/src/generators/NextGenerator.test.js +++ b/src/generators/NextGenerator.test.js @@ -45,6 +45,7 @@ describe("generate", () => { [ "/config/entrypoint.ts", "/components/abc/List.tsx", + "/components/abc/PageList.tsx", "/components/abc/Show.tsx", "/components/abc/Form.tsx", "/components/common/Layout.tsx", @@ -55,6 +56,7 @@ describe("generate", () => { "/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", diff --git a/templates/next/components/common/Pagination.tsx b/templates/next/components/common/Pagination.tsx index 8fa7ffd5..8a52758e 100644 --- a/templates/next/components/common/Pagination.tsx +++ b/templates/next/components/common/Pagination.tsx @@ -3,9 +3,11 @@ import { PagedCollection } from "../../types/collection"; interface Props { collection: PagedCollection; + // eslint-disable-next-line no-unused-vars + getPagePath: (path: string) => string; } -const Pagination = ({ collection }: Props) => { +const Pagination = ({ collection, getPagePath }: Props) => { const view = collection && collection['{{{hydraPrefix}}}view']; if (!view) return null; @@ -23,7 +25,7 @@ const Pagination = ({ collection }: Props) => { aria-label="Page navigation" > { First { Previous { Next = ({ {{{lc}}}s }) => ( {{{lc}}}['@id'] && - + {{#each fields}} {{#if isReferences}} - ({ href: getPath(ref, '/{{{lowercase reference.title}}}s/[id]'), name: ref })) } /> + ({ href: getItemPath(ref, '/{{{lowercase reference.title}}}s/[id]'), name: ref })) } /> {{else if reference}} - + {{else if isEmbeddeds}} - ({ href: getPath(emb['@id'], '/{{{lowercase embedded.title}}}s/[id]'), name: emb['@id'] })) } /> + ({ href: getItemPath(emb['@id'], '/{{{lowercase embedded.title}}}s/[id]'), name: emb['@id'] })) } /> {{else if embedded}} - + {{else if (compare type "==" "Date") }} { {{{../lc}}}['{{{name}}}']?.toLocaleString() } {{else}} @@ -56,7 +56,7 @@ export const List: FunctionComponent = ({ {{{lc}}}s }) => ( {{/each}} Show @@ -68,7 +68,7 @@ export const List: FunctionComponent = ({ {{{lc}}}s }) => ( Edit diff --git a/templates/next/components/foo/PageList.tsx b/templates/next/components/foo/PageList.tsx new file mode 100644 index 00000000..6c89b089 --- /dev/null +++ b/templates/next/components/foo/PageList.tsx @@ -0,0 +1,36 @@ +import { NextComponentType, NextPageContext } from "next"; +import { useRouter } from "next/router"; +import Head from "next/head"; +import { useQuery } from "react-query"; + +import Pagination from "../common/Pagination"; +import { List } from "./List"; +import { PagedCollection } from "../../types/collection"; +import { {{{ucf}}} } from "../../types/{{{ucf}}}"; +import { fetch, FetchResponse, parsePage } from "../../utils/dataAccess"; +import { useMercure } from "../../utils/mercure"; + +export const get{{{ucf}}}sPath = (page?: string | string[] | undefined) => `/{{{name}}}${typeof page === 'string' ? `?page=${page}` : ''}`; +export const get{{{ucf}}}s = (page?: string | string[] | undefined) => async () => await fetch>(get{{{ucf}}}sPath(page)); +const getPagePath = (path: string) => `/{{{lc}}}s/page/${parsePage("{{{name}}}", path)}`; + +export const PageList: NextComponentType = () => { + const { query: { page } } = useRouter(); + const { data: { data: {{lc}}s, hubURL } = { hubURL: null } } = + useQuery> | undefined>(get{{{ucf}}}sPath(page), get{{{ucf}}}s(page)); + const collection = useMercure({{lc}}s, hubURL); + + if (!collection || !collection["{{{hydraPrefix}}}member"]) return null; + + return ( +
+
+ + {{{ucf}}} List + +
+ + +
+ ); +}; diff --git a/templates/next/components/foo/Show.tsx b/templates/next/components/foo/Show.tsx index 49f1afd1..ea06a6e6 100644 --- a/templates/next/components/foo/Show.tsx +++ b/templates/next/components/foo/Show.tsx @@ -4,7 +4,7 @@ import { useRouter } from "next/router"; import Head from "next/head"; {{#if hasRelations}}import ReferenceLinks from "../common/ReferenceLinks";{{/if}} -import { fetch, getPath } from "../../utils/dataAccess"; +import { fetch, getItemPath } from "../../utils/dataAccess"; import { {{{ucf}}} } from "../../types/{{{ucf}}}"; interface Props { @@ -55,13 +55,13 @@ export const Show: FunctionComponent = ({ {{{lc}}}, text }) => { {{name}} {{#if isReferences}} - ({ href: getPath(ref, '/{{{lowercase reference.title}}}s/[id]'), name: ref })) } /> + ({ href: getItemPath(ref, '/{{{lowercase reference.title}}}s/[id]'), name: ref })) } /> {{else if reference}} - + {{else if isEmbeddeds}} - ({ href: getPath(emb['@id'], '/{{{lowercase embedded.title}}}s/[id]'), name: emb['@id'] })) } /> + ({ href: getItemPath(emb['@id'], '/{{{lowercase embedded.title}}}s/[id]'), name: emb['@id'] })) } /> {{else if embedded}} - + {{else if (compare type "==" "Date") }} { {{{../lc}}}['{{{name}}}']?.toLocaleString() } {{else}} @@ -79,7 +79,7 @@ export const Show: FunctionComponent = ({ {{{lc}}}, text }) => { )}
Edit diff --git a/templates/next/pages/foos/[id]/edit.tsx b/templates/next/pages/foos/[id]/edit.tsx index d009d9e6..59b650ac 100644 --- a/templates/next/pages/foos/[id]/edit.tsx +++ b/templates/next/pages/foos/[id]/edit.tsx @@ -7,7 +7,7 @@ import { dehydrate, QueryClient, useQuery } from "react-query"; import { Form } from "../../../components/{{{lc}}}/Form"; import { PagedCollection } from "../../../types/collection"; import { {{{ucf}}} } from "../../../types/{{{ucf}}}"; -import { fetch, FetchResponse, getPaths } from "../../../utils/dataAccess"; +import { fetch, FetchResponse, getItemPaths } from "../../../utils/dataAccess"; const get{{{ucf}}} = async (id: string|string[]|undefined) => id ? await fetch<{{{ucf}}}>(`/{{{name}}}/${id}`) : Promise.resolve(undefined); @@ -48,7 +48,7 @@ export const getStaticProps: GetStaticProps = async ({ params: { id } = {} }) => export const getStaticPaths: GetStaticPaths = async () => { const response = await fetch>("/{{{name}}}"); - const paths = await getPaths(response, "{{{name}}}", '/{{{lc}}}s/[id]/edit'); + const paths = await getItemPaths(response, "{{{name}}}", '/{{{lc}}}s/[id]/edit'); return { paths, diff --git a/templates/next/pages/foos/[id]/index.tsx b/templates/next/pages/foos/[id]/index.tsx index e768570f..2d837925 100644 --- a/templates/next/pages/foos/[id]/index.tsx +++ b/templates/next/pages/foos/[id]/index.tsx @@ -7,7 +7,7 @@ import { dehydrate, QueryClient, useQuery } from "react-query"; import { Show } from "../../../components/{{{lc}}}/Show"; import { PagedCollection } from "../../../types/collection"; import { {{{ucf}}} } from "../../../types/{{{ucf}}}"; -import { fetch, FetchResponse, getPaths } from "../../../utils/dataAccess"; +import { fetch, FetchResponse, getItemPaths } from "../../../utils/dataAccess"; import { useMercure } from "../../../utils/mercure"; const get{{{ucf}}} = async (id: string|string[]|undefined) => id ? await fetch<{{{ucf}}}>(`/{{{name}}}/${id}`) : Promise.resolve(undefined); @@ -51,7 +51,7 @@ export const getStaticProps: GetStaticProps = async ({ params: { id } = {} }) => export const getStaticPaths: GetStaticPaths = async () => { const response = await fetch>("/{{{name}}}"); - const paths = await getPaths(response, "{{{name}}}", '/{{{lc}}}s/[id]'); + const paths = await getItemPaths(response, "{{{name}}}", '/{{{lc}}}s/[id]'); return { paths, diff --git a/templates/next/pages/foos/index.tsx b/templates/next/pages/foos/index.tsx index 27449425..6a48ec8e 100644 --- a/templates/next/pages/foos/index.tsx +++ b/templates/next/pages/foos/index.tsx @@ -1,45 +1,18 @@ -import { GetServerSideProps, NextComponentType, NextPageContext } from "next"; -import Head from "next/head"; -import { dehydrate, QueryClient, useQuery } from "react-query"; +import { GetStaticProps } from "next"; +import { dehydrate, QueryClient } from "react-query"; -import Pagination from "../../components/common/Pagination"; -import { List } from "../../components/{{{lc}}}/List"; -import { PagedCollection } from "../../types/collection"; -import { {{{ucf}}} } from "../../types/{{{ucf}}}"; -import { fetch, FetchResponse } from "../../utils/dataAccess"; -import { useMercure } from "../../utils/mercure"; +import { PageList, get{{{ucf}}}s, get{{{ucf}}}sPath } from "../../components/{{{lc}}}/PageList"; -const get{{{ucf}}}s = async () => await fetch>('/{{{name}}}'); - -const Page: NextComponentType = () => { - const { data: { data: {{lc}}s, hubURL } = { hubURL: null } } = - useQuery> | undefined>('{{{name}}}', get{{{ucf}}}s); - const collection = useMercure({{lc}}s, hubURL); - - if (!collection || !collection["{{{hydraPrefix}}}member"]) return null; - - return ( -
-
- - {{{ucf}}} List - -
- - -
- ); -}; - -export const getServerSideProps: GetServerSideProps = async () => { +export const getStaticProps: GetStaticProps = async () => { const queryClient = new QueryClient(); - await queryClient.prefetchQuery('{{{name}}}', get{{{ucf}}}s); + await queryClient.prefetchQuery(get{{{ucf}}}sPath(), get{{{ucf}}}s()); return { props: { dehydratedState: dehydrate(queryClient), }, + revalidate: 1, }; -} +}; -export default Page; +export default PageList; diff --git a/templates/next/pages/foos/page/[page].tsx b/templates/next/pages/foos/page/[page].tsx new file mode 100644 index 00000000..bc3f0251 --- /dev/null +++ b/templates/next/pages/foos/page/[page].tsx @@ -0,0 +1,31 @@ +import { GetStaticPaths, GetStaticProps } from "next"; +import { dehydrate, QueryClient } from "react-query"; + +import { PageList, get{{{ucf}}}s, get{{{ucf}}}sPath } from "../../../components/{{{lc}}}/PageList"; +import { PagedCollection } from "../../../types/collection"; +import { {{{ucf}}} } from "../../../types/{{{ucf}}}"; +import { fetch, getCollectionPaths } from "../../../utils/dataAccess"; + +export const getStaticProps: GetStaticProps = async ({ params: { page } = {} }) => { + const queryClient = new QueryClient(); + await queryClient.prefetchQuery(get{{{ucf}}}sPath(page), get{{{ucf}}}s(page)); + + return { + props: { + dehydratedState: dehydrate(queryClient), + }, + revalidate: 1, + }; +}; + +export const getStaticPaths: GetStaticPaths = async () => { + const response = await fetch>("/{{{name}}}"); + const paths = await getCollectionPaths(response, "{{{name}}}", "/{{{lc}}}s/page/[page]"); + + return { + paths, + fallback: true, + }; +}; + +export default PageList; diff --git a/templates/next/utils/dataAccess.ts b/templates/next/utils/dataAccess.ts index a1770892..6e9b7f32 100644 --- a/templates/next/utils/dataAccess.ts +++ b/templates/next/utils/dataAccess.ts @@ -70,7 +70,7 @@ export const fetch = async (id: string, init: RequestInit = {}): Promise< throw { message: errorMessage, status, fields } as FetchError; }; -export const getPath = (iri: string | undefined, pathTemplate: string): string => { +export const getItemPath = (iri: string | undefined, pathTemplate: string): string => { if (!iri) { return ''; } @@ -80,21 +80,22 @@ export const getPath = (iri: string | undefined, pathTemplate: string): string = return pathTemplate.replace('[id]', resourceId); } -export const getPaths = async (response: FetchResponse> | undefined, resourceName: string, pathTemplate: string) => { +export const parsePage = (resourceName: string, path: string) => parseInt(new RegExp(`^/${resourceName}\\?page=(\\d+)`).exec(path)?.[1] ?? '1', 10); + +export const getItemPaths = async (response: FetchResponse> | undefined, resourceName: string, pathTemplate: string) => { if (!response) return []; try { const view = response.data["{{{hydraPrefix}}}view"]; - const paths = response.data["{{{hydraPrefix}}}member"]?.map((resourceData) => getPath(resourceData['@id'] ?? '', pathTemplate)) || []; - - const { "hydra:last": last } = view || {}; - if (last) { - for (let page = 2; page <= parseInt(last.replace(new RegExp(`^\/${resourceName}\?page=(\d+)`), "$1")); page++) { - paths.concat( - (await fetch>(`/${resourceName}?page=${page}`)) - ?.data["{{{hydraPrefix}}}member"]?.map((resourceData) => getPath(resourceData['@id'] ?? '', pathTemplate)) || [] - ); - } + const { "{{{hydraPrefix}}}last": last } = view ?? {}; + const paths = response.data["{{{hydraPrefix}}}member"]?.map((resourceData) => getItemPath(resourceData['@id'] ?? '', pathTemplate)) ?? []; + const lastPage = parsePage(resourceName, last ?? ''); + + for (let page = 2; page <= lastPage; page++) { + paths.push( + ...(await fetch>(`/${resourceName}?page=${page}`)) + ?.data["{{{hydraPrefix}}}member"]?.map((resourceData) => getItemPath(resourceData['@id'] ?? '', pathTemplate)) ?? [] + ); } return paths; @@ -104,3 +105,18 @@ export const getPaths = async (response: FetchResponse(response: FetchResponse> | undefined, resourceName: string, pathTemplate: string) => { + if (!response) return []; + + const view = response.data["{{{hydraPrefix}}}view"]; + const { "{{{hydraPrefix}}}last": last } = view ?? {}; + const paths = [pathTemplate.replace('[page]', '1')]; + const lastPage = parsePage(resourceName, last ?? ''); + + for (let page = 2; page <= lastPage; page++) { + paths.push(pathTemplate.replace('[page]', page.toString())); + } + + return paths; +};