From cef3964ec54eb2ede887609987d72cbb8760693e Mon Sep 17 00:00:00 2001 From: Thomas Dax Date: Mon, 26 Feb 2024 12:19:05 +0100 Subject: [PATCH 01/18] Enable focus aware polling also in development mode (#1748) This makes it easier to discover bugs like https://github.com/vivid-planet/comet/pull/1745 already in development --- .../cms-admin/src/pages/pageTreeSelect/PageTreeSelectDialog.tsx | 2 +- packages/admin/cms-admin/src/pages/pagesPage/PagesPage.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/admin/cms-admin/src/pages/pageTreeSelect/PageTreeSelectDialog.tsx b/packages/admin/cms-admin/src/pages/pageTreeSelect/PageTreeSelectDialog.tsx index 944bb65127..0d1d754151 100644 --- a/packages/admin/cms-admin/src/pages/pageTreeSelect/PageTreeSelectDialog.tsx +++ b/packages/admin/cms-admin/src/pages/pageTreeSelect/PageTreeSelectDialog.tsx @@ -97,7 +97,7 @@ export default function PageTreeSelectDialog({ value, onChange, open, onClose, d }); useFocusAwarePolling({ - pollInterval: process.env.NODE_ENV === "development" ? undefined : 10000, + pollInterval: 10000, skip: !open, refetch, startPolling, diff --git a/packages/admin/cms-admin/src/pages/pagesPage/PagesPage.tsx b/packages/admin/cms-admin/src/pages/pagesPage/PagesPage.tsx index 31e2ff66b2..0a4c3b6f85 100644 --- a/packages/admin/cms-admin/src/pages/pagesPage/PagesPage.tsx +++ b/packages/admin/cms-admin/src/pages/pagesPage/PagesPage.tsx @@ -79,7 +79,7 @@ export function PagesPage({ }); useFocusAwarePolling({ - pollInterval: process.env.NODE_ENV === "development" ? undefined : 10000, + pollInterval: 10000, refetch, startPolling, stopPolling, From 9f9c16062364247d4f5e3ca008602a6ab690fd72 Mon Sep 17 00:00:00 2001 From: Phillip <69114037+Flips2001@users.noreply.github.com> Date: Mon, 26 Feb 2024 15:18:53 +0100 Subject: [PATCH 02/18] Fix a typo in Admin Generator and Demo (#1754) Changed `mutationReponse` to `mutationResponse` in admin generator and in demo files. --------- Co-authored-by: Phillip Lechenauer --- demo/admin/src/news/generated/NewsForm.tsx | 4 ++-- demo/admin/src/products/ProductForm.tsx | 4 ++-- demo/admin/src/products/categories/ProductCategoryForm.tsx | 4 ++-- demo/admin/src/products/generated/ProductForm.tsx | 4 ++-- demo/admin/src/products/tags/ProductTagForm.tsx | 4 ++-- packages/admin/cms-admin/src/generator/generateForm.ts | 4 ++-- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/demo/admin/src/news/generated/NewsForm.tsx b/demo/admin/src/news/generated/NewsForm.tsx index 6debd6a3d6..830a346df5 100644 --- a/demo/admin/src/news/generated/NewsForm.tsx +++ b/demo/admin/src/news/generated/NewsForm.tsx @@ -119,12 +119,12 @@ export function NewsForm({ id }: FormProps): React.ReactElement { variables: { id, input: output, lastUpdatedAt: data?.news?.updatedAt }, }); } else { - const { data: mutationReponse } = await client.mutate({ + const { data: mutationResponse } = await client.mutate({ mutation: createNewsMutation, variables: { scope, input: output }, }); if (!event.navigatingBack) { - const id = mutationReponse?.createNews.id; + const id = mutationResponse?.createNews.id; if (id) { setTimeout(() => { stackSwitchApi.activatePage("edit", id); diff --git a/demo/admin/src/products/ProductForm.tsx b/demo/admin/src/products/ProductForm.tsx index cfcc30746a..efaba151ac 100644 --- a/demo/admin/src/products/ProductForm.tsx +++ b/demo/admin/src/products/ProductForm.tsx @@ -117,12 +117,12 @@ function ProductForm({ id }: FormProps): React.ReactElement { variables: { id, input: output, lastUpdatedAt: data?.product.updatedAt }, }); } else { - const { data: mutationReponse } = await client.mutate({ + const { data: mutationResponse } = await client.mutate({ mutation: createProductMutation, variables: { input: output }, }); if (!event.navigatingBack) { - const id = mutationReponse?.createProduct.id; + const id = mutationResponse?.createProduct.id; if (id) { setTimeout(() => { stackSwitchApi.activatePage(`edit`, id); diff --git a/demo/admin/src/products/categories/ProductCategoryForm.tsx b/demo/admin/src/products/categories/ProductCategoryForm.tsx index 9e74209527..0135657548 100644 --- a/demo/admin/src/products/categories/ProductCategoryForm.tsx +++ b/demo/admin/src/products/categories/ProductCategoryForm.tsx @@ -98,7 +98,7 @@ function ProductCategoryForm({ id }: FormProps): React.ReactElement { variables: { id, input: output, lastUpdatedAt: data?.productCategory.updatedAt }, }); } else { - const { data: mutationReponse } = await client.mutate< + const { data: mutationResponse } = await client.mutate< GQLProductCategoryFormCreateProductCategoryMutation, GQLProductCategoryFormCreateProductCategoryMutationVariables >({ @@ -106,7 +106,7 @@ function ProductCategoryForm({ id }: FormProps): React.ReactElement { variables: { input: output }, }); if (!event.navigatingBack) { - const id = mutationReponse?.createProductCategory.id; + const id = mutationResponse?.createProductCategory.id; if (id) { setTimeout(() => { stackSwitchApi.activatePage(`edit`, id); diff --git a/demo/admin/src/products/generated/ProductForm.tsx b/demo/admin/src/products/generated/ProductForm.tsx index eedbf4c97f..cf55878280 100644 --- a/demo/admin/src/products/generated/ProductForm.tsx +++ b/demo/admin/src/products/generated/ProductForm.tsx @@ -113,12 +113,12 @@ export function ProductForm({ id }: FormProps): React.ReactElement { variables: { id, input: output, lastUpdatedAt: data?.product?.updatedAt }, }); } else { - const { data: mutationReponse } = await client.mutate({ + const { data: mutationResponse } = await client.mutate({ mutation: createProductMutation, variables: { input: output }, }); if (!event.navigatingBack) { - const id = mutationReponse?.createProduct.id; + const id = mutationResponse?.createProduct.id; if (id) { setTimeout(() => { stackSwitchApi.activatePage("edit", id); diff --git a/demo/admin/src/products/tags/ProductTagForm.tsx b/demo/admin/src/products/tags/ProductTagForm.tsx index f246049be9..d0dea723e0 100644 --- a/demo/admin/src/products/tags/ProductTagForm.tsx +++ b/demo/admin/src/products/tags/ProductTagForm.tsx @@ -95,7 +95,7 @@ function ProductTagForm({ id }: FormProps): React.ReactElement { variables: { id, input: output, lastUpdatedAt: data?.productTag.updatedAt }, }); } else { - const { data: mutationReponse } = await client.mutate< + const { data: mutationResponse } = await client.mutate< GQLProductTagFormCreateProductTagMutation, GQLProductTagFormCreateProductTagMutationVariables >({ @@ -103,7 +103,7 @@ function ProductTagForm({ id }: FormProps): React.ReactElement { variables: { input: output }, }); if (!event.navigatingBack) { - const id = mutationReponse?.createProductTag.id; + const id = mutationResponse?.createProductTag.id; if (id) { setTimeout(() => { stackSwitchApi.activatePage(`edit`, id); diff --git a/packages/admin/cms-admin/src/generator/generateForm.ts b/packages/admin/cms-admin/src/generator/generateForm.ts index 6b97384154..ea258badc3 100644 --- a/packages/admin/cms-admin/src/generator/generateForm.ts +++ b/packages/admin/cms-admin/src/generator/generateForm.ts @@ -246,12 +246,12 @@ export async function writeCrudForm(generatorConfig: CrudGeneratorConfig, schema variables: { id, input: output, lastUpdatedAt: data?.${instanceEntityName}?.updatedAt }, }); } else { - const { data: mutationReponse } = await client.mutate({ + const { data: mutationResponse } = await client.mutate({ mutation: create${entityName}Mutation, variables: { ${hasScope ? `scope, ` : ""}input: output }, }); if (!event.navigatingBack) { - const id = mutationReponse?.create${entityName}.id; + const id = mutationResponse?.create${entityName}.id; if (id) { setTimeout(() => { stackSwitchApi.activatePage("edit", id); From ad153c99ba4bfa696324b9d2783f6363f9d45fbd Mon Sep 17 00:00:00 2001 From: Thomas Dax Date: Mon, 26 Feb 2024 15:23:04 +0100 Subject: [PATCH 03/18] Fix bug where PDFs can't be included in iFrame by always using preview URLs (#1749) ## Problem PDFs weren't shown in the DAM preview in our client projects: Bildschirmfoto 2024-02-24 um 14 13 23 ## Reason In our starter (and our projects) we use [helmet](https://www.npmjs.com/package/helmet) to secure our API: https://github.com/vivid-planet/comet-starter/blob/main/api/src/main.ts#L48-L52 Helmet adds an [X-Frame-Options](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Frame-Options): SAMEORIGIN header to the response: Bildschirmfoto 2024-02-24 um 14 15 27 This header prevents including a PDF in an iFrame. Since helmet was only added to our starter but never to our demo project, this didn't strike in development. ## Solution Including the PDF works if the admin and API run on the same domain. This is the case (in deployed envs) if preview URLs are used because they are routed through the authproxy. Therefore, I now ensured that only preview URLs are used in the admin by - also adding the x-preview-dam-urls header to our axios client - passing the header on to `createFileUrl()` in `FilesController` and `FilesResolver` I already intended to do this in https://github.com/vivid-planet/comet/pull/1503/files but I didn't test throughly enough. I missed that the header wasn't passed to the service everywhere it's used. ### Dev Mode In dev mode we don't have an authproxy, meaning API and Admin URL are always different. As a workaround I added a [frame-ancestors CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy/frame-ancestors) that allows including the files in localhost. This isn't ideal because it's different behavior in local and deployed envs. But I couldn't come up with a better solution. Bildschirmfoto 2024-02-26 um 09 54 18 ### Alternative Alternatively, we could keep delivering the files via the public API url and always add a frame-ancestors CSP allowing the Admin URL. Since we decided in the past that we always want to use preview URLs in the admin, I opted for the other solution. --- .changeset/many-rocks-design.md | 7 ++ .changeset/olive-waves-matter.md | 9 +++ demo/api/package.json | 1 + demo/api/src/config/config.ts | 1 + demo/api/src/config/environment-variables.ts | 3 + demo/api/src/main.ts | 14 ++++ .../cms-admin/src/http/createHttpClient.ts | 1 + .../cms-api/src/dam/files/files.controller.ts | 8 ++- .../cms-api/src/dam/files/files.resolver.ts | 7 +- pnpm-lock.yaml | 72 ++++++++++++++++--- 10 files changed, 108 insertions(+), 15 deletions(-) create mode 100644 .changeset/many-rocks-design.md create mode 100644 .changeset/olive-waves-matter.md diff --git a/.changeset/many-rocks-design.md b/.changeset/many-rocks-design.md new file mode 100644 index 0000000000..f6aecbc4e7 --- /dev/null +++ b/.changeset/many-rocks-design.md @@ -0,0 +1,7 @@ +--- +"@comet/cms-admin": patch +--- + +Add the `x-preview-dam-urls` header to our axios client + +Now the axios client always requests preview DAM urls just like the GraphQL client. diff --git a/.changeset/olive-waves-matter.md b/.changeset/olive-waves-matter.md new file mode 100644 index 0000000000..927b5f9181 --- /dev/null +++ b/.changeset/olive-waves-matter.md @@ -0,0 +1,9 @@ +--- +"@comet/cms-api": patch +--- + +Always use preview DAM URLs in the admin application + +This fixes a bug where the PDF preview in the DAM wouldn't work because the file couldn't be included in an iFrame on the admin domain. + +We already intended to use preview URLs everywhere in [v5.3.0](https://github.com/vivid-planet/comet/releases/tag/v5.3.0#:~:text=Always%20use%20the%20/preview%20file%20URLs%20in%20the%20admin%20application). However, the `x-preview-dam-urls` header wasn't passed correctly to the `createFileUrl()` method everywhere. As a result, preview URLs were only used in blocks but not in the DAM. Now, the DAM uses preview URLs as well. diff --git a/demo/api/package.json b/demo/api/package.json index 0f9ac4d176..4078dd9aa2 100644 --- a/demo/api/package.json +++ b/demo/api/package.json @@ -63,6 +63,7 @@ "graphql": "^15.0.0", "graphql-type-json": "^0.3.0", "hasha": "^5.0.0", + "helmet": "^4.6.0", "image-size": "^0.9.0", "mime": "^2.0.0", "multer": "^1.0.0", diff --git a/demo/api/src/config/config.ts b/demo/api/src/config/config.ts index fb2ee6b1c0..1a97206ac2 100644 --- a/demo/api/src/config/config.ts +++ b/demo/api/src/config/config.ts @@ -17,6 +17,7 @@ export function createConfig(processEnv: NodeJS.ProcessEnv) { helmRelease: envVars.HELM_RELEASE, apiUrl: envVars.API_URL, apiPort: envVars.API_PORT, + adminUrl: envVars.ADMIN_URL, corsAllowedOrigins: envVars.CORS_ALLOWED_ORIGINS.split(","), imgproxy: { ...cometConfig.imgproxy, diff --git a/demo/api/src/config/environment-variables.ts b/demo/api/src/config/environment-variables.ts index bb2f51399e..7df3ec272f 100644 --- a/demo/api/src/config/environment-variables.ts +++ b/demo/api/src/config/environment-variables.ts @@ -32,6 +32,9 @@ export class EnvironmentVariables { @IsString() API_URL: string; + @IsString() + ADMIN_URL: string; + @Type(() => Number) @IsInt() API_PORT: number; diff --git a/demo/api/src/main.ts b/demo/api/src/main.ts index a24e8fe5b9..caf70212bd 100644 --- a/demo/api/src/main.ts +++ b/demo/api/src/main.ts @@ -1,3 +1,5 @@ +import helmet from "helmet"; + if (process.env.TRACING_ENABLED) { require("./tracing"); } @@ -39,6 +41,18 @@ async function bootstrap(): Promise { }), ); + app.use( + helmet({ + contentSecurityPolicy: { + useDefaults: false, + directives: { + "default-src": helmet.contentSecurityPolicy.dangerouslyDisableDefaultSrc, + // locally: allow localhost in frame-ancestors to enable including files from the API in iframes in admin + "frame-ancestors": `'self' ${process.env.NODE_ENV === "development" ? config.adminUrl : ""}`, + }, + }, + }), + ); app.use(json({ limit: "1mb" })); // increase default limit of 100kb for saving large pages app.use(compression()); app.use(cookieParser()); diff --git a/packages/admin/cms-admin/src/http/createHttpClient.ts b/packages/admin/cms-admin/src/http/createHttpClient.ts index 4f5f4a94c9..58a03757fc 100644 --- a/packages/admin/cms-admin/src/http/createHttpClient.ts +++ b/packages/admin/cms-admin/src/http/createHttpClient.ts @@ -5,6 +5,7 @@ export function createHttpClient(apiUrl: string): AxiosInstance { Accept: "application/json", "Content-Type": "application/json", "X-Requested-With": "XMLHttpRequest", + "x-preview-dam-urls": "1", }; const requestInterceptor = async (config: AxiosRequestConfig) => { config.headers["x-include-invisible-content"] = ["Pages:Unpublished", "Pages:Archived", "Blocks:Invisible"]; diff --git a/packages/api/cms-api/src/dam/files/files.controller.ts b/packages/api/cms-api/src/dam/files/files.controller.ts index a1b603d3b0..dd41ec651d 100644 --- a/packages/api/cms-api/src/dam/files/files.controller.ts +++ b/packages/api/cms-api/src/dam/files/files.controller.ts @@ -1,3 +1,4 @@ +import { BaseEntity } from "@mikro-orm/core"; import { Body, Controller, @@ -66,7 +67,8 @@ export function createFilesController({ Scope: PassedScope }: { Scope?: Type { + @Headers("x-preview-dam-urls") previewDamUrls: string | undefined, + ): Promise> & { fileUrl: string }> { const transformedBody = plainToInstance(UploadFileBody, body); const errors = await validate(transformedBody, { whitelist: true, forbidNonWhitelisted: true }); @@ -80,7 +82,9 @@ export function createFilesController({ Scope: PassedScope }: { Scope?: Type String) - async fileUrl(@Parent() file: FileInterface): Promise { - return this.filesService.createFileUrl(file); + async fileUrl(@Parent() file: FileInterface, @Context("req") req: IncomingMessage): Promise { + return this.filesService.createFileUrl(file, Boolean(req.headers["x-preview-dam-urls"])); } @ResolveField(() => [File]) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 55bc879af4..01395217cd 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -537,6 +537,9 @@ importers: hasha: specifier: ^5.0.0 version: 5.2.2 + helmet: + specifier: ^4.6.0 + version: 4.6.0 image-size: specifier: ^0.9.0 version: 0.9.7 @@ -2418,7 +2421,7 @@ importers: version: 7.8.0 ts-jest: specifier: ^29.0.5 - version: 29.0.5(@babel/core@7.20.12)(jest@29.5.0)(typescript@4.9.4) + version: 29.0.5(jest@29.5.0)(typescript@4.9.4) ts-node: specifier: ^10.0.0 version: 10.9.1(@types/node@18.15.3)(typescript@4.9.4) @@ -4381,7 +4384,7 @@ packages: '@babel/traverse': 7.22.11 '@babel/types': 7.22.11 convert-source-map: 1.9.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -6851,7 +6854,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.22.14 '@babel/types': 7.22.11 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -8204,7 +8207,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 espree: 9.5.2 globals: 13.19.0 ignore: 5.2.4 @@ -9571,7 +9574,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -18408,6 +18411,17 @@ packages: ms: 2.1.3 dev: false + /debug@4.3.4: + resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + dependencies: + ms: 2.1.2 + /debug@4.3.4(supports-color@5.5.0): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -19735,7 +19749,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.1.1 @@ -21366,7 +21380,7 @@ packages: peerDependencies: graphql: '>=0.9 <0.14 || ^14.0.2 || ^15.4.0 || ^16.3.0' dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 graphql: 15.8.0 tslib: 2.4.1 transitivePeerDependencies: @@ -21679,6 +21693,11 @@ packages: resolution: {integrity: sha512-xAxZkM1dRyGV2Ou5bzMxBPNLoRCjcX+ya7KSWybQD2KwLphxsapUVK6x/02o7f4VU6GPSXch9vNY2+gkU8tYWQ==} dev: true + /helmet@4.6.0: + resolution: {integrity: sha512-HVqALKZlR95ROkrnesdhbbZJFi/rIVSoNq6f3jA/9u6MIbTsPh3xZwihjeI5+DO/2sOV6HMHooXcEOuwskHpTg==} + engines: {node: '>=10.0.0'} + dev: false + /hexer@1.5.0: resolution: {integrity: sha512-dyrPC8KzBzUJ19QTIo1gXNqIISRXQ0NwteW6OeQHRN4ZuZeHkdODfj0zHBdOlHbRY8GqbqK57C9oWSvQZizFsg==} engines: {node: '>= 0.10.x'} @@ -22879,7 +22898,7 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 istanbul-lib-coverage: 3.2.0 source-map: 0.6.1 transitivePeerDependencies: @@ -23767,7 +23786,7 @@ packages: dependencies: '@types/express': 4.17.16 '@types/jsonwebtoken': 9.0.1 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 jose: 4.11.2 limiter: 1.1.5 lru-memoizer: 2.1.4 @@ -23858,7 +23877,7 @@ packages: dependencies: colorette: 2.0.19 commander: 9.5.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4 escalade: 3.1.1 esm: 3.2.25 get-package-type: 0.1.0 @@ -30252,6 +30271,39 @@ packages: yargs-parser: 21.1.1 dev: true + /ts-jest@29.0.5(jest@29.5.0)(typescript@4.9.4): + resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + hasBin: true + peerDependencies: + '@babel/core': '>=7.0.0-beta.0 <8' + '@jest/types': ^29.0.0 + babel-jest: ^29.0.0 + esbuild: '*' + jest: ^29.0.0 + typescript: '>=4.3' + peerDependenciesMeta: + '@babel/core': + optional: true + '@jest/types': + optional: true + babel-jest: + optional: true + esbuild: + optional: true + dependencies: + bs-logger: 0.2.6 + fast-json-stable-stringify: 2.1.0 + jest: 29.5.0(@types/node@18.15.3)(ts-node@10.9.1) + jest-util: 29.5.0 + json5: 2.2.3 + lodash.memoize: 4.1.2 + make-error: 1.3.6 + semver: 7.3.8 + typescript: 4.9.4 + yargs-parser: 21.1.1 + dev: true + /ts-loader@6.2.2(typescript@4.9.4): resolution: {integrity: sha512-HDo5kXZCBml3EUPcc7RlZOV/JGlLHwppTLEHb3SHnr5V7NXD4klMEkrhJe5wgRbaWsSXi+Y1SIBN/K9B6zWGWQ==} engines: {node: '>=8.6'} From 151e12188af1884ecad33f9230115ad81142ce4f Mon Sep 17 00:00:00 2001 From: Franz Unger Date: Mon, 26 Feb 2024 16:00:07 +0100 Subject: [PATCH 04/18] Support multiple `@AffectedEntity()`-decorators (#1733) Notable changes: 1. AffectedEntity()-decorator is only valid for Methods, not for classes anymore 2. Every (not at least one) affected ContentScope must apply to user-permissions (was a security hole before) 3. In entities without scope-field the @ScopedEntity-decorator is now mandatory 4. The PageTreeNode must have a scope-field when AffectedEntity is used 5. Removed equality-check when using AffectedEntity() and at the same time submitting a scope argument 6. Support array of ids submitted in args 1-4 are breaking changes, but mainly in theory. Practically no project will be affected. --------- Co-authored-by: Johannes Obermair <48853629+johnnyomair@users.noreply.github.com> --- .changeset/flat-onions-turn.md | 5 + demo/api/src/menus/main-menu-item.resolver.ts | 3 +- .../auth/user-permissions.guard.ts | 71 +++++++------- .../user-permissions/content-scope.service.ts | 98 +++++++++---------- .../decorators/affected-entity.decorator.ts | 18 ++-- 5 files changed, 101 insertions(+), 94 deletions(-) create mode 100644 .changeset/flat-onions-turn.md diff --git a/.changeset/flat-onions-turn.md b/.changeset/flat-onions-turn.md new file mode 100644 index 0000000000..5ed59252fa --- /dev/null +++ b/.changeset/flat-onions-turn.md @@ -0,0 +1,5 @@ +--- +"@comet/cms-api": minor +--- + +Support multiple `@AffectedEntity()`-decorators for a single function diff --git a/demo/api/src/menus/main-menu-item.resolver.ts b/demo/api/src/menus/main-menu-item.resolver.ts index 3f3e0a4266..f2e4c0eefa 100644 --- a/demo/api/src/menus/main-menu-item.resolver.ts +++ b/demo/api/src/menus/main-menu-item.resolver.ts @@ -17,7 +17,6 @@ import { MainMenuItem } from "./entities/main-menu-item.entity"; @Resolver(() => MainMenuItem) @RequiredPermission(["pageTree"]) -@AffectedEntity(MainMenuItem, { pageTreeNodeIdArg: "pageTreeNodeId" }) export class MainMenuItemResolver { constructor( @InjectRepository(MainMenuItem) private readonly mainMenuItemRepository: EntityRepository, @@ -25,6 +24,7 @@ export class MainMenuItemResolver { ) {} @Query(() => MainMenuItem) + @AffectedEntity(MainMenuItem, { pageTreeNodeIdArg: "pageTreeNodeId" }) async mainMenuItem( @Args("pageTreeNodeId", { type: () => ID }) pageTreeNodeId: string, @RequestContext() { includeInvisiblePages }: RequestContextInterface, @@ -47,6 +47,7 @@ export class MainMenuItemResolver { } @Mutation(() => MainMenuItem) + @AffectedEntity(MainMenuItem, { pageTreeNodeIdArg: "pageTreeNodeId" }) async updateMainMenuItem( @Args("pageTreeNodeId", { type: () => ID }) pageTreeNodeId: string, @Args("input", { type: () => MainMenuItemInput }) input: MainMenuItemInput, diff --git a/packages/api/cms-api/src/user-permissions/auth/user-permissions.guard.ts b/packages/api/cms-api/src/user-permissions/auth/user-permissions.guard.ts index 5d9631a4ec..17eec92d00 100644 --- a/packages/api/cms-api/src/user-permissions/auth/user-permissions.guard.ts +++ b/packages/api/cms-api/src/user-permissions/auth/user-permissions.guard.ts @@ -5,7 +5,6 @@ import { GqlContextType, GqlExecutionContext } from "@nestjs/graphql"; import { ContentScopeService } from "../content-scope.service"; import { RequiredPermissionMetadata } from "../decorators/required-permission.decorator"; import { CurrentUser } from "../dto/current-user"; -import { ContentScope } from "../interfaces/content-scope.interface"; import { ACCESS_CONTROL_SERVICE } from "../user-permissions.constants"; import { AccessControlServiceInterface } from "../user-permissions.types"; @@ -18,52 +17,52 @@ export class UserPermissionsGuard implements CanActivate { ) {} async canActivate(context: ExecutionContext): Promise { - if (this.reflector.getAllAndOverride("disableGlobalGuard", [context.getHandler(), context.getClass()])) { - return true; - } + const location = `${context.getClass().name}::${context.getHandler().name}()`; - if (this.reflector.getAllAndOverride("publicApi", [context.getHandler(), context.getClass()])) { - return true; - } + if (this.getDecorator(context, "disableGlobalGuard")) return true; + if (this.getDecorator(context, "publicApi")) return true; - const request = - context.getType().toString() === "graphql" ? GqlExecutionContext.create(context).getContext().req : context.switchToHttp().getRequest(); - const user = request.user as CurrentUser | undefined; + const user = this.getUser(context); if (!user) return false; - const requiredPermission = this.reflector.getAllAndOverride("requiredPermission", [ - context.getHandler(), - context.getClass(), - ]); - if (!requiredPermission) { - throw new Error(`RequiredPermission decorator is missing in ${context.getClass().name}::${context.getHandler().name}()`); - } - - let requiredContentScopes: ContentScope[] | undefined; - if (!this.isResolvingGraphQLField(context) && !requiredPermission.options?.skipScopeCheck) { - requiredContentScopes = await this.contentScopeService.inferScopesFromExecutionContext(context); - if (!requiredContentScopes) { + const requiredPermission = this.getDecorator(context, "requiredPermission"); + if (!requiredPermission) throw new Error(`RequiredPermission decorator is missing in ${location}`); + const requiredPermissions = requiredPermission.requiredPermission; + if (requiredPermissions.length === 0) throw new Error(`RequiredPermission decorator has empty permissions in ${location}`); + if (this.isResolvingGraphQLField(context) || requiredPermission.options?.skipScopeCheck) { + // At least one permission is required + return requiredPermissions.some((permission) => this.accessControlService.isAllowed(user, permission)); + } else { + const requiredContentScopes = await this.contentScopeService.getScopesForPermissionCheck(context); + if (requiredContentScopes.length === 0) throw new Error( - `Could not get ContentScope. Either pass a scope-argument or add @AffectedEntity()-decorator or enable skipScopeCheck in @RequiredPermission() (${ - context.getClass().name - }::${context.getHandler().name}())`, + `Could not get content scope. Either pass a scope-argument or add an @AffectedEntity()-decorator or enable skipScopeCheck in the @RequiredPermission()-decorator of ${location}`, ); - } - } - const requiredPermissions = requiredPermission.requiredPermission; - if (requiredPermissions.length === 0) { - throw new Error(`RequiredPermission decorator has empty permissions in ${context.getClass().name}::${context.getHandler().name}()`); + // requiredContentScopes is an two level array of scopes + // The first level has to be checked with AND, the second level with OR + // The first level consists of submitted scopes and affected entities + // The only case that there is more than one scope in the second level is when the ScopedEntity returns more scopes + return requiredPermissions.some((permission) => + requiredContentScopes.every((contentScopes) => + contentScopes.some((contentScope) => this.accessControlService.isAllowed(user, permission, contentScope)), + ), + ); } - return requiredPermissions.some((permission) => - requiredContentScopes - ? requiredContentScopes.some((contentScope) => this.accessControlService.isAllowed(user, permission, contentScope)) - : this.accessControlService.isAllowed(user, permission), - ); + } + + private getUser(context: ExecutionContext): CurrentUser | undefined { + const request = + context.getType().toString() === "graphql" ? GqlExecutionContext.create(context).getContext().req : context.switchToHttp().getRequest(); + return request.user as CurrentUser | undefined; + } + + private getDecorator(context: ExecutionContext, decorator: string): T { + return this.reflector.getAllAndOverride(decorator, [context.getClass(), context.getHandler()]); } // See https://docs.nestjs.com/graphql/other-features#execute-enhancers-at-the-field-resolver-level - isResolvingGraphQLField(context: ExecutionContext): boolean { + private isResolvingGraphQLField(context: ExecutionContext): boolean { if (context.getType() === "graphql") { const gqlContext = GqlExecutionContext.create(context); const info = gqlContext.getInfo(); diff --git a/packages/api/cms-api/src/user-permissions/content-scope.service.ts b/packages/api/cms-api/src/user-permissions/content-scope.service.ts index 689863142e..00cb075cbf 100644 --- a/packages/api/cms-api/src/user-permissions/content-scope.service.ts +++ b/packages/api/cms-api/src/user-permissions/content-scope.service.ts @@ -1,4 +1,4 @@ -import { EntityClass, MikroORM } from "@mikro-orm/core"; +import { MikroORM } from "@mikro-orm/core"; import { ExecutionContext, Injectable, Optional } from "@nestjs/common"; import { Reflector } from "@nestjs/core"; import { GqlExecutionContext } from "@nestjs/graphql"; @@ -9,6 +9,7 @@ import { ScopedEntityMeta } from "../user-permissions/decorators/scoped-entity.d import { ContentScope } from "../user-permissions/interfaces/content-scope.interface"; import { AffectedEntityMeta } from "./decorators/affected-entity.decorator"; +// TODO Remove service and move into UserPermissionsGuard once ChangesCheckerInterceptor is removed @Injectable() export class ContentScopeService { constructor(private reflector: Reflector, private readonly orm: MikroORM, @Optional() private readonly pageTreeService?: PageTreeService) {} @@ -21,63 +22,60 @@ export class ContentScopeService { return isEqual({ ...scope1 }, { ...scope2 }); } - async inferScopeFromExecutionContext(context: ExecutionContext): Promise { + async getScopesForPermissionCheck(context: ExecutionContext): Promise { + const contentScopes: ContentScope[][] = []; const args = await this.getArgs(context); - const affectedEntity = this.reflector.getAllAndOverride("affectedEntity", [context.getHandler(), context.getClass()]); - if (affectedEntity) { - let contentScope: ContentScope | ContentScope[] | undefined; - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const repo = this.orm.em.getRepository(affectedEntity.entity); - if (affectedEntity.options.idArg) { - if (!args[affectedEntity.options.idArg]) { - throw new Error(`${affectedEntity.options.idArg} arg not found`); - } - const row = await repo.findOneOrFail(args[affectedEntity.options.idArg]); + const affectedEntities = this.reflector.getAllAndOverride("affectedEntities", [context.getHandler()]) || []; + for (const affectedEntity of affectedEntities) { + contentScopes.push(...(await this.getContentScopesFromEntity(affectedEntity, args))); + } + if (args.scope) { + contentScopes.push([args.scope as ContentScope]); + } + return contentScopes; + } + + async inferScopesFromExecutionContext(context: ExecutionContext): Promise { + return [...new Set((await this.getScopesForPermissionCheck(context)).flat())]; + } + + private async getContentScopesFromEntity(affectedEntity: AffectedEntityMeta, args: Record): Promise { + const contentScopes: ContentScope[][] = []; + if (affectedEntity.options.idArg) { + if (!args[affectedEntity.options.idArg]) throw new Error(`${affectedEntity.options.idArg} arg not found`); + const repo = this.orm.em.getRepository<{ scope?: ContentScope }>(affectedEntity.entity); + const id = args[affectedEntity.options.idArg]; + const ids = Array.isArray(id) ? id : [id]; + for (const id of ids) { + const row = await repo.findOneOrFail(id); if (row.scope) { - contentScope = row.scope; + contentScopes.push([row.scope as ContentScope]); } else { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const scoped = this.reflector.getAllAndOverride("scopedEntity", [ - affectedEntity.entity as EntityClass, - ]); - if (!scoped) { - return undefined; - } - contentScope = await scoped.fn(row); - } - } else if (affectedEntity.options.pageTreeNodeIdArg && args[affectedEntity.options.pageTreeNodeIdArg]) { - if (!args[affectedEntity.options.pageTreeNodeIdArg]) { - throw new Error(`${affectedEntity.options.pageTreeNodeIdArg} arg not found`); + const scoped = this.reflector.getAllAndOverride("scopedEntity", [affectedEntity.entity]); + if (!scoped) throw new Error(`Entity ${affectedEntity.entity} is missing @ScopedEntity decorator`); + const scopes = await scoped.fn(row); + if (!scopes) throw new Error(`@ScopedEntity function for ${affectedEntity.entity} didn't return any scopes`); + contentScopes.push(Array.isArray(scopes) ? scopes : [scopes]); } - if (this.pageTreeService === undefined) { - throw new Error("pageTreeNodeIdArg was given but no PageTreeModule is registered"); - } - const node = await this.pageTreeService.createReadApi({ visibility: "all" }).getNode(args[affectedEntity.options.pageTreeNodeIdArg]); - if (!node) throw new Error("Can't find pageTreeNode"); - contentScope = node.scope; - } else { - // TODO implement something more flexible that supports something like that: @AffectedEntity(Product, ProductEntityLoader) - throw new Error("idArg or pageTreeNodeIdArg is required"); } - if (contentScope === undefined) throw new Error("Scope not found"); - if (args.scope) { - // args.scope also exists, check if they match - if (!isEqual(args.scope, contentScope)) { - throw new Error("Content Scope from arg doesn't match affectedEntity scope, usually you only need one of them"); - } + } else if (affectedEntity.options.pageTreeNodeIdArg && args[affectedEntity.options.pageTreeNodeIdArg]) { + if (!args[affectedEntity.options.pageTreeNodeIdArg]) throw new Error(`${affectedEntity.options.pageTreeNodeIdArg} arg not found`); + if (this.pageTreeService === undefined) throw new Error("pageTreeNodeIdArg was given but no PageTreeModule is registered"); + const pageTreeApi = await this.pageTreeService.createReadApi({ visibility: "all" }); + const id = args[affectedEntity.options.pageTreeNodeIdArg]; + const ids = Array.isArray(id) ? id : [id]; + for (const id of ids) { + const node = await pageTreeApi.getNode(id); + if (!node) throw new Error("Can't find pageTreeNode"); + if (!node.scope) throw new Error("PageTreeNode doesn't have a scope"); + contentScopes.push([node.scope as ContentScope]); } - return contentScope; - } - if (args.scope) { - return args.scope; + } else { + // TODO implement something more flexible that supports something like that: @AffectedEntity(Product, ProductEntityLoader) + throw new Error("idArg or pageTreeNodeIdArg is required"); } - } - - async inferScopesFromExecutionContext(context: ExecutionContext): Promise { - const scope = await this.inferScopeFromExecutionContext(context); - if (scope === undefined) return scope; - return Array.isArray(scope) ? scope : [scope]; + return contentScopes; } // eslint-disable-next-line @typescript-eslint/no-explicit-any diff --git a/packages/api/cms-api/src/user-permissions/decorators/affected-entity.decorator.ts b/packages/api/cms-api/src/user-permissions/decorators/affected-entity.decorator.ts index 6ad3e86520..5454b3782a 100644 --- a/packages/api/cms-api/src/user-permissions/decorators/affected-entity.decorator.ts +++ b/packages/api/cms-api/src/user-permissions/decorators/affected-entity.decorator.ts @@ -1,20 +1,24 @@ -import { EntityName } from "@mikro-orm/core"; -import { CustomDecorator, SetMetadata } from "@nestjs/common"; +import { EntityClass, EntityName } from "@mikro-orm/core"; export interface AffectedEntityOptions { idArg?: string; pageTreeNodeIdArg?: string; } -export interface AffectedEntityMeta { +export type AffectedEntityMeta = { // eslint-disable-next-line @typescript-eslint/no-explicit-any - entity: EntityName; //TODO + entity: EntityClass; options: AffectedEntityOptions; -} +}; export const AffectedEntity = ( // eslint-disable-next-line @typescript-eslint/no-explicit-any entity: EntityName, { idArg, pageTreeNodeIdArg }: AffectedEntityOptions = { idArg: "id" }, -): CustomDecorator => { - return SetMetadata("affectedEntity", { entity, options: { idArg, pageTreeNodeIdArg } }); +): MethodDecorator => { + return (target: object, key: string | symbol, descriptor: PropertyDescriptor) => { + const metadata = Reflect.getOwnMetadata("affectedEntities", descriptor.value) || []; + metadata.push({ entity, options: { idArg, pageTreeNodeIdArg } }); + Reflect.defineMetadata("affectedEntities", metadata, descriptor.value); + return descriptor; + }; }; From 71927a1d5a2793df8e62604035e4e6be8e666182 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 26 Feb 2024 16:29:44 +0100 Subject: [PATCH 05/18] Version Packages (#1736) This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @comet/cms-admin@6.2.0 ### Minor Changes - 75865caa: Deprecate `isHref` validator, `IsHref` decorator and `IsHrefConstraint` class. New versions `isLinkTarget`, `IsLinkTarget` and `IsLinkTargetConstraint` are added as replacement. ### Patch Changes - ad153c99: Add the `x-preview-dam-urls` header to our axios client Now the axios client always requests preview DAM urls just like the GraphQL client. - 5dfe4839: Prevent the document editor from losing its state when (re)gaining focus In v6.1.0 a loading indicator was added to the document editor (in `PagesPage`). This had an unwanted side effect: Focusing the edit page automatically causes a GraphQL request to check for a newer version of the document. This request also caused the loading indicator to render, thus unmounting the editor (`EditComponent`). Consequently, the local state of the editor was lost. - @comet/admin@6.2.0 - @comet/admin-date-time@6.2.0 - @comet/admin-icons@6.2.0 - @comet/admin-rte@6.2.0 - @comet/admin-theme@6.2.0 - @comet/blocks-admin@6.2.0 ## @comet/blocks-api@6.2.0 ### Minor Changes - 75865caa: Deprecate `isHref` validator, `IsHref` decorator and `IsHrefConstraint` class. New versions `isLinkTarget`, `IsLinkTarget` and `IsLinkTargetConstraint` are added as replacement. ## @comet/cms-api@6.2.0 ### Minor Changes - beeea1dd: Remove `availablePermissions`-option in `UserPermissionsModule` Simply remove the `Permission` interface module augmentation and the `availablePermissions`-option from the application. - 151e1218: Support multiple `@AffectedEntity()`-decorators for a single function ### Patch Changes - 04afb3ee: Fix attached document deletion when deleting a page tree node - ad153c99: Always use preview DAM URLs in the admin application This fixes a bug where the PDF preview in the DAM wouldn't work because the file couldn't be included in an iFrame on the admin domain. We already intended to use preview URLs everywhere in [v5.3.0](https://github.com/vivid-planet/comet/releases/tag/v5.3.0#:~:text=Always%20use%20the%20/preview%20file%20URLs%20in%20the%20admin%20application). However, the `x-preview-dam-urls` header wasn't passed correctly to the `createFileUrl()` method everywhere. As a result, preview URLs were only used in blocks but not in the DAM. Now, the DAM uses preview URLs as well. - Updated dependencies [75865caa] - @comet/blocks-api@6.2.0 ## @comet/cms-site@6.2.0 ### Minor Changes - 34bb33fe: Add `SeoBlock` Can be used as a drop-in replacement for `SeoBlock` defined in application code. Add a `resolveOpenGraphImageUrlTemplate` to resolve the correct image URL template when using a custom Open Graph image block. **Example Default Use Case:** ```tsx ``` **Example Custom Use Case:** ```tsx data={exampleData} title={"Some Example Title"} resolveOpenGraphImageUrlTemplate={(block) => block.some.path.to.urlTemplate} /> ``` ## @comet/admin@6.2.0 ### Patch Changes - @comet/admin-icons@6.2.0 ## @comet/admin-color-picker@6.2.0 ### Patch Changes - @comet/admin@6.2.0 - @comet/admin-icons@6.2.0 ## @comet/admin-date-time@6.2.0 ### Patch Changes - @comet/admin@6.2.0 - @comet/admin-icons@6.2.0 ## @comet/admin-react-select@6.2.0 ### Patch Changes - @comet/admin@6.2.0 ## @comet/admin-rte@6.2.0 ### Patch Changes - @comet/admin@6.2.0 - @comet/admin-icons@6.2.0 ## @comet/admin-theme@6.2.0 ### Patch Changes - @comet/admin-icons@6.2.0 ## @comet/blocks-admin@6.2.0 ### Patch Changes - @comet/admin@6.2.0 - @comet/admin-icons@6.2.0 ## @comet/eslint-config@6.2.0 ### Patch Changes - @comet/eslint-plugin@6.2.0 ## @comet/admin-babel-preset@6.2.0 ## @comet/admin-icons@6.2.0 ## @comet/cli@6.2.0 ## @comet/eslint-plugin@6.2.0 Co-authored-by: github-actions[bot] --- .changeset/angry-plums-retire.md | 7 - .changeset/clever-guests-sleep.md | 5 - .changeset/flat-onions-turn.md | 5 - .changeset/many-rocks-design.md | 7 - .changeset/metal-penguins-tease.md | 8 - .changeset/olive-waves-matter.md | 9 - .changeset/tame-cougars-serve.md | 21 -- .changeset/tiny-stingrays-shave.md | 8 - .../admin/admin-babel-preset/CHANGELOG.md | 2 + .../admin/admin-babel-preset/package.json | 2 +- .../admin/admin-color-picker/CHANGELOG.md | 7 + .../admin/admin-color-picker/package.json | 10 +- packages/admin/admin-date-time/CHANGELOG.md | 7 + packages/admin/admin-date-time/package.json | 10 +- packages/admin/admin-icons/CHANGELOG.md | 2 + packages/admin/admin-icons/package.json | 6 +- .../admin/admin-react-select/CHANGELOG.md | 6 + .../admin/admin-react-select/package.json | 8 +- packages/admin/admin-rte/CHANGELOG.md | 7 + packages/admin/admin-rte/package.json | 10 +- packages/admin/admin-theme/CHANGELOG.md | 6 + packages/admin/admin-theme/package.json | 8 +- packages/admin/admin/CHANGELOG.md | 6 + packages/admin/admin/package.json | 8 +- packages/admin/blocks-admin/CHANGELOG.md | 7 + packages/admin/blocks-admin/package.json | 12 +- packages/admin/cms-admin/CHANGELOG.md | 26 ++ packages/admin/cms-admin/package.json | 20 +- packages/api/blocks-api/CHANGELOG.md | 8 + packages/api/blocks-api/package.json | 4 +- packages/api/cms-api/CHANGELOG.md | 22 ++ packages/api/cms-api/package.json | 6 +- packages/cli/CHANGELOG.md | 2 + packages/cli/package.json | 4 +- packages/eslint-config/CHANGELOG.md | 6 + packages/eslint-config/package.json | 4 +- packages/eslint-plugin/CHANGELOG.md | 2 + packages/eslint-plugin/package.json | 2 +- packages/site/cms-site/CHANGELOG.md | 24 ++ packages/site/cms-site/package.json | 6 +- pnpm-lock.yaml | 227 ++++++++---------- 41 files changed, 299 insertions(+), 258 deletions(-) delete mode 100644 .changeset/angry-plums-retire.md delete mode 100644 .changeset/clever-guests-sleep.md delete mode 100644 .changeset/flat-onions-turn.md delete mode 100644 .changeset/many-rocks-design.md delete mode 100644 .changeset/metal-penguins-tease.md delete mode 100644 .changeset/olive-waves-matter.md delete mode 100644 .changeset/tame-cougars-serve.md delete mode 100644 .changeset/tiny-stingrays-shave.md diff --git a/.changeset/angry-plums-retire.md b/.changeset/angry-plums-retire.md deleted file mode 100644 index b2f2e7bc96..0000000000 --- a/.changeset/angry-plums-retire.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@comet/cms-api": minor ---- - -Remove `availablePermissions`-option in `UserPermissionsModule` - -Simply remove the `Permission` interface module augmentation and the `availablePermissions`-option from the application. diff --git a/.changeset/clever-guests-sleep.md b/.changeset/clever-guests-sleep.md deleted file mode 100644 index ba4fc9096f..0000000000 --- a/.changeset/clever-guests-sleep.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@comet/cms-api": patch ---- - -Fix attached document deletion when deleting a page tree node diff --git a/.changeset/flat-onions-turn.md b/.changeset/flat-onions-turn.md deleted file mode 100644 index 5ed59252fa..0000000000 --- a/.changeset/flat-onions-turn.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -"@comet/cms-api": minor ---- - -Support multiple `@AffectedEntity()`-decorators for a single function diff --git a/.changeset/many-rocks-design.md b/.changeset/many-rocks-design.md deleted file mode 100644 index f6aecbc4e7..0000000000 --- a/.changeset/many-rocks-design.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@comet/cms-admin": patch ---- - -Add the `x-preview-dam-urls` header to our axios client - -Now the axios client always requests preview DAM urls just like the GraphQL client. diff --git a/.changeset/metal-penguins-tease.md b/.changeset/metal-penguins-tease.md deleted file mode 100644 index a26f2162b0..0000000000 --- a/.changeset/metal-penguins-tease.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@comet/cms-admin": patch ---- - -Prevent the document editor from losing its state when (re)gaining focus - -In v6.1.0 a loading indicator was added to the document editor (in `PagesPage`). -This had an unwanted side effect: Focusing the edit page automatically causes a GraphQL request to check for a newer version of the document. This request also caused the loading indicator to render, thus unmounting the editor (`EditComponent`). Consequently, the local state of the editor was lost. diff --git a/.changeset/olive-waves-matter.md b/.changeset/olive-waves-matter.md deleted file mode 100644 index 927b5f9181..0000000000 --- a/.changeset/olive-waves-matter.md +++ /dev/null @@ -1,9 +0,0 @@ ---- -"@comet/cms-api": patch ---- - -Always use preview DAM URLs in the admin application - -This fixes a bug where the PDF preview in the DAM wouldn't work because the file couldn't be included in an iFrame on the admin domain. - -We already intended to use preview URLs everywhere in [v5.3.0](https://github.com/vivid-planet/comet/releases/tag/v5.3.0#:~:text=Always%20use%20the%20/preview%20file%20URLs%20in%20the%20admin%20application). However, the `x-preview-dam-urls` header wasn't passed correctly to the `createFileUrl()` method everywhere. As a result, preview URLs were only used in blocks but not in the DAM. Now, the DAM uses preview URLs as well. diff --git a/.changeset/tame-cougars-serve.md b/.changeset/tame-cougars-serve.md deleted file mode 100644 index fcb2a8ab9b..0000000000 --- a/.changeset/tame-cougars-serve.md +++ /dev/null @@ -1,21 +0,0 @@ ---- -"@comet/cms-site": minor ---- - -Add `SeoBlock` - -Can be used as a drop-in replacement for `SeoBlock` defined in application code. Add a `resolveOpenGraphImageUrlTemplate` to resolve the correct image URL template when using a custom Open Graph image block. - -**Example Default Use Case:** -```tsx - -``` - -**Example Custom Use Case:** -```tsx - - data={exampleData} - title={"Some Example Title"} - resolveOpenGraphImageUrlTemplate={(block) => block.some.path.to.urlTemplate} /> -``` - diff --git a/.changeset/tiny-stingrays-shave.md b/.changeset/tiny-stingrays-shave.md deleted file mode 100644 index 871a6889f7..0000000000 --- a/.changeset/tiny-stingrays-shave.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -"@comet/cms-admin": minor -"@comet/blocks-api": minor ---- - -Deprecate `isHref` validator, `IsHref` decorator and `IsHrefConstraint` class. - -New versions `isLinkTarget`, `IsLinkTarget` and `IsLinkTargetConstraint` are added as replacement. diff --git a/packages/admin/admin-babel-preset/CHANGELOG.md b/packages/admin/admin-babel-preset/CHANGELOG.md index 0880336f0a..f4d30cdc2a 100644 --- a/packages/admin/admin-babel-preset/CHANGELOG.md +++ b/packages/admin/admin-babel-preset/CHANGELOG.md @@ -1,5 +1,7 @@ # @comet/admin-babel-preset +## 6.2.0 + ## 6.1.0 ## 6.0.0 diff --git a/packages/admin/admin-babel-preset/package.json b/packages/admin/admin-babel-preset/package.json index 45a5c9db1d..5ca6ac97be 100644 --- a/packages/admin/admin-babel-preset/package.json +++ b/packages/admin/admin-babel-preset/package.json @@ -1,6 +1,6 @@ { "name": "@comet/admin-babel-preset", - "version": "6.1.0", + "version": "6.2.0", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", diff --git a/packages/admin/admin-color-picker/CHANGELOG.md b/packages/admin/admin-color-picker/CHANGELOG.md index 8d3f9109c1..3cd799dc35 100644 --- a/packages/admin/admin-color-picker/CHANGELOG.md +++ b/packages/admin/admin-color-picker/CHANGELOG.md @@ -1,5 +1,12 @@ # @comet/admin-color-picker +## 6.2.0 + +### Patch Changes + +- @comet/admin@6.2.0 +- @comet/admin-icons@6.2.0 + ## 6.1.0 ### Patch Changes diff --git a/packages/admin/admin-color-picker/package.json b/packages/admin/admin-color-picker/package.json index 17efc2b1fb..1fecdd4488 100644 --- a/packages/admin/admin-color-picker/package.json +++ b/packages/admin/admin-color-picker/package.json @@ -1,6 +1,6 @@ { "name": "@comet/admin-color-picker", - "version": "6.1.0", + "version": "6.2.0", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -25,8 +25,8 @@ "start:types": "tsc --project ./tsconfig.json --emitDeclarationOnly --watch --preserveWatchOutput" }, "dependencies": { - "@comet/admin": "workspace:^6.1.0", - "@comet/admin-icons": "workspace:^6.1.0", + "@comet/admin": "workspace:^6.2.0", + "@comet/admin-icons": "workspace:^6.2.0", "clsx": "^1.1.1", "react-colorful": "^5.5.1", "tinycolor2": "^1.4.1", @@ -35,8 +35,8 @@ "devDependencies": { "@babel/cli": "^7.17.6", "@babel/core": "^7.20.12", - "@comet/admin-babel-preset": "workspace:^6.1.0", - "@comet/eslint-config": "workspace:^6.1.0", + "@comet/admin-babel-preset": "workspace:^6.2.0", + "@comet/eslint-config": "workspace:^6.2.0", "@mui/icons-material": "^5.0.0", "@mui/material": "^5.0.0", "@mui/styles": "^5.0.0", diff --git a/packages/admin/admin-date-time/CHANGELOG.md b/packages/admin/admin-date-time/CHANGELOG.md index b994a3a14d..3d0fbeb515 100644 --- a/packages/admin/admin-date-time/CHANGELOG.md +++ b/packages/admin/admin-date-time/CHANGELOG.md @@ -1,5 +1,12 @@ # @comet/admin-date-time +## 6.2.0 + +### Patch Changes + +- @comet/admin@6.2.0 +- @comet/admin-icons@6.2.0 + ## 6.1.0 ### Patch Changes diff --git a/packages/admin/admin-date-time/package.json b/packages/admin/admin-date-time/package.json index 088ca123d7..2faccc3567 100644 --- a/packages/admin/admin-date-time/package.json +++ b/packages/admin/admin-date-time/package.json @@ -1,6 +1,6 @@ { "name": "@comet/admin-date-time", - "version": "6.1.0", + "version": "6.2.0", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -25,8 +25,8 @@ "start:types": "tsc --project ./tsconfig.json --emitDeclarationOnly --watch --preserveWatchOutput" }, "dependencies": { - "@comet/admin": "workspace:^6.1.0", - "@comet/admin-icons": "workspace:^6.1.0", + "@comet/admin": "workspace:^6.2.0", + "@comet/admin-icons": "workspace:^6.2.0", "@mui/utils": "^5.4.1", "clsx": "^1.1.1", "date-fns": "^2.28.0", @@ -35,8 +35,8 @@ "devDependencies": { "@babel/cli": "^7.17.6", "@babel/core": "^7.20.12", - "@comet/admin-babel-preset": "workspace:^6.1.0", - "@comet/eslint-config": "workspace:^6.1.0", + "@comet/admin-babel-preset": "workspace:^6.2.0", + "@comet/eslint-config": "workspace:^6.2.0", "@mui/material": "^5.0.0", "@mui/styles": "^5.0.0", "@types/react": "^17.0", diff --git a/packages/admin/admin-icons/CHANGELOG.md b/packages/admin/admin-icons/CHANGELOG.md index 8a736f54ad..6c836a3e6d 100644 --- a/packages/admin/admin-icons/CHANGELOG.md +++ b/packages/admin/admin-icons/CHANGELOG.md @@ -1,5 +1,7 @@ # @comet/admin-icons +## 6.2.0 + ## 6.1.0 ### Patch Changes diff --git a/packages/admin/admin-icons/package.json b/packages/admin/admin-icons/package.json index 9e4775a6de..b658d67309 100644 --- a/packages/admin/admin-icons/package.json +++ b/packages/admin/admin-icons/package.json @@ -1,6 +1,6 @@ { "name": "@comet/admin-icons", - "version": "6.1.0", + "version": "6.2.0", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -24,8 +24,8 @@ "devDependencies": { "@babel/cli": "^7.17.6", "@babel/core": "^7.20.12", - "@comet/admin-babel-preset": "workspace:^6.1.0", - "@comet/eslint-config": "workspace:^6.1.0", + "@comet/admin-babel-preset": "workspace:^6.2.0", + "@comet/eslint-config": "workspace:^6.2.0", "@mui/material": "^5.0.0", "@types/cli-progress": "^3.8.0", "@types/node": "^18.0.0", diff --git a/packages/admin/admin-react-select/CHANGELOG.md b/packages/admin/admin-react-select/CHANGELOG.md index 8ad745b752..d3f2aacf66 100644 --- a/packages/admin/admin-react-select/CHANGELOG.md +++ b/packages/admin/admin-react-select/CHANGELOG.md @@ -1,5 +1,11 @@ # @comet/admin-react-select +## 6.2.0 + +### Patch Changes + +- @comet/admin@6.2.0 + ## 6.1.0 ### Patch Changes diff --git a/packages/admin/admin-react-select/package.json b/packages/admin/admin-react-select/package.json index a2f4a41715..e11afd811c 100644 --- a/packages/admin/admin-react-select/package.json +++ b/packages/admin/admin-react-select/package.json @@ -1,6 +1,6 @@ { "name": "@comet/admin-react-select", - "version": "6.1.0", + "version": "6.2.0", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -25,14 +25,14 @@ "start:types": "tsc --project ./tsconfig.json --emitDeclarationOnly --watch --preserveWatchOutput" }, "dependencies": { - "@comet/admin": "workspace:^6.1.0", + "@comet/admin": "workspace:^6.2.0", "classnames": "^2.2.6" }, "devDependencies": { "@babel/cli": "^7.17.6", "@babel/core": "^7.20.12", - "@comet/admin-babel-preset": "workspace:^6.1.0", - "@comet/eslint-config": "workspace:^6.1.0", + "@comet/admin-babel-preset": "workspace:^6.2.0", + "@comet/eslint-config": "workspace:^6.2.0", "@mui/icons-material": "^5.0.0", "@mui/material": "^5.0.0", "@mui/styles": "^5.0.0", diff --git a/packages/admin/admin-rte/CHANGELOG.md b/packages/admin/admin-rte/CHANGELOG.md index 50d2887199..5c465bac5d 100644 --- a/packages/admin/admin-rte/CHANGELOG.md +++ b/packages/admin/admin-rte/CHANGELOG.md @@ -1,5 +1,12 @@ # @comet/admin-rte +## 6.2.0 + +### Patch Changes + +- @comet/admin@6.2.0 +- @comet/admin-icons@6.2.0 + ## 6.1.0 ### Minor Changes diff --git a/packages/admin/admin-rte/package.json b/packages/admin/admin-rte/package.json index efd1a189e5..de64abc602 100644 --- a/packages/admin/admin-rte/package.json +++ b/packages/admin/admin-rte/package.json @@ -1,6 +1,6 @@ { "name": "@comet/admin-rte", - "version": "6.1.0", + "version": "6.2.0", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -27,8 +27,8 @@ "test:watch": "jest --watch" }, "dependencies": { - "@comet/admin": "workspace:^6.1.0", - "@comet/admin-icons": "workspace:^6.1.0", + "@comet/admin": "workspace:^6.2.0", + "@comet/admin-icons": "workspace:^6.2.0", "detect-browser": "^5.2.1", "draft-js-export-html": "^1.4.1", "draft-js-import-html": "^1.4.1", @@ -38,8 +38,8 @@ "devDependencies": { "@babel/cli": "^7.17.6", "@babel/core": "^7.20.12", - "@comet/admin-babel-preset": "workspace:^6.1.0", - "@comet/eslint-config": "workspace:^6.1.0", + "@comet/admin-babel-preset": "workspace:^6.2.0", + "@comet/eslint-config": "workspace:^6.2.0", "@mui/icons-material": "^5.0.0", "@mui/material": "^5.0.0", "@mui/styles": "^5.0.0", diff --git a/packages/admin/admin-theme/CHANGELOG.md b/packages/admin/admin-theme/CHANGELOG.md index 2f663f5b67..4a8a7d3044 100644 --- a/packages/admin/admin-theme/CHANGELOG.md +++ b/packages/admin/admin-theme/CHANGELOG.md @@ -1,5 +1,11 @@ # @comet/admin-theme +## 6.2.0 + +### Patch Changes + +- @comet/admin-icons@6.2.0 + ## 6.1.0 ### Minor Changes diff --git a/packages/admin/admin-theme/package.json b/packages/admin/admin-theme/package.json index 17cfef313e..386c8694b4 100644 --- a/packages/admin/admin-theme/package.json +++ b/packages/admin/admin-theme/package.json @@ -1,6 +1,6 @@ { "name": "@comet/admin-theme", - "version": "6.1.0", + "version": "6.2.0", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -25,14 +25,14 @@ "start:types": "tsc --project ./tsconfig.json --emitDeclarationOnly --watch --preserveWatchOutput" }, "dependencies": { - "@comet/admin-icons": "workspace:^6.1.0", + "@comet/admin-icons": "workspace:^6.2.0", "@mui/utils": "^5.4.1" }, "devDependencies": { "@babel/cli": "^7.17.6", "@babel/core": "^7.20.12", - "@comet/admin-babel-preset": "workspace:^6.1.0", - "@comet/eslint-config": "workspace:^6.1.0", + "@comet/admin-babel-preset": "workspace:^6.2.0", + "@comet/eslint-config": "workspace:^6.2.0", "@mui/material": "^5.0.0", "@mui/styles": "^5.0.0", "@mui/system": "^5.0.0", diff --git a/packages/admin/admin/CHANGELOG.md b/packages/admin/admin/CHANGELOG.md index 0986f93038..c5352fb7de 100644 --- a/packages/admin/admin/CHANGELOG.md +++ b/packages/admin/admin/CHANGELOG.md @@ -1,5 +1,11 @@ # @comet/admin +## 6.2.0 + +### Patch Changes + +- @comet/admin-icons@6.2.0 + ## 6.1.0 ### Minor Changes diff --git a/packages/admin/admin/package.json b/packages/admin/admin/package.json index f44904c3d3..0a2ca7aff7 100644 --- a/packages/admin/admin/package.json +++ b/packages/admin/admin/package.json @@ -1,6 +1,6 @@ { "name": "@comet/admin", - "version": "6.1.0", + "version": "6.2.0", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -27,7 +27,7 @@ "test:watch": "jest --watch" }, "dependencies": { - "@comet/admin-icons": "workspace:^6.1.0", + "@comet/admin-icons": "workspace:^6.2.0", "@mui/private-theming": "^5.0.0", "clsx": "^1.1.1", "exceljs": "^3.4.0", @@ -45,8 +45,8 @@ "@apollo/client": "^3.7.0", "@babel/cli": "^7.17.6", "@babel/core": "^7.20.12", - "@comet/admin-babel-preset": "workspace:^6.1.0", - "@comet/eslint-config": "workspace:^6.1.0", + "@comet/admin-babel-preset": "workspace:^6.2.0", + "@comet/eslint-config": "workspace:^6.2.0", "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", "@mui/icons-material": "^5.0.0", diff --git a/packages/admin/blocks-admin/CHANGELOG.md b/packages/admin/blocks-admin/CHANGELOG.md index 9d6e7492d4..8ac142ab97 100644 --- a/packages/admin/blocks-admin/CHANGELOG.md +++ b/packages/admin/blocks-admin/CHANGELOG.md @@ -1,5 +1,12 @@ # @comet/blocks-admin +## 6.2.0 + +### Patch Changes + +- @comet/admin@6.2.0 +- @comet/admin-icons@6.2.0 + ## 6.1.0 ### Patch Changes diff --git a/packages/admin/blocks-admin/package.json b/packages/admin/blocks-admin/package.json index 129f07f139..c5c9dc6ee1 100644 --- a/packages/admin/blocks-admin/package.json +++ b/packages/admin/blocks-admin/package.json @@ -1,6 +1,6 @@ { "name": "@comet/blocks-admin", - "version": "6.1.0", + "version": "6.2.0", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -29,8 +29,8 @@ "test:watch": "jest --watch" }, "dependencies": { - "@comet/admin": "workspace:^6.1.0", - "@comet/admin-icons": "workspace:^6.1.0", + "@comet/admin": "workspace:^6.2.0", + "@comet/admin-icons": "workspace:^6.2.0", "@mui/lab": "^5.0.0-alpha.76", "clipboard-copy": "^4.0.0", "clsx": "^1.1.1", @@ -42,9 +42,9 @@ "devDependencies": { "@babel/cli": "^7.17.6", "@babel/core": "^7.20.12", - "@comet/admin-babel-preset": "workspace:^6.1.0", - "@comet/cli": "workspace:^6.1.0", - "@comet/eslint-config": "workspace:^6.1.0", + "@comet/admin-babel-preset": "workspace:^6.2.0", + "@comet/cli": "workspace:^6.2.0", + "@comet/eslint-config": "workspace:^6.2.0", "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", "@mui/lab": "^5.0.0-alpha.76", diff --git a/packages/admin/cms-admin/CHANGELOG.md b/packages/admin/cms-admin/CHANGELOG.md index 3e06627d0b..3c1bfadb6f 100644 --- a/packages/admin/cms-admin/CHANGELOG.md +++ b/packages/admin/cms-admin/CHANGELOG.md @@ -1,5 +1,31 @@ # @comet/cms-admin +## 6.2.0 + +### Minor Changes + +- 75865caa: Deprecate `isHref` validator, `IsHref` decorator and `IsHrefConstraint` class. + + New versions `isLinkTarget`, `IsLinkTarget` and `IsLinkTargetConstraint` are added as replacement. + +### Patch Changes + +- ad153c99: Add the `x-preview-dam-urls` header to our axios client + + Now the axios client always requests preview DAM urls just like the GraphQL client. + +- 5dfe4839: Prevent the document editor from losing its state when (re)gaining focus + + In v6.1.0 a loading indicator was added to the document editor (in `PagesPage`). + This had an unwanted side effect: Focusing the edit page automatically causes a GraphQL request to check for a newer version of the document. This request also caused the loading indicator to render, thus unmounting the editor (`EditComponent`). Consequently, the local state of the editor was lost. + + - @comet/admin@6.2.0 + - @comet/admin-date-time@6.2.0 + - @comet/admin-icons@6.2.0 + - @comet/admin-rte@6.2.0 + - @comet/admin-theme@6.2.0 + - @comet/blocks-admin@6.2.0 + ## 6.1.0 ### Patch Changes diff --git a/packages/admin/cms-admin/package.json b/packages/admin/cms-admin/package.json index 0be9360373..8aeebd0e26 100644 --- a/packages/admin/cms-admin/package.json +++ b/packages/admin/cms-admin/package.json @@ -1,6 +1,6 @@ { "name": "@comet/cms-admin", - "version": "6.1.0", + "version": "6.2.0", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -34,12 +34,12 @@ "test:watch": "jest --watch" }, "dependencies": { - "@comet/admin": "workspace:^6.1.0", - "@comet/admin-date-time": "workspace:^6.1.0", - "@comet/admin-icons": "workspace:^6.1.0", - "@comet/admin-rte": "workspace:^6.1.0", - "@comet/admin-theme": "workspace:^6.1.0", - "@comet/blocks-admin": "workspace:^6.1.0", + "@comet/admin": "workspace:^6.2.0", + "@comet/admin-date-time": "workspace:^6.2.0", + "@comet/admin-icons": "workspace:^6.2.0", + "@comet/admin-rte": "workspace:^6.2.0", + "@comet/admin-theme": "workspace:^6.2.0", + "@comet/blocks-admin": "workspace:^6.2.0", "@graphql-tools/graphql-file-loader": "^7.5.17", "@graphql-tools/load": "^7.8.14", "@graphql-typed-document-node/core": "^3.1.1", @@ -80,9 +80,9 @@ "@apollo/client": "^3.7.0", "@babel/cli": "^7.17.6", "@babel/core": "^7.20.12", - "@comet/admin-babel-preset": "workspace:^6.1.0", - "@comet/cli": "workspace:^6.1.0", - "@comet/eslint-config": "workspace:^6.1.0", + "@comet/admin-babel-preset": "workspace:^6.2.0", + "@comet/cli": "workspace:^6.2.0", + "@comet/eslint-config": "workspace:^6.2.0", "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", "@graphql-codegen/cli": "^2.0.0", diff --git a/packages/api/blocks-api/CHANGELOG.md b/packages/api/blocks-api/CHANGELOG.md index f558b4b515..57c0541533 100644 --- a/packages/api/blocks-api/CHANGELOG.md +++ b/packages/api/blocks-api/CHANGELOG.md @@ -1,5 +1,13 @@ # @comet/blocks-api +## 6.2.0 + +### Minor Changes + +- 75865caa: Deprecate `isHref` validator, `IsHref` decorator and `IsHrefConstraint` class. + + New versions `isLinkTarget`, `IsLinkTarget` and `IsLinkTargetConstraint` are added as replacement. + ## 6.1.0 ## 6.0.0 diff --git a/packages/api/blocks-api/package.json b/packages/api/blocks-api/package.json index 27d63e6200..706407ebf9 100644 --- a/packages/api/blocks-api/package.json +++ b/packages/api/blocks-api/package.json @@ -1,6 +1,6 @@ { "name": "@comet/blocks-api", - "version": "6.1.0", + "version": "6.2.0", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -29,7 +29,7 @@ "rimraf": "^3.0.0" }, "devDependencies": { - "@comet/eslint-config": "workspace:^6.1.0", + "@comet/eslint-config": "workspace:^6.2.0", "@nestjs/common": "^9.0.0", "@types/draft-js": "^0.11.10", "@types/jest": "^29.5.0", diff --git a/packages/api/cms-api/CHANGELOG.md b/packages/api/cms-api/CHANGELOG.md index a2a275662b..21eb7605f8 100644 --- a/packages/api/cms-api/CHANGELOG.md +++ b/packages/api/cms-api/CHANGELOG.md @@ -1,5 +1,27 @@ # @comet/cms-api +## 6.2.0 + +### Minor Changes + +- beeea1dd: Remove `availablePermissions`-option in `UserPermissionsModule` + + Simply remove the `Permission` interface module augmentation and the `availablePermissions`-option from the application. + +- 151e1218: Support multiple `@AffectedEntity()`-decorators for a single function + +### Patch Changes + +- 04afb3ee: Fix attached document deletion when deleting a page tree node +- ad153c99: Always use preview DAM URLs in the admin application + + This fixes a bug where the PDF preview in the DAM wouldn't work because the file couldn't be included in an iFrame on the admin domain. + + We already intended to use preview URLs everywhere in [v5.3.0](https://github.com/vivid-planet/comet/releases/tag/v5.3.0#:~:text=Always%20use%20the%20/preview%20file%20URLs%20in%20the%20admin%20application). However, the `x-preview-dam-urls` header wasn't passed correctly to the `createFileUrl()` method everywhere. As a result, preview URLs were only used in blocks but not in the DAM. Now, the DAM uses preview URLs as well. + +- Updated dependencies [75865caa] + - @comet/blocks-api@6.2.0 + ## 6.1.0 ### Minor Changes diff --git a/packages/api/cms-api/package.json b/packages/api/cms-api/package.json index c5278d3681..e3fce12ceb 100644 --- a/packages/api/cms-api/package.json +++ b/packages/api/cms-api/package.json @@ -1,6 +1,6 @@ { "name": "@comet/cms-api", - "version": "6.1.0", + "version": "6.2.0", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -32,7 +32,7 @@ "test:watch": "jest --watch" }, "dependencies": { - "@comet/blocks-api": "workspace:^6.1.0", + "@comet/blocks-api": "workspace:^6.2.0", "@golevelup/nestjs-discovery": "^3.0.0", "@hapi/accept": "^5.0.2", "@nestjs/jwt": "^9.0.0", @@ -79,7 +79,7 @@ "@aws-sdk/client-s3": "^3.47.0", "@aws-sdk/types": "^3.47.0", "@azure/storage-blob": "^12.0.0", - "@comet/eslint-config": "workspace:^6.1.0", + "@comet/eslint-config": "workspace:^6.2.0", "@kubernetes/client-node": "^0.18.1", "@mikro-orm/cli": "^5.7.1", "@mikro-orm/core": "^5.7.1", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 250b000f07..2c526b2553 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,7 @@ # @comet/cli +## 6.2.0 + ## 6.1.0 ## 6.0.0 diff --git a/packages/cli/package.json b/packages/cli/package.json index a7ba837f46..4dbb8bed60 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@comet/cli", - "version": "6.1.0", + "version": "6.2.0", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -29,7 +29,7 @@ "prettier": "^2.7.1" }, "devDependencies": { - "@comet/eslint-config": "^6.1.0", + "@comet/eslint-config": "^6.2.0", "@types/node": "^18.0.0", "eslint": "^8.0.0", "npm-run-all": "^4.1.5", diff --git a/packages/eslint-config/CHANGELOG.md b/packages/eslint-config/CHANGELOG.md index bb5824e7ea..68ef198ca2 100644 --- a/packages/eslint-config/CHANGELOG.md +++ b/packages/eslint-config/CHANGELOG.md @@ -1,5 +1,11 @@ # @comet/eslint-config +## 6.2.0 + +### Patch Changes + +- @comet/eslint-plugin@6.2.0 + ## 6.1.0 ### Patch Changes diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index fed7c58dcf..731d29a9f8 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@comet/eslint-config", - "version": "6.1.0", + "version": "6.2.0", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -23,7 +23,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-simple-import-sort": "^9.0.0", "eslint-plugin-unused-imports": "^2.0.0", - "@comet/eslint-plugin": "workspace:^6.1.0" + "@comet/eslint-plugin": "workspace:^6.2.0" }, "devDependencies": { "eslint": "^8.32.0", diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 5efb2dcbab..3cf45fdd46 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -1,5 +1,7 @@ # @comet/eslint-plugin +## 6.2.0 + ## 6.1.0 ## 6.0.0 diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index d05395ee99..980e39d905 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@comet/eslint-plugin", - "version": "6.1.0", + "version": "6.2.0", "main": "lib/index.js", "scripts": { "build": "$npm_execpath run clean && tsc", diff --git a/packages/site/cms-site/CHANGELOG.md b/packages/site/cms-site/CHANGELOG.md index d4f39664f2..d4cf963828 100644 --- a/packages/site/cms-site/CHANGELOG.md +++ b/packages/site/cms-site/CHANGELOG.md @@ -1,5 +1,29 @@ # @comet/cms-site +## 6.2.0 + +### Minor Changes + +- 34bb33fe: Add `SeoBlock` + + Can be used as a drop-in replacement for `SeoBlock` defined in application code. Add a `resolveOpenGraphImageUrlTemplate` to resolve the correct image URL template when using a custom Open Graph image block. + + **Example Default Use Case:** + + ```tsx + + ``` + + **Example Custom Use Case:** + + ```tsx + + data={exampleData} + title={"Some Example Title"} + resolveOpenGraphImageUrlTemplate={(block) => block.some.path.to.urlTemplate} + /> + ``` + ## 6.1.0 ## 6.0.0 diff --git a/packages/site/cms-site/package.json b/packages/site/cms-site/package.json index bf43cef583..7ffb0870b1 100644 --- a/packages/site/cms-site/package.json +++ b/packages/site/cms-site/package.json @@ -1,6 +1,6 @@ { "name": "@comet/cms-site", - "version": "6.1.0", + "version": "6.2.0", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -32,8 +32,8 @@ "use-debounce": "^6.0.0" }, "devDependencies": { - "@comet/cli": "workspace:^6.1.0", - "@comet/eslint-config": "workspace:^6.1.0", + "@comet/cli": "workspace:^6.2.0", + "@comet/eslint-config": "workspace:^6.2.0", "@gitbeaker/node": "^34.0.0", "@testing-library/react-hooks": "^8.0.0", "@types/draft-js": "^0.11.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 01395217cd..5eab7ad9ee 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -973,7 +973,7 @@ importers: packages/admin/admin: dependencies: '@comet/admin-icons': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin-icons '@mui/private-theming': specifier: ^5.0.0 @@ -1022,10 +1022,10 @@ importers: specifier: ^7.20.12 version: 7.20.12 '@comet/admin-babel-preset': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin-babel-preset '@comet/eslint-config': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../../eslint-config '@emotion/react': specifier: ^11.5.0 @@ -1193,10 +1193,10 @@ importers: packages/admin/admin-color-picker: dependencies: '@comet/admin': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin '@comet/admin-icons': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin-icons clsx: specifier: ^1.1.1 @@ -1218,10 +1218,10 @@ importers: specifier: ^7.20.12 version: 7.20.12 '@comet/admin-babel-preset': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin-babel-preset '@comet/eslint-config': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../../eslint-config '@mui/icons-material': specifier: ^5.0.0 @@ -1275,10 +1275,10 @@ importers: packages/admin/admin-date-time: dependencies: '@comet/admin': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin '@comet/admin-icons': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin-icons '@mui/utils': specifier: ^5.4.1 @@ -1300,10 +1300,10 @@ importers: specifier: ^7.20.12 version: 7.20.12 '@comet/admin-babel-preset': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin-babel-preset '@comet/eslint-config': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../../eslint-config '@mui/material': specifier: ^5.0.0 @@ -1360,10 +1360,10 @@ importers: specifier: ^7.20.12 version: 7.20.12 '@comet/admin-babel-preset': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin-babel-preset '@comet/eslint-config': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../../eslint-config '@mui/material': specifier: ^5.0.0 @@ -1417,7 +1417,7 @@ importers: packages/admin/admin-react-select: dependencies: '@comet/admin': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin classnames: specifier: ^2.2.6 @@ -1430,10 +1430,10 @@ importers: specifier: ^7.20.12 version: 7.20.12 '@comet/admin-babel-preset': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin-babel-preset '@comet/eslint-config': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../../eslint-config '@mui/icons-material': specifier: ^5.0.0 @@ -1487,10 +1487,10 @@ importers: packages/admin/admin-rte: dependencies: '@comet/admin': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin '@comet/admin-icons': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin-icons detect-browser: specifier: ^5.2.1 @@ -1515,10 +1515,10 @@ importers: specifier: ^7.20.12 version: 7.20.12 '@comet/admin-babel-preset': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin-babel-preset '@comet/eslint-config': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../../eslint-config '@mui/icons-material': specifier: ^5.0.0 @@ -1602,7 +1602,7 @@ importers: packages/admin/admin-theme: dependencies: '@comet/admin-icons': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin-icons '@mui/utils': specifier: ^5.4.1 @@ -1615,10 +1615,10 @@ importers: specifier: ^7.20.12 version: 7.20.12 '@comet/admin-babel-preset': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin-babel-preset '@comet/eslint-config': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../../eslint-config '@mui/material': specifier: ^5.0.0 @@ -1663,10 +1663,10 @@ importers: packages/admin/blocks-admin: dependencies: '@comet/admin': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin '@comet/admin-icons': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin-icons '@mui/lab': specifier: ^5.0.0-alpha.76 @@ -1697,13 +1697,13 @@ importers: specifier: ^7.20.12 version: 7.20.12 '@comet/admin-babel-preset': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin-babel-preset '@comet/cli': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../../cli '@comet/eslint-config': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../../eslint-config '@emotion/react': specifier: ^11.5.0 @@ -1796,22 +1796,22 @@ importers: packages/admin/cms-admin: dependencies: '@comet/admin': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin '@comet/admin-date-time': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin-date-time '@comet/admin-icons': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin-icons '@comet/admin-rte': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin-rte '@comet/admin-theme': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin-theme '@comet/blocks-admin': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../blocks-admin '@graphql-tools/graphql-file-loader': specifier: ^7.5.17 @@ -1929,13 +1929,13 @@ importers: specifier: ^7.20.12 version: 7.20.12 '@comet/admin-babel-preset': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../admin-babel-preset '@comet/cli': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../../cli '@comet/eslint-config': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../../eslint-config '@emotion/react': specifier: ^11.5.0 @@ -2113,7 +2113,7 @@ importers: version: 3.0.2 devDependencies: '@comet/eslint-config': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../../eslint-config '@nestjs/common': specifier: ^9.0.0 @@ -2170,7 +2170,7 @@ importers: packages/api/cms-api: dependencies: '@comet/blocks-api': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../blocks-api '@golevelup/nestjs-discovery': specifier: ^3.0.0 @@ -2306,7 +2306,7 @@ importers: specifier: ^12.0.0 version: 12.12.0 '@comet/eslint-config': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../../eslint-config '@kubernetes/client-node': specifier: ^0.18.1 @@ -2421,7 +2421,7 @@ importers: version: 7.8.0 ts-jest: specifier: ^29.0.5 - version: 29.0.5(jest@29.5.0)(typescript@4.9.4) + version: 29.0.5(@babel/core@7.20.12)(jest@29.5.0)(typescript@4.9.4) ts-node: specifier: ^10.0.0 version: 10.9.1(@types/node@18.15.3)(typescript@4.9.4) @@ -2439,7 +2439,7 @@ importers: version: 2.8.3 devDependencies: '@comet/eslint-config': - specifier: ^6.1.0 + specifier: ^6.2.0 version: link:../eslint-config '@types/node': specifier: ^18.0.0 @@ -2463,7 +2463,7 @@ importers: specifier: ^1.4.1 version: 1.4.1 '@comet/eslint-plugin': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../eslint-plugin '@next/eslint-plugin-next': specifier: ^12.0.0 @@ -2561,10 +2561,10 @@ importers: version: 6.0.1(react@17.0.2) devDependencies: '@comet/cli': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../../cli '@comet/eslint-config': - specifier: workspace:^6.1.0 + specifier: workspace:^6.2.0 version: link:../../eslint-config '@gitbeaker/node': specifier: ^34.0.0 @@ -4336,7 +4336,7 @@ packages: '@babel/traverse': 7.22.11 '@babel/types': 7.22.11 convert-source-map: 1.9.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) gensync: 1.0.0-beta.2 json5: 2.2.3 lodash: 4.17.21 @@ -4359,10 +4359,10 @@ packages: '@babel/helpers': 7.20.13 '@babel/parser': 7.20.13 '@babel/template': 7.20.7 - '@babel/traverse': 7.20.13(supports-color@5.5.0) + '@babel/traverse': 7.20.13 '@babel/types': 7.20.7 convert-source-map: 1.9.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.0 @@ -4384,7 +4384,7 @@ packages: '@babel/traverse': 7.22.11 '@babel/types': 7.22.11 convert-source-map: 1.9.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.3.1) gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 @@ -4525,7 +4525,7 @@ packages: '@babel/helper-module-imports': 7.22.5 '@babel/helper-plugin-utils': 7.22.5 '@babel/traverse': 7.22.11 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) lodash.debounce: 4.0.8 resolve: 1.22.1 semver: 6.3.1 @@ -4541,7 +4541,7 @@ packages: '@babel/core': 7.20.12 '@babel/helper-compilation-targets': 7.22.10 '@babel/helper-plugin-utils': 7.22.5 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) lodash.debounce: 4.0.8 resolve: 1.22.1 semver: 6.3.1 @@ -4556,7 +4556,7 @@ packages: '@babel/core': 7.22.11 '@babel/helper-compilation-targets': 7.22.10 '@babel/helper-plugin-utils': 7.22.5 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) lodash.debounce: 4.0.8 resolve: 1.22.1 semver: 6.3.1 @@ -4632,7 +4632,7 @@ packages: '@babel/helper-split-export-declaration': 7.18.6 '@babel/helper-validator-identifier': 7.19.1 '@babel/template': 7.20.7 - '@babel/traverse': 7.20.13(supports-color@5.5.0) + '@babel/traverse': 7.20.13 '@babel/types': 7.20.7 transitivePeerDependencies: - supports-color @@ -4807,7 +4807,7 @@ packages: engines: {node: '>=6.9.0'} dependencies: '@babel/template': 7.20.7 - '@babel/traverse': 7.20.13(supports-color@5.5.0) + '@babel/traverse': 7.20.13 '@babel/types': 7.20.7 transitivePeerDependencies: - supports-color @@ -6825,6 +6825,23 @@ packages: '@babel/parser': 7.22.14 '@babel/types': 7.22.11 + /@babel/traverse@7.20.13: + resolution: {integrity: sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==} + engines: {node: '>=6.9.0'} + dependencies: + '@babel/code-frame': 7.18.6 + '@babel/generator': 7.20.7 + '@babel/helper-environment-visitor': 7.18.9 + '@babel/helper-function-name': 7.19.0 + '@babel/helper-hoist-variables': 7.18.6 + '@babel/helper-split-export-declaration': 7.18.6 + '@babel/parser': 7.20.13 + '@babel/types': 7.20.7 + debug: 4.3.4(supports-color@9.3.1) + globals: 11.12.0 + transitivePeerDependencies: + - supports-color + /@babel/traverse@7.20.13(supports-color@5.5.0): resolution: {integrity: sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==} engines: {node: '>=6.9.0'} @@ -6854,7 +6871,7 @@ packages: '@babel/helper-split-export-declaration': 7.22.6 '@babel/parser': 7.22.14 '@babel/types': 7.22.11 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.3.1) globals: 11.12.0 transitivePeerDependencies: - supports-color @@ -7147,7 +7164,7 @@ packages: '@babel/preset-typescript': 7.18.6(@babel/core@7.22.11) '@babel/runtime': 7.20.13 '@babel/runtime-corejs3': 7.22.6 - '@babel/traverse': 7.20.13(supports-color@5.5.0) + '@babel/traverse': 7.20.13 '@docusaurus/cssnano-preset': 2.4.1 '@docusaurus/logger': 2.4.1 '@docusaurus/mdx-loader': 2.4.1(@docusaurus/types@2.4.1)(react-dom@17.0.2)(react@17.0.2) @@ -8207,7 +8224,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.3.1) espree: 9.5.2 globals: 13.19.0 ignore: 5.2.4 @@ -8223,7 +8240,7 @@ packages: engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) espree: 9.5.2 globals: 13.19.0 ignore: 5.2.4 @@ -9223,7 +9240,7 @@ packages: '@types/json-stable-stringify': 1.0.34 '@types/jsonwebtoken': 9.0.1 chalk: 4.1.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) dotenv: 16.0.3 graphql: 15.8.0 graphql-request: 5.1.0(graphql@15.8.0) @@ -9574,7 +9591,7 @@ packages: engines: {node: '>=10.10.0'} dependencies: '@humanwhocodes/object-schema': 1.2.1 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.3.1) minimatch: 3.1.2 transitivePeerDependencies: - supports-color @@ -10232,7 +10249,7 @@ packages: dependencies: '@open-draft/until': 1.0.3 '@xmldom/xmldom': 0.7.10 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) headers-utils: 3.0.2 outvariant: 1.4.0 strict-event-emitter: 0.2.8 @@ -13028,7 +13045,7 @@ packages: typescript: '>= 3.x' webpack: '>= 4' dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) endent: 2.1.0 find-cache-dir: 3.3.2 flat-cache: 3.0.4 @@ -14592,7 +14609,7 @@ packages: '@typescript-eslint/scope-manager': 5.49.0 '@typescript-eslint/type-utils': 5.49.0(eslint@8.32.0)(typescript@4.9.4) '@typescript-eslint/utils': 5.49.0(eslint@8.32.0)(typescript@4.9.4) - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) eslint: 8.32.0 ignore: 5.2.4 natural-compare-lite: 1.4.0 @@ -14617,7 +14634,7 @@ packages: '@typescript-eslint/scope-manager': 5.49.0 '@typescript-eslint/types': 5.49.0 '@typescript-eslint/typescript-estree': 5.49.0(typescript@4.9.4) - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) eslint: 8.32.0 typescript: 4.9.4 transitivePeerDependencies: @@ -14644,7 +14661,7 @@ packages: dependencies: '@typescript-eslint/typescript-estree': 5.49.0(typescript@4.9.4) '@typescript-eslint/utils': 5.49.0(eslint@8.32.0)(typescript@4.9.4) - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) eslint: 8.32.0 tsutils: 3.21.0(typescript@4.9.4) typescript: 4.9.4 @@ -14668,7 +14685,7 @@ packages: dependencies: '@typescript-eslint/types': 5.49.0 '@typescript-eslint/visitor-keys': 5.49.0 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) globby: 11.1.0 is-glob: 4.0.3 semver: 7.3.8 @@ -15304,7 +15321,7 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) transitivePeerDependencies: - supports-color dev: true @@ -15313,7 +15330,7 @@ packages: resolution: {integrity: sha512-Zn4cw2NEqd+9fiSVWMscnjyQ1a8Yfoc5oBajLeo5w+YBHgDUcEBY2hS4YpTz6iN5f/2zQiktcuM6tS8x1p9dpA==} engines: {node: '>= 8.0.0'} dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) depd: 1.1.2 humanize-ms: 1.2.1 transitivePeerDependencies: @@ -18411,17 +18428,6 @@ packages: ms: 2.1.3 dev: false - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true - dependencies: - ms: 2.1.2 - /debug@4.3.4(supports-color@5.5.0): resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -18445,7 +18451,6 @@ packages: dependencies: ms: 2.1.2 supports-color: 9.3.1 - dev: true /decamelize-keys@1.1.1: resolution: {integrity: sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==} @@ -18683,7 +18688,7 @@ packages: hasBin: true dependencies: address: 1.2.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) transitivePeerDependencies: - supports-color dev: false @@ -19108,7 +19113,7 @@ packages: basic-auth: 2.0.1 cookie: 0.5.0 core-util-is: 1.0.3 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) elastic-apm-http-client: 11.2.0 end-of-stream: 1.4.4 error-callsites: 2.0.4 @@ -19462,7 +19467,7 @@ packages: eslint: '*' eslint-plugin-import: '*' dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) enhanced-resolve: 5.15.0 eslint: 8.32.0 eslint-plugin-import: 2.27.5(@typescript-eslint/parser@5.49.0)(eslint-import-resolver-typescript@3.5.3)(eslint@8.32.0) @@ -19749,7 +19754,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.3.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.1.1 @@ -19799,7 +19804,7 @@ packages: ajv: 6.12.6 chalk: 4.1.2 cross-spawn: 7.0.3 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) doctrine: 3.0.0 escape-string-regexp: 4.0.0 eslint-scope: 7.1.1 @@ -21380,7 +21385,7 @@ packages: peerDependencies: graphql: '>=0.9 <0.14 || ^14.0.2 || ^15.4.0 || ^16.3.0' dependencies: - debug: 4.3.4 + debug: 4.3.4(supports-color@9.3.1) graphql: 15.8.0 tslib: 2.4.1 transitivePeerDependencies: @@ -21914,7 +21919,7 @@ packages: dependencies: '@tootallnate/once': 2.0.0 agent-base: 6.0.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) transitivePeerDependencies: - supports-color dev: true @@ -21975,7 +21980,7 @@ packages: engines: {node: '>= 6'} dependencies: agent-base: 6.0.2 - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) transitivePeerDependencies: - supports-color dev: true @@ -22898,7 +22903,7 @@ packages: resolution: {integrity: sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==} engines: {node: '>=10'} dependencies: - debug: 4.3.4 + debug: 4.3.4(supports-color@9.3.1) istanbul-lib-coverage: 3.2.0 source-map: 0.6.1 transitivePeerDependencies: @@ -23786,7 +23791,7 @@ packages: dependencies: '@types/express': 4.17.16 '@types/jsonwebtoken': 9.0.1 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.3.1) jose: 4.11.2 limiter: 1.1.5 lru-memoizer: 2.1.4 @@ -23877,7 +23882,7 @@ packages: dependencies: colorette: 2.0.19 commander: 9.5.0 - debug: 4.3.4 + debug: 4.3.4(supports-color@9.3.1) escalade: 3.1.1 esm: 3.2.25 get-package-type: 0.1.0 @@ -28259,7 +28264,7 @@ packages: resolution: {integrity: sha512-efCx3b+0Z69/LGJmm9Yvi4cqEdxnoGnxYxGxBghkkTTFeXRtTCmmhO0AnAfHz59k957uTSuy8WaHqOs8wbYUWg==} engines: {node: '>=6'} dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) module-details-from-path: 1.0.3 resolve: 1.22.1 transitivePeerDependencies: @@ -29088,7 +29093,7 @@ packages: /spdy-transport@3.0.0: resolution: {integrity: sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==} dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) detect-node: 2.1.0 hpack.js: 2.1.6 obuf: 1.1.2 @@ -29101,7 +29106,7 @@ packages: resolution: {integrity: sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==} engines: {node: '>=6.0.0'} dependencies: - debug: 4.3.4(supports-color@5.5.0) + debug: 4.3.4(supports-color@9.3.1) handle-thing: 2.0.1 http-deceiver: 1.2.7 select-hose: 2.0.0 @@ -29693,7 +29698,6 @@ packages: /supports-color@9.3.1: resolution: {integrity: sha512-knBY82pjmnIzK3NifMo3RxEIRD9E0kIzV4BKcyTZ9+9kWgLMxd4PrsTSMoFQUabgRBbF8KOLRDCyKgNV+iK44Q==} engines: {node: '>=12'} - dev: true /supports-preserve-symlinks-flag@1.0.0: resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} @@ -30271,39 +30275,6 @@ packages: yargs-parser: 21.1.1 dev: true - /ts-jest@29.0.5(jest@29.5.0)(typescript@4.9.4): - resolution: {integrity: sha512-PL3UciSgIpQ7f6XjVOmbi96vmDHUqAyqDr8YxzopDqX3kfgYtX1cuNeBjP+L9sFXi6nzsGGA6R3fP3DDDJyrxA==} - engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} - hasBin: true - peerDependencies: - '@babel/core': '>=7.0.0-beta.0 <8' - '@jest/types': ^29.0.0 - babel-jest: ^29.0.0 - esbuild: '*' - jest: ^29.0.0 - typescript: '>=4.3' - peerDependenciesMeta: - '@babel/core': - optional: true - '@jest/types': - optional: true - babel-jest: - optional: true - esbuild: - optional: true - dependencies: - bs-logger: 0.2.6 - fast-json-stable-stringify: 2.1.0 - jest: 29.5.0(@types/node@18.15.3)(ts-node@10.9.1) - jest-util: 29.5.0 - json5: 2.2.3 - lodash.memoize: 4.1.2 - make-error: 1.3.6 - semver: 7.3.8 - typescript: 4.9.4 - yargs-parser: 21.1.1 - dev: true - /ts-loader@6.2.2(typescript@4.9.4): resolution: {integrity: sha512-HDo5kXZCBml3EUPcc7RlZOV/JGlLHwppTLEHb3SHnr5V7NXD4klMEkrhJe5wgRbaWsSXi+Y1SIBN/K9B6zWGWQ==} engines: {node: '>=8.6'} From f145730619854d7c9282aa15e1128770dbef54c1 Mon Sep 17 00:00:00 2001 From: Johannes Obermair <48853629+johnnyomair@users.noreply.github.com> Date: Tue, 27 Feb 2024 13:50:08 +0100 Subject: [PATCH 06/18] Ignore user permissions when using system user (#1761) The `UserPermissionsGuard` didn't allow requests when using a system user (e.g., basic authorization during site build). --- .changeset/moody-falcons-fix.md | 7 +++++++ packages/api/cms-api/src/access-log/access-log.module.ts | 3 ++- .../src/user-permissions/auth/user-permissions.guard.ts | 9 ++++++--- .../src/user-permissions/user-permissions.types.ts | 4 +++- 4 files changed, 18 insertions(+), 5 deletions(-) create mode 100644 .changeset/moody-falcons-fix.md diff --git a/.changeset/moody-falcons-fix.md b/.changeset/moody-falcons-fix.md new file mode 100644 index 0000000000..08f0ff9a3f --- /dev/null +++ b/.changeset/moody-falcons-fix.md @@ -0,0 +1,7 @@ +--- +"@comet/cms-api": patch +--- + +Ignore user permissions when using system user + +The `UserPermissionsGuard` didn't allow requests when using a system user (e.g., basic authorization during site build). diff --git a/packages/api/cms-api/src/access-log/access-log.module.ts b/packages/api/cms-api/src/access-log/access-log.module.ts index a2e3612cec..b1afc2c571 100644 --- a/packages/api/cms-api/src/access-log/access-log.module.ts +++ b/packages/api/cms-api/src/access-log/access-log.module.ts @@ -3,10 +3,11 @@ import { APP_INTERCEPTOR } from "@nestjs/core"; import { Request } from "express"; import { CurrentUser } from "../user-permissions/dto/current-user"; +import { SystemUser } from "../user-permissions/user-permissions.types"; import { SHOULD_LOG_REQUEST } from "./access-log.constants"; import { AccessLogInterceptor } from "./access-log.interceptor"; -export type ShouldLogRequest = ({ user, req }: { user?: CurrentUser | true; req: Request }) => boolean; +export type ShouldLogRequest = ({ user, req }: { user?: CurrentUser | SystemUser; req: Request }) => boolean; interface AccessLogModuleOptions { shouldLogRequest?: ShouldLogRequest; diff --git a/packages/api/cms-api/src/user-permissions/auth/user-permissions.guard.ts b/packages/api/cms-api/src/user-permissions/auth/user-permissions.guard.ts index 17eec92d00..a5d23e510a 100644 --- a/packages/api/cms-api/src/user-permissions/auth/user-permissions.guard.ts +++ b/packages/api/cms-api/src/user-permissions/auth/user-permissions.guard.ts @@ -6,7 +6,7 @@ import { ContentScopeService } from "../content-scope.service"; import { RequiredPermissionMetadata } from "../decorators/required-permission.decorator"; import { CurrentUser } from "../dto/current-user"; import { ACCESS_CONTROL_SERVICE } from "../user-permissions.constants"; -import { AccessControlServiceInterface } from "../user-permissions.types"; +import { AccessControlServiceInterface, SystemUser } from "../user-permissions.types"; @Injectable() export class UserPermissionsGuard implements CanActivate { @@ -25,6 +25,9 @@ export class UserPermissionsGuard implements CanActivate { const user = this.getUser(context); if (!user) return false; + // System user authenticated via basic auth + if (user === true) return true; + const requiredPermission = this.getDecorator(context, "requiredPermission"); if (!requiredPermission) throw new Error(`RequiredPermission decorator is missing in ${location}`); const requiredPermissions = requiredPermission.requiredPermission; @@ -51,10 +54,10 @@ export class UserPermissionsGuard implements CanActivate { } } - private getUser(context: ExecutionContext): CurrentUser | undefined { + private getUser(context: ExecutionContext): CurrentUser | SystemUser | undefined { const request = context.getType().toString() === "graphql" ? GqlExecutionContext.create(context).getContext().req : context.switchToHttp().getRequest(); - return request.user as CurrentUser | undefined; + return request.user as CurrentUser | SystemUser | undefined; } private getDecorator(context: ExecutionContext, decorator: string): T { diff --git a/packages/api/cms-api/src/user-permissions/user-permissions.types.ts b/packages/api/cms-api/src/user-permissions/user-permissions.types.ts index de0510dc4a..79a4d1c1a4 100644 --- a/packages/api/cms-api/src/user-permissions/user-permissions.types.ts +++ b/packages/api/cms-api/src/user-permissions/user-permissions.types.ts @@ -13,6 +13,8 @@ export enum UserPermissions { export type Users = [User[], number]; +export type SystemUser = true; + type PermissionForUser = { permission: string; contentScopes?: ContentScope[]; @@ -22,7 +24,7 @@ export type PermissionsForUser = PermissionForUser[] | UserPermissions.allPermis export type ContentScopesForUser = ContentScope[] | UserPermissions.allContentScopes; export interface AccessControlServiceInterface { - isAllowed(user: CurrentUser, permission: string, contentScope?: ContentScope): boolean; + isAllowed(user: CurrentUser | SystemUser, permission: string, contentScope?: ContentScope): boolean; getPermissionsForUser?: (user: User) => Promise | PermissionsForUser; getContentScopesForUser?: (user: User) => Promise | ContentScopesForUser; } From f9ce022a7d2bc75c5e78432fcb6fac8afe98ea60 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 27 Feb 2024 13:57:35 +0100 Subject: [PATCH 07/18] Version Packages (#1762) This PR was opened by the [Changesets release](https://github.com/changesets/action) GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated. # Releases ## @comet/admin@6.2.1 ### Patch Changes - @comet/admin-icons@6.2.1 ## @comet/admin-color-picker@6.2.1 ### Patch Changes - @comet/admin@6.2.1 - @comet/admin-icons@6.2.1 ## @comet/admin-date-time@6.2.1 ### Patch Changes - @comet/admin@6.2.1 - @comet/admin-icons@6.2.1 ## @comet/admin-react-select@6.2.1 ### Patch Changes - @comet/admin@6.2.1 ## @comet/admin-rte@6.2.1 ### Patch Changes - @comet/admin@6.2.1 - @comet/admin-icons@6.2.1 ## @comet/admin-theme@6.2.1 ### Patch Changes - @comet/admin-icons@6.2.1 ## @comet/blocks-admin@6.2.1 ### Patch Changes - @comet/admin@6.2.1 - @comet/admin-icons@6.2.1 ## @comet/cms-admin@6.2.1 ### Patch Changes - @comet/admin@6.2.1 - @comet/admin-date-time@6.2.1 - @comet/admin-icons@6.2.1 - @comet/admin-rte@6.2.1 - @comet/admin-theme@6.2.1 - @comet/blocks-admin@6.2.1 ## @comet/cms-api@6.2.1 ### Patch Changes - f1457306: Ignore user permissions when using system user The `UserPermissionsGuard` didn't allow requests when using a system user (e.g., basic authorization during site build). - @comet/blocks-api@6.2.1 ## @comet/eslint-config@6.2.1 ### Patch Changes - @comet/eslint-plugin@6.2.1 ## @comet/admin-babel-preset@6.2.1 ## @comet/admin-icons@6.2.1 ## @comet/blocks-api@6.2.1 ## @comet/cli@6.2.1 ## @comet/eslint-plugin@6.2.1 ## @comet/cms-site@6.2.1 Co-authored-by: github-actions[bot] --- .changeset/moody-falcons-fix.md | 7 -- .../admin/admin-babel-preset/CHANGELOG.md | 2 + .../admin/admin-babel-preset/package.json | 2 +- .../admin/admin-color-picker/CHANGELOG.md | 7 ++ .../admin/admin-color-picker/package.json | 10 +-- packages/admin/admin-date-time/CHANGELOG.md | 7 ++ packages/admin/admin-date-time/package.json | 10 +-- packages/admin/admin-icons/CHANGELOG.md | 2 + packages/admin/admin-icons/package.json | 6 +- .../admin/admin-react-select/CHANGELOG.md | 6 ++ .../admin/admin-react-select/package.json | 8 +- packages/admin/admin-rte/CHANGELOG.md | 7 ++ packages/admin/admin-rte/package.json | 10 +-- packages/admin/admin-theme/CHANGELOG.md | 6 ++ packages/admin/admin-theme/package.json | 8 +- packages/admin/admin/CHANGELOG.md | 6 ++ packages/admin/admin/package.json | 8 +- packages/admin/blocks-admin/CHANGELOG.md | 7 ++ packages/admin/blocks-admin/package.json | 12 +-- packages/admin/cms-admin/CHANGELOG.md | 11 +++ packages/admin/cms-admin/package.json | 20 ++--- packages/api/blocks-api/CHANGELOG.md | 2 + packages/api/blocks-api/package.json | 4 +- packages/api/cms-api/CHANGELOG.md | 10 +++ packages/api/cms-api/package.json | 6 +- packages/cli/CHANGELOG.md | 2 + packages/cli/package.json | 4 +- packages/eslint-config/CHANGELOG.md | 6 ++ packages/eslint-config/package.json | 4 +- packages/eslint-plugin/CHANGELOG.md | 2 + packages/eslint-plugin/package.json | 2 +- packages/site/cms-site/CHANGELOG.md | 2 + packages/site/cms-site/package.json | 6 +- pnpm-lock.yaml | 88 +++++++++---------- 34 files changed, 189 insertions(+), 111 deletions(-) delete mode 100644 .changeset/moody-falcons-fix.md diff --git a/.changeset/moody-falcons-fix.md b/.changeset/moody-falcons-fix.md deleted file mode 100644 index 08f0ff9a3f..0000000000 --- a/.changeset/moody-falcons-fix.md +++ /dev/null @@ -1,7 +0,0 @@ ---- -"@comet/cms-api": patch ---- - -Ignore user permissions when using system user - -The `UserPermissionsGuard` didn't allow requests when using a system user (e.g., basic authorization during site build). diff --git a/packages/admin/admin-babel-preset/CHANGELOG.md b/packages/admin/admin-babel-preset/CHANGELOG.md index f4d30cdc2a..59e428d991 100644 --- a/packages/admin/admin-babel-preset/CHANGELOG.md +++ b/packages/admin/admin-babel-preset/CHANGELOG.md @@ -1,5 +1,7 @@ # @comet/admin-babel-preset +## 6.2.1 + ## 6.2.0 ## 6.1.0 diff --git a/packages/admin/admin-babel-preset/package.json b/packages/admin/admin-babel-preset/package.json index 5ca6ac97be..b564bba782 100644 --- a/packages/admin/admin-babel-preset/package.json +++ b/packages/admin/admin-babel-preset/package.json @@ -1,6 +1,6 @@ { "name": "@comet/admin-babel-preset", - "version": "6.2.0", + "version": "6.2.1", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", diff --git a/packages/admin/admin-color-picker/CHANGELOG.md b/packages/admin/admin-color-picker/CHANGELOG.md index 3cd799dc35..16ac04672c 100644 --- a/packages/admin/admin-color-picker/CHANGELOG.md +++ b/packages/admin/admin-color-picker/CHANGELOG.md @@ -1,5 +1,12 @@ # @comet/admin-color-picker +## 6.2.1 + +### Patch Changes + +- @comet/admin@6.2.1 +- @comet/admin-icons@6.2.1 + ## 6.2.0 ### Patch Changes diff --git a/packages/admin/admin-color-picker/package.json b/packages/admin/admin-color-picker/package.json index 1fecdd4488..c1c3b58f2f 100644 --- a/packages/admin/admin-color-picker/package.json +++ b/packages/admin/admin-color-picker/package.json @@ -1,6 +1,6 @@ { "name": "@comet/admin-color-picker", - "version": "6.2.0", + "version": "6.2.1", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -25,8 +25,8 @@ "start:types": "tsc --project ./tsconfig.json --emitDeclarationOnly --watch --preserveWatchOutput" }, "dependencies": { - "@comet/admin": "workspace:^6.2.0", - "@comet/admin-icons": "workspace:^6.2.0", + "@comet/admin": "workspace:^6.2.1", + "@comet/admin-icons": "workspace:^6.2.1", "clsx": "^1.1.1", "react-colorful": "^5.5.1", "tinycolor2": "^1.4.1", @@ -35,8 +35,8 @@ "devDependencies": { "@babel/cli": "^7.17.6", "@babel/core": "^7.20.12", - "@comet/admin-babel-preset": "workspace:^6.2.0", - "@comet/eslint-config": "workspace:^6.2.0", + "@comet/admin-babel-preset": "workspace:^6.2.1", + "@comet/eslint-config": "workspace:^6.2.1", "@mui/icons-material": "^5.0.0", "@mui/material": "^5.0.0", "@mui/styles": "^5.0.0", diff --git a/packages/admin/admin-date-time/CHANGELOG.md b/packages/admin/admin-date-time/CHANGELOG.md index 3d0fbeb515..0f619db67e 100644 --- a/packages/admin/admin-date-time/CHANGELOG.md +++ b/packages/admin/admin-date-time/CHANGELOG.md @@ -1,5 +1,12 @@ # @comet/admin-date-time +## 6.2.1 + +### Patch Changes + +- @comet/admin@6.2.1 +- @comet/admin-icons@6.2.1 + ## 6.2.0 ### Patch Changes diff --git a/packages/admin/admin-date-time/package.json b/packages/admin/admin-date-time/package.json index 2faccc3567..d086846ef7 100644 --- a/packages/admin/admin-date-time/package.json +++ b/packages/admin/admin-date-time/package.json @@ -1,6 +1,6 @@ { "name": "@comet/admin-date-time", - "version": "6.2.0", + "version": "6.2.1", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -25,8 +25,8 @@ "start:types": "tsc --project ./tsconfig.json --emitDeclarationOnly --watch --preserveWatchOutput" }, "dependencies": { - "@comet/admin": "workspace:^6.2.0", - "@comet/admin-icons": "workspace:^6.2.0", + "@comet/admin": "workspace:^6.2.1", + "@comet/admin-icons": "workspace:^6.2.1", "@mui/utils": "^5.4.1", "clsx": "^1.1.1", "date-fns": "^2.28.0", @@ -35,8 +35,8 @@ "devDependencies": { "@babel/cli": "^7.17.6", "@babel/core": "^7.20.12", - "@comet/admin-babel-preset": "workspace:^6.2.0", - "@comet/eslint-config": "workspace:^6.2.0", + "@comet/admin-babel-preset": "workspace:^6.2.1", + "@comet/eslint-config": "workspace:^6.2.1", "@mui/material": "^5.0.0", "@mui/styles": "^5.0.0", "@types/react": "^17.0", diff --git a/packages/admin/admin-icons/CHANGELOG.md b/packages/admin/admin-icons/CHANGELOG.md index 6c836a3e6d..b1c72cfe73 100644 --- a/packages/admin/admin-icons/CHANGELOG.md +++ b/packages/admin/admin-icons/CHANGELOG.md @@ -1,5 +1,7 @@ # @comet/admin-icons +## 6.2.1 + ## 6.2.0 ## 6.1.0 diff --git a/packages/admin/admin-icons/package.json b/packages/admin/admin-icons/package.json index b658d67309..5245635c48 100644 --- a/packages/admin/admin-icons/package.json +++ b/packages/admin/admin-icons/package.json @@ -1,6 +1,6 @@ { "name": "@comet/admin-icons", - "version": "6.2.0", + "version": "6.2.1", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -24,8 +24,8 @@ "devDependencies": { "@babel/cli": "^7.17.6", "@babel/core": "^7.20.12", - "@comet/admin-babel-preset": "workspace:^6.2.0", - "@comet/eslint-config": "workspace:^6.2.0", + "@comet/admin-babel-preset": "workspace:^6.2.1", + "@comet/eslint-config": "workspace:^6.2.1", "@mui/material": "^5.0.0", "@types/cli-progress": "^3.8.0", "@types/node": "^18.0.0", diff --git a/packages/admin/admin-react-select/CHANGELOG.md b/packages/admin/admin-react-select/CHANGELOG.md index d3f2aacf66..01be86f3f2 100644 --- a/packages/admin/admin-react-select/CHANGELOG.md +++ b/packages/admin/admin-react-select/CHANGELOG.md @@ -1,5 +1,11 @@ # @comet/admin-react-select +## 6.2.1 + +### Patch Changes + +- @comet/admin@6.2.1 + ## 6.2.0 ### Patch Changes diff --git a/packages/admin/admin-react-select/package.json b/packages/admin/admin-react-select/package.json index e11afd811c..b0ab58169a 100644 --- a/packages/admin/admin-react-select/package.json +++ b/packages/admin/admin-react-select/package.json @@ -1,6 +1,6 @@ { "name": "@comet/admin-react-select", - "version": "6.2.0", + "version": "6.2.1", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -25,14 +25,14 @@ "start:types": "tsc --project ./tsconfig.json --emitDeclarationOnly --watch --preserveWatchOutput" }, "dependencies": { - "@comet/admin": "workspace:^6.2.0", + "@comet/admin": "workspace:^6.2.1", "classnames": "^2.2.6" }, "devDependencies": { "@babel/cli": "^7.17.6", "@babel/core": "^7.20.12", - "@comet/admin-babel-preset": "workspace:^6.2.0", - "@comet/eslint-config": "workspace:^6.2.0", + "@comet/admin-babel-preset": "workspace:^6.2.1", + "@comet/eslint-config": "workspace:^6.2.1", "@mui/icons-material": "^5.0.0", "@mui/material": "^5.0.0", "@mui/styles": "^5.0.0", diff --git a/packages/admin/admin-rte/CHANGELOG.md b/packages/admin/admin-rte/CHANGELOG.md index 5c465bac5d..c6092b19dc 100644 --- a/packages/admin/admin-rte/CHANGELOG.md +++ b/packages/admin/admin-rte/CHANGELOG.md @@ -1,5 +1,12 @@ # @comet/admin-rte +## 6.2.1 + +### Patch Changes + +- @comet/admin@6.2.1 +- @comet/admin-icons@6.2.1 + ## 6.2.0 ### Patch Changes diff --git a/packages/admin/admin-rte/package.json b/packages/admin/admin-rte/package.json index de64abc602..9caed7bef6 100644 --- a/packages/admin/admin-rte/package.json +++ b/packages/admin/admin-rte/package.json @@ -1,6 +1,6 @@ { "name": "@comet/admin-rte", - "version": "6.2.0", + "version": "6.2.1", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -27,8 +27,8 @@ "test:watch": "jest --watch" }, "dependencies": { - "@comet/admin": "workspace:^6.2.0", - "@comet/admin-icons": "workspace:^6.2.0", + "@comet/admin": "workspace:^6.2.1", + "@comet/admin-icons": "workspace:^6.2.1", "detect-browser": "^5.2.1", "draft-js-export-html": "^1.4.1", "draft-js-import-html": "^1.4.1", @@ -38,8 +38,8 @@ "devDependencies": { "@babel/cli": "^7.17.6", "@babel/core": "^7.20.12", - "@comet/admin-babel-preset": "workspace:^6.2.0", - "@comet/eslint-config": "workspace:^6.2.0", + "@comet/admin-babel-preset": "workspace:^6.2.1", + "@comet/eslint-config": "workspace:^6.2.1", "@mui/icons-material": "^5.0.0", "@mui/material": "^5.0.0", "@mui/styles": "^5.0.0", diff --git a/packages/admin/admin-theme/CHANGELOG.md b/packages/admin/admin-theme/CHANGELOG.md index 4a8a7d3044..aadbf5cc97 100644 --- a/packages/admin/admin-theme/CHANGELOG.md +++ b/packages/admin/admin-theme/CHANGELOG.md @@ -1,5 +1,11 @@ # @comet/admin-theme +## 6.2.1 + +### Patch Changes + +- @comet/admin-icons@6.2.1 + ## 6.2.0 ### Patch Changes diff --git a/packages/admin/admin-theme/package.json b/packages/admin/admin-theme/package.json index 386c8694b4..0b7d388a06 100644 --- a/packages/admin/admin-theme/package.json +++ b/packages/admin/admin-theme/package.json @@ -1,6 +1,6 @@ { "name": "@comet/admin-theme", - "version": "6.2.0", + "version": "6.2.1", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -25,14 +25,14 @@ "start:types": "tsc --project ./tsconfig.json --emitDeclarationOnly --watch --preserveWatchOutput" }, "dependencies": { - "@comet/admin-icons": "workspace:^6.2.0", + "@comet/admin-icons": "workspace:^6.2.1", "@mui/utils": "^5.4.1" }, "devDependencies": { "@babel/cli": "^7.17.6", "@babel/core": "^7.20.12", - "@comet/admin-babel-preset": "workspace:^6.2.0", - "@comet/eslint-config": "workspace:^6.2.0", + "@comet/admin-babel-preset": "workspace:^6.2.1", + "@comet/eslint-config": "workspace:^6.2.1", "@mui/material": "^5.0.0", "@mui/styles": "^5.0.0", "@mui/system": "^5.0.0", diff --git a/packages/admin/admin/CHANGELOG.md b/packages/admin/admin/CHANGELOG.md index c5352fb7de..eb550026d2 100644 --- a/packages/admin/admin/CHANGELOG.md +++ b/packages/admin/admin/CHANGELOG.md @@ -1,5 +1,11 @@ # @comet/admin +## 6.2.1 + +### Patch Changes + +- @comet/admin-icons@6.2.1 + ## 6.2.0 ### Patch Changes diff --git a/packages/admin/admin/package.json b/packages/admin/admin/package.json index 0a2ca7aff7..b680e1d529 100644 --- a/packages/admin/admin/package.json +++ b/packages/admin/admin/package.json @@ -1,6 +1,6 @@ { "name": "@comet/admin", - "version": "6.2.0", + "version": "6.2.1", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -27,7 +27,7 @@ "test:watch": "jest --watch" }, "dependencies": { - "@comet/admin-icons": "workspace:^6.2.0", + "@comet/admin-icons": "workspace:^6.2.1", "@mui/private-theming": "^5.0.0", "clsx": "^1.1.1", "exceljs": "^3.4.0", @@ -45,8 +45,8 @@ "@apollo/client": "^3.7.0", "@babel/cli": "^7.17.6", "@babel/core": "^7.20.12", - "@comet/admin-babel-preset": "workspace:^6.2.0", - "@comet/eslint-config": "workspace:^6.2.0", + "@comet/admin-babel-preset": "workspace:^6.2.1", + "@comet/eslint-config": "workspace:^6.2.1", "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", "@mui/icons-material": "^5.0.0", diff --git a/packages/admin/blocks-admin/CHANGELOG.md b/packages/admin/blocks-admin/CHANGELOG.md index 8ac142ab97..92d30c2179 100644 --- a/packages/admin/blocks-admin/CHANGELOG.md +++ b/packages/admin/blocks-admin/CHANGELOG.md @@ -1,5 +1,12 @@ # @comet/blocks-admin +## 6.2.1 + +### Patch Changes + +- @comet/admin@6.2.1 +- @comet/admin-icons@6.2.1 + ## 6.2.0 ### Patch Changes diff --git a/packages/admin/blocks-admin/package.json b/packages/admin/blocks-admin/package.json index c5c9dc6ee1..f5be311778 100644 --- a/packages/admin/blocks-admin/package.json +++ b/packages/admin/blocks-admin/package.json @@ -1,6 +1,6 @@ { "name": "@comet/blocks-admin", - "version": "6.2.0", + "version": "6.2.1", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -29,8 +29,8 @@ "test:watch": "jest --watch" }, "dependencies": { - "@comet/admin": "workspace:^6.2.0", - "@comet/admin-icons": "workspace:^6.2.0", + "@comet/admin": "workspace:^6.2.1", + "@comet/admin-icons": "workspace:^6.2.1", "@mui/lab": "^5.0.0-alpha.76", "clipboard-copy": "^4.0.0", "clsx": "^1.1.1", @@ -42,9 +42,9 @@ "devDependencies": { "@babel/cli": "^7.17.6", "@babel/core": "^7.20.12", - "@comet/admin-babel-preset": "workspace:^6.2.0", - "@comet/cli": "workspace:^6.2.0", - "@comet/eslint-config": "workspace:^6.2.0", + "@comet/admin-babel-preset": "workspace:^6.2.1", + "@comet/cli": "workspace:^6.2.1", + "@comet/eslint-config": "workspace:^6.2.1", "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", "@mui/lab": "^5.0.0-alpha.76", diff --git a/packages/admin/cms-admin/CHANGELOG.md b/packages/admin/cms-admin/CHANGELOG.md index 3c1bfadb6f..3724aa996d 100644 --- a/packages/admin/cms-admin/CHANGELOG.md +++ b/packages/admin/cms-admin/CHANGELOG.md @@ -1,5 +1,16 @@ # @comet/cms-admin +## 6.2.1 + +### Patch Changes + +- @comet/admin@6.2.1 +- @comet/admin-date-time@6.2.1 +- @comet/admin-icons@6.2.1 +- @comet/admin-rte@6.2.1 +- @comet/admin-theme@6.2.1 +- @comet/blocks-admin@6.2.1 + ## 6.2.0 ### Minor Changes diff --git a/packages/admin/cms-admin/package.json b/packages/admin/cms-admin/package.json index 8aeebd0e26..c940398e8a 100644 --- a/packages/admin/cms-admin/package.json +++ b/packages/admin/cms-admin/package.json @@ -1,6 +1,6 @@ { "name": "@comet/cms-admin", - "version": "6.2.0", + "version": "6.2.1", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -34,12 +34,12 @@ "test:watch": "jest --watch" }, "dependencies": { - "@comet/admin": "workspace:^6.2.0", - "@comet/admin-date-time": "workspace:^6.2.0", - "@comet/admin-icons": "workspace:^6.2.0", - "@comet/admin-rte": "workspace:^6.2.0", - "@comet/admin-theme": "workspace:^6.2.0", - "@comet/blocks-admin": "workspace:^6.2.0", + "@comet/admin": "workspace:^6.2.1", + "@comet/admin-date-time": "workspace:^6.2.1", + "@comet/admin-icons": "workspace:^6.2.1", + "@comet/admin-rte": "workspace:^6.2.1", + "@comet/admin-theme": "workspace:^6.2.1", + "@comet/blocks-admin": "workspace:^6.2.1", "@graphql-tools/graphql-file-loader": "^7.5.17", "@graphql-tools/load": "^7.8.14", "@graphql-typed-document-node/core": "^3.1.1", @@ -80,9 +80,9 @@ "@apollo/client": "^3.7.0", "@babel/cli": "^7.17.6", "@babel/core": "^7.20.12", - "@comet/admin-babel-preset": "workspace:^6.2.0", - "@comet/cli": "workspace:^6.2.0", - "@comet/eslint-config": "workspace:^6.2.0", + "@comet/admin-babel-preset": "workspace:^6.2.1", + "@comet/cli": "workspace:^6.2.1", + "@comet/eslint-config": "workspace:^6.2.1", "@emotion/react": "^11.5.0", "@emotion/styled": "^11.3.0", "@graphql-codegen/cli": "^2.0.0", diff --git a/packages/api/blocks-api/CHANGELOG.md b/packages/api/blocks-api/CHANGELOG.md index 57c0541533..5668c8262a 100644 --- a/packages/api/blocks-api/CHANGELOG.md +++ b/packages/api/blocks-api/CHANGELOG.md @@ -1,5 +1,7 @@ # @comet/blocks-api +## 6.2.1 + ## 6.2.0 ### Minor Changes diff --git a/packages/api/blocks-api/package.json b/packages/api/blocks-api/package.json index 706407ebf9..9eacb4c5f6 100644 --- a/packages/api/blocks-api/package.json +++ b/packages/api/blocks-api/package.json @@ -1,6 +1,6 @@ { "name": "@comet/blocks-api", - "version": "6.2.0", + "version": "6.2.1", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -29,7 +29,7 @@ "rimraf": "^3.0.0" }, "devDependencies": { - "@comet/eslint-config": "workspace:^6.2.0", + "@comet/eslint-config": "workspace:^6.2.1", "@nestjs/common": "^9.0.0", "@types/draft-js": "^0.11.10", "@types/jest": "^29.5.0", diff --git a/packages/api/cms-api/CHANGELOG.md b/packages/api/cms-api/CHANGELOG.md index 21eb7605f8..09261c1d09 100644 --- a/packages/api/cms-api/CHANGELOG.md +++ b/packages/api/cms-api/CHANGELOG.md @@ -1,5 +1,15 @@ # @comet/cms-api +## 6.2.1 + +### Patch Changes + +- f1457306: Ignore user permissions when using system user + + The `UserPermissionsGuard` didn't allow requests when using a system user (e.g., basic authorization during site build). + + - @comet/blocks-api@6.2.1 + ## 6.2.0 ### Minor Changes diff --git a/packages/api/cms-api/package.json b/packages/api/cms-api/package.json index e3fce12ceb..c69214bed9 100644 --- a/packages/api/cms-api/package.json +++ b/packages/api/cms-api/package.json @@ -1,6 +1,6 @@ { "name": "@comet/cms-api", - "version": "6.2.0", + "version": "6.2.1", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -32,7 +32,7 @@ "test:watch": "jest --watch" }, "dependencies": { - "@comet/blocks-api": "workspace:^6.2.0", + "@comet/blocks-api": "workspace:^6.2.1", "@golevelup/nestjs-discovery": "^3.0.0", "@hapi/accept": "^5.0.2", "@nestjs/jwt": "^9.0.0", @@ -79,7 +79,7 @@ "@aws-sdk/client-s3": "^3.47.0", "@aws-sdk/types": "^3.47.0", "@azure/storage-blob": "^12.0.0", - "@comet/eslint-config": "workspace:^6.2.0", + "@comet/eslint-config": "workspace:^6.2.1", "@kubernetes/client-node": "^0.18.1", "@mikro-orm/cli": "^5.7.1", "@mikro-orm/core": "^5.7.1", diff --git a/packages/cli/CHANGELOG.md b/packages/cli/CHANGELOG.md index 2c526b2553..7ad16ce762 100644 --- a/packages/cli/CHANGELOG.md +++ b/packages/cli/CHANGELOG.md @@ -1,5 +1,7 @@ # @comet/cli +## 6.2.1 + ## 6.2.0 ## 6.1.0 diff --git a/packages/cli/package.json b/packages/cli/package.json index 4dbb8bed60..189035e3d1 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@comet/cli", - "version": "6.2.0", + "version": "6.2.1", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -29,7 +29,7 @@ "prettier": "^2.7.1" }, "devDependencies": { - "@comet/eslint-config": "^6.2.0", + "@comet/eslint-config": "^6.2.1", "@types/node": "^18.0.0", "eslint": "^8.0.0", "npm-run-all": "^4.1.5", diff --git a/packages/eslint-config/CHANGELOG.md b/packages/eslint-config/CHANGELOG.md index 68ef198ca2..f8db38be34 100644 --- a/packages/eslint-config/CHANGELOG.md +++ b/packages/eslint-config/CHANGELOG.md @@ -1,5 +1,11 @@ # @comet/eslint-config +## 6.2.1 + +### Patch Changes + +- @comet/eslint-plugin@6.2.1 + ## 6.2.0 ### Patch Changes diff --git a/packages/eslint-config/package.json b/packages/eslint-config/package.json index 731d29a9f8..e8b7511a60 100644 --- a/packages/eslint-config/package.json +++ b/packages/eslint-config/package.json @@ -1,6 +1,6 @@ { "name": "@comet/eslint-config", - "version": "6.2.0", + "version": "6.2.1", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -23,7 +23,7 @@ "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-simple-import-sort": "^9.0.0", "eslint-plugin-unused-imports": "^2.0.0", - "@comet/eslint-plugin": "workspace:^6.2.0" + "@comet/eslint-plugin": "workspace:^6.2.1" }, "devDependencies": { "eslint": "^8.32.0", diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index 3cf45fdd46..c19555a3a8 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -1,5 +1,7 @@ # @comet/eslint-plugin +## 6.2.1 + ## 6.2.0 ## 6.1.0 diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index 980e39d905..8a73ca846d 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@comet/eslint-plugin", - "version": "6.2.0", + "version": "6.2.1", "main": "lib/index.js", "scripts": { "build": "$npm_execpath run clean && tsc", diff --git a/packages/site/cms-site/CHANGELOG.md b/packages/site/cms-site/CHANGELOG.md index d4cf963828..91d3e44b99 100644 --- a/packages/site/cms-site/CHANGELOG.md +++ b/packages/site/cms-site/CHANGELOG.md @@ -1,5 +1,7 @@ # @comet/cms-site +## 6.2.1 + ## 6.2.0 ### Minor Changes diff --git a/packages/site/cms-site/package.json b/packages/site/cms-site/package.json index 7ffb0870b1..b252d3db87 100644 --- a/packages/site/cms-site/package.json +++ b/packages/site/cms-site/package.json @@ -1,6 +1,6 @@ { "name": "@comet/cms-site", - "version": "6.2.0", + "version": "6.2.1", "repository": { "type": "git", "url": "https://github.com/vivid-planet/comet", @@ -32,8 +32,8 @@ "use-debounce": "^6.0.0" }, "devDependencies": { - "@comet/cli": "workspace:^6.2.0", - "@comet/eslint-config": "workspace:^6.2.0", + "@comet/cli": "workspace:^6.2.1", + "@comet/eslint-config": "workspace:^6.2.1", "@gitbeaker/node": "^34.0.0", "@testing-library/react-hooks": "^8.0.0", "@types/draft-js": "^0.11.10", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5eab7ad9ee..43ea7e941f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -973,7 +973,7 @@ importers: packages/admin/admin: dependencies: '@comet/admin-icons': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin-icons '@mui/private-theming': specifier: ^5.0.0 @@ -1022,10 +1022,10 @@ importers: specifier: ^7.20.12 version: 7.20.12 '@comet/admin-babel-preset': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin-babel-preset '@comet/eslint-config': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../../eslint-config '@emotion/react': specifier: ^11.5.0 @@ -1193,10 +1193,10 @@ importers: packages/admin/admin-color-picker: dependencies: '@comet/admin': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin '@comet/admin-icons': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin-icons clsx: specifier: ^1.1.1 @@ -1218,10 +1218,10 @@ importers: specifier: ^7.20.12 version: 7.20.12 '@comet/admin-babel-preset': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin-babel-preset '@comet/eslint-config': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../../eslint-config '@mui/icons-material': specifier: ^5.0.0 @@ -1275,10 +1275,10 @@ importers: packages/admin/admin-date-time: dependencies: '@comet/admin': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin '@comet/admin-icons': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin-icons '@mui/utils': specifier: ^5.4.1 @@ -1300,10 +1300,10 @@ importers: specifier: ^7.20.12 version: 7.20.12 '@comet/admin-babel-preset': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin-babel-preset '@comet/eslint-config': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../../eslint-config '@mui/material': specifier: ^5.0.0 @@ -1360,10 +1360,10 @@ importers: specifier: ^7.20.12 version: 7.20.12 '@comet/admin-babel-preset': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin-babel-preset '@comet/eslint-config': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../../eslint-config '@mui/material': specifier: ^5.0.0 @@ -1417,7 +1417,7 @@ importers: packages/admin/admin-react-select: dependencies: '@comet/admin': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin classnames: specifier: ^2.2.6 @@ -1430,10 +1430,10 @@ importers: specifier: ^7.20.12 version: 7.20.12 '@comet/admin-babel-preset': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin-babel-preset '@comet/eslint-config': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../../eslint-config '@mui/icons-material': specifier: ^5.0.0 @@ -1487,10 +1487,10 @@ importers: packages/admin/admin-rte: dependencies: '@comet/admin': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin '@comet/admin-icons': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin-icons detect-browser: specifier: ^5.2.1 @@ -1515,10 +1515,10 @@ importers: specifier: ^7.20.12 version: 7.20.12 '@comet/admin-babel-preset': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin-babel-preset '@comet/eslint-config': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../../eslint-config '@mui/icons-material': specifier: ^5.0.0 @@ -1602,7 +1602,7 @@ importers: packages/admin/admin-theme: dependencies: '@comet/admin-icons': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin-icons '@mui/utils': specifier: ^5.4.1 @@ -1615,10 +1615,10 @@ importers: specifier: ^7.20.12 version: 7.20.12 '@comet/admin-babel-preset': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin-babel-preset '@comet/eslint-config': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../../eslint-config '@mui/material': specifier: ^5.0.0 @@ -1663,10 +1663,10 @@ importers: packages/admin/blocks-admin: dependencies: '@comet/admin': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin '@comet/admin-icons': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin-icons '@mui/lab': specifier: ^5.0.0-alpha.76 @@ -1697,13 +1697,13 @@ importers: specifier: ^7.20.12 version: 7.20.12 '@comet/admin-babel-preset': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin-babel-preset '@comet/cli': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../../cli '@comet/eslint-config': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../../eslint-config '@emotion/react': specifier: ^11.5.0 @@ -1796,22 +1796,22 @@ importers: packages/admin/cms-admin: dependencies: '@comet/admin': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin '@comet/admin-date-time': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin-date-time '@comet/admin-icons': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin-icons '@comet/admin-rte': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin-rte '@comet/admin-theme': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin-theme '@comet/blocks-admin': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../blocks-admin '@graphql-tools/graphql-file-loader': specifier: ^7.5.17 @@ -1929,13 +1929,13 @@ importers: specifier: ^7.20.12 version: 7.20.12 '@comet/admin-babel-preset': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../admin-babel-preset '@comet/cli': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../../cli '@comet/eslint-config': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../../eslint-config '@emotion/react': specifier: ^11.5.0 @@ -2113,7 +2113,7 @@ importers: version: 3.0.2 devDependencies: '@comet/eslint-config': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../../eslint-config '@nestjs/common': specifier: ^9.0.0 @@ -2170,7 +2170,7 @@ importers: packages/api/cms-api: dependencies: '@comet/blocks-api': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../blocks-api '@golevelup/nestjs-discovery': specifier: ^3.0.0 @@ -2306,7 +2306,7 @@ importers: specifier: ^12.0.0 version: 12.12.0 '@comet/eslint-config': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../../eslint-config '@kubernetes/client-node': specifier: ^0.18.1 @@ -2439,7 +2439,7 @@ importers: version: 2.8.3 devDependencies: '@comet/eslint-config': - specifier: ^6.2.0 + specifier: ^6.2.1 version: link:../eslint-config '@types/node': specifier: ^18.0.0 @@ -2463,7 +2463,7 @@ importers: specifier: ^1.4.1 version: 1.4.1 '@comet/eslint-plugin': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../eslint-plugin '@next/eslint-plugin-next': specifier: ^12.0.0 @@ -2561,10 +2561,10 @@ importers: version: 6.0.1(react@17.0.2) devDependencies: '@comet/cli': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../../cli '@comet/eslint-config': - specifier: workspace:^6.2.0 + specifier: workspace:^6.2.1 version: link:../../eslint-config '@gitbeaker/node': specifier: ^34.0.0 From 9733845d2888a1d97fc47d78b6efcc408c213e35 Mon Sep 17 00:00:00 2001 From: Phillip <69114037+Flips2001@users.noreply.github.com> Date: Tue, 27 Feb 2024 17:24:01 +0100 Subject: [PATCH 08/18] Uniform delete option in context menus (#1760) COM-485 Added a `` before any `} />` inside a `RowActionsMenu` that didn't have one. The now are all uniform. Co-authored-by: Phillip Lechenauer --- packages/admin/admin/src/dataGrid/CrudContextMenu.tsx | 3 ++- packages/admin/cms-admin/src/dam/DataGrid/DamContextMenu.tsx | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/admin/admin/src/dataGrid/CrudContextMenu.tsx b/packages/admin/admin/src/dataGrid/CrudContextMenu.tsx index 39e64b8f78..c183e152d4 100644 --- a/packages/admin/admin/src/dataGrid/CrudContextMenu.tsx +++ b/packages/admin/admin/src/dataGrid/CrudContextMenu.tsx @@ -1,6 +1,6 @@ import { ApolloClient, RefetchQueriesOptions, useApolloClient } from "@apollo/client"; import { Copy, Delete as DeleteIcon, Domain, Paste, ThreeDotSaving } from "@comet/admin-icons"; -import { Button, Dialog, DialogActions, DialogContent, DialogTitle } from "@mui/material"; +import { Button, Dialog, DialogActions, DialogContent, DialogTitle, Divider } from "@mui/material"; import * as React from "react"; import { FormattedMessage, useIntl } from "react-intl"; @@ -151,6 +151,7 @@ export function CrudContextMenu({ url, onPaste, onDelete, refetchQueri {intl.formatMessage(messages.paste)} )} + {onDelete && (onPaste || copyData || url) && } {onDelete && ( } diff --git a/packages/admin/cms-admin/src/dam/DataGrid/DamContextMenu.tsx b/packages/admin/cms-admin/src/dam/DataGrid/DamContextMenu.tsx index c752fbbcb3..310e3b2d0e 100644 --- a/packages/admin/cms-admin/src/dam/DataGrid/DamContextMenu.tsx +++ b/packages/admin/cms-admin/src/dam/DataGrid/DamContextMenu.tsx @@ -88,6 +88,7 @@ const FolderInnerMenu = ({ folder, openMoveDialog }: FolderInnerMenuProps): Reac > + } onClick={() => { From b7510969e9a351641f7ee824f1c56a79af5a651b Mon Sep 17 00:00:00 2001 From: Johannes Obermair <48853629+johnnyomair@users.noreply.github.com> Date: Wed, 28 Feb 2024 10:15:52 +0100 Subject: [PATCH 09/18] Install missing peer dependencies in docs (#1763) - `draft-js` is required by `@comet/admin-rte` - `react-select` is required by `@comet/admin-react-select` --- docs/package.json | 16 +++++++++------- pnpm-lock.yaml | 20 +++++++++++++------- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/docs/package.json b/docs/package.json index 5b93c2b5ce..9183733ed3 100644 --- a/docs/package.json +++ b/docs/package.json @@ -30,13 +30,13 @@ "dependencies": { "@algolia/client-search": "^4.18.0", "@apollo/client": "^3.7.0", - "@comet/admin": "*", - "@comet/admin-color-picker": "*", - "@comet/admin-date-time": "*", - "@comet/admin-icons": "*", - "@comet/admin-react-select": "*", - "@comet/admin-rte": "*", - "@comet/admin-theme": "*", + "@comet/admin": "workspace:*", + "@comet/admin-color-picker": "workspace:*", + "@comet/admin-date-time": "workspace:*", + "@comet/admin-icons": "workspace:*", + "@comet/admin-react-select": "workspace:*", + "@comet/admin-rte": "workspace:*", + "@comet/admin-theme": "workspace:*", "@docusaurus/core": "2.4.1", "@docusaurus/preset-classic": "2.4.1", "@docusaurus/theme-common": "2.4.1", @@ -51,6 +51,7 @@ "@mui/system": "^5.0.0", "@mui/x-data-grid": "^5.17.6", "clsx": "^1.2.1", + "draft-js": "^0.11.7", "final-form": "^4.20.9", "graphql": "^15.0.0", "history": "^4.10.1", @@ -65,6 +66,7 @@ "react-live": "^2.4.1", "react-router": "^5.0.0", "react-router-dom": "^5.0.0", + "react-select": "^3.2.0", "search-insights": "^2.7.0" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 43ea7e941f..c8381ad2d8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -828,25 +828,25 @@ importers: specifier: ^3.7.0 version: 3.7.4(graphql@15.8.0)(react-dom@17.0.2)(react@17.0.2) '@comet/admin': - specifier: '*' + specifier: workspace:* version: link:../packages/admin/admin '@comet/admin-color-picker': - specifier: '*' + specifier: workspace:* version: link:../packages/admin/admin-color-picker '@comet/admin-date-time': - specifier: '*' + specifier: workspace:* version: link:../packages/admin/admin-date-time '@comet/admin-icons': - specifier: '*' + specifier: workspace:* version: link:../packages/admin/admin-icons '@comet/admin-react-select': - specifier: '*' + specifier: workspace:* version: link:../packages/admin/admin-react-select '@comet/admin-rte': - specifier: '*' + specifier: workspace:* version: link:../packages/admin/admin-rte '@comet/admin-theme': - specifier: '*' + specifier: workspace:* version: link:../packages/admin/admin-theme '@docusaurus/core': specifier: 2.4.1 @@ -890,6 +890,9 @@ importers: clsx: specifier: ^1.2.1 version: 1.2.1 + draft-js: + specifier: ^0.11.7 + version: 0.11.7(react-dom@17.0.2)(react@17.0.2) final-form: specifier: ^4.20.9 version: 4.20.9 @@ -932,6 +935,9 @@ importers: react-router-dom: specifier: ^5.0.0 version: 5.3.4(react@17.0.2) + react-select: + specifier: ^3.2.0 + version: 3.2.0(react-dom@17.0.2)(react@17.0.2) search-insights: specifier: ^2.7.0 version: 2.7.0 From c339fa738f6aad9e1951f3601320b6d1f798ef83 Mon Sep 17 00:00:00 2001 From: Johannes Obermair <48853629+johnnyomair@users.noreply.github.com> Date: Wed, 28 Feb 2024 10:35:31 +0100 Subject: [PATCH 10/18] Fix main-into-next-pr workflow (#1767) Need an update to create-pull-request@v6 to fix [an issue with the GitHub API](https://github.com/peter-evans/create-pull-request/issues/2790). --- .github/workflows/main-into-next-pr.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/main-into-next-pr.yml b/.github/workflows/main-into-next-pr.yml index ca9a473789..633bd3901b 100644 --- a/.github/workflows/main-into-next-pr.yml +++ b/.github/workflows/main-into-next-pr.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 # Needed to also fetch next branch @@ -36,7 +36,7 @@ jobs: echo 'PR_BODY=This is an automated pull request to merge changes from `main` into `next`. It has merge conflicts. To resolve conflicts, check out the branch `merge-main-into-next` locally, make any necessary changes to conflicting files, and commit and publish your changes.' >> $GITHUB_ENV - name: Create pull request - uses: peter-evans/create-pull-request@v5 + uses: peter-evans/create-pull-request@v6 with: token: ${{ secrets.GITHUB_TOKEN }} title: ${{ env.PR_TITLE }} From cd912d1cb31e76e3388035ce52c33085fa26a144 Mon Sep 17 00:00:00 2001 From: Johannes Obermair Date: Wed, 28 Feb 2024 10:38:23 +0100 Subject: [PATCH 11/18] Resolve conflicts --- pnpm-lock.yaml | 262 +------------------------------------------------ 1 file changed, 1 insertion(+), 261 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 873d36c0b7..6fed64a0e1 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -828,28 +828,6 @@ importers: specifier: ^3.7.0 version: 3.7.4(graphql@15.8.0)(react-dom@17.0.2)(react@17.0.2) '@comet/admin': -<<<<<<< HEAD - specifier: '*' - version: 6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2) - '@comet/admin-color-picker': - specifier: '*' - version: 6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2) - '@comet/admin-date-time': - specifier: '*' - version: 6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2) - '@comet/admin-icons': - specifier: '*' - version: 6.0.0(@mui/material@5.11.6)(react-dom@17.0.2)(react@17.0.2) - '@comet/admin-react-select': - specifier: '*' - version: 6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2) - '@comet/admin-rte': - specifier: '*' - version: 6.0.0(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(final-form@4.20.9)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react@17.0.2) - '@comet/admin-theme': - specifier: '*' - version: 6.0.0(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/system@5.11.5)(react-dom@17.0.2)(react@17.0.2) -======= specifier: workspace:* version: link:../packages/admin/admin '@comet/admin-color-picker': @@ -870,7 +848,6 @@ importers: '@comet/admin-theme': specifier: workspace:* version: link:../packages/admin/admin-theme ->>>>>>> main '@docusaurus/core': specifier: 2.4.1 version: 2.4.1(@docusaurus/types@2.4.1)(react-dom@17.0.2)(react@17.0.2)(typescript@4.9.4) @@ -7123,243 +7100,6 @@ packages: requiresBuild: true optional: true - /@comet/admin-color-picker@6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2): - resolution: {integrity: sha512-5gYTW4E2W0PLT1251TYtG8dUtx28pwEWE1O8tjlXGMge5e3YBz9MCrPE6Nn9EqfkJ89MJnSagCK2dLi8BdJ0CQ==} - peerDependencies: - '@mui/icons-material': ^5.0.0 - '@mui/material': ^5.0.0 - '@mui/styles': ^5.0.0 - react: ^17.0 - react-dom: ^17.0 - react-final-form: ^6.3.1 - react-intl: ^5.24.6 - dependencies: - '@comet/admin': 6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2) - '@comet/admin-icons': 6.0.0(@mui/material@5.11.6)(react-dom@17.0.2)(react@17.0.2) - '@mui/icons-material': 5.11.0(@mui/material@5.11.6)(@types/react@17.0.53)(react@17.0.2) - '@mui/material': 5.11.6(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) - '@mui/styles': 5.11.2(@types/react@17.0.53)(react@17.0.2) - clsx: 1.2.1 - react: 17.0.2 - react-colorful: 5.6.1(react-dom@17.0.2)(react@17.0.2) - react-dom: 17.0.2(react@17.0.2) - react-final-form: 6.5.9(final-form@4.20.9)(react@17.0.2) - react-intl: 5.25.1(react@17.0.2)(typescript@4.9.4) - tinycolor2: 1.5.2 - use-debounce: 6.0.1(react@17.0.2) - transitivePeerDependencies: - - '@apollo/client' - - '@emotion/react' - - '@emotion/styled' - - '@mui/x-data-grid' - - '@mui/x-data-grid-premium' - - '@mui/x-data-grid-pro' - - '@types/react' - - final-form - - graphql - - history - - react-dnd - - react-router - - react-router-dom - dev: false - - /@comet/admin-date-time@6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2): - resolution: {integrity: sha512-fm2wfw0RNl+LiB+3DE491QDlcOddL052+Cjr80A6bzHNbhD6N3DpL3nsWbF5BYZL1cX66hJSFrBgmR13d3hxNQ==} - peerDependencies: - '@mui/material': ^5.0.0 - '@mui/styles': ^5.0.0 - react: ^17.0 - react-dom: ^17.0 - react-final-form: ^6.5.7 - react-intl: ^5.24.6 - dependencies: - '@comet/admin': 6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2) - '@comet/admin-icons': 6.0.0(@mui/material@5.11.6)(react-dom@17.0.2)(react@17.0.2) - '@mui/material': 5.11.6(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) - '@mui/styles': 5.11.2(@types/react@17.0.53)(react@17.0.2) - '@mui/utils': 5.11.2(react@17.0.2) - clsx: 1.2.1 - date-fns: 2.29.3 - react: 17.0.2 - react-date-range: 1.4.0(date-fns@2.29.3)(react@17.0.2) - react-dom: 17.0.2(react@17.0.2) - react-final-form: 6.5.9(final-form@4.20.9)(react@17.0.2) - react-intl: 5.25.1(react@17.0.2)(typescript@4.9.4) - transitivePeerDependencies: - - '@apollo/client' - - '@emotion/react' - - '@emotion/styled' - - '@mui/icons-material' - - '@mui/x-data-grid' - - '@mui/x-data-grid-premium' - - '@mui/x-data-grid-pro' - - '@types/react' - - final-form - - graphql - - history - - react-dnd - - react-router - - react-router-dom - dev: false - - /@comet/admin-icons@6.0.0(@mui/material@5.11.6)(react-dom@17.0.2)(react@17.0.2): - resolution: {integrity: sha512-VNh+62H3dnZ5pvJEUX5MJ2zLir6fiTPXYkDl8yCmJ48Vm/YxagTljlCUbp+DcJYrM6PA/qMlheE6lnqdIG3+LA==} - peerDependencies: - '@mui/material': ^5.0.0 - react: ^17.0 - react-dom: ^17.0 - dependencies: - '@mui/material': 5.11.6(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) - react: 17.0.2 - react-dom: 17.0.2(react@17.0.2) - dev: false - - /@comet/admin-react-select@6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2): - resolution: {integrity: sha512-36VWyrmorT6Sqvd5Fu7hocpFCWituEQRyruEexG2HdWq1Ts4B+yyheCul9lfcla3A+XMHPqmK9t7oyuUP/yG2Q==} - peerDependencies: - '@mui/icons-material': ^5.0.0 - '@mui/material': ^5.0.0 - '@mui/styles': ^5.0.0 - final-form: ^4.16.1 - react: ^17.0 - react-dom: ^17.0 - react-final-form: ^6.3.1 - react-select: ^3.0.4 - dependencies: - '@comet/admin': 6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2) - '@mui/icons-material': 5.11.0(@mui/material@5.11.6)(@types/react@17.0.53)(react@17.0.2) - '@mui/material': 5.11.6(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) - '@mui/styles': 5.11.2(@types/react@17.0.53)(react@17.0.2) - classnames: 2.3.2 - final-form: 4.20.9 - react: 17.0.2 - react-dom: 17.0.2(react@17.0.2) - react-final-form: 6.5.9(final-form@4.20.9)(react@17.0.2) - transitivePeerDependencies: - - '@apollo/client' - - '@emotion/react' - - '@emotion/styled' - - '@mui/x-data-grid' - - '@mui/x-data-grid-premium' - - '@mui/x-data-grid-pro' - - '@types/react' - - graphql - - history - - react-dnd - - react-intl - - react-router - - react-router-dom - dev: false - - /@comet/admin-rte@6.0.0(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(final-form@4.20.9)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react@17.0.2): - resolution: {integrity: sha512-9WddOXtYH71a4hmjyexZHurEauEV6Tr2BFmQjCaf77LUrHOuDbH9/FMicqyu10lrD61zvJRrxLxsEbpt6r7WnQ==} - peerDependencies: - '@mui/icons-material': ^5.0.0 - '@mui/material': ^5.0.0 - '@mui/styles': ^5.0.0 - draft-js: ^0.11.4 - final-form: ^4.16.1 - react: ^17.0 - react-dom: ^17.0 - react-final-form: ^6.3.1 - react-intl: ^5.10.0 - dependencies: - '@comet/admin-icons': 6.0.0(@mui/material@5.11.6)(react-dom@17.0.2)(react@17.0.2) - '@mui/icons-material': 5.11.0(@mui/material@5.11.6)(@types/react@17.0.53)(react@17.0.2) - '@mui/material': 5.11.6(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) - '@mui/styles': 5.11.2(@types/react@17.0.53)(react@17.0.2) - detect-browser: 5.3.0 - draftjs-conductor: 3.0.0(draft-js@0.11.7) - final-form: 4.20.9 - immutable: 3.7.6 - react: 17.0.2 - react-dom: 17.0.2(react@17.0.2) - react-final-form: 6.5.9(final-form@4.20.9)(react@17.0.2) - react-intl: 5.25.1(react@17.0.2)(typescript@4.9.4) - dev: false - - /@comet/admin-theme@6.0.0(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/system@5.11.5)(react-dom@17.0.2)(react@17.0.2): - resolution: {integrity: sha512-3KouYLXS7UY9UdALfC2NNX6V+GaLilaSdpLTRRFa4y7Ss+bgktW13t19AvP3D9A3H78+3zn3Y2BPn19o+xwZ+Q==} - peerDependencies: - '@mui/material': ^5.0.0 - '@mui/styles': ^5.0.0 - '@mui/system': ^5.0.0 - react: ^17.0 - dependencies: - '@comet/admin-icons': 6.0.0(@mui/material@5.11.6)(react-dom@17.0.2)(react@17.0.2) - '@mui/material': 5.11.6(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) - '@mui/styles': 5.11.2(@types/react@17.0.53)(react@17.0.2) - '@mui/system': 5.11.5(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@types/react@17.0.53)(react@17.0.2) - '@mui/utils': 5.11.2(react@17.0.2) - react: 17.0.2 - transitivePeerDependencies: - - react-dom - dev: false - - /@comet/admin@6.0.0(@apollo/client@3.7.4)(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@mui/icons-material@5.11.0)(@mui/material@5.11.6)(@mui/styles@5.11.2)(@mui/x-data-grid@5.17.20)(@types/react@17.0.53)(final-form@4.20.9)(graphql@15.8.0)(history@4.10.1)(react-dnd@16.0.1)(react-dom@17.0.2)(react-final-form@6.5.9)(react-intl@5.25.1)(react-router-dom@5.3.4)(react-router@5.3.4)(react@17.0.2): - resolution: {integrity: sha512-sKqKw2KD9BgvCjctzg86O3V3T5U7ouUCCMPRRszW7kmR5WF+OMQGCpS8PolSu7WS+mTunLxMV9ZhbS08HQzcJQ==} - peerDependencies: - '@apollo/client': ^3.7.0 - '@emotion/react': ^11.5.0 - '@emotion/styled': ^11.3.0 - '@mui/icons-material': ^5.0.0 - '@mui/material': ^5.0.0 - '@mui/styles': ^5.0.0 - '@mui/x-data-grid': ^5.0.0 - '@mui/x-data-grid-premium': ^5.0.0 - '@mui/x-data-grid-pro': ^5.0.0 - final-form: ^4.16.1 - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 - history: ^4.10.1 - react: ^17.0 - react-dnd: ^16.0.0 - react-dom: ^17.0 - react-final-form: ^6.3.1 - react-intl: ^5.10.0 - react-router: ^5.1.2 - react-router-dom: ^5.1.2 - peerDependenciesMeta: - '@mui/x-data-grid-premium': - optional: true - '@mui/x-data-grid-pro': - optional: true - react-dnd: - optional: true - dependencies: - '@apollo/client': 3.7.4(graphql@15.8.0)(react-dom@17.0.2)(react@17.0.2) - '@comet/admin-icons': 6.0.0(@mui/material@5.11.6)(react-dom@17.0.2)(react@17.0.2) - '@emotion/react': 11.9.3(@babel/core@7.22.11)(@types/react@17.0.53)(react@17.0.2) - '@emotion/styled': 11.10.5(@babel/core@7.22.11)(@emotion/react@11.9.3)(@types/react@17.0.53)(react@17.0.2) - '@mui/icons-material': 5.11.0(@mui/material@5.11.6)(@types/react@17.0.53)(react@17.0.2) - '@mui/material': 5.11.6(@emotion/react@11.9.3)(@emotion/styled@11.10.5)(@types/react@17.0.53)(react-dom@17.0.2)(react@17.0.2) - '@mui/private-theming': 5.11.2(@types/react@17.0.53)(react@17.0.2) - '@mui/styles': 5.11.2(@types/react@17.0.53)(react@17.0.2) - '@mui/x-data-grid': 5.17.20(@mui/material@5.11.6)(@mui/system@5.11.5)(react-dom@17.0.2)(react@17.0.2) - clsx: 1.2.1 - exceljs: 3.10.0 - file-saver: 2.0.5 - final-form: 4.20.9 - final-form-set-field-data: 1.0.2(final-form@4.20.9) - graphql: 15.8.0 - history: 4.10.1 - http-status-codes: 2.3.0 - is-mobile: 4.0.0 - lodash.debounce: 4.0.8 - lodash.isequal: 4.5.0 - query-string: 6.14.1 - react: 17.0.2 - react-dnd: 16.0.1(@types/node@18.15.3)(@types/react@17.0.53)(react@17.0.2) - react-dom: 17.0.2(react@17.0.2) - react-final-form: 6.5.9(final-form@4.20.9)(react@17.0.2) - react-intl: 5.25.1(react@17.0.2)(typescript@4.9.4) - react-router: 5.3.4(react@17.0.2) - react-router-dom: 5.3.4(react@17.0.2) - use-constant: 1.1.1(react@17.0.2) - uuid: 9.0.0 - transitivePeerDependencies: - - '@types/react' - dev: false - /@comet/dev-process-manager@2.3.2: resolution: {integrity: sha512-SOP1H8rZBpNhgRzFMofiZPtXYzU16s/uD4ME3J7IXPtqsHNkSjm+WD1LzpK1czVqWAGgowZsXbjL46cYPt41oA==} engines: {node: '>=14'} @@ -16423,7 +16163,7 @@ packages: /babel-plugin-emotion@10.2.2: resolution: {integrity: sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA==} dependencies: - '@babel/helper-module-imports': 7.18.6 + '@babel/helper-module-imports': 7.22.5 '@emotion/hash': 0.8.0 '@emotion/memoize': 0.7.4 '@emotion/serialize': 0.11.16 From f74544524099a0a9867513b46feee1e4d7057c6b Mon Sep 17 00:00:00 2001 From: Franz Unger Date: Thu, 29 Feb 2024 07:52:54 +0100 Subject: [PATCH 12/18] Use locale instead language for CurrentUser (#1777) --- .changeset/calm-plums-taste.md | 6 ++++++ demo/api/schema.gql | 8 ++++---- demo/api/src/auth/static-users.ts | 4 ++-- packages/admin/cms-admin/src/userPermissions/UserGrid.tsx | 7 +++---- .../cms-admin/src/userPermissions/hooks/currentUser.tsx | 3 ++- .../src/userPermissions/user/basicData/UserBasicData.tsx | 6 +++--- packages/api/cms-api/schema.gql | 8 ++++---- .../src/auth/strategies/auth-proxy-jwt.strategy.ts | 2 +- .../api/cms-api/src/user-permissions/dto/current-user.ts | 2 +- .../src/user-permissions/dto/paginated-user-list.ts | 4 ++-- packages/api/cms-api/src/user-permissions/dto/user.ts | 2 +- 11 files changed, 29 insertions(+), 23 deletions(-) create mode 100644 .changeset/calm-plums-taste.md diff --git a/.changeset/calm-plums-taste.md b/.changeset/calm-plums-taste.md new file mode 100644 index 0000000000..7b9b735953 --- /dev/null +++ b/.changeset/calm-plums-taste.md @@ -0,0 +1,6 @@ +--- +"@comet/cms-admin": major +"@comet/cms-api": major +--- + +Change language field in User and CurrentUser to locale diff --git a/demo/api/schema.gql b/demo/api/schema.gql index 8eaa014b62..532477c959 100644 --- a/demo/api/schema.gql +++ b/demo/api/schema.gql @@ -16,7 +16,7 @@ type CurrentUser { id: String! name: String! email: String! - language: String! + locale: String! permissions: [CurrentUserPermission!]! } @@ -179,7 +179,7 @@ type User { id: String! name: String! email: String! - language: String! + locale: String! } type PaginatedUserList { @@ -729,7 +729,7 @@ input UserFilter { name: StringFilter email: StringFilter status: StringFilter - language: StringFilter + locale: StringFilter and: [UserFilter!] or: [UserFilter!] } @@ -751,7 +751,7 @@ enum UserSortField { name email status - language + locale } enum SortDirection { diff --git a/demo/api/src/auth/static-users.ts b/demo/api/src/auth/static-users.ts index 8e998c8ce7..6ce76fbdf1 100644 --- a/demo/api/src/auth/static-users.ts +++ b/demo/api/src/auth/static-users.ts @@ -5,12 +5,12 @@ export const staticUsers: User[] = [ id: "1", name: "Admin", email: "demo@comet-dxp.com", - language: "en", + locale: "en", }, { id: "2", name: "Non-Admin", email: "test@test.com", - language: "en", + locale: "en", }, ]; diff --git a/packages/admin/cms-admin/src/userPermissions/UserGrid.tsx b/packages/admin/cms-admin/src/userPermissions/UserGrid.tsx index 5ccf87d086..41e841ff09 100644 --- a/packages/admin/cms-admin/src/userPermissions/UserGrid.tsx +++ b/packages/admin/cms-admin/src/userPermissions/UserGrid.tsx @@ -44,11 +44,10 @@ export const UserGrid: React.FC = () => { headerName: intl.formatMessage({ id: "comet.userPermissions.email", defaultMessage: "E-Mail" }), }, { - field: "language", + field: "locale", flex: 0.5, pinnable: false, - headerName: intl.formatMessage({ id: "comet.userPermissions.language", defaultMessage: "Language" }), - renderCell: ({ row }) => row.language.toUpperCase(), + headerName: intl.formatMessage({ id: "comet.userPermissions.locale", defaultMessage: "Locale" }), }, { field: "actions", @@ -82,7 +81,7 @@ export const UserGrid: React.FC = () => { id name email - language + locale } `, { diff --git a/packages/admin/cms-admin/src/userPermissions/hooks/currentUser.tsx b/packages/admin/cms-admin/src/userPermissions/hooks/currentUser.tsx index baea62a2f3..32734f8c0d 100644 --- a/packages/admin/cms-admin/src/userPermissions/hooks/currentUser.tsx +++ b/packages/admin/cms-admin/src/userPermissions/hooks/currentUser.tsx @@ -17,7 +17,7 @@ export const CurrentUserContext = React.createContext } + label={} /> diff --git a/packages/api/cms-api/schema.gql b/packages/api/cms-api/schema.gql index 3c009ad8bb..5597cb6e24 100644 --- a/packages/api/cms-api/schema.gql +++ b/packages/api/cms-api/schema.gql @@ -12,7 +12,7 @@ type CurrentUser { id: String! name: String! email: String! - language: String! + locale: String! permissions: [CurrentUserPermission!]! } @@ -175,7 +175,7 @@ type User { id: String! name: String! email: String! - language: String! + locale: String! } type PaginatedUserList { @@ -480,7 +480,7 @@ input UserFilter { name: StringFilter email: StringFilter status: StringFilter - language: StringFilter + locale: StringFilter and: [UserFilter!] or: [UserFilter!] } @@ -494,7 +494,7 @@ enum UserSortField { name email status - language + locale } type Mutation { diff --git a/packages/api/cms-api/src/auth/strategies/auth-proxy-jwt.strategy.ts b/packages/api/cms-api/src/auth/strategies/auth-proxy-jwt.strategy.ts index d85cdfffb1..10246ad4b7 100644 --- a/packages/api/cms-api/src/auth/strategies/auth-proxy-jwt.strategy.ts +++ b/packages/api/cms-api/src/auth/strategies/auth-proxy-jwt.strategy.ts @@ -39,7 +39,7 @@ export function createAuthProxyJwtStrategy({ id: data.sub, name: data.name, email: data.email, - language: data.language, + locale: data.locale, }); } } diff --git a/packages/api/cms-api/src/user-permissions/dto/current-user.ts b/packages/api/cms-api/src/user-permissions/dto/current-user.ts index d895a76739..c27b0f1bc3 100644 --- a/packages/api/cms-api/src/user-permissions/dto/current-user.ts +++ b/packages/api/cms-api/src/user-permissions/dto/current-user.ts @@ -20,7 +20,7 @@ export class CurrentUser { @Field() email: string; @Field() - language: string; + locale: string; @Field(() => [CurrentUserPermission]) permissions: CurrentUserPermission[]; } diff --git a/packages/api/cms-api/src/user-permissions/dto/paginated-user-list.ts b/packages/api/cms-api/src/user-permissions/dto/paginated-user-list.ts index 15d1455941..93ed4d5b33 100644 --- a/packages/api/cms-api/src/user-permissions/dto/paginated-user-list.ts +++ b/packages/api/cms-api/src/user-permissions/dto/paginated-user-list.ts @@ -26,7 +26,7 @@ class UserFilter { @Field(() => StringFilter, { nullable: true }) @ValidateNested() @Type(() => StringFilter) - language?: StringFilter; + locale?: StringFilter; @Field(() => [UserFilter], { nullable: true }) @Type(() => UserFilter) @@ -43,7 +43,7 @@ enum UserSortField { name = "name", email = "email", status = "status", - language = "language", + locale = "locale", } registerEnumType(UserSortField, { name: "UserSortField", diff --git a/packages/api/cms-api/src/user-permissions/dto/user.ts b/packages/api/cms-api/src/user-permissions/dto/user.ts index a19fe0fc69..5778dc5caf 100644 --- a/packages/api/cms-api/src/user-permissions/dto/user.ts +++ b/packages/api/cms-api/src/user-permissions/dto/user.ts @@ -12,5 +12,5 @@ export class User { email: string; @Field() - language: string; + locale: string; } From 37050333073aadd2a97ce7a4c2a1ef725097db0b Mon Sep 17 00:00:00 2001 From: Niko Sams Date: Mon, 4 Mar 2024 11:21:54 +0100 Subject: [PATCH 13/18] Replace `EditDialogFormApi` with more generic `SaveBoundary` (#1449) This replaces the custom EditDialogFormApi with SaveRange. Much cleaner and more generic because: - the EditDialog can now contain multiple forms or other components that save things (aka have a ``) - EditDialog has no dependency on a Form specific api --- packages/admin/admin/src/EditDialog.tsx | 87 +++++++------------ .../admin/src/EditDialogFormApiContext.tsx | 44 ---------- packages/admin/admin/src/FinalForm.tsx | 19 ++-- packages/admin/admin/src/router/Prompt.tsx | 2 +- 4 files changed, 36 insertions(+), 116 deletions(-) delete mode 100644 packages/admin/admin/src/EditDialogFormApiContext.tsx diff --git a/packages/admin/admin/src/EditDialog.tsx b/packages/admin/admin/src/EditDialog.tsx index 93a9d64532..cbe47e7c3b 100644 --- a/packages/admin/admin/src/EditDialog.tsx +++ b/packages/admin/admin/src/EditDialog.tsx @@ -9,15 +9,13 @@ import { DialogTitleProps, } from "@mui/material"; import * as React from "react"; -import { FormattedMessage, useIntl } from "react-intl"; +import { useIntl } from "react-intl"; import { CancelButton } from "./common/buttons/cancel/CancelButton"; -import { SaveButton } from "./common/buttons/save/SaveButton"; -import { CloseDialogOptions, EditDialogApiContext, IEditDialogApi } from "./EditDialogApiContext"; -import { EditDialogFormApiProvider, useEditDialogFormApi } from "./EditDialogFormApiContext"; +import { CloseDialogOptions, IEditDialogApi } from "./EditDialogApiContext"; import { messages } from "./messages"; -import { RouterContext } from "./router/Context"; -import { SaveAction } from "./router/PromptHandler"; +import { SaveBoundary } from "./saveBoundary/SaveBoundary"; +import { SaveBoundarySaveButton } from "./saveBoundary/SaveBoundarySaveButton"; import { ISelectionApi } from "./SelectionApi"; import { useSelectionRoute } from "./SelectionRoute"; @@ -85,9 +83,7 @@ export function useEditDialog(): [React.ComponentType, { id?: s return (props: EditDialogProps) => { return ( - - - + ); }; @@ -116,32 +112,12 @@ const EditDialogInner: React.FunctionComponent = ( componentsProps, }) => { const intl = useIntl(); - const editDialogFormApi = useEditDialogFormApi(); - const parentRouterContext = React.useContext(RouterContext); - const saveActionRef = React.useRef(); const title = maybeTitle ?? { edit: intl.formatMessage(messages.edit), add: intl.formatMessage(messages.add), }; - const handleSaveClick = async () => { - if (!saveActionRef.current) { - console.error("Can't save, no RouterPrompt registered with saveAction"); - return; - } - const saveResult = await saveActionRef.current(); - - if (saveResult) { - setTimeout(() => { - // TODO DirtyHandler removal: do we need a onReset functionality here? - if (!disableCloseAfterSave) { - api.closeDialog({ delay: true }); - } - }); - } - }; - const handleCancelClick = () => { // TODO DirtyHandler removal: do we need a onReset functionality here? api.closeDialog(); @@ -151,38 +127,33 @@ const EditDialogInner: React.FunctionComponent = ( api.closeDialog(); }; + const handleAfterSave = React.useCallback(() => { + setTimeout(() => { + // TODO DirtyHandler removal: do we need a onReset functionality here? + if (!disableCloseAfterSave) { + api.closeDialog({ delay: true }); + } + }); + onAfterSave?.(); + }, [api, disableCloseAfterSave, onAfterSave]); + const isOpen = !!selection.mode; return ( - { - saveActionRef.current = saveAction; - parentRouterContext?.register({ saveAction, ...args }); - }, - unregister: (id) => { - saveActionRef.current = undefined; - parentRouterContext?.unregister(id); - }, - }} - > - - -
- - {typeof title === "string" ? title : selection.mode === "edit" ? title.edit : title.add} - - {children} - - - - - - -
-
-
-
+ + +
+ + {typeof title === "string" ? title : selection.mode === "edit" ? title.edit : title.add} + + {children} + + + + +
+
+
); }; diff --git a/packages/admin/admin/src/EditDialogFormApiContext.tsx b/packages/admin/admin/src/EditDialogFormApiContext.tsx deleted file mode 100644 index 2bfeae42fe..0000000000 --- a/packages/admin/admin/src/EditDialogFormApiContext.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import * as React from "react"; - -type FormStatus = "saving" | "error"; - -export interface EditDialogFormApi { - saving: boolean; - hasErrors: boolean; - onFormStatusChange: (status: FormStatus) => void; - resetFormStatus: () => void; - onAfterSave?: () => void; -} - -export const EditDialogFormApiContext = React.createContext(null); -export function useEditDialogFormApi() { - return React.useContext(EditDialogFormApiContext); -} - -type EditDialogFormApiProviderProps = { - onAfterSave?: () => void; -}; - -export const EditDialogFormApiProvider: React.FunctionComponent = ({ children, onAfterSave }) => { - const [status, setStatus] = React.useState(null); - - const onFormStatusChange = React.useCallback((status: FormStatus) => { - setStatus(status); - }, []); - - const resetFormStatus = React.useCallback(() => { - setStatus(null); - }, []); - - const editDialogFormApi: EditDialogFormApi = React.useMemo(() => { - return { - saving: status === "saving", - hasErrors: status === "error", - onFormStatusChange, - resetFormStatus, - onAfterSave, - }; - }, [onFormStatusChange, resetFormStatus, status, onAfterSave]); - - return {children}; -}; diff --git a/packages/admin/admin/src/FinalForm.tsx b/packages/admin/admin/src/FinalForm.tsx index efd15f1ad0..b2cb29529c 100644 --- a/packages/admin/admin/src/FinalForm.tsx +++ b/packages/admin/admin/src/FinalForm.tsx @@ -5,7 +5,6 @@ import * as React from "react"; import { AnyObject, Form, FormRenderProps, FormSpy, RenderableProps } from "react-final-form"; import { useIntl } from "react-intl"; -import { useEditDialogFormApi } from "./EditDialogFormApiContext"; import { renderComponent } from "./finalFormRenderComponent"; import { FinalFormContext, FinalFormContextProvider } from "./form/FinalFormContextProvider"; import { messages } from "./messages"; @@ -102,7 +101,6 @@ export class FinalFormSubmitEvent extends Event { export function FinalForm(props: IProps) { const { client } = React.useContext(getApolloContext()); const tableQuery = React.useContext(TableQueryContext); - const editDialogFormApi = useEditDialogFormApi(); const { onAfterSubmit, validateWarning } = props; @@ -131,6 +129,10 @@ export function FinalForm(props: IProps) { const submit = React.useCallback( (event: any) => { event.preventDefault(); // Prevents from reloading the page with GET-params on submit + if (saveBoundaryApi) { + // if we are inside a SaveBoundary, save the whole SaveBoundary + return saveBoundaryApi.save(); + } if (!formRenderProps.dirty) return; return new Promise((resolve) => { Promise.resolve(formRenderProps.handleSubmit(event)).then( @@ -147,7 +149,7 @@ export function FinalForm(props: IProps) { ); }); }, - [formRenderProps], + [formRenderProps, saveBoundaryApi], ); const currentWarningValidationRound = React.useRef(0); @@ -184,16 +186,13 @@ export function FinalForm(props: IProps) { }, [formRenderProps.values, setFieldData, registeredFields]); const doSave = React.useCallback(async () => { - editDialogFormApi?.onFormStatusChange("saving"); const hasValidationErrors = await waitForValidationToFinish(formRenderProps.form); if (hasValidationErrors) { - editDialogFormApi?.onFormStatusChange("error"); return false; } const submissionErrors = await formRenderProps.form.submit(); if (submissionErrors) { - editDialogFormApi?.onFormStatusChange("error"); return false; } @@ -235,8 +234,7 @@ export function FinalForm(props: IProps) { async function handleSubmit(values: FormValues, form: FormApi) { const submitEvent = (form.mutators.getSubmitEvent ? form.mutators.getSubmitEvent() : undefined) || new FinalFormSubmitEvent("submit"); const ret = props.onSubmit(values, form, submitEvent); - - editDialogFormApi?.onFormStatusChange("saving"); + if (ret === undefined) return ret; return Promise.resolve(ret) .then((data) => { @@ -254,21 +252,16 @@ export function FinalForm(props: IProps) { } onAfterSubmit?.(values, form); - editDialogFormApi?.onAfterSave?.(); }); return data; }) .then( (data) => { // for final-form undefined means success, an obj means error - editDialogFormApi?.resetFormStatus(); - form.reset(values); return undefined; }, (error) => { - editDialogFormApi?.onFormStatusChange("error"); - if (props.resolveSubmitErrors) { return props.resolveSubmitErrors(error); } diff --git a/packages/admin/admin/src/router/Prompt.tsx b/packages/admin/admin/src/router/Prompt.tsx index 2ffc42f08c..2c48d3e8b6 100644 --- a/packages/admin/admin/src/router/Prompt.tsx +++ b/packages/admin/admin/src/router/Prompt.tsx @@ -21,7 +21,7 @@ interface IProps { export const RouterPrompt: React.FunctionComponent = ({ message, saveAction, subRoutePath, children }) => { const id = useConstant(() => uuid()); const reactRouterContext = React.useContext(__RouterContext); // reactRouterContext can be undefined if no router is used, don't fail in that case - const path: string | undefined = reactRouterContext?.match.path; + const path: string | undefined = reactRouterContext?.match?.path; const context = React.useContext(RouterContext); React.useEffect(() => { if (context) { From 9c49738a9f61ea11f2e35aeee648f77f1c5ccb13 Mon Sep 17 00:00:00 2001 From: Niko Sams Date: Mon, 4 Mar 2024 12:27:56 +0100 Subject: [PATCH 14/18] Admin Generator (Future): Implement first, basic version of asyncSelect (#1711) Usage example: ``` { type: "asyncSelect", name: "category", rootQuery: "productCategories" }, ``` for generated code see diff in demo admin product form. --- .../products/future/ProductForm.cometGen.ts | 2 +- .../future/generated/ProductForm.gql.tsx | 20 ++++ .../products/future/generated/ProductForm.tsx | 22 ++++- .../src/generator/future/generateForm.ts | 71 ++++++++------ .../src/generator/future/generateFormField.ts | 92 ++++++++++++++++++- .../src/generator/future/generator.ts | 2 +- 6 files changed, 173 insertions(+), 36 deletions(-) diff --git a/demo/admin/src/products/future/ProductForm.cometGen.ts b/demo/admin/src/products/future/ProductForm.cometGen.ts index 9cce26495a..f6c70d4faa 100644 --- a/demo/admin/src/products/future/ProductForm.cometGen.ts +++ b/demo/admin/src/products/future/ProductForm.cometGen.ts @@ -16,7 +16,7 @@ export const ProductForm: FormConfig = { { type: "text", name: "slug" }, { type: "text", name: "description", label: "Description", multiline: true }, { type: "staticSelect", name: "type", label: "Type" /*, values: from gql schema (TODO overridable)*/ }, - //TODO { type: "asyncSelect", name: "category", label: "Category" /*, endpoint: from gql schema (overridable)*/ }, + { type: "asyncSelect", name: "category", rootQuery: "productCategories" }, { type: "number", name: "price", helperText: "Enter price in this format: 123,45" }, { type: "boolean", name: "inStock" }, { type: "date", name: "availableSince" }, diff --git a/demo/admin/src/products/future/generated/ProductForm.gql.tsx b/demo/admin/src/products/future/generated/ProductForm.gql.tsx index 82c222c320..3f0e399718 100644 --- a/demo/admin/src/products/future/generated/ProductForm.gql.tsx +++ b/demo/admin/src/products/future/generated/ProductForm.gql.tsx @@ -2,12 +2,32 @@ // You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. import { gql } from "@apollo/client"; +export const productCategoriesSelectFragment = gql` + fragment ProductCategorySelect on ProductCategory { + id + title + } +`; +export const productCategoriesQuery = gql` + query ProductCategoriesSelect { + productCategories { + nodes { + ...ProductCategorySelect + } + } + } + ${productCategoriesSelectFragment} +`; export const productFormFragment = gql` fragment ProductFormDetails on Product { title slug description type + category { + id + title + } price inStock availableSince diff --git a/demo/admin/src/products/future/generated/ProductForm.tsx b/demo/admin/src/products/future/generated/ProductForm.tsx index 470d9048a9..8f04da9d41 100644 --- a/demo/admin/src/products/future/generated/ProductForm.tsx +++ b/demo/admin/src/products/future/generated/ProductForm.tsx @@ -18,6 +18,7 @@ import { ToolbarFillSpace, ToolbarItem, ToolbarTitleItem, + useAsyncOptionsProps, useFormApiRef, useStackApi, useStackSwitchApi, @@ -34,10 +35,13 @@ import React from "react"; import { FormattedMessage } from "react-intl"; import { validateTitle } from "../validateTitle"; -import { createProductMutation, productFormFragment, productQuery, updateProductMutation } from "./ProductForm.gql"; +import { createProductMutation, productCategoriesQuery, productFormFragment, productQuery, updateProductMutation } from "./ProductForm.gql"; import { GQLCreateProductMutation, GQLCreateProductMutationVariables, + GQLProductCategoriesSelectQuery, + GQLProductCategoriesSelectQueryVariables, + GQLProductCategorySelectFragment, GQLProductFormDetailsFragment, GQLProductQuery, GQLProductQueryVariables, @@ -100,6 +104,7 @@ export function ProductForm({ id }: FormProps): React.ReactElement { if (await saveConflict.checkForConflicts()) throw new Error("Conflicts detected"); const output = { ...formValues, + category: formValues.category?.id, price: parseFloat(formValues.price), image: rootBlocks.image.state2Output(formValues.image), }; @@ -125,6 +130,13 @@ export function ProductForm({ id }: FormProps): React.ReactElement { } }; + const categorySelectAsyncProps = useAsyncOptionsProps(async () => { + const result = await client.query({ + query: productCategoriesQuery, + }); + return result.data.productCategories.nodes; + }); + if (error) throw error; if (loading) { @@ -193,6 +205,14 @@ export function ProductForm({ id }: FormProps): React.ReactElement { )}
+ } + component={FinalFormSelect} + {...categorySelectAsyncProps} + getOptionLabel={(option: GQLProductCategorySelectFragment) => option.title} + /> field.type == "number"); const booleanFields = config.fields.filter((field) => field.type == "boolean"); + let hooksCode = ""; + let formValueToGqlInputCode = ""; + + const formFragmentFields: string[] = []; + const fieldsCode = config.fields + .map((field) => { + const generated = generateFormField({ gqlIntrospection }, field, config); + for (const name in generated.gqlDocuments) { + gqlDocuments[name] = generated.gqlDocuments[name]; + } + imports.push(...generated.imports); + hooksCode += generated.hooksCode; + formValueToGqlInputCode += generated.formValueToGqlInputCode; + formFragmentFields.push(generated.formFragmentField); + return generated.code; + }) + .join("\n"); + const fragmentName = config.fragmentName ?? `${gqlType}Form`; gqlDocuments[`${instanceGqlType}FormFragment`] = ` fragment ${fragmentName} on ${gqlType} { - ${config.fields.map((field) => field.name).join("\n")} + ${formFragmentFields.join("\n")} } `; @@ -68,16 +86,25 @@ export function generateForm( \${${`${instanceGqlType}FormFragment`}} `; - const fieldsCode = config.fields - .map((field) => { - const generated = generateFormField({ gqlIntrospection }, field, config); - for (const name in generated.gqlDocuments) { - gqlDocuments[name] = generated.gqlDocuments[name]; - } - imports.push(...generated.imports); - return generated.code; - }) - .join("\n"); + for (const name in gqlDocuments) { + const gqlDocument = gqlDocuments[name]; + imports.push({ + name: name, + importPath: `./${baseOutputFilename}.gql`, + }); + const match = gqlDocument.match(/^\s*(query|mutation|fragment)\s+(\w+)/); + if (!match) throw new Error(`Could not find query or mutation name in ${gqlDocument}`); + const type = match[1]; + const documentName = match[2]; + imports.push({ + name: `GQL${documentName}${type[0].toUpperCase() + type.substring(1)}`, + importPath: `./${baseOutputFilename}.gql.generated`, + }); + imports.push({ + name: `GQL${documentName}${type[0].toUpperCase() + type.substring(1)}Variables`, + importPath: `./${baseOutputFilename}.gql.generated`, + }); + } const code = `import { useApolloClient, useQuery } from "@apollo/client"; import { @@ -113,21 +140,6 @@ export function generateForm( import { FormattedMessage } from "react-intl"; ${generateImportsCode(imports)} - import { - create${gqlType}Mutation, - ${instanceGqlType}FormFragment, - ${instanceGqlType}Query, - update${gqlType}Mutation, - } from "./${baseOutputFilename}.gql"; - import { - GQLCreate${gqlType}Mutation, - GQLCreate${gqlType}MutationVariables, - GQL${fragmentName}Fragment, - GQL${gqlType}Query, - GQL${gqlType}QueryVariables, - GQLUpdate${gqlType}Mutation, - GQLUpdate${gqlType}MutationVariables, - } from "./${baseOutputFilename}.gql.generated"; ${Object.entries(rootBlocks) .map(([rootBlockKey, rootBlock]) => `import { ${rootBlock.name} } from "${rootBlock.import}";`) .join("\n")} @@ -202,10 +214,7 @@ export function generateForm( if (await saveConflict.checkForConflicts()) throw new Error("Conflicts detected"); const output = { ...formValues, - ${numberFields.map((field) => `${String(field.name)}: parseFloat(formValues.${String(field.name)}),`).join("\n")} - ${Object.keys(rootBlocks) - .map((rootBlockKey) => `${rootBlockKey}: rootBlocks.${rootBlockKey}.state2Output(formValues.${rootBlockKey}),`) - .join("\n")} + ${formValueToGqlInputCode} }; if (mode === "edit") { if (!id) throw new Error(); @@ -228,6 +237,8 @@ export function generateForm( } } }; + + ${hooksCode} if (error) throw error; diff --git a/packages/admin/cms-admin/src/generator/future/generateFormField.ts b/packages/admin/cms-admin/src/generator/future/generateFormField.ts index bfaab250ad..df73d60273 100644 --- a/packages/admin/cms-admin/src/generator/future/generateFormField.ts +++ b/packages/admin/cms-admin/src/generator/future/generateFormField.ts @@ -10,7 +10,7 @@ export function generateFormField( config: FormFieldConfig, // eslint-disable-next-line @typescript-eslint/no-explicit-any formConfig: FormConfig, -): GeneratorReturn & { imports: Imports } { +): GeneratorReturn & { imports: Imports; hooksCode: string; formFragmentField: string; formValueToGqlInputCode: string } { const gqlType = formConfig.gqlType; const instanceGqlType = gqlType[0].toLowerCase() + gqlType.substring(1); @@ -34,6 +34,9 @@ export function generateFormField( const imports: Imports = []; + const gqlDocuments: Record = {}; + let hooksCode = ""; + let validateCode = ""; if (config.validate) { let importPath = config.validate.import; @@ -49,6 +52,8 @@ export function generateFormField( } let code = ""; + let formValueToGqlInputCode = ""; + let formFragmentField = name; if (config.type == "text") { const TextInputComponent = config.multiline ? "TextAreaField" : "TextField"; code = ` @@ -83,6 +88,7 @@ export function generateFormField( ${validateCode} />`; //TODO MUI suggest not using type=number https://mui.com/material-ui/react-text-field/#type-quot-number-quot + formValueToGqlInputCode = `${name}: parseFloat(formValues.${name}),`; } else if (config.type == "boolean") { code = ` {(props) => ( @@ -124,6 +130,7 @@ export function generateFormField( code = ` {createFinalFormBlock(${config.block.name})} `; + formValueToGqlInputCode = `${name}: rootBlocks.${name}.state2Output(formValues.${name}),`; } else if (config.type == "staticSelect") { if (config.values) { throw new Error("custom values for staticSelect is not yet supported"); // TODO add support @@ -155,12 +162,91 @@ export function generateFormField( } `; + } else if (config.type == "asyncSelect") { + if (introspectionFieldType.kind !== "OBJECT") throw new Error(`asyncSelect only supports OBJECT types`); + const objectType = gqlIntrospection.__schema.types.find((t) => t.kind === "OBJECT" && t.name === introspectionFieldType.name) as + | IntrospectionObjectType + | undefined; + if (!objectType) throw new Error(`Object type ${introspectionFieldType.name} not found for field ${name}`); + + //find labelField: 1. as configured + let labelField = config.labelField; + + //find labelField: 2. common names (name or title) + if (!labelField) { + labelField = objectType.fields.find((field) => { + let type = field.type; + if (type.kind == "NON_NULL") type = type.ofType; + if ((field.name == "name" || field.name == "title") && type.kind == "SCALAR" && type.name == "String") { + return true; + } + })?.name; + } + + //find labelField: 3. first string field + if (!labelField) { + labelField = objectType.fields.find((field) => { + let type = field.type; + if (type.kind == "NON_NULL") type = type.ofType; + if (field.type.kind == "SCALAR" && field.type.name == "String") { + return true; + } + })?.name; + } + + const rootQuery = config.rootQuery; //TODO we should infer a default value from the gql schema + const queryType = objectType.name; + const queryVariableName = `${rootQuery}Query`; + const queryName = `${rootQuery[0].toUpperCase() + rootQuery.substring(1)}Select`; + const fragmentVariableName = `${rootQuery}SelectFragment`; + const fragmentName = `${objectType.name}Select`; + + formFragmentField = `${name} { id ${labelField} }`; + + gqlDocuments[fragmentVariableName] = ` + fragment ${fragmentName} on ${queryType} { + id + ${labelField} + } + `; + gqlDocuments[queryVariableName] = `query ${queryName} { + ${rootQuery} { + nodes { + ...${fragmentName} + } + } + } + \${${fragmentVariableName}} + `; + + imports.push({ + name: "useAsyncOptionsProps", + importPath: "@comet/admin", + }); + hooksCode += `const ${name}SelectAsyncProps = useAsyncOptionsProps(async () => { + const result = await client.query({ query: ${queryVariableName} }); + return result.data.${rootQuery}.nodes; + });`; + + formValueToGqlInputCode = `${name}: formValues.${name}?.id,`; + + code = `} + component={FinalFormSelect} + {...${name}SelectAsyncProps} + getOptionLabel={(option: GQL${fragmentName}Fragment) => option.${labelField}} + />`; } else { - throw new Error(`Unsupported type: ${config.type}`); + throw new Error(`Unsupported type`); } return { code, - gqlDocuments: {}, + hooksCode, + formValueToGqlInputCode, + formFragmentField, + gqlDocuments, imports, }; } diff --git a/packages/admin/cms-admin/src/generator/future/generator.ts b/packages/admin/cms-admin/src/generator/future/generator.ts index 21bc921bd5..c4ef468a00 100644 --- a/packages/admin/cms-admin/src/generator/future/generator.ts +++ b/packages/admin/cms-admin/src/generator/future/generator.ts @@ -20,7 +20,7 @@ export type FormFieldConfig = ( | { type: "date" } // TODO | { type: "dateTime" } | { type: "staticSelect"; values?: string[] } - | { type: "asyncSelect"; values?: string[] } + | { type: "asyncSelect"; rootQuery: string; labelField?: string } | { type: "block"; block: ImportReference } ) & { name: keyof T; label?: string; required?: boolean; validate?: ImportReference; helperText?: string }; From 7939469ac23c106115f91fde5c6b01fc5fc1115a Mon Sep 17 00:00:00 2001 From: Ricky James Smith Date: Tue, 5 Mar 2024 08:03:48 +0100 Subject: [PATCH 15/18] Admin Generator (Future): Adjust column-sizing of generated grids to make the grid span over the full width and align the actions to the right (#1784) By default, columns no longer have a fixed width and instead grow to fill the available space and push the action icon buttons all the way to the right. In addition to `width`, the `minWidth`, `maxWidth`, and `flex` settings are now exposed to the grid's column definition to allow more customization. ## Previously Previously ## Now Now --------- Co-authored-by: Johannes Obermair <48853629+johnnyomair@users.noreply.github.com> --- .../products/future/ProductsGrid.cometGen.ts | 12 +++--- .../future/generated/ProductsGrid.tsx | 20 ++++++--- .../src/generator/future/generateGrid.ts | 42 +++++++++++++++---- .../src/generator/future/generator.ts | 5 ++- 4 files changed, 59 insertions(+), 20 deletions(-) diff --git a/demo/admin/src/products/future/ProductsGrid.cometGen.ts b/demo/admin/src/products/future/ProductsGrid.cometGen.ts index a4375d4b3d..3458f51c25 100644 --- a/demo/admin/src/products/future/ProductsGrid.cometGen.ts +++ b/demo/admin/src/products/future/ProductsGrid.cometGen.ts @@ -6,11 +6,11 @@ export const ProductsGrid: GridConfig = { gqlType: "Product", fragmentName: "ProductsGridFuture", // configurable as it must be unique across project columns: [ - { type: "text", name: "title", headerName: "Titel", width: 150 }, - { type: "text", name: "description", headerName: "Description", width: 150 }, - { type: "number", name: "price", headerName: "Price", width: 150 }, - { type: "staticSelect", name: "type" /*, values: from gql schema (TODO overridable)*/ }, - { type: "date", name: "availableSince" }, - { type: "dateTime", name: "createdAt" }, + { type: "text", name: "title", headerName: "Titel", minWidth: 200, maxWidth: 250 }, + { type: "text", name: "description", headerName: "Description" }, + { type: "number", name: "price", headerName: "Price", maxWidth: 150 }, + { type: "staticSelect", name: "type", maxWidth: 150 /*, values: from gql schema (TODO overridable)*/ }, + { type: "date", name: "availableSince", width: 140 }, + { type: "dateTime", name: "createdAt", width: 170 }, ], }; diff --git a/demo/admin/src/products/future/generated/ProductsGrid.tsx b/demo/admin/src/products/future/generated/ProductsGrid.tsx index f92d806cad..4b629b57bd 100644 --- a/demo/admin/src/products/future/generated/ProductsGrid.tsx +++ b/demo/admin/src/products/future/generated/ProductsGrid.tsx @@ -104,9 +104,14 @@ export function ProductsGrid(): React.ReactElement { const dataGridProps = { ...useDataGridRemote(), ...usePersistentColumnState("ProductsGrid") }; const columns: GridColDef[] = [ - { field: "title", headerName: intl.formatMessage({ id: "product.title", defaultMessage: "Titel" }), width: 150 }, - { field: "description", headerName: intl.formatMessage({ id: "product.description", defaultMessage: "Description" }), width: 150 }, - { field: "price", headerName: intl.formatMessage({ id: "product.price", defaultMessage: "Price" }), width: 150 }, + { field: "title", headerName: intl.formatMessage({ id: "product.title", defaultMessage: "Titel" }), flex: 1, maxWidth: 250, minWidth: 200 }, + { + field: "description", + headerName: intl.formatMessage({ id: "product.description", defaultMessage: "Description" }), + flex: 1, + minWidth: 150, + }, + { field: "price", headerName: intl.formatMessage({ id: "product.price", defaultMessage: "Price" }), flex: 1, maxWidth: 150, minWidth: 150 }, { field: "type", headerName: intl.formatMessage({ id: "product.type", defaultMessage: "Type" }), @@ -116,21 +121,23 @@ export function ProductsGrid(): React.ReactElement { { value: "Shirt", label: intl.formatMessage({ id: "product.type.shirt", defaultMessage: "Shirt" }) }, { value: "Tie", label: intl.formatMessage({ id: "product.type.tie", defaultMessage: "Tie" }) }, ], - width: 150, + flex: 1, + maxWidth: 150, + minWidth: 150, }, { field: "availableSince", headerName: intl.formatMessage({ id: "product.availableSince", defaultMessage: "Available Since" }), type: "date", valueGetter: ({ value }) => value && new Date(value), - width: 150, + width: 140, }, { field: "createdAt", headerName: intl.formatMessage({ id: "product.createdAt", defaultMessage: "Created At" }), type: "dateTime", valueGetter: ({ value }) => value && new Date(value), - width: 150, + width: 170, }, { field: "actions", @@ -138,6 +145,7 @@ export function ProductsGrid(): React.ReactElement { sortable: false, filterable: false, type: "actions", + align: "right", renderCell: (params) => { return ( <> diff --git a/packages/admin/cms-admin/src/generator/future/generateGrid.ts b/packages/admin/cms-admin/src/generator/future/generateGrid.ts index 88aec5e413..0e11d3ca91 100644 --- a/packages/admin/cms-admin/src/generator/future/generateGrid.ts +++ b/packages/admin/cms-admin/src/generator/future/generateGrid.ts @@ -12,7 +12,9 @@ import { GeneratorReturn, GridConfig } from "./generator"; import { camelCaseToHumanReadable } from "./utils/camelCaseToHumanReadable"; import { findRootBlocks } from "./utils/findRootBlocks"; -function tsCodeRecordToString(object: Record) { +type TsCodeRecordToStringObject = Record; + +function tsCodeRecordToString(object: TsCodeRecordToStringObject) { return `{${Object.entries(object) .filter(([key, value]) => value !== undefined) .map(([key, value]) => `${key}: ${value},`) @@ -191,6 +193,10 @@ export function generateGrid( type, gridType: "singleSelect" as const, valueOptions, + width: column.width, + minWidth: column.minWidth, + maxWidth: column.maxWidth, + flex: column.flex, }; } @@ -199,11 +205,14 @@ export function generateGrid( return { name, headerName: column.headerName, - width: column.width, type, gridType, renderCell, valueGetter, + width: column.width, + minWidth: column.minWidth, + maxWidth: column.maxWidth, + flex: column.flex, }; }); @@ -345,8 +354,8 @@ export function generateGrid( const columns: GridColDef[] = [ ${gridColumnFields - .map((column) => - tsCodeRecordToString({ + .map((column) => { + const columnDefinition: TsCodeRecordToStringObject = { field: `"${column.name}"`, headerName: `intl.formatMessage({ id: "${instanceGqlType}.${column.name}", defaultMessage: "${ column.headerName || camelCaseToHumanReadable(column.name) @@ -356,10 +365,28 @@ export function generateGrid( sortable: !sortFields.includes(column.name) ? `false` : undefined, valueGetter: column.valueGetter, valueOptions: column.valueOptions, - width: column.width ? String(column.width) : "150", renderCell: column.renderCell, - }), - ) + width: column.width, + flex: column.flex, + }; + + if (typeof column.width === "undefined") { + const defaultMinWidth = 150; + columnDefinition.flex = 1; + columnDefinition.maxWidth = column.maxWidth; + + if ( + typeof column.minWidth === "undefined" && + (typeof column.maxWidth === "undefined" || column.maxWidth >= defaultMinWidth) + ) { + columnDefinition.minWidth = defaultMinWidth; + } else if (typeof column.minWidth !== "undefined") { + columnDefinition.minWidth = column.minWidth; + } + } + + return tsCodeRecordToString(columnDefinition); + }) .join(",\n")}, { field: "actions", @@ -367,6 +394,7 @@ export function generateGrid( sortable: false, filterable: false, type: "actions", + align: "right", renderCell: (params) => { return ( <> diff --git a/packages/admin/cms-admin/src/generator/future/generator.ts b/packages/admin/cms-admin/src/generator/future/generator.ts index c4ef468a00..be20eb5f03 100644 --- a/packages/admin/cms-admin/src/generator/future/generator.ts +++ b/packages/admin/cms-admin/src/generator/future/generator.ts @@ -1,5 +1,6 @@ import { GraphQLFileLoader } from "@graphql-tools/graphql-file-loader"; import { loadSchema } from "@graphql-tools/load"; +import { GridColDef } from "@mui/x-data-grid"; import { glob } from "glob"; import { introspectionFromSchema } from "graphql"; import { basename, dirname } from "path"; @@ -34,6 +35,8 @@ export type FormConfig = { export type TabsConfig = { type: "tabs"; tabs: { name: string; content: GeneratorConfig }[] }; +type DataGridSettings = Pick; + export type GridColumnConfig = ( | { type: "text" } | { type: "number" } @@ -42,7 +45,7 @@ export type GridColumnConfig = ( | { type: "dateTime" } | { type: "staticSelect"; values?: string[] } | { type: "block"; block: ImportReference } -) & { name: keyof T; headerName?: string; width?: number }; +) & { name: keyof T } & DataGridSettings; export type GridConfig = { type: "grid"; gqlType: T["__typename"]; From 2497a062fd8a9793f9faeb51a52a9053d3fc9494 Mon Sep 17 00:00:00 2001 From: Niko Sams Date: Tue, 5 Mar 2024 08:09:12 +0100 Subject: [PATCH 16/18] API Generator: Add support for `status` enum that can include archived/deleted, remove visibility flag support (#1317) --------- Co-authored-by: Johannes Obermair <48853629+johnnyomair@users.noreply.github.com> --- .changeset/shy-scissors-joke.md | 9 + demo/admin/src/news/generated/NewsForm.gql.ts | 2 + demo/admin/src/news/generated/NewsForm.tsx | 24 +- demo/admin/src/news/generated/NewsGrid.tsx | 13 + demo/admin/src/products/ProductsGrid.tsx | 31 +-- .../future/generated/ProductsGrid.tsx | 3 +- .../src/products/generated/ProductForm.gql.ts | 1 + .../src/products/generated/ProductForm.tsx | 12 + .../src/products/generated/ProductsGrid.tsx | 14 +- demo/api/schema.gql | 41 +++- .../db/migrations/Migration20231009090817.ts | 16 ++ demo/api/src/news/entities/news.entity.ts | 12 +- .../src/news/generated/dto/news-list.args.ts | 8 +- .../api/src/news/generated/dto/news.filter.ts | 10 +- demo/api/src/news/generated/dto/news.input.ts | 14 +- demo/api/src/news/generated/dto/news.sort.ts | 1 + demo/api/src/news/generated/news.resolver.ts | 24 +- .../src/products/entities/product.entity.ts | 16 +- .../products/generated/dto/product.filter.ts | 9 +- .../products/generated/dto/product.input.ts | 7 +- .../products/generated/dto/product.sort.ts | 2 +- .../products/generated/product.resolver.ts | 17 -- .../src/generator/generate-crud-input.ts | 12 +- .../generate-crud-update-status.spec.ts | 225 ++++++++++++++++++ .../cms-api/src/generator/generate-crud.ts | 159 ++++++++++--- .../src/generator/utils/ts-morph-helper.ts | 22 +- 26 files changed, 575 insertions(+), 129 deletions(-) create mode 100644 .changeset/shy-scissors-joke.md create mode 100644 demo/api/src/db/migrations/Migration20231009090817.ts create mode 100644 packages/api/cms-api/src/generator/generate-crud-update-status.spec.ts diff --git a/.changeset/shy-scissors-joke.md b/.changeset/shy-scissors-joke.md new file mode 100644 index 0000000000..410ceb8ece --- /dev/null +++ b/.changeset/shy-scissors-joke.md @@ -0,0 +1,9 @@ +--- +"@comet/cms-api": major +--- + +API Generator: Remove support for `visible` boolean, use `status` enum instead. + +Recommended enum values: Published/Unpublished or Visible/Invisible or Active/Deleted or Active/Archived + +Remove support for update visibility mutation, use existing generic update instead diff --git a/demo/admin/src/news/generated/NewsForm.gql.ts b/demo/admin/src/news/generated/NewsForm.gql.ts index 61fda9a4d9..d01a097fd2 100644 --- a/demo/admin/src/news/generated/NewsForm.gql.ts +++ b/demo/admin/src/news/generated/NewsForm.gql.ts @@ -7,8 +7,10 @@ export const newsFormFragment = gql` fragment NewsForm on News { slug title + status date category + visible image content } diff --git a/demo/admin/src/news/generated/NewsForm.tsx b/demo/admin/src/news/generated/NewsForm.tsx index 830a346df5..63116e1d98 100644 --- a/demo/admin/src/news/generated/NewsForm.tsx +++ b/demo/admin/src/news/generated/NewsForm.tsx @@ -5,6 +5,7 @@ import { useApolloClient, useQuery } from "@apollo/client"; import { Field, FinalForm, + FinalFormCheckbox, FinalFormInput, FinalFormSaveSplitButton, FinalFormSelect, @@ -24,7 +25,7 @@ import { FinalFormDatePicker } from "@comet/admin-date-time"; import { ArrowLeft } from "@comet/admin-icons"; import { BlockState, createFinalFormBlock } from "@comet/blocks-admin"; import { DamImageBlock, EditPageLayout, queryUpdatedAt, resolveHasSaveConflict, useFormSaveConflict } from "@comet/cms-admin"; -import { IconButton, MenuItem } from "@mui/material"; +import { FormControlLabel, IconButton, MenuItem } from "@mui/material"; import { useContentScope } from "@src/common/ContentScopeProvider"; import { FormApi } from "final-form"; import { filter } from "graphql-anywhere"; @@ -81,6 +82,7 @@ export function NewsForm({ id }: FormProps): React.ReactElement { content: rootBlocks.content.input2State(data.news.content), } : { + visible: false, image: rootBlocks.image.defaultValues(), content: rootBlocks.content.defaultValues(), }, @@ -174,6 +176,18 @@ export function NewsForm({ id }: FormProps): React.ReactElement { component={FinalFormInput} label={} /> + }> + {(props) => ( + + + + + + + + + )} + )} + + {(props) => ( + } + control={} + /> + )} + {createFinalFormBlock(rootBlocks.image)} diff --git a/demo/admin/src/news/generated/NewsGrid.tsx b/demo/admin/src/news/generated/NewsGrid.tsx index d169b12321..6586fa3fd6 100644 --- a/demo/admin/src/news/generated/NewsGrid.tsx +++ b/demo/admin/src/news/generated/NewsGrid.tsx @@ -43,6 +43,7 @@ const newsFragment = gql` updatedAt slug title + status date category visible @@ -114,6 +115,16 @@ export function NewsGrid(): React.ReactElement { }, { field: "slug", headerName: intl.formatMessage({ id: "news.slug", defaultMessage: "Slug" }), width: 150 }, { field: "title", headerName: intl.formatMessage({ id: "news.title", defaultMessage: "Title" }), width: 150 }, + { + field: "status", + headerName: intl.formatMessage({ id: "news.status", defaultMessage: "Status" }), + type: "singleSelect", + valueOptions: [ + { value: "Active", label: intl.formatMessage({ id: "news.status.active", defaultMessage: "Active" }) }, + { value: "Deleted", label: intl.formatMessage({ id: "news.status.deleted", defaultMessage: "Deleted" }) }, + ], + width: 150, + }, { field: "date", headerName: intl.formatMessage({ id: "news.date", defaultMessage: "Date" }), @@ -178,8 +189,10 @@ export function NewsGrid(): React.ReactElement { return { slug: row.slug, title: row.title, + status: row.status, date: row.date, category: row.category, + visible: row.visible, image: DamImageBlock.state2Output(DamImageBlock.input2State(row.image)), content: NewsContentBlock.state2Output(NewsContentBlock.input2State(row.content)), }; diff --git a/demo/admin/src/products/ProductsGrid.tsx b/demo/admin/src/products/ProductsGrid.tsx index 8f60c712cd..55eb9cca70 100644 --- a/demo/admin/src/products/ProductsGrid.tsx +++ b/demo/admin/src/products/ProductsGrid.tsx @@ -35,8 +35,8 @@ import { GQLProductsListManualFragment, GQLProductsListQuery, GQLProductsListQueryVariables, - GQLUpdateProductVisibilityMutation, - GQLUpdateProductVisibilityMutationVariables, + GQLUpdateProductStatusMutation, + GQLUpdateProductStatusMutationVariables, } from "./ProductsGrid.generated"; function ProductsGridToolbar() { @@ -111,21 +111,22 @@ function ProductsGrid() { }, { field: "inStock", headerName: "In Stock", width: 50, type: "boolean" }, { - field: "visible", - headerName: "Visible", + field: "status", + headerName: "Status", width: 100, type: "boolean", + valueGetter: (params) => params.row.status == "Published", renderCell: (params) => { return ( { - await client.mutate({ - mutation: updateProductVisibilityMutation, - variables: { id: params.row.id, visible }, + visibility={params.row.status == "Published"} + onUpdateVisibility={async (status) => { + await client.mutate({ + mutation: updateProductStatusMutation, + variables: { id: params.row.id, status: status ? "Published" : "Unpublished" }, optimisticResponse: { __typename: "Mutation", - updateProductVisibility: { __typename: "Product", id: params.row.id, visible }, + updateProduct: { __typename: "Product", id: params.row.id, status: status ? "Published" : "Unpublished" }, }, }); }} @@ -226,7 +227,7 @@ const productsFragment = gql` type inStock image - visible + status category { id title @@ -285,11 +286,11 @@ const createProductMutation = gql` } `; -const updateProductVisibilityMutation = gql` - mutation UpdateProductVisibility($id: ID!, $visible: Boolean!) { - updateProductVisibility(id: $id, visible: $visible) { +const updateProductStatusMutation = gql` + mutation UpdateProductStatus($id: ID!, $status: ProductStatus!) { + updateProduct(id: $id, input: { status: $status }) { id - visible + status } } `; diff --git a/demo/admin/src/products/future/generated/ProductsGrid.tsx b/demo/admin/src/products/future/generated/ProductsGrid.tsx index 4b629b57bd..25e4c56d18 100644 --- a/demo/admin/src/products/future/generated/ProductsGrid.tsx +++ b/demo/admin/src/products/future/generated/ProductsGrid.tsx @@ -39,7 +39,7 @@ const productsFragment = gql` id updatedAt title - visible + status slug description type @@ -157,6 +157,7 @@ export function ProductsGrid(): React.ReactElement { const row = params.row; return { title: row.title, + status: row.status, slug: row.slug, description: row.description, type: row.type, diff --git a/demo/admin/src/products/generated/ProductForm.gql.ts b/demo/admin/src/products/generated/ProductForm.gql.ts index a0c438713c..7a02cf280c 100644 --- a/demo/admin/src/products/generated/ProductForm.gql.ts +++ b/demo/admin/src/products/generated/ProductForm.gql.ts @@ -6,6 +6,7 @@ import { gql } from "@apollo/client"; export const productFormFragment = gql` fragment ProductForm on Product { title + status slug description type diff --git a/demo/admin/src/products/generated/ProductForm.tsx b/demo/admin/src/products/generated/ProductForm.tsx index b178cf2199..d65f0d4d84 100644 --- a/demo/admin/src/products/generated/ProductForm.tsx +++ b/demo/admin/src/products/generated/ProductForm.tsx @@ -162,6 +162,18 @@ export function ProductForm({ id }: FormProps): React.ReactElement { component={FinalFormInput} label={} /> + }> + {(props) => ( + + + + + + + + + )} + { + this.addSql('alter table "Product" add column "status" text check ("status" in (\'Published\', \'Unpublished\')) not null default \'Published\';'); + this.addSql('alter table "Product" alter colulmn status drop default;'); + this.addSql('alter table "Product" drop column "visible";'); + } + + async down(): Promise { + this.addSql('alter table "Product" drop column "status";'); + this.addSql('alter table "Product" add column "visible" bool not null default null;'); + } + +} diff --git a/demo/api/src/news/entities/news.entity.ts b/demo/api/src/news/entities/news.entity.ts index 73d80c79c9..e9e0638552 100644 --- a/demo/api/src/news/entities/news.entity.ts +++ b/demo/api/src/news/entities/news.entity.ts @@ -8,6 +8,12 @@ import { v4 as uuid } from "uuid"; import { NewsContentBlock } from "../blocks/news-content.block"; import { NewsComment } from "./news-comment.entity"; +export enum NewsStatus { + Active = "Active", + Deleted = "Deleted", +} +registerEnumType(NewsStatus, { name: "NewsStatus" }); + export enum NewsCategory { Events = "Events", Company = "Company", @@ -39,7 +45,7 @@ export class NewsContentScope { @Entity() @CrudGenerator({ targetDirectory: `${__dirname}/../generated/` }) export class News extends BaseEntity implements DocumentInterface { - [OptionalProps]?: "createdAt" | "updatedAt" | "category"; // TODO remove "category" once CRUD generator supports enums + [OptionalProps]?: "createdAt" | "updatedAt" | "category" | "status"; // TODO remove "category" once CRUD generator supports enums @PrimaryKey({ type: "uuid" }) @Field(() => ID) @@ -57,6 +63,10 @@ export class News extends BaseEntity implements DocumentInterface { @Field() title: string; + @Enum({ items: () => NewsStatus }) + @Field(() => NewsStatus) + status: NewsStatus = NewsStatus.Active; + @Property() @Field() date: Date; diff --git a/demo/api/src/news/generated/dto/news-list.args.ts b/demo/api/src/news/generated/dto/news-list.args.ts index 1f30d71c47..acd16f0e55 100644 --- a/demo/api/src/news/generated/dto/news-list.args.ts +++ b/demo/api/src/news/generated/dto/news-list.args.ts @@ -3,9 +3,9 @@ import { OffsetBasedPaginationArgs } from "@comet/cms-api"; import { ArgsType, Field } from "@nestjs/graphql"; import { Type } from "class-transformer"; -import { IsOptional, IsString, ValidateNested } from "class-validator"; +import { IsEnum, IsOptional, IsString, ValidateNested } from "class-validator"; -import { NewsContentScope } from "../../entities/news.entity"; +import { NewsContentScope, NewsStatus } from "../../entities/news.entity"; import { NewsFilter } from "./news.filter"; import { NewsSort } from "./news.sort"; @@ -16,6 +16,10 @@ export class NewsListArgs extends OffsetBasedPaginationArgs { @Type(() => NewsContentScope) scope: NewsContentScope; + @Field(() => NewsStatus, { defaultValue: NewsStatus.Active }) + @IsEnum(NewsStatus) + status: NewsStatus; + @Field({ nullable: true }) @IsOptional() @IsString() diff --git a/demo/api/src/news/generated/dto/news.filter.ts b/demo/api/src/news/generated/dto/news.filter.ts index 9ce3430513..5f016d114c 100644 --- a/demo/api/src/news/generated/dto/news.filter.ts +++ b/demo/api/src/news/generated/dto/news.filter.ts @@ -5,8 +5,10 @@ import { Field, InputType } from "@nestjs/graphql"; import { Type } from "class-transformer"; import { IsOptional, ValidateNested } from "class-validator"; -import { NewsCategory } from "../../entities/news.entity"; +import { NewsCategory, NewsStatus } from "../../entities/news.entity"; +@InputType() +class NewsStatusEnumFilter extends createEnumFilter(NewsStatus) {} @InputType() class NewsCategoryEnumFilter extends createEnumFilter(NewsCategory) {} @@ -24,6 +26,12 @@ export class NewsFilter { @Type(() => StringFilter) title?: StringFilter; + @Field(() => NewsStatusEnumFilter, { nullable: true }) + @ValidateNested() + @IsOptional() + @Type(() => NewsStatusEnumFilter) + status?: NewsStatusEnumFilter; + @Field(() => DateFilter, { nullable: true }) @ValidateNested() @IsOptional() diff --git a/demo/api/src/news/generated/dto/news.input.ts b/demo/api/src/news/generated/dto/news.input.ts index 13af237642..d632dfa97e 100644 --- a/demo/api/src/news/generated/dto/news.input.ts +++ b/demo/api/src/news/generated/dto/news.input.ts @@ -4,10 +4,10 @@ import { BlockInputInterface, isBlockInputInterface } from "@comet/blocks-api"; import { DamImageBlock, IsSlug, PartialType, RootBlockInputScalar } from "@comet/cms-api"; import { Field, InputType } from "@nestjs/graphql"; import { Transform } from "class-transformer"; -import { IsDate, IsEnum, IsNotEmpty, IsString, ValidateNested } from "class-validator"; +import { IsBoolean, IsDate, IsEnum, IsNotEmpty, IsString, ValidateNested } from "class-validator"; import { NewsContentBlock } from "../../blocks/news-content.block"; -import { NewsCategory } from "../../entities/news.entity"; +import { NewsCategory, NewsStatus } from "../../entities/news.entity"; @InputType() export class NewsInput { @@ -22,6 +22,11 @@ export class NewsInput { @Field() title: string; + @IsNotEmpty() + @IsEnum(NewsStatus) + @Field(() => NewsStatus, { defaultValue: NewsStatus.Active }) + status: NewsStatus; + @IsNotEmpty() @IsDate() @Field() @@ -32,6 +37,11 @@ export class NewsInput { @Field(() => NewsCategory, { defaultValue: NewsCategory.Awards }) category: NewsCategory; + @IsNotEmpty() + @IsBoolean() + @Field() + visible: boolean; + @IsNotEmpty() @Field(() => RootBlockInputScalar(DamImageBlock)) @Transform(({ value }) => (isBlockInputInterface(value) ? value : DamImageBlock.blockInputFactory(value)), { toClassOnly: true }) diff --git a/demo/api/src/news/generated/dto/news.sort.ts b/demo/api/src/news/generated/dto/news.sort.ts index 3d60f54045..99d7cebdba 100644 --- a/demo/api/src/news/generated/dto/news.sort.ts +++ b/demo/api/src/news/generated/dto/news.sort.ts @@ -7,6 +7,7 @@ import { IsEnum } from "class-validator"; export enum NewsSortField { slug = "slug", title = "title", + status = "status", date = "date", category = "category", visible = "visible", diff --git a/demo/api/src/news/generated/news.resolver.ts b/demo/api/src/news/generated/news.resolver.ts index a77cde09d4..96bd946303 100644 --- a/demo/api/src/news/generated/news.resolver.ts +++ b/demo/api/src/news/generated/news.resolver.ts @@ -38,8 +38,13 @@ export class NewsResolver { } @Query(() => PaginatedNews) - async newsList(@Args() { scope, search, filter, sort, offset, limit }: NewsListArgs, @Info() info: GraphQLResolveInfo): Promise { + async newsList( + @Args() { scope, status, search, filter, sort, offset, limit }: NewsListArgs, + @Info() info: GraphQLResolveInfo, + ): Promise { const where = this.newsService.getFindCondition({ search, filter }); + + where.status = status; where.scope = scope; const fields = extractGraphqlFields(info, { root: "nodes" }); @@ -71,7 +76,6 @@ export class NewsResolver { const { image: imageInput, content: contentInput, ...assignInput } = input; const news = this.repository.create({ ...assignInput, - visible: false, scope, image: imageInput.transformToBlockData(), @@ -121,22 +125,6 @@ export class NewsResolver { return true; } - @Mutation(() => News) - @AffectedEntity(News) - async updateNewsVisibility( - @Args("id", { type: () => ID }) id: string, - @Args("visible", { type: () => Boolean }) visible: boolean, - ): Promise { - const news = await this.repository.findOneOrFail(id); - - news.assign({ - visible, - }); - await this.entityManager.flush(); - - return news; - } - @ResolveField(() => [NewsComment]) async comments(@Parent() news: News): Promise { return news.comments.loadItems(); diff --git a/demo/api/src/products/entities/product.entity.ts b/demo/api/src/products/entities/product.entity.ts index 3f94ebf941..f6531e607d 100644 --- a/demo/api/src/products/entities/product.entity.ts +++ b/demo/api/src/products/entities/product.entity.ts @@ -15,7 +15,7 @@ import { Ref, types, } from "@mikro-orm/core"; -import { Field, ID, InputType, ObjectType } from "@nestjs/graphql"; +import { Field, ID, InputType, ObjectType, registerEnumType } from "@nestjs/graphql"; import { Manufacturer } from "@src/products/entities/manufacturer.entity"; import { IsNumber } from "class-validator"; import { v4 as uuid } from "uuid"; @@ -26,6 +26,12 @@ import { ProductTag } from "./product-tag.entity"; import { ProductType } from "./product-type.enum"; import { ProductVariant } from "./product-variant.entity"; +export enum ProductStatus { + Published = "Published", + Unpublished = "Unpublished", +} +registerEnumType(ProductStatus, { name: "ProductStatus" }); + @ObjectType() @InputType("ProductDiscountsInput") export class ProductDiscounts { @@ -61,7 +67,7 @@ export class ProductDimensions { @RootBlockEntity() @CrudGenerator({ targetDirectory: `${__dirname}/../generated/` }) export class Product extends BaseEntity implements DocumentInterface { - [OptionalProps]?: "createdAt" | "updatedAt"; + [OptionalProps]?: "createdAt" | "updatedAt" | "status"; @PrimaryKey({ type: "uuid" }) @Field(() => ID) @@ -77,9 +83,9 @@ export class Product extends BaseEntity implements DocumentInterf }) title: string; - @Property() - @Field() - visible: boolean; + @Enum({ items: () => ProductStatus }) + @Field(() => ProductStatus) + status: ProductStatus = ProductStatus.Unpublished; @Property() @Field() diff --git a/demo/api/src/products/generated/dto/product.filter.ts b/demo/api/src/products/generated/dto/product.filter.ts index 26d7d98548..5cc2319c3c 100644 --- a/demo/api/src/products/generated/dto/product.filter.ts +++ b/demo/api/src/products/generated/dto/product.filter.ts @@ -5,8 +5,11 @@ import { Field, InputType } from "@nestjs/graphql"; import { Type } from "class-transformer"; import { IsOptional, ValidateNested } from "class-validator"; +import { ProductStatus } from "../../entities/product.entity"; import { ProductType } from "../../entities/product-type.enum"; +@InputType() +class ProductStatusEnumFilter extends createEnumFilter(ProductStatus) {} @InputType() class ProductTypeEnumFilter extends createEnumFilter(ProductType) {} @@ -18,11 +21,11 @@ export class ProductFilter { @Type(() => StringFilter) title?: StringFilter; - @Field(() => BooleanFilter, { nullable: true }) + @Field(() => ProductStatusEnumFilter, { nullable: true }) @ValidateNested() @IsOptional() - @Type(() => BooleanFilter) - visible?: BooleanFilter; + @Type(() => ProductStatusEnumFilter) + status?: ProductStatusEnumFilter; @Field(() => StringFilter, { nullable: true }) @ValidateNested() diff --git a/demo/api/src/products/generated/dto/product.input.ts b/demo/api/src/products/generated/dto/product.input.ts index 63751caf88..3a183a4f11 100644 --- a/demo/api/src/products/generated/dto/product.input.ts +++ b/demo/api/src/products/generated/dto/product.input.ts @@ -6,7 +6,7 @@ import { Field, ID, InputType } from "@nestjs/graphql"; import { Transform, Type } from "class-transformer"; import { IsArray, IsBoolean, IsDate, IsEnum, IsNotEmpty, IsNumber, IsString, IsUUID, ValidateNested } from "class-validator"; -import { ProductDimensions, ProductDiscounts } from "../../entities/product.entity"; +import { ProductDimensions, ProductDiscounts, ProductStatus } from "../../entities/product.entity"; import { ProductType } from "../../entities/product-type.enum"; import { ProductStatisticsInput } from "./product-statistics.nested.input"; import { ProductVariantInput } from "./product-variant.nested.input"; @@ -18,6 +18,11 @@ export class ProductInput { @Field() title: string; + @IsNotEmpty() + @IsEnum(ProductStatus) + @Field(() => ProductStatus, { defaultValue: ProductStatus.Unpublished }) + status: ProductStatus; + @IsNotEmpty() @IsString() @IsSlug() diff --git a/demo/api/src/products/generated/dto/product.sort.ts b/demo/api/src/products/generated/dto/product.sort.ts index e419c8bbe1..912f708655 100644 --- a/demo/api/src/products/generated/dto/product.sort.ts +++ b/demo/api/src/products/generated/dto/product.sort.ts @@ -6,7 +6,7 @@ import { IsEnum } from "class-validator"; export enum ProductSortField { title = "title", - visible = "visible", + status = "status", slug = "slug", description = "description", type = "type", diff --git a/demo/api/src/products/generated/product.resolver.ts b/demo/api/src/products/generated/product.resolver.ts index c4cd4bcadc..823842efcc 100644 --- a/demo/api/src/products/generated/product.resolver.ts +++ b/demo/api/src/products/generated/product.resolver.ts @@ -96,7 +96,6 @@ export class ProductResolver { } = input; const product = this.repository.create({ ...assignInput, - visible: false, category: categoryInput ? Reference.create(await this.productCategoryRepository.findOneOrFail(categoryInput)) : undefined, manufacturer: manufacturerInput ? Reference.create(await this.manufacturerRepository.findOneOrFail(manufacturerInput)) : undefined, @@ -211,22 +210,6 @@ export class ProductResolver { return true; } - @Mutation(() => Product) - @AffectedEntity(Product) - async updateProductVisibility( - @Args("id", { type: () => ID }) id: string, - @Args("visible", { type: () => Boolean }) visible: boolean, - ): Promise { - const product = await this.repository.findOneOrFail(id); - - product.assign({ - visible, - }); - await this.entityManager.flush(); - - return product; - } - @ResolveField(() => ProductCategory, { nullable: true }) async category(@Parent() product: Product): Promise { return product.category?.load(); diff --git a/packages/api/cms-api/src/generator/generate-crud-input.ts b/packages/api/cms-api/src/generator/generate-crud-input.ts index 6eb9e50dd8..fbcdf43a2b 100644 --- a/packages/api/cms-api/src/generator/generate-crud-input.ts +++ b/packages/api/cms-api/src/generator/generate-crud-input.ts @@ -65,7 +65,7 @@ export async function generateCrudInput( } else { decorators.push("@IsNullable()"); } - if (["id", "createdAt", "updatedAt", "visible", "scope"].includes(prop.name)) { + if (["id", "createdAt", "updatedAt", "scope"].includes(prop.name)) { //skip those (TODO find a non-magic solution?) continue; } else if (prop.enum) { @@ -73,7 +73,7 @@ export async function generateCrudInput( const defaultValue = prop.nullable && (initializer == "undefined" || initializer == "null") ? "null" : initializer; const fieldOptions = tsCodeRecordToString({ nullable: prop.nullable ? "true" : undefined, defaultValue }); const enumName = findEnumName(prop.name, metadata); - const importPath = findEnumImportPath(enumName, generatorOptions, metadata); + const importPath = findEnumImportPath(enumName, `${generatorOptions.targetDirectory}/dto`, metadata); imports.push({ name: enumName, importPath }); decorators.push(`@IsEnum(${enumName})`); decorators.push(`@Field(() => ${enumName}, ${fieldOptions})`); @@ -86,7 +86,7 @@ export async function generateCrudInput( const initializer = morphTsProperty(prop.name, metadata).getInitializer()?.getText(); const fieldOptions = tsCodeRecordToString({ defaultValue: initializer }); const enumName = findEnumName(prop.name, metadata); - const importPath = findEnumImportPath(enumName, generatorOptions, metadata); + const importPath = findEnumImportPath(enumName, `${generatorOptions.targetDirectory}/dto`, metadata); imports.push({ name: enumName, importPath }); decorators.push(`@IsEnum(${enumName}, { each: true })`); decorators.push(`@Field(() => [${enumName}], ${fieldOptions})`); @@ -128,7 +128,7 @@ export async function generateCrudInput( type = "boolean"; } else if (prop.type === "RootBlockType") { const blockName = findBlockName(prop.name, metadata); - const importPath = findBlockImportPath(blockName, generatorOptions, metadata); + const importPath = findBlockImportPath(blockName, `${generatorOptions.targetDirectory}/dto`, metadata); imports.push({ name: blockName, importPath }); decorators.push(`@Field(() => RootBlockInputScalar(${blockName})${prop.nullable ? ", { nullable: true }" : ""})`); @@ -276,7 +276,7 @@ export async function generateCrudInput( decorators.push("@IsBoolean({ each: true })"); } else if (tsType.getArrayElementTypeOrThrow().isClass()) { const nestedClassName = tsType.getArrayElementTypeOrThrow().getText(tsProp); - const importPath = findInputClassImportPath(nestedClassName, generatorOptions, metadata); + const importPath = findInputClassImportPath(nestedClassName, `${generatorOptions.targetDirectory}/dto`, metadata); imports.push({ name: nestedClassName, importPath }); decorators.push(`@ValidateNested()`); decorators.push(`@Type(() => ${nestedClassName})`); @@ -286,7 +286,7 @@ export async function generateCrudInput( } } else if (tsType.isClass()) { const nestedClassName = tsType.getText(tsProp); - const importPath = findInputClassImportPath(nestedClassName, generatorOptions, metadata); + const importPath = findInputClassImportPath(nestedClassName, `${generatorOptions.targetDirectory}/dto`, metadata); imports.push({ name: nestedClassName, importPath }); decorators.push(`@ValidateNested()`); decorators.push(`@Type(() => ${nestedClassName})`); diff --git a/packages/api/cms-api/src/generator/generate-crud-update-status.spec.ts b/packages/api/cms-api/src/generator/generate-crud-update-status.spec.ts new file mode 100644 index 0000000000..b7ada94ae2 --- /dev/null +++ b/packages/api/cms-api/src/generator/generate-crud-update-status.spec.ts @@ -0,0 +1,225 @@ +import { BaseEntity, Entity, Enum, PrimaryKey, Property } from "@mikro-orm/core"; +import { MikroORM } from "@mikro-orm/postgresql"; +import { Field, registerEnumType } from "@nestjs/graphql"; +import { LazyMetadataStorage } from "@nestjs/graphql/dist/schema-builder/storages/lazy-metadata.storage"; +import { v4 as uuid } from "uuid"; + +import { generateCrud } from "./generate-crud"; +import { lintGeneratedFiles, parseSource } from "./utils/test-helper"; +import { GeneratedFile } from "./utils/write-generated-files"; + +export enum TestEntitiy1Status { + Active = "Active", + Archived = "Archived", + Deleted = "Deleted", +} + +registerEnumType(TestEntitiy1Status, { name: "TestEntitiyStatus" }); + +@Entity() +class TestEntity1 extends BaseEntity { + @PrimaryKey({ type: "uuid" }) + id: string = uuid(); + + @Property() + title: string; + + @Enum({ items: () => TestEntitiy1Status }) + @Field(() => TestEntitiy1Status) + status: TestEntitiy1Status = TestEntitiy1Status.Active; +} + +describe("GenerateCrud Status with active", () => { + let lintedOut: GeneratedFile[]; + let orm: MikroORM; + beforeAll(async () => { + LazyMetadataStorage.load(); + orm = await MikroORM.init({ + type: "postgresql", + dbName: "test-db", + entities: [TestEntity1], + }); + const out = await generateCrud({ targetDirectory: __dirname }, orm.em.getMetadata().get("TestEntity1")); + lintedOut = await lintGeneratedFiles(out); + }); + afterAll(async () => { + orm.close(); + }); + + it("input should contain status", async () => { + const file = lintedOut.find((file) => file.name === "dto/test-entity1.input.ts"); + if (!file) throw new Error("File not found"); + const source = parseSource(file.content); + + const classes = source.getClasses(); + expect(classes.length).toBe(2); //update + create + + const cls = classes[0]; + expect(cls.getName()).toBe("TestEntity1Input"); + const structure = cls.getStructure(); + + const propNames = (structure.properties || []).map((prop) => prop.name); + + expect(propNames).toEqual(["title", "status"]); + }); + + it("resolver should not include update status mutation", async () => { + const file = lintedOut.find((file) => file.name === "test-entity1.resolver.ts"); + if (!file) throw new Error("File not found"); + + const source = parseSource(file.content); + + const classes = source.getClasses(); + expect(classes.length).toBe(1); + + const cls = classes[0]; + expect(cls.getName()).toBe("TestEntity1Resolver"); + const structure = cls.getStructure(); + + const methodNames = (structure.methods || []).map((method) => method.name); + + expect(methodNames).not.toContain("updateTestEntity1Status"); + }); + + it("args should use status enum as defined for enitity", async () => { + const file = lintedOut.find((file) => file.name === "dto/test-entity1s.args.ts"); + if (!file) throw new Error("File not found"); + const source = parseSource(file.content); + + const classes = source.getClasses(); + expect(classes.length).toBe(1); + + const cls = classes[0]; + expect(cls.getName()).toBe("TestEntity1sArgs"); + const structure = cls.getStructure(); + + const statusProp = (structure.properties || []).find((prop) => prop.name == "status"); + expect(statusProp).toBeTruthy(); + expect(statusProp?.type).toBe("TestEntitiy1Status"); + }); +}); + +export enum TestEntitiy2Status { + Published = "Published", + Unpublished = "Unpublished", + Archived = "Archived", + Deleted = "Deleted", +} + +registerEnumType(TestEntitiy2Status, { name: "TestEntitiy2Status" }); + +@Entity() +class TestEntity2 extends BaseEntity { + @PrimaryKey({ type: "uuid" }) + id: string = uuid(); + + @Property() + title: string; + + @Enum({ items: () => TestEntitiy2Status }) + @Field(() => TestEntitiy2Status) + status: TestEntitiy2Status = TestEntitiy2Status.Unpublished; +} + +describe("GenerateCrud Status with published/unpublished", () => { + let lintedOut: GeneratedFile[]; + let orm: MikroORM; + beforeAll(async () => { + LazyMetadataStorage.load(); + orm = await MikroORM.init({ + type: "postgresql", + dbName: "test-db", + entities: [TestEntity2], + }); + const out = await generateCrud({ targetDirectory: __dirname }, orm.em.getMetadata().get("TestEntity2")); + lintedOut = await lintGeneratedFiles(out); + }); + afterAll(async () => { + orm.close(); + }); + + it("args should include own status enum that doesn't include published/unpublished", async () => { + const file = lintedOut.find((file) => file.name === "dto/test-entity2s.args.ts"); + if (!file) throw new Error("File not found"); + + const source = parseSource(file.content); + + const classes = source.getClasses(); + expect(classes.length).toBe(1); + + const cls = classes[0]; + expect(cls.getName()).toBe("TestEntity2sArgs"); + const structure = cls.getStructure(); + + const statusProp = (structure.properties || []).find((prop) => prop.name == "status"); + expect(statusProp).toBeTruthy(); + expect(statusProp?.type).toBe("TestEntity2StatusFilter"); + + const enums = source.getEnums(); + expect(enums.length).toBe(1); + + const enumm = enums[0]; + expect(enumm.getName()).toBe("TestEntity2StatusFilter"); + const enumNames = enumm.getMembers().map((member) => member.getName()); + expect(enumNames).toContain("Active"); + expect(enumNames).toContain("Deleted"); + expect(enumNames).not.toContain("Published"); + expect(enumNames).not.toContain("Unpublished"); + }); +}); + +export enum TestEntitiy3Status { + Published = "Published", + Unpublished = "Unpublished", +} + +registerEnumType(TestEntitiy3Status, { name: "TestEntitiy3Status" }); + +@Entity() +class TestEntity3 extends BaseEntity { + @PrimaryKey({ type: "uuid" }) + id: string = uuid(); + + @Property() + title: string; + + @Enum({ items: () => TestEntitiy3Status }) + @Field(() => TestEntitiy3Status) + status: TestEntitiy3Status = TestEntitiy3Status.Unpublished; +} + +describe("GenerateCrud Status with published/unpublished", () => { + let lintedOut: GeneratedFile[]; + let orm: MikroORM; + beforeAll(async () => { + LazyMetadataStorage.load(); + orm = await MikroORM.init({ + type: "postgresql", + dbName: "test-db", + entities: [TestEntity3], + }); + const out = await generateCrud({ targetDirectory: __dirname }, orm.em.getMetadata().get("TestEntity3")); + lintedOut = await lintGeneratedFiles(out); + }); + afterAll(async () => { + orm.close(); + }); + + it("args should not include status filter as all are active ones", async () => { + const file = lintedOut.find((file) => file.name === "dto/test-entity3s.args.ts"); + if (!file) throw new Error("File not found"); + + const source = parseSource(file.content); + + const classes = source.getClasses(); + expect(classes.length).toBe(1); + + const cls = classes[0]; + expect(cls.getName()).toBe("TestEntity3sArgs"); + const structure = cls.getStructure(); + + const propNames = (structure.properties || []).map((prop) => prop.name); + + expect(propNames).not.toContain("status"); + }); +}); diff --git a/packages/api/cms-api/src/generator/generate-crud.ts b/packages/api/cms-api/src/generator/generate-crud.ts index 00c1fb802a..5189eef824 100644 --- a/packages/api/cms-api/src/generator/generate-crud.ts +++ b/packages/api/cms-api/src/generator/generate-crud.ts @@ -8,7 +8,7 @@ import { generateCrudInput } from "./generate-crud-input"; import { buildNameVariants, classNameToInstanceName } from "./utils/build-name-variants"; import { integerTypes } from "./utils/constants"; import { generateImportsCode, Imports } from "./utils/generate-imports-code"; -import { findEnumImportPath, findEnumName } from "./utils/ts-morph-helper"; +import { findEnumImportPath, findEnumName, morphTsProperty } from "./utils/ts-morph-helper"; import { GeneratedFile } from "./utils/write-generated-files"; // eslint-disable-next-line @typescript-eslint/explicit-function-return-type @@ -16,6 +16,7 @@ function buildOptions(metadata: EntityMetadata) { const { classNameSingular, classNamePlural, fileNameSingular, fileNamePlural } = buildNameVariants(metadata); const crudSearchPropNames = metadata.props + .filter((prop) => prop.name != "status") .filter((prop) => hasFieldFeature(metadata.class, prop.name, "search") && !prop.name.startsWith("scope_")) .reduce((acc, prop) => { if (prop.type === "string" || prop.type === "text") { @@ -37,6 +38,32 @@ function buildOptions(metadata: EntityMetadata) { }, [] as string[]); const hasSearchArg = crudSearchPropNames.length > 0; + let statusProp = metadata.props.find((prop) => prop.name == "status"); + if (statusProp) { + if (!statusProp.enum) { + console.warn(`${metadata.className} status prop must be an enum to be supported by crud generator`); + statusProp = undefined; + } else if (statusProp.nullable) { + console.warn(`${metadata.className} status prop must not be nullable to be supported by crud generator`); + statusProp = undefined; + } else if (morphTsProperty(statusProp.name, metadata).getInitializer()?.getText() == "") { + console.warn(`${metadata.className} status prop must have a default value to be supported by crud generator`); + statusProp = undefined; + } + } + let statusActiveItems: Array | undefined = undefined; + if (statusProp) { + if (!statusProp.items) throw new Error("Status enum prop has not items"); + statusActiveItems = statusProp.items.filter((item) => { + if (typeof item == "number") { + console.warn(`${metadata.className} status prop must not have numeric items to be supported by crud generator`); + return false; + } + return ["Active", "Visible", "Invisible", "Published", "Unpublished"].includes(item); + }); + } + const hasStatusFilter = statusProp && statusActiveItems && statusActiveItems.length != statusProp.items?.length; //if all items are active ones, no need for status filter + const crudFilterProps = metadata.props.filter( (prop) => hasFieldFeature(metadata.class, prop.name, "filter") && @@ -72,7 +99,7 @@ function buildOptions(metadata: EntityMetadata) { const hasSortArg = crudSortProps.length > 0; const hasSlugProp = metadata.props.some((prop) => prop.name == "slug"); - const hasVisibleProp = metadata.props.some((prop) => prop.name == "visible"); + const scopeProp = metadata.props.find((prop) => prop.name == "scope"); if (scopeProp && !scopeProp.targetMeta) throw new Error("Scope prop has no targetMeta"); const hasUpdatedAt = metadata.props.some((prop) => prop.name == "updatedAt"); @@ -91,7 +118,9 @@ function buildOptions(metadata: EntityMetadata) { crudSortProps, hasSortArg, hasSlugProp, - hasVisibleProp, + statusProp, + statusActiveItems, + hasStatusFilter, scopeProp, hasUpdatedAt, argsClassName, @@ -111,7 +140,7 @@ function generateFilterDto({ generatorOptions, metadata }: { generatorOptions: C crudFilterProps.map((prop) => { if (prop.enum) { const enumName = findEnumName(prop.name, metadata); - const importPath = findEnumImportPath(enumName, generatorOptions, metadata); + const importPath = findEnumImportPath(enumName, `${generatorOptions.targetDirectory}/dto`, metadata); if (!generatedEnumNames.has(enumName)) { generatedEnumNames.add(enumName); enumFiltersOut += `@InputType() @@ -252,21 +281,52 @@ function generatePaginatedDto({ generatorOptions, metadata }: { generatorOptions function generateArgsDto({ generatorOptions, metadata }: { generatorOptions: CrudGeneratorOptions; metadata: EntityMetadata }): string { const { classNameSingular, fileNameSingular } = buildNameVariants(metadata); - const { scopeProp, argsClassName, hasSearchArg, hasSortArg, hasFilterArg } = buildOptions(metadata); + const { scopeProp, argsClassName, hasSearchArg, hasSortArg, hasFilterArg, statusProp, statusActiveItems, hasStatusFilter } = + buildOptions(metadata); const imports: Imports = []; if (scopeProp && scopeProp.targetMeta) { imports.push(generateEntityImport(scopeProp.targetMeta, `${generatorOptions.targetDirectory}/dto`)); } - const argsOut = `import { ArgsType, Field, IntersectionType } from "@nestjs/graphql"; + let statusFilterClassName: string | undefined = undefined; + let statusFilterDefaultValue; + let statusFilterItems: Array | undefined = undefined; + if (hasStatusFilter && statusProp) { + if (statusActiveItems && statusActiveItems.length > 1) { + //more than one active status, create enum that compresses them into single "Active" + statusFilterItems = ["Active", ...(statusProp.items?.filter((item) => !statusActiveItems.includes(item)) ?? [])]; + statusFilterClassName = `${classNameSingular}StatusFilter`; + statusFilterDefaultValue = `${statusFilterClassName}.Active`; + } else { + statusFilterClassName = findEnumName(statusProp.name, metadata); + statusFilterDefaultValue = morphTsProperty(statusProp.name, metadata).getInitializer()?.getText(); + imports.push({ + name: statusFilterClassName, + importPath: findEnumImportPath(statusFilterClassName, `${generatorOptions.targetDirectory}/dto`, metadata), + }); + } + } + + const argsOut = `import { ArgsType, Field, IntersectionType, registerEnumType } from "@nestjs/graphql"; import { Type } from "class-transformer"; - import { IsOptional, IsString, ValidateNested } from "class-validator"; + import { IsOptional, IsString, ValidateNested, IsEnum } from "class-validator"; import { OffsetBasedPaginationArgs } from "@comet/cms-api"; import { ${classNameSingular}Filter } from "./${fileNameSingular}.filter"; import { ${classNameSingular}Sort } from "./${fileNameSingular}.sort"; ${generateImportsCode(imports)} + ${ + statusFilterItems && statusFilterClassName + ? ` + export enum ${statusFilterClassName} { + ${statusFilterItems.map((item) => `${item} = "${item}",`).join("\n")} + } + registerEnumType(${statusFilterClassName}, { name: "${statusFilterClassName}" }); + ` + : "" + } + @ArgsType() export class ${argsClassName} extends OffsetBasedPaginationArgs { ${ @@ -280,6 +340,16 @@ function generateArgsDto({ generatorOptions, metadata }: { generatorOptions: Cru : "" } + ${ + hasStatusFilter + ? ` + @Field(() => ${statusFilterClassName}, { defaultValue: ${statusFilterDefaultValue} }) + @IsEnum(${statusFilterClassName}) + status: ${statusFilterClassName}; + ` + : "" + } + ${ hasSearchArg ? ` @@ -382,7 +452,7 @@ function generateInputHandling( metadata: EntityMetadata, ): string { const { instanceNameSingular } = buildNameVariants(metadata); - const { blockProps, hasVisibleProp, scopeProp } = buildOptions(metadata); + const { blockProps, scopeProp } = buildOptions(metadata); const props = metadata.props.filter((prop) => !options.excludeFields || !options.excludeFields.includes(prop.name)); @@ -442,7 +512,6 @@ function generateInputHandling( } ${options.assignEntityCode} ...${noAssignProps.length ? `assignInput` : options.inputName}, - ${options.mode == "create" && hasVisibleProp ? `visible: false,` : ""} ${options.mode == "create" && scopeProp ? `scope,` : ""} ${ options.mode == "create" || options.mode == "updateNested" @@ -669,8 +738,19 @@ function generateRelationsFieldResolver({ generatorOptions, metadata }: { genera function generateResolver({ generatorOptions, metadata }: { generatorOptions: CrudGeneratorOptions; metadata: EntityMetadata }): string { const { classNameSingular, fileNameSingular, instanceNameSingular, classNamePlural, fileNamePlural, instanceNamePlural } = buildNameVariants(metadata); - const { scopeProp, argsClassName, argsFileName, hasSlugProp, hasSearchArg, hasSortArg, hasFilterArg, hasVisibleProp, hasUpdatedAt } = - buildOptions(metadata); + const { + scopeProp, + argsClassName, + argsFileName, + hasSlugProp, + hasSearchArg, + hasSortArg, + hasFilterArg, + statusProp, + statusActiveItems, + hasStatusFilter, + hasUpdatedAt, + } = buildOptions(metadata); const relationManyToOneProps = metadata.props.filter((prop) => prop.reference === "m:1"); const relationOneToManyProps = metadata.props.filter((prop) => prop.reference === "1:m"); @@ -706,6 +786,15 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr imports.push(generateEntityImport(scopeProp.targetMeta, generatorOptions.targetDirectory)); } + if (statusProp) { + const enumName = findEnumName(statusProp.name, metadata); + const importPath = findEnumImportPath(enumName, generatorOptions.targetDirectory, metadata); + imports.push({ + name: enumName, + importPath, + }); + } + const resolverOut = `import { InjectRepository } from "@mikro-orm/nestjs"; import { EntityRepository, EntityManager } from "@mikro-orm/postgresql"; import { FindOptions, Reference } from "@mikro-orm/core"; @@ -760,15 +849,35 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr @Query(() => Paginated${classNamePlural}) async ${instanceNameSingular != instanceNamePlural ? instanceNamePlural : `${instanceNamePlural}List`}( - @Args() { ${scopeProp ? `scope, ` : ""}${hasSearchArg ? `search, ` : ""}${hasFilterArg ? `filter, ` : ""}${ - hasSortArg ? `sort, ` : "" - }offset, limit }: ${argsClassName}${hasOutputRelations ? `, @Info() info: GraphQLResolveInfo` : ""} + @Args() {${Object.entries({ + scope: !!scopeProp, + status: !!hasStatusFilter, + search: !!hasSearchArg, + filter: !!hasFilterArg, + sort: !!hasSortArg, + offset: true, + limit: true, + }) + .filter(([key, use]) => use) + .map(([key]) => key) + .join(", ")}}: ${argsClassName} + ${hasOutputRelations ? `, @Info() info: GraphQLResolveInfo` : ""} ): Promise { const where${ hasSearchArg || hasFilterArg ? ` = this.${instanceNamePlural}Service.getFindCondition({ ${hasSearchArg ? `search, ` : ""}${hasFilterArg ? `filter, ` : ""} });` : `: ObjectQuery<${metadata.className}> = {}` } + ${ + hasStatusFilter && statusActiveItems && statusActiveItems.length > 1 + ? `if (status == "Active") { + where.status = { $in: [${statusActiveItems.map((item) => `"${item}"`).join(", ")}] }; + } else { + where.status = status; + }` + : "" + } + ${hasStatusFilter && statusActiveItems && statusActiveItems.length <= 1 ? `where.status = status;` : ""} ${scopeProp ? `where.scope = scope;` : ""} ${ @@ -882,28 +991,6 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr : "" } - ${ - hasVisibleProp && generatorOptions.update - ? ` - @Mutation(() => ${metadata.className}) - @AffectedEntity(${metadata.className}) - async update${classNameSingular}Visibility( - @Args("id", { type: () => ID }) id: string, - @Args("visible", { type: () => Boolean }) visible: boolean, - ): Promise<${metadata.className}> { - const ${instanceNameSingular} = await this.repository.findOneOrFail(id); - - ${instanceNameSingular}.assign({ - visible, - }); - await this.entityManager.flush(); - - return ${instanceNameSingular}; - } - ` - : "" - } - ${relationsFieldResolverCode} } diff --git a/packages/api/cms-api/src/generator/utils/ts-morph-helper.ts b/packages/api/cms-api/src/generator/utils/ts-morph-helper.ts index 21b60a074b..23d5f84589 100644 --- a/packages/api/cms-api/src/generator/utils/ts-morph-helper.ts +++ b/packages/api/cms-api/src/generator/utils/ts-morph-helper.ts @@ -25,7 +25,7 @@ export function morphTsProperty(name: string, metadata: EntityMetadata) { return tsClass.getPropertyOrThrow(name); } -function findImportPath(importName: string, generatorOptions: { targetDirectory: string }, metadata: EntityMetadata) { +function findImportPath(importName: string, targetDirectory: string, metadata: EntityMetadata) { const tsSource = morphTsSource(metadata); for (const tsImport of tsSource.getImportDeclarations()) { for (const namedImport of tsImport.getNamedImports()) { @@ -45,7 +45,7 @@ function findImportPath(importName: string, generatorOptions: { targetDirectory: if (importPath.startsWith("./") || importPath.startsWith("../")) { const absolutePath = path.resolve(path.dirname(metadata.path), importPath); return { - importPath: path.relative(`${generatorOptions.targetDirectory}/dto`, absolutePath), + importPath: path.relative(targetDirectory, absolutePath), exportedDeclaration, }; } else { @@ -66,17 +66,17 @@ export function findEnumName(propertyName: string, metadata: EntityMetadata .replace(/\[\]$/, ""); } -export function findEnumImportPath(enumName: string, generatorOptions: { targetDirectory: string }, metadata: EntityMetadata): string { +export function findEnumImportPath(enumName: string, targetDirectory: string, metadata: EntityMetadata): string { const tsSource = morphTsSource(metadata); if (tsSource.getEnum(enumName)) { //enum defined in same file as entity if (!tsSource.getEnum(enumName)?.isExported()) { throw new Error(`Enum ${enumName} is not exported in ${metadata.path}`); } - return path.relative(`${generatorOptions.targetDirectory}/dto`, metadata.path).replace(/\.ts$/, ""); + return path.relative(targetDirectory, metadata.path).replace(/\.ts$/, ""); } else { //try to find import where enum is imported from - const { importPath } = findImportPath(enumName, generatorOptions, metadata); + const { importPath } = findImportPath(enumName, targetDirectory, metadata); return importPath; } } @@ -89,7 +89,7 @@ export function findBlockName(propertyName: string, metadata: EntityMetadata): string { +export function findBlockImportPath(blockName: string, targetDirectory: string, metadata: EntityMetadata): string { const tsSource = morphTsSource(metadata); if (tsSource.getVariableDeclaration(blockName)) { @@ -97,15 +97,15 @@ export function findBlockImportPath(blockName: string, generatorOptions: { targe if (!tsSource.getVariableDeclaration(blockName)?.isExported()) { throw new Error(`Block ${blockName} is not exported in ${metadata.path}`); } - return path.relative(`${generatorOptions.targetDirectory}/dto`, metadata.path).replace(/\.ts$/, ""); + return path.relative(targetDirectory, metadata.path).replace(/\.ts$/, ""); } else { //try to find import where block is imported from - const { importPath } = findImportPath(blockName, generatorOptions, metadata); + const { importPath } = findImportPath(blockName, targetDirectory, metadata); return importPath; } } -export function findInputClassImportPath(className: string, generatorOptions: { targetDirectory: string }, metadata: EntityMetadata): string { +export function findInputClassImportPath(className: string, targetDirectory: string, metadata: EntityMetadata): string { const tsSource = morphTsSource(metadata); let returnImportPath: string; @@ -118,10 +118,10 @@ export function findInputClassImportPath(className: string, generatorOptions: { } // eslint-disable-next-line @typescript-eslint/no-non-null-assertion classDeclaration = tsSource.getClass(className)!; - returnImportPath = path.relative(`${generatorOptions.targetDirectory}/dto`, metadata.path).replace(/\.ts$/, ""); + returnImportPath = path.relative(targetDirectory, metadata.path).replace(/\.ts$/, ""); } else { //try to find import where block is imported from - const { importPath, exportedDeclaration } = findImportPath(className, generatorOptions, metadata); + const { importPath, exportedDeclaration } = findImportPath(className, targetDirectory, metadata); if (!(exportedDeclaration instanceof ClassDeclaration)) { throw new Error(`Exported declaration for ${className} is not a class`); } From a952ab0a98a37f9d878517c8c43d562922813bf0 Mon Sep 17 00:00:00 2001 From: Ricky James Smith Date: Tue, 5 Mar 2024 09:20:39 +0100 Subject: [PATCH 17/18] Fix syntax error in Demo migration (#1788) --- demo/api/src/db/migrations/Migration20231009090817.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/demo/api/src/db/migrations/Migration20231009090817.ts b/demo/api/src/db/migrations/Migration20231009090817.ts index d71a48f289..1d376df959 100644 --- a/demo/api/src/db/migrations/Migration20231009090817.ts +++ b/demo/api/src/db/migrations/Migration20231009090817.ts @@ -4,7 +4,7 @@ export class Migration20231009090817 extends Migration { async up(): Promise { this.addSql('alter table "Product" add column "status" text check ("status" in (\'Published\', \'Unpublished\')) not null default \'Published\';'); - this.addSql('alter table "Product" alter colulmn status drop default;'); + this.addSql('alter table "Product" alter column status drop default;'); this.addSql('alter table "Product" drop column "visible";'); } From 9bed75638032607b11272e1822ff7a1cbaf32e6f Mon Sep 17 00:00:00 2001 From: Niko Sams Date: Tue, 5 Mar 2024 10:33:53 +0100 Subject: [PATCH 18/18] API Generator: Generate better API for Many-to-one-relations with `orphanRemoval` activated where the reverse side has its own API generated (#1478) - Add `id` as argument to create mutation - Add `id` as argument to list query --------- Co-authored-by: Johannes Obermair <48853629+johnnyomair@users.noreply.github.com> --- .changeset/giant-apples-cheer.md | 8 ++ demo/admin/src/products/ProductForm.tsx | 1 - demo/admin/src/products/ProductsGrid.tsx | 12 +- .../categories/ProductCategoriesTable.tsx | 7 +- demo/api/schema.gql | 60 ++++++++- .../db/migrations/Migration20231204165948.ts | 15 +++ .../entities/product-category.entity.ts | 2 +- .../products/entities/product-color.entity.ts | 44 ++++++ .../entities/product-variant.entity.ts | 5 +- .../src/products/entities/product.entity.ts | 13 +- .../dto/paginated-product-variants.ts | 9 ++ .../generated/dto/product-category.input.ts | 9 +- .../dto/product-color.nested.input.ts | 17 +++ .../generated/dto/product-variant.filter.ts | 39 ++++++ ...sted.input.ts => product-variant.input.ts} | 5 +- .../generated/dto/product-variant.sort.ts | 26 ++++ .../generated/dto/product-variants.args.ts | 33 +++++ .../products/generated/dto/product.input.ts | 8 +- .../generated/product-category.resolver.ts | 23 +--- .../generated/product-color.resolver.ts | 17 +++ .../generated/product-variant.resolver.ts | 115 +++++++++++++++- .../generated/product-variants.service.ts | 34 +++++ .../products/generated/product.resolver.ts | 41 +++--- demo/api/src/products/products.module.ts | 7 +- .../src/generator/generate-crud-input.ts | 7 + ...enerate-crud-relation-orpahremoval.spec.ts | 114 ++++++++++++++++ .../cms-api/src/generator/generate-crud.ts | 127 ++++++++++++++---- 27 files changed, 700 insertions(+), 98 deletions(-) create mode 100644 .changeset/giant-apples-cheer.md create mode 100644 demo/api/src/db/migrations/Migration20231204165948.ts create mode 100644 demo/api/src/products/entities/product-color.entity.ts create mode 100644 demo/api/src/products/generated/dto/paginated-product-variants.ts create mode 100644 demo/api/src/products/generated/dto/product-color.nested.input.ts create mode 100644 demo/api/src/products/generated/dto/product-variant.filter.ts rename demo/api/src/products/generated/dto/{product-variant.nested.input.ts => product-variant.input.ts} (81%) create mode 100644 demo/api/src/products/generated/dto/product-variant.sort.ts create mode 100644 demo/api/src/products/generated/dto/product-variants.args.ts create mode 100644 demo/api/src/products/generated/product-color.resolver.ts create mode 100644 demo/api/src/products/generated/product-variants.service.ts create mode 100644 packages/api/cms-api/src/generator/generate-crud-relation-orpahremoval.spec.ts diff --git a/.changeset/giant-apples-cheer.md b/.changeset/giant-apples-cheer.md new file mode 100644 index 0000000000..ca8f0310fb --- /dev/null +++ b/.changeset/giant-apples-cheer.md @@ -0,0 +1,8 @@ +--- +"@comet/cms-api": major +--- + +API Generator: Generate better API for Many-to-one-relations with `orphanRemoval` activated where the reverse side has its own API generated + +- Add `id` as argument to create mutation +- Add `id` as argument to list query diff --git a/demo/admin/src/products/ProductForm.tsx b/demo/admin/src/products/ProductForm.tsx index efaba151ac..1465dfb0f6 100644 --- a/demo/admin/src/products/ProductForm.tsx +++ b/demo/admin/src/products/ProductForm.tsx @@ -104,7 +104,6 @@ function ProductForm({ id }: FormProps): React.ReactElement { type: formValues.type as GQLProductType, category: formValues.category?.id, tags: formValues.tags.map((i) => i.id), - variants: [], articleNumbers: [], discounts: [], statistics: { views: 0 }, diff --git a/demo/admin/src/products/ProductsGrid.tsx b/demo/admin/src/products/ProductsGrid.tsx index 55eb9cca70..90643086c5 100644 --- a/demo/admin/src/products/ProductsGrid.tsx +++ b/demo/admin/src/products/ProductsGrid.tsx @@ -160,10 +160,7 @@ function ProductsGrid() { type: input.type, category: input.category?.id, tags: input.tags.map((tag) => tag.id), - variants: input.variants.map((variant) => ({ - name: variant.name, - image: DamImageBlock.state2Output(DamImageBlock.input2State(variant.image)), - })), + colors: input.colors, articleNumbers: input.articleNumbers, discounts: input.discounts, statistics: { views: 0 }, @@ -236,9 +233,12 @@ const productsFragment = gql` id title } - variants { - image + colors { name + hexCode + } + variants { + id } articleNumbers discounts { diff --git a/demo/admin/src/products/categories/ProductCategoriesTable.tsx b/demo/admin/src/products/categories/ProductCategoriesTable.tsx index 3a2682669f..7faf9028c5 100644 --- a/demo/admin/src/products/categories/ProductCategoriesTable.tsx +++ b/demo/admin/src/products/categories/ProductCategoriesTable.tsx @@ -74,7 +74,12 @@ const columns: GridColDef[] = [ onPaste={async ({ input, client }) => { await client.mutate({ mutation: createProductMutation, - variables: { input: { ...input, products: [] } }, + variables: { + input: { + title: input.title, + slug: input.slug, + }, + }, }); }} onDelete={async ({ client }) => { diff --git a/demo/api/schema.gql b/demo/api/schema.gql index d6ebb2d3db..d7561bfd04 100644 --- a/demo/api/schema.gql +++ b/demo/api/schema.gql @@ -466,6 +466,14 @@ type ProductCategory implements DocumentInterface { products: [Product!]! } +type ProductColor implements DocumentInterface { + id: ID! + updatedAt: DateTime! + name: String! + hexCode: String! + createdAt: DateTime! +} + type ProductStatistics { id: ID! views: Float! @@ -487,6 +495,7 @@ type ProductVariant implements DocumentInterface { name: String! image: DamImageBlockData! createdAt: DateTime! + product: Product! } type ProductDiscounts { @@ -520,6 +529,7 @@ type Product implements DocumentInterface { createdAt: DateTime! category: ProductCategory manufacturer: Manufacturer + colors: [ProductColor!]! variants: [ProductVariant!]! tags: [ProductTag!]! } @@ -550,6 +560,11 @@ type PaginatedProductTags { totalCount: Int! } +type PaginatedProductVariants { + nodes: [ProductVariant!]! + totalCount: Int! +} + type RedirectScope { domain: String! } @@ -732,6 +747,8 @@ type Query { productCategories(offset: Int! = 0, limit: Int! = 25, search: String, filter: ProductCategoryFilter, sort: [ProductCategorySort!]): PaginatedProductCategories! productTag(id: ID!): ProductTag! productTags(offset: Int! = 0, limit: Int! = 25, search: String, filter: ProductTagFilter, sort: [ProductTagSort!]): PaginatedProductTags! + productVariant(id: ID!): ProductVariant! + productVariants(offset: Int! = 0, limit: Int! = 25, product: ID!, search: String, filter: ProductVariantFilter, sort: [ProductVariantSort!]): PaginatedProductVariants! manufacturer(id: ID!): Manufacturer! manufacturers(offset: Int! = 0, limit: Int! = 25, search: String, filter: ManufacturerFilter, sort: [ManufacturerSort!]): PaginatedManufacturers! } @@ -1003,6 +1020,26 @@ enum ProductTagSortField { updatedAt } +input ProductVariantFilter { + name: StringFilter + createdAt: DateFilter + updatedAt: DateFilter + and: [ProductVariantFilter!] + or: [ProductVariantFilter!] +} + +input ProductVariantSort { + field: ProductVariantSortField! + direction: SortDirection! = ASC +} + +enum ProductVariantSortField { + name + product + createdAt + updatedAt +} + input ManufacturerFilter { addressAsEmbeddable_street: StringFilter addressAsEmbeddable_streetNumber: NumberFilter @@ -1088,6 +1125,9 @@ type Mutation { createProductTag(input: ProductTagInput!): ProductTag! updateProductTag(id: ID!, input: ProductTagUpdateInput!, lastUpdatedAt: DateTime): ProductTag! deleteProductTag(id: ID!): Boolean! + createProductVariant(product: ID!, input: ProductVariantInput!): ProductVariant! + updateProductVariant(id: ID!, input: ProductVariantUpdateInput!, lastUpdatedAt: DateTime): ProductVariant! + deleteProductVariant(id: ID!): Boolean! createManufacturer(input: ManufacturerInput!): Manufacturer! updateManufacturer(id: ID!, input: ManufacturerUpdateInput!, lastUpdatedAt: DateTime): Manufacturer! deleteManufacturer(id: ID!): Boolean! @@ -1284,7 +1324,7 @@ input ProductInput { articleNumbers: [String!]! = [] dimensions: ProductDimensionsInput statistics: ProductStatisticsInput - variants: [ProductVariantInput!]! = [] + colors: [ProductColorInput!]! = [] category: ID = null tags: [ID!]! = [] manufacturer: ID @@ -1294,9 +1334,9 @@ input ProductStatisticsInput { views: Float! } -input ProductVariantInput { +input ProductColorInput { name: String! - image: DamImageBlockInput! + hexCode: String! } input ProductUpdateInput { @@ -1313,7 +1353,7 @@ input ProductUpdateInput { articleNumbers: [String!] dimensions: ProductDimensionsInput statistics: ProductStatisticsInput - variants: [ProductVariantInput!] + colors: [ProductColorInput!] category: ID tags: [ID!] manufacturer: ID @@ -1322,13 +1362,11 @@ input ProductUpdateInput { input ProductCategoryInput { title: String! slug: String! - products: [ID!]! = [] } input ProductCategoryUpdateInput { title: String slug: String - products: [ID!] } input ProductTagInput { @@ -1341,6 +1379,16 @@ input ProductTagUpdateInput { products: [ID!] } +input ProductVariantInput { + name: String! + image: DamImageBlockInput! +} + +input ProductVariantUpdateInput { + name: String + image: DamImageBlockInput +} + input ManufacturerInput { address: AddressInput addressAsEmbeddable: AddressAsEmbeddableInput! diff --git a/demo/api/src/db/migrations/Migration20231204165948.ts b/demo/api/src/db/migrations/Migration20231204165948.ts new file mode 100644 index 0000000000..58f912acc8 --- /dev/null +++ b/demo/api/src/db/migrations/Migration20231204165948.ts @@ -0,0 +1,15 @@ +import { Migration } from '@mikro-orm/migrations'; + +export class Migration20231204165948 extends Migration { + + async up(): Promise { + this.addSql('create table "ProductColor" ("id" uuid not null, "name" varchar(255) not null, "hexCode" varchar(255) not null, "product" uuid not null, "createdAt" timestamptz(0) not null, "updatedAt" timestamptz(0) not null, constraint "ProductColor_pkey" primary key ("id"));'); + + this.addSql('alter table "ProductColor" add constraint "ProductColor_product_foreign" foreign key ("product") references "Product" ("id") on update cascade;'); + } + + async down(): Promise { + this.addSql('drop table if exists "ProductColor" cascade;'); + } + +} diff --git a/demo/api/src/products/entities/product-category.entity.ts b/demo/api/src/products/entities/product-category.entity.ts index d0a9ed81ce..45168f1f52 100644 --- a/demo/api/src/products/entities/product-category.entity.ts +++ b/demo/api/src/products/entities/product-category.entity.ts @@ -30,7 +30,7 @@ export class ProductCategory extends BaseEntity implement //search: true, //not implemented //filter: true, //not implemented //sort: true, //not implemented - input: true, //default is true + input: false, //default is true }) @OneToMany(() => Product, (products) => products.category) products = new Collection(this); diff --git a/demo/api/src/products/entities/product-color.entity.ts b/demo/api/src/products/entities/product-color.entity.ts new file mode 100644 index 0000000000..ff595393b0 --- /dev/null +++ b/demo/api/src/products/entities/product-color.entity.ts @@ -0,0 +1,44 @@ +import { CrudField, DocumentInterface } from "@comet/cms-api"; +import { BaseEntity, Entity, ManyToOne, OptionalProps, PrimaryKey, Property, Ref } from "@mikro-orm/core"; +import { Field, ID, ObjectType } from "@nestjs/graphql"; +import { v4 as uuid } from "uuid"; + +import { Product } from "./product.entity"; + +@ObjectType({ + implements: () => [DocumentInterface], +}) +@Entity() +export class ProductColor extends BaseEntity implements DocumentInterface { + [OptionalProps]?: "createdAt" | "updatedAt"; + + @PrimaryKey({ type: "uuid" }) + @Field(() => ID) + id: string = uuid(); + + @Property() + @Field() + name: string; + + @Property() + @Field() + hexCode: string; + + @ManyToOne(() => Product, { ref: true }) + @CrudField({ + resolveField: true, // default is true + // search: true, // not yet supported for nested + // filter: true, // not yet supported for nested + // sort: true, // not yet supported for nested + // input: true, // not supported for nested, doesn't make sense + }) + product: Ref; + + @Property() + @Field() + createdAt: Date = new Date(); + + @Property({ onUpdate: () => new Date() }) + @Field() + updatedAt: Date = new Date(); +} diff --git a/demo/api/src/products/entities/product-variant.entity.ts b/demo/api/src/products/entities/product-variant.entity.ts index 27b1b2e1a6..08a4ff88a3 100644 --- a/demo/api/src/products/entities/product-variant.entity.ts +++ b/demo/api/src/products/entities/product-variant.entity.ts @@ -1,5 +1,5 @@ import { BlockDataInterface, RootBlock, RootBlockEntity } from "@comet/blocks-api"; -import { CrudField, DamImageBlock, DocumentInterface, RootBlockDataScalar, RootBlockType } from "@comet/cms-api"; +import { CrudField, CrudGenerator, DamImageBlock, DocumentInterface, RootBlockDataScalar, RootBlockType } from "@comet/cms-api"; import { BaseEntity, Entity, ManyToOne, OptionalProps, PrimaryKey, Property, Ref } from "@mikro-orm/core"; import { Field, ID, ObjectType } from "@nestjs/graphql"; import { v4 as uuid } from "uuid"; @@ -11,6 +11,7 @@ import { Product } from "./product.entity"; }) @Entity() @RootBlockEntity() +@CrudGenerator({ targetDirectory: `${__dirname}/../generated/`, requiredPermission: "products" }) export class ProductVariant extends BaseEntity implements DocumentInterface { [OptionalProps]?: "createdAt" | "updatedAt"; @@ -33,7 +34,7 @@ export class ProductVariant extends BaseEntity implements // search: true, // not yet supported for nested // filter: true, // not yet supported for nested // sort: true, // not yet supported for nested - // input: true, // not supported for nested, doesn't make sense + // input: false, // ignored because product is a root argument for create }) product: Ref; diff --git a/demo/api/src/products/entities/product.entity.ts b/demo/api/src/products/entities/product.entity.ts index f6531e607d..5b801e0645 100644 --- a/demo/api/src/products/entities/product.entity.ts +++ b/demo/api/src/products/entities/product.entity.ts @@ -21,6 +21,7 @@ import { IsNumber } from "class-validator"; import { v4 as uuid } from "uuid"; import { ProductCategory } from "./product-category.entity"; +import { ProductColor } from "./product-color.entity"; import { ProductStatistics } from "./product-statistics.entity"; import { ProductTag } from "./product-tag.entity"; import { ProductType } from "./product-type.enum"; @@ -140,7 +141,7 @@ export class Product extends BaseEntity implements DocumentInterf @Field(() => ProductStatistics, { nullable: true }) statistics?: Ref = undefined; - @OneToMany(() => ProductVariant, (variant) => variant.product, { orphanRemoval: true }) + @OneToMany(() => ProductColor, (variant) => variant.product, { orphanRemoval: true }) @CrudField({ resolveField: true, //default is true //search: true, //not yet implemented @@ -148,6 +149,16 @@ export class Product extends BaseEntity implements DocumentInterf //sort: true, //not yet implemented input: true, //default is true }) + colors = new Collection(this); + + @OneToMany(() => ProductVariant, (variant) => variant.product, { orphanRemoval: true }) + @CrudField({ + resolveField: true, //default is true + //search: true, //not yet implemented + //filter: true, //not yet implemented + //sort: true, //not yet implemented + input: false, //default is true, disabled here because it is edited using it's own crud api + }) variants = new Collection(this); @ManyToOne(() => ProductCategory, { nullable: true, ref: true }) diff --git a/demo/api/src/products/generated/dto/paginated-product-variants.ts b/demo/api/src/products/generated/dto/paginated-product-variants.ts new file mode 100644 index 0000000000..b587754995 --- /dev/null +++ b/demo/api/src/products/generated/dto/paginated-product-variants.ts @@ -0,0 +1,9 @@ +// This file has been generated by comet api-generator. +// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. +import { PaginatedResponseFactory } from "@comet/cms-api"; +import { ObjectType } from "@nestjs/graphql"; + +import { ProductVariant } from "../../entities/product-variant.entity"; + +@ObjectType() +export class PaginatedProductVariants extends PaginatedResponseFactory.create(ProductVariant) {} diff --git a/demo/api/src/products/generated/dto/product-category.input.ts b/demo/api/src/products/generated/dto/product-category.input.ts index 4cfef5962f..966b1d3d67 100644 --- a/demo/api/src/products/generated/dto/product-category.input.ts +++ b/demo/api/src/products/generated/dto/product-category.input.ts @@ -1,8 +1,8 @@ // This file has been generated by comet api-generator. // You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. import { IsSlug, PartialType } from "@comet/cms-api"; -import { Field, ID, InputType } from "@nestjs/graphql"; -import { IsArray, IsNotEmpty, IsString, IsUUID } from "class-validator"; +import { Field, InputType } from "@nestjs/graphql"; +import { IsNotEmpty, IsString } from "class-validator"; @InputType() export class ProductCategoryInput { @@ -16,11 +16,6 @@ export class ProductCategoryInput { @IsSlug() @Field() slug: string; - - @Field(() => [ID], { defaultValue: [] }) - @IsArray() - @IsUUID(undefined, { each: true }) - products: string[]; } @InputType() diff --git a/demo/api/src/products/generated/dto/product-color.nested.input.ts b/demo/api/src/products/generated/dto/product-color.nested.input.ts new file mode 100644 index 0000000000..1c294a03e5 --- /dev/null +++ b/demo/api/src/products/generated/dto/product-color.nested.input.ts @@ -0,0 +1,17 @@ +// This file has been generated by comet api-generator. +// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. +import { Field, InputType } from "@nestjs/graphql"; +import { IsNotEmpty, IsString } from "class-validator"; + +@InputType() +export class ProductColorInput { + @IsNotEmpty() + @IsString() + @Field() + name: string; + + @IsNotEmpty() + @IsString() + @Field() + hexCode: string; +} diff --git a/demo/api/src/products/generated/dto/product-variant.filter.ts b/demo/api/src/products/generated/dto/product-variant.filter.ts new file mode 100644 index 0000000000..58a50e98d5 --- /dev/null +++ b/demo/api/src/products/generated/dto/product-variant.filter.ts @@ -0,0 +1,39 @@ +// This file has been generated by comet api-generator. +// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. +import { DateFilter, StringFilter } from "@comet/cms-api"; +import { Field, InputType } from "@nestjs/graphql"; +import { Type } from "class-transformer"; +import { IsOptional, ValidateNested } from "class-validator"; + +@InputType() +export class ProductVariantFilter { + @Field(() => StringFilter, { nullable: true }) + @ValidateNested() + @IsOptional() + @Type(() => StringFilter) + name?: StringFilter; + + @Field(() => DateFilter, { nullable: true }) + @ValidateNested() + @IsOptional() + @Type(() => DateFilter) + createdAt?: DateFilter; + + @Field(() => DateFilter, { nullable: true }) + @ValidateNested() + @IsOptional() + @Type(() => DateFilter) + updatedAt?: DateFilter; + + @Field(() => [ProductVariantFilter], { nullable: true }) + @Type(() => ProductVariantFilter) + @ValidateNested({ each: true }) + @IsOptional() + and?: ProductVariantFilter[]; + + @Field(() => [ProductVariantFilter], { nullable: true }) + @Type(() => ProductVariantFilter) + @ValidateNested({ each: true }) + @IsOptional() + or?: ProductVariantFilter[]; +} diff --git a/demo/api/src/products/generated/dto/product-variant.nested.input.ts b/demo/api/src/products/generated/dto/product-variant.input.ts similarity index 81% rename from demo/api/src/products/generated/dto/product-variant.nested.input.ts rename to demo/api/src/products/generated/dto/product-variant.input.ts index 1e6ab9343d..f9d5a6dbd2 100644 --- a/demo/api/src/products/generated/dto/product-variant.nested.input.ts +++ b/demo/api/src/products/generated/dto/product-variant.input.ts @@ -1,7 +1,7 @@ // This file has been generated by comet api-generator. // You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. import { BlockInputInterface, isBlockInputInterface } from "@comet/blocks-api"; -import { DamImageBlock, RootBlockInputScalar } from "@comet/cms-api"; +import { DamImageBlock, PartialType, RootBlockInputScalar } from "@comet/cms-api"; import { Field, InputType } from "@nestjs/graphql"; import { Transform } from "class-transformer"; import { IsNotEmpty, IsString, ValidateNested } from "class-validator"; @@ -19,3 +19,6 @@ export class ProductVariantInput { @ValidateNested() image: BlockInputInterface; } + +@InputType() +export class ProductVariantUpdateInput extends PartialType(ProductVariantInput) {} diff --git a/demo/api/src/products/generated/dto/product-variant.sort.ts b/demo/api/src/products/generated/dto/product-variant.sort.ts new file mode 100644 index 0000000000..41b8738148 --- /dev/null +++ b/demo/api/src/products/generated/dto/product-variant.sort.ts @@ -0,0 +1,26 @@ +// This file has been generated by comet api-generator. +// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. +import { SortDirection } from "@comet/cms-api"; +import { Field, InputType, registerEnumType } from "@nestjs/graphql"; +import { IsEnum } from "class-validator"; + +export enum ProductVariantSortField { + name = "name", + product = "product", + createdAt = "createdAt", + updatedAt = "updatedAt", +} +registerEnumType(ProductVariantSortField, { + name: "ProductVariantSortField", +}); + +@InputType() +export class ProductVariantSort { + @Field(() => ProductVariantSortField) + @IsEnum(ProductVariantSortField) + field: ProductVariantSortField; + + @Field(() => SortDirection, { defaultValue: SortDirection.ASC }) + @IsEnum(SortDirection) + direction: SortDirection = SortDirection.ASC; +} diff --git a/demo/api/src/products/generated/dto/product-variants.args.ts b/demo/api/src/products/generated/dto/product-variants.args.ts new file mode 100644 index 0000000000..72e19395c0 --- /dev/null +++ b/demo/api/src/products/generated/dto/product-variants.args.ts @@ -0,0 +1,33 @@ +// This file has been generated by comet api-generator. +// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. +import { OffsetBasedPaginationArgs } from "@comet/cms-api"; +import { ArgsType, Field, ID } from "@nestjs/graphql"; +import { Type } from "class-transformer"; +import { IsOptional, IsString, IsUUID, ValidateNested } from "class-validator"; + +import { ProductVariantFilter } from "./product-variant.filter"; +import { ProductVariantSort } from "./product-variant.sort"; + +@ArgsType() +export class ProductVariantsArgs extends OffsetBasedPaginationArgs { + @Field(() => ID) + @IsUUID() + product: string; + + @Field({ nullable: true }) + @IsOptional() + @IsString() + search?: string; + + @Field(() => ProductVariantFilter, { nullable: true }) + @ValidateNested() + @Type(() => ProductVariantFilter) + @IsOptional() + filter?: ProductVariantFilter; + + @Field(() => [ProductVariantSort], { nullable: true }) + @ValidateNested({ each: true }) + @Type(() => ProductVariantSort) + @IsOptional() + sort?: ProductVariantSort[]; +} diff --git a/demo/api/src/products/generated/dto/product.input.ts b/demo/api/src/products/generated/dto/product.input.ts index 3a183a4f11..25f64605d5 100644 --- a/demo/api/src/products/generated/dto/product.input.ts +++ b/demo/api/src/products/generated/dto/product.input.ts @@ -8,8 +8,8 @@ import { IsArray, IsBoolean, IsDate, IsEnum, IsNotEmpty, IsNumber, IsString, IsU import { ProductDimensions, ProductDiscounts, ProductStatus } from "../../entities/product.entity"; import { ProductType } from "../../entities/product-type.enum"; +import { ProductColorInput } from "./product-color.nested.input"; import { ProductStatisticsInput } from "./product-statistics.nested.input"; -import { ProductVariantInput } from "./product-variant.nested.input"; @InputType() export class ProductInput { @@ -85,10 +85,10 @@ export class ProductInput { @ValidateNested() statistics?: ProductStatisticsInput; - @Field(() => [ProductVariantInput], { defaultValue: [] }) + @Field(() => [ProductColorInput], { defaultValue: [] }) @IsArray() - @Type(() => ProductVariantInput) - variants: ProductVariantInput[]; + @Type(() => ProductColorInput) + colors: ProductColorInput[]; @IsNullable() @Field(() => ID, { nullable: true, defaultValue: null }) diff --git a/demo/api/src/products/generated/product-category.resolver.ts b/demo/api/src/products/generated/product-category.resolver.ts index ba7be4ffa2..fa960ea697 100644 --- a/demo/api/src/products/generated/product-category.resolver.ts +++ b/demo/api/src/products/generated/product-category.resolver.ts @@ -1,7 +1,7 @@ // This file has been generated by comet api-generator. // You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. import { AffectedEntity, extractGraphqlFields, RequiredPermission, validateNotModified } from "@comet/cms-api"; -import { FindOptions, Reference } from "@mikro-orm/core"; +import { FindOptions } from "@mikro-orm/core"; import { InjectRepository } from "@mikro-orm/nestjs"; import { EntityManager, EntityRepository } from "@mikro-orm/postgresql"; import { Args, ID, Info, Mutation, Parent, Query, ResolveField, Resolver } from "@nestjs/graphql"; @@ -21,7 +21,6 @@ export class ProductCategoryResolver { private readonly entityManager: EntityManager, private readonly productCategoriesService: ProductCategoriesService, @InjectRepository(ProductCategory) private readonly repository: EntityRepository, - @InjectRepository(Product) private readonly productRepository: EntityRepository, ) {} @Query(() => ProductCategory) @@ -68,18 +67,10 @@ export class ProductCategoryResolver { @Mutation(() => ProductCategory) async createProductCategory(@Args("input", { type: () => ProductCategoryInput }) input: ProductCategoryInput): Promise { - const { products: productsInput, ...assignInput } = input; const productCategory = this.repository.create({ - ...assignInput, + ...input, }); - if (productsInput) { - const products = await this.productRepository.find({ id: productsInput }); - if (products.length != productsInput.length) throw new Error("Couldn't find all products that were passed as input"); - await productCategory.products.loadItems(); - productCategory.products.set(products.map((product) => Reference.create(product))); - } - await this.entityManager.flush(); return productCategory; @@ -97,18 +88,10 @@ export class ProductCategoryResolver { validateNotModified(productCategory, lastUpdatedAt); } - const { products: productsInput, ...assignInput } = input; productCategory.assign({ - ...assignInput, + ...input, }); - if (productsInput) { - const products = await this.productRepository.find({ id: productsInput }); - if (products.length != productsInput.length) throw new Error("Couldn't find all products that were passed as input"); - await productCategory.products.loadItems(); - productCategory.products.set(products.map((product) => Reference.create(product))); - } - await this.entityManager.flush(); return productCategory; diff --git a/demo/api/src/products/generated/product-color.resolver.ts b/demo/api/src/products/generated/product-color.resolver.ts new file mode 100644 index 0000000000..228625eac1 --- /dev/null +++ b/demo/api/src/products/generated/product-color.resolver.ts @@ -0,0 +1,17 @@ +// This file has been generated by comet api-generator. +// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. + +import { RequiredPermission } from "@comet/cms-api"; +import { Parent, ResolveField, Resolver } from "@nestjs/graphql"; + +import { Product } from "../entities/product.entity"; +import { ProductColor } from "../entities/product-color.entity"; + +@Resolver(() => ProductColor) +@RequiredPermission(["products"], { skipScopeCheck: true }) +export class ProductColorResolver { + @ResolveField(() => Product) + async product(@Parent() productColor: ProductColor): Promise { + return productColor.product.load(); + } +} diff --git a/demo/api/src/products/generated/product-variant.resolver.ts b/demo/api/src/products/generated/product-variant.resolver.ts index 9aeef7a4d7..0da9a3c353 100644 --- a/demo/api/src/products/generated/product-variant.resolver.ts +++ b/demo/api/src/products/generated/product-variant.resolver.ts @@ -1,15 +1,122 @@ // This file has been generated by comet api-generator. // You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. - -import { RequiredPermission } from "@comet/cms-api"; -import { Parent, ResolveField, Resolver } from "@nestjs/graphql"; +import { AffectedEntity, extractGraphqlFields, RequiredPermission, validateNotModified } from "@comet/cms-api"; +import { FindOptions, Reference } from "@mikro-orm/core"; +import { InjectRepository } from "@mikro-orm/nestjs"; +import { EntityManager, EntityRepository } from "@mikro-orm/postgresql"; +import { Args, ID, Info, Mutation, Parent, Query, ResolveField, Resolver } from "@nestjs/graphql"; +import { GraphQLResolveInfo } from "graphql"; import { Product } from "../entities/product.entity"; import { ProductVariant } from "../entities/product-variant.entity"; +import { PaginatedProductVariants } from "./dto/paginated-product-variants"; +import { ProductVariantInput, ProductVariantUpdateInput } from "./dto/product-variant.input"; +import { ProductVariantsArgs } from "./dto/product-variants.args"; +import { ProductVariantsService } from "./product-variants.service"; @Resolver(() => ProductVariant) -@RequiredPermission(["products"], { skipScopeCheck: true }) +@RequiredPermission("products", { skipScopeCheck: true }) export class ProductVariantResolver { + constructor( + private readonly entityManager: EntityManager, + private readonly productVariantsService: ProductVariantsService, + @InjectRepository(ProductVariant) private readonly repository: EntityRepository, + @InjectRepository(Product) private readonly productRepository: EntityRepository, + ) {} + + @Query(() => ProductVariant) + @AffectedEntity(ProductVariant) + async productVariant(@Args("id", { type: () => ID }) id: string): Promise { + const productVariant = await this.repository.findOneOrFail(id); + return productVariant; + } + + @Query(() => PaginatedProductVariants) + @AffectedEntity(Product, { idArg: "product" }) + async productVariants( + @Args() { product, search, filter, sort, offset, limit }: ProductVariantsArgs, + @Info() info: GraphQLResolveInfo, + ): Promise { + const where = this.productVariantsService.getFindCondition({ search, filter }); + + where.product = product; + + const fields = extractGraphqlFields(info, { root: "nodes" }); + const populate: string[] = []; + if (fields.includes("product")) { + populate.push("product"); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const options: FindOptions = { offset, limit, populate }; + + if (sort) { + options.orderBy = sort.map((sortItem) => { + return { + [sortItem.field]: sortItem.direction, + }; + }); + } + + const [entities, totalCount] = await this.repository.findAndCount(where, options); + return new PaginatedProductVariants(entities, totalCount); + } + + @Mutation(() => ProductVariant) + @AffectedEntity(Product, { idArg: "product" }) + async createProductVariant( + @Args("product", { type: () => ID }) product: string, + @Args("input", { type: () => ProductVariantInput }) input: ProductVariantInput, + ): Promise { + const { image: imageInput, ...assignInput } = input; + const productVariant = this.repository.create({ + ...assignInput, + + product: Reference.create(await this.productRepository.findOneOrFail(product)), + + image: imageInput.transformToBlockData(), + }); + + await this.entityManager.flush(); + + return productVariant; + } + + @Mutation(() => ProductVariant) + @AffectedEntity(ProductVariant) + async updateProductVariant( + @Args("id", { type: () => ID }) id: string, + @Args("input", { type: () => ProductVariantUpdateInput }) input: ProductVariantUpdateInput, + @Args("lastUpdatedAt", { type: () => Date, nullable: true }) lastUpdatedAt?: Date, + ): Promise { + const productVariant = await this.repository.findOneOrFail(id); + if (lastUpdatedAt) { + validateNotModified(productVariant, lastUpdatedAt); + } + + const { image: imageInput, ...assignInput } = input; + productVariant.assign({ + ...assignInput, + }); + + if (imageInput) { + productVariant.image = imageInput.transformToBlockData(); + } + + await this.entityManager.flush(); + + return productVariant; + } + + @Mutation(() => Boolean) + @AffectedEntity(ProductVariant) + async deleteProductVariant(@Args("id", { type: () => ID }) id: string): Promise { + const productVariant = await this.repository.findOneOrFail(id); + await this.entityManager.remove(productVariant); + await this.entityManager.flush(); + return true; + } + @ResolveField(() => Product) async product(@Parent() productVariant: ProductVariant): Promise { return productVariant.product.load(); diff --git a/demo/api/src/products/generated/product-variants.service.ts b/demo/api/src/products/generated/product-variants.service.ts new file mode 100644 index 0000000000..a1a65855ec --- /dev/null +++ b/demo/api/src/products/generated/product-variants.service.ts @@ -0,0 +1,34 @@ +// This file has been generated by comet api-generator. +// You may choose to use this file as scaffold by moving this file out of generated folder and removing this comment. +import { filtersToMikroOrmQuery, searchToMikroOrmQuery } from "@comet/cms-api"; +import { ObjectQuery } from "@mikro-orm/core"; +import { Injectable } from "@nestjs/common"; + +import { ProductVariant } from "../entities/product-variant.entity"; +import { ProductVariantFilter } from "./dto/product-variant.filter"; + +@Injectable() +export class ProductVariantsService { + getFindCondition(options: { search?: string; filter?: ProductVariantFilter }): ObjectQuery { + const andFilters = []; + + if (options.search) { + andFilters.push( + searchToMikroOrmQuery(options.search, [ + "name", + "product.title", + "product.status", + "product.slug", + "product.description", + "product.type", + ]), + ); + } + + if (options.filter) { + andFilters.push(filtersToMikroOrmQuery(options.filter)); + } + + return andFilters.length > 0 ? { $and: andFilters } : {}; + } +} diff --git a/demo/api/src/products/generated/product.resolver.ts b/demo/api/src/products/generated/product.resolver.ts index 823842efcc..2b11669788 100644 --- a/demo/api/src/products/generated/product.resolver.ts +++ b/demo/api/src/products/generated/product.resolver.ts @@ -10,6 +10,7 @@ import { GraphQLResolveInfo } from "graphql"; import { Manufacturer } from "../entities/manufacturer.entity"; import { Product } from "../entities/product.entity"; import { ProductCategory } from "../entities/product-category.entity"; +import { ProductColor } from "../entities/product-color.entity"; import { ProductStatistics } from "../entities/product-statistics.entity"; import { ProductTag } from "../entities/product-tag.entity"; import { ProductVariant } from "../entities/product-variant.entity"; @@ -28,7 +29,7 @@ export class ProductResolver { @InjectRepository(ProductCategory) private readonly productCategoryRepository: EntityRepository, @InjectRepository(Manufacturer) private readonly manufacturerRepository: EntityRepository, @InjectRepository(ProductStatistics) private readonly productStatisticsRepository: EntityRepository, - @InjectRepository(ProductVariant) private readonly productVariantRepository: EntityRepository, + @InjectRepository(ProductColor) private readonly productColorRepository: EntityRepository, @InjectRepository(ProductTag) private readonly productTagRepository: EntityRepository, ) {} @@ -58,6 +59,9 @@ export class ProductResolver { if (fields.includes("manufacturer")) { populate.push("manufacturer"); } + if (fields.includes("colors")) { + populate.push("colors"); + } if (fields.includes("variants")) { populate.push("variants"); } @@ -86,7 +90,7 @@ export class ProductResolver { @Mutation(() => Product) async createProduct(@Args("input", { type: () => ProductInput }) input: ProductInput): Promise { const { - variants: variantsInput, + colors: colorsInput, tags: tagsInput, category: categoryInput, manufacturer: manufacturerInput, @@ -101,14 +105,11 @@ export class ProductResolver { manufacturer: manufacturerInput ? Reference.create(await this.manufacturerRepository.findOneOrFail(manufacturerInput)) : undefined, image: imageInput.transformToBlockData(), }); - if (variantsInput) { - product.variants.set( - variantsInput.map((variantInput) => { - const { image: imageInput, ...assignInput } = variantInput; - return this.productVariantRepository.assign(new ProductVariant(), { - ...assignInput, - - image: imageInput.transformToBlockData(), + if (colorsInput) { + product.colors.set( + colorsInput.map((colorInput) => { + return this.productColorRepository.assign(new ProductColor(), { + ...colorInput, }); }), ); @@ -146,7 +147,7 @@ export class ProductResolver { } const { - variants: variantsInput, + colors: colorsInput, tags: tagsInput, category: categoryInput, manufacturer: manufacturerInput, @@ -157,14 +158,11 @@ export class ProductResolver { product.assign({ ...assignInput, }); - if (variantsInput) { - product.variants.set( - variantsInput.map((variantInput) => { - const { image: imageInput, ...assignInput } = variantInput; - return this.productVariantRepository.assign(new ProductVariant(), { - ...assignInput, - - image: imageInput.transformToBlockData(), + if (colorsInput) { + product.colors.set( + colorsInput.map((colorInput) => { + return this.productColorRepository.assign(new ProductColor(), { + ...colorInput, }); }), ); @@ -220,6 +218,11 @@ export class ProductResolver { return product.manufacturer?.load(); } + @ResolveField(() => [ProductColor]) + async colors(@Parent() product: Product): Promise { + return product.colors.loadItems(); + } + @ResolveField(() => [ProductVariant]) async variants(@Parent() product: Product): Promise { return product.variants.loadItems(); diff --git a/demo/api/src/products/products.module.ts b/demo/api/src/products/products.module.ts index e762f9c91a..e88d53d648 100644 --- a/demo/api/src/products/products.module.ts +++ b/demo/api/src/products/products.module.ts @@ -6,6 +6,7 @@ import { ManufacturersService } from "@src/products/generated/manufacturers.serv import { Product } from "./entities/product.entity"; import { ProductCategory } from "./entities/product-category.entity"; +import { ProductColor } from "./entities/product-color.entity"; import { ProductStatistics } from "./entities/product-statistics.entity"; import { ProductTag } from "./entities/product-tag.entity"; import { ProductVariant } from "./entities/product-variant.entity"; @@ -14,10 +15,12 @@ import { ProductCategoriesService } from "./generated/product-categories.service import { ProductCategoryResolver } from "./generated/product-category.resolver"; import { ProductTagResolver } from "./generated/product-tag.resolver"; import { ProductTagsService } from "./generated/product-tags.service"; +import { ProductVariantResolver } from "./generated/product-variant.resolver"; +import { ProductVariantsService } from "./generated/product-variants.service"; import { ProductsService } from "./generated/products.service"; @Module({ - imports: [MikroOrmModule.forFeature([Product, ProductCategory, ProductTag, ProductVariant, ProductStatistics, Manufacturer])], + imports: [MikroOrmModule.forFeature([Product, ProductCategory, ProductTag, ProductVariant, ProductStatistics, ProductColor, Manufacturer])], providers: [ ProductResolver, ProductsService, @@ -25,6 +28,8 @@ import { ProductsService } from "./generated/products.service"; ProductCategoriesService, ProductTagResolver, ProductTagsService, + ProductVariantsService, + ProductVariantResolver, ManufacturerResolver, ManufacturersService, ], diff --git a/packages/api/cms-api/src/generator/generate-crud-input.ts b/packages/api/cms-api/src/generator/generate-crud-input.ts index fbcdf43a2b..5c98564c8b 100644 --- a/packages/api/cms-api/src/generator/generate-crud-input.ts +++ b/packages/api/cms-api/src/generator/generate-crud-input.ts @@ -1,6 +1,7 @@ import { EntityMetadata } from "@mikro-orm/core"; import { hasFieldFeature } from "./crud-generator.decorator"; +import { buildOptions } from "./generate-crud"; import { buildNameVariants } from "./utils/build-name-variants"; import { integerTypes } from "./utils/constants"; import { generateImportsCode, Imports } from "./utils/generate-imports-code"; @@ -45,6 +46,8 @@ export async function generateCrudInput( ): Promise { const generatedFiles: GeneratedFile[] = []; + const { rootArgProps } = buildOptions(metadata); + const props = metadata.props .filter((prop) => { return !prop.embedded; @@ -52,6 +55,10 @@ export async function generateCrudInput( .filter((prop) => { return hasFieldFeature(metadata.class, prop.name, "input"); }) + .filter((prop) => { + //filter out props that are rootArgProps + return !rootArgProps.some((rootArgProps) => rootArgProps.name === prop.name); + }) .filter((prop) => !options.excludeFields.includes(prop.name)); let fieldsOut = ""; diff --git a/packages/api/cms-api/src/generator/generate-crud-relation-orpahremoval.spec.ts b/packages/api/cms-api/src/generator/generate-crud-relation-orpahremoval.spec.ts new file mode 100644 index 0000000000..3d79493c0b --- /dev/null +++ b/packages/api/cms-api/src/generator/generate-crud-relation-orpahremoval.spec.ts @@ -0,0 +1,114 @@ +import { BaseEntity, Collection, Entity, ManyToOne, OneToMany, PrimaryKey, Property, Ref } from "@mikro-orm/core"; +import { MikroORM } from "@mikro-orm/postgresql"; +import { LazyMetadataStorage } from "@nestjs/graphql/dist/schema-builder/storages/lazy-metadata.storage"; +import { v4 as uuid } from "uuid"; + +import { CrudGenerator } from "./crud-generator.decorator"; +import { generateCrud } from "./generate-crud"; +import { lintGeneratedFiles, parseSource } from "./utils/test-helper"; + +@Entity() +@CrudGenerator({ targetDirectory: __dirname }) +class TestEntityProductVariant extends BaseEntity { + @PrimaryKey({ columnType: "text", type: "string" }) + id: string; + + @Property() + name: string; + + @ManyToOne(() => TestEntityProduct, { ref: true }) + product: Ref; +} + +@Entity() +@CrudGenerator({ targetDirectory: __dirname }) +class TestEntityProduct extends BaseEntity { + @PrimaryKey({ type: "uuid" }) + id: string = uuid(); + + @Property() + name: string; + + @OneToMany(() => TestEntityProductVariant, (variants) => variants.product, { orphanRemoval: true }) + variants = new Collection(this); +} + +describe("GenerateCrudRelationsNonNullable", () => { + describe("resolver class", () => { + it("input type must not include product relation", async () => { + LazyMetadataStorage.load(); + const orm = await MikroORM.init({ + type: "postgresql", + dbName: "test-db", + entities: [TestEntityProduct, TestEntityProductVariant], + }); + + const out = await generateCrud({ targetDirectory: __dirname }, orm.em.getMetadata().get("TestEntityProductVariant")); + const lintedOut = await lintGeneratedFiles(out); + const file = lintedOut.find((file) => file.name === "dto/test-entity-product-variant.input.ts"); + if (!file) throw new Error("File not found"); + + const source = parseSource(file.content); + const classes = source.getClasses(); + expect(classes.length).toBe(2); + + const cls = classes[0]; + const structure = cls.getStructure(); + expect(structure.properties?.length).toBe(1); + expect(structure.properties?.[0].name).toBe("name"); + + orm.close(); + }); + it("query list must include product arg", async () => { + LazyMetadataStorage.load(); + const orm = await MikroORM.init({ + type: "postgresql", + dbName: "test-db", + entities: [TestEntityProduct, TestEntityProductVariant], + }); + + const out = await generateCrud({ targetDirectory: __dirname }, orm.em.getMetadata().get("TestEntityProductVariant")); + const lintedOut = await lintGeneratedFiles(out); + const file = lintedOut.find((file) => file.name === "dto/test-entity-product-variants.args.ts"); + if (!file) throw new Error("File not found"); + + const source = parseSource(file.content); + + const classes = source.getClasses(); + expect(classes.length).toBe(1); + const cls = classes[0]; + + const structure = cls.getStructure(); + const params = structure?.properties?.map((prop) => prop.name); + expect(params).toContain("product"); + + orm.close(); + }); + it("create mutation must include product arg", async () => { + LazyMetadataStorage.load(); + const orm = await MikroORM.init({ + type: "postgresql", + dbName: "test-db", + entities: [TestEntityProduct, TestEntityProductVariant], + }); + + const out = await generateCrud({ targetDirectory: __dirname }, orm.em.getMetadata().get("TestEntityProductVariant")); + const lintedOut = await lintGeneratedFiles(out); + const file = lintedOut.find((file) => file.name === "test-entity-product-variant.resolver.ts"); + if (!file) throw new Error("File not found"); + + const source = parseSource(file.content); + + const classes = source.getClasses(); + expect(classes.length).toBe(1); + const cls = classes[0]; + + const structure = cls.getStructure(); + const createMethod = structure.methods?.find((method) => method.name === "createTestEntityProductVariant"); + const params = createMethod?.parameters?.map((param) => param.name); + expect(params).toContain("product"); + + orm.close(); + }); + }); +}); diff --git a/packages/api/cms-api/src/generator/generate-crud.ts b/packages/api/cms-api/src/generator/generate-crud.ts index 5189eef824..1fc0b63a11 100644 --- a/packages/api/cms-api/src/generator/generate-crud.ts +++ b/packages/api/cms-api/src/generator/generate-crud.ts @@ -11,10 +11,26 @@ import { generateImportsCode, Imports } from "./utils/generate-imports-code"; import { findEnumImportPath, findEnumName, morphTsProperty } from "./utils/ts-morph-helper"; import { GeneratedFile } from "./utils/write-generated-files"; +// TODO move into own file // eslint-disable-next-line @typescript-eslint/explicit-function-return-type -function buildOptions(metadata: EntityMetadata) { +export function buildOptions(metadata: EntityMetadata) { const { classNameSingular, classNamePlural, fileNameSingular, fileNamePlural } = buildNameVariants(metadata); + const rootArgProps = metadata.props.filter((prop) => { + if (prop.reference == "m:1") { + if (!prop.targetMeta) throw new Error("targetMeta is not set for relation"); + for (const innerProp of prop.targetMeta.props) { + if (innerProp.reference == "1:m" && innerProp.targetMeta == metadata && innerProp.mappedBy == prop.name) { + const hasOwnCrudGenerator = Reflect.getMetadata(`data:crudGeneratorOptions`, prop.targetMeta.class); + if (hasOwnCrudGenerator && innerProp.orphanRemoval) { + //if the back relation has its own crud generator and has orphan removal, it's a root arg + return true; + } + } + } + } + }); + const crudSearchPropNames = metadata.props .filter((prop) => prop.name != "status") .filter((prop) => hasFieldFeature(metadata.class, prop.name, "search") && !prop.name.startsWith("scope_")) @@ -78,7 +94,8 @@ function buildOptions(metadata: EntityMetadata) { prop.type === "boolean" || prop.type === "DateType" || prop.type === "Date" || - prop.reference === "m:1"), + prop.reference === "m:1") && + !rootArgProps.some((rootArgProp) => rootArgProp.name == prop.name), ); const hasFilterArg = crudFilterProps.length > 0; const crudSortProps = metadata.props.filter( @@ -126,6 +143,7 @@ function buildOptions(metadata: EntityMetadata) { argsClassName, argsFileName, blockProps, + rootArgProps, }; } @@ -281,7 +299,7 @@ function generatePaginatedDto({ generatorOptions, metadata }: { generatorOptions function generateArgsDto({ generatorOptions, metadata }: { generatorOptions: CrudGeneratorOptions; metadata: EntityMetadata }): string { const { classNameSingular, fileNameSingular } = buildNameVariants(metadata); - const { scopeProp, argsClassName, hasSearchArg, hasSortArg, hasFilterArg, statusProp, statusActiveItems, hasStatusFilter } = + const { scopeProp, argsClassName, hasSearchArg, hasSortArg, hasFilterArg, statusProp, statusActiveItems, hasStatusFilter, rootArgProps } = buildOptions(metadata); const imports: Imports = []; if (scopeProp && scopeProp.targetMeta) { @@ -307,9 +325,9 @@ function generateArgsDto({ generatorOptions, metadata }: { generatorOptions: Cru } } - const argsOut = `import { ArgsType, Field, IntersectionType, registerEnumType } from "@nestjs/graphql"; + const argsOut = `import { ArgsType, Field, IntersectionType, registerEnumType, ID } from "@nestjs/graphql"; import { Type } from "class-transformer"; - import { IsOptional, IsString, ValidateNested, IsEnum } from "class-validator"; + import { IsOptional, IsString, ValidateNested, IsEnum, IsUUID } from "class-validator"; import { OffsetBasedPaginationArgs } from "@comet/cms-api"; import { ${classNameSingular}Filter } from "./${fileNameSingular}.filter"; import { ${classNameSingular}Sort } from "./${fileNameSingular}.sort"; @@ -340,6 +358,21 @@ function generateArgsDto({ generatorOptions, metadata }: { generatorOptions: Cru : "" } + ${rootArgProps + .map((rootArgProp) => { + if (integerTypes.includes(rootArgProp.type)) { + return `@Field(() => ID) + @Transform(({ value }) => value.map((id: string) => parseInt(id))) + @IsInt() + ${rootArgProp.name}: number;`; + } else { + return `@Field(() => ID) + @IsUUID() + ${rootArgProp.name}: string;`; + } + }) + .join("")} + ${ hasStatusFilter ? ` @@ -452,7 +485,7 @@ function generateInputHandling( metadata: EntityMetadata, ): string { const { instanceNameSingular } = buildNameVariants(metadata); - const { blockProps, scopeProp } = buildOptions(metadata); + const { blockProps, scopeProp, rootArgProps } = buildOptions(metadata); const props = metadata.props.filter((prop) => !options.excludeFields || !options.excludeFields.includes(prop.name)); @@ -463,6 +496,10 @@ function generateInputHandling( const inputRelationManyToOneProps = relationManyToOneProps .filter((prop) => hasFieldFeature(metadata.class, prop.name, "input")) + .filter((prop) => { + //filter out props that are rootArgProps + return !rootArgProps.some((rootArgProps) => rootArgProps.name === prop.name); + }) .map((prop) => { return { name: prop.name, @@ -513,6 +550,17 @@ function generateInputHandling( ${options.assignEntityCode} ...${noAssignProps.length ? `assignInput` : options.inputName}, ${options.mode == "create" && scopeProp ? `scope,` : ""} + ${ + options.mode == "create" + ? rootArgProps + .map((rootArgProp) => { + return `${rootArgProp.name}: Reference.create(await this.${classNameToInstanceName( + rootArgProp.type, + )}Repository.findOneOrFail(${rootArgProp.name})), `; + }) + .join("") + : "" + } ${ options.mode == "create" || options.mode == "updateNested" ? inputRelationManyToOneProps @@ -750,6 +798,7 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr statusActiveItems, hasStatusFilter, hasUpdatedAt, + rootArgProps, } = buildOptions(metadata); const relationManyToOneProps = metadata.props.filter((prop) => prop.reference === "m:1"); @@ -770,6 +819,9 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr .forEach((prop) => { injectRepositories.add(prop.type); }); + rootArgProps.forEach((prop) => { + injectRepositories.add(prop.type); + }); const { imports: relationsFieldResolverImports, @@ -795,6 +847,14 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr }); } + function generateIdArg(name: string, metadata: EntityMetadata): string { + if (integerTypes.includes(metadata.properties[name].type)) { + return `@Args("${name}", { type: () => ID }, { transform: (value) => parseInt(value) }) ${name}: number`; + } else { + return `@Args("${name}", { type: () => ID }) ${name}: string`; + } + } + const resolverOut = `import { InjectRepository } from "@mikro-orm/nestjs"; import { EntityRepository, EntityManager } from "@mikro-orm/postgresql"; import { FindOptions, Reference } from "@mikro-orm/core"; @@ -822,11 +882,7 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr @Query(() => ${metadata.className}) @AffectedEntity(${metadata.className}) - async ${instanceNameSingular}(${ - integerTypes.includes(metadata.properties.id.type) - ? `@Args("id", { type: () => ID }, { transform: (value) => parseInt(value) }) id: number` - : `@Args("id", { type: () => ID }) id: string` - }): Promise<${metadata.className}> { + async ${instanceNameSingular}(${generateIdArg("id", metadata)}): Promise<${metadata.className}> { const ${instanceNameSingular} = await this.repository.findOneOrFail(id); return ${instanceNameSingular}; } @@ -848,9 +904,18 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr } @Query(() => Paginated${classNamePlural}) + ${rootArgProps + .map((rootArgProp) => { + return `@AffectedEntity(${rootArgProp.targetMeta?.className}, { idArg: "${rootArgProp.name}" })`; + }) + .join("")} async ${instanceNameSingular != instanceNamePlural ? instanceNamePlural : `${instanceNamePlural}List`}( @Args() {${Object.entries({ scope: !!scopeProp, + ...rootArgProps.reduce((acc, rootArgProp) => { + acc[rootArgProp.name] = true; + return acc; + }, {} as Record), status: !!hasStatusFilter, search: !!hasSearchArg, filter: !!hasFilterArg, @@ -879,6 +944,11 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr } ${hasStatusFilter && statusActiveItems && statusActiveItems.length <= 1 ? `where.status = status;` : ""} ${scopeProp ? `where.scope = scope;` : ""} + ${rootArgProps + .map((rootArgProp) => { + return `where.${rootArgProp.name} = ${rootArgProp.name};`; + }) + .join("\n")} ${ hasOutputRelations @@ -922,9 +992,17 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr ? ` @Mutation(() => ${metadata.className}) + ${rootArgProps + .map((rootArgProp) => { + return `@AffectedEntity(${rootArgProp.targetMeta?.className}, { idArg: "${rootArgProp.name}" })`; + }) + .join("")} async create${classNameSingular}( - ${scopeProp ? `@Args("scope", { type: () => ${scopeProp.type} }) scope: ${scopeProp.type},` : ""} - @Args("input", { type: () => ${classNameSingular}Input }) input: ${classNameSingular}Input + ${scopeProp ? `@Args("scope", { type: () => ${scopeProp.type} }) scope: ${scopeProp.type},` : ""}${rootArgProps + .map((rootArgProp) => { + return `${generateIdArg(rootArgProp.name, metadata)}, `; + }) + .join("")}@Args("input", { type: () => ${classNameSingular}Input }) input: ${classNameSingular}Input ): Promise<${metadata.className}> { ${generateInputHandling( @@ -946,11 +1024,7 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr @Mutation(() => ${metadata.className}) @AffectedEntity(${metadata.className}) async update${classNameSingular}( - ${ - integerTypes.includes(metadata.properties.id.type) - ? `@Args("id", { type: () => ID }, { transform: (value) => parseInt(value) }) id: number,` - : `@Args("id", { type: () => ID }) id: string,` - } + ${generateIdArg("id", metadata)}, @Args("input", { type: () => ${classNameSingular}UpdateInput }) input: ${classNameSingular}UpdateInput, ${hasUpdatedAt ? `@Args("lastUpdatedAt", { type: () => Date, nullable: true }) lastUpdatedAt?: Date,` : ""} ): Promise<${metadata.className}> { @@ -977,11 +1051,7 @@ function generateResolver({ generatorOptions, metadata }: { generatorOptions: Cr ? ` @Mutation(() => Boolean) @AffectedEntity(${metadata.className}) - async delete${metadata.className}(${ - integerTypes.includes(metadata.properties.id.type) - ? `@Args("id", { type: () => ID }, { transform: (value) => parseInt(value) }) id: number` - : `@Args("id", { type: () => ID }) id: string` - }): Promise { + async delete${metadata.className}(${generateIdArg("id", metadata)}): Promise { const ${instanceNameSingular} = await this.repository.findOneOrFail(id); await this.entityManager.remove(${instanceNameSingular}); await this.entityManager.flush(); @@ -1050,7 +1120,16 @@ export async function generateCrud(generatorOptionsParam: CrudGeneratorOptions, }); metadata.props - .filter((prop) => prop.reference === "1:m" && prop.orphanRemoval) + .filter((prop) => { + if (prop.reference === "1:m" && prop.orphanRemoval) { + if (!prop.targetMeta) throw new Error(`Target metadata not set`); + const hasOwnCrudGenerator = Reflect.getMetadata(`data:crudGeneratorOptions`, prop.targetMeta.class); + if (!hasOwnCrudGenerator) { + //generate nested resolver only if target entity has no own crud generator + return true; + } + } + }) .forEach((prop) => { if (!prop.targetMeta) throw new Error(`Target metadata not set`); const { fileNameSingular } = buildNameVariants(prop.targetMeta);