Skip to content

Commit

Permalink
feat:1007 admin liste des contenus modifiés lors dune maj des données…
Browse files Browse the repository at this point in the history
… - page info (#1013)

* 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 <[email protected]>

* 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 <[email protected]>
  • Loading branch information
Viczei and Victor Zeinstra authored Oct 6, 2023
1 parent 529ffb4 commit 195dc17
Show file tree
Hide file tree
Showing 80 changed files with 3,394 additions and 142 deletions.
1 change: 1 addition & 0 deletions shared/graphql-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
6 changes: 4 additions & 2 deletions targets/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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",
Expand All @@ -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:^",
Expand Down
32 changes: 32 additions & 0 deletions targets/frontend/src/components/forms/Checkbox/index.tsx
Original file line number Diff line number Diff line change
@@ -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<CommonFormProps>;
export const FormCheckbox = ({
name,
rules,
label,
control,
disabled,
}: FormCheckboxProps) => {
return (
<Controller
name={name}
control={control}
rules={rules}
render={({ field: { onChange, value } }) => {
return (
<FormGroup>
<FormControlLabel
control={<Checkbox onChange={onChange} checked={value} />}
label={label}
disabled={disabled}
/>
</FormGroup>
);
}}
/>
);
};
45 changes: 23 additions & 22 deletions targets/frontend/src/components/forms/RadioGroup/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,28 +34,29 @@ export const FormRadioGroup = ({
name={name}
control={control}
rules={rules}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<FormControl fullWidth={fullWidth} error={!!error}>
<FormLabel>{label}</FormLabel>

<RadioGroup
value={value}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
onChange(event.target.value)
}
>
{options.map(({ label, value }) => (
<FormControlLabel
key={value}
value={value}
control={<Radio />}
label={label}
disabled={disabled}
/>
))}
</RadioGroup>
</FormControl>
)}
render={({ field: { onChange, value }, fieldState: { error } }) => {
return (
<FormControl fullWidth={fullWidth} error={!!error}>
<FormLabel>{label}</FormLabel>
<RadioGroup
value={value}
onChange={(event: React.ChangeEvent<HTMLInputElement>) =>
onChange(event.target.value)
}
>
{options.map(({ label, value }) => (
<FormControlLabel
key={value}
value={value}
control={<Radio />}
label={label}
disabled={disabled}
/>
))}
</RadioGroup>
</FormControl>
);
}}
/>
);
};
34 changes: 17 additions & 17 deletions targets/frontend/src/components/forms/TextField/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,23 +32,23 @@ export const FormTextField = ({
name={name}
control={control}
rules={rules}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<TextField
helperText={
error && error.type === "required" ? "Ce champ est requis" : null
}
size={size}
error={!!error}
onChange={onChange}
value={value}
fullWidth={fullWidth}
label={label}
variant="outlined"
multiline={multiline}
disabled={disabled}
InputLabelProps={labelFixed ? { shrink: true } : {}}
/>
)}
render={({ field: { onChange, value }, fieldState: { error } }) => {
return (
<TextField
helperText={error?.message}
size={size}
error={!!error}
onChange={onChange}
value={value}
fullWidth={fullWidth}
label={label}
variant="outlined"
multiline={multiline}
disabled={disabled}
InputLabelProps={labelFixed ? { shrink: true } : {}}
/>
);
}}
/>
);
};
2 changes: 1 addition & 1 deletion targets/frontend/src/components/layout/Navigation.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export function Navigation() {
label: "Contenus",
},
{
href: "/contenus?source=information",
href: "/informations",
label: "Contenus éditoriaux",
},
{
Expand Down
34 changes: 34 additions & 0 deletions targets/frontend/src/lib/__tests__/mutationUtils.test.ts
Original file line number Diff line number Diff line change
@@ -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);
});
});
74 changes: 74 additions & 0 deletions targets/frontend/src/lib/api/ApiClient.ts
Original file line number Diff line number Diff line change
@@ -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<Data = any, Variables extends object = {}>(
query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
variables?: Variables,
context?: Partial<OperationContext>
): Promise<OperationResult<Data, Variables>> {
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<Data = any, Variables extends object = {}>(
query: DocumentNode | TypedDocumentNode<Data, Variables> | string,
variables?: Variables,
context?: Partial<OperationContext>
): Promise<OperationResult<Data, Variables>> {
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;
}
}
29 changes: 29 additions & 0 deletions targets/frontend/src/lib/api/ApiErrors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
interface ErrorWithCause<T> {
name: T;
message: string;
cause: any;
}

export class ErrorBase<T extends string> extends Error {
name: T;
message: string;
cause: any;

constructor(error: ErrorWithCause<T>) {
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 });
}
}
2 changes: 2 additions & 0 deletions targets/frontend/src/lib/api/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from "./ApiClient";
export * from "./ApiErrors";
9 changes: 0 additions & 9 deletions targets/frontend/src/lib/apiError.js

This file was deleted.

15 changes: 15 additions & 0 deletions targets/frontend/src/lib/apiError.ts
Original file line number Diff line number Diff line change
@@ -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);
}
26 changes: 26 additions & 0 deletions targets/frontend/src/lib/mutationUtils.ts
Original file line number Diff line number Diff line change
@@ -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);
};
Loading

0 comments on commit 195dc17

Please sign in to comment.