From 195dc17c094efaa7fb5d2810d133c356ae528cc7 Mon Sep 17 00:00:00 2001 From: Victor Zeinstra Date: Fri, 6 Oct 2023 09:09:39 +0200 Subject: [PATCH] =?UTF-8?q?feat:1007=20admin=20liste=20des=20contenus=20mo?= =?UTF-8?q?difi=C3=A9s=20lors=20dune=20maj=20des=20donn=C3=A9es=20-=20page?= =?UTF-8?q?=20info=20(#1013)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: implementation du schéma de donnée page info * feat: implementation des pages info * fix: review * refactor: implement page info refactor * feat: implementation page creation * feat: add upsert + publish * feat: publish * feat: publish + refactor to module * chore: remove npmrc * fix: publish dep * feat: page info validation (#1040) * feat: zod validation page info * feat: implementation page info validation avec zod * feat: clean deepPartial * chore: clean --------- Co-authored-by: Victor Zeinstra * chore: clean * chore: clean * chore: use helper text for error * feat: change url to regex check for files * chore: add test * chore: review * chore: yarn * refactor: use confirm modal * chore: downgrade next * fix: breadcrumb display * chore: clean --------- Co-authored-by: Victor Zeinstra --- shared/graphql-client/src/index.ts | 1 + targets/frontend/package.json | 6 +- .../src/components/forms/Checkbox/index.tsx | 32 +++ .../src/components/forms/RadioGroup/index.tsx | 45 ++-- .../src/components/forms/TextField/index.tsx | 34 +-- .../src/components/layout/Navigation.tsx | 2 +- .../src/lib/__tests__/mutationUtils.test.ts | 34 +++ targets/frontend/src/lib/api/ApiClient.ts | 74 ++++++ targets/frontend/src/lib/api/ApiErrors.ts | 29 ++ targets/frontend/src/lib/api/index.ts | 2 + targets/frontend/src/lib/apiError.js | 9 - targets/frontend/src/lib/apiError.ts | 15 ++ targets/frontend/src/lib/mutationUtils.ts | 26 ++ .../documents/api/documents.controller.ts | 81 ++++++ .../documents/api/documents.mutation.ts | 21 ++ .../documents/api/documents.repository.ts | 29 ++ .../documents/api/documents.service.ts | 114 ++++++++ .../src/modules/documents/api/index.ts | 3 + .../frontend/src/modules/documents/index.ts | 2 + .../frontend/src/modules/documents/type.ts | 21 ++ .../src/modules/informations/api/index.ts | 1 + .../informations/api/informations.query.ts | 70 +++++ .../api/informations.repository.ts | 32 +++ .../modules/informations/components/index.ts | 2 + .../informationsEdit/Informations.query.ts | 129 +++++++++ .../informationsEdit/InformationsBlock.tsx | 91 +++++++ .../InformationsBlockContent.tsx | 56 ++++ .../InformationsBlockGraphic.tsx | 77 ++++++ .../InformationsBlockText.tsx | 37 +++ .../informationsEdit/InformationsContent.tsx | 194 ++++++++++++++ .../informationsEdit/InformationsCreate.tsx | 74 ++++++ .../informationsEdit/InformationsEdit.tsx | 128 +++++++++ .../informationsEdit/InformationsForm.tsx | 248 +++++++++++++++++ .../InformationsReference.tsx | 78 ++++++ .../deleteInformation.mutation.ts | 30 +++ .../editInformation.mapping.ts | 139 ++++++++++ .../editInformation.mutation.ts | 115 ++++++++ .../informationsEdit/editInformation.type.ts | 62 +++++ .../components/informationsEdit/index.ts | 2 + .../publishInformation.mutation.ts | 28 ++ .../InformationsList.query.ts | 51 ++++ .../informationsList/InformationsList.tsx | 68 +++++ .../informationsList/InformationsRow.tsx | 28 ++ .../components/informationsList/index.ts | 1 + .../src/modules/informations/index.ts | 3 + .../frontend/src/modules/informations/type.ts | 92 +++++++ .../frontend/src/pages/api/actions/publish.ts | 12 + .../frontend/src/pages/informations/[id].tsx | 18 ++ .../src/pages/informations/creation.tsx | 14 + .../frontend/src/pages/informations/index.tsx | 15 ++ targets/frontend/src/types/common.ts | 5 + targets/hasura/metadata/actions.graphql | 11 + targets/hasura/metadata/actions.yaml | 9 + .../tables/information_informations.yaml | 130 +++++++++ .../information_informations_contents.yaml | 67 +++++ ...ormation_informations_contents_blocks.yaml | 79 ++++++ ...informations_contents_blocks_contents.yaml | 50 ++++ ...tion_informations_contents_references.yaml | 49 ++++ .../information_informations_references.yaml | 49 ++++ .../default/tables/public_files.yaml | 43 +++ .../databases/default/tables/tables.yaml | 7 + .../down.sql | 1 + .../up.sql | 1 + .../down.sql | 3 + .../up.sql | 18 ++ .../down.sql | 1 + .../up.sql | 13 + .../down.sql | 1 + .../up.sql | 9 + .../down.sql | 1 + .../up.sql | 17 ++ .../down.sql | 2 + .../up.sql | 13 + .../down.sql | 1 + .../up.sql | 13 + .../down.sql | 1 + .../up.sql | 12 + .../1694012383773_migrate_info_page/down.sql | 1 + .../1694012383773_migrate_info_page/up.sql | 251 ++++++++++++++++++ yarn.lock | 203 +++++++------- 80 files changed, 3394 insertions(+), 142 deletions(-) create mode 100644 targets/frontend/src/components/forms/Checkbox/index.tsx create mode 100644 targets/frontend/src/lib/__tests__/mutationUtils.test.ts create mode 100644 targets/frontend/src/lib/api/ApiClient.ts create mode 100644 targets/frontend/src/lib/api/ApiErrors.ts create mode 100644 targets/frontend/src/lib/api/index.ts delete mode 100644 targets/frontend/src/lib/apiError.js create mode 100644 targets/frontend/src/lib/apiError.ts create mode 100644 targets/frontend/src/lib/mutationUtils.ts create mode 100644 targets/frontend/src/modules/documents/api/documents.controller.ts create mode 100644 targets/frontend/src/modules/documents/api/documents.mutation.ts create mode 100644 targets/frontend/src/modules/documents/api/documents.repository.ts create mode 100644 targets/frontend/src/modules/documents/api/documents.service.ts create mode 100644 targets/frontend/src/modules/documents/api/index.ts create mode 100644 targets/frontend/src/modules/documents/index.ts create mode 100644 targets/frontend/src/modules/documents/type.ts create mode 100644 targets/frontend/src/modules/informations/api/index.ts create mode 100644 targets/frontend/src/modules/informations/api/informations.query.ts create mode 100644 targets/frontend/src/modules/informations/api/informations.repository.ts create mode 100644 targets/frontend/src/modules/informations/components/index.ts create mode 100644 targets/frontend/src/modules/informations/components/informationsEdit/Informations.query.ts create mode 100644 targets/frontend/src/modules/informations/components/informationsEdit/InformationsBlock.tsx create mode 100644 targets/frontend/src/modules/informations/components/informationsEdit/InformationsBlockContent.tsx create mode 100644 targets/frontend/src/modules/informations/components/informationsEdit/InformationsBlockGraphic.tsx create mode 100644 targets/frontend/src/modules/informations/components/informationsEdit/InformationsBlockText.tsx create mode 100644 targets/frontend/src/modules/informations/components/informationsEdit/InformationsContent.tsx create mode 100644 targets/frontend/src/modules/informations/components/informationsEdit/InformationsCreate.tsx create mode 100644 targets/frontend/src/modules/informations/components/informationsEdit/InformationsEdit.tsx create mode 100644 targets/frontend/src/modules/informations/components/informationsEdit/InformationsForm.tsx create mode 100644 targets/frontend/src/modules/informations/components/informationsEdit/InformationsReference.tsx create mode 100644 targets/frontend/src/modules/informations/components/informationsEdit/deleteInformation.mutation.ts create mode 100644 targets/frontend/src/modules/informations/components/informationsEdit/editInformation.mapping.ts create mode 100644 targets/frontend/src/modules/informations/components/informationsEdit/editInformation.mutation.ts create mode 100644 targets/frontend/src/modules/informations/components/informationsEdit/editInformation.type.ts create mode 100644 targets/frontend/src/modules/informations/components/informationsEdit/index.ts create mode 100644 targets/frontend/src/modules/informations/components/informationsEdit/publishInformation.mutation.ts create mode 100644 targets/frontend/src/modules/informations/components/informationsList/InformationsList.query.ts create mode 100644 targets/frontend/src/modules/informations/components/informationsList/InformationsList.tsx create mode 100644 targets/frontend/src/modules/informations/components/informationsList/InformationsRow.tsx create mode 100644 targets/frontend/src/modules/informations/components/informationsList/index.ts create mode 100644 targets/frontend/src/modules/informations/index.ts create mode 100644 targets/frontend/src/modules/informations/type.ts create mode 100644 targets/frontend/src/pages/api/actions/publish.ts create mode 100644 targets/frontend/src/pages/informations/[id].tsx create mode 100644 targets/frontend/src/pages/informations/creation.tsx create mode 100644 targets/frontend/src/pages/informations/index.tsx create mode 100644 targets/frontend/src/types/common.ts create mode 100644 targets/hasura/metadata/databases/default/tables/information_informations.yaml create mode 100644 targets/hasura/metadata/databases/default/tables/information_informations_contents.yaml create mode 100644 targets/hasura/metadata/databases/default/tables/information_informations_contents_blocks.yaml create mode 100644 targets/hasura/metadata/databases/default/tables/information_informations_contents_blocks_contents.yaml create mode 100644 targets/hasura/metadata/databases/default/tables/information_informations_contents_references.yaml create mode 100644 targets/hasura/metadata/databases/default/tables/information_informations_references.yaml create mode 100644 targets/hasura/metadata/databases/default/tables/public_files.yaml create mode 100644 targets/hasura/migrations/default/1693835658115_create_schema_information/down.sql create mode 100644 targets/hasura/migrations/default/1693835658115_create_schema_information/up.sql create mode 100644 targets/hasura/migrations/default/1693836035778_create_table_information_informations/down.sql create mode 100644 targets/hasura/migrations/default/1693836035778_create_table_information_informations/up.sql create mode 100644 targets/hasura/migrations/default/1693836242023_create_table_information_informations_contents/down.sql create mode 100644 targets/hasura/migrations/default/1693836242023_create_table_information_informations_contents/up.sql create mode 100644 targets/hasura/migrations/default/1693837558650_create_table_public_files/down.sql create mode 100644 targets/hasura/migrations/default/1693837558650_create_table_public_files/up.sql create mode 100644 targets/hasura/migrations/default/1693837558654_create_table_information_informations_contents_blocks/down.sql create mode 100644 targets/hasura/migrations/default/1693837558654_create_table_information_informations_contents_blocks/up.sql create mode 100644 targets/hasura/migrations/default/1693908863392_create_table_information_informations_contents_references/down.sql create mode 100644 targets/hasura/migrations/default/1693908863392_create_table_information_informations_contents_references/up.sql create mode 100644 targets/hasura/migrations/default/1693912613427_create_table_information_informations_references/down.sql create mode 100644 targets/hasura/migrations/default/1693912613427_create_table_information_informations_references/up.sql create mode 100644 targets/hasura/migrations/default/1693991664253_create_table_information_informations_contents_blocks_contents/down.sql create mode 100644 targets/hasura/migrations/default/1693991664253_create_table_information_informations_contents_blocks_contents/up.sql create mode 100644 targets/hasura/migrations/default/1694012383773_migrate_info_page/down.sql create mode 100644 targets/hasura/migrations/default/1694012383773_migrate_info_page/up.sql diff --git a/shared/graphql-client/src/index.ts b/shared/graphql-client/src/index.ts index cf358541d..d26f54c14 100644 --- a/shared/graphql-client/src/index.ts +++ b/shared/graphql-client/src/index.ts @@ -14,6 +14,7 @@ export const client = createClient({ "x-hasura-admin-secret": HASURA_GRAPHQL_ADMIN_SECRET, }, }, + maskTypename: true, requestPolicy: "network-only", url: HASURA_GRAPHQL_ENDPOINT, }); diff --git a/targets/frontend/package.json b/targets/frontend/package.json index c4849832c..079eb6313 100644 --- a/targets/frontend/package.json +++ b/targets/frontend/package.json @@ -10,6 +10,7 @@ "@emotion/styled": "11.10.6", "@hapi/boom": "^9.1.4", "@hookform/error-message": "^2.0.0", + "@hookform/resolvers": "^3.3.1", "@mui/icons-material": "5.11.16", "@mui/material": "5.14.11", "@reach/accordion": "^0.16.1", @@ -49,7 +50,7 @@ "jsonwebtoken": "^8.5.1", "memoizee": "^0.4.15", "micromark": "^2.11.4", - "next": "13.5.3", + "next": "13.2.4", "next-urql": "^3.2.1", "nodemailer": "^6.6.5", "p-limit": "^4.0.0", @@ -75,7 +76,8 @@ "unified": "^9.2.2", "urql": "^2.0.5", "uuid": "^8.3.2", - "zod": "^3.22.2" + "wonka": "^4.0.15", + "zod": "3.21.4" }, "devDependencies": { "@shared/types": "workspace:^", diff --git a/targets/frontend/src/components/forms/Checkbox/index.tsx b/targets/frontend/src/components/forms/Checkbox/index.tsx new file mode 100644 index 000000000..790c0e169 --- /dev/null +++ b/targets/frontend/src/components/forms/Checkbox/index.tsx @@ -0,0 +1,32 @@ +import { FormGroup, FormControlLabel, Checkbox } from "@mui/material"; +import React, { PropsWithChildren } from "react"; +import { Controller } from "react-hook-form"; +import { CommonFormProps } from "../type"; + +export type FormCheckboxProps = PropsWithChildren; +export const FormCheckbox = ({ + name, + rules, + label, + control, + disabled, +}: FormCheckboxProps) => { + return ( + { + return ( + + } + label={label} + disabled={disabled} + /> + + ); + }} + /> + ); +}; diff --git a/targets/frontend/src/components/forms/RadioGroup/index.tsx b/targets/frontend/src/components/forms/RadioGroup/index.tsx index a106a1ffd..0ce5a688e 100644 --- a/targets/frontend/src/components/forms/RadioGroup/index.tsx +++ b/targets/frontend/src/components/forms/RadioGroup/index.tsx @@ -34,28 +34,29 @@ export const FormRadioGroup = ({ name={name} control={control} rules={rules} - render={({ field: { onChange, value }, fieldState: { error } }) => ( - - {label} - - ) => - onChange(event.target.value) - } - > - {options.map(({ label, value }) => ( - } - label={label} - disabled={disabled} - /> - ))} - - - )} + render={({ field: { onChange, value }, fieldState: { error } }) => { + return ( + + {label} + ) => + onChange(event.target.value) + } + > + {options.map(({ label, value }) => ( + } + label={label} + disabled={disabled} + /> + ))} + + + ); + }} /> ); }; diff --git a/targets/frontend/src/components/forms/TextField/index.tsx b/targets/frontend/src/components/forms/TextField/index.tsx index 61d26e5d9..dc5d623ea 100644 --- a/targets/frontend/src/components/forms/TextField/index.tsx +++ b/targets/frontend/src/components/forms/TextField/index.tsx @@ -32,23 +32,23 @@ export const FormTextField = ({ name={name} control={control} rules={rules} - render={({ field: { onChange, value }, fieldState: { error } }) => ( - - )} + render={({ field: { onChange, value }, fieldState: { error } }) => { + return ( + + ); + }} /> ); }; diff --git a/targets/frontend/src/components/layout/Navigation.tsx b/targets/frontend/src/components/layout/Navigation.tsx index c9a861800..41a7b1c14 100644 --- a/targets/frontend/src/components/layout/Navigation.tsx +++ b/targets/frontend/src/components/layout/Navigation.tsx @@ -37,7 +37,7 @@ export function Navigation() { label: "Contenus", }, { - href: "/contenus?source=information", + href: "/informations", label: "Contenus éditoriaux", }, { diff --git a/targets/frontend/src/lib/__tests__/mutationUtils.test.ts b/targets/frontend/src/lib/__tests__/mutationUtils.test.ts new file mode 100644 index 000000000..6a7872f71 --- /dev/null +++ b/targets/frontend/src/lib/__tests__/mutationUtils.test.ts @@ -0,0 +1,34 @@ +import { getElementsToDelete } from "../mutationUtils"; + +describe("Fonction utilitaire getElementsToDelete", () => { + it("doit remonter pour un champs donné, la liste d'élément différent entre les 2 objets", () => { + const result = getElementsToDelete( + { + list: [ + { + id: 1, + }, + { + id: 2, + }, + { + id: 3, + }, + ], + }, + { + list: [ + { + id: 1, + }, + { + id: 3, + }, + ], + }, + ["list", "id"] + ); + expect(result.length).toEqual(1); + expect(result[0]).toEqual(2); + }); +}); diff --git a/targets/frontend/src/lib/api/ApiClient.ts b/targets/frontend/src/lib/api/ApiClient.ts new file mode 100644 index 000000000..ab62a1bc6 --- /dev/null +++ b/targets/frontend/src/lib/api/ApiClient.ts @@ -0,0 +1,74 @@ +import { Client } from "urql"; +import { DocumentNode } from "graphql/index"; +import { TypedDocumentNode } from "@graphql-typed-document-node/core"; +import { OperationContext, OperationResult } from "@urql/core/dist/types/types"; +import { client as gqlClient } from "@shared/graphql-client"; + +export class ApiClient { + client: Client; + hasuraGraphqlAdminSecret: string; + sessionVariables?: any; + + constructor(client: Client, sessionVariables?: any) { + this.client = client; + this.hasuraGraphqlAdminSecret = + process.env.HASURA_GRAPHQL_ADMIN_SECRET ?? "admin1"; + this.sessionVariables = sessionVariables; + } + + public static build(sessionVariables: any = undefined): ApiClient { + return new ApiClient(gqlClient, sessionVariables); + } + + async query( + query: DocumentNode | TypedDocumentNode | string, + variables?: Variables, + context?: Partial + ): Promise> { + let headers = context?.headers; + if (this.sessionVariables) { + headers = { + ...headers, + ...this.sessionVariables, + "x-hasura-admin-secret": this.hasuraGraphqlAdminSecret, + }; + } + const result = await this.client + .query(query, variables, { + ...context, + fetchOptions: () => ({ + ...context?.fetchOptions, + headers, + }), + }) + .toPromise(); + + return result; + } + + async mutation( + query: DocumentNode | TypedDocumentNode | string, + variables?: Variables, + context?: Partial + ): Promise> { + let headers = context?.headers; + if (this.sessionVariables) { + headers = { + ...headers, + ...this.sessionVariables, + "x-hasura-admin-secret": this.hasuraGraphqlAdminSecret, + }; + } + const result = await this.client + .mutation(query, variables, { + ...context, + fetchOptions: () => ({ + ...context?.fetchOptions, + headers, + }), + }) + .toPromise(); + + return result; + } +} diff --git a/targets/frontend/src/lib/api/ApiErrors.ts b/targets/frontend/src/lib/api/ApiErrors.ts new file mode 100644 index 000000000..17bbe2ac0 --- /dev/null +++ b/targets/frontend/src/lib/api/ApiErrors.ts @@ -0,0 +1,29 @@ +interface ErrorWithCause { + name: T; + message: string; + cause: any; +} + +export class ErrorBase extends Error { + name: T; + message: string; + cause: any; + + constructor(error: ErrorWithCause) { + super(); + this.name = error.name; + this.message = error.message; + this.cause = error.cause; + } +} + +export class NotFoundError extends ErrorBase<"NOT_FOUND"> {} + +export const DEFAULT_ERROR_500_MESSAGE = + "Internal server error during fetching data"; + +export class InvalidQueryError extends ErrorBase<"INVALID_QUERY"> { + constructor(message: string, cause: any) { + super({ name: "INVALID_QUERY", message, cause }); + } +} diff --git a/targets/frontend/src/lib/api/index.ts b/targets/frontend/src/lib/api/index.ts new file mode 100644 index 000000000..8cdcb5b7c --- /dev/null +++ b/targets/frontend/src/lib/api/index.ts @@ -0,0 +1,2 @@ +export * from "./ApiClient"; +export * from "./ApiErrors"; diff --git a/targets/frontend/src/lib/apiError.js b/targets/frontend/src/lib/apiError.js deleted file mode 100644 index 16dcd224e..000000000 --- a/targets/frontend/src/lib/apiError.js +++ /dev/null @@ -1,9 +0,0 @@ -export function createErrorFor(res) { - return function toError({ output: { statusCode, payload } }) { - res.status(statusCode).json(payload); - }; -} - -export function serverError(res, { output: { statusCode, payload } }) { - return res.status(statusCode).json(payload); -} diff --git a/targets/frontend/src/lib/apiError.ts b/targets/frontend/src/lib/apiError.ts new file mode 100644 index 000000000..c1315cf48 --- /dev/null +++ b/targets/frontend/src/lib/apiError.ts @@ -0,0 +1,15 @@ +import { NextApiResponse } from "next"; +import { Boom } from "@hapi/boom"; + +export function createErrorFor(res: NextApiResponse) { + return function toError({ output: { statusCode, payload } }: Boom) { + res.status(statusCode).json(payload); + }; +} + +export function serverError( + res: NextApiResponse, + { output: { statusCode, payload } }: Boom +) { + return res.status(statusCode).json(payload); +} diff --git a/targets/frontend/src/lib/mutationUtils.ts b/targets/frontend/src/lib/mutationUtils.ts new file mode 100644 index 000000000..eb863f239 --- /dev/null +++ b/targets/frontend/src/lib/mutationUtils.ts @@ -0,0 +1,26 @@ +const getElementsByPath = (obj: any, path: string[]): string[] => { + const clonedKeys = [...path]; + const key = clonedKeys.shift(); + if (!key) return []; + const objToParse = obj[key]; + if (!clonedKeys.length && objToParse && typeof objToParse !== "object") { + return [objToParse]; + } else if (Array.isArray(objToParse)) { + return objToParse.reduce((arr, item) => { + return arr.concat(getElementsByPath(item, clonedKeys)); + }, []); + } else if (typeof objToParse === "object") { + return getElementsByPath(objToParse, clonedKeys); + } + return []; +}; + +export const getElementsToDelete = ( + oldObj: any, + newObj: any, + keys: string[] +) => { + const oldIds = getElementsByPath(oldObj, keys); + const newIds = getElementsByPath(newObj, keys); + return oldIds.filter((el) => newIds.indexOf(el) === -1); +}; diff --git a/targets/frontend/src/modules/documents/api/documents.controller.ts b/targets/frontend/src/modules/documents/api/documents.controller.ts new file mode 100644 index 000000000..fa2ed0aab --- /dev/null +++ b/targets/frontend/src/modules/documents/api/documents.controller.ts @@ -0,0 +1,81 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { z } from "zod"; + +import { + ApiClient, + DEFAULT_ERROR_500_MESSAGE, + InvalidQueryError, + NotFoundError, +} from "src/lib/api"; +import { DocumentsRepository, DocumentsService } from "."; +import { InformationsRepository } from "../../informations/api"; + +const inputSchema = z.object({ + id: z.string().uuid(), + source: z.string(), +}); + +const actionSchema = z.object({ + action: z.object({ + name: z.string(), + }), + input: inputSchema, + session_variables: z.object({ + "x-hasura-user-id": z.string().uuid().optional(), + "x-hasura-role": z.string().optional(), + }), +}); + +export class DocumentsController { + private readonly req: NextApiRequest; + private readonly res: NextApiResponse; + + constructor(req: NextApiRequest, res: NextApiResponse) { + this.req = req; + this.res = res; + } + + public async publish() { + try { + const inputs = this.checkInputs(); + + if (inputs) { + const client = ApiClient.build(inputs.session_variables); + const service = new DocumentsService( + new InformationsRepository(client), + new DocumentsRepository(client) + ); + const cdtnId = await service.publish( + inputs.input.id, + inputs.input.source + ); + this.res.status(201).json({ cdtnId }); + } + } catch (error) { + if (error instanceof NotFoundError) { + this.res.status(404).json({ message: error.message }); + } else { + if (error instanceof InvalidQueryError) { + this.res.status(400).json({ message: error.message }); + } else { + this.res.status(400).json({ + message: DEFAULT_ERROR_500_MESSAGE, + }); + } + } + } + } + + checkInputs(): z.infer { + const inputResult = actionSchema.safeParse(this.req.body); + + if (!inputResult.success) { + throw new InvalidQueryError( + inputResult.error.message, + inputResult.error.errors + ); + } + + return inputResult.data; + } +} diff --git a/targets/frontend/src/modules/documents/api/documents.mutation.ts b/targets/frontend/src/modules/documents/api/documents.mutation.ts new file mode 100644 index 000000000..147e74b9e --- /dev/null +++ b/targets/frontend/src/modules/documents/api/documents.mutation.ts @@ -0,0 +1,21 @@ +export const documentsPublishMutation = ` + mutation publish_document($upsert: documents_insert_input!) { + insert_documents_one( + object: $upsert, + on_conflict: { + constraint: documents_pkey + update_columns: [ + initial_id + document + text + title + meta_description + slug + source + ] + } + ) { + cdtn_id + } + } +`; diff --git a/targets/frontend/src/modules/documents/api/documents.repository.ts b/targets/frontend/src/modules/documents/api/documents.repository.ts new file mode 100644 index 000000000..7691a4b8d --- /dev/null +++ b/targets/frontend/src/modules/documents/api/documents.repository.ts @@ -0,0 +1,29 @@ +import { ApiClient } from "src/lib/api"; +import { documentsPublishMutation } from "./documents.mutation"; +import { DocumentRaw } from "../type"; + +export class DocumentsRepository { + client: ApiClient; + + constructor(client: ApiClient) { + this.client = client; + } + + async update(document: DocumentRaw): Promise { + try { + const { data, error } = await this.client.mutation< + any, + { upsert: DocumentRaw } + >(documentsPublishMutation, { upsert: document }); + if (error) { + console.log("Error: ", error); + throw error; + } + const { cdtn_id: cdtnId } = data.insert_documents_one; + return cdtnId; + } catch (error) { + console.log("Error: ", error); + throw error; + } + } +} diff --git a/targets/frontend/src/modules/documents/api/documents.service.ts b/targets/frontend/src/modules/documents/api/documents.service.ts new file mode 100644 index 000000000..6ec79b3f0 --- /dev/null +++ b/targets/frontend/src/modules/documents/api/documents.service.ts @@ -0,0 +1,114 @@ +import { DocumentsRepository } from "./documents.repository"; +import { NotFoundError } from "src/lib/api/ApiErrors"; +import { Information, InformationsRepository } from "src/modules/informations"; +import { DocumentRaw } from "../type"; +import { format } from "date-fns"; +import { generateIds } from "@shared/id-generator"; +import slugify from "@socialgouv/cdtn-slugify"; + +export class DocumentsService { + private readonly informationsRepository: InformationsRepository; + private readonly documentsRepository: DocumentsRepository; + + constructor( + informationsRepository: InformationsRepository, + documentsRepository: DocumentsRepository + ) { + this.informationsRepository = informationsRepository; + this.documentsRepository = documentsRepository; + } + + private mapInformationToDocument(data: Information): DocumentRaw { + return { + ...generateIds(data.title), + source: "information", + meta_description: data.metaDescription ?? data.description, + title: data.title, + text: data.title, + slug: slugify(data.title), + document: { + date: data.updatedAt + ? format(new Date(data.updatedAt), "dd/MM/yyyy") + : undefined, + intro: data.intro, + description: data.description, + sectionDisplayMode: data.sectionDisplayMode, + dismissalProcess: data.dismissalProcess, + references: data.references.length + ? [ + { + label: data.referenceLabel, + links: data.references, + }, + ] + : undefined, + contents: data.contents.map( + ({ name, title, blocks, references, referenceLabel }) => { + return { + name, + title, + blocks: blocks.map( + ({ + file, + img, + type, + content, + contentDisplayMode, + contents, + }) => { + return { + size: file?.size, + type, + imgUrl: img?.url, + fileUrl: file?.url, + markdown: content, + blockDisplayMode: contentDisplayMode, + contents: contents?.length + ? contents.map(({ document }) => { + return { + title: document.title, + cdtnId: document.cdtnId, + source: document.source, + }; + }) + : undefined, + }; + } + ), + references: references?.length + ? [ + { + label: referenceLabel, + links: references, + }, + ] + : undefined, + }; + } + ), + }, + }; + } + + public async publish(id: string, source: string) { + let document: DocumentRaw | undefined; + switch (source) { + case "information": + default: + const information = await this.informationsRepository.fetchInformation( + id + ); + if (!information) { + throw new NotFoundError({ + message: `data not found with id ${id}`, + name: "NOT_FOUND", + cause: null, + }); + } + document = this.mapInformationToDocument(information); + break; + } + const result = await this.documentsRepository.update(document); + return result; + } +} diff --git a/targets/frontend/src/modules/documents/api/index.ts b/targets/frontend/src/modules/documents/api/index.ts new file mode 100644 index 000000000..17621a1ef --- /dev/null +++ b/targets/frontend/src/modules/documents/api/index.ts @@ -0,0 +1,3 @@ +export * from "./documents.repository"; +export * from "./documents.service"; +export * from "./documents.controller"; diff --git a/targets/frontend/src/modules/documents/index.ts b/targets/frontend/src/modules/documents/index.ts new file mode 100644 index 000000000..7ac274e08 --- /dev/null +++ b/targets/frontend/src/modules/documents/index.ts @@ -0,0 +1,2 @@ +export * from "./api"; +export * from "./type"; diff --git a/targets/frontend/src/modules/documents/type.ts b/targets/frontend/src/modules/documents/type.ts new file mode 100644 index 000000000..94a4e8403 --- /dev/null +++ b/targets/frontend/src/modules/documents/type.ts @@ -0,0 +1,21 @@ +export type Document = { + cdtnId: string; + initialId: string; + source: string; + document: any; + slug: string; + text: string; + title: string; + metaDescription: string; +}; + +export type DocumentRaw = { + cdtn_id: string; + initial_id: string; + source: string; + document: any; + slug: string; + text: string; + title: string; + meta_description: string; +}; diff --git a/targets/frontend/src/modules/informations/api/index.ts b/targets/frontend/src/modules/informations/api/index.ts new file mode 100644 index 000000000..5f5228269 --- /dev/null +++ b/targets/frontend/src/modules/informations/api/index.ts @@ -0,0 +1 @@ +export * from "./informations.repository"; diff --git a/targets/frontend/src/modules/informations/api/informations.query.ts b/targets/frontend/src/modules/informations/api/informations.query.ts new file mode 100644 index 000000000..8ed166ce5 --- /dev/null +++ b/targets/frontend/src/modules/informations/api/informations.query.ts @@ -0,0 +1,70 @@ +import { gql } from "@urql/core"; +import { Information } from "../type"; + +export const informationsQuery = gql` + query informations($id: uuid) { + information_informations(where: { id: { _eq: $id } }) { + cdtnId + description + id + intro + metaDescription + metaTitle + referenceLabel + sectionDisplayMode + title + updatedAt + dismissalProcess + contents(order_by: { order: asc }) { + id + name + title + referenceLabel + order + blocks(order_by: { order: asc }) { + id + content + order + type + file { + id + url + altText + size + } + contents(order_by: { order: asc }) { + id + document { + cdtnId: cdtn_id + source + title + slug + } + } + } + references(order_by: { order: asc }) { + id + url + type + title + order + } + } + references(order_by: { order: asc }) { + id + url + type + title + order + } + } + } +`; + +export type InformationsRequest = { + id: string; +}; + +export type InformationsResponse = { + information_informations: Information[]; +}; diff --git a/targets/frontend/src/modules/informations/api/informations.repository.ts b/targets/frontend/src/modules/informations/api/informations.repository.ts new file mode 100644 index 000000000..a668926e5 --- /dev/null +++ b/targets/frontend/src/modules/informations/api/informations.repository.ts @@ -0,0 +1,32 @@ +import { + informationsQuery, + InformationsRequest, + InformationsResponse, +} from "./informations.query"; +import { ApiClient } from "src/lib/api"; + +export class InformationsRepository { + client: ApiClient; + + constructor(client: ApiClient) { + this.client = client; + } + + async fetchInformation(id: string) { + const { error, data } = await this.client.query< + InformationsResponse, + InformationsRequest + >(informationsQuery, { + id, + }); + if (error) { + console.log("Error: ", error); + throw error; + } + if (!data || data.information_informations.length === 0) { + throw new Error(`Pas de page information pour l'id ${id}`); + } + const information = data.information_informations[0]; + return information; + } +} diff --git a/targets/frontend/src/modules/informations/components/index.ts b/targets/frontend/src/modules/informations/components/index.ts new file mode 100644 index 000000000..66633c156 --- /dev/null +++ b/targets/frontend/src/modules/informations/components/index.ts @@ -0,0 +1,2 @@ +export * from "./informationsList"; +export * from "./informationsEdit"; diff --git a/targets/frontend/src/modules/informations/components/informationsEdit/Informations.query.ts b/targets/frontend/src/modules/informations/components/informationsEdit/Informations.query.ts new file mode 100644 index 000000000..b72effc8b --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsEdit/Informations.query.ts @@ -0,0 +1,129 @@ +import { CombinedError, useQuery } from "urql"; +import { format, parseISO } from "date-fns"; + +import { Information } from "../../type"; + +const informationsQuery = `query informations($id: uuid) { + information_informations( + where: { + id: { _eq: $id } + } + ) { + cdtnId + description + id + intro + metaDescription + metaTitle + referenceLabel + sectionDisplayMode + title + updatedAt + dismissalProcess + contents( + order_by: {order: asc} + ) { + id + name + title + referenceLabel + order + blocks( + order_by: {order: asc} + ) { + id + content + order + type + file { + id + url + altText + size + } + img { + id + url + altText + size + } + contentDisplayMode + contents( + order_by: {order: asc} + ) { + id + document { + cdtnId: cdtn_id + source + title + slug + } + } + } + references( + order_by: {order: asc} + ) { + id + url + type + title + order + } + } + references( + order_by: {order: asc} + ) { + id + url + type + title + order + } + } + }`; + +export type QueryInformation = Information; + +export type QueryResult = { + information_informations: QueryInformation[]; +}; + +export type InformationsQueryProps = { + id?: string; +}; + +export type InformationsResult = Information & { + updateDate: string; +}; + +export type InformationsQueryResult = { + data?: InformationsResult; + error?: CombinedError; + fetching: boolean; +}; + +export const useInformationsQuery = ({ + id, +}: InformationsQueryProps): InformationsQueryResult => { + const [{ data, error, fetching }] = useQuery({ + query: informationsQuery, + requestPolicy: "cache-and-network", + variables: { + id, + }, + }); + const information = data?.information_informations[0]; + const updateDate = information?.updatedAt + ? format(parseISO(information.updatedAt), "dd/MM/yyyy") + : ""; + return { + data: information + ? { + ...information, + updateDate, + } + : undefined, + error, + fetching, + }; +}; diff --git a/targets/frontend/src/modules/informations/components/informationsEdit/InformationsBlock.tsx b/targets/frontend/src/modules/informations/components/informationsEdit/InformationsBlock.tsx new file mode 100644 index 000000000..6072c7844 --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsEdit/InformationsBlock.tsx @@ -0,0 +1,91 @@ +import { Stack, IconButton } from "@mui/material"; +import React from "react"; +import { FormRadioGroup } from "src/components/forms"; +import { Control, useWatch } from "react-hook-form"; +import { styled } from "@mui/system"; +import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; +import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; +import DeleteIcon from "@mui/icons-material/Delete"; + +import { InformationsBlockText } from "./InformationsBlockText"; +import { InformationsBlockGraphic } from "./InformationsBlockGraphic"; +import { InformationsBlockContent } from "./InformationsBlockContent"; + +export type InformationBlockProps = { + name: string; + control: Control; + first: boolean; + last: boolean; + onDown: () => void; + onUp: () => void; + onDelete: () => void; +}; + +export const InformationsBlock = ({ + name: blockName, + control, + first, + last, + onDown, + onUp, + onDelete, +}: InformationBlockProps): JSX.Element => { + const blockType = useWatch({ name: `${blockName}.type`, control }); + const RenderBlock = (name: string, type?: string) => { + switch (type) { + case "graphic": + return ; + case "content": + return ; + default: + return ; + } + }; + + return ( + <> + + + + + + + + + + + + + + + {blockType && ( + + )} + {RenderBlock(blockName, blockType)} + + + ); +}; + +const StyledBlock = styled(Stack)` + border: 1px solid; + padding: 12px; +`; diff --git a/targets/frontend/src/modules/informations/components/informationsEdit/InformationsBlockContent.tsx b/targets/frontend/src/modules/informations/components/informationsEdit/InformationsBlockContent.tsx new file mode 100644 index 000000000..13436bd5b --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsEdit/InformationsBlockContent.tsx @@ -0,0 +1,56 @@ +import { Stack, FormControl } from "@mui/material"; +import React from "react"; +import { FormRadioGroup, FormTextField } from "src/components/forms"; +import { Control } from "react-hook-form"; +import { CdtnReferenceInput } from "src/components/contributions/answers/references"; + +export type InformationBlockContentProps = { + name: string; + control: Control; +}; + +export const InformationsBlockContent = ({ + name, + control, +}: InformationBlockContentProps): JSX.Element => { + return ( + <> + + + + + + + + + ); +}; diff --git a/targets/frontend/src/modules/informations/components/informationsEdit/InformationsBlockGraphic.tsx b/targets/frontend/src/modules/informations/components/informationsEdit/InformationsBlockGraphic.tsx new file mode 100644 index 000000000..5659499a9 --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsEdit/InformationsBlockGraphic.tsx @@ -0,0 +1,77 @@ +import { Stack, FormControl } from "@mui/material"; +import React from "react"; +import { FormTextField } from "src/components/forms"; +import { Control } from "react-hook-form"; + +export type InformationBlockGraphicProps = { + name: string; + control: Control; +}; + +export const InformationsBlockGraphic = ({ + name, + control, +}: InformationBlockGraphicProps): JSX.Element => { + return ( + <> + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/targets/frontend/src/modules/informations/components/informationsEdit/InformationsBlockText.tsx b/targets/frontend/src/modules/informations/components/informationsEdit/InformationsBlockText.tsx new file mode 100644 index 000000000..a10b48fb0 --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsEdit/InformationsBlockText.tsx @@ -0,0 +1,37 @@ +import { Stack, FormControl } from "@mui/material"; +import React from "react"; +import { FormTextField } from "src/components/forms"; +import { Control } from "react-hook-form"; + +export type InformationBlockTextProps = { + name: string; + control: Control; +}; + +export const InformationsBlockText = ({ + name, + control, +}: InformationBlockTextProps): JSX.Element => { + return ( + <> + + + + + + + ); +}; diff --git a/targets/frontend/src/modules/informations/components/informationsEdit/InformationsContent.tsx b/targets/frontend/src/modules/informations/components/informationsEdit/InformationsContent.tsx new file mode 100644 index 000000000..354b9a585 --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsEdit/InformationsContent.tsx @@ -0,0 +1,194 @@ +import { + Stack, + FormControl, + Accordion, + AccordionDetails, + AccordionSummary, + IconButton, + Button, + Typography, +} from "@mui/material"; +import { styled } from "@mui/system"; +import React from "react"; +import ExpandMoreIcon from "@mui/icons-material/ExpandMore"; +import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; +import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; +import DeleteIcon from "@mui/icons-material/Delete"; +import { Control, useFieldArray, useWatch } from "react-hook-form"; + +import { Information } from "../../type"; +import { FormRadioGroup, FormTextField } from "src/components/forms"; +import { InformationsBlock } from "./InformationsBlock"; +import { InformationsReference } from "./InformationsReference"; + +export type InformationBlockProps = { + name: `contents.${number}`; + control: Control; + expanded: boolean; + expand: () => void; + first: boolean; + last: boolean; + onDown: () => void; + onUp: () => void; + onDelete: () => void; +}; + +export const InformationsContent = ({ + name, + control, + expanded, + expand, + first, + last, + onDown, + onUp, + onDelete, +}: InformationBlockProps): JSX.Element => { + const title = useWatch({ name: `${name}.title`, control }); + const { + fields: blocks, + swap: swapBlock, + remove: removeBlock, + append: appendBlock, + } = useFieldArray({ + control, + name: `${name}.blocks`, + }); + const { + fields: references, + swap: swapReference, + remove: removeReference, + append: appendReference, + } = useFieldArray({ + control, + name: `${name}.references`, + }); + + return ( + <> + + + + + } + aria-controls={name} + > + +
{title}
+ + + + + + + + + + + +
+
+ + + + + + Blocs + {blocks.map(({ id }, index) => ( + swapBlock(index, index + 1)} + onUp={() => swapBlock(index, index - 1)} + onDelete={() => { + removeBlock(index); + }} + > + ))} + + References + {!!references.length && ( + + )} + {references.map(({ id }, index) => ( + swapReference(index, index + 1)} + onUp={() => swapReference(index, index - 1)} + onDelete={() => removeReference(index)} + > + ))} + + + +
+ + ); +}; + +const StyledAccordion = styled(Accordion)` + cursor: default; + div { + cursor: default; + } + .MuiAccordionSummary-expandIconWrapper { + cursor: pointer; + } +`; diff --git a/targets/frontend/src/modules/informations/components/informationsEdit/InformationsCreate.tsx b/targets/frontend/src/modules/informations/components/informationsEdit/InformationsCreate.tsx new file mode 100644 index 000000000..d455f7f90 --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsEdit/InformationsCreate.tsx @@ -0,0 +1,74 @@ +import { AlertColor, Stack } from "@mui/material"; +import React, { useState } from "react"; +import { BreadcrumbLink } from "src/components/utils"; +import { + setDefaultData, + useEditInformationMutation, +} from "./editInformation.mutation"; +import { InformationsForm } from "./InformationsForm"; +import { useRouter } from "next/router"; +import { SnackBar } from "src/components/utils/SnackBar"; + +export const InformationsCreate = (): JSX.Element => { + const router = useRouter(); + const onUpsert = useEditInformationMutation(); + const [snack, setSnack] = useState<{ + open: boolean; + severity?: AlertColor; + message?: string; + }>({ + open: false, + }); + setDefaultData({ + title: "", + updatedAt: "", + dismissalProcess: false, + description: "", + metaDescription: "", + metaTitle: "", + references: [], + contents: [], + }); + + const Header = () => ( +
    + Informations + creation +
+ ); + + return ( + <> + +
+ + { + try { + await onUpsert(data); + await router.push(`/informations`); + setSnack({ + open: true, + severity: "success", + message: "La page information a été crée", + }); + } catch (e: any) { + setSnack({ + open: true, + severity: "error", + message: e.message, + }); + } + }} + > + + + + + ); +}; diff --git a/targets/frontend/src/modules/informations/components/informationsEdit/InformationsEdit.tsx b/targets/frontend/src/modules/informations/components/informationsEdit/InformationsEdit.tsx new file mode 100644 index 000000000..9f46b6870 --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsEdit/InformationsEdit.tsx @@ -0,0 +1,128 @@ +import { AlertColor, Skeleton, Stack } from "@mui/material"; +import React, { useEffect, useState } from "react"; +import { Breadcrumb, BreadcrumbLink } from "src/components/utils"; + +import { useInformationsQuery } from "./Informations.query"; +import { + setDefaultData, + useEditInformationMutation, +} from "./editInformation.mutation"; +import { InformationsForm } from "./InformationsForm"; +import { useDeleteInformationMutation } from "./deleteInformation.mutation"; +import { usePublishInformationMutation } from "./publishInformation.mutation"; +import { useRouter } from "next/router"; +import { SnackBar } from "src/components/utils/SnackBar"; +import { ConfirmModal } from "src/modules/common/components/modals/ConfirmModal"; + +export type EditInformationProps = { + id?: string; +}; + +export const InformationsEdit = ({ id }: EditInformationProps): JSX.Element => { + const { data: information, fetching } = useInformationsQuery({ id }); + const router = useRouter(); + + const [snack, setSnack] = useState<{ + open: boolean; + severity?: AlertColor; + message?: string; + }>({ + open: false, + }); + const [modalDelete, setModalDelete] = useState(false); + const onUpsert = useEditInformationMutation(); + const onDelete = useDeleteInformationMutation(); + const onPublish = usePublishInformationMutation(); + + useEffect(() => { + if (!fetching && information) { + setDefaultData(information); + } + }, [fetching]); + + if (!information) { + return ( + <> + + + ); + } + + const Header = () => ( + + Informations + {information?.title} + + ); + + return ( + <> + +
+ + {!fetching && ( + { + setModalDelete(true); + }} + onUpsert={async (upsertData) => { + try { + const idUpsert = await onUpsert(upsertData); + await router.push(`/informations/${idUpsert}`); + setSnack({ + open: true, + severity: "success", + message: "La page information a été modifiée", + }); + } catch (e: any) { + setSnack({ + open: true, + severity: "error", + message: e.message, + }); + } + }} + onPublish={async () => { + try { + if (information?.id) { + await onPublish(information.id); + setSnack({ + open: true, + severity: "success", + message: "La page information a été publiée", + }); + } + } catch (e: any) { + setSnack({ + open: true, + severity: "error", + message: e.message, + }); + } + }} + > + )} + + + setModalDelete(false)} + onCancel={() => setModalDelete(false)} + onValidate={async () => { + if (!information?.id) return; + await onDelete(information?.id); + router.push("/informations"); + }} + /> + + + ); +}; diff --git a/targets/frontend/src/modules/informations/components/informationsEdit/InformationsForm.tsx b/targets/frontend/src/modules/informations/components/informationsEdit/InformationsForm.tsx new file mode 100644 index 000000000..dd683c36c --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsEdit/InformationsForm.tsx @@ -0,0 +1,248 @@ +import { Stack, Button, FormControl, Typography } from "@mui/material"; +import React from "react"; +import { useForm, useFieldArray } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { FormTextField, FormRadioGroup } from "src/components/forms"; + +import { InformationsResult } from "./Informations.query"; +import { Information, informationSchema } from "../../type"; +import { InformationsContent } from "./InformationsContent"; +import { InformationsReference } from "./InformationsReference"; +import { FormCheckbox } from "src/components/forms/Checkbox"; + +export type InformationsFormProps = { + data?: InformationsResult; + onUpsert: (props: Information) => Promise; + onDelete?: () => Promise; + onPublish?: () => Promise; +}; + +export const InformationsForm = ({ + data, + onUpsert, + onDelete, + onPublish, +}: InformationsFormProps): JSX.Element => { + const { control, handleSubmit, trigger } = useForm({ + defaultValues: data ?? { title: "", dismissalProcess: false }, + resolver: zodResolver(informationSchema), + shouldFocusError: true, + }); + + const { + fields: contents, + swap: swapContent, + remove: removeContent, + append: appendContent, + } = useFieldArray({ + control, + name: "contents", + }); + const { + fields: references, + swap: swapReference, + remove: removeReference, + append: appendReference, + } = useFieldArray({ + control, + name: "references", + }); + + const [expandedContent, setExpandedContent] = React.useState( + false + ); + + const onSubmit = async (information: Information) => { + const isValid = await trigger(); + if (isValid) { + onUpsert(information); + } + }; + + return ( + <> +
+ + + + + + + + + + + + + + + + + + + + + Contenus + {!!contents.length && ( + + )} + {contents.map(({ id }, index) => { + return ( + + setExpandedContent(expandedContent !== id ? id : false) + } + name={`contents.${index}`} + first={index === 0} + last={index === contents.length - 1} + onDown={() => swapContent(index, index + 1)} + onUp={() => swapContent(index, index - 1)} + onDelete={() => removeContent(index)} + /> + ); + })} + + References + {!!references.length && ( + + )} + {references.map(({ id }, index) => { + return ( + swapReference(index, index + 1)} + onUp={() => swapReference(index, index - 1)} + onDelete={() => removeReference(index)} + /> + ); + })} + + + + + + + +
+ + ); +}; diff --git a/targets/frontend/src/modules/informations/components/informationsEdit/InformationsReference.tsx b/targets/frontend/src/modules/informations/components/informationsEdit/InformationsReference.tsx new file mode 100644 index 000000000..36540babb --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsEdit/InformationsReference.tsx @@ -0,0 +1,78 @@ +import { Stack, IconButton, FormControl } from "@mui/material"; +import React from "react"; +import { FormTextField } from "src/components/forms"; +import { Control } from "react-hook-form"; +import { styled } from "@mui/system"; +import ArrowUpwardIcon from "@mui/icons-material/ArrowUpward"; +import ArrowDownwardIcon from "@mui/icons-material/ArrowDownward"; +import DeleteIcon from "@mui/icons-material/Delete"; + +export type InformationsReferenceProps = { + name: string; + control: Control; + first: boolean; + last: boolean; + onDown: () => void; + onUp: () => void; + onDelete: () => void; +}; + +export const InformationsReference = ({ + name, + control, + first, + last, + onDown, + onUp, + onDelete, +}: InformationsReferenceProps): JSX.Element => { + return ( + <> + + + + + + + + + + + + + + + + + + + + + + + ); +}; + +const StyledReference = styled(Stack)` + border: 1px solid; + padding: 12px; +`; diff --git a/targets/frontend/src/modules/informations/components/informationsEdit/deleteInformation.mutation.ts b/targets/frontend/src/modules/informations/components/informationsEdit/deleteInformation.mutation.ts new file mode 100644 index 000000000..4ac0400f4 --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsEdit/deleteInformation.mutation.ts @@ -0,0 +1,30 @@ +import { OperationResult, useMutation } from "urql"; + +export const deleteInformationMutation = ` +mutation delete_information( + $id: uuid! +) { + delete_information_informations ( + where: {id: {_eq: $id}} + ) { + affectedRows: affected_rows + } +} +`; + +export type DeleteInformationMutationResult = ( + id: string +) => Promise; + +export const useDeleteInformationMutation = + (): DeleteInformationMutationResult => { + const [, execute] = useMutation(deleteInformationMutation); + const resultFunction = async (id: string) => { + const result = await execute({ id }); + if (result.error) { + throw new Error(result.error.message); + } + return result; + }; + return resultFunction; + }; diff --git a/targets/frontend/src/modules/informations/components/informationsEdit/editInformation.mapping.ts b/targets/frontend/src/modules/informations/components/informationsEdit/editInformation.mapping.ts new file mode 100644 index 000000000..cd1be6b20 --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsEdit/editInformation.mapping.ts @@ -0,0 +1,139 @@ +import { + Information, + InformationContent, + InformationContentBlock, + InformationContentBlockContent, + Reference, + File, +} from "../../type"; +import { UpsertInformationObject } from "./editInformation.type"; + +const removeTypename = (obj: any) => { + delete obj?.__typename; + delete obj?.updateDate; + return obj; +}; + +const getRawColumns = (obj?: any): string[] => { + if (!obj) return []; + return Object.entries(obj).reduce((result, [key, value]) => { + if (key === "__typename" || typeof value === "object") return result; + return [...result, key]; + }, []); +}; + +const mapInformationContentsBlocksFile = (file?: File | null) => { + if (!file) return; + return { + on_conflict: { + constraint: "files_pkey", + update_columns: getRawColumns(file), + }, + data: { ...removeTypename(file) }, + }; +}; + +const mapInformationContentsBlocksContents = ( + contents?: InformationContentBlockContent[] | null +) => { + return { + on_conflict: { + constraint: + "informations_contents_blocks__informations_contents_blocks__key", + update_columns: [...getRawColumns(contents?.[0]), "order"], + }, + data: + contents?.map((contentBlock, contentBlockIndex) => { + return { + ...removeTypename(contentBlock), + document: undefined, + cdtnId: contentBlock.document.cdtnId, + order: contentBlockIndex + 1, + }; + }) ?? [], + }; +}; + +const mapInformationContentsBlocks = ( + blocks: InformationContentBlock[] | null +) => { + return { + on_conflict: { + constraint: "informations_contents_blocks_pkey", + update_columns: [...getRawColumns(blocks?.[0]), "order"], + }, + data: + blocks?.map((block, blockIndex) => { + const file = mapInformationContentsBlocksFile(block.file); + const img = mapInformationContentsBlocksFile(block.img); + const contents = mapInformationContentsBlocksContents(block.contents); + return { + ...removeTypename(block), + file, + img, + contents, + order: blockIndex + 1, + }; + }) ?? [], + }; +}; + +const mapInformationContentsReferences = (references?: Reference[] | null) => { + return { + on_conflict: { + constraint: "informations_contents_references_pkey", + update_columns: [...getRawColumns(references?.[0]), "order"], + }, + data: + references?.map((reference, referenceIndex) => ({ + ...removeTypename(reference), + order: referenceIndex + 1, + })) ?? [], + }; +}; + +const mapInformationContents = (contents?: InformationContent[]) => { + return { + on_conflict: { + constraint: "informations_contents_pkey", + update_columns: [...getRawColumns(contents?.[0]), "order"], + }, + data: contents?.map((content, contentIndex) => { + const blocks = mapInformationContentsBlocks(content.blocks); + const references = mapInformationContentsReferences(content.references); + return { + ...removeTypename(content), + order: contentIndex + 1, + name: content.title, + blocks, + references, + }; + }), + }; +}; + +const mapInformationReferences = (references?: Reference[]) => { + return { + on_conflict: { + constraint: "informations_references_pkey", + update_columns: [...getRawColumns(references?.[0]), "order"], + }, + data: + references?.map((reference, referenceIndex) => ({ + ...removeTypename(reference), + order: referenceIndex + 1, + })) ?? [], + }; +}; + +export const mapInformation = ( + information: Information +): UpsertInformationObject => { + const contents = mapInformationContents(information.contents); + const references = mapInformationReferences(information.references); + return { + ...removeTypename(information), + contents, + references, + }; +}; diff --git a/targets/frontend/src/modules/informations/components/informationsEdit/editInformation.mutation.ts b/targets/frontend/src/modules/informations/components/informationsEdit/editInformation.mutation.ts new file mode 100644 index 000000000..f3a9fc486 --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsEdit/editInformation.mutation.ts @@ -0,0 +1,115 @@ +import { useMutation } from "urql"; + +import { Information } from "../../type"; + +import { mapInformation } from "./editInformation.mapping"; +import { getElementsToDelete } from "src/lib/mutationUtils"; + +export const informationMutation = ` +mutation edit_information( + $upsert: information_informations_insert_input!, + $contentIdsToDelete: [uuid!], + $referenceIdsToDelete: [uuid!], + $contentBlockIdsToDelete: [uuid!], + $contentBlockContentIdsToDelete: [uuid!], + $contentReferenceIdsToDelete: [uuid!] +) { + insert_information_informations_one( + object: $upsert, + on_conflict: { + constraint: informations_pkey, + update_columns: [cdtnId,description,intro, metaTitle, metaDescription,referenceLabel,sectionDisplayMode] + } + ) { + id + } + delete_information_informations_references ( + where: {id: {_in: $referenceIdsToDelete}} + ) { + affectedRows: affected_rows + } + delete_information_informations_contents ( + where: {id: {_in: $contentIdsToDelete}} + ) { + affectedRows: affected_rows + } + delete_information_informations_contents_references ( + where: {id: {_in: $contentReferenceIdsToDelete}} + ) { + affectedRows: affected_rows + } + delete_information_informations_contents_blocks ( + where: {id: {_in: $contentBlockIdsToDelete}} + ) { + affectedRows: affected_rows + } + delete_information_informations_contents_blocks_contents ( + where: {id: {_in: $contentBlockContentIdsToDelete}} + ) { + affectedRows: affected_rows + } +} +`; + +export type EditInformationMutationResult = { + insert_information_informations_one: { id?: string }; +}; + +export type EditInformationMutationExecute = ( + props: Information +) => Promise; + +export const useEditInformationMutation = + (): EditInformationMutationExecute => { + const [, executeUpdate] = + useMutation(informationMutation); + const resultFunction = async ( + information: Information + ): Promise => { + const upsert = mapInformation(information); + const contentIdsToDelete = getElementsToDelete( + defaultInformation, + information, + ["contents", "id"] + ); + const referenceIdsToDelete = getElementsToDelete( + defaultInformation, + information, + ["references", "id"] + ); + const contentBlockIdsToDelete = getElementsToDelete( + defaultInformation, + information, + ["contents", "blocks", "id"] + ); + const contentBlockContentIdsToDelete = getElementsToDelete( + defaultInformation, + information, + ["contents", "blocks", "contents", "id"] + ); + const contentReferenceIdsToDelete = getElementsToDelete( + defaultInformation, + information, + ["contents", "references", "id"] + ); + const result = await executeUpdate({ + upsert, + contentIdsToDelete, + referenceIdsToDelete, + contentReferenceIdsToDelete, + contentBlockIdsToDelete, + contentBlockContentIdsToDelete, + }); + if (result.error) { + throw new Error(result.error.message); + } + return result?.data?.insert_information_informations_one.id; + }; + return resultFunction; + }; + +let defaultInformation: Information; + +export const setDefaultData = (data: Information) => { + defaultInformation = data; +}; diff --git a/targets/frontend/src/modules/informations/components/informationsEdit/editInformation.type.ts b/targets/frontend/src/modules/informations/components/informationsEdit/editInformation.type.ts new file mode 100644 index 000000000..142cdda5b --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsEdit/editInformation.type.ts @@ -0,0 +1,62 @@ +import { + Information, + InformationContent, + InformationContentBlock, + InformationContentBlockContent, + Reference, + File, +} from "../../type"; + +export type HasuraOnConflict = { + constraint: string; + update_columns: string; +}; + +export type UpsertInformationBlockContent = Omit< + InformationContentBlockContent, + "document" +> & { + cdtnId: string; +}; + +export type UpsertInformationBlock = Omit< + InformationContentBlock, + "contents" | "file" +> & { + contents: { + data: UpsertInformationBlockContent[]; + on_conflict: HasuraOnConflict; + }; + file: { + data?: File; + on_conflict: HasuraOnConflict; + }; +}; + +export type UpsertInformationContent = Omit< + InformationContent, + "blocks" | "references" +> & { + blocks: { + data: UpsertInformationBlock[]; + on_conflict: HasuraOnConflict; + }; + references: { + data: Reference[]; + on_conflict: HasuraOnConflict; + }; +}; + +export type UpsertInformationObject = Omit< + Information, + "contents" | "references" +> & { + contents: { + data: UpsertInformationContent[]; + on_conflict: HasuraOnConflict; + }; + references: { + data: Reference[]; + on_conflict: HasuraOnConflict; + }; +}; diff --git a/targets/frontend/src/modules/informations/components/informationsEdit/index.ts b/targets/frontend/src/modules/informations/components/informationsEdit/index.ts new file mode 100644 index 000000000..92884c2a3 --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsEdit/index.ts @@ -0,0 +1,2 @@ +export * from "./InformationsEdit"; +export * from "./InformationsCreate"; diff --git a/targets/frontend/src/modules/informations/components/informationsEdit/publishInformation.mutation.ts b/targets/frontend/src/modules/informations/components/informationsEdit/publishInformation.mutation.ts new file mode 100644 index 000000000..4fec37361 --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsEdit/publishInformation.mutation.ts @@ -0,0 +1,28 @@ +import { OperationResult, useMutation } from "urql"; + +export const publishInformationMutation = ` +mutation publish_information( + $id: uuid! +) { + publish(id: $id, source: "information") { + cdtnId + } +} +`; + +export type PublishInformationMutationResult = ( + id: string +) => Promise; + +export const usePublishInformationMutation = + (): PublishInformationMutationResult => { + const [, execute] = useMutation(publishInformationMutation); + const resultFunction = async (id: string) => { + const result = await execute({ id }); + if (result.error) { + throw new Error(result.error.message); + } + return result; + }; + return resultFunction; + }; diff --git a/targets/frontend/src/modules/informations/components/informationsList/InformationsList.query.ts b/targets/frontend/src/modules/informations/components/informationsList/InformationsList.query.ts new file mode 100644 index 000000000..fe0673749 --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsList/InformationsList.query.ts @@ -0,0 +1,51 @@ +import { useQuery } from "urql"; +import { Information } from "../../type"; + +export const informationsListQuery = `query informationsList($search: String) { + information_informations( + where: { + title: { _ilike: $search } + } + order_by: {updatedAt: desc} + ) { + cdtnId + description + id + intro + metaDescription + metaTitle + referenceLabel + sectionDisplayMode + title + updatedAt + } + }`; + +export type QueryInformation = Information; + +export type QueryResult = { + information_informations: QueryInformation[]; +}; + +export type InformationsListQueryProps = { + search?: string; +}; + +export type InformationsListQueryResult = { + rows: QueryInformation[]; +}; + +export const useInformationsListQuery = ({ + search, +}: InformationsListQueryProps): InformationsListQueryResult => { + const [result] = useQuery({ + query: informationsListQuery, + requestPolicy: "cache-and-network", + variables: { + search, + }, + }); + return { + rows: result.data?.information_informations ?? [], + }; +}; diff --git a/targets/frontend/src/modules/informations/components/informationsList/InformationsList.tsx b/targets/frontend/src/modules/informations/components/informationsList/InformationsList.tsx new file mode 100644 index 000000000..3acda19c9 --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsList/InformationsList.tsx @@ -0,0 +1,68 @@ +import { + Paper, + Stack, + Table, + TableBody, + TableHead, + TableContainer, + TextField, + TableRow, + TableCell, + Button, +} from "@mui/material"; +import { useState } from "react"; +import { useRouter } from "next/router"; + +import { useInformationsListQuery } from "./InformationsList.query"; +import { InformationsRow } from "./InformationsRow"; + +export const QuestionList = (): JSX.Element => { + const [search, setSearch] = useState(); + const router = useRouter(); + const { rows } = useInformationsListQuery({ + search, + }); + return ( + + + { + const value = event.target.value; + setSearch(value ? `%${value}%` : undefined); + }} + data-testid="informations-list-search" + /> + + + + + + + Titre + + + + {rows.map((row) => ( + + ))} + +
+
+
+ ); +}; diff --git a/targets/frontend/src/modules/informations/components/informationsList/InformationsRow.tsx b/targets/frontend/src/modules/informations/components/informationsList/InformationsRow.tsx new file mode 100644 index 000000000..cc0e3c802 --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsList/InformationsRow.tsx @@ -0,0 +1,28 @@ +import TableCell from "@mui/material/TableCell"; +import TableRow from "@mui/material/TableRow"; +import { useRouter } from "next/router"; + +import { QueryInformation } from "./InformationsList.query"; + +export const InformationsRow = (props: { row: QueryInformation }) => { + const { row } = props; + const router = useRouter(); + + return ( + <> + { + router.push(`/informations/${row.id}`); + }} + style={{ cursor: "pointer" }} + hover + > + + {row.title} + + + + ); +}; diff --git a/targets/frontend/src/modules/informations/components/informationsList/index.ts b/targets/frontend/src/modules/informations/components/informationsList/index.ts new file mode 100644 index 000000000..bc7481f52 --- /dev/null +++ b/targets/frontend/src/modules/informations/components/informationsList/index.ts @@ -0,0 +1 @@ +export * from "./InformationsList"; diff --git a/targets/frontend/src/modules/informations/index.ts b/targets/frontend/src/modules/informations/index.ts new file mode 100644 index 000000000..5fb5a6cf7 --- /dev/null +++ b/targets/frontend/src/modules/informations/index.ts @@ -0,0 +1,3 @@ +export * from "./api"; +export * from "./components"; +export * from "./type"; diff --git a/targets/frontend/src/modules/informations/type.ts b/targets/frontend/src/modules/informations/type.ts new file mode 100644 index 000000000..ab9f4361b --- /dev/null +++ b/targets/frontend/src/modules/informations/type.ts @@ -0,0 +1,92 @@ +import { z } from "zod"; + +export const referenceSchema = z.object({ + id: z.string().uuid().nullable().optional(), + url: z.string({ required_error: "une url doit être renseigner" }), + type: z.string({ required_error: "un type doit être renseigner" }), + title: z.string({ required_error: "un titre doit être renseigner" }), + order: z.number().nullable().optional(), +}); +export type Reference = z.infer; + +export const fileSchema = z.object({ + id: z.string().uuid().nullable().optional(), + url: z + .string({ required_error: "une url doit être renseigner" }) + .min(1, "un nom de fichier doit être renseigner") + .regex( + /.*(\.|\/)(svg|jpe?g|png|pdf)$/g, + "Le format doit correspondre à une url" + ), + altText: z.string().nullable().optional(), + size: z.string().nullable().optional(), +}); +export type File = z.infer; + +export const informationContentBlockContentSchema = z.object({ + document: z.object({ + cdtnId: z.string(), + source: z.string(), + title: z.string(), + slug: z.string(), + }), +}); +export type InformationContentBlockContent = z.infer< + typeof informationContentBlockContentSchema +>; + +export const informationContentBlockSchema = z.object({ + id: z.string().uuid().nullable().optional(), + content: z.string(), + type: z.string({ required_error: "un type doit être renseigner" }), + contentDisplayMode: z.string().nullable().optional(), + order: z.number().nullable().optional(), + file: fileSchema.nullable().optional(), + img: fileSchema.nullable().optional(), + contents: z.array(informationContentBlockContentSchema).nullable().optional(), +}); +export type InformationContentBlock = z.infer< + typeof informationContentBlockSchema +>; + +export const informationContentSchema = z.object({ + id: z.string().uuid().nullable().optional(), + name: z.string().nullable().optional(), + title: z + .string({ required_error: "un titre doit être renseigner" }) + .min(1, "un titre doit être renseigner"), + referenceLabel: z.string().nullable().optional(), + order: z.number().nullable().optional(), + blocks: z.array(informationContentBlockSchema), + references: z.array(referenceSchema).nullable().optional(), +}); +export type InformationContent = z.infer; + +export const informationSchema = z.object({ + id: z.string().uuid().optional(), + cdtnId: z.string().nullable().optional(), + title: z + .string({ required_error: "un titre doit être renseigner" }) + .min(1, "un titre doit être renseigner"), + metaTitle: z + .string({ required_error: "un titre meta doit être renseigner" }) + .min(1, "un titre meta doit être renseigner"), + description: z + .string({ + required_error: "une description doit être renseigner", + }) + .min(1, "une description doit être renseigner"), + metaDescription: z + .string({ + required_error: "une description meta doit être renseigner", + }) + .min(1, "une description meta doit être renseigner"), + intro: z.string().nullable().optional(), + referenceLabel: z.string().nullable().optional(), + sectionDisplayMode: z.string().optional(), + dismissalProcess: z.boolean(), + updatedAt: z.string().nullable().optional(), + contents: z.array(informationContentSchema), + references: z.array(referenceSchema), +}); +export type Information = z.infer; diff --git a/targets/frontend/src/pages/api/actions/publish.ts b/targets/frontend/src/pages/api/actions/publish.ts new file mode 100644 index 000000000..c1fa070cd --- /dev/null +++ b/targets/frontend/src/pages/api/actions/publish.ts @@ -0,0 +1,12 @@ +import { NextApiRequest, NextApiResponse } from "next"; +import { DocumentsController } from "src/modules/documents"; + +export default async function publish( + req: NextApiRequest, + res: NextApiResponse +) { + if (req.method === "POST") { + const controller = new DocumentsController(req, res); + return await controller.publish(); + } +} diff --git a/targets/frontend/src/pages/informations/[id].tsx b/targets/frontend/src/pages/informations/[id].tsx new file mode 100644 index 000000000..75518c98c --- /dev/null +++ b/targets/frontend/src/pages/informations/[id].tsx @@ -0,0 +1,18 @@ +import { useRouter } from "next/router"; +import { InformationsEdit } from "src/modules/informations"; +import { Layout } from "src/components/layout/auth.layout"; +import { withCustomUrqlClient } from "src/hoc/CustomUrqlClient"; +import { withUserProvider } from "src/hoc/UserProvider"; + +export function EditAnswerPage() { + const router = useRouter(); + const id = router?.query?.id as string; + + return ( + + + + ); +} + +export default withCustomUrqlClient(withUserProvider(EditAnswerPage)); diff --git a/targets/frontend/src/pages/informations/creation.tsx b/targets/frontend/src/pages/informations/creation.tsx new file mode 100644 index 000000000..cd0bd5e6f --- /dev/null +++ b/targets/frontend/src/pages/informations/creation.tsx @@ -0,0 +1,14 @@ +import { InformationsCreate } from "src/modules/informations"; +import { Layout } from "src/components/layout/auth.layout"; +import { withCustomUrqlClient } from "src/hoc/CustomUrqlClient"; +import { withUserProvider } from "src/hoc/UserProvider"; + +export function EditAnswerPage() { + return ( + + + + ); +} + +export default withCustomUrqlClient(withUserProvider(EditAnswerPage)); diff --git a/targets/frontend/src/pages/informations/index.tsx b/targets/frontend/src/pages/informations/index.tsx new file mode 100644 index 000000000..34ddd42f9 --- /dev/null +++ b/targets/frontend/src/pages/informations/index.tsx @@ -0,0 +1,15 @@ +import { QuestionList } from "src/modules/informations"; + +import { Layout } from "src/components/layout/auth.layout"; +import { withCustomUrqlClient } from "src/hoc/CustomUrqlClient"; +import { withUserProvider } from "src/hoc/UserProvider"; + +export function InformationsPage() { + return ( + + + + ); +} + +export default withCustomUrqlClient(withUserProvider(InformationsPage)); diff --git a/targets/frontend/src/types/common.ts b/targets/frontend/src/types/common.ts new file mode 100644 index 000000000..6ac64c68d --- /dev/null +++ b/targets/frontend/src/types/common.ts @@ -0,0 +1,5 @@ +export type DeepPartial = T extends object + ? { + [P in keyof T]?: DeepPartial; + } + : T; diff --git a/targets/hasura/metadata/actions.graphql b/targets/hasura/metadata/actions.graphql index 39b345141..4909a7626 100644 --- a/targets/hasura/metadata/actions.graphql +++ b/targets/hasura/metadata/actions.graphql @@ -25,6 +25,13 @@ type Mutation { ): Status } +type Mutation { + publish( + id: uuid! + source: String! + ): publishOutput +} + type Query { recentKaliReference( agreementId: bpchar! @@ -97,3 +104,7 @@ type RecentKaliReferenceOutput { refs: [KaliArticles] } +type publishOutput { + cdtnId: String! +} + diff --git a/targets/hasura/metadata/actions.yaml b/targets/hasura/metadata/actions.yaml index bf8391151..8acfd6e16 100644 --- a/targets/hasura/metadata/actions.yaml +++ b/targets/hasura/metadata/actions.yaml @@ -40,6 +40,14 @@ actions: permissions: - role: super - role: user + - name: publish + definition: + kind: synchronous + handler: '{{API_URL}}/actions/publish' + forward_client_headers: true + permissions: + - role: super + comment: Action pour publier un document - name: recentKaliReference definition: kind: "" @@ -90,4 +98,5 @@ custom_types: type: object - name: KaliArticles - name: RecentKaliReferenceOutput + - name: publishOutput scalars: [] diff --git a/targets/hasura/metadata/databases/default/tables/information_informations.yaml b/targets/hasura/metadata/databases/default/tables/information_informations.yaml new file mode 100644 index 000000000..e02e4af19 --- /dev/null +++ b/targets/hasura/metadata/databases/default/tables/information_informations.yaml @@ -0,0 +1,130 @@ +table: + name: informations + schema: information +configuration: + column_config: + cdtn_id: + custom_name: cdtnId + dismissal_process: + custom_name: dismissalProcess + meta_description: + custom_name: metaDescription + meta_title: + custom_name: metaTitle + reference_label: + custom_name: referenceLabel + section_display_mode: + custom_name: sectionDisplayMode + updated_at: + custom_name: updatedAt + custom_column_names: + cdtn_id: cdtnId + dismissal_process: dismissalProcess + meta_description: metaDescription + meta_title: metaTitle + reference_label: referenceLabel + section_display_mode: sectionDisplayMode + updated_at: updatedAt + custom_root_fields: {} +array_relationships: + - name: contents + using: + foreign_key_constraint_on: + column: informations_id + table: + name: informations_contents + schema: information + - name: references + using: + foreign_key_constraint_on: + column: informations_id + table: + name: informations_references + schema: information +insert_permissions: + - role: super + permission: + check: {} + columns: + - cdtn_id + - description + - dismissal_process + - id + - intro + - meta_description + - meta_title + - reference_label + - section_display_mode + - title + - updated_at + - role: user + permission: + check: {} + columns: + - cdtn_id + - description + - dismissal_process + - id + - intro + - meta_description + - meta_title + - reference_label + - section_display_mode + - title + - updated_at +select_permissions: + - role: super + permission: + columns: + - cdtn_id + - description + - dismissal_process + - id + - intro + - meta_description + - meta_title + - reference_label + - section_display_mode + - title + - updated_at + filter: {} + allow_aggregations: true + - role: user + permission: + columns: + - cdtn_id + - description + - dismissal_process + - id + - intro + - meta_description + - meta_title + - reference_label + - section_display_mode + - title + - updated_at + filter: {} + allow_aggregations: true +update_permissions: + - role: super + permission: + columns: + - cdtn_id + - description + - dismissal_process + - id + - intro + - meta_description + - meta_title + - reference_label + - section_display_mode + - title + - updated_at + filter: {} + check: {} + set: + updated_at: now() +delete_permissions: + - role: super + permission: + filter: {} diff --git a/targets/hasura/metadata/databases/default/tables/information_informations_contents.yaml b/targets/hasura/metadata/databases/default/tables/information_informations_contents.yaml new file mode 100644 index 000000000..d999b0c9d --- /dev/null +++ b/targets/hasura/metadata/databases/default/tables/information_informations_contents.yaml @@ -0,0 +1,67 @@ +table: + name: informations_contents + schema: information +configuration: + column_config: + informations_id: + custom_name: informationsId + reference_label: + custom_name: referenceLabel + custom_column_names: + informations_id: informationsId + reference_label: referenceLabel + custom_root_fields: {} +array_relationships: + - name: blocks + using: + foreign_key_constraint_on: + column: informations_contents_id + table: + name: informations_contents_blocks + schema: information + - name: references + using: + foreign_key_constraint_on: + column: informations_contents_id + table: + name: informations_contents_references + schema: information +insert_permissions: + - role: super + permission: + check: {} + columns: + - order + - name + - title + - reference_label + - id + - informations_id +select_permissions: + - role: super + permission: + columns: + - order + - name + - title + - reference_label + - id + - informations_id + filter: {} + allow_aggregations: true +update_permissions: + - role: super + permission: + columns: + - order + - name + - title + - reference_label + - id + - informations_id + filter: {} + check: {} +delete_permissions: + - role: super + permission: + filter: {} diff --git a/targets/hasura/metadata/databases/default/tables/information_informations_contents_blocks.yaml b/targets/hasura/metadata/databases/default/tables/information_informations_contents_blocks.yaml new file mode 100644 index 000000000..a9f7c3301 --- /dev/null +++ b/targets/hasura/metadata/databases/default/tables/information_informations_contents_blocks.yaml @@ -0,0 +1,79 @@ +table: + name: informations_contents_blocks + schema: information +configuration: + column_config: + content_display_mode: + custom_name: contentDisplayMode + file_id: + custom_name: fileId + img_id: + custom_name: imgId + informations_contents_id: + custom_name: informationsContentsId + custom_column_names: + content_display_mode: contentDisplayMode + file_id: fileId + img_id: imgId + informations_contents_id: informationsContentsId + custom_root_fields: {} +object_relationships: + - name: file + using: + foreign_key_constraint_on: file_id + - name: img + using: + foreign_key_constraint_on: img_id +array_relationships: + - name: contents + using: + foreign_key_constraint_on: + column: informations_contents_blocks_id + table: + name: informations_contents_blocks_contents + schema: information +insert_permissions: + - role: super + permission: + check: {} + columns: + - content + - content_display_mode + - file_id + - id + - img_id + - informations_contents_id + - order + - type +select_permissions: + - role: super + permission: + columns: + - content + - content_display_mode + - file_id + - id + - img_id + - informations_contents_id + - order + - type + filter: {} + allow_aggregations: true +update_permissions: + - role: super + permission: + columns: + - content + - content_display_mode + - file_id + - id + - img_id + - informations_contents_id + - order + - type + filter: {} + check: {} +delete_permissions: + - role: super + permission: + filter: {} diff --git a/targets/hasura/metadata/databases/default/tables/information_informations_contents_blocks_contents.yaml b/targets/hasura/metadata/databases/default/tables/information_informations_contents_blocks_contents.yaml new file mode 100644 index 000000000..06ca64869 --- /dev/null +++ b/targets/hasura/metadata/databases/default/tables/information_informations_contents_blocks_contents.yaml @@ -0,0 +1,50 @@ +table: + name: informations_contents_blocks_contents + schema: information +configuration: + column_config: + cdtn_id: + custom_name: cdtnId + informations_contents_blocks_id: + custom_name: informationsContentsBlocksId + custom_column_names: + cdtn_id: cdtnId + informations_contents_blocks_id: informationsContentsBlocksId + custom_root_fields: {} +object_relationships: + - name: document + using: + foreign_key_constraint_on: cdtn_id +insert_permissions: + - role: super + permission: + check: {} + columns: + - order + - cdtn_id + - id + - informations_contents_blocks_id +select_permissions: + - role: super + permission: + columns: + - order + - cdtn_id + - id + - informations_contents_blocks_id + filter: {} + allow_aggregations: true +update_permissions: + - role: super + permission: + columns: + - order + - cdtn_id + - id + - informations_contents_blocks_id + filter: {} + check: {} +delete_permissions: + - role: super + permission: + filter: {} diff --git a/targets/hasura/metadata/databases/default/tables/information_informations_contents_references.yaml b/targets/hasura/metadata/databases/default/tables/information_informations_contents_references.yaml new file mode 100644 index 000000000..00a9b43c5 --- /dev/null +++ b/targets/hasura/metadata/databases/default/tables/information_informations_contents_references.yaml @@ -0,0 +1,49 @@ +table: + name: informations_contents_references + schema: information +configuration: + column_config: + informations_contents_id: + custom_name: informationsContentsId + custom_column_names: + informations_contents_id: informationsContentsId + custom_root_fields: {} +insert_permissions: + - role: super + permission: + check: {} + columns: + - order + - title + - url + - type + - id + - informations_contents_id +select_permissions: + - role: super + permission: + columns: + - id + - type + - url + - title + - order + - informations_contents_id + filter: {} + allow_aggregations: true +update_permissions: + - role: super + permission: + columns: + - order + - title + - url + - type + - id + - informations_contents_id + filter: {} + check: {} +delete_permissions: + - role: super + permission: + filter: {} diff --git a/targets/hasura/metadata/databases/default/tables/information_informations_references.yaml b/targets/hasura/metadata/databases/default/tables/information_informations_references.yaml new file mode 100644 index 000000000..0b194e0b6 --- /dev/null +++ b/targets/hasura/metadata/databases/default/tables/information_informations_references.yaml @@ -0,0 +1,49 @@ +table: + name: informations_references + schema: information +configuration: + column_config: + informations_id: + custom_name: informationsId + custom_column_names: + informations_id: informationsId + custom_root_fields: {} +insert_permissions: + - role: super + permission: + check: {} + columns: + - order + - title + - url + - type + - id + - informations_id +select_permissions: + - role: super + permission: + columns: + - id + - url + - type + - title + - informations_id + - order + filter: {} + allow_aggregations: true +update_permissions: + - role: super + permission: + columns: + - order + - title + - url + - type + - id + - informations_id + filter: {} + check: {} +delete_permissions: + - role: super + permission: + filter: {} diff --git a/targets/hasura/metadata/databases/default/tables/public_files.yaml b/targets/hasura/metadata/databases/default/tables/public_files.yaml new file mode 100644 index 000000000..bab604947 --- /dev/null +++ b/targets/hasura/metadata/databases/default/tables/public_files.yaml @@ -0,0 +1,43 @@ +table: + name: files + schema: public +configuration: + column_config: + alt_text: + custom_name: altText + custom_column_names: + alt_text: altText + custom_root_fields: {} +insert_permissions: + - role: super + permission: + check: {} + columns: + - alt_text + - size + - url + - id +select_permissions: + - role: super + permission: + columns: + - alt_text + - size + - url + - id + filter: {} + allow_aggregations: true +update_permissions: + - role: super + permission: + columns: + - alt_text + - size + - url + - id + filter: {} + check: {} +delete_permissions: + - role: super + permission: + filter: {} diff --git a/targets/hasura/metadata/databases/default/tables/tables.yaml b/targets/hasura/metadata/databases/default/tables/tables.yaml index 2e1070439..5add9f204 100644 --- a/targets/hasura/metadata/databases/default/tables/tables.yaml +++ b/targets/hasura/metadata/databases/default/tables/tables.yaml @@ -17,6 +17,12 @@ - "!include contribution_answers.yaml" - "!include contribution_question_messages.yaml" - "!include contribution_questions.yaml" +- "!include information_informations.yaml" +- "!include information_informations_contents.yaml" +- "!include information_informations_contents_blocks.yaml" +- "!include information_informations_contents_blocks_contents.yaml" +- "!include information_informations_contents_references.yaml" +- "!include information_informations_references.yaml" - "!include public_agreements.yaml" - "!include public_alert_notes.yaml" - "!include public_alert_status.yaml" @@ -25,6 +31,7 @@ - "!include public_document_relations.yaml" - "!include public_documents.yaml" - "!include public_export_es_status.yaml" +- "!include public_files.yaml" - "!include public_glossary.yaml" - "!include public_kali_articles.yaml" - "!include public_kali_blocks.yaml" diff --git a/targets/hasura/migrations/default/1693835658115_create_schema_information/down.sql b/targets/hasura/migrations/default/1693835658115_create_schema_information/down.sql new file mode 100644 index 000000000..7e8a43289 --- /dev/null +++ b/targets/hasura/migrations/default/1693835658115_create_schema_information/down.sql @@ -0,0 +1 @@ +drop schema "information" cascade; diff --git a/targets/hasura/migrations/default/1693835658115_create_schema_information/up.sql b/targets/hasura/migrations/default/1693835658115_create_schema_information/up.sql new file mode 100644 index 000000000..e96ecee9e --- /dev/null +++ b/targets/hasura/migrations/default/1693835658115_create_schema_information/up.sql @@ -0,0 +1 @@ +create schema "information"; diff --git a/targets/hasura/migrations/default/1693836035778_create_table_information_informations/down.sql b/targets/hasura/migrations/default/1693836035778_create_table_information_informations/down.sql new file mode 100644 index 000000000..5e4c0d93b --- /dev/null +++ b/targets/hasura/migrations/default/1693836035778_create_table_information_informations/down.sql @@ -0,0 +1,3 @@ +DROP TABLE "information"."informations"; +DROP TYPE "information"."SectionDisplayModeType"; +DROP TYPE "information"."ReferenceLabelType"; diff --git a/targets/hasura/migrations/default/1693836035778_create_table_information_informations/up.sql b/targets/hasura/migrations/default/1693836035778_create_table_information_informations/up.sql new file mode 100644 index 000000000..c98bf7fbb --- /dev/null +++ b/targets/hasura/migrations/default/1693836035778_create_table_information_informations/up.sql @@ -0,0 +1,18 @@ +CREATE TABLE "information"."informations" ( + "updated_at" timestamptz NOT NULL DEFAULT now(), + "intro" text, + "title" text NOT NULL, + "meta_title" text NOT NULL, + "meta_description" text NOT NULL, + "description" text NOT NULL, + "cdtn_id" text, + "section_display_mode" text NOT NULL default 'accordion', + "reference_label" text, + "dismissal_process" boolean not null default false, + "id" uuid NOT NULL DEFAULT gen_random_uuid(), + PRIMARY KEY ("id"), + FOREIGN KEY ("cdtn_id") REFERENCES "public"."documents"("cdtn_id") ON UPDATE restrict ON DELETE restrict, + UNIQUE ("id"), + UNIQUE ("cdtn_id") +); +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/targets/hasura/migrations/default/1693836242023_create_table_information_informations_contents/down.sql b/targets/hasura/migrations/default/1693836242023_create_table_information_informations_contents/down.sql new file mode 100644 index 000000000..da678924c --- /dev/null +++ b/targets/hasura/migrations/default/1693836242023_create_table_information_informations_contents/down.sql @@ -0,0 +1 @@ +DROP TABLE "information"."informations_contents"; diff --git a/targets/hasura/migrations/default/1693836242023_create_table_information_informations_contents/up.sql b/targets/hasura/migrations/default/1693836242023_create_table_information_informations_contents/up.sql new file mode 100644 index 000000000..0572de921 --- /dev/null +++ b/targets/hasura/migrations/default/1693836242023_create_table_information_informations_contents/up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "information"."informations_contents" ( + "id" uuid NOT NULL DEFAULT gen_random_uuid(), + "name" text NOT NULL, + "title" text NOT NULL, + "order" Integer NOT NULL, + "informations_id" uuid NOT NULL, + "reference_label" text, + PRIMARY KEY ("id"), + FOREIGN KEY ("informations_id") REFERENCES "information"."informations"("id") ON UPDATE cascade ON DELETE cascade, + UNIQUE ("id"), + UNIQUE ("informations_id", "order") +); +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/targets/hasura/migrations/default/1693837558650_create_table_public_files/down.sql b/targets/hasura/migrations/default/1693837558650_create_table_public_files/down.sql new file mode 100644 index 000000000..9bd02859f --- /dev/null +++ b/targets/hasura/migrations/default/1693837558650_create_table_public_files/down.sql @@ -0,0 +1 @@ +DROP TABLE "public"."files"; diff --git a/targets/hasura/migrations/default/1693837558650_create_table_public_files/up.sql b/targets/hasura/migrations/default/1693837558650_create_table_public_files/up.sql new file mode 100644 index 000000000..0fae7a161 --- /dev/null +++ b/targets/hasura/migrations/default/1693837558650_create_table_public_files/up.sql @@ -0,0 +1,9 @@ +CREATE TABLE "public"."files" ( + "id" uuid NOT NULL DEFAULT gen_random_uuid(), + "url" text NOT NULL, + "alt_text" text, + "size" text, + PRIMARY KEY ("id"), + UNIQUE ("id") +); +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/targets/hasura/migrations/default/1693837558654_create_table_information_informations_contents_blocks/down.sql b/targets/hasura/migrations/default/1693837558654_create_table_information_informations_contents_blocks/down.sql new file mode 100644 index 000000000..e0391a3f5 --- /dev/null +++ b/targets/hasura/migrations/default/1693837558654_create_table_information_informations_contents_blocks/down.sql @@ -0,0 +1 @@ +DROP TABLE "information"."informations_contents_blocks"; diff --git a/targets/hasura/migrations/default/1693837558654_create_table_information_informations_contents_blocks/up.sql b/targets/hasura/migrations/default/1693837558654_create_table_information_informations_contents_blocks/up.sql new file mode 100644 index 000000000..be4a61082 --- /dev/null +++ b/targets/hasura/migrations/default/1693837558654_create_table_information_informations_contents_blocks/up.sql @@ -0,0 +1,17 @@ +CREATE TABLE "information"."informations_contents_blocks" ( + "id" uuid NOT NULL DEFAULT gen_random_uuid(), + "content" text NOT NULL, + "order" integer NOT NULL, + "type" text NOT NULL default 'markdown', + "content_display_mode" text, + "informations_contents_id" uuid NOT NULL, + "file_id" uuid, + "img_id" uuid, + PRIMARY KEY ("id"), + FOREIGN KEY ("informations_contents_id") REFERENCES "information"."informations_contents"("id") ON UPDATE cascade ON DELETE cascade, + FOREIGN KEY ("file_id") REFERENCES "public"."files"("id") ON UPDATE cascade ON DELETE cascade, + FOREIGN KEY ("img_id") REFERENCES "public"."files"("id") ON UPDATE cascade ON DELETE cascade, + UNIQUE ("id"), + UNIQUE ("informations_contents_id", "order") +); +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/targets/hasura/migrations/default/1693908863392_create_table_information_informations_contents_references/down.sql b/targets/hasura/migrations/default/1693908863392_create_table_information_informations_contents_references/down.sql new file mode 100644 index 000000000..bb2bd8a96 --- /dev/null +++ b/targets/hasura/migrations/default/1693908863392_create_table_information_informations_contents_references/down.sql @@ -0,0 +1,2 @@ +DROP TABLE "information"."informations_contents_references"; +DROP TYPE "information"."referenceType"; diff --git a/targets/hasura/migrations/default/1693908863392_create_table_information_informations_contents_references/up.sql b/targets/hasura/migrations/default/1693908863392_create_table_information_informations_contents_references/up.sql new file mode 100644 index 000000000..3a389b0f7 --- /dev/null +++ b/targets/hasura/migrations/default/1693908863392_create_table_information_informations_contents_references/up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "information"."informations_contents_references" ( + "id" uuid NOT NULL DEFAULT gen_random_uuid(), + "type" text NOT NULL default 'external', + "url" text NOT NULL, + "title" text NOT NULL, + "order" integer NOT NULL, + "informations_contents_id" uuid NOT NULL, + PRIMARY KEY ("id"), + FOREIGN KEY ("informations_contents_id") REFERENCES "information"."informations_contents"("id") ON UPDATE cascade ON DELETE cascade, + UNIQUE ("id"), + UNIQUE ("informations_contents_id", "order") +); +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/targets/hasura/migrations/default/1693912613427_create_table_information_informations_references/down.sql b/targets/hasura/migrations/default/1693912613427_create_table_information_informations_references/down.sql new file mode 100644 index 000000000..ad2bc2b30 --- /dev/null +++ b/targets/hasura/migrations/default/1693912613427_create_table_information_informations_references/down.sql @@ -0,0 +1 @@ +DROP TABLE "information"."informations_references"; diff --git a/targets/hasura/migrations/default/1693912613427_create_table_information_informations_references/up.sql b/targets/hasura/migrations/default/1693912613427_create_table_information_informations_references/up.sql new file mode 100644 index 000000000..dc26df1b1 --- /dev/null +++ b/targets/hasura/migrations/default/1693912613427_create_table_information_informations_references/up.sql @@ -0,0 +1,13 @@ +CREATE TABLE "information"."informations_references" ( + "id" uuid NOT NULL DEFAULT gen_random_uuid(), + "url" text NOT NULL, + "type" text NOT NULL default 'external', + "title" text NOT NULL, + "informations_id" uuid NOT NULL, + "order" integer NOT NULL, + PRIMARY KEY ("id"), + FOREIGN KEY ("informations_id") REFERENCES "information"."informations"("id") ON UPDATE cascade ON DELETE cascade, + UNIQUE ("id"), + UNIQUE ("informations_id", "order") +); +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/targets/hasura/migrations/default/1693991664253_create_table_information_informations_contents_blocks_contents/down.sql b/targets/hasura/migrations/default/1693991664253_create_table_information_informations_contents_blocks_contents/down.sql new file mode 100644 index 000000000..0aab7394a --- /dev/null +++ b/targets/hasura/migrations/default/1693991664253_create_table_information_informations_contents_blocks_contents/down.sql @@ -0,0 +1 @@ +DROP TABLE "information"."informations_contents_blocks_contents"; diff --git a/targets/hasura/migrations/default/1693991664253_create_table_information_informations_contents_blocks_contents/up.sql b/targets/hasura/migrations/default/1693991664253_create_table_information_informations_contents_blocks_contents/up.sql new file mode 100644 index 000000000..2d29371be --- /dev/null +++ b/targets/hasura/migrations/default/1693991664253_create_table_information_informations_contents_blocks_contents/up.sql @@ -0,0 +1,12 @@ +CREATE TABLE "information"."informations_contents_blocks_contents" ( + "id" uuid NOT NULL DEFAULT gen_random_uuid(), + "informations_contents_blocks_id" uuid NOT NULL, + "cdtn_id" text NOT NULL, + "order" integer NOT NULL, + PRIMARY KEY ("id"), + FOREIGN KEY ("informations_contents_blocks_id") REFERENCES "information"."informations_contents_blocks"("id") ON UPDATE cascade ON DELETE cascade, + FOREIGN KEY ("cdtn_id") REFERENCES "public"."documents"("cdtn_id") ON UPDATE restrict ON DELETE restrict, + UNIQUE ("id"), + UNIQUE ("informations_contents_blocks_id", "order") +); +CREATE EXTENSION IF NOT EXISTS pgcrypto; diff --git a/targets/hasura/migrations/default/1694012383773_migrate_info_page/down.sql b/targets/hasura/migrations/default/1694012383773_migrate_info_page/down.sql new file mode 100644 index 000000000..225005b0d --- /dev/null +++ b/targets/hasura/migrations/default/1694012383773_migrate_info_page/down.sql @@ -0,0 +1 @@ +truncate table information.informations cascade; diff --git a/targets/hasura/migrations/default/1694012383773_migrate_info_page/up.sql b/targets/hasura/migrations/default/1694012383773_migrate_info_page/up.sql new file mode 100644 index 000000000..05f81c709 --- /dev/null +++ b/targets/hasura/migrations/default/1694012383773_migrate_info_page/up.sql @@ -0,0 +1,251 @@ +with _informations as ( + select cdtn_id, + title, + "document"->>'meta_title' as meta_title, + meta_description, + TO_TIMESTAMP("document"->>'date', 'DD/MM/YYYY') as updated_at, + "document"->>'intro' as intro, + "document"->'contents' as contents, + "document"->'references' as "references", + "document"->>'description' as "description", + "document"->>'sectionDisplayMode' as "section_display_mode", + case + when "document"->>'dismissalProcess' = 'true' then true + else false + end as "dismissal_process", + "document"->'references'->0->>'label' as "reference_label" + from documents + where source = 'information' +), +_informations_inserted as ( + insert into "information".informations( + updated_at, + intro, + title, + meta_title, + meta_description, + description, + section_display_mode, + dismissal_process, + reference_label, + cdtn_id + ) + select updated_at, + intro, + title, + coalesce(meta_title, title), + meta_description, + description, + coalesce( + section_display_mode, + 'accordion' + ), + dismissal_process, + reference_label, + cdtn_id + from _informations + returning id, + cdtn_id +), +_informations_references as ( + select i.id as informations_id, + l.links->'id' as id, + l.links->>'url' as url, + l.links->>'type' as "type", + l.links->>'title' as title, + row_number() over(partition by i.id) as "order" + from ( + select r.cdtn_id, + jsonb_array_elements(r.refs->'links') as "links" + from ( + select cdtn_id, + jsonb_array_elements("references") as refs + from _informations + where jsonb_typeof("references") = 'array' + ) r + ) l + inner join _informations_inserted i on i.cdtn_id = l.cdtn_id +), +_informations_references_inserted as ( + insert into information.informations_references(informations_id, url, "type", title, "order") + select informations_id, + url, + "type", + title, + "order" + from _informations_references + returning id +), +_informations_contents as ( + select i.id as informations_id, + c."content"->>'name' as "name", + c."content"->>'title' as title, + c."content"->'blocks' as blocks, + c."content"->'references' as "references", + c."content"->'references'->0->>'label' as "reference_label", + row_number() over(partition by i.id) as "order" + from ( + select cdtn_id, + jsonb_array_elements(contents) as "content" + from _informations + ) c + inner join _informations_inserted i on i.cdtn_id = c.cdtn_id +), +_informations_contents_inserted as ( + insert into information.informations_contents( + "name", + "title", + "order", + informations_id, + reference_label + ) + select "name", + "title", + "order", + informations_id, + reference_label + from _informations_contents + returning id, + informations_id, + title +), +_informations_contents_references as ( + select i.id as informations_contents_id, + l.links->>'id' as id, + l.links->>'url' as url, + l.links->>'type' as "type", + l.links->>'title' as title, + row_number() over(partition by i.id) as "order" + from ( + select title, + informations_id, + jsonb_array_elements(r.refs->'links') as "links" + from ( + select title, + informations_id, + jsonb_array_elements("references") as refs + from _informations_contents + where jsonb_typeof("references") = 'array' + ) r + ) l + inner join _informations_contents_inserted i on i.informations_id = l.informations_id + and i.title = l.title +), +_informations_contents_references_inserted as ( + insert into information.informations_contents_references( + informations_contents_id, + url, + "type", + title, + "order" + ) + select informations_contents_id, + url, + "type", + title, + "order" + from _informations_contents_references + returning id +), +_informations_contents_blocks as ( + select i.id as informations_contents_id, + coalesce(b.block->>'markdown', b.block->>'title') as "content", + b.block->>'type' as "type", + b.block->>'size' as "size", + b.block->>'imgUrl' as "img_url", + b.block->>'fileUrl' as "file_url", + b.block->>'altText' as "alt_text", + b.block->>'blockDisplayMode' as "content_display_mode", + b.block->'contents' as contents, + row_number() over(partition by b.informations_id, b.title) as "order" + from ( + select informations_id, + title, + jsonb_array_elements(blocks) as block + from _informations_contents + where jsonb_typeof(blocks) = 'array' + ) b + inner join _informations_contents_inserted i on i.informations_id = b.informations_id + and i.title = b.title +), +_files as ( + select fa.url, + jsonb_agg( + jsonb_build_object( + 'altText', + fa.alt_text, + 'size', + fa."size" + ) + ) as agg + from ( + select distinct img_url as url, + alt_text, + "size" + from _informations_contents_blocks + where img_url is not null + union + select distinct file_url as url, + alt_text, + "size" + from _informations_contents_blocks + where file_url is not null + ) as fa + group by fa.url +), +_files_inserted as ( + insert into public.files(url, alt_text, "size") + select url, + agg->0->>'altText' as alt_text, + agg->0->>'size' as "size" + from _files + returning id, + url +), +_informations_contents_blocks_inserted as ( + insert into information.informations_contents_blocks( + informations_contents_id, + "content", + "order", + file_id, + img_id, + "type", + "content_display_mode" + ) + select informations_contents_id, + "content", + "order", + ff.id, + fi.id, + "type", + content_display_mode + from _informations_contents_blocks i + left outer join _files_inserted fi on fi.url = i.img_url + left outer join _files_inserted ff on ff.url = i.file_url + returning id, + informations_contents_id, + "order" +), +_informations_contents_blocks_contents as ( + select c."content"->>'cdtnId' as "cdtn_id", + i.id as informations_contents_blocks_id, + row_number() over(partition by i.id) as "order" + from ( + select informations_contents_id, + "order", + jsonb_array_elements(contents) as "content" + from _informations_contents_blocks + where jsonb_typeof(contents) = 'array' + ) c + inner join _informations_contents_blocks_inserted i on i.informations_contents_id = c.informations_contents_id + and i."order" = c."order" +) +insert into information.informations_contents_blocks_contents( + cdtn_id, + informations_contents_blocks_id, + "order" + ) +select cdtn_id, + informations_contents_blocks_id, + "order" +from _informations_contents_blocks_contents; diff --git a/yarn.lock b/yarn.lock index 4c13ea246..ae0d5153a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2274,6 +2274,15 @@ __metadata: languageName: node linkType: hard +"@hookform/resolvers@npm:^3.3.1": + version: 3.3.1 + resolution: "@hookform/resolvers@npm:3.3.1" + peerDependencies: + react-hook-form: ^7.0.0 + checksum: 1ddc250a8d6769fb11b03110b586677b03463276dda1cdfd0225ab94f46d422868f74b01bef85f785010cc3d836f0669d6b6c0ed752cae532d2badf3537b1e72 + languageName: node + linkType: hard + "@humanwhocodes/config-array@npm:^0.11.10": version: 0.11.10 resolution: "@humanwhocodes/config-array@npm:0.11.10" @@ -3954,10 +3963,10 @@ __metadata: languageName: node linkType: hard -"@next/env@npm:13.5.3": - version: 13.5.3 - resolution: "@next/env@npm:13.5.3" - checksum: ebea3bfca114ca66616557a534fbb37d580f1ab91143eb46ba3bdb5803864dc0e72c08814110809f207d625846f0053871adb75b51b68686ec3a9ed76d9d26bf +"@next/env@npm:13.2.4": + version: 13.2.4 + resolution: "@next/env@npm:13.2.4" + checksum: 4123e08a79e66d6144006972027a9ceb8f3fdd782c4a869df1eb3b91b59ad9f4a44082d3f8e421f4df5214c6bc7190b52b94881369452d65eb4580485f33b9e6 languageName: node linkType: hard @@ -4012,6 +4021,20 @@ __metadata: languageName: node linkType: hard +"@next/swc-android-arm-eabi@npm:13.2.4": + version: 13.2.4 + resolution: "@next/swc-android-arm-eabi@npm:13.2.4" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + +"@next/swc-android-arm64@npm:13.2.4": + version: 13.2.4 + resolution: "@next/swc-android-arm64@npm:13.2.4" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@next/swc-darwin-arm64@npm:11.1.2": version: 11.1.2 resolution: "@next/swc-darwin-arm64@npm:11.1.2" @@ -4019,9 +4042,9 @@ __metadata: languageName: node linkType: hard -"@next/swc-darwin-arm64@npm:13.5.3": - version: 13.5.3 - resolution: "@next/swc-darwin-arm64@npm:13.5.3" +"@next/swc-darwin-arm64@npm:13.2.4": + version: 13.2.4 + resolution: "@next/swc-darwin-arm64@npm:13.2.4" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -4033,23 +4056,37 @@ __metadata: languageName: node linkType: hard -"@next/swc-darwin-x64@npm:13.5.3": - version: 13.5.3 - resolution: "@next/swc-darwin-x64@npm:13.5.3" +"@next/swc-darwin-x64@npm:13.2.4": + version: 13.2.4 + resolution: "@next/swc-darwin-x64@npm:13.2.4" conditions: os=darwin & cpu=x64 languageName: node linkType: hard -"@next/swc-linux-arm64-gnu@npm:13.5.3": - version: 13.5.3 - resolution: "@next/swc-linux-arm64-gnu@npm:13.5.3" +"@next/swc-freebsd-x64@npm:13.2.4": + version: 13.2.4 + resolution: "@next/swc-freebsd-x64@npm:13.2.4" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + +"@next/swc-linux-arm-gnueabihf@npm:13.2.4": + version: 13.2.4 + resolution: "@next/swc-linux-arm-gnueabihf@npm:13.2.4" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + +"@next/swc-linux-arm64-gnu@npm:13.2.4": + version: 13.2.4 + resolution: "@next/swc-linux-arm64-gnu@npm:13.2.4" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-arm64-musl@npm:13.5.3": - version: 13.5.3 - resolution: "@next/swc-linux-arm64-musl@npm:13.5.3" +"@next/swc-linux-arm64-musl@npm:13.2.4": + version: 13.2.4 + resolution: "@next/swc-linux-arm64-musl@npm:13.2.4" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard @@ -4061,30 +4098,30 @@ __metadata: languageName: node linkType: hard -"@next/swc-linux-x64-gnu@npm:13.5.3": - version: 13.5.3 - resolution: "@next/swc-linux-x64-gnu@npm:13.5.3" +"@next/swc-linux-x64-gnu@npm:13.2.4": + version: 13.2.4 + resolution: "@next/swc-linux-x64-gnu@npm:13.2.4" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard -"@next/swc-linux-x64-musl@npm:13.5.3": - version: 13.5.3 - resolution: "@next/swc-linux-x64-musl@npm:13.5.3" +"@next/swc-linux-x64-musl@npm:13.2.4": + version: 13.2.4 + resolution: "@next/swc-linux-x64-musl@npm:13.2.4" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard -"@next/swc-win32-arm64-msvc@npm:13.5.3": - version: 13.5.3 - resolution: "@next/swc-win32-arm64-msvc@npm:13.5.3" +"@next/swc-win32-arm64-msvc@npm:13.2.4": + version: 13.2.4 + resolution: "@next/swc-win32-arm64-msvc@npm:13.2.4" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard -"@next/swc-win32-ia32-msvc@npm:13.5.3": - version: 13.5.3 - resolution: "@next/swc-win32-ia32-msvc@npm:13.5.3" +"@next/swc-win32-ia32-msvc@npm:13.2.4": + version: 13.2.4 + resolution: "@next/swc-win32-ia32-msvc@npm:13.2.4" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard @@ -4096,9 +4133,9 @@ __metadata: languageName: node linkType: hard -"@next/swc-win32-x64-msvc@npm:13.5.3": - version: 13.5.3 - resolution: "@next/swc-win32-x64-msvc@npm:13.5.3" +"@next/swc-win32-x64-msvc@npm:13.2.4": + version: 13.2.4 + resolution: "@next/swc-win32-x64-msvc@npm:13.2.4" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -5704,12 +5741,12 @@ __metadata: languageName: node linkType: hard -"@swc/helpers@npm:0.5.2": - version: 0.5.2 - resolution: "@swc/helpers@npm:0.5.2" +"@swc/helpers@npm:0.4.14": + version: 0.4.14 + resolution: "@swc/helpers@npm:0.4.14" dependencies: tslib: ^2.4.0 - checksum: 51d7e3d8bd56818c49d6bfbd715f0dbeedc13cf723af41166e45c03e37f109336bbcb57a1f2020f4015957721aeb21e1a7fff281233d797ff7d3dd1f447fa258 + checksum: 273fd3f3fc461a92f3790cc551ea054745c6d6959afbe1232e6d7aa1c722bbc114d308aab96bef5c78fc0303c85c7b472ef00e2253251cc89737f3b1af56e5a5 languageName: node linkType: hard @@ -8555,15 +8592,6 @@ __metadata: languageName: node linkType: hard -"busboy@npm:1.6.0": - version: 1.6.0 - resolution: "busboy@npm:1.6.0" - dependencies: - streamsearch: ^1.1.0 - checksum: 32801e2c0164e12106bf236291a00795c3c4e4b709ae02132883fe8478ba2ae23743b11c5735a0aae8afe65ac4b6ca4568b91f0d9fed1fdbc32ede824a73746e - languageName: node - linkType: hard - "byte-size@npm:^7.0.0": version: 7.0.1 resolution: "byte-size@npm:7.0.1" @@ -12287,6 +12315,7 @@ __metadata: "@emotion/styled": 11.10.6 "@hapi/boom": ^9.1.4 "@hookform/error-message": ^2.0.0 + "@hookform/resolvers": ^3.3.1 "@mui/icons-material": 5.11.16 "@mui/material": 5.14.11 "@reach/accordion": ^0.16.1 @@ -12339,7 +12368,7 @@ __metadata: lint-staged: ^12.0.0 memoizee: ^0.4.15 micromark: ^2.11.4 - next: 13.5.3 + next: 13.2.4 next-urql: ^3.2.1 nodemailer: ^6.6.5 p-limit: ^4.0.0 @@ -12367,7 +12396,8 @@ __metadata: unified: ^9.2.2 urql: ^2.0.5 uuid: ^8.3.2 - zod: ^3.22.2 + wonka: ^4.0.15 + zod: 3.21.4 languageName: unknown linkType: soft @@ -17659,37 +17689,48 @@ __metadata: languageName: node linkType: hard -"next@npm:13.5.3": - version: 13.5.3 - resolution: "next@npm:13.5.3" - dependencies: - "@next/env": 13.5.3 - "@next/swc-darwin-arm64": 13.5.3 - "@next/swc-darwin-x64": 13.5.3 - "@next/swc-linux-arm64-gnu": 13.5.3 - "@next/swc-linux-arm64-musl": 13.5.3 - "@next/swc-linux-x64-gnu": 13.5.3 - "@next/swc-linux-x64-musl": 13.5.3 - "@next/swc-win32-arm64-msvc": 13.5.3 - "@next/swc-win32-ia32-msvc": 13.5.3 - "@next/swc-win32-x64-msvc": 13.5.3 - "@swc/helpers": 0.5.2 - busboy: 1.6.0 +"next@npm:13.2.4": + version: 13.2.4 + resolution: "next@npm:13.2.4" + dependencies: + "@next/env": 13.2.4 + "@next/swc-android-arm-eabi": 13.2.4 + "@next/swc-android-arm64": 13.2.4 + "@next/swc-darwin-arm64": 13.2.4 + "@next/swc-darwin-x64": 13.2.4 + "@next/swc-freebsd-x64": 13.2.4 + "@next/swc-linux-arm-gnueabihf": 13.2.4 + "@next/swc-linux-arm64-gnu": 13.2.4 + "@next/swc-linux-arm64-musl": 13.2.4 + "@next/swc-linux-x64-gnu": 13.2.4 + "@next/swc-linux-x64-musl": 13.2.4 + "@next/swc-win32-arm64-msvc": 13.2.4 + "@next/swc-win32-ia32-msvc": 13.2.4 + "@next/swc-win32-x64-msvc": 13.2.4 + "@swc/helpers": 0.4.14 caniuse-lite: ^1.0.30001406 postcss: 8.4.14 styled-jsx: 5.1.1 - watchpack: 2.4.0 - zod: 3.21.4 peerDependencies: - "@opentelemetry/api": ^1.1.0 + "@opentelemetry/api": ^1.4.0 + fibers: ">= 3.1.0" + node-sass: ^6.0.0 || ^7.0.0 react: ^18.2.0 react-dom: ^18.2.0 sass: ^1.3.0 dependenciesMeta: + "@next/swc-android-arm-eabi": + optional: true + "@next/swc-android-arm64": + optional: true "@next/swc-darwin-arm64": optional: true "@next/swc-darwin-x64": optional: true + "@next/swc-freebsd-x64": + optional: true + "@next/swc-linux-arm-gnueabihf": + optional: true "@next/swc-linux-arm64-gnu": optional: true "@next/swc-linux-arm64-musl": @@ -17707,11 +17748,15 @@ __metadata: peerDependenciesMeta: "@opentelemetry/api": optional: true + fibers: + optional: true + node-sass: + optional: true sass: optional: true bin: next: dist/bin/next - checksum: bdf97002aee33e03859bc00c6b4115956c449e33ad5a8060ff6a6bcd1f32405fc3f0d0464c293ac94a45753f3d6da513af2cb7fe730e37163f7b0dda0567ac12 + checksum: 8531dee41b60181b582f5ee80858907b102f083ef8808ff9352d589dd39e6b3a96f7a11b3776a03eef3a28430cff768336fa2e3ff2c6f8fcd699fbc891749051 languageName: node linkType: hard @@ -22186,13 +22231,6 @@ __metadata: languageName: node linkType: hard -"streamsearch@npm:^1.1.0": - version: 1.1.0 - resolution: "streamsearch@npm:1.1.0" - checksum: 1cce16cea8405d7a233d32ca5e00a00169cc0e19fbc02aa839959985f267335d435c07f96e5e0edd0eadc6d39c98d5435fb5bbbdefc62c41834eadc5622ad942 - languageName: node - linkType: hard - "strict-uri-encode@npm:^2.0.0": version: 2.0.0 resolution: "strict-uri-encode@npm:2.0.0" @@ -24617,16 +24655,6 @@ __metadata: languageName: node linkType: hard -"watchpack@npm:2.4.0": - version: 2.4.0 - resolution: "watchpack@npm:2.4.0" - dependencies: - glob-to-regexp: ^0.4.1 - graceful-fs: ^4.1.2 - checksum: 23d4bc58634dbe13b86093e01c6a68d8096028b664ab7139d58f0c37d962d549a940e98f2f201cecdabd6f9c340338dc73ef8bf094a2249ef582f35183d1a131 - languageName: node - linkType: hard - "wcwidth@npm:^1.0.0, wcwidth@npm:^1.0.1": version: 1.0.1 resolution: "wcwidth@npm:1.0.1" @@ -24892,7 +24920,7 @@ __metadata: languageName: node linkType: hard -"wonka@npm:^4.0.14": +"wonka@npm:^4.0.14, wonka@npm:^4.0.15": version: 4.0.15 resolution: "wonka@npm:4.0.15" checksum: afbee7359ed2d0a9146bf682f3953cb093f47d5f827e767e6ef33cb70ca6f30631afe5fe31dbb8d6c7eaed26c4ac6426e7c13568917c017ef6f42c71139b38f7 @@ -25315,13 +25343,6 @@ __metadata: languageName: node linkType: hard -"zod@npm:^3.22.2": - version: 3.22.2 - resolution: "zod@npm:3.22.2" - checksum: 231e2180c8eabb56e88680d80baff5cf6cbe6d64df3c44c50ebe52f73081ecd0229b1c7215b9552537f537a36d9e36afac2737ddd86dc14e3519bdbc777e82b9 - languageName: node - linkType: hard - "zwitch@npm:^1.0.0": version: 1.0.5 resolution: "zwitch@npm:1.0.5"