From f26c65fd41b75c8291ada3c0152f7298c9922dfe Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Thu, 17 Oct 2024 11:50:43 +0200 Subject: [PATCH 01/67] Try out depot as CI provider --- .github/workflows/ci-front.yaml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml index e90e7012fece..52ca3c825f55 100644 --- a/.github/workflows/ci-front.yaml +++ b/.github/workflows/ci-front.yaml @@ -43,7 +43,8 @@ jobs: - name: Front / Build storybook run: npx nx storybook:build twenty-front front-sb-test: - runs-on: shipfox-8vcpu-ubuntu-2204 + runs-on: depot-ubuntu-22.04-8 + timeout-minutes: 30 needs: front-sb-build strategy: matrix: @@ -68,7 +69,8 @@ jobs: - name: Run storybook tests run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }} front-sb-test-performance: - runs-on: shipfox-8vcpu-ubuntu-2204 + runs-on: depot-ubuntu-22.04-8 + timeout-minutes: 30 env: REACT_APP_SERVER_BASE_URL: http://localhost:3000 NX_REJECT_UNKNOWN_LOCAL_CACHE: 0 From ddbfabfc99343e4f7939e7d6e399171c32342331 Mon Sep 17 00:00:00 2001 From: martmull Date: Thu, 17 Oct 2024 14:41:38 +0200 Subject: [PATCH 02/67] Precise wording for api example (#7783) Enhance composite type filter example in open-api --- .../open-api/utils/__tests__/parameters.utils.spec.ts | 4 ++-- .../engine/core-modules/open-api/utils/parameters.utils.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts index bab186af5231..6d5ab0751d5a 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/parameters.utils.spec.ts @@ -84,7 +84,7 @@ describe('computeParameters', () => { in: 'query', description: `Filters objects returned. Should have the following shape: **field_1[COMPARATOR]:value_1,field_2[COMPARATOR]:value_2... - To filter on nested objects use **field.nestedField[COMPARATOR]:value_1 + To filter on composite type fields use **field.subField[COMPARATOR]:value_1 ** Available comparators are **${Object.values(FilterComparators).join( '**, **', @@ -106,7 +106,7 @@ describe('computeParameters', () => { }, simpleNested: { value: 'emails.primaryEmail[eq]:foo99@example.com', - description: 'A simple nested filter param', + description: 'A simple composite type filter param', }, complex: { value: diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts index f16c6fe436b9..26679a18fc34 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/parameters.utils.ts @@ -74,7 +74,7 @@ export const computeFilterParameters = (): OpenAPIV3_1.ParameterObject => { in: 'query', description: `Filters objects returned. Should have the following shape: **field_1[COMPARATOR]:value_1,field_2[COMPARATOR]:value_2... - To filter on nested objects use **field.nestedField[COMPARATOR]:value_1 + To filter on composite type fields use **field.subField[COMPARATOR]:value_1 ** Available comparators are **${Object.values(FilterComparators).join( '**, **', @@ -97,7 +97,7 @@ export const computeFilterParameters = (): OpenAPIV3_1.ParameterObject => { }, simpleNested: { value: 'emails.primaryEmail[eq]:foo99@example.com', - description: 'A simple nested filter param', + description: 'A simple composite type filter param', }, complex: { value: From f338d01b4f525f07e9e98f82dab44e2649d71bc9 Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Thu, 17 Oct 2024 15:08:42 +0200 Subject: [PATCH 03/67] Build code introspection service (#7760) Starting to use ts-morph to retrieve function parameters --- packages/twenty-server/package.json | 1 + .../serverless-function.service.ts | 14 +-- .../code-introspection.service.spec.ts | 106 ++++++++++++++++++ .../code-introspection.exception.ts | 12 ++ .../code-introspection.module.ts | 9 ++ .../code-introspection.service.ts | 92 +++++++++++++++ yarn.lock | 51 +++++++++ 7 files changed, 278 insertions(+), 7 deletions(-) create mode 100644 packages/twenty-server/src/modules/code-introspection/__tests__/code-introspection.service.spec.ts create mode 100644 packages/twenty-server/src/modules/code-introspection/code-introspection.exception.ts create mode 100644 packages/twenty-server/src/modules/code-introspection/code-introspection.module.ts create mode 100644 packages/twenty-server/src/modules/code-introspection/code-introspection.service.ts diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index 7cedf04348bf..c5778aa6493d 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -44,6 +44,7 @@ "monaco-editor-auto-typings": "^0.4.5", "passport": "^0.7.0", "psl": "^1.9.0", + "ts-morph": "^24.0.0", "tsconfig-paths": "^4.2.0", "typeorm": "patch:typeorm@0.3.20#./patches/typeorm+0.3.20.patch", "unzipper": "^0.12.3", diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts index 191dc9edf414..7e0fcbf9deec 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.service.ts @@ -4,19 +4,24 @@ import { InjectRepository } from '@nestjs/typeorm'; import { basename, dirname, join } from 'path'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; -import { Repository } from 'typeorm'; import deepEqual from 'deep-equal'; +import { Repository } from 'typeorm'; import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception'; import { ServerlessExecuteResult } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface'; -import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { readFileContent } from 'src/engine/core-modules/file-storage/utils/read-file-content'; +import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name'; import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name'; +import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version'; +import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files'; +import { getLastLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-layer-dependencies'; import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service'; import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.service'; +import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input'; import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input'; import { ServerlessFunctionEntity, @@ -27,11 +32,6 @@ import { ServerlessFunctionExceptionCode, } from 'src/engine/metadata-modules/serverless-function/serverless-function.exception'; import { isDefined } from 'src/utils/is-defined'; -import { getLastLayerDependencies } from 'src/engine/core-modules/serverless/drivers/utils/get-last-layer-dependencies'; -import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version'; -import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input'; -import { getBaseTypescriptProjectFiles } from 'src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files'; -import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name'; @Injectable() export class ServerlessFunctionService extends TypeOrmQueryService { diff --git a/packages/twenty-server/src/modules/code-introspection/__tests__/code-introspection.service.spec.ts b/packages/twenty-server/src/modules/code-introspection/__tests__/code-introspection.service.spec.ts new file mode 100644 index 000000000000..8829699154b5 --- /dev/null +++ b/packages/twenty-server/src/modules/code-introspection/__tests__/code-introspection.service.spec.ts @@ -0,0 +1,106 @@ +import { Test, TestingModule } from '@nestjs/testing'; + +import { CodeIntrospectionException } from 'src/modules/code-introspection/code-introspection.exception'; +import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service'; + +describe('CodeIntrospectionService', () => { + let service: CodeIntrospectionService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [CodeIntrospectionService], + }).compile(); + + service = module.get(CodeIntrospectionService); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('analyze', () => { + it('should analyze a function declaration correctly', () => { + const fileContent = ` + function testFunction(param1: string, param2: number): void { + console.log(param1, param2); + } + `; + + const result = service.analyze(fileContent); + + expect(result).toEqual([ + { name: 'param1', type: 'string' }, + { name: 'param2', type: 'number' }, + ]); + }); + + it('should analyze an arrow function correctly', () => { + const fileContent = ` + const testArrowFunction = (param1: string, param2: number): void => { + console.log(param1, param2); + }; + `; + + const result = service.analyze(fileContent); + + expect(result).toEqual([ + { name: 'param1', type: 'string' }, + { name: 'param2', type: 'number' }, + ]); + }); + + it('should return an empty array for files without functions', () => { + const fileContent = ` + const x = 5; + console.log(x); + `; + + const result = service.analyze(fileContent); + + expect(result).toEqual([]); + }); + + it('should throw an exception for multiple function declarations', () => { + const fileContent = ` + function func1(param1: string) {} + function func2(param2: number) {} + `; + + expect(() => service.analyze(fileContent)).toThrow( + CodeIntrospectionException, + ); + expect(() => service.analyze(fileContent)).toThrow( + 'Only one function is allowed', + ); + }); + + it('should throw an exception for multiple arrow functions', () => { + const fileContent = ` + const func1 = (param1: string) => {}; + const func2 = (param2: number) => {}; + `; + + expect(() => service.analyze(fileContent)).toThrow( + CodeIntrospectionException, + ); + expect(() => service.analyze(fileContent)).toThrow( + 'Only one arrow function is allowed', + ); + }); + + it('should correctly analyze complex types', () => { + const fileContent = ` + function complexFunction(param1: string[], param2: { key: number }): Promise { + return Promise.resolve(true); + } + `; + + const result = service.analyze(fileContent); + + expect(result).toEqual([ + { name: 'param1', type: 'string[]' }, + { name: 'param2', type: '{ key: number; }' }, + ]); + }); + }); +}); diff --git a/packages/twenty-server/src/modules/code-introspection/code-introspection.exception.ts b/packages/twenty-server/src/modules/code-introspection/code-introspection.exception.ts new file mode 100644 index 000000000000..22ebbd7bf300 --- /dev/null +++ b/packages/twenty-server/src/modules/code-introspection/code-introspection.exception.ts @@ -0,0 +1,12 @@ +import { CustomException } from 'src/utils/custom-exception'; + +export class CodeIntrospectionException extends CustomException { + code: CodeIntrospectionExceptionCode; + constructor(message: string, code: CodeIntrospectionExceptionCode) { + super(message, code); + } +} + +export enum CodeIntrospectionExceptionCode { + ONLY_ONE_FUNCTION_ALLOWED = 'ONLY_ONE_FUNCTION_ALLOWED', +} diff --git a/packages/twenty-server/src/modules/code-introspection/code-introspection.module.ts b/packages/twenty-server/src/modules/code-introspection/code-introspection.module.ts new file mode 100644 index 000000000000..d12a94ecf4ee --- /dev/null +++ b/packages/twenty-server/src/modules/code-introspection/code-introspection.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; + +import { CodeIntrospectionService } from 'src/modules/code-introspection/code-introspection.service'; + +@Module({ + providers: [CodeIntrospectionService], + exports: [CodeIntrospectionService], +}) +export class CodeIntrospectionModule {} diff --git a/packages/twenty-server/src/modules/code-introspection/code-introspection.service.ts b/packages/twenty-server/src/modules/code-introspection/code-introspection.service.ts new file mode 100644 index 000000000000..31e18b25fe09 --- /dev/null +++ b/packages/twenty-server/src/modules/code-introspection/code-introspection.service.ts @@ -0,0 +1,92 @@ +import { Injectable } from '@nestjs/common'; + +import { + ArrowFunction, + FunctionDeclaration, + ParameterDeclaration, + Project, + SyntaxKind, +} from 'ts-morph'; + +import { + CodeIntrospectionException, + CodeIntrospectionExceptionCode, +} from 'src/modules/code-introspection/code-introspection.exception'; + +type FunctionParameter = { + name: string; + type: string; +}; + +@Injectable() +export class CodeIntrospectionService { + private project: Project; + + constructor() { + this.project = new Project(); + } + + public analyze( + fileContent: string, + fileName = 'temp.ts', + ): FunctionParameter[] { + const sourceFile = this.project.createSourceFile(fileName, fileContent, { + overwrite: true, + }); + + const functionDeclarations = sourceFile.getFunctions(); + + if (functionDeclarations.length > 0) { + return this.analyzeFunctions(functionDeclarations); + } + + const arrowFunctions = sourceFile.getDescendantsOfKind( + SyntaxKind.ArrowFunction, + ); + + if (arrowFunctions.length > 0) { + return this.analyzeArrowFunctions(arrowFunctions); + } + + return []; + } + + private analyzeFunctions( + functionDeclarations: FunctionDeclaration[], + ): FunctionParameter[] { + if (functionDeclarations.length > 1) { + throw new CodeIntrospectionException( + 'Only one function is allowed', + CodeIntrospectionExceptionCode.ONLY_ONE_FUNCTION_ALLOWED, + ); + } + + const functionDeclaration = functionDeclarations[0]; + + return functionDeclaration.getParameters().map(this.buildFunctionParameter); + } + + private analyzeArrowFunctions( + arrowFunctions: ArrowFunction[], + ): FunctionParameter[] { + if (arrowFunctions.length > 1) { + throw new CodeIntrospectionException( + 'Only one arrow function is allowed', + CodeIntrospectionExceptionCode.ONLY_ONE_FUNCTION_ALLOWED, + ); + } + + const arrowFunction = arrowFunctions[0]; + + return arrowFunction.getParameters().map(this.buildFunctionParameter); + } + + private buildFunctionParameter( + parameter: ParameterDeclaration, + ): FunctionParameter { + return { + name: parameter.getName(), + type: parameter.getType().getText(), + }; + } +} diff --git a/yarn.lock b/yarn.lock index d3400e4a26b3..9d8eabefb6c3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15073,6 +15073,17 @@ __metadata: languageName: node linkType: hard +"@ts-morph/common@npm:~0.25.0": + version: 0.25.0 + resolution: "@ts-morph/common@npm:0.25.0" + dependencies: + minimatch: "npm:^9.0.4" + path-browserify: "npm:^1.0.1" + tinyglobby: "npm:^0.2.9" + checksum: 10c0/c67e66db678e44886e9823e6482834acebfae0ea52ccbfa2af1ca9abfe5a9774dad6e852c8f480909bc196175f17e15454af71d7a41a1c137db09e74f046a830 + languageName: node + linkType: hard + "@tsconfig/node10@npm:^1.0.7": version: 1.0.11 resolution: "@tsconfig/node10@npm:1.0.11" @@ -22021,6 +22032,13 @@ __metadata: languageName: node linkType: hard +"code-block-writer@npm:^13.0.3": + version: 13.0.3 + resolution: "code-block-writer@npm:13.0.3" + checksum: 10c0/87db97b37583f71cfd7eced8bf3f0a0a0ca53af912751a734372b36c08cd27f3e8a4878ec05591c0cd9ae11bea8add1423e132d660edd86aab952656dd41fd66 + languageName: node + linkType: hard + "code-point-at@npm:^1.0.0": version: 1.1.0 resolution: "code-point-at@npm:1.1.0" @@ -26491,6 +26509,18 @@ __metadata: languageName: node linkType: hard +"fdir@npm:^6.4.0": + version: 6.4.0 + resolution: "fdir@npm:6.4.0" + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + checksum: 10c0/9a03efa1335d78ea386b701799b08ad9e7e8da85d88567dc162cd28dd8e9486e8c269b3e95bfeb21dd6a5b14ebf69d230eb6e18f49d33fbda3cd97432f648c48 + languageName: node + linkType: hard + "fetch-retry@npm:^5.0.2": version: 5.0.6 resolution: "fetch-retry@npm:5.0.6" @@ -43048,6 +43078,16 @@ __metadata: languageName: node linkType: hard +"tinyglobby@npm:^0.2.9": + version: 0.2.9 + resolution: "tinyglobby@npm:0.2.9" + dependencies: + fdir: "npm:^6.4.0" + picomatch: "npm:^4.0.2" + checksum: 10c0/f65f847afe70f56de069d4f1f9c3b0c1a76aaf2b0297656754734a83b9bac8e105b5534dfbea8599560476b88f7b747d0855370a957a07246d18b976addb87ec + languageName: node + linkType: hard + "tinypool@npm:^0.8.2": version: 0.8.4 resolution: "tinypool@npm:0.8.4" @@ -43474,6 +43514,16 @@ __metadata: languageName: node linkType: hard +"ts-morph@npm:^24.0.0": + version: 24.0.0 + resolution: "ts-morph@npm:24.0.0" + dependencies: + "@ts-morph/common": "npm:~0.25.0" + code-block-writer: "npm:^13.0.3" + checksum: 10c0/2a0813ba428a154966d4038901f6c32457a60870936b23778f2629433257f87d1881fc4ecae7b791a223a88c2edf96aaac9fb0f88bf34d3c652af8c09c4f43bc + languageName: node + linkType: hard + "ts-node@npm:10.9.1": version: 10.9.1 resolution: "ts-node@npm:10.9.1" @@ -43817,6 +43867,7 @@ __metadata: passport: "npm:^0.7.0" psl: "npm:^1.9.0" rimraf: "npm:^5.0.5" + ts-morph: "npm:^24.0.0" tsconfig-paths: "npm:^4.2.0" typeorm: "patch:typeorm@0.3.20#./patches/typeorm+0.3.20.patch" typescript: "npm:5.3.3" From c07650fd7e997aa7d9cabf76a00f757804af8cf2 Mon Sep 17 00:00:00 2001 From: Nazar Poshtarenko <32395926+unrenamed@users.noreply.github.com> Date: Thu, 17 Oct 2024 17:11:02 +0300 Subject: [PATCH 04/67] fix(front): move "Add to favorites" btn to start of action menu (#7785) ### What does this PR do? Moves the "Add to favourites" action button to the beginning of the action menu, thus moving the "Delete" button to its right edge. Fixes #7780. image --- .../components/SingleRecordActionMenuEntriesSetter.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx index feeba5aabc61..4b61fa58eadb 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx @@ -4,9 +4,9 @@ import { ManageFavoritesActionEffect } from '@/action-menu/actions/record-action export const SingleRecordActionMenuEntriesSetter = () => { const actionEffects = [ + ManageFavoritesActionEffect, ExportRecordsActionEffect, DeleteRecordsActionEffect, - ManageFavoritesActionEffect, ]; return ( <> From d827d80ddcb65675bedd0fc623b6d28eddd4c0ed Mon Sep 17 00:00:00 2001 From: Shlok Koirala <85124057+shlok-py@users.noreply.github.com> Date: Thu, 17 Oct 2024 21:30:55 +0545 Subject: [PATCH 05/67] =?UTF-8?q?[=F0=9F=95=B9=EF=B8=8F]=20Twenty=20Design?= =?UTF-8?q?=20Challenges:=20New=20twenty=20logo=20by=20Shlok-py=20(#7790)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit added shlok-py nmew logo for twenty --- oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md b/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md index fca791fc0491..d67c49b64154 100644 --- a/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md +++ b/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md @@ -26,4 +26,7 @@ Your turn 👇 » 16-October-2024 by [harshsbhat](https://oss.gg/harshsbhat) Logo Link: [logo](https://drive.google.com/file/d/1jmqwNvlSyWSY1-pCG63TAtDvCoVa8xg-/view?usp=sharing) » tweet Link: [tweet](https://x.com/HarshBhatX/status/1846234658712772977) +» 17-October-2024 by [shlok-py](https://oss.gg/shlok-py) Logo Link: [logo](https://drive.google.com/file/d/1BakHRLJul6DcNbLyeOXgJO9Ap4DpUxO9/view?usp=sharing) » tweet Link: [tweet](https://x.com/koirala_shlok/status/1846910669658247201) + + --- From f08b8fda16b0005af6718756ca42b86b2011d010 Mon Sep 17 00:00:00 2001 From: Atharva_404 <72994819+Atharva-3000@users.noreply.github.com> Date: Thu, 17 Oct 2024 21:16:46 +0530 Subject: [PATCH 06/67] Updated 1-design-promotional-poster-20-share.md with (#7791) Added my own entry to the list with the following poster: ### Points: 300
![twenty](https://github.com/user-attachments/assets/bd7648a5-8012-4d73-a992-b8e7e8ed08a0) --- .../1-design-promotional-poster-20-share.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md b/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md index 78edaedd3d36..9f1f55ae767e 100644 --- a/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md +++ b/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md @@ -25,4 +25,7 @@ Your turn 👇 » 14-October-2024 by [AliYar-Khan](https://oss.gg/AliYar-Khan) poster Link: [poster](https://x.com/Mr_Programmer14/status/1845888855183884352) » 16-October-2024 by [Harsh BHat](https://oss.gg/harshsbhat) poster Link: [poster](https://x.com/HarshBhatX/status/1846233330435477531) + +» 17-October-2024 by [Atharva Deshmukh](https://oss.gg/Atharva-3000) poster Link: [poster](https://x.com/0x_atharva/status/1846915861191577697) + --- From 58fd34071c90c2f2a0c788e7add33611bd85b465 Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Thu, 17 Oct 2024 19:16:19 +0200 Subject: [PATCH 07/67] [Server Integration tests] Enrich integration GraphQL API tests (#7699) ### Description - We are using gql instead of strings to be able to see the graphql code highlighted ### Demo ![](https://assets-service.gitstart.com/28455/d06016b9-c62c-4e0d-bb16-3d7dd42c5b6b.png) Fixes #7526 --------- Co-authored-by: gitstart-twenty Co-authored-by: Charles Bochet Co-authored-by: Charles Bochet --- packages/twenty-server/.env.test | 2 +- packages/twenty-server/felix | 1 - .../twenty-server/jest-integration.config.ts | 9 +- packages/twenty-server/project.json | 2 +- .../test/company.integration-spec.ts | 48 -- .../integration/graphql/codegen}/index.ts | 2 +- .../graphql/codegen}/introspection-query.ts | 0 .../codegen}/introspection.interface.ts | 0 .../suites/all-resolvers.integration-spec.ts | 430 ++++++++++++++++++ .../graphql/suites}/auth.integration-spec.ts | 0 .../activities.integration-spec.ts | 2 +- .../activity-targets.integration-spec.ts | 4 +- .../api-keys.integration-spec.ts | 2 +- .../attachments.integration-spec.ts | 4 +- .../audit-logs.integration-spec.ts | 2 +- .../blocklists.integration-spec.ts | 2 +- ...nel-event-associations.integration-spec.ts | 6 +- .../calendar-channels.integration-spec.ts | 4 +- ...dar-event-participants.integration-spec.ts | 2 +- .../comments.integration-spec.ts | 2 +- .../companies.integration-spec.ts | 4 +- .../connected-accounts.integration-spec.ts | 4 +- .../favorites.integration-spec.ts | 10 +- .../index-metadatas.integration-spec.ts | 59 +++ ...l-message-associations.integration-spec.ts | 6 +- .../message-channels.integration-spec.ts | 2 +- .../message-participants.integration-spec.ts | 2 +- .../message-threads.integration-spec.ts | 2 +- .../note-targets.integration-spec.ts | 4 +- .../notes.integration-spec.ts | 2 +- .../objects.integration-spec.ts | 2 +- .../opportunities.integration-spec.ts | 4 +- .../people.integration-spec.ts | 14 +- .../rockets.integration-spec.ts | 57 +++ .../search-activities.integration-spec.ts | 67 +++ ...earch-activity-targets.integration-spec.ts | 61 +++ .../search-api-keys.integration-spec.ts | 57 +++ .../search-attachments.integration-spec.ts | 73 +++ .../search-audit-logs.integration-spec.ts | 65 +++ .../search-blocklists.integration-spec.ts | 55 +++ ...nel-event-associations.integration-spec.ts | 73 +++ ...arch-calendar-channels.integration-spec.ts | 79 ++++ ...dar-event-participants.integration-spec.ts | 71 +++ ...search-calendar-events.integration-spec.ts | 73 +++ .../search-comments.integration-spec.ts | 57 +++ .../search-companies.integration-spec.ts | 69 +++ ...rch-connected-accounts.integration-spec.ts | 69 +++ .../search-favorites.integration-spec.ts | 75 +++ ...l-message-associations.integration-spec.ts | 77 ++++ ...earch-message-channels.integration-spec.ts | 87 ++++ ...h-message-participants.integration-spec.ts | 63 +++ ...search-message-threads.integration-spec.ts | 51 +++ .../search-messages.integration-spec.ts | 61 +++ .../search-note-targets.integration-spec.ts | 61 +++ .../search-notes.integration-spec.ts | 57 +++ .../search-opportunities.integration-spec.ts | 65 +++ .../search-people.integration-spec.ts | 69 +++ .../search-rockets.integration-spec.ts | 57 +++ .../search-task-targets.integration-spec.ts | 61 +++ .../search-tasks.integration-spec.ts | 63 +++ ...ch-timeline-activities.integration-spec.ts | 87 ++++ .../search-view-fields.integration-spec.ts | 61 +++ .../search-view-filters.integration-spec.ts | 61 +++ .../search-view-sorts.integration-spec.ts | 57 +++ .../search-views.integration-spec.ts | 67 +++ .../search-webhooks.integration-spec.ts | 57 +++ ...rkflow-event-listeners.integration-spec.ts | 55 +++ .../search-workflow-runs.integration-spec.ts | 69 +++ ...arch-workflow-versions.integration-spec.ts | 63 +++ .../search-workflows.integration-spec.ts | 59 +++ ...arch-workspace-members.integration-spec.ts | 67 +++ .../serverless-functions.integration-spec.ts | 59 +++ .../task-targets.integration-spec.ts | 4 +- .../tasks.integration-spec.ts | 2 +- .../timeline-activities.integration-spec.ts | 10 +- .../view-fields.integration-spec.ts | 2 +- .../view-filters.integration-spec.ts | 2 +- .../view-sorts.integration-spec.ts | 2 +- .../views.integration-spec.ts | 6 +- .../webhooks.integration-spec.ts | 6 +- ...rkflow-event-listeners.integration-spec.ts | 55 +++ .../workflow-versions.integration-spec.ts | 63 +++ .../workflows.integration-spec.ts | 59 +++ .../workspace-members.integration-spec.ts | 2 +- .../create-many-operation-factory.util.ts | 28 ++ .../create-one-operation-factory.util.ts | 26 ++ .../delete-many-operation-factory.util.ts | 30 ++ .../delete-one-operation-factory.util.ts | 26 ++ .../destroy-many-operation-factory.util.ts | 30 ++ .../destroy-one-operation-factory.util.ts | 26 ++ .../utils/find-many-operation-factory.util.ts | 32 ++ .../utils/find-one-operation-factory.util.ts | 26 ++ .../utils/make-graphql-api-request.util.ts | 19 + .../update-many-operation-factory.util.ts | 34 ++ .../update-one-operation-factory.util.ts | 29 ++ .../{ => integration}/utils/create-app.ts | 0 .../integration/utils/generate-record-name.ts | 4 + .../{ => integration}/utils/setup-test.ts | 0 .../{ => integration}/utils/teardown-test.ts | 0 99 files changed, 3595 insertions(+), 102 deletions(-) delete mode 160000 packages/twenty-server/felix delete mode 100644 packages/twenty-server/test/company.integration-spec.ts rename packages/twenty-server/{scripts/generate-integration-tests => test/integration/graphql/codegen}/index.ts (98%) rename packages/twenty-server/{scripts/generate-integration-tests => test/integration/graphql/codegen}/introspection-query.ts (100%) rename packages/twenty-server/{scripts/generate-integration-tests => test/integration/graphql/codegen}/introspection.interface.ts (100%) create mode 100644 packages/twenty-server/test/integration/graphql/suites/all-resolvers.integration-spec.ts rename packages/twenty-server/test/{ => integration/graphql/suites}/auth.integration-spec.ts (100%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/activities.integration-spec.ts (97%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/activity-targets.integration-spec.ts (92%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/api-keys.integration-spec.ts (96%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/attachments.integration-spec.ts (94%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/audit-logs.integration-spec.ts (97%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/blocklists.integration-spec.ts (96%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/calendar-channel-event-associations.integration-spec.ts (89%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/calendar-channels.integration-spec.ts (94%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/calendar-event-participants.integration-spec.ts (96%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/comments.integration-spec.ts (96%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/companies.integration-spec.ts (93%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/connected-accounts.integration-spec.ts (93%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/favorites.integration-spec.ts (82%) create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/index-metadatas.integration-spec.ts rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/message-channel-message-associations.integration-spec.ts (96%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/message-channels.integration-spec.ts (97%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/message-participants.integration-spec.ts (96%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/message-threads.integration-spec.ts (95%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/note-targets.integration-spec.ts (92%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/notes.integration-spec.ts (96%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/objects.integration-spec.ts (97%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/opportunities.integration-spec.ts (92%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/people.integration-spec.ts (82%) create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/rockets.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-activities.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-activity-targets.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-api-keys.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-attachments.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-audit-logs.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-blocklists.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-channel-event-associations.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-channels.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-event-participants.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-events.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-comments.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-companies.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-connected-accounts.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-favorites.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-channel-message-associations.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-channels.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-participants.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-threads.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-messages.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-note-targets.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-notes.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-opportunities.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-people.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-rockets.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-task-targets.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-tasks.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-timeline-activities.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-fields.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-filters.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-sorts.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-views.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-webhooks.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-event-listeners.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-runs.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-versions.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflows.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/search-workspace-members.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/serverless-functions.integration-spec.ts rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/task-targets.integration-spec.ts (92%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/tasks.integration-spec.ts (97%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/timeline-activities.integration-spec.ts (84%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/view-fields.integration-spec.ts (96%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/view-filters.integration-spec.ts (96%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/view-sorts.integration-spec.ts (96%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/views.integration-spec.ts (97%) rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/webhooks.integration-spec.ts (96%) create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/workflow-event-listeners.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/workflow-versions.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/object-generated/workflows.integration-spec.ts rename packages/twenty-server/test/{ => integration/graphql/suites/object-generated}/workspace-members.integration-spec.ts (97%) create mode 100644 packages/twenty-server/test/integration/graphql/utils/create-many-operation-factory.util.ts create mode 100644 packages/twenty-server/test/integration/graphql/utils/create-one-operation-factory.util.ts create mode 100644 packages/twenty-server/test/integration/graphql/utils/delete-many-operation-factory.util.ts create mode 100644 packages/twenty-server/test/integration/graphql/utils/delete-one-operation-factory.util.ts create mode 100644 packages/twenty-server/test/integration/graphql/utils/destroy-many-operation-factory.util.ts create mode 100644 packages/twenty-server/test/integration/graphql/utils/destroy-one-operation-factory.util.ts create mode 100644 packages/twenty-server/test/integration/graphql/utils/find-many-operation-factory.util.ts create mode 100644 packages/twenty-server/test/integration/graphql/utils/find-one-operation-factory.util.ts create mode 100644 packages/twenty-server/test/integration/graphql/utils/make-graphql-api-request.util.ts create mode 100644 packages/twenty-server/test/integration/graphql/utils/update-many-operation-factory.util.ts create mode 100644 packages/twenty-server/test/integration/graphql/utils/update-one-operation-factory.util.ts rename packages/twenty-server/test/{ => integration}/utils/create-app.ts (100%) create mode 100644 packages/twenty-server/test/integration/utils/generate-record-name.ts rename packages/twenty-server/test/{ => integration}/utils/setup-test.ts (100%) rename packages/twenty-server/test/{ => integration}/utils/teardown-test.ts (100%) diff --git a/packages/twenty-server/.env.test b/packages/twenty-server/.env.test index b8fb82f4c8fc..e768984fbdc8 100644 --- a/packages/twenty-server/.env.test +++ b/packages/twenty-server/.env.test @@ -11,7 +11,7 @@ EXCEPTION_HANDLER_DRIVER=console SENTRY_DSN=https://ba869cb8fd72d5faeb6643560939cee0@o4505516959793152.ingest.sentry.io/4506660900306944 DEMO_WORKSPACE_IDS=63db4589-590f-42b3-bdf1-85268b3da02f,8de58f3f-7e86-4a0b-998d-b2cbe314ee3a,4d957b72-0b37-4bad-9468-8dc828ee082d,daa0b739-269e-49b6-9be5-5f0215941489,59c15f6a-909a-4495-9cf4-3ce1b0abbb6a,7202cc9d-92da-4b52-a323-d29d38cd3b4e,5f071b0d-646b-411a-94f1-5d9ba9d5c6ac,7bc10973-897b-4767-ab2f-35cdac3b2aec,4b3ba0be-2d29-4b1e-be66-8ac7eb65d000,edfb500d-cc4e-4f22-8e2b-f139a9758a68,eee459c9-9057-4459-ae0d-d51d14c01635,3dd2f505-0075-4217-ba33-fc4244aeaaa9,3d1a9165-3f3f-494e-a99d-f858eae95144,84db6ded-cfce-4aee-9160-6553b05c8143,96fb1540-269b-4d13-af21-2a8268eff8ca,b2463e69-d121-4ea5-80c9-bba82403e93e,5af30c15-867d-49ed-b939-d4856bed8514,b5677aa1-68fa-4818-aaaa-434a07ae2ed4,1ec7fa9a-d6bf-4fa2-a753-9a235d75ee3f,753a6fa2-df27-4c87-8c90-4da78fcb30dd,2138f2f2-bbe9-41df-b483-687a9075f94e,a885cfef-4636-4c3a-9788-1ff6e6b92df5,5458f7fb-9431-47a2-b7a0-32f31d115e23,6c09929f-11c3-4f92-9508-aa0e6b934d1e,57ae0a2c-7a4e-4c7d-8f43-68548e7f1206,cc7f0b85-6868-4c2d-85c5-3ce9977ea346,21871a7f-f067-45ea-989e-44339bb5ad07,c3efedab-84f5-4656-8297-55964b3d26cb,647dcdd1-4540-4003-9f58-fd84d4d759b7,fc5e6857-8d67-47b8-98f2-edeb0671e326,1ad8d72c-1826-40ed-8b44-d15a1d2aab70,eac6c90a-d25d-4c8c-a053-cfbc7cde0afb,023a70de-a85e-43fc-bbc6-757fbf6562f0,f3f0a7fb-1409-443b-8e39-4e58e628796e,62828804-97d4-40ec-82fa-2992a6ce4a81,af5441fe-b16f-4996-87f4-1a433ec53dd6,e8857860-f7b1-4478-9741-1eb9e7c11f2c,6bca9c44-c8c0-49f8-b0b5-1bb2ca7842b8,d50da092-09df-448f-84ea-3ebddfe1d9f6,9efd5d6d-db64-47d4-9ad3-5e4d8b65ff7f,6f089094-2dd2-4b0e-b5b7-8bb52b93ea8e,299b0822-68e9-4bfa-af35-da799012e80e,a3dd579c-93be-45a0-ad35-f518d8ed45dd,023b1b3e-4891-4061-aae0-f34368644f40,50174445-33c5-4482-bb8c-3ef6c511c8cd,9933c048-07a7-4735-9af2-940c2f9b6683,beadc568-3962-46bd-ad4d-06e23b37615b,0cdafc9f-d4c1-4576-837e-d7f6ec28643d,50bb24ce-1709-4928-a87b-d9d9e147a2ab,7690ed72-910d-4357-8e0e-17aa702b0b94,1ad0d69f-60fa-414f-bf79-4f94c2abba43,946d84a4-db4d-48cb-a5d3-03081b5c7e8e,1a080055-d2bf-4b14-8957-88a7d08769b8,ed343e38-e405-4fae-9486-27b09c98bdad,c8bdef75-a98c-4646-a372-3251340d2dea,87a8c6fa-f93e-4950-aff2-5f956ca1a6ba,604781ba-23c2-4220-a717-b5615431fcd9,31af6841-ad9f-4f28-a637-b5c5e6589447,cf067451-7b88-4ff2-a96d-3fc9c5d6fea0,26a8ad5e-29d9-4e7d-aa1f-e6221e8ea32a,fd14db29-e4df-44a7-9b3f-d00384458122,73b477a8-fcf4-4860-a685-65a0a79b8653,82e0f305-4c6c-4160-be1d-b0de834124e6,e38567ab-a6e2-4a94-99c5-a7db31c0aae8,faf3d6dc-66ff-4c1b-9658-f65a9cd9fcf1,6df6bb90-200e-4290-b73d-9bb374554229,2ff10cf4-a871-404a-9e7b-5ca7a232567e,06c614e2-0f36-4b72-8c82-59631680add2,5e508c81-3453-4185-ae8c-4c9b841f8c15,21b5c371-6010-4b1b-be67-7538eb877efb,54e61442-e291-4eea-8d49-7f11b5f85bd2,b6b7260a-4eea-40b0-9f7f-1dfd4c3cc7a8,e163fe76-30fb-44fb-b51a-50cc78745a21,4da672f2-29b4-4a98-b27c-b39a4aecc858,2fdb0601-c882-4aaf-ad49-ae17e530d47a,49525e1b-1b47-4545-a98c-0ba58778179f,f958ab32-b152-4004-9228-18148f7380f1,0ff5025a-62cd-4a10-a722-79f7cf360f01,642df445-e314-409a-a97d-64fc2aa2a15e,38b0dab5-d4fb-44f9-8cf9-bb35cf82e91d,62054133-f35a-4f64-a2ee-a31e48952835,536dbe8c-af33-4eab-a0a8-8d039a00db40,a04998ba-52c9-4538-b6d9-6d04408dbaf2,89016c7a-3d36-4619-a5c6-4f31795eebf7,7708b9a9-776c-46fc-94a4-dc28e7880958,5c92bc69-b328-4c66-a791-a05dbaf7a6f8,ad580a50-80b4-44be-9bc4-f2b57cd23207,36c0241c-891e-4b74-bd10-5e99df96bbc8,a96842ff-18be-4536-a23d-20d973d91621,0ea549b0-9558-4bdf-9944-5abc707c7660,0186c353-5ed2-4c94-b71a-fc0b48c90288,1508a165-2217-4911-b31c-1ea42a08f097,1731e392-dfdf-4fc4-863b-27ae62b0e374,0b245cea-96a6-4a3a-af6a-ef43496c239c,a844e208-7078-43a2-8bd0-86f31498cd3f,53d112b5-87f2-490b-a788-df1f4624f9ad,0d5794d4-3a52-482b-9a6a-f8185018bad1,df877aa6-231c-47fb-9be0-906e61677356,c56c6d1a-3418-49d2-82ce-bd9370668043,6e0b6f34-3cd0-4aa0-ae1f-25f5545dca68 MUTATION_MAXIMUM_RECORD_AFFECTED=100 -MESSAGE_QUEUE_TYPE=pg-boss +MESSAGE_QUEUE_TYPE=bull-mq CACHE_STORAGE_TYPE=redis REDIS_URL=redis://localhost:6379 diff --git a/packages/twenty-server/felix b/packages/twenty-server/felix deleted file mode 160000 index a33b01797795..000000000000 --- a/packages/twenty-server/felix +++ /dev/null @@ -1 +0,0 @@ -Subproject commit a33b01797795419edef84f122b5214472648d1ce diff --git a/packages/twenty-server/jest-integration.config.ts b/packages/twenty-server/jest-integration.config.ts index 10b5cc9e2099..deb2ba3ee5a1 100644 --- a/packages/twenty-server/jest-integration.config.ts +++ b/packages/twenty-server/jest-integration.config.ts @@ -11,11 +11,14 @@ const jestConfig: JestConfigWithTsJest = { testEnvironment: 'node', testRegex: '.integration-spec.ts$', modulePathIgnorePatterns: ['/dist'], - globalSetup: '/test/utils/setup-test.ts', - globalTeardown: '/test/utils/teardown-test.ts', + globalSetup: '/test/integration/utils/setup-test.ts', + globalTeardown: '/test/integration/utils/teardown-test.ts', testTimeout: 15000, moduleNameMapper: { - ...pathsToModuleNameMapper(tsConfig.compilerOptions.paths), + ...pathsToModuleNameMapper(tsConfig.compilerOptions.paths, { + prefix: '/../..', + }), + '^test/(.*)$': '/test/$1', 'twenty-emails': '/../twenty-emails/dist/index.js', }, fakeTimers: { diff --git a/packages/twenty-server/project.json b/packages/twenty-server/project.json index ed8ad716b6e0..de8dec76a92d 100644 --- a/packages/twenty-server/project.json +++ b/packages/twenty-server/project.json @@ -162,7 +162,7 @@ "options": { "cwd": "packages/twenty-server", "commands": [ - "nx ts-node-no-deps -- ./scripts/generate-integration-tests/index.ts" + "nx ts-node-no-deps -- ./test/integration/graphql/codegen/index.ts" ], "parallel": false } diff --git a/packages/twenty-server/test/company.integration-spec.ts b/packages/twenty-server/test/company.integration-spec.ts deleted file mode 100644 index bd25d66eb4dc..000000000000 --- a/packages/twenty-server/test/company.integration-spec.ts +++ /dev/null @@ -1,48 +0,0 @@ -import request from 'supertest'; - -const graphqlClient = request(`http://localhost:${APP_PORT}`); - -describe('CompanyResolver (integration)', () => { - it('should find many companies', () => { - const queryData = { - query: ` - query Companies { - companies { - edges { - node { - id - name - } - } - } - } - `, - }; - - return graphqlClient - .post('/graphql') - .set('Authorization', `Bearer ${ACCESS_TOKEN}`) - .send(queryData) - .expect(200) - .expect((res) => { - expect(res.body.data).toBeDefined(); - expect(res.body.errors).toBeUndefined(); - }) - .expect((res) => { - const data = res.body.data.companies; - - expect(data).toBeDefined(); - expect(Array.isArray(data.edges)).toBe(true); - - const edges = data.edges; - - if (edges.length > 0) { - const company = edges[0].node; - - expect(company).toBeDefined(); - expect(company).toHaveProperty('id'); - expect(company).toHaveProperty('name'); - } - }); - }); -}); diff --git a/packages/twenty-server/scripts/generate-integration-tests/index.ts b/packages/twenty-server/test/integration/graphql/codegen/index.ts similarity index 98% rename from packages/twenty-server/scripts/generate-integration-tests/index.ts rename to packages/twenty-server/test/integration/graphql/codegen/index.ts index 5b635d3bccd7..9f1937cae2b5 100644 --- a/packages/twenty-server/scripts/generate-integration-tests/index.ts +++ b/packages/twenty-server/test/integration/graphql/codegen/index.ts @@ -13,7 +13,7 @@ import { const GRAPHQL_URL = 'http://localhost:3000/graphql'; const BEARER_TOKEN = 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC05ZTNiLTQ2ZDQtYTU1Ni04OGI5ZGRjMmIwMzQiLCJ3b3Jrc3BhY2VJZCI6IjIwMjAyMDIwLTFjMjUtNGQwMi1iZjI1LTZhZWNjZjdlYTQxOSIsIndvcmtzcGFjZU1lbWJlcklkIjoiMjAyMDIwMjAtMDY4Ny00YzQxLWI3MDctZWQxYmZjYTk3MmE3IiwiaWF0IjoxNzI2NDkyNTAyLCJleHAiOjEzMjQ1MDE2NTAyfQ.zM6TbfeOqYVH5Sgryc2zf02hd9uqUOSL1-iJlMgwzsI'; -const TEST_OUTPUT_DIR = './test'; +const TEST_OUTPUT_DIR = './test/integration/graphql/suites/object-generated'; const fetchGraphQLSchema = async (): Promise => { const headers = { diff --git a/packages/twenty-server/scripts/generate-integration-tests/introspection-query.ts b/packages/twenty-server/test/integration/graphql/codegen/introspection-query.ts similarity index 100% rename from packages/twenty-server/scripts/generate-integration-tests/introspection-query.ts rename to packages/twenty-server/test/integration/graphql/codegen/introspection-query.ts diff --git a/packages/twenty-server/scripts/generate-integration-tests/introspection.interface.ts b/packages/twenty-server/test/integration/graphql/codegen/introspection.interface.ts similarity index 100% rename from packages/twenty-server/scripts/generate-integration-tests/introspection.interface.ts rename to packages/twenty-server/test/integration/graphql/codegen/introspection.interface.ts diff --git a/packages/twenty-server/test/integration/graphql/suites/all-resolvers.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/all-resolvers.integration-spec.ts new file mode 100644 index 000000000000..3b1a46f89a44 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/all-resolvers.integration-spec.ts @@ -0,0 +1,430 @@ +import { createManyOperationFactory } from 'test/integration/graphql/utils/create-many-operation-factory.util'; +import { createOneOperationFactory } from 'test/integration/graphql/utils/create-one-operation-factory.util'; +import { deleteManyOperationFactory } from 'test/integration/graphql/utils/delete-many-operation-factory.util'; +import { deleteOneOperationFactory } from 'test/integration/graphql/utils/delete-one-operation-factory.util'; +import { destroyManyOperationFactory } from 'test/integration/graphql/utils/destroy-many-operation-factory.util'; +import { destroyOneOperationFactory } from 'test/integration/graphql/utils/destroy-one-operation-factory.util'; +import { findManyOperationFactory } from 'test/integration/graphql/utils/find-many-operation-factory.util'; +import { findOneOperationFactory } from 'test/integration/graphql/utils/find-one-operation-factory.util'; +import { makeGraphqlAPIRequest } from 'test/integration/graphql/utils/make-graphql-api-request.util'; +import { updateManyOperationFactory } from 'test/integration/graphql/utils/update-many-operation-factory.util'; +import { updateOneOperationFactory } from 'test/integration/graphql/utils/update-one-operation-factory.util'; +import { generateRecordName } from 'test/integration/utils/generate-record-name'; + +const COMPANY_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; +const COMPANY_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; +const COMPANY_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; +const COMPANY_GQL_FIELDS = ` + id + name + employees + idealCustomerProfile + position + createdAt + updatedAt + deletedAt + accountOwnerId + tagline + workPolicy + visaSponsorship +`; + +describe('companies resolvers (integration)', () => { + it('1. should create and return companies', async () => { + const companyName1 = generateRecordName(COMPANY_1_ID); + const companyName2 = generateRecordName(COMPANY_2_ID); + + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: COMPANY_GQL_FIELDS, + data: [ + { + id: COMPANY_1_ID, + name: companyName1, + }, + { + id: COMPANY_2_ID, + name: companyName2, + }, + ], + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.createCompanies).toHaveLength(2); + + response.body.data.createCompanies.forEach((company) => { + expect(company).toHaveProperty('name'); + expect([companyName1, companyName2]).toContain(company.name); + + expect(company).toHaveProperty('employees'); + expect(company).toHaveProperty('idealCustomerProfile'); + expect(company).toHaveProperty('position'); + expect(company).toHaveProperty('id'); + expect(company).toHaveProperty('createdAt'); + expect(company).toHaveProperty('updatedAt'); + expect(company).toHaveProperty('deletedAt'); + expect(company).toHaveProperty('accountOwnerId'); + expect(company).toHaveProperty('tagline'); + expect(company).toHaveProperty('workPolicy'); + expect(company).toHaveProperty('visaSponsorship'); + }); + }); + + it('1b. should create and return one company', async () => { + const companyName = generateRecordName(COMPANY_3_ID); + + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: COMPANY_GQL_FIELDS, + data: { + id: COMPANY_3_ID, + name: companyName, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdCompany = response.body.data.createCompany; + + expect(createdCompany).toHaveProperty('name'); + expect(createdCompany.name).toEqual(companyName); + + expect(createdCompany).toHaveProperty('employees'); + expect(createdCompany).toHaveProperty('idealCustomerProfile'); + expect(createdCompany).toHaveProperty('position'); + expect(createdCompany).toHaveProperty('id'); + expect(createdCompany).toHaveProperty('createdAt'); + expect(createdCompany).toHaveProperty('updatedAt'); + expect(createdCompany).toHaveProperty('deletedAt'); + expect(createdCompany).toHaveProperty('accountOwnerId'); + expect(createdCompany).toHaveProperty('tagline'); + expect(createdCompany).toHaveProperty('workPolicy'); + expect(createdCompany).toHaveProperty('visaSponsorship'); + }); + + it('2. should find many companies', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: COMPANY_GQL_FIELDS, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const data = response.body.data.companies; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const companies = edges[0].node; + + expect(companies).toHaveProperty('name'); + expect(companies).toHaveProperty('employees'); + expect(companies).toHaveProperty('idealCustomerProfile'); + expect(companies).toHaveProperty('position'); + expect(companies).toHaveProperty('id'); + expect(companies).toHaveProperty('createdAt'); + expect(companies).toHaveProperty('updatedAt'); + expect(companies).toHaveProperty('deletedAt'); + expect(companies).toHaveProperty('accountOwnerId'); + expect(companies).toHaveProperty('tagline'); + expect(companies).toHaveProperty('workPolicy'); + expect(companies).toHaveProperty('visaSponsorship'); + } + }); + + it('2b. should find one company', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + id: { + eq: COMPANY_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const company = response.body.data.company; + + expect(company).toHaveProperty('name'); + + expect(company).toHaveProperty('employees'); + expect(company).toHaveProperty('idealCustomerProfile'); + expect(company).toHaveProperty('position'); + expect(company).toHaveProperty('id'); + expect(company).toHaveProperty('createdAt'); + expect(company).toHaveProperty('updatedAt'); + expect(company).toHaveProperty('deletedAt'); + expect(company).toHaveProperty('accountOwnerId'); + expect(company).toHaveProperty('tagline'); + expect(company).toHaveProperty('workPolicy'); + expect(company).toHaveProperty('visaSponsorship'); + }); + + it('3. should update many companies', async () => { + const graphqlOperation = updateManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: COMPANY_GQL_FIELDS, + data: { + employees: 123, + }, + filter: { + id: { + in: [COMPANY_1_ID, COMPANY_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedCompanies = response.body.data.updateCompanies; + + expect(updatedCompanies).toHaveLength(2); + + updatedCompanies.forEach((company) => { + expect(company.employees).toEqual(123); + }); + }); + + it('3b. should update one company', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: COMPANY_GQL_FIELDS, + data: { + employees: 122, + }, + recordId: COMPANY_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedCompany = response.body.data.updateCompany; + + expect(updatedCompany.employees).toEqual(122); + }); + + it('4. should find many companies with updated employees', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + employees: { + eq: 123, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.companies.edges).toHaveLength(2); + }); + + it('4b. should find one company with updated employees', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + employees: { + eq: 122, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.company.employees).toEqual(122); + }); + + it('5. should delete many companies', async () => { + const graphqlOperation = deleteManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + id: { + in: [COMPANY_1_ID, COMPANY_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const deleteCompanies = response.body.data.deleteCompanies; + + expect(deleteCompanies).toHaveLength(2); + + deleteCompanies.forEach((company) => { + expect(company.deletedAt).toBeTruthy(); + }); + }); + + it('5b. should delete one company', async () => { + const graphqlOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: COMPANY_GQL_FIELDS, + recordId: COMPANY_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.deleteCompany.deletedAt).toBeTruthy(); + }); + + it('6. should not find many companies anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + id: { + in: [COMPANY_1_ID, COMPANY_2_ID], + }, + }, + }); + + const findCompaniesResponse = await makeGraphqlAPIRequest(graphqlOperation); + + expect(findCompaniesResponse.body.data.companies.edges).toHaveLength(0); + }); + + it('6b. should not find one company anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + id: { + eq: COMPANY_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.company).toBeNull(); + }); + + it('7. should find many deleted companies with deletedAt filter', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + id: { + in: [COMPANY_1_ID, COMPANY_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.companies.edges).toHaveLength(2); + }); + + it('7b. should find one deleted company with deletedAt filter', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + id: { + eq: COMPANY_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.company.id).toEqual(COMPANY_3_ID); + }); + + it('8. should destroy many companies', async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + id: { + in: [COMPANY_1_ID, COMPANY_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.destroyCompanies).toHaveLength(2); + }); + + it('8b. should destroy one company', async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: COMPANY_GQL_FIELDS, + recordId: COMPANY_3_ID, + }); + + const destroyCompanyResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect(destroyCompanyResponse.body.data.destroyCompany).toBeTruthy(); + }); + + it('9. should not find many companies anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'company', + objectMetadataPluralName: 'companies', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + id: { + in: [COMPANY_1_ID, COMPANY_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.companies.edges).toHaveLength(0); + }); + + it('9b. should not find one company anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'company', + gqlFields: COMPANY_GQL_FIELDS, + filter: { + id: { + eq: COMPANY_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.company).toBeNull(); + }); +}); diff --git a/packages/twenty-server/test/auth.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/auth.integration-spec.ts similarity index 100% rename from packages/twenty-server/test/auth.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/auth.integration-spec.ts diff --git a/packages/twenty-server/test/activities.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/activities.integration-spec.ts similarity index 97% rename from packages/twenty-server/test/activities.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/activities.integration-spec.ts index 01f262c8f295..0e0134578e44 100644 --- a/packages/twenty-server/test/activities.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/activities.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('activitiesResolver (integration)', () => { +describe('activitiesResolver (e2e)', () => { it('should find many activities', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/activity-targets.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/activity-targets.integration-spec.ts similarity index 92% rename from packages/twenty-server/test/activity-targets.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/activity-targets.integration-spec.ts index cbbeb216f0aa..99b4f0b1e10f 100644 --- a/packages/twenty-server/test/activity-targets.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/activity-targets.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('activityTargetsResolver (integration)', () => { +describe('activityTargetsResolver (e2e)', () => { it('should find many activityTargets', () => { const queryData = { query: ` @@ -18,6 +18,7 @@ describe('activityTargetsResolver (integration)', () => { personId companyId opportunityId + rocketId } } } @@ -53,6 +54,7 @@ describe('activityTargetsResolver (integration)', () => { expect(activityTargets).toHaveProperty('personId'); expect(activityTargets).toHaveProperty('companyId'); expect(activityTargets).toHaveProperty('opportunityId'); + expect(activityTargets).toHaveProperty('rocketId'); } }); }); diff --git a/packages/twenty-server/test/api-keys.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/api-keys.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/api-keys.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/api-keys.integration-spec.ts index a196db0861c4..5515abb9a620 100644 --- a/packages/twenty-server/test/api-keys.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/api-keys.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('apiKeysResolver (integration)', () => { +describe('apiKeysResolver (e2e)', () => { it('should find many apiKeys', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/attachments.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/attachments.integration-spec.ts similarity index 94% rename from packages/twenty-server/test/attachments.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/attachments.integration-spec.ts index 440a6484e6b1..fc9637963342 100644 --- a/packages/twenty-server/test/attachments.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/attachments.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('attachmentsResolver (integration)', () => { +describe('attachmentsResolver (e2e)', () => { it('should find many attachments', () => { const queryData = { query: ` @@ -24,6 +24,7 @@ describe('attachmentsResolver (integration)', () => { personId companyId opportunityId + rocketId } } } @@ -65,6 +66,7 @@ describe('attachmentsResolver (integration)', () => { expect(attachments).toHaveProperty('personId'); expect(attachments).toHaveProperty('companyId'); expect(attachments).toHaveProperty('opportunityId'); + expect(attachments).toHaveProperty('rocketId'); } }); }); diff --git a/packages/twenty-server/test/audit-logs.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/audit-logs.integration-spec.ts similarity index 97% rename from packages/twenty-server/test/audit-logs.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/audit-logs.integration-spec.ts index 99a573235cda..77a4507188f8 100644 --- a/packages/twenty-server/test/audit-logs.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/audit-logs.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('auditLogsResolver (integration)', () => { +describe('auditLogsResolver (e2e)', () => { it('should find many auditLogs', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/blocklists.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/blocklists.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/blocklists.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/blocklists.integration-spec.ts index d8080b3cd570..60da5e4673fd 100644 --- a/packages/twenty-server/test/blocklists.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/blocklists.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('blocklistsResolver (integration)', () => { +describe('blocklistsResolver (e2e)', () => { it('should find many blocklists', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/calendar-channel-event-associations.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/calendar-channel-event-associations.integration-spec.ts similarity index 89% rename from packages/twenty-server/test/calendar-channel-event-associations.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/calendar-channel-event-associations.integration-spec.ts index 5d03268b840d..023b0876915d 100644 --- a/packages/twenty-server/test/calendar-channel-event-associations.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/calendar-channel-event-associations.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('calendarChannelEventAssociationsResolver (integration)', () => { +describe('calendarChannelEventAssociationsResolver (e2e)', () => { it('should find many calendarChannelEventAssociations', () => { const queryData = { query: ` @@ -11,6 +11,7 @@ describe('calendarChannelEventAssociationsResolver (integration)', () => { edges { node { eventExternalId + recurringEventExternalId id createdAt updatedAt @@ -47,6 +48,9 @@ describe('calendarChannelEventAssociationsResolver (integration)', () => { expect(calendarChannelEventAssociations).toHaveProperty( 'eventExternalId', ); + expect(calendarChannelEventAssociations).toHaveProperty( + 'recurringEventExternalId', + ); expect(calendarChannelEventAssociations).toHaveProperty('id'); expect(calendarChannelEventAssociations).toHaveProperty('createdAt'); expect(calendarChannelEventAssociations).toHaveProperty('updatedAt'); diff --git a/packages/twenty-server/test/calendar-channels.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/calendar-channels.integration-spec.ts similarity index 94% rename from packages/twenty-server/test/calendar-channels.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/calendar-channels.integration-spec.ts index 6056af6ac616..baab9d5003d3 100644 --- a/packages/twenty-server/test/calendar-channels.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/calendar-channels.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('calendarChannelsResolver (integration)', () => { +describe('calendarChannelsResolver (e2e)', () => { it('should find many calendarChannels', () => { const queryData = { query: ` @@ -18,6 +18,7 @@ describe('calendarChannelsResolver (integration)', () => { contactAutoCreationPolicy isSyncEnabled syncCursor + syncedAt syncStageStartedAt throttleFailureCount id @@ -62,6 +63,7 @@ describe('calendarChannelsResolver (integration)', () => { expect(calendarChannels).toHaveProperty('contactAutoCreationPolicy'); expect(calendarChannels).toHaveProperty('isSyncEnabled'); expect(calendarChannels).toHaveProperty('syncCursor'); + expect(calendarChannels).toHaveProperty('syncedAt'); expect(calendarChannels).toHaveProperty('syncStageStartedAt'); expect(calendarChannels).toHaveProperty('throttleFailureCount'); expect(calendarChannels).toHaveProperty('id'); diff --git a/packages/twenty-server/test/calendar-event-participants.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/calendar-event-participants.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/calendar-event-participants.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/calendar-event-participants.integration-spec.ts index 50e65547e406..45a8c87a8471 100644 --- a/packages/twenty-server/test/calendar-event-participants.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/calendar-event-participants.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('calendarEventParticipantsResolver (integration)', () => { +describe('calendarEventParticipantsResolver (e2e)', () => { it('should find many calendarEventParticipants', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/comments.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/comments.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/comments.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/comments.integration-spec.ts index 0f89ba5491e6..2508ff628acd 100644 --- a/packages/twenty-server/test/comments.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/comments.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('commentsResolver (integration)', () => { +describe('commentsResolver (e2e)', () => { it('should find many comments', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/companies.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/companies.integration-spec.ts similarity index 93% rename from packages/twenty-server/test/companies.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/companies.integration-spec.ts index 63c7f9eec481..1273e624b0ac 100644 --- a/packages/twenty-server/test/companies.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/companies.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('companiesResolver (integration)', () => { +describe('companiesResolver (e2e)', () => { it('should find many companies', () => { const queryData = { query: ` @@ -14,6 +14,7 @@ describe('companiesResolver (integration)', () => { employees idealCustomerProfile position + searchVector id createdAt updatedAt @@ -53,6 +54,7 @@ describe('companiesResolver (integration)', () => { expect(companies).toHaveProperty('employees'); expect(companies).toHaveProperty('idealCustomerProfile'); expect(companies).toHaveProperty('position'); + expect(companies).toHaveProperty('searchVector'); expect(companies).toHaveProperty('id'); expect(companies).toHaveProperty('createdAt'); expect(companies).toHaveProperty('updatedAt'); diff --git a/packages/twenty-server/test/connected-accounts.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/connected-accounts.integration-spec.ts similarity index 93% rename from packages/twenty-server/test/connected-accounts.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/connected-accounts.integration-spec.ts index e17fd5b28504..0a6858940c6a 100644 --- a/packages/twenty-server/test/connected-accounts.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/connected-accounts.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('connectedAccountsResolver (integration)', () => { +describe('connectedAccountsResolver (e2e)', () => { it('should find many connectedAccounts', () => { const queryData = { query: ` @@ -17,6 +17,7 @@ describe('connectedAccountsResolver (integration)', () => { lastSyncHistoryId authFailedAt handleAliases + scopes id createdAt updatedAt @@ -56,6 +57,7 @@ describe('connectedAccountsResolver (integration)', () => { expect(connectedAccounts).toHaveProperty('lastSyncHistoryId'); expect(connectedAccounts).toHaveProperty('authFailedAt'); expect(connectedAccounts).toHaveProperty('handleAliases'); + expect(connectedAccounts).toHaveProperty('scopes'); expect(connectedAccounts).toHaveProperty('id'); expect(connectedAccounts).toHaveProperty('createdAt'); expect(connectedAccounts).toHaveProperty('updatedAt'); diff --git a/packages/twenty-server/test/favorites.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/favorites.integration-spec.ts similarity index 82% rename from packages/twenty-server/test/favorites.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/favorites.integration-spec.ts index f58e702e5517..ea410fabc912 100644 --- a/packages/twenty-server/test/favorites.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/favorites.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('favoritesResolver (integration)', () => { +describe('favoritesResolver (e2e)', () => { it('should find many favorites', () => { const queryData = { query: ` @@ -19,9 +19,13 @@ describe('favoritesResolver (integration)', () => { personId companyId opportunityId + workflowId + workflowVersionId + workflowRunId taskId noteId viewId + rocketId } } } @@ -58,9 +62,13 @@ describe('favoritesResolver (integration)', () => { expect(favorites).toHaveProperty('personId'); expect(favorites).toHaveProperty('companyId'); expect(favorites).toHaveProperty('opportunityId'); + expect(favorites).toHaveProperty('workflowId'); + expect(favorites).toHaveProperty('workflowVersionId'); + expect(favorites).toHaveProperty('workflowRunId'); expect(favorites).toHaveProperty('taskId'); expect(favorites).toHaveProperty('noteId'); expect(favorites).toHaveProperty('viewId'); + expect(favorites).toHaveProperty('rocketId'); } }); }); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/index-metadatas.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/index-metadatas.integration-spec.ts new file mode 100644 index 000000000000..02d3d54b0562 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/index-metadatas.integration-spec.ts @@ -0,0 +1,59 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('indexMetadatasResolver (e2e)', () => { + it('should find many indexMetadatas', () => { + const queryData = { + query: ` + query indexMetadatas { + indexMetadatas { + edges { + node { + id + name + isCustom + isUnique + indexWhereClause + indexType + createdAt + updatedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.indexMetadatas; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const indexMetadatas = edges[0].node; + + expect(indexMetadatas).toHaveProperty('id'); + expect(indexMetadatas).toHaveProperty('name'); + expect(indexMetadatas).toHaveProperty('isCustom'); + expect(indexMetadatas).toHaveProperty('isUnique'); + expect(indexMetadatas).toHaveProperty('indexWhereClause'); + expect(indexMetadatas).toHaveProperty('indexType'); + expect(indexMetadatas).toHaveProperty('createdAt'); + expect(indexMetadatas).toHaveProperty('updatedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/message-channel-message-associations.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/message-channel-message-associations.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/message-channel-message-associations.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/message-channel-message-associations.integration-spec.ts index a250550f4b88..db17b067bb18 100644 --- a/packages/twenty-server/test/message-channel-message-associations.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/message-channel-message-associations.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('messageChannelMessageAssociationsResolver (integration)', () => { +describe('messageChannelMessageAssociationsResolver (e2e)', () => { it('should find many messageChannelMessageAssociations', () => { const queryData = { query: ` @@ -10,11 +10,11 @@ describe('messageChannelMessageAssociationsResolver (integration)', () => { messageChannelMessageAssociations { edges { node { - createdAt messageExternalId messageThreadExternalId direction id + createdAt updatedAt deletedAt messageChannelId @@ -46,7 +46,6 @@ describe('messageChannelMessageAssociationsResolver (integration)', () => { if (edges.length > 0) { const messageChannelMessageAssociations = edges[0].node; - expect(messageChannelMessageAssociations).toHaveProperty('createdAt'); expect(messageChannelMessageAssociations).toHaveProperty( 'messageExternalId', ); @@ -55,6 +54,7 @@ describe('messageChannelMessageAssociationsResolver (integration)', () => { ); expect(messageChannelMessageAssociations).toHaveProperty('direction'); expect(messageChannelMessageAssociations).toHaveProperty('id'); + expect(messageChannelMessageAssociations).toHaveProperty('createdAt'); expect(messageChannelMessageAssociations).toHaveProperty('updatedAt'); expect(messageChannelMessageAssociations).toHaveProperty('deletedAt'); expect(messageChannelMessageAssociations).toHaveProperty( diff --git a/packages/twenty-server/test/message-channels.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/message-channels.integration-spec.ts similarity index 97% rename from packages/twenty-server/test/message-channels.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/message-channels.integration-spec.ts index 8100a885d89c..58f9b3ea8016 100644 --- a/packages/twenty-server/test/message-channels.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/message-channels.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('messageChannelsResolver (integration)', () => { +describe('messageChannelsResolver (e2e)', () => { it('should find many messageChannels', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/message-participants.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/message-participants.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/message-participants.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/message-participants.integration-spec.ts index 45c190c536ec..1271455c0c42 100644 --- a/packages/twenty-server/test/message-participants.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/message-participants.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('messageParticipantsResolver (integration)', () => { +describe('messageParticipantsResolver (e2e)', () => { it('should find many messageParticipants', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/message-threads.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/message-threads.integration-spec.ts similarity index 95% rename from packages/twenty-server/test/message-threads.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/message-threads.integration-spec.ts index 714bf06bb65a..85ec6e2a5046 100644 --- a/packages/twenty-server/test/message-threads.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/message-threads.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('messageThreadsResolver (integration)', () => { +describe('messageThreadsResolver (e2e)', () => { it('should find many messageThreads', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/note-targets.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/note-targets.integration-spec.ts similarity index 92% rename from packages/twenty-server/test/note-targets.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/note-targets.integration-spec.ts index 30d309dc32f5..8cd6b76a34d2 100644 --- a/packages/twenty-server/test/note-targets.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/note-targets.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('noteTargetsResolver (integration)', () => { +describe('noteTargetsResolver (e2e)', () => { it('should find many noteTargets', () => { const queryData = { query: ` @@ -18,6 +18,7 @@ describe('noteTargetsResolver (integration)', () => { personId companyId opportunityId + rocketId } } } @@ -53,6 +54,7 @@ describe('noteTargetsResolver (integration)', () => { expect(noteTargets).toHaveProperty('personId'); expect(noteTargets).toHaveProperty('companyId'); expect(noteTargets).toHaveProperty('opportunityId'); + expect(noteTargets).toHaveProperty('rocketId'); } }); }); diff --git a/packages/twenty-server/test/notes.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/notes.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/notes.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/notes.integration-spec.ts index eb13fedbac42..5f25d3ffa1d1 100644 --- a/packages/twenty-server/test/notes.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/notes.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('notesResolver (integration)', () => { +describe('notesResolver (e2e)', () => { it('should find many notes', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/objects.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/objects.integration-spec.ts similarity index 97% rename from packages/twenty-server/test/objects.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/objects.integration-spec.ts index 80d1458ab668..afeb568c4c5b 100644 --- a/packages/twenty-server/test/objects.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/objects.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('objectsResolver (integration)', () => { +describe('objectsResolver (e2e)', () => { it('should find many objects', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/opportunities.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/opportunities.integration-spec.ts similarity index 92% rename from packages/twenty-server/test/opportunities.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/opportunities.integration-spec.ts index 9eebe96a0da7..82099405c9c7 100644 --- a/packages/twenty-server/test/opportunities.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/opportunities.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('opportunitiesResolver (integration)', () => { +describe('opportunitiesResolver (e2e)', () => { it('should find many opportunities', () => { const queryData = { query: ` @@ -14,6 +14,7 @@ describe('opportunitiesResolver (integration)', () => { closeDate stage position + searchVector id createdAt updatedAt @@ -51,6 +52,7 @@ describe('opportunitiesResolver (integration)', () => { expect(opportunities).toHaveProperty('closeDate'); expect(opportunities).toHaveProperty('stage'); expect(opportunities).toHaveProperty('position'); + expect(opportunities).toHaveProperty('searchVector'); expect(opportunities).toHaveProperty('id'); expect(opportunities).toHaveProperty('createdAt'); expect(opportunities).toHaveProperty('updatedAt'); diff --git a/packages/twenty-server/test/people.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/people.integration-spec.ts similarity index 82% rename from packages/twenty-server/test/people.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/people.integration-spec.ts index 28b981e22dbe..7a8454a12f8a 100644 --- a/packages/twenty-server/test/people.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/people.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('peopleResolver (integration)', () => { +describe('peopleResolver (e2e)', () => { it('should find many people', () => { const queryData = { query: ` @@ -11,22 +11,16 @@ describe('peopleResolver (integration)', () => { edges { node { jobTitle - phones { - primaryPhoneNumber - primaryPhoneCountryCode - } city avatarUrl position + searchVector id createdAt updatedAt deletedAt companyId intro - whatsapp { - primaryPhoneNumber - } workPreference performanceRating } @@ -42,7 +36,6 @@ describe('peopleResolver (integration)', () => { .send(queryData) .expect(200) .expect((res) => { - console.log(res.body); expect(res.body.data).toBeDefined(); expect(res.body.errors).toBeUndefined(); }) @@ -58,17 +51,16 @@ describe('peopleResolver (integration)', () => { const people = edges[0].node; expect(people).toHaveProperty('jobTitle'); - expect(people).toHaveProperty('phones'); expect(people).toHaveProperty('city'); expect(people).toHaveProperty('avatarUrl'); expect(people).toHaveProperty('position'); + expect(people).toHaveProperty('searchVector'); expect(people).toHaveProperty('id'); expect(people).toHaveProperty('createdAt'); expect(people).toHaveProperty('updatedAt'); expect(people).toHaveProperty('deletedAt'); expect(people).toHaveProperty('companyId'); expect(people).toHaveProperty('intro'); - expect(people).toHaveProperty('whatsapp'); expect(people).toHaveProperty('workPreference'); expect(people).toHaveProperty('performanceRating'); } diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/rockets.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/rockets.integration-spec.ts new file mode 100644 index 000000000000..a9fa0f88f432 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/rockets.integration-spec.ts @@ -0,0 +1,57 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('rocketsResolver (e2e)', () => { + it('should find many rockets', () => { + const queryData = { + query: ` + query rockets { + rockets { + edges { + node { + id + name + createdAt + updatedAt + deletedAt + position + searchVector + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.rockets; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const rockets = edges[0].node; + + expect(rockets).toHaveProperty('id'); + expect(rockets).toHaveProperty('name'); + expect(rockets).toHaveProperty('createdAt'); + expect(rockets).toHaveProperty('updatedAt'); + expect(rockets).toHaveProperty('deletedAt'); + expect(rockets).toHaveProperty('position'); + expect(rockets).toHaveProperty('searchVector'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-activities.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-activities.integration-spec.ts new file mode 100644 index 000000000000..7d9be3362461 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-activities.integration-spec.ts @@ -0,0 +1,67 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchActivitiesResolver (e2e)', () => { + it('should find many searchActivities', () => { + const queryData = { + query: ` + query searchActivities { + searchActivities { + edges { + node { + title + body + type + reminderAt + dueAt + completedAt + id + createdAt + updatedAt + deletedAt + authorId + assigneeId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchActivities; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchActivities = edges[0].node; + + expect(searchActivities).toHaveProperty('title'); + expect(searchActivities).toHaveProperty('body'); + expect(searchActivities).toHaveProperty('type'); + expect(searchActivities).toHaveProperty('reminderAt'); + expect(searchActivities).toHaveProperty('dueAt'); + expect(searchActivities).toHaveProperty('completedAt'); + expect(searchActivities).toHaveProperty('id'); + expect(searchActivities).toHaveProperty('createdAt'); + expect(searchActivities).toHaveProperty('updatedAt'); + expect(searchActivities).toHaveProperty('deletedAt'); + expect(searchActivities).toHaveProperty('authorId'); + expect(searchActivities).toHaveProperty('assigneeId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-activity-targets.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-activity-targets.integration-spec.ts new file mode 100644 index 000000000000..64b5fa8c2f1a --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-activity-targets.integration-spec.ts @@ -0,0 +1,61 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchActivityTargetsResolver (e2e)', () => { + it('should find many searchActivityTargets', () => { + const queryData = { + query: ` + query searchActivityTargets { + searchActivityTargets { + edges { + node { + id + createdAt + updatedAt + deletedAt + activityId + personId + companyId + opportunityId + rocketId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchActivityTargets; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchActivityTargets = edges[0].node; + + expect(searchActivityTargets).toHaveProperty('id'); + expect(searchActivityTargets).toHaveProperty('createdAt'); + expect(searchActivityTargets).toHaveProperty('updatedAt'); + expect(searchActivityTargets).toHaveProperty('deletedAt'); + expect(searchActivityTargets).toHaveProperty('activityId'); + expect(searchActivityTargets).toHaveProperty('personId'); + expect(searchActivityTargets).toHaveProperty('companyId'); + expect(searchActivityTargets).toHaveProperty('opportunityId'); + expect(searchActivityTargets).toHaveProperty('rocketId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-api-keys.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-api-keys.integration-spec.ts new file mode 100644 index 000000000000..6d403e20d238 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-api-keys.integration-spec.ts @@ -0,0 +1,57 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchApiKeysResolver (e2e)', () => { + it('should find many searchApiKeys', () => { + const queryData = { + query: ` + query searchApiKeys { + searchApiKeys { + edges { + node { + name + expiresAt + revokedAt + id + createdAt + updatedAt + deletedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchApiKeys; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchApiKeys = edges[0].node; + + expect(searchApiKeys).toHaveProperty('name'); + expect(searchApiKeys).toHaveProperty('expiresAt'); + expect(searchApiKeys).toHaveProperty('revokedAt'); + expect(searchApiKeys).toHaveProperty('id'); + expect(searchApiKeys).toHaveProperty('createdAt'); + expect(searchApiKeys).toHaveProperty('updatedAt'); + expect(searchApiKeys).toHaveProperty('deletedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-attachments.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-attachments.integration-spec.ts new file mode 100644 index 000000000000..1debda9ff470 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-attachments.integration-spec.ts @@ -0,0 +1,73 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchAttachmentsResolver (e2e)', () => { + it('should find many searchAttachments', () => { + const queryData = { + query: ` + query searchAttachments { + searchAttachments { + edges { + node { + name + fullPath + type + id + createdAt + updatedAt + deletedAt + authorId + activityId + taskId + noteId + personId + companyId + opportunityId + rocketId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchAttachments; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchAttachments = edges[0].node; + + expect(searchAttachments).toHaveProperty('name'); + expect(searchAttachments).toHaveProperty('fullPath'); + expect(searchAttachments).toHaveProperty('type'); + expect(searchAttachments).toHaveProperty('id'); + expect(searchAttachments).toHaveProperty('createdAt'); + expect(searchAttachments).toHaveProperty('updatedAt'); + expect(searchAttachments).toHaveProperty('deletedAt'); + expect(searchAttachments).toHaveProperty('authorId'); + expect(searchAttachments).toHaveProperty('activityId'); + expect(searchAttachments).toHaveProperty('taskId'); + expect(searchAttachments).toHaveProperty('noteId'); + expect(searchAttachments).toHaveProperty('personId'); + expect(searchAttachments).toHaveProperty('companyId'); + expect(searchAttachments).toHaveProperty('opportunityId'); + expect(searchAttachments).toHaveProperty('rocketId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-audit-logs.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-audit-logs.integration-spec.ts new file mode 100644 index 000000000000..0a7ecd6f8f06 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-audit-logs.integration-spec.ts @@ -0,0 +1,65 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchAuditLogsResolver (e2e)', () => { + it('should find many searchAuditLogs', () => { + const queryData = { + query: ` + query searchAuditLogs { + searchAuditLogs { + edges { + node { + name + properties + context + objectName + objectMetadataId + recordId + id + createdAt + updatedAt + deletedAt + workspaceMemberId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchAuditLogs; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchAuditLogs = edges[0].node; + + expect(searchAuditLogs).toHaveProperty('name'); + expect(searchAuditLogs).toHaveProperty('properties'); + expect(searchAuditLogs).toHaveProperty('context'); + expect(searchAuditLogs).toHaveProperty('objectName'); + expect(searchAuditLogs).toHaveProperty('objectMetadataId'); + expect(searchAuditLogs).toHaveProperty('recordId'); + expect(searchAuditLogs).toHaveProperty('id'); + expect(searchAuditLogs).toHaveProperty('createdAt'); + expect(searchAuditLogs).toHaveProperty('updatedAt'); + expect(searchAuditLogs).toHaveProperty('deletedAt'); + expect(searchAuditLogs).toHaveProperty('workspaceMemberId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-blocklists.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-blocklists.integration-spec.ts new file mode 100644 index 000000000000..f864a62857f6 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-blocklists.integration-spec.ts @@ -0,0 +1,55 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchBlocklistsResolver (e2e)', () => { + it('should find many searchBlocklists', () => { + const queryData = { + query: ` + query searchBlocklists { + searchBlocklists { + edges { + node { + handle + id + createdAt + updatedAt + deletedAt + workspaceMemberId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchBlocklists; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchBlocklists = edges[0].node; + + expect(searchBlocklists).toHaveProperty('handle'); + expect(searchBlocklists).toHaveProperty('id'); + expect(searchBlocklists).toHaveProperty('createdAt'); + expect(searchBlocklists).toHaveProperty('updatedAt'); + expect(searchBlocklists).toHaveProperty('deletedAt'); + expect(searchBlocklists).toHaveProperty('workspaceMemberId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-channel-event-associations.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-channel-event-associations.integration-spec.ts new file mode 100644 index 000000000000..749ed8c12df1 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-channel-event-associations.integration-spec.ts @@ -0,0 +1,73 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchCalendarChannelEventAssociationsResolver (e2e)', () => { + it('should find many searchCalendarChannelEventAssociations', () => { + const queryData = { + query: ` + query searchCalendarChannelEventAssociations { + searchCalendarChannelEventAssociations { + edges { + node { + eventExternalId + recurringEventExternalId + id + createdAt + updatedAt + deletedAt + calendarChannelId + calendarEventId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchCalendarChannelEventAssociations; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchCalendarChannelEventAssociations = edges[0].node; + + expect(searchCalendarChannelEventAssociations).toHaveProperty( + 'eventExternalId', + ); + expect(searchCalendarChannelEventAssociations).toHaveProperty( + 'recurringEventExternalId', + ); + expect(searchCalendarChannelEventAssociations).toHaveProperty('id'); + expect(searchCalendarChannelEventAssociations).toHaveProperty( + 'createdAt', + ); + expect(searchCalendarChannelEventAssociations).toHaveProperty( + 'updatedAt', + ); + expect(searchCalendarChannelEventAssociations).toHaveProperty( + 'deletedAt', + ); + expect(searchCalendarChannelEventAssociations).toHaveProperty( + 'calendarChannelId', + ); + expect(searchCalendarChannelEventAssociations).toHaveProperty( + 'calendarEventId', + ); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-channels.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-channels.integration-spec.ts new file mode 100644 index 000000000000..28196dce8066 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-channels.integration-spec.ts @@ -0,0 +1,79 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchCalendarChannelsResolver (e2e)', () => { + it('should find many searchCalendarChannels', () => { + const queryData = { + query: ` + query searchCalendarChannels { + searchCalendarChannels { + edges { + node { + handle + syncStatus + syncStage + visibility + isContactAutoCreationEnabled + contactAutoCreationPolicy + isSyncEnabled + syncCursor + syncedAt + syncStageStartedAt + throttleFailureCount + id + createdAt + updatedAt + deletedAt + connectedAccountId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchCalendarChannels; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchCalendarChannels = edges[0].node; + + expect(searchCalendarChannels).toHaveProperty('handle'); + expect(searchCalendarChannels).toHaveProperty('syncStatus'); + expect(searchCalendarChannels).toHaveProperty('syncStage'); + expect(searchCalendarChannels).toHaveProperty('visibility'); + expect(searchCalendarChannels).toHaveProperty( + 'isContactAutoCreationEnabled', + ); + expect(searchCalendarChannels).toHaveProperty( + 'contactAutoCreationPolicy', + ); + expect(searchCalendarChannels).toHaveProperty('isSyncEnabled'); + expect(searchCalendarChannels).toHaveProperty('syncCursor'); + expect(searchCalendarChannels).toHaveProperty('syncedAt'); + expect(searchCalendarChannels).toHaveProperty('syncStageStartedAt'); + expect(searchCalendarChannels).toHaveProperty('throttleFailureCount'); + expect(searchCalendarChannels).toHaveProperty('id'); + expect(searchCalendarChannels).toHaveProperty('createdAt'); + expect(searchCalendarChannels).toHaveProperty('updatedAt'); + expect(searchCalendarChannels).toHaveProperty('deletedAt'); + expect(searchCalendarChannels).toHaveProperty('connectedAccountId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-event-participants.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-event-participants.integration-spec.ts new file mode 100644 index 000000000000..b72c8aeaa4b5 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-event-participants.integration-spec.ts @@ -0,0 +1,71 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchCalendarEventParticipantsResolver (e2e)', () => { + it('should find many searchCalendarEventParticipants', () => { + const queryData = { + query: ` + query searchCalendarEventParticipants { + searchCalendarEventParticipants { + edges { + node { + handle + displayName + isOrganizer + responseStatus + id + createdAt + updatedAt + deletedAt + calendarEventId + personId + workspaceMemberId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchCalendarEventParticipants; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchCalendarEventParticipants = edges[0].node; + + expect(searchCalendarEventParticipants).toHaveProperty('handle'); + expect(searchCalendarEventParticipants).toHaveProperty('displayName'); + expect(searchCalendarEventParticipants).toHaveProperty('isOrganizer'); + expect(searchCalendarEventParticipants).toHaveProperty( + 'responseStatus', + ); + expect(searchCalendarEventParticipants).toHaveProperty('id'); + expect(searchCalendarEventParticipants).toHaveProperty('createdAt'); + expect(searchCalendarEventParticipants).toHaveProperty('updatedAt'); + expect(searchCalendarEventParticipants).toHaveProperty('deletedAt'); + expect(searchCalendarEventParticipants).toHaveProperty( + 'calendarEventId', + ); + expect(searchCalendarEventParticipants).toHaveProperty('personId'); + expect(searchCalendarEventParticipants).toHaveProperty( + 'workspaceMemberId', + ); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-events.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-events.integration-spec.ts new file mode 100644 index 000000000000..76ef636a0d4a --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-calendar-events.integration-spec.ts @@ -0,0 +1,73 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchCalendarEventsResolver (e2e)', () => { + it('should find many searchCalendarEvents', () => { + const queryData = { + query: ` + query searchCalendarEvents { + searchCalendarEvents { + edges { + node { + title + isCanceled + isFullDay + startsAt + endsAt + externalCreatedAt + externalUpdatedAt + description + location + iCalUID + conferenceSolution + id + createdAt + updatedAt + deletedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchCalendarEvents; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchCalendarEvents = edges[0].node; + + expect(searchCalendarEvents).toHaveProperty('title'); + expect(searchCalendarEvents).toHaveProperty('isCanceled'); + expect(searchCalendarEvents).toHaveProperty('isFullDay'); + expect(searchCalendarEvents).toHaveProperty('startsAt'); + expect(searchCalendarEvents).toHaveProperty('endsAt'); + expect(searchCalendarEvents).toHaveProperty('externalCreatedAt'); + expect(searchCalendarEvents).toHaveProperty('externalUpdatedAt'); + expect(searchCalendarEvents).toHaveProperty('description'); + expect(searchCalendarEvents).toHaveProperty('location'); + expect(searchCalendarEvents).toHaveProperty('iCalUID'); + expect(searchCalendarEvents).toHaveProperty('conferenceSolution'); + expect(searchCalendarEvents).toHaveProperty('id'); + expect(searchCalendarEvents).toHaveProperty('createdAt'); + expect(searchCalendarEvents).toHaveProperty('updatedAt'); + expect(searchCalendarEvents).toHaveProperty('deletedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-comments.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-comments.integration-spec.ts new file mode 100644 index 000000000000..549f1d3011d1 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-comments.integration-spec.ts @@ -0,0 +1,57 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchCommentsResolver (e2e)', () => { + it('should find many searchComments', () => { + const queryData = { + query: ` + query searchComments { + searchComments { + edges { + node { + body + id + createdAt + updatedAt + deletedAt + authorId + activityId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchComments; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchComments = edges[0].node; + + expect(searchComments).toHaveProperty('body'); + expect(searchComments).toHaveProperty('id'); + expect(searchComments).toHaveProperty('createdAt'); + expect(searchComments).toHaveProperty('updatedAt'); + expect(searchComments).toHaveProperty('deletedAt'); + expect(searchComments).toHaveProperty('authorId'); + expect(searchComments).toHaveProperty('activityId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-companies.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-companies.integration-spec.ts new file mode 100644 index 000000000000..da309385f76f --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-companies.integration-spec.ts @@ -0,0 +1,69 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchCompaniesResolver (e2e)', () => { + it('should find many searchCompanies', () => { + const queryData = { + query: ` + query searchCompanies { + searchCompanies { + edges { + node { + name + employees + idealCustomerProfile + position + searchVector + id + createdAt + updatedAt + deletedAt + accountOwnerId + tagline + workPolicy + visaSponsorship + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchCompanies; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchCompanies = edges[0].node; + + expect(searchCompanies).toHaveProperty('name'); + expect(searchCompanies).toHaveProperty('employees'); + expect(searchCompanies).toHaveProperty('idealCustomerProfile'); + expect(searchCompanies).toHaveProperty('position'); + expect(searchCompanies).toHaveProperty('searchVector'); + expect(searchCompanies).toHaveProperty('id'); + expect(searchCompanies).toHaveProperty('createdAt'); + expect(searchCompanies).toHaveProperty('updatedAt'); + expect(searchCompanies).toHaveProperty('deletedAt'); + expect(searchCompanies).toHaveProperty('accountOwnerId'); + expect(searchCompanies).toHaveProperty('tagline'); + expect(searchCompanies).toHaveProperty('workPolicy'); + expect(searchCompanies).toHaveProperty('visaSponsorship'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-connected-accounts.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-connected-accounts.integration-spec.ts new file mode 100644 index 000000000000..d00c81ecb1a9 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-connected-accounts.integration-spec.ts @@ -0,0 +1,69 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchConnectedAccountsResolver (e2e)', () => { + it('should find many searchConnectedAccounts', () => { + const queryData = { + query: ` + query searchConnectedAccounts { + searchConnectedAccounts { + edges { + node { + handle + provider + accessToken + refreshToken + lastSyncHistoryId + authFailedAt + handleAliases + scopes + id + createdAt + updatedAt + deletedAt + accountOwnerId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchConnectedAccounts; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchConnectedAccounts = edges[0].node; + + expect(searchConnectedAccounts).toHaveProperty('handle'); + expect(searchConnectedAccounts).toHaveProperty('provider'); + expect(searchConnectedAccounts).toHaveProperty('accessToken'); + expect(searchConnectedAccounts).toHaveProperty('refreshToken'); + expect(searchConnectedAccounts).toHaveProperty('lastSyncHistoryId'); + expect(searchConnectedAccounts).toHaveProperty('authFailedAt'); + expect(searchConnectedAccounts).toHaveProperty('handleAliases'); + expect(searchConnectedAccounts).toHaveProperty('scopes'); + expect(searchConnectedAccounts).toHaveProperty('id'); + expect(searchConnectedAccounts).toHaveProperty('createdAt'); + expect(searchConnectedAccounts).toHaveProperty('updatedAt'); + expect(searchConnectedAccounts).toHaveProperty('deletedAt'); + expect(searchConnectedAccounts).toHaveProperty('accountOwnerId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-favorites.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-favorites.integration-spec.ts new file mode 100644 index 000000000000..982aff72675a --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-favorites.integration-spec.ts @@ -0,0 +1,75 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchFavoritesResolver (e2e)', () => { + it('should find many searchFavorites', () => { + const queryData = { + query: ` + query searchFavorites { + searchFavorites { + edges { + node { + position + id + createdAt + updatedAt + deletedAt + workspaceMemberId + personId + companyId + opportunityId + workflowId + workflowVersionId + workflowRunId + taskId + noteId + viewId + rocketId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchFavorites; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchFavorites = edges[0].node; + + expect(searchFavorites).toHaveProperty('position'); + expect(searchFavorites).toHaveProperty('id'); + expect(searchFavorites).toHaveProperty('createdAt'); + expect(searchFavorites).toHaveProperty('updatedAt'); + expect(searchFavorites).toHaveProperty('deletedAt'); + expect(searchFavorites).toHaveProperty('workspaceMemberId'); + expect(searchFavorites).toHaveProperty('personId'); + expect(searchFavorites).toHaveProperty('companyId'); + expect(searchFavorites).toHaveProperty('opportunityId'); + expect(searchFavorites).toHaveProperty('workflowId'); + expect(searchFavorites).toHaveProperty('workflowVersionId'); + expect(searchFavorites).toHaveProperty('workflowRunId'); + expect(searchFavorites).toHaveProperty('taskId'); + expect(searchFavorites).toHaveProperty('noteId'); + expect(searchFavorites).toHaveProperty('viewId'); + expect(searchFavorites).toHaveProperty('rocketId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-channel-message-associations.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-channel-message-associations.integration-spec.ts new file mode 100644 index 000000000000..514b67bb32a3 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-channel-message-associations.integration-spec.ts @@ -0,0 +1,77 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchMessageChannelMessageAssociationsResolver (e2e)', () => { + it('should find many searchMessageChannelMessageAssociations', () => { + const queryData = { + query: ` + query searchMessageChannelMessageAssociations { + searchMessageChannelMessageAssociations { + edges { + node { + messageExternalId + messageThreadExternalId + direction + id + createdAt + updatedAt + deletedAt + messageChannelId + messageId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchMessageChannelMessageAssociations; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchMessageChannelMessageAssociations = edges[0].node; + + expect(searchMessageChannelMessageAssociations).toHaveProperty( + 'messageExternalId', + ); + expect(searchMessageChannelMessageAssociations).toHaveProperty( + 'messageThreadExternalId', + ); + expect(searchMessageChannelMessageAssociations).toHaveProperty( + 'direction', + ); + expect(searchMessageChannelMessageAssociations).toHaveProperty('id'); + expect(searchMessageChannelMessageAssociations).toHaveProperty( + 'createdAt', + ); + expect(searchMessageChannelMessageAssociations).toHaveProperty( + 'updatedAt', + ); + expect(searchMessageChannelMessageAssociations).toHaveProperty( + 'deletedAt', + ); + expect(searchMessageChannelMessageAssociations).toHaveProperty( + 'messageChannelId', + ); + expect(searchMessageChannelMessageAssociations).toHaveProperty( + 'messageId', + ); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-channels.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-channels.integration-spec.ts new file mode 100644 index 000000000000..c39ccae7c5de --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-channels.integration-spec.ts @@ -0,0 +1,87 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchMessageChannelsResolver (e2e)', () => { + it('should find many searchMessageChannels', () => { + const queryData = { + query: ` + query searchMessageChannels { + searchMessageChannels { + edges { + node { + visibility + handle + type + isContactAutoCreationEnabled + contactAutoCreationPolicy + excludeNonProfessionalEmails + excludeGroupEmails + isSyncEnabled + syncCursor + syncedAt + syncStatus + syncStage + syncStageStartedAt + throttleFailureCount + id + createdAt + updatedAt + deletedAt + connectedAccountId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchMessageChannels; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchMessageChannels = edges[0].node; + + expect(searchMessageChannels).toHaveProperty('visibility'); + expect(searchMessageChannels).toHaveProperty('handle'); + expect(searchMessageChannels).toHaveProperty('type'); + expect(searchMessageChannels).toHaveProperty( + 'isContactAutoCreationEnabled', + ); + expect(searchMessageChannels).toHaveProperty( + 'contactAutoCreationPolicy', + ); + expect(searchMessageChannels).toHaveProperty( + 'excludeNonProfessionalEmails', + ); + expect(searchMessageChannels).toHaveProperty('excludeGroupEmails'); + expect(searchMessageChannels).toHaveProperty('isSyncEnabled'); + expect(searchMessageChannels).toHaveProperty('syncCursor'); + expect(searchMessageChannels).toHaveProperty('syncedAt'); + expect(searchMessageChannels).toHaveProperty('syncStatus'); + expect(searchMessageChannels).toHaveProperty('syncStage'); + expect(searchMessageChannels).toHaveProperty('syncStageStartedAt'); + expect(searchMessageChannels).toHaveProperty('throttleFailureCount'); + expect(searchMessageChannels).toHaveProperty('id'); + expect(searchMessageChannels).toHaveProperty('createdAt'); + expect(searchMessageChannels).toHaveProperty('updatedAt'); + expect(searchMessageChannels).toHaveProperty('deletedAt'); + expect(searchMessageChannels).toHaveProperty('connectedAccountId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-participants.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-participants.integration-spec.ts new file mode 100644 index 000000000000..71b9ee486251 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-participants.integration-spec.ts @@ -0,0 +1,63 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchMessageParticipantsResolver (e2e)', () => { + it('should find many searchMessageParticipants', () => { + const queryData = { + query: ` + query searchMessageParticipants { + searchMessageParticipants { + edges { + node { + role + handle + displayName + id + createdAt + updatedAt + deletedAt + messageId + personId + workspaceMemberId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchMessageParticipants; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchMessageParticipants = edges[0].node; + + expect(searchMessageParticipants).toHaveProperty('role'); + expect(searchMessageParticipants).toHaveProperty('handle'); + expect(searchMessageParticipants).toHaveProperty('displayName'); + expect(searchMessageParticipants).toHaveProperty('id'); + expect(searchMessageParticipants).toHaveProperty('createdAt'); + expect(searchMessageParticipants).toHaveProperty('updatedAt'); + expect(searchMessageParticipants).toHaveProperty('deletedAt'); + expect(searchMessageParticipants).toHaveProperty('messageId'); + expect(searchMessageParticipants).toHaveProperty('personId'); + expect(searchMessageParticipants).toHaveProperty('workspaceMemberId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-threads.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-threads.integration-spec.ts new file mode 100644 index 000000000000..5c38ebfeb441 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-message-threads.integration-spec.ts @@ -0,0 +1,51 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchMessageThreadsResolver (e2e)', () => { + it('should find many searchMessageThreads', () => { + const queryData = { + query: ` + query searchMessageThreads { + searchMessageThreads { + edges { + node { + id + createdAt + updatedAt + deletedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchMessageThreads; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchMessageThreads = edges[0].node; + + expect(searchMessageThreads).toHaveProperty('id'); + expect(searchMessageThreads).toHaveProperty('createdAt'); + expect(searchMessageThreads).toHaveProperty('updatedAt'); + expect(searchMessageThreads).toHaveProperty('deletedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-messages.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-messages.integration-spec.ts new file mode 100644 index 000000000000..4865fb8d312b --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-messages.integration-spec.ts @@ -0,0 +1,61 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchMessagesResolver (e2e)', () => { + it('should find many searchMessages', () => { + const queryData = { + query: ` + query searchMessages { + searchMessages { + edges { + node { + headerMessageId + subject + text + receivedAt + id + createdAt + updatedAt + deletedAt + messageThreadId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchMessages; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchMessages = edges[0].node; + + expect(searchMessages).toHaveProperty('headerMessageId'); + expect(searchMessages).toHaveProperty('subject'); + expect(searchMessages).toHaveProperty('text'); + expect(searchMessages).toHaveProperty('receivedAt'); + expect(searchMessages).toHaveProperty('id'); + expect(searchMessages).toHaveProperty('createdAt'); + expect(searchMessages).toHaveProperty('updatedAt'); + expect(searchMessages).toHaveProperty('deletedAt'); + expect(searchMessages).toHaveProperty('messageThreadId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-note-targets.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-note-targets.integration-spec.ts new file mode 100644 index 000000000000..45188e8aca7c --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-note-targets.integration-spec.ts @@ -0,0 +1,61 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchNoteTargetsResolver (e2e)', () => { + it('should find many searchNoteTargets', () => { + const queryData = { + query: ` + query searchNoteTargets { + searchNoteTargets { + edges { + node { + id + createdAt + updatedAt + deletedAt + noteId + personId + companyId + opportunityId + rocketId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchNoteTargets; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchNoteTargets = edges[0].node; + + expect(searchNoteTargets).toHaveProperty('id'); + expect(searchNoteTargets).toHaveProperty('createdAt'); + expect(searchNoteTargets).toHaveProperty('updatedAt'); + expect(searchNoteTargets).toHaveProperty('deletedAt'); + expect(searchNoteTargets).toHaveProperty('noteId'); + expect(searchNoteTargets).toHaveProperty('personId'); + expect(searchNoteTargets).toHaveProperty('companyId'); + expect(searchNoteTargets).toHaveProperty('opportunityId'); + expect(searchNoteTargets).toHaveProperty('rocketId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-notes.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-notes.integration-spec.ts new file mode 100644 index 000000000000..8965c5006f26 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-notes.integration-spec.ts @@ -0,0 +1,57 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchNotesResolver (e2e)', () => { + it('should find many searchNotes', () => { + const queryData = { + query: ` + query searchNotes { + searchNotes { + edges { + node { + position + title + body + id + createdAt + updatedAt + deletedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchNotes; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchNotes = edges[0].node; + + expect(searchNotes).toHaveProperty('position'); + expect(searchNotes).toHaveProperty('title'); + expect(searchNotes).toHaveProperty('body'); + expect(searchNotes).toHaveProperty('id'); + expect(searchNotes).toHaveProperty('createdAt'); + expect(searchNotes).toHaveProperty('updatedAt'); + expect(searchNotes).toHaveProperty('deletedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-opportunities.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-opportunities.integration-spec.ts new file mode 100644 index 000000000000..0f63d73d7175 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-opportunities.integration-spec.ts @@ -0,0 +1,65 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchOpportunitiesResolver (e2e)', () => { + it('should find many searchOpportunities', () => { + const queryData = { + query: ` + query searchOpportunities { + searchOpportunities { + edges { + node { + name + closeDate + stage + position + searchVector + id + createdAt + updatedAt + deletedAt + pointOfContactId + companyId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchOpportunities; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchOpportunities = edges[0].node; + + expect(searchOpportunities).toHaveProperty('name'); + expect(searchOpportunities).toHaveProperty('closeDate'); + expect(searchOpportunities).toHaveProperty('stage'); + expect(searchOpportunities).toHaveProperty('position'); + expect(searchOpportunities).toHaveProperty('searchVector'); + expect(searchOpportunities).toHaveProperty('id'); + expect(searchOpportunities).toHaveProperty('createdAt'); + expect(searchOpportunities).toHaveProperty('updatedAt'); + expect(searchOpportunities).toHaveProperty('deletedAt'); + expect(searchOpportunities).toHaveProperty('pointOfContactId'); + expect(searchOpportunities).toHaveProperty('companyId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-people.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-people.integration-spec.ts new file mode 100644 index 000000000000..8c45c0c7e29b --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-people.integration-spec.ts @@ -0,0 +1,69 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchPeopleResolver (e2e)', () => { + it('should find many searchPeople', () => { + const queryData = { + query: ` + query searchPeople { + searchPeople { + edges { + node { + jobTitle + city + avatarUrl + position + searchVector + id + createdAt + updatedAt + deletedAt + companyId + intro + workPreference + performanceRating + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchPeople; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchPeople = edges[0].node; + + expect(searchPeople).toHaveProperty('jobTitle'); + expect(searchPeople).toHaveProperty('city'); + expect(searchPeople).toHaveProperty('avatarUrl'); + expect(searchPeople).toHaveProperty('position'); + expect(searchPeople).toHaveProperty('searchVector'); + expect(searchPeople).toHaveProperty('id'); + expect(searchPeople).toHaveProperty('createdAt'); + expect(searchPeople).toHaveProperty('updatedAt'); + expect(searchPeople).toHaveProperty('deletedAt'); + expect(searchPeople).toHaveProperty('companyId'); + expect(searchPeople).toHaveProperty('intro'); + expect(searchPeople).toHaveProperty('workPreference'); + expect(searchPeople).toHaveProperty('performanceRating'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-rockets.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-rockets.integration-spec.ts new file mode 100644 index 000000000000..1bf738515746 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-rockets.integration-spec.ts @@ -0,0 +1,57 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchRocketsResolver (e2e)', () => { + it('should find many searchRockets', () => { + const queryData = { + query: ` + query searchRockets { + searchRockets { + edges { + node { + id + name + createdAt + updatedAt + deletedAt + position + searchVector + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchRockets; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchRockets = edges[0].node; + + expect(searchRockets).toHaveProperty('id'); + expect(searchRockets).toHaveProperty('name'); + expect(searchRockets).toHaveProperty('createdAt'); + expect(searchRockets).toHaveProperty('updatedAt'); + expect(searchRockets).toHaveProperty('deletedAt'); + expect(searchRockets).toHaveProperty('position'); + expect(searchRockets).toHaveProperty('searchVector'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-task-targets.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-task-targets.integration-spec.ts new file mode 100644 index 000000000000..76f9d7b1ec92 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-task-targets.integration-spec.ts @@ -0,0 +1,61 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchTaskTargetsResolver (e2e)', () => { + it('should find many searchTaskTargets', () => { + const queryData = { + query: ` + query searchTaskTargets { + searchTaskTargets { + edges { + node { + id + createdAt + updatedAt + deletedAt + taskId + personId + companyId + opportunityId + rocketId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchTaskTargets; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchTaskTargets = edges[0].node; + + expect(searchTaskTargets).toHaveProperty('id'); + expect(searchTaskTargets).toHaveProperty('createdAt'); + expect(searchTaskTargets).toHaveProperty('updatedAt'); + expect(searchTaskTargets).toHaveProperty('deletedAt'); + expect(searchTaskTargets).toHaveProperty('taskId'); + expect(searchTaskTargets).toHaveProperty('personId'); + expect(searchTaskTargets).toHaveProperty('companyId'); + expect(searchTaskTargets).toHaveProperty('opportunityId'); + expect(searchTaskTargets).toHaveProperty('rocketId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-tasks.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-tasks.integration-spec.ts new file mode 100644 index 000000000000..d9af7a1c6a42 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-tasks.integration-spec.ts @@ -0,0 +1,63 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchTasksResolver (e2e)', () => { + it('should find many searchTasks', () => { + const queryData = { + query: ` + query searchTasks { + searchTasks { + edges { + node { + position + title + body + dueAt + status + id + createdAt + updatedAt + deletedAt + assigneeId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchTasks; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchTasks = edges[0].node; + + expect(searchTasks).toHaveProperty('position'); + expect(searchTasks).toHaveProperty('title'); + expect(searchTasks).toHaveProperty('body'); + expect(searchTasks).toHaveProperty('dueAt'); + expect(searchTasks).toHaveProperty('status'); + expect(searchTasks).toHaveProperty('id'); + expect(searchTasks).toHaveProperty('createdAt'); + expect(searchTasks).toHaveProperty('updatedAt'); + expect(searchTasks).toHaveProperty('deletedAt'); + expect(searchTasks).toHaveProperty('assigneeId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-timeline-activities.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-timeline-activities.integration-spec.ts new file mode 100644 index 000000000000..b87ec60f1dd4 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-timeline-activities.integration-spec.ts @@ -0,0 +1,87 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchTimelineActivitiesResolver (e2e)', () => { + it('should find many searchTimelineActivities', () => { + const queryData = { + query: ` + query searchTimelineActivities { + searchTimelineActivities { + edges { + node { + happensAt + name + properties + linkedRecordCachedName + linkedRecordId + linkedObjectMetadataId + id + createdAt + updatedAt + deletedAt + workspaceMemberId + personId + companyId + opportunityId + noteId + taskId + workflowId + workflowVersionId + workflowRunId + rocketId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchTimelineActivities; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchTimelineActivities = edges[0].node; + + expect(searchTimelineActivities).toHaveProperty('happensAt'); + expect(searchTimelineActivities).toHaveProperty('name'); + expect(searchTimelineActivities).toHaveProperty('properties'); + expect(searchTimelineActivities).toHaveProperty( + 'linkedRecordCachedName', + ); + expect(searchTimelineActivities).toHaveProperty('linkedRecordId'); + expect(searchTimelineActivities).toHaveProperty( + 'linkedObjectMetadataId', + ); + expect(searchTimelineActivities).toHaveProperty('id'); + expect(searchTimelineActivities).toHaveProperty('createdAt'); + expect(searchTimelineActivities).toHaveProperty('updatedAt'); + expect(searchTimelineActivities).toHaveProperty('deletedAt'); + expect(searchTimelineActivities).toHaveProperty('workspaceMemberId'); + expect(searchTimelineActivities).toHaveProperty('personId'); + expect(searchTimelineActivities).toHaveProperty('companyId'); + expect(searchTimelineActivities).toHaveProperty('opportunityId'); + expect(searchTimelineActivities).toHaveProperty('noteId'); + expect(searchTimelineActivities).toHaveProperty('taskId'); + expect(searchTimelineActivities).toHaveProperty('workflowId'); + expect(searchTimelineActivities).toHaveProperty('workflowVersionId'); + expect(searchTimelineActivities).toHaveProperty('workflowRunId'); + expect(searchTimelineActivities).toHaveProperty('rocketId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-fields.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-fields.integration-spec.ts new file mode 100644 index 000000000000..b84def1a813c --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-fields.integration-spec.ts @@ -0,0 +1,61 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchViewFieldsResolver (e2e)', () => { + it('should find many searchViewFields', () => { + const queryData = { + query: ` + query searchViewFields { + searchViewFields { + edges { + node { + fieldMetadataId + isVisible + size + position + id + createdAt + updatedAt + deletedAt + viewId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchViewFields; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchViewFields = edges[0].node; + + expect(searchViewFields).toHaveProperty('fieldMetadataId'); + expect(searchViewFields).toHaveProperty('isVisible'); + expect(searchViewFields).toHaveProperty('size'); + expect(searchViewFields).toHaveProperty('position'); + expect(searchViewFields).toHaveProperty('id'); + expect(searchViewFields).toHaveProperty('createdAt'); + expect(searchViewFields).toHaveProperty('updatedAt'); + expect(searchViewFields).toHaveProperty('deletedAt'); + expect(searchViewFields).toHaveProperty('viewId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-filters.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-filters.integration-spec.ts new file mode 100644 index 000000000000..fe1e96c4b63c --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-filters.integration-spec.ts @@ -0,0 +1,61 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchViewFiltersResolver (e2e)', () => { + it('should find many searchViewFilters', () => { + const queryData = { + query: ` + query searchViewFilters { + searchViewFilters { + edges { + node { + fieldMetadataId + operand + value + displayValue + id + createdAt + updatedAt + deletedAt + viewId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchViewFilters; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchViewFilters = edges[0].node; + + expect(searchViewFilters).toHaveProperty('fieldMetadataId'); + expect(searchViewFilters).toHaveProperty('operand'); + expect(searchViewFilters).toHaveProperty('value'); + expect(searchViewFilters).toHaveProperty('displayValue'); + expect(searchViewFilters).toHaveProperty('id'); + expect(searchViewFilters).toHaveProperty('createdAt'); + expect(searchViewFilters).toHaveProperty('updatedAt'); + expect(searchViewFilters).toHaveProperty('deletedAt'); + expect(searchViewFilters).toHaveProperty('viewId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-sorts.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-sorts.integration-spec.ts new file mode 100644 index 000000000000..7f5b2e6f6a91 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-view-sorts.integration-spec.ts @@ -0,0 +1,57 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchViewSortsResolver (e2e)', () => { + it('should find many searchViewSorts', () => { + const queryData = { + query: ` + query searchViewSorts { + searchViewSorts { + edges { + node { + fieldMetadataId + direction + id + createdAt + updatedAt + deletedAt + viewId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchViewSorts; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchViewSorts = edges[0].node; + + expect(searchViewSorts).toHaveProperty('fieldMetadataId'); + expect(searchViewSorts).toHaveProperty('direction'); + expect(searchViewSorts).toHaveProperty('id'); + expect(searchViewSorts).toHaveProperty('createdAt'); + expect(searchViewSorts).toHaveProperty('updatedAt'); + expect(searchViewSorts).toHaveProperty('deletedAt'); + expect(searchViewSorts).toHaveProperty('viewId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-views.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-views.integration-spec.ts new file mode 100644 index 000000000000..716c4a843301 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-views.integration-spec.ts @@ -0,0 +1,67 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchViewsResolver (e2e)', () => { + it('should find many searchViews', () => { + const queryData = { + query: ` + query searchViews { + searchViews { + edges { + node { + name + objectMetadataId + type + key + icon + kanbanFieldMetadataId + position + isCompact + id + createdAt + updatedAt + deletedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchViews; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchViews = edges[0].node; + + expect(searchViews).toHaveProperty('name'); + expect(searchViews).toHaveProperty('objectMetadataId'); + expect(searchViews).toHaveProperty('type'); + expect(searchViews).toHaveProperty('key'); + expect(searchViews).toHaveProperty('icon'); + expect(searchViews).toHaveProperty('kanbanFieldMetadataId'); + expect(searchViews).toHaveProperty('position'); + expect(searchViews).toHaveProperty('isCompact'); + expect(searchViews).toHaveProperty('id'); + expect(searchViews).toHaveProperty('createdAt'); + expect(searchViews).toHaveProperty('updatedAt'); + expect(searchViews).toHaveProperty('deletedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-webhooks.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-webhooks.integration-spec.ts new file mode 100644 index 000000000000..d5a93db25ec0 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-webhooks.integration-spec.ts @@ -0,0 +1,57 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchWebhooksResolver (e2e)', () => { + it('should find many searchWebhooks', () => { + const queryData = { + query: ` + query searchWebhooks { + searchWebhooks { + edges { + node { + id + targetUrl + operation + description + createdAt + updatedAt + deletedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchWebhooks; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchWebhooks = edges[0].node; + + expect(searchWebhooks).toHaveProperty('id'); + expect(searchWebhooks).toHaveProperty('targetUrl'); + expect(searchWebhooks).toHaveProperty('operation'); + expect(searchWebhooks).toHaveProperty('description'); + expect(searchWebhooks).toHaveProperty('createdAt'); + expect(searchWebhooks).toHaveProperty('updatedAt'); + expect(searchWebhooks).toHaveProperty('deletedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-event-listeners.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-event-listeners.integration-spec.ts new file mode 100644 index 000000000000..ddf55a1a493c --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-event-listeners.integration-spec.ts @@ -0,0 +1,55 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchWorkflowEventListenersResolver (e2e)', () => { + it('should find many searchWorkflowEventListeners', () => { + const queryData = { + query: ` + query searchWorkflowEventListeners { + searchWorkflowEventListeners { + edges { + node { + eventName + id + createdAt + updatedAt + deletedAt + workflowId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchWorkflowEventListeners; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchWorkflowEventListeners = edges[0].node; + + expect(searchWorkflowEventListeners).toHaveProperty('eventName'); + expect(searchWorkflowEventListeners).toHaveProperty('id'); + expect(searchWorkflowEventListeners).toHaveProperty('createdAt'); + expect(searchWorkflowEventListeners).toHaveProperty('updatedAt'); + expect(searchWorkflowEventListeners).toHaveProperty('deletedAt'); + expect(searchWorkflowEventListeners).toHaveProperty('workflowId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-runs.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-runs.integration-spec.ts new file mode 100644 index 000000000000..6307d8ae6bbd --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-runs.integration-spec.ts @@ -0,0 +1,69 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchWorkflowRunsResolver (e2e)', () => { + it('should find many searchWorkflowRuns', () => { + const queryData = { + query: ` + query searchWorkflowRuns { + searchWorkflowRuns { + edges { + node { + workflowRunId + name + startedAt + endedAt + status + output + position + id + createdAt + updatedAt + deletedAt + workflowVersionId + workflowId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchWorkflowRuns; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchWorkflowRuns = edges[0].node; + + expect(searchWorkflowRuns).toHaveProperty('workflowRunId'); + expect(searchWorkflowRuns).toHaveProperty('name'); + expect(searchWorkflowRuns).toHaveProperty('startedAt'); + expect(searchWorkflowRuns).toHaveProperty('endedAt'); + expect(searchWorkflowRuns).toHaveProperty('status'); + expect(searchWorkflowRuns).toHaveProperty('output'); + expect(searchWorkflowRuns).toHaveProperty('position'); + expect(searchWorkflowRuns).toHaveProperty('id'); + expect(searchWorkflowRuns).toHaveProperty('createdAt'); + expect(searchWorkflowRuns).toHaveProperty('updatedAt'); + expect(searchWorkflowRuns).toHaveProperty('deletedAt'); + expect(searchWorkflowRuns).toHaveProperty('workflowVersionId'); + expect(searchWorkflowRuns).toHaveProperty('workflowId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-versions.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-versions.integration-spec.ts new file mode 100644 index 000000000000..86bd6df8099c --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflow-versions.integration-spec.ts @@ -0,0 +1,63 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchWorkflowVersionsResolver (e2e)', () => { + it('should find many searchWorkflowVersions', () => { + const queryData = { + query: ` + query searchWorkflowVersions { + searchWorkflowVersions { + edges { + node { + name + trigger + steps + status + position + id + createdAt + updatedAt + deletedAt + workflowId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchWorkflowVersions; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchWorkflowVersions = edges[0].node; + + expect(searchWorkflowVersions).toHaveProperty('name'); + expect(searchWorkflowVersions).toHaveProperty('trigger'); + expect(searchWorkflowVersions).toHaveProperty('steps'); + expect(searchWorkflowVersions).toHaveProperty('status'); + expect(searchWorkflowVersions).toHaveProperty('position'); + expect(searchWorkflowVersions).toHaveProperty('id'); + expect(searchWorkflowVersions).toHaveProperty('createdAt'); + expect(searchWorkflowVersions).toHaveProperty('updatedAt'); + expect(searchWorkflowVersions).toHaveProperty('deletedAt'); + expect(searchWorkflowVersions).toHaveProperty('workflowId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflows.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflows.integration-spec.ts new file mode 100644 index 000000000000..b12d780dfeb5 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workflows.integration-spec.ts @@ -0,0 +1,59 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchWorkflowsResolver (e2e)', () => { + it('should find many searchWorkflows', () => { + const queryData = { + query: ` + query searchWorkflows { + searchWorkflows { + edges { + node { + name + lastPublishedVersionId + statuses + position + id + createdAt + updatedAt + deletedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchWorkflows; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchWorkflows = edges[0].node; + + expect(searchWorkflows).toHaveProperty('name'); + expect(searchWorkflows).toHaveProperty('lastPublishedVersionId'); + expect(searchWorkflows).toHaveProperty('statuses'); + expect(searchWorkflows).toHaveProperty('position'); + expect(searchWorkflows).toHaveProperty('id'); + expect(searchWorkflows).toHaveProperty('createdAt'); + expect(searchWorkflows).toHaveProperty('updatedAt'); + expect(searchWorkflows).toHaveProperty('deletedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workspace-members.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workspace-members.integration-spec.ts new file mode 100644 index 000000000000..efc76b40438a --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/search-workspace-members.integration-spec.ts @@ -0,0 +1,67 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('searchWorkspaceMembersResolver (e2e)', () => { + it('should find many searchWorkspaceMembers', () => { + const queryData = { + query: ` + query searchWorkspaceMembers { + searchWorkspaceMembers { + edges { + node { + id + colorScheme + avatarUrl + locale + timeZone + dateFormat + timeFormat + userEmail + userId + createdAt + updatedAt + deletedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.searchWorkspaceMembers; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const searchWorkspaceMembers = edges[0].node; + + expect(searchWorkspaceMembers).toHaveProperty('id'); + expect(searchWorkspaceMembers).toHaveProperty('colorScheme'); + expect(searchWorkspaceMembers).toHaveProperty('avatarUrl'); + expect(searchWorkspaceMembers).toHaveProperty('locale'); + expect(searchWorkspaceMembers).toHaveProperty('timeZone'); + expect(searchWorkspaceMembers).toHaveProperty('dateFormat'); + expect(searchWorkspaceMembers).toHaveProperty('timeFormat'); + expect(searchWorkspaceMembers).toHaveProperty('userEmail'); + expect(searchWorkspaceMembers).toHaveProperty('userId'); + expect(searchWorkspaceMembers).toHaveProperty('createdAt'); + expect(searchWorkspaceMembers).toHaveProperty('updatedAt'); + expect(searchWorkspaceMembers).toHaveProperty('deletedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/serverless-functions.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/serverless-functions.integration-spec.ts new file mode 100644 index 000000000000..9e8d50619e0d --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/serverless-functions.integration-spec.ts @@ -0,0 +1,59 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('serverlessFunctionsResolver (e2e)', () => { + it('should find many serverlessFunctions', () => { + const queryData = { + query: ` + query serverlessFunctions { + serverlessFunctions { + edges { + node { + id + name + description + runtime + latestVersion + syncStatus + createdAt + updatedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.serverlessFunctions; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const serverlessFunctions = edges[0].node; + + expect(serverlessFunctions).toHaveProperty('id'); + expect(serverlessFunctions).toHaveProperty('name'); + expect(serverlessFunctions).toHaveProperty('description'); + expect(serverlessFunctions).toHaveProperty('runtime'); + expect(serverlessFunctions).toHaveProperty('latestVersion'); + expect(serverlessFunctions).toHaveProperty('syncStatus'); + expect(serverlessFunctions).toHaveProperty('createdAt'); + expect(serverlessFunctions).toHaveProperty('updatedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/task-targets.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/task-targets.integration-spec.ts similarity index 92% rename from packages/twenty-server/test/task-targets.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/task-targets.integration-spec.ts index e54e855d31e5..b9d5cb493093 100644 --- a/packages/twenty-server/test/task-targets.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/task-targets.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('taskTargetsResolver (integration)', () => { +describe('taskTargetsResolver (e2e)', () => { it('should find many taskTargets', () => { const queryData = { query: ` @@ -18,6 +18,7 @@ describe('taskTargetsResolver (integration)', () => { personId companyId opportunityId + rocketId } } } @@ -53,6 +54,7 @@ describe('taskTargetsResolver (integration)', () => { expect(taskTargets).toHaveProperty('personId'); expect(taskTargets).toHaveProperty('companyId'); expect(taskTargets).toHaveProperty('opportunityId'); + expect(taskTargets).toHaveProperty('rocketId'); } }); }); diff --git a/packages/twenty-server/test/tasks.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/tasks.integration-spec.ts similarity index 97% rename from packages/twenty-server/test/tasks.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/tasks.integration-spec.ts index 900fd3de5ce1..016341966bd0 100644 --- a/packages/twenty-server/test/tasks.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/tasks.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('tasksResolver (integration)', () => { +describe('tasksResolver (e2e)', () => { it('should find many tasks', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/timeline-activities.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/timeline-activities.integration-spec.ts similarity index 84% rename from packages/twenty-server/test/timeline-activities.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/timeline-activities.integration-spec.ts index a5ef6a5f9662..3e5c72fec0ac 100644 --- a/packages/twenty-server/test/timeline-activities.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/timeline-activities.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('timelineActivitiesResolver (integration)', () => { +describe('timelineActivitiesResolver (e2e)', () => { it('should find many timelineActivities', () => { const queryData = { query: ` @@ -26,6 +26,10 @@ describe('timelineActivitiesResolver (integration)', () => { opportunityId noteId taskId + workflowId + workflowVersionId + workflowRunId + rocketId } } } @@ -69,6 +73,10 @@ describe('timelineActivitiesResolver (integration)', () => { expect(timelineActivities).toHaveProperty('opportunityId'); expect(timelineActivities).toHaveProperty('noteId'); expect(timelineActivities).toHaveProperty('taskId'); + expect(timelineActivities).toHaveProperty('workflowId'); + expect(timelineActivities).toHaveProperty('workflowVersionId'); + expect(timelineActivities).toHaveProperty('workflowRunId'); + expect(timelineActivities).toHaveProperty('rocketId'); } }); }); diff --git a/packages/twenty-server/test/view-fields.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/view-fields.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/view-fields.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/view-fields.integration-spec.ts index 058568763900..24b28bc5b6d9 100644 --- a/packages/twenty-server/test/view-fields.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/view-fields.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('viewFieldsResolver (integration)', () => { +describe('viewFieldsResolver (e2e)', () => { it('should find many viewFields', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/view-filters.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/view-filters.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/view-filters.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/view-filters.integration-spec.ts index 8caa942b2b61..e76c2f12fd08 100644 --- a/packages/twenty-server/test/view-filters.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/view-filters.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('viewFiltersResolver (integration)', () => { +describe('viewFiltersResolver (e2e)', () => { it('should find many viewFilters', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/view-sorts.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/view-sorts.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/view-sorts.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/view-sorts.integration-spec.ts index fc29b1d4c293..850d24cf8795 100644 --- a/packages/twenty-server/test/view-sorts.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/view-sorts.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('viewSortsResolver (integration)', () => { +describe('viewSortsResolver (e2e)', () => { it('should find many viewSorts', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/views.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/views.integration-spec.ts similarity index 97% rename from packages/twenty-server/test/views.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/views.integration-spec.ts index 122a8c398fcc..29cf849985f0 100644 --- a/packages/twenty-server/test/views.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/views.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('viewsResolver (integration)', () => { +describe('viewsResolver (e2e)', () => { it('should find many views', () => { const queryData = { query: ` @@ -10,13 +10,13 @@ describe('viewsResolver (integration)', () => { views { edges { node { - position name objectMetadataId type key icon kanbanFieldMetadataId + position isCompact id createdAt @@ -49,13 +49,13 @@ describe('viewsResolver (integration)', () => { if (edges.length > 0) { const views = edges[0].node; - expect(views).toHaveProperty('position'); expect(views).toHaveProperty('name'); expect(views).toHaveProperty('objectMetadataId'); expect(views).toHaveProperty('type'); expect(views).toHaveProperty('key'); expect(views).toHaveProperty('icon'); expect(views).toHaveProperty('kanbanFieldMetadataId'); + expect(views).toHaveProperty('position'); expect(views).toHaveProperty('isCompact'); expect(views).toHaveProperty('id'); expect(views).toHaveProperty('createdAt'); diff --git a/packages/twenty-server/test/webhooks.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/webhooks.integration-spec.ts similarity index 96% rename from packages/twenty-server/test/webhooks.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/webhooks.integration-spec.ts index 7c4224b69aa6..aaf181bf3849 100644 --- a/packages/twenty-server/test/webhooks.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/webhooks.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('webhooksResolver (integration)', () => { +describe('webhooksResolver (e2e)', () => { it('should find many webhooks', () => { const queryData = { query: ` @@ -10,10 +10,10 @@ describe('webhooksResolver (integration)', () => { webhooks { edges { node { + id targetUrl operation description - id createdAt updatedAt deletedAt @@ -44,10 +44,10 @@ describe('webhooksResolver (integration)', () => { if (edges.length > 0) { const webhooks = edges[0].node; + expect(webhooks).toHaveProperty('id'); expect(webhooks).toHaveProperty('targetUrl'); expect(webhooks).toHaveProperty('operation'); expect(webhooks).toHaveProperty('description'); - expect(webhooks).toHaveProperty('id'); expect(webhooks).toHaveProperty('createdAt'); expect(webhooks).toHaveProperty('updatedAt'); expect(webhooks).toHaveProperty('deletedAt'); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/workflow-event-listeners.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/workflow-event-listeners.integration-spec.ts new file mode 100644 index 000000000000..6859b52abe29 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/workflow-event-listeners.integration-spec.ts @@ -0,0 +1,55 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('workflowEventListenersResolver (e2e)', () => { + it('should find many workflowEventListeners', () => { + const queryData = { + query: ` + query workflowEventListeners { + workflowEventListeners { + edges { + node { + eventName + id + createdAt + updatedAt + deletedAt + workflowId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.workflowEventListeners; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const workflowEventListeners = edges[0].node; + + expect(workflowEventListeners).toHaveProperty('eventName'); + expect(workflowEventListeners).toHaveProperty('id'); + expect(workflowEventListeners).toHaveProperty('createdAt'); + expect(workflowEventListeners).toHaveProperty('updatedAt'); + expect(workflowEventListeners).toHaveProperty('deletedAt'); + expect(workflowEventListeners).toHaveProperty('workflowId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/workflow-versions.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/workflow-versions.integration-spec.ts new file mode 100644 index 000000000000..cf3a7d113e8d --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/workflow-versions.integration-spec.ts @@ -0,0 +1,63 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('workflowVersionsResolver (e2e)', () => { + it('should find many workflowVersions', () => { + const queryData = { + query: ` + query workflowVersions { + workflowVersions { + edges { + node { + name + trigger + steps + status + position + id + createdAt + updatedAt + deletedAt + workflowId + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.workflowVersions; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const workflowVersions = edges[0].node; + + expect(workflowVersions).toHaveProperty('name'); + expect(workflowVersions).toHaveProperty('trigger'); + expect(workflowVersions).toHaveProperty('steps'); + expect(workflowVersions).toHaveProperty('status'); + expect(workflowVersions).toHaveProperty('position'); + expect(workflowVersions).toHaveProperty('id'); + expect(workflowVersions).toHaveProperty('createdAt'); + expect(workflowVersions).toHaveProperty('updatedAt'); + expect(workflowVersions).toHaveProperty('deletedAt'); + expect(workflowVersions).toHaveProperty('workflowId'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/workflows.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/workflows.integration-spec.ts new file mode 100644 index 000000000000..a1c0450f02bc --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/workflows.integration-spec.ts @@ -0,0 +1,59 @@ +import request from 'supertest'; + +const client = request(`http://localhost:${APP_PORT}`); + +describe('workflowsResolver (e2e)', () => { + it('should find many workflows', () => { + const queryData = { + query: ` + query workflows { + workflows { + edges { + node { + name + lastPublishedVersionId + statuses + position + id + createdAt + updatedAt + deletedAt + } + } + } + } + `, + }; + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send(queryData) + .expect(200) + .expect((res) => { + expect(res.body.data).toBeDefined(); + expect(res.body.errors).toBeUndefined(); + }) + .expect((res) => { + const data = res.body.data.workflows; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const workflows = edges[0].node; + + expect(workflows).toHaveProperty('name'); + expect(workflows).toHaveProperty('lastPublishedVersionId'); + expect(workflows).toHaveProperty('statuses'); + expect(workflows).toHaveProperty('position'); + expect(workflows).toHaveProperty('id'); + expect(workflows).toHaveProperty('createdAt'); + expect(workflows).toHaveProperty('updatedAt'); + expect(workflows).toHaveProperty('deletedAt'); + } + }); + }); +}); diff --git a/packages/twenty-server/test/workspace-members.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/workspace-members.integration-spec.ts similarity index 97% rename from packages/twenty-server/test/workspace-members.integration-spec.ts rename to packages/twenty-server/test/integration/graphql/suites/object-generated/workspace-members.integration-spec.ts index 5ef7a415d886..63fd94d81baa 100644 --- a/packages/twenty-server/test/workspace-members.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/workspace-members.integration-spec.ts @@ -2,7 +2,7 @@ import request from 'supertest'; const client = request(`http://localhost:${APP_PORT}`); -describe('workspaceMembersResolver (integration)', () => { +describe('workspaceMembersResolver (e2e)', () => { it('should find many workspaceMembers', () => { const queryData = { query: ` diff --git a/packages/twenty-server/test/integration/graphql/utils/create-many-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/create-many-operation-factory.util.ts new file mode 100644 index 000000000000..70604c009399 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/create-many-operation-factory.util.ts @@ -0,0 +1,28 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type CreateManyOperationFactoryParams = { + objectMetadataSingularName: string; + objectMetadataPluralName: string; + gqlFields: string; + data?: object; +}; + +export const createManyOperationFactory = ({ + objectMetadataSingularName, + objectMetadataPluralName, + gqlFields, + data = {}, +}: CreateManyOperationFactoryParams) => ({ + query: gql` + mutation Create${capitalize(objectMetadataSingularName)}($data: [${capitalize(objectMetadataSingularName)}CreateInput!]!) { + create${capitalize(objectMetadataPluralName)}(data: $data) { + ${gqlFields} + } + } + `, + variables: { + data, + }, +}); diff --git a/packages/twenty-server/test/integration/graphql/utils/create-one-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/create-one-operation-factory.util.ts new file mode 100644 index 000000000000..ed477b1a7792 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/create-one-operation-factory.util.ts @@ -0,0 +1,26 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type CreateOneOperationFactoryParams = { + objectMetadataSingularName: string; + gqlFields: string; + data?: object; +}; + +export const createOneOperationFactory = ({ + objectMetadataSingularName, + gqlFields, + data = {}, +}: CreateOneOperationFactoryParams) => ({ + query: gql` + mutation Create${capitalize(objectMetadataSingularName)}($data: ${capitalize(objectMetadataSingularName)}CreateInput) { + create${capitalize(objectMetadataSingularName)}(data: $data) { + ${gqlFields} + } + } + `, + variables: { + data, + }, +}); diff --git a/packages/twenty-server/test/integration/graphql/utils/delete-many-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/delete-many-operation-factory.util.ts new file mode 100644 index 000000000000..2bfe3c158e7a --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/delete-many-operation-factory.util.ts @@ -0,0 +1,30 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type DeleteManyOperationFactoryParams = { + objectMetadataSingularName: string; + objectMetadataPluralName: string; + gqlFields: string; + filter?: object; +}; + +export const deleteManyOperationFactory = ({ + objectMetadataSingularName, + objectMetadataPluralName, + gqlFields, + filter = {}, +}: DeleteManyOperationFactoryParams) => ({ + query: gql` + mutation Delete${capitalize(objectMetadataPluralName)}( + $filter: ${capitalize(objectMetadataSingularName)}FilterInput + ) { + delete${capitalize(objectMetadataPluralName)}(filter: $filter) { + ${gqlFields} + } + } + `, + variables: { + filter, + }, +}); diff --git a/packages/twenty-server/test/integration/graphql/utils/delete-one-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/delete-one-operation-factory.util.ts new file mode 100644 index 000000000000..f3cfd765b25a --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/delete-one-operation-factory.util.ts @@ -0,0 +1,26 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type DeleteOneOperationFactoryParams = { + objectMetadataSingularName: string; + gqlFields: string; + recordId: string; +}; + +export const deleteOneOperationFactory = ({ + objectMetadataSingularName, + gqlFields, + recordId, +}: DeleteOneOperationFactoryParams) => ({ + query: gql` + mutation Delete${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: ID!) { + delete${capitalize(objectMetadataSingularName)}(id: $${objectMetadataSingularName}Id) { + ${gqlFields} + } + } + `, + variables: { + [`${objectMetadataSingularName}Id`]: recordId, + }, +}); diff --git a/packages/twenty-server/test/integration/graphql/utils/destroy-many-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/destroy-many-operation-factory.util.ts new file mode 100644 index 000000000000..f664a4088268 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/destroy-many-operation-factory.util.ts @@ -0,0 +1,30 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type DestroyManyOperationFactoryParams = { + objectMetadataSingularName: string; + objectMetadataPluralName: string; + gqlFields: string; + filter?: object; +}; + +export const destroyManyOperationFactory = ({ + objectMetadataSingularName, + objectMetadataPluralName, + gqlFields, + filter = {}, +}: DestroyManyOperationFactoryParams) => ({ + query: gql` + mutation Destroy${capitalize(objectMetadataPluralName)}( + $filter: ${capitalize(objectMetadataSingularName)}FilterInput + ) { + destroy${capitalize(objectMetadataPluralName)}(filter: $filter) { + ${gqlFields} + } + } + `, + variables: { + filter, + }, +}); diff --git a/packages/twenty-server/test/integration/graphql/utils/destroy-one-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/destroy-one-operation-factory.util.ts new file mode 100644 index 000000000000..4062e9319f5a --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/destroy-one-operation-factory.util.ts @@ -0,0 +1,26 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type DestroyOneOperationFactoryParams = { + objectMetadataSingularName: string; + gqlFields: string; + recordId: string; +}; + +export const destroyOneOperationFactory = ({ + objectMetadataSingularName, + gqlFields, + recordId, +}: DestroyOneOperationFactoryParams) => ({ + query: gql` + mutation Destroy${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: ID!) { + destroy${capitalize(objectMetadataSingularName)}(id: $${objectMetadataSingularName}Id) { + ${gqlFields} + } + } + `, + variables: { + [`${objectMetadataSingularName}Id`]: recordId, + }, +}); diff --git a/packages/twenty-server/test/integration/graphql/utils/find-many-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/find-many-operation-factory.util.ts new file mode 100644 index 000000000000..752e9aca0c20 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/find-many-operation-factory.util.ts @@ -0,0 +1,32 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type FindManyOperationFactoryParams = { + objectMetadataSingularName: string; + objectMetadataPluralName: string; + gqlFields: string; + filter?: object; +}; + +export const findManyOperationFactory = ({ + objectMetadataSingularName, + objectMetadataPluralName, + gqlFields, + filter = {}, +}: FindManyOperationFactoryParams) => ({ + query: gql` + query ${capitalize(objectMetadataPluralName)}($filter: ${capitalize(objectMetadataSingularName)}FilterInput) { + ${objectMetadataPluralName}(filter: $filter) { + edges { + node { + ${gqlFields} + } + } + } + } + `, + variables: { + filter, + }, +}); diff --git a/packages/twenty-server/test/integration/graphql/utils/find-one-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/find-one-operation-factory.util.ts new file mode 100644 index 000000000000..1a6972a841a9 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/find-one-operation-factory.util.ts @@ -0,0 +1,26 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type FindOneOperationFactoryParams = { + objectMetadataSingularName: string; + gqlFields: string; + filter?: object; +}; + +export const findOneOperationFactory = ({ + objectMetadataSingularName, + gqlFields, + filter = {}, +}: FindOneOperationFactoryParams) => ({ + query: gql` + query ${capitalize(objectMetadataSingularName)}($filter: ${capitalize(objectMetadataSingularName)}FilterInput) { + ${objectMetadataSingularName}(filter: $filter) { + ${gqlFields} + } + } + `, + variables: { + filter, + }, +}); diff --git a/packages/twenty-server/test/integration/graphql/utils/make-graphql-api-request.util.ts b/packages/twenty-server/test/integration/graphql/utils/make-graphql-api-request.util.ts new file mode 100644 index 000000000000..21b3e889716c --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/make-graphql-api-request.util.ts @@ -0,0 +1,19 @@ +import { ASTNode, print } from 'graphql'; +import request from 'supertest'; + +type GraphqlOperation = { + query: ASTNode; + variables?: Record; +}; + +export const makeGraphqlAPIRequest = (graphqlOperation: GraphqlOperation) => { + const client = request(`http://localhost:${APP_PORT}`); + + return client + .post('/graphql') + .set('Authorization', `Bearer ${ACCESS_TOKEN}`) + .send({ + query: print(graphqlOperation.query), + variables: graphqlOperation.variables || {}, + }); +}; diff --git a/packages/twenty-server/test/integration/graphql/utils/update-many-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/update-many-operation-factory.util.ts new file mode 100644 index 000000000000..688ae9199950 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/update-many-operation-factory.util.ts @@ -0,0 +1,34 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type UpdateManyOperationFactoryParams = { + objectMetadataSingularName: string; + objectMetadataPluralName: string; + gqlFields: string; + data?: object; + filter?: object; +}; + +export const updateManyOperationFactory = ({ + objectMetadataSingularName, + objectMetadataPluralName, + gqlFields, + data = {}, + filter = {}, +}: UpdateManyOperationFactoryParams) => ({ + query: gql` + mutation Update${capitalize(objectMetadataPluralName)}( + $data: ${capitalize(objectMetadataSingularName)}UpdateInput + $filter: ${capitalize(objectMetadataSingularName)}FilterInput + ) { + update${capitalize(objectMetadataPluralName)}(data: $data, filter: $filter) { + ${gqlFields} + } + } + `, + variables: { + data, + filter, + }, +}); diff --git a/packages/twenty-server/test/integration/graphql/utils/update-one-operation-factory.util.ts b/packages/twenty-server/test/integration/graphql/utils/update-one-operation-factory.util.ts new file mode 100644 index 000000000000..cf7827241292 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/utils/update-one-operation-factory.util.ts @@ -0,0 +1,29 @@ +import gql from 'graphql-tag'; + +import { capitalize } from 'src/utils/capitalize'; + +type UpdateOneOperationFactoryParams = { + objectMetadataSingularName: string; + gqlFields: string; + data?: object; + recordId: string; +}; + +export const updateOneOperationFactory = ({ + objectMetadataSingularName, + gqlFields, + data = {}, + recordId, +}: UpdateOneOperationFactoryParams) => ({ + query: gql` + mutation Update${capitalize(objectMetadataSingularName)}($${objectMetadataSingularName}Id: ID, $data: ${capitalize(objectMetadataSingularName)}UpdateInput) { + update${capitalize(objectMetadataSingularName)}(id: $${objectMetadataSingularName}Id, data: $data) { + ${gqlFields} + } + } + `, + variables: { + data, + [`${objectMetadataSingularName}Id`]: recordId, + }, +}); diff --git a/packages/twenty-server/test/utils/create-app.ts b/packages/twenty-server/test/integration/utils/create-app.ts similarity index 100% rename from packages/twenty-server/test/utils/create-app.ts rename to packages/twenty-server/test/integration/utils/create-app.ts diff --git a/packages/twenty-server/test/integration/utils/generate-record-name.ts b/packages/twenty-server/test/integration/utils/generate-record-name.ts new file mode 100644 index 000000000000..123de9b6b17d --- /dev/null +++ b/packages/twenty-server/test/integration/utils/generate-record-name.ts @@ -0,0 +1,4 @@ +export const TEST_NAME_PREFIX = 'test_record_'; + +export const generateRecordName = (uuid: string) => + `${TEST_NAME_PREFIX}-${uuid}`; diff --git a/packages/twenty-server/test/utils/setup-test.ts b/packages/twenty-server/test/integration/utils/setup-test.ts similarity index 100% rename from packages/twenty-server/test/utils/setup-test.ts rename to packages/twenty-server/test/integration/utils/setup-test.ts diff --git a/packages/twenty-server/test/utils/teardown-test.ts b/packages/twenty-server/test/integration/utils/teardown-test.ts similarity index 100% rename from packages/twenty-server/test/utils/teardown-test.ts rename to packages/twenty-server/test/integration/utils/teardown-test.ts From fad13630955e5cf4df78581a5e4c4172b77cebae Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Thu, 17 Oct 2024 19:35:27 +0200 Subject: [PATCH 08/67] Fix CIs not running --- .github/workflows/ci-front.yaml | 34 +++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml index 52ca3c825f55..595081c0a973 100644 --- a/.github/workflows/ci-front.yaml +++ b/.github/workflows/ci-front.yaml @@ -43,8 +43,34 @@ jobs: - name: Front / Build storybook run: npx nx storybook:build twenty-front front-sb-test: - runs-on: depot-ubuntu-22.04-8 - timeout-minutes: 30 + runs-on: ci-8-cores + timeout-minutes: 60 + needs: front-sb-build + strategy: + matrix: + storybook_scope: [pages, modules] + env: + REACT_APP_SERVER_BASE_URL: http://localhost:3000 + NX_REJECT_UNKNOWN_LOCAL_CACHE: 0 + steps: + - name: Fetch local actions + uses: actions/checkout@v4 + - name: Install dependencies + uses: ./.github/workflows/actions/yarn-install + - name: Install Playwright + run: cd packages/twenty-front && npx playwright install + - name: Front / Restore Storybook Task Cache + uses: ./.github/workflows/actions/task-cache + with: + tag: scope:frontend + tasks: storybook:build + - name: Front / Write .env + run: npx nx reset:env twenty-front + - name: Run storybook tests + run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }} + front-sb-test-shipfox: + runs-on: shipfox-8vcpu-ubuntu-2204 + timeout-minutes: 60 needs: front-sb-build strategy: matrix: @@ -69,8 +95,8 @@ jobs: - name: Run storybook tests run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }} front-sb-test-performance: - runs-on: depot-ubuntu-22.04-8 - timeout-minutes: 30 + runs-on: ci-8-cores + timeout-minutes: 60 env: REACT_APP_SERVER_BASE_URL: http://localhost:3000 NX_REJECT_UNKNOWN_LOCAL_CACHE: 0 From c0e6fb6fdbb47c640c2561e8cc54fd112f6b947e Mon Sep 17 00:00:00 2001 From: Shashank Suman <103516291+SShanks451@users.noreply.github.com> Date: Fri, 18 Oct 2024 01:06:44 +0530 Subject: [PATCH 09/67] added left padding in filter chip (#7800) Fixes: #7779 --------- Co-authored-by: Shashank Suman --- .../src/modules/views/components/SortOrFilterChip.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx b/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx index 1b55e6a32790..55c2a77f3c59 100644 --- a/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx +++ b/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx @@ -40,6 +40,7 @@ const StyledChip = styled.div<{ variant: SortOrFitlerChipVariant }>` font-size: ${({ theme }) => theme.font.size.sm}; font-weight: ${({ theme }) => theme.font.weight.medium}; padding: ${({ theme }) => theme.spacing(0.5) + ' ' + theme.spacing(2)}; + margin-left: ${({ theme }) => theme.spacing(2)}; user-select: none; white-space: nowrap; From a45d3148ac372c7e7ec8b7c3a595455eb25201ed Mon Sep 17 00:00:00 2001 From: Harshit Singh <73997189+harshit078@users.noreply.github.com> Date: Fri, 18 Oct 2024 01:07:03 +0530 Subject: [PATCH 10/67] fix: Blocklist table optimised for all viewports (#7618) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description - This PR fixes the issue #7549 - Optimised blocktable for all viewports ## Changes - Screenshot 2024-10-12 at 5 11 11 PM https://github.com/user-attachments/assets/d5fa063d-2819-4a9d-a9b2-e3ceefe65c8d --------- Co-authored-by: Charles Bochet --- .../components/SettingsAccountsBlocklistTable.tsx | 8 +++++--- .../components/SettingsAccountsBlocklistTableRow.tsx | 12 +++++++++--- 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTable.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTable.tsx index a4c9f5306fad..3d513dc1eff4 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTable.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTable.tsx @@ -1,11 +1,10 @@ -import styled from '@emotion/styled'; - import { BlocklistItem } from '@/accounts/types/BlocklistItem'; import { SettingsAccountsBlocklistTableRow } from '@/settings/accounts/components/SettingsAccountsBlocklistTableRow'; import { Table } from '@/ui/layout/table/components/Table'; import { TableBody } from '@/ui/layout/table/components/TableBody'; import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableRow } from '@/ui/layout/table/components/TableRow'; +import styled from '@emotion/styled'; type SettingsAccountsBlocklistTableProps = { blocklist: BlocklistItem[]; @@ -28,7 +27,10 @@ export const SettingsAccountsBlocklistTable = ({ <> {blocklist.length > 0 && ( - + Email/Domain Added to blocklist diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTableRow.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTableRow.tsx index 9a1148447a17..30cf3a37a313 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTableRow.tsx @@ -1,4 +1,4 @@ -import { IconX } from 'twenty-ui'; +import { IconX, OverflowingTextWithTooltip } from 'twenty-ui'; import { BlocklistItem } from '@/accounts/types/BlocklistItem'; import { IconButton } from '@/ui/input/button/components/IconButton'; @@ -16,8 +16,14 @@ export const SettingsAccountsBlocklistTableRow = ({ onRemove, }: SettingsAccountsBlocklistTableRowProps) => { return ( - - {blocklistItem.handle} + + + + {blocklistItem.createdAt ? formatToHumanReadableDate(blocklistItem.createdAt) From 249c7324a2c3aed53e777db999549b3d2cd371df Mon Sep 17 00:00:00 2001 From: Thibault Le Ouay Date: Thu, 17 Oct 2024 22:40:30 +0200 Subject: [PATCH 11/67] Improve error message for Graphql API (#7805) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ![CleanShot 2024-10-17 at 11 39 39](https://github.com/user-attachments/assets/616b8317-de1f-4b61-b2b4-980b14b09f66) This improves this error message. --------- Co-authored-by: Félix Malfait --- .../src/engine/decorators/auth/auth-user.decorator.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/twenty-server/src/engine/decorators/auth/auth-user.decorator.ts b/packages/twenty-server/src/engine/decorators/auth/auth-user.decorator.ts index 75f3a982e949..35d3ccc08da4 100644 --- a/packages/twenty-server/src/engine/decorators/auth/auth-user.decorator.ts +++ b/packages/twenty-server/src/engine/decorators/auth/auth-user.decorator.ts @@ -15,7 +15,9 @@ export const AuthUser = createParamDecorator( const request = getRequest(ctx); if (!options?.allowUndefined && !request.user) { - throw new ForbiddenException("You're not authorized to do this"); + throw new ForbiddenException( + "You're not authorized to do this. Note: This endpoint requires a user and won't work with just an API key.", + ); } return request.user; From 6f5dc1c924773606a4520066c3bee3181331852e Mon Sep 17 00:00:00 2001 From: Syed Hamza Hussain <96618778+SyedHamzaHussain000@users.noreply.github.com> Date: Fri, 18 Oct 2024 02:55:50 +0500 Subject: [PATCH 12/67] Bug Fix: created new div and p tag styles and wrap it on the workspace member as container (#7581) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hello, Hope you are doing well.I created a special style for the text to make sure it stays in one line and wont exceed the width if the text width will be more then 80px it will ecplise and set ... at the end of the text. I created these 2 styles variables and wrap my text in these styles StyledObjectSummary StyledEllipsisParagraph Fixes #7574 #Screens Shots Screenshot 2024-10-10 at 10 58 04 PM Screenshot 2024-10-10 at 10 58 20 PM --------- Co-authored-by: Charles Bochet --- .../SettingsDataModelFieldSettingsFormCard.tsx | 4 +++- ...sDataModelFieldRelationSettingsFormCard.tsx | 1 - .../SettingsDataModelFieldPreviewCard.tsx | 1 - .../objects/SettingsDataModelObjectSummary.tsx | 18 +++++++++++------- 4 files changed, 14 insertions(+), 10 deletions(-) diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx index 9b50515b10b9..ca1ef939abdb 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx @@ -129,7 +129,9 @@ export const SettingsDataModelFieldSettingsFormCard = ({ fieldMetadataItem, objectMetadataItem, }: SettingsDataModelFieldSettingsFormCardProps) => { - if (!previewableTypes.includes(fieldMetadataItem.type)) return null; + if (!previewableTypes.includes(fieldMetadataItem.type)) { + return null; + } if (fieldMetadataItem.type === FieldMetadataType.Boolean) { return ( diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx index 0372ba071417..59bee20064ea 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx @@ -23,7 +23,6 @@ type SettingsDataModelFieldRelationSettingsFormCardProps = { } & Pick; const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` - display: grid; flex: 1 1 100%; `; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx index 06684997a4b2..360c9a300692 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx @@ -19,7 +19,6 @@ const StyledCard = styled(Card)` `; const StyledCardContent = styled(CardContent)` - display: grid; padding: ${({ theme }) => theme.spacing(2)}; `; diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectSummary.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectSummary.tsx index f6b8aaa0ab54..931fb6990e09 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectSummary.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectSummary.tsx @@ -1,6 +1,6 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { useIcons } from 'twenty-ui'; +import { OverflowingTextWithTooltip, useIcons } from 'twenty-ui'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/SettingsDataModelObjectTypeTag'; @@ -14,15 +14,17 @@ export type SettingsDataModelObjectSummaryProps = { const StyledObjectSummary = styled.div` align-items: center; display: flex; - gap: ${({ theme }) => theme.spacing(2)}; justify-content: space-between; `; const StyledObjectName = styled.div` - align-items: center; display: flex; - font-weight: ${({ theme }) => theme.font.weight.medium}; - gap: ${({ theme }) => theme.spacing(1)}; + gap: ${({ theme }) => theme.spacing(2)}; + max-width: 60%; +`; + +const StyledIconContainer = styled.div` + flex-shrink: 0; `; export const SettingsDataModelObjectSummary = ({ @@ -38,8 +40,10 @@ export const SettingsDataModelObjectSummary = ({ return ( - - {objectMetadataItem.labelPlural} + + + + From 8f7ca6a0e32d2ad3d77a42a645906af772861dc9 Mon Sep 17 00:00:00 2001 From: Pushpender <129095696+Pushpender1122@users.noreply.github.com> Date: Fri, 18 Oct 2024 03:51:57 +0530 Subject: [PATCH 13/67] Fix Google Auth displays Status: 401 on screen (#7659) When the user presses the cancel button, the server sends the following response: ![image](https://github.com/user-attachments/assets/cb68cf01-b32c-4680-a811-cd917db88ca9) {"statusCode": 401, "message": "Unauthorized"} Now, when the user clicks the cancel button, they are redirected to the home page for login. Related Issue Fixes #7584 --------- Co-authored-by: Charles Bochet --- .../core-modules/auth/auth.exception.ts | 1 + .../controllers/google-auth.controller.ts | 2 ++ .../filters/auth-oauth-exception.filter.ts | 34 +++++++++++++++++++ .../auth/guards/google-oauth.guard.ts | 12 +++++++ .../src/utils/apply-cors-to-exceptions.ts | 4 +-- 5 files changed, 51 insertions(+), 2 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts index 2387ff9d310a..62b215f2691b 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts @@ -16,4 +16,5 @@ export enum AuthExceptionCode { UNAUTHENTICATED = 'UNAUTHENTICATED', INVALID_DATA = 'INVALID_DATA', INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', + OAUTH_ACCESS_DENIED = 'OAUTH_ACCESS_DENIED', } diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts index 6ae9b11d7429..c674569d43bf 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-auth.controller.ts @@ -9,6 +9,7 @@ import { import { Response } from 'express'; +import { AuthOAuthExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-oauth-exception.filter'; import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter'; import { GoogleOauthGuard } from 'src/engine/core-modules/auth/guards/google-oauth.guard'; import { GoogleProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/google-provider-enabled.guard'; @@ -33,6 +34,7 @@ export class GoogleAuthController { @Get('redirect') @UseGuards(GoogleProviderEnabledGuard, GoogleOauthGuard) + @UseFilters(AuthOAuthExceptionFilter) async googleAuthRedirect(@Req() req: GoogleRequest, @Res() res: Response) { const { firstName, diff --git a/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts new file mode 100644 index 000000000000..008e7d11032f --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts @@ -0,0 +1,34 @@ +import { + ArgumentsHost, + Catch, + ExceptionFilter, + InternalServerErrorException, +} from '@nestjs/common'; + +import { Response } from 'express'; + +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; + +@Catch(AuthException) +export class AuthOAuthExceptionFilter implements ExceptionFilter { + constructor(private readonly environmentService: EnvironmentService) {} + + catch(exception: AuthException, host: ArgumentsHost) { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + switch (exception.code) { + case AuthExceptionCode.OAUTH_ACCESS_DENIED: + response + .status(403) + .redirect(this.environmentService.get('FRONT_BASE_URL')); + break; + default: + throw new InternalServerErrorException(exception.message); + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts index dd9fbf17f2c0..f4675888b2e8 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts @@ -1,6 +1,11 @@ import { ExecutionContext, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; + @Injectable() export class GoogleOauthGuard extends AuthGuard('google') { constructor() { @@ -14,6 +19,13 @@ export class GoogleOauthGuard extends AuthGuard('google') { const workspaceInviteHash = request.query.inviteHash; const workspacePersonalInviteToken = request.query.inviteToken; + if (request.query.error === 'access_denied') { + throw new AuthException( + 'Google OAuth access denied', + AuthExceptionCode.OAUTH_ACCESS_DENIED, + ); + } + if (workspaceInviteHash && typeof workspaceInviteHash === 'string') { request.params.workspaceInviteHash = workspaceInviteHash; } diff --git a/packages/twenty-server/src/utils/apply-cors-to-exceptions.ts b/packages/twenty-server/src/utils/apply-cors-to-exceptions.ts index 0dd3a5cc12ef..eba73f8e0571 100644 --- a/packages/twenty-server/src/utils/apply-cors-to-exceptions.ts +++ b/packages/twenty-server/src/utils/apply-cors-to-exceptions.ts @@ -1,7 +1,7 @@ import { - ExceptionFilter, - Catch, ArgumentsHost, + Catch, + ExceptionFilter, HttpException, } from '@nestjs/common'; From f6c094a56fa1c63d937c03215c6eef27417fcbbb Mon Sep 17 00:00:00 2001 From: Hitarth Sheth <133380930+Hitarthsheth07@users.noreply.github.com> Date: Thu, 17 Oct 2024 18:49:42 -0400 Subject: [PATCH 14/67] [FIX] fix navigation overflow (#7795) FIX #7733 Fixes the overflow and responsive problem on large and small devices. ![image](https://github.com/user-attachments/assets/6cd8b33f-a52f-4452-b161-9c84ebbb4cce) ![image](https://github.com/user-attachments/assets/c8c0386f-e2a2-4f96-a06e-7e37f54c0564) The 'Workspace' title is fixed and only links under it are scrolled when overflown. --------- Co-authored-by: Lucas Bordeau --- .../components/WorkspaceFavorites.tsx | 7 +- ...igationDrawerItemForObjectMetadataItem.tsx | 84 ++++++++++ .../NavigationDrawerOpenedSection.tsx | 5 - ...ionDrawerSectionForObjectMetadataItems.tsx | 154 ++++++------------ ...erSectionForObjectMetadataItemsWrapper.tsx | 6 - .../components/NavigationDrawer.tsx | 9 +- ...avigationDrawerAnimatedCollapseWrapper.tsx | 6 +- .../components/NavigationDrawerItem.tsx | 1 - .../components/NavigationDrawerSection.tsx | 2 + .../scroll/contexts/ScrollWrapperContexts.tsx | 7 +- 10 files changed, 157 insertions(+), 124 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerItemForObjectMetadataItem.tsx diff --git a/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx b/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx index cf106211405b..b975799fd499 100644 --- a/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx +++ b/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx @@ -2,17 +2,13 @@ import { useFilteredObjectMetadataItemsForWorkspaceFavorites } from '@/navigatio import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems'; import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; -import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; -import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; -import { View } from '@/views/types/View'; export const WorkspaceFavorites = () => { - const { records: views } = usePrefetchedData(PrefetchKey.AllViews); - const { activeObjectMetadataItems: objectMetadataItemsToDisplay } = useFilteredObjectMetadataItemsForWorkspaceFavorites(); const loading = useIsPrefetchLoading(); + if (loading) { return ; } @@ -21,7 +17,6 @@ export const WorkspaceFavorites = () => { ); diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerItemForObjectMetadataItem.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerItemForObjectMetadataItem.tsx new file mode 100644 index 000000000000..8c7f1e3ceda2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerItemForObjectMetadataItem.tsx @@ -0,0 +1,84 @@ +import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; +import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; +import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; +import { NavigationDrawerItemsCollapsedContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer'; +import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem'; +import { getNavigationSubItemState } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemState'; +import { View } from '@/views/types/View'; +import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews'; +import { useLocation } from 'react-router-dom'; +import { useIcons } from 'twenty-ui'; + +export type NavigationDrawerItemForObjectMetadataItemProps = { + objectMetadataItem: ObjectMetadataItem; +}; + +export const NavigationDrawerItemForObjectMetadataItem = ({ + objectMetadataItem, +}: NavigationDrawerItemForObjectMetadataItemProps) => { + const { records: views } = usePrefetchedData(PrefetchKey.AllViews); + + const objectMetadataViews = getObjectMetadataItemViews( + objectMetadataItem.id, + views, + ); + + const { getIcon } = useIcons(); + const currentPath = useLocation().pathname; + const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView(); + + const lastVisitedViewId = getLastVisitedViewIdFromObjectMetadataItemId( + objectMetadataItem.id, + ); + + const viewId = lastVisitedViewId ?? objectMetadataViews[0]?.id; + + const navigationPath = `/objects/${objectMetadataItem.namePlural}${ + viewId ? `?view=${viewId}` : '' + }`; + + const isActive = currentPath === `/objects/${objectMetadataItem.namePlural}`; + const shouldSubItemsBeDisplayed = isActive && objectMetadataViews.length > 1; + + const sortedObjectMetadataViews = [...objectMetadataViews].sort( + (viewA, viewB) => + viewA.key === 'INDEX' ? -1 : viewA.position - viewB.position, + ); + + const selectedSubItemIndex = sortedObjectMetadataViews.findIndex( + (view) => viewId === view.id, + ); + + const subItemArrayLength = sortedObjectMetadataViews.length; + + return ( + + + {shouldSubItemsBeDisplayed && + sortedObjectMetadataViews.map((view, index) => ( + + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx index fb17b643078f..b17ad4c310e2 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx @@ -5,9 +5,6 @@ import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; -import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; -import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; -import { View } from '@/views/types/View'; export const NavigationDrawerOpenedSection = () => { const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); @@ -15,7 +12,6 @@ export const NavigationDrawerOpenedSection = () => { (item) => !item.isRemote, ); - const { records: views } = usePrefetchedData(PrefetchKey.AllViews); const loading = useIsPrefetchLoading(); const currentObjectNamePlural = useParams().objectNamePlural; @@ -49,7 +45,6 @@ export const NavigationDrawerOpenedSection = () => { ) diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx index 5e666e0cf542..9960670d1beb 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx @@ -1,18 +1,13 @@ -import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView'; +import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/components/NavigationDrawerItemForObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; -import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper'; -import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem'; +import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; +import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle'; import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection'; -import { getNavigationSubItemState } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemState'; -import { View } from '@/views/types/View'; -import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews'; -import { useLocation } from 'react-router-dom'; +import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; +import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; -import { useIcons } from 'twenty-ui'; -import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle'; -import { NavigationDrawerItemsCollapsedContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer'; + const ORDERED_STANDARD_OBJECTS = [ 'person', 'company', @@ -21,111 +16,59 @@ const ORDERED_STANDARD_OBJECTS = [ 'note', ]; +const StyledObjectsMetaDataItemsWrapper = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.betweenSiblingsGap}; + width: 100%; + margin-bottom: ${({ theme }) => theme.spacing(3)}; + flex: 1; + overflow-y: auto; +`; + export const NavigationDrawerSectionForObjectMetadataItems = ({ sectionTitle, isRemote, - views, objectMetadataItems, }: { sectionTitle: string; isRemote: boolean; - views: View[]; objectMetadataItems: ObjectMetadataItem[]; }) => { const { toggleNavigationSection, isNavigationSectionOpenState } = useNavigationSection('Objects' + (isRemote ? 'Remote' : 'Workspace')); const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState); - const { getIcon } = useIcons(); - const currentPath = useLocation().pathname; - const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView(); - - const renderObjectMetadataItems = () => { - return [ - ...objectMetadataItems - .filter((item) => ORDERED_STANDARD_OBJECTS.includes(item.nameSingular)) - .sort((objectMetadataItemA, objectMetadataItemB) => { - const indexA = ORDERED_STANDARD_OBJECTS.indexOf( - objectMetadataItemA.nameSingular, - ); - const indexB = ORDERED_STANDARD_OBJECTS.indexOf( - objectMetadataItemB.nameSingular, - ); - if (indexA === -1 || indexB === -1) { - return objectMetadataItemA.nameSingular.localeCompare( - objectMetadataItemB.nameSingular, - ); - } - return indexA - indexB; - }), - ...objectMetadataItems - .filter((item) => !ORDERED_STANDARD_OBJECTS.includes(item.nameSingular)) - .sort((objectMetadataItemA, objectMetadataItemB) => { - return new Date(objectMetadataItemA.createdAt) < - new Date(objectMetadataItemB.createdAt) - ? 1 - : -1; - }), - ].map((objectMetadataItem) => { - const objectMetadataViews = getObjectMetadataItemViews( - objectMetadataItem.id, - views, - ); - const lastVisitedViewId = getLastVisitedViewIdFromObjectMetadataItemId( - objectMetadataItem.id, - ); - const viewId = lastVisitedViewId ?? objectMetadataViews[0]?.id; - - const navigationPath = `/objects/${objectMetadataItem.namePlural}${ - viewId ? `?view=${viewId}` : '' - }`; - - const isActive = - currentPath === `/objects/${objectMetadataItem.namePlural}`; - const shouldSubItemsBeDisplayed = - isActive && objectMetadataViews.length > 1; - - const sortedObjectMetadataViews = [...objectMetadataViews].sort( - (viewA, viewB) => - viewA.key === 'INDEX' ? -1 : viewA.position - viewB.position, + const sortedStandardObjectMetadataItems = [...objectMetadataItems] + .filter((item) => ORDERED_STANDARD_OBJECTS.includes(item.nameSingular)) + .sort((objectMetadataItemA, objectMetadataItemB) => { + const indexA = ORDERED_STANDARD_OBJECTS.indexOf( + objectMetadataItemA.nameSingular, ); - - const selectedSubItemIndex = sortedObjectMetadataViews.findIndex( - (view) => viewId === view.id, + const indexB = ORDERED_STANDARD_OBJECTS.indexOf( + objectMetadataItemB.nameSingular, ); + if (indexA === -1 || indexB === -1) { + return objectMetadataItemA.nameSingular.localeCompare( + objectMetadataItemB.nameSingular, + ); + } + return indexA - indexB; + }); - const subItemArrayLength = sortedObjectMetadataViews.length; - - return ( - - - {shouldSubItemsBeDisplayed && - sortedObjectMetadataViews.map((view, index) => ( - - ))} - - ); + const sortedCustomObjectMetadataItems = [...objectMetadataItems] + .filter((item) => !ORDERED_STANDARD_OBJECTS.includes(item.nameSingular)) + .sort((objectMetadataItemA, objectMetadataItemB) => { + return new Date(objectMetadataItemA.createdAt) < + new Date(objectMetadataItemB.createdAt) + ? 1 + : -1; }); - }; + + const objectMetadataItemsForNavigationItems = [ + ...sortedStandardObjectMetadataItems, + ...sortedCustomObjectMetadataItems, + ]; return ( objectMetadataItems.length > 0 && ( @@ -136,7 +79,18 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({ onClick={() => toggleNavigationSection()} /> - {isNavigationSectionOpen && renderObjectMetadataItems()} + + + {isNavigationSectionOpen && + objectMetadataItemsForNavigationItems.map( + (objectMetadataItem) => ( + + ), + )} + + ) ); diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper.tsx index 2127db1fc604..91a22ca5ab1e 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper.tsx @@ -6,9 +6,6 @@ import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; -import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; -import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; -import { View } from '@/views/types/View'; export const NavigationDrawerSectionForObjectMetadataItemsWrapper = ({ isRemote, @@ -21,8 +18,6 @@ export const NavigationDrawerSectionForObjectMetadataItemsWrapper = ({ const filteredActiveObjectMetadataItems = activeObjectMetadataItems.filter( (item) => (isRemote ? item.isRemote : !item.isRemote), ); - - const { records: views } = usePrefetchedData(PrefetchKey.AllViews); const loading = useIsPrefetchLoading(); if (loading && isDefined(currentUser)) { @@ -33,7 +28,6 @@ export const NavigationDrawerSectionForObjectMetadataItemsWrapper = ({ ); diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx index 8fbd853a56ed..33e9ef152708 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawer.tsx @@ -9,10 +9,10 @@ import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths'; +import { useIsSettingsDrawer } from '@/navigation/hooks/useIsSettingsDrawer'; import { isNavigationDrawerExpandedState } from '../../states/isNavigationDrawerExpanded'; import { NavigationDrawerBackButton } from './NavigationDrawerBackButton'; import { NavigationDrawerHeader } from './NavigationDrawerHeader'; -import { useIsSettingsDrawer } from '@/navigation/hooks/useIsSettingsDrawer'; export type NavigationDrawerProps = { children: ReactNode; @@ -22,7 +22,10 @@ export type NavigationDrawerProps = { title?: string; }; -const StyledAnimatedContainer = styled(motion.div)``; +const StyledAnimatedContainer = styled(motion.div)` + max-height: 100vh; + overflow: hidden; +`; const StyledContainer = styled.div<{ isSettings?: boolean; @@ -51,6 +54,8 @@ const StyledItemsContainer = styled.div` display: flex; flex-direction: column; margin-bottom: auto; + overflow: hidden; + flex: 1; `; export const NavigationDrawer = ({ diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper.tsx index 19575ff0d483..3a4d42541d26 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper.tsx @@ -1,9 +1,9 @@ +import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage'; +import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; +import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { AnimationControls, motion, TargetAndTransition } from 'framer-motion'; import { useRecoilValue } from 'recoil'; -import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; -import { useTheme } from '@emotion/react'; -import { useIsSettingsPage } from '@/navigation/hooks/useIsSettingsPage'; const StyledAnimatedContainer = styled(motion.div)``; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx index d98fc547091c..65944df37e5d 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx @@ -142,7 +142,6 @@ const StyledKeyBoardShortcut = styled.div` const StyledNavigationDrawerItemContainer = styled.div` display: flex; - flex-grow: 1; width: 100%; `; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSection.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSection.tsx index 2ba98503329b..afc7fe803744 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSection.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSection.tsx @@ -6,6 +6,8 @@ const StyledSection = styled.div` gap: ${({ theme }) => theme.betweenSiblingsGap}; width: 100%; margin-bottom: ${({ theme }) => theme.spacing(3)}; + flex-shrink: 1; + overflow: hidden; `; export { StyledSection as NavigationDrawerSection }; diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx b/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx index b6f7d2103a60..1d83d1ddd94b 100644 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx @@ -17,7 +17,8 @@ export type ContextProviderName = | 'tabList' | 'releases' | 'test' - | 'showPageActivityContainer'; + | 'showPageActivityContainer' + | 'navigationDrawer'; const createScrollWrapperContext = (id: string) => createContext({ @@ -47,6 +48,8 @@ export const ReleasesScrollWrapperContext = createScrollWrapperContext('releases'); export const ShowPageActivityContainerScrollWrapperContext = createScrollWrapperContext('showPageActivityContainer'); +export const NavigationDrawerScrollWrapperContext = + createScrollWrapperContext('navigationDrawer'); export const TestScrollWrapperContext = createScrollWrapperContext('test'); export const getContextByProviderName = ( @@ -77,6 +80,8 @@ export const getContextByProviderName = ( return TestScrollWrapperContext; case 'showPageActivityContainer': return ShowPageActivityContainerScrollWrapperContext; + case 'navigationDrawer': + return NavigationDrawerScrollWrapperContext; default: throw new Error('Context Provider not available'); } From 0c24001e23060f112d81d41c97eb5ceca6aa52d6 Mon Sep 17 00:00:00 2001 From: martmull Date: Fri, 18 Oct 2024 10:20:21 +0200 Subject: [PATCH 15/67] Fix update event webhook triggering (#7814) --- .../graphql-query-runner.service.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts index 7eb55d60ae95..2330ec472815 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service.ts @@ -361,19 +361,21 @@ export class GraphqlQueryRunnerService { authContext.workspace.id, ); + const resultWithGettersArray = Array.isArray(resultWithGetters) + ? resultWithGetters + : [resultWithGetters]; + await this.workspaceQueryHookService.executePostQueryHooks( authContext, objectMetadataItem.nameSingular, operationName, - Array.isArray(resultWithGetters) - ? resultWithGetters - : [resultWithGetters], + resultWithGettersArray, ); const jobOperation = this.operationNameToJobOperation(operationName); if (jobOperation) { - await this.triggerWebhooks(resultWithGetters, jobOperation, options); + await this.triggerWebhooks(resultWithGettersArray, jobOperation, options); } return resultWithGetters; From 8cadcdf577f505c107de31dc73a8d25bf21f8274 Mon Sep 17 00:00:00 2001 From: Ana Sofia Marin Alexandre <61988046+anamarn@users.noreply.github.com> Date: Fri, 18 Oct 2024 11:00:21 +0200 Subject: [PATCH 16/67] add dynamic dates for webhookGraphDataUsage (#7720) **Before:** Only last 5 days where displayed on Developers Settings Webhook Usage Graph. ![image](https://github.com/user-attachments/assets/7b7f2e6b-9637-489e-a7a7-5a3cb70525aa) **Now** Added component where you can select the time range where you want to view the webhook usage. To do better the styling and content depassing . Screenshot 2024-10-15 at 16 56 45 **In order to test** 1. Set ANALYTICS_ENABLED to true 2. Set TINYBIRD_TOKEN to your token from the workspace twenty_analytics_playground 3. Write your client tinybird token in SettingsDeveloppersWebhookDetail.tsx in line 93 4. Create a Webhook in twenty and set wich events it needs to track 5. Run twenty-worker in order to make the webhooks work. 6. Do your tasks in order to populate the data 7. Enter to settings> webhook>your webhook and the statistics section should be displayed. 8. Select the desired time range in the dropdown **To do list** - Tooltip is truncated when accessing values at the right end of the graph - DateTicks needs to follow a more clear standard - Update this PR with more representative images --- .../twenty-front/src/generated/graphql.tsx | 4 +- .../src/modules/auth/hooks/useAuth.ts | 6 +- .../components/ClientConfigProviderEffect.tsx | 9 +- .../graphql/queries/getClientConfig.ts | 1 + .../states/isAnalyticsEnabledState.ts | 6 + .../constants/DateFormatWithoutYear.ts | 11 ++ .../utils/__tests__/detectDateFormat.test.ts | 17 +-- .../utils/__tests__/detectTimeFormat.test.ts | 9 +- ...tDateISOStringToDateTimeSimplified.test.js | 90 +++++++++++ .../localization/utils/detectDateFormat.ts | 10 +- .../localization/utils/detectTimeFormat.ts | 6 +- ...formatDateISOStringToDateTimeSimplified.ts | 18 +++ .../getDateFormatFromWorkspaceDateFormat.ts | 2 +- .../getTimeFormatFromWorkspaceTimeFormat.ts | 2 +- .../SettingsDevelopersWebhookTooltip.tsx | 89 +++++++++++ .../SettingsDevelopersWebhookUsageGraph.tsx | 142 ++++++++++++++++-- ...tingsDevelopersWebhookUsageGraphEffect.tsx | 89 +---------- .../constants/WebhookGraphApiOptionsMap.ts | 6 + .../developers/webhook/hooks/useGraphData.tsx | 23 +++ .../__tests__/fetchGraphDataOrThrow.test.js | 115 ++++++++++++++ .../webhook/utils/fetchGraphDataOrThrow.ts | 80 ++++++++++ .../users/components/UserProviderEffect.tsx | 6 +- .../SettingsDevelopersWebhookDetail.tsx | 10 +- .../components/DateTimeSettings.tsx | 4 +- .../DateTimeSettingsDateFormatSelect.tsx | 2 +- .../DateTimeSettingsTimeFormatSelect.tsx | 2 +- .../client-config/client-config.entity.ts | 3 + .../client-config/client-config.resolver.ts | 1 + 28 files changed, 631 insertions(+), 132 deletions(-) create mode 100644 packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts create mode 100644 packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts create mode 100644 packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js create mode 100644 packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts create mode 100644 packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx create mode 100644 packages/twenty-front/src/modules/settings/developers/webhook/constants/WebhookGraphApiOptionsMap.ts create mode 100644 packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx create mode 100644 packages/twenty-front/src/modules/settings/developers/webhook/utils/__tests__/fetchGraphDataOrThrow.test.js create mode 100644 packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 5950c81d5cc1..7f053fc6b10f 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -141,6 +141,7 @@ export enum CaptchaDriverType { export type ClientConfig = { __typename?: 'ClientConfig'; + analyticsEnabled: Scalars['Boolean']; api: ApiConfig; authProviders: AuthProviders; billing: Billing; @@ -1599,7 +1600,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>; -export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; +export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } }; export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>; @@ -2765,6 +2766,7 @@ export const GetClientConfigDocument = gql` signInPrefilled signUpDisabled debugMode + analyticsEnabled support { supportDriver supportFrontChatId diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index 7a7de0807f1b..ae13d831fb7a 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -32,6 +32,8 @@ import { import { isDefined } from '~/utils/isDefined'; import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates'; +import { DateFormat } from '@/localization/constants/DateFormat'; +import { TimeFormat } from '@/localization/constants/TimeFormat'; import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState'; import { detectDateFormat } from '@/localization/utils/detectDateFormat'; import { detectTimeFormat } from '@/localization/utils/detectTimeFormat'; @@ -143,12 +145,12 @@ export const useAuth = () => { ? getDateFormatFromWorkspaceDateFormat( user.workspaceMember.dateFormat, ) - : detectDateFormat(), + : DateFormat[detectDateFormat()], timeFormat: isDefined(user.workspaceMember.timeFormat) ? getTimeFormatFromWorkspaceTimeFormat( user.workspaceMember.timeFormat, ) - : detectTimeFormat(), + : TimeFormat[detectTimeFormat()], }); } diff --git a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx index 9eccbeb98e10..ed06d3f0ee69 100644 --- a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx +++ b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx @@ -1,23 +1,24 @@ -import { useEffect } from 'react'; -import { useRecoilState, useSetRecoilState } from 'recoil'; - import { apiConfigState } from '@/client-config/states/apiConfigState'; import { authProvidersState } from '@/client-config/states/authProvidersState'; import { billingState } from '@/client-config/states/billingState'; import { captchaProviderState } from '@/client-config/states/captchaProviderState'; import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState'; +import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState'; import { isClientConfigLoadedState } from '@/client-config/states/isClientConfigLoadedState'; import { isDebugModeState } from '@/client-config/states/isDebugModeState'; import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState'; import { isSignUpDisabledState } from '@/client-config/states/isSignUpDisabledState'; import { sentryConfigState } from '@/client-config/states/sentryConfigState'; import { supportChatState } from '@/client-config/states/supportChatState'; +import { useEffect } from 'react'; +import { useRecoilState, useSetRecoilState } from 'recoil'; import { useGetClientConfigQuery } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; export const ClientConfigProviderEffect = () => { const setAuthProviders = useSetRecoilState(authProvidersState); const setIsDebugMode = useSetRecoilState(isDebugModeState); + const setIsAnalyticsEnabled = useSetRecoilState(isAnalyticsEnabledState); const setIsSignInPrefilled = useSetRecoilState(isSignInPrefilledState); const setIsSignUpDisabled = useSetRecoilState(isSignUpDisabledState); @@ -50,6 +51,7 @@ export const ClientConfigProviderEffect = () => { magicLink: false, }); setIsDebugMode(data?.clientConfig.debugMode); + setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled); setIsSignInPrefilled(data?.clientConfig.signInPrefilled); setIsSignUpDisabled(data?.clientConfig.signUpDisabled); @@ -84,6 +86,7 @@ export const ClientConfigProviderEffect = () => { setCaptchaProvider, setChromeExtensionId, setApiConfig, + setIsAnalyticsEnabled, ]); return <>; diff --git a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts index e702acefa4f1..9a060b0d7b2b 100644 --- a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts +++ b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts @@ -16,6 +16,7 @@ export const GET_CLIENT_CONFIG = gql` signInPrefilled signUpDisabled debugMode + analyticsEnabled support { supportDriver supportFrontChatId diff --git a/packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts b/packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts new file mode 100644 index 000000000000..50c0f5c89c25 --- /dev/null +++ b/packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const isAnalyticsEnabledState = createState({ + key: 'isAnalyticsEnabled', + defaultValue: false, +}); diff --git a/packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts b/packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts new file mode 100644 index 000000000000..a1c7f2af3b72 --- /dev/null +++ b/packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts @@ -0,0 +1,11 @@ +import { DateFormat } from '@/localization/constants/DateFormat'; + +type DateFormatWithoutYear = { + [K in keyof typeof DateFormat]: string; +}; +export const DATE_FORMAT_WITHOUT_YEAR: DateFormatWithoutYear = { + SYSTEM: 'SYSTEM', + MONTH_FIRST: 'MMM d', + DAY_FIRST: 'd MMM', + YEAR_FIRST: 'MMM d', +}; diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts b/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts index 2b641f302a63..b267622bf0cc 100644 --- a/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts +++ b/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts @@ -1,8 +1,7 @@ -import { DateFormat } from '@/localization/constants/DateFormat'; import { detectDateFormat } from '@/localization/utils/detectDateFormat'; describe('detectDateFormat', () => { - it('should return DateFormat.MONTH_FIRST if the detected format starts with month', () => { + it('should return MONTH_FIRST if the detected format starts with month', () => { // Mock the Intl.DateTimeFormat to return a specific format const mockDateTimeFormat = jest.fn().mockReturnValue({ formatToParts: () => [ @@ -16,10 +15,10 @@ describe('detectDateFormat', () => { const result = detectDateFormat(); - expect(result).toBe(DateFormat.MONTH_FIRST); + expect(result).toBe('MONTH_FIRST'); }); - it('should return DateFormat.DAY_FIRST if the detected format starts with day', () => { + it('should return DAY_FIRST if the detected format starts with day', () => { // Mock the Intl.DateTimeFormat to return a specific format const mockDateTimeFormat = jest.fn().mockReturnValue({ formatToParts: () => [ @@ -32,10 +31,10 @@ describe('detectDateFormat', () => { const result = detectDateFormat(); - expect(result).toBe(DateFormat.DAY_FIRST); + expect(result).toBe('DAY_FIRST'); }); - it('should return DateFormat.YEAR_FIRST if the detected format starts with year', () => { + it('should return YEAR_FIRST if the detected format starts with year', () => { // Mock the Intl.DateTimeFormat to return a specific format const mockDateTimeFormat = jest.fn().mockReturnValue({ formatToParts: () => [ @@ -48,10 +47,10 @@ describe('detectDateFormat', () => { const result = detectDateFormat(); - expect(result).toBe(DateFormat.YEAR_FIRST); + expect(result).toBe('YEAR_FIRST'); }); - it('should return DateFormat.MONTH_FIRST by default if the detected format does not match any specific order', () => { + it('should return MONTH_FIRST by default if the detected format does not match any specific order', () => { // Mock the Intl.DateTimeFormat to return a specific format const mockDateTimeFormat = jest.fn().mockReturnValue({ formatToParts: () => [ @@ -64,6 +63,6 @@ describe('detectDateFormat', () => { const result = detectDateFormat(); - expect(result).toBe(DateFormat.MONTH_FIRST); + expect(result).toBe('MONTH_FIRST'); }); }); diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts b/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts index 6433495789ee..9445068a5f7f 100644 --- a/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts +++ b/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts @@ -1,8 +1,7 @@ -import { TimeFormat } from '@/localization/constants/TimeFormat'; import { detectTimeFormat } from '@/localization/utils/detectTimeFormat'; describe('detectTimeFormat', () => { - it('should return TimeFormat.HOUR_12 if the hour format is 12-hour', () => { + it('should return HOUR_12 if the hour format is 12-hour', () => { // Mock the resolvedOptions method to return hour12 as true const mockResolvedOptions = jest.fn(() => ({ hour12: true })); Intl.DateTimeFormat = jest.fn().mockImplementation(() => ({ @@ -11,11 +10,11 @@ describe('detectTimeFormat', () => { const result = detectTimeFormat(); - expect(result).toBe(TimeFormat.HOUR_12); + expect(result).toBe('HOUR_12'); expect(mockResolvedOptions).toHaveBeenCalled(); }); - it('should return TimeFormat.HOUR_24 if the hour format is 24-hour', () => { + it('should return HOUR_24 if the hour format is 24-hour', () => { // Mock the resolvedOptions method to return hour12 as false const mockResolvedOptions = jest.fn(() => ({ hour12: false })); Intl.DateTimeFormat = jest.fn().mockImplementation(() => ({ @@ -24,7 +23,7 @@ describe('detectTimeFormat', () => { const result = detectTimeFormat(); - expect(result).toBe(TimeFormat.HOUR_24); + expect(result).toBe('HOUR_24'); expect(mockResolvedOptions).toHaveBeenCalled(); }); }); diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js b/packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js new file mode 100644 index 000000000000..4caee3aedf0d --- /dev/null +++ b/packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js @@ -0,0 +1,90 @@ +import { detectDateFormat } from '@/localization/utils/detectDateFormat'; +import { formatDateISOStringToDateTimeSimplified } from '@/localization/utils/formatDateISOStringToDateTimeSimplified'; +import { formatInTimeZone } from 'date-fns-tz'; +// Mock the imported modules +jest.mock('@/localization/utils/detectDateFormat'); +jest.mock('date-fns-tz'); + +describe('formatDateISOStringToDateTimeSimplified', () => { + const mockDate = new Date('2023-08-15T10:30:00Z'); + const mockTimeZone = 'America/New_York'; + const mockTimeFormat = 'HH:mm'; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should format the date correctly when DATE_FORMAT is MONTH_FIRST', () => { + detectDateFormat.mockReturnValue('MONTH_FIRST'); + formatInTimeZone.mockReturnValue('Oct 15 · 06:30'); + + const result = formatDateISOStringToDateTimeSimplified( + mockDate, + mockTimeZone, + mockTimeFormat, + ); + + expect(detectDateFormat).toHaveBeenCalled(); + expect(formatInTimeZone).toHaveBeenCalledWith( + mockDate, + mockTimeZone, + 'MMM d · HH:mm', + ); + expect(result).toBe('Oct 15 · 06:30'); + }); + + it('should format the date correctly when DATE_FORMAT is DAY_FIRST', () => { + detectDateFormat.mockReturnValue('DAY_FIRST'); + formatInTimeZone.mockReturnValue('15 Oct · 06:30'); + + const result = formatDateISOStringToDateTimeSimplified( + mockDate, + mockTimeZone, + mockTimeFormat, + ); + + expect(detectDateFormat).toHaveBeenCalled(); + expect(formatInTimeZone).toHaveBeenCalledWith( + mockDate, + mockTimeZone, + 'd MMM · HH:mm', + ); + expect(result).toBe('15 Oct · 06:30'); + }); + + it('should use the provided time format', () => { + detectDateFormat.mockReturnValue('MONTH_FIRST'); + formatInTimeZone.mockReturnValue('Oct 15 · 6:30 AM'); + + const result = formatDateISOStringToDateTimeSimplified( + mockDate, + mockTimeZone, + 'h:mm aa', + ); + + expect(formatInTimeZone).toHaveBeenCalledWith( + mockDate, + mockTimeZone, + 'MMM d · h:mm aa', + ); + expect(result).toBe('Oct 15 · 6:30 AM'); + }); + + it('should handle different time zones', () => { + detectDateFormat.mockReturnValue('MONTH_FIRST'); + formatInTimeZone.mockReturnValue('Oct 16 · 02:30'); + + const result = formatDateISOStringToDateTimeSimplified( + mockDate, + 'Asia/Tokyo', + mockTimeFormat, + ); + + expect(formatInTimeZone).toHaveBeenCalledWith( + mockDate, + 'Asia/Tokyo', + 'MMM d · HH:mm', + ); + expect(result).toBe('Oct 16 · 02:30'); + }); +}); diff --git a/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts b/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts index b503ef826e60..e38b018df445 100644 --- a/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts +++ b/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts @@ -1,6 +1,6 @@ import { DateFormat } from '@/localization/constants/DateFormat'; -export const detectDateFormat = (): DateFormat => { +export const detectDateFormat = (): keyof typeof DateFormat => { const date = new Date(); const formatter = new Intl.DateTimeFormat(navigator.language); const parts = formatter.formatToParts(date); @@ -9,9 +9,9 @@ export const detectDateFormat = (): DateFormat => { .filter((part) => ['year', 'month', 'day'].includes(part.type)) .map((part) => part.type); - if (partOrder[0] === 'month') return DateFormat.MONTH_FIRST; - if (partOrder[0] === 'day') return DateFormat.DAY_FIRST; - if (partOrder[0] === 'year') return DateFormat.YEAR_FIRST; + if (partOrder[0] === 'month') return 'MONTH_FIRST'; + if (partOrder[0] === 'day') return 'DAY_FIRST'; + if (partOrder[0] === 'year') return 'YEAR_FIRST'; - return DateFormat.MONTH_FIRST; + return 'MONTH_FIRST'; }; diff --git a/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts b/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts index 01bad17167a5..d6d914d83637 100644 --- a/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts +++ b/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts @@ -1,14 +1,14 @@ import { TimeFormat } from '@/localization/constants/TimeFormat'; import { isDefined } from '~/utils/isDefined'; -export const detectTimeFormat = () => { +export const detectTimeFormat = (): keyof typeof TimeFormat => { const isHour12 = Intl.DateTimeFormat(navigator.language, { hour: 'numeric', }).resolvedOptions().hour12; if (isDefined(isHour12) && isHour12) { - return TimeFormat.HOUR_12; + return 'HOUR_12'; } - return TimeFormat.HOUR_24; + return 'HOUR_24'; }; diff --git a/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts b/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts new file mode 100644 index 000000000000..c96d9f2f885d --- /dev/null +++ b/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts @@ -0,0 +1,18 @@ +import { DATE_FORMAT_WITHOUT_YEAR } from '@/localization/constants/DateFormatWithoutYear'; +import { TimeFormat } from '@/localization/constants/TimeFormat'; +import { detectDateFormat } from '@/localization/utils/detectDateFormat'; +import { formatInTimeZone } from 'date-fns-tz'; + +export const formatDateISOStringToDateTimeSimplified = ( + date: Date, + timeZone: string, + timeFormat: TimeFormat, +) => { + const simplifiedDateFormat = DATE_FORMAT_WITHOUT_YEAR[detectDateFormat()]; + + return formatInTimeZone( + date, + timeZone, + `${simplifiedDateFormat} · ${timeFormat}`, + ); +}; diff --git a/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts b/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts index f32bdbb93355..09293fbb8ec8 100644 --- a/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts +++ b/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts @@ -7,7 +7,7 @@ export const getDateFormatFromWorkspaceDateFormat = ( ) => { switch (workspaceDateFormat) { case WorkspaceMemberDateFormatEnum.System: - return detectDateFormat(); + return DateFormat[detectDateFormat()]; case WorkspaceMemberDateFormatEnum.MonthFirst: return DateFormat.MONTH_FIRST; case WorkspaceMemberDateFormatEnum.DayFirst: diff --git a/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts b/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts index f6aebb43779b..7519d0cb4068 100644 --- a/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts +++ b/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts @@ -7,7 +7,7 @@ export const getTimeFormatFromWorkspaceTimeFormat = ( ) => { switch (workspaceTimeFormat) { case WorkspaceMemberTimeFormatEnum.System: - return detectTimeFormat(); + return TimeFormat[detectTimeFormat()]; case WorkspaceMemberTimeFormatEnum.Hour_24: return TimeFormat.HOUR_24; case WorkspaceMemberTimeFormatEnum.Hour_12: diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx new file mode 100644 index 000000000000..40925c5d3830 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx @@ -0,0 +1,89 @@ +import { formatDateISOStringToDateTimeSimplified } from '@/localization/utils/formatDateISOStringToDateTimeSimplified'; +import { UserContext } from '@/users/contexts/UserContext'; +import styled from '@emotion/styled'; +import { Point } from '@nivo/line'; +import { ReactElement, useContext } from 'react'; + +const StyledTooltipContainer = styled.div` + align-items: center; + border-radius: ${({ theme }) => theme.border.radius.md}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + display: flex; + width: 128px; + flex-direction: column; + justify-content: center; + background: ${({ theme }) => theme.background.transparent.secondary}; + box-shadow: ${({ theme }) => theme.boxShadow.light}; + backdrop-filter: ${({ theme }) => theme.blur.medium}; +`; + +const StyledTooltipDateContainer = styled.div` + align-items: flex-start; + align-self: stretch; + display: flex; + justify-content: center; + font-weight: ${({ theme }) => theme.font.weight.medium}; + font-family: ${({ theme }) => theme.font.family}; + gap: ${({ theme }) => theme.spacing(2)}; + color: ${({ theme }) => theme.font.color.secondary}; + padding: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledTooltipDataRow = styled.div` + align-items: flex-start; + align-self: stretch; + display: flex; + justify-content: space-between; + color: ${({ theme }) => theme.font.color.tertiary}; + padding: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledLine = styled.div` + background-color: ${({ theme }) => theme.border.color.medium}; + height: 1px; + width: 100%; +`; +const StyledColorPoint = styled.div<{ color: string }>` + background-color: ${({ color }) => color}; + border-radius: 50%; + height: 8px; + width: 8px; + display: inline-block; +`; +const StyledDataDefinition = styled.div` + display: flex; + align-items: center; + gap: ${({ theme }) => theme.spacing(2)}; +`; +const StyledSpan = styled.span` + color: ${({ theme }) => theme.font.color.primary}; +`; +type SettingsDevelopersWebhookTooltipProps = { + point: Point; +}; +export const SettingsDevelopersWebhookTooltip = ({ + point, +}: SettingsDevelopersWebhookTooltipProps): ReactElement => { + const { timeFormat, timeZone } = useContext(UserContext); + const windowInterval = new Date(point.data.x); + const windowIntervalDate = formatDateISOStringToDateTimeSimplified( + windowInterval, + timeZone, + timeFormat, + ); + return ( + + + {windowIntervalDate} + + + + + + {String(point.serieId)} + + {String(point.data.y)} + + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx index eb2e359fff16..9626c6712eef 100644 --- a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx +++ b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx @@ -1,8 +1,13 @@ +import { SettingsDevelopersWebhookTooltip } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip'; +import { useGraphData } from '@/settings/developers/webhook/hooks/useGraphData'; import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState'; +import { Select } from '@/ui/input/components/Select'; +import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { ResponsiveLine } from '@nivo/line'; import { Section } from '@react-email/components'; -import { useRecoilValue } from 'recoil'; +import { useState } from 'react'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { H2Title } from 'twenty-ui'; export type NivoLineInput = { @@ -14,22 +19,102 @@ export type NivoLineInput = { }>; }; const StyledGraphContainer = styled.div` - height: 200px; - width: 100%; + background-color: ${({ theme }) => theme.background.secondary}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + border-radius: ${({ theme }) => theme.border.radius.md}; + height: 199px; + + padding: ${({ theme }) => theme.spacing(4, 2, 2, 2)}; + width: 496px; +`; +const StyledTitleContainer = styled.div` + align-items: flex-start; + display: flex; + justify-content: space-between; `; -export const SettingsDeveloppersWebhookUsageGraph = () => { + +type SettingsDevelopersWebhookUsageGraphProps = { + webhookId: string; +}; + +export const SettingsDevelopersWebhookUsageGraph = ({ + webhookId, +}: SettingsDevelopersWebhookUsageGraphProps) => { const webhookGraphData = useRecoilValue(webhookGraphDataState); + const setWebhookGraphData = useSetRecoilState(webhookGraphDataState); + const theme = useTheme(); + + const [windowLengthGraphOption, setWindowLengthGraphOption] = useState< + '7D' | '1D' | '12H' | '4H' + >('7D'); + + const { fetchGraphData } = useGraphData(webhookId); return ( <> {webhookGraphData.length ? (
- + + + Boolean) debugMode: boolean; + @Field(() => Boolean) + analyticsEnabled: boolean; + @Field(() => Support) support: Support; diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts index 3615066a4390..f6ba1aaf4abd 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts @@ -48,6 +48,7 @@ export class ClientConfigResolver { 'MUTATION_MAXIMUM_AFFECTED_RECORDS', ), }, + analyticsEnabled: this.environmentService.get('ANALYTICS_ENABLED'), }; return Promise.resolve(clientConfig); From 6fef12596536d1a0857f153433ad133d9fc44b92 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Fri, 18 Oct 2024 14:50:04 +0200 Subject: [PATCH 17/67] Use search instead of findMany in relation pickers (#7798) First step of #https://github.com/twentyhq/twenty/issues/3298. Here we update the search endpoint to allow for a filter argument, which we currently use in the relation pickers to restrict or exclude ids from search. In a future PR we will try to simplify the search logic in the FE --- .../effect-components/GotoHotkeysEffect.tsx | 1 + ...ionDrawerSectionForObjectMetadataItems.tsx | 1 + ...bjectMetadataItemsRelationPickerEffect.tsx | 29 -------- .../object-record/hooks/useSearchRecords.ts | 10 ++- .../components/RelationFromManyFieldInput.tsx | 2 - .../RecordDetailRelationSection.tsx | 2 - .../SingleEntitySelectMenuItemsWithSearch.tsx | 4 -- .../hooks/useRelationPickerEntitiesOptions.ts | 17 ++--- .../utils/generateSearchRecordsQuery.ts | 8 ++- .../useFilteredSearchEntityQuery.test.tsx | 4 +- .../hooks/useFilteredSearchEntityQuery.ts | 69 +++---------------- .../twenty-front/src/testing/graphqlMocks.ts | 46 +++++++++++++ .../graphql-query-search-resolver.service.ts | 46 ++++++++++--- .../workspace-resolvers-builder.interface.ts | 5 +- .../utils/get-resolver-args.util.ts | 4 ++ 15 files changed, 123 insertions(+), 125 deletions(-) delete mode 100644 packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsRelationPickerEffect.tsx diff --git a/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx index 15d371f9f44a..202b58b963e5 100644 --- a/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx +++ b/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx @@ -11,6 +11,7 @@ export const GotoHotkeys = () => { return nonSystemActiveObjectMetadataItems.map((objectMetadataItem) => ( diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx index 9960670d1beb..f90a160b57c4 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx @@ -85,6 +85,7 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({ objectMetadataItemsForNavigationItems.map( (objectMetadataItem) => ( ), diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsRelationPickerEffect.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsRelationPickerEffect.tsx deleted file mode 100644 index 1d2d1ecd74b9..000000000000 --- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsRelationPickerEffect.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { useEffect } from 'react'; - -import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker'; - -export const ObjectMetadataItemsRelationPickerEffect = ({ - relationPickerScopeId, -}: { - relationPickerScopeId?: string; -} = {}) => { - const { setSearchQuery } = useRelationPicker({ relationPickerScopeId }); - - const computeFilterFields = (relationPickerType: string) => { - if (relationPickerType === 'company') { - return ['name']; - } - - if (['workspaceMember', 'person'].includes(relationPickerType)) { - return ['name.firstName', 'name.lastName']; - } - - return ['name']; - }; - - useEffect(() => { - setSearchQuery({ computeFilterFields }); - }, [setSearchQuery]); - - return <>; -}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useSearchRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useSearchRecords.ts index 175f84554f19..7c1f90162597 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useSearchRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useSearchRecords.ts @@ -13,10 +13,11 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useQuery, WatchQueryFetchPolicy } from '@apollo/client'; import { useMemo } from 'react'; import { useRecoilValue } from 'recoil'; +import { isDefined } from '~/utils/isDefined'; import { logError } from '~/utils/logError'; export type UseSearchRecordsParams = ObjectMetadataItemIdentifier & - RecordGqlOperationVariables & { + Pick & { onError?: (error?: Error) => void; skip?: boolean; recordGqlFields?: RecordGqlOperationGqlRecordFields; @@ -29,6 +30,7 @@ export const useSearchRecords = ({ searchInput, limit, skip, + filter, recordGqlFields, fetchPolicy, }: UseSearchRecordsParams) => { @@ -45,10 +47,14 @@ export const useSearchRecords = ({ const { data, loading, error, previousData } = useQuery(searchRecordsQuery, { skip: - skip || !objectMetadataItem || !currentWorkspaceMember || !searchInput, + skip || + !objectMetadataItem || + !currentWorkspaceMember || + !isDefined(searchInput), variables: { search: searchInput, limit: limit, + filter: filter, }, fetchPolicy: fetchPolicy, onError: (error) => { diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx index be1cba38161b..f0cbf686dfc2 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/RelationFromManyFieldInput.tsx @@ -1,6 +1,5 @@ import { useContext } from 'react'; -import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { RelationFromManyFieldInputMultiRecordsEffect } from '@/object-record/record-field/meta-types/input/components/RelationFromManyFieldInputMultiRecordsEffect'; @@ -54,7 +53,6 @@ export const RelationFromManyFieldInput = ({ return ( <> - ) : ( <> - - gql` - query Search${capitalize(objectMetadataItem.namePlural)}($search: String, $limit: Int) { - ${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(searchInput: $search, limit: $limit){ + query Search${capitalize(objectMetadataItem.namePlural)}($search: String, $limit: Int, $filter: ${capitalize( + objectMetadataItem.nameSingular, + )}FilterInput) { + ${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(searchInput: $search, limit: $limit, filter: $filter){ edges { node ${mapObjectMetadataToGraphQLQuery({ objectMetadataItems, diff --git a/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchEntityQuery.test.tsx b/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchEntityQuery.test.tsx index 263b70decf0f..6b2409af45a3 100644 --- a/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchEntityQuery.test.tsx +++ b/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchEntityQuery.test.tsx @@ -80,13 +80,11 @@ describe('useFilteredSearchEntityQuery', () => { setMetadataItems(generatedMockObjectMetadataItems); return useFilteredSearchEntityQuery({ - orderByField: 'name', - filters: [{ fieldNames: ['name'], filter: 'Entity' }], - sortOrder: 'AscNullsLast', selectedIds: ['1'], limit: 10, excludeRecordIds: ['2'], objectNameSingular: 'person', + searchFilter: 'Entity', }); }, { wrapper: Wrapper }, diff --git a/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts b/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts index 06ba43f92e60..9d6973385230 100644 --- a/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts +++ b/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts @@ -1,39 +1,26 @@ -import { isNonEmptyString } from '@sniptt/guards'; - import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier'; import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit'; -import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { useSearchRecords } from '@/object-record/hooks/useSearchRecords'; import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect'; import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; -import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables'; -import { OrderBy } from '@/types/OrderBy'; -import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields'; import { isDefined } from '~/utils/isDefined'; -type SearchFilter = { fieldNames: string[]; filter: string | number }; - // TODO: use this for all search queries, because we need selectedEntities and entitiesToSelect each time we want to search // Filtered entities to select are export const useFilteredSearchEntityQuery = ({ - orderByField, - filters, - sortOrder = 'AscNullsLast', selectedIds, limit, excludeRecordIds = [], objectNameSingular, + searchFilter, }: { - orderByField: string; - filters: SearchFilter[]; - sortOrder?: OrderBy; selectedIds: string[]; limit?: number; excludeRecordIds?: string[]; objectNameSingular: string; + searchFilter?: string; }): EntitiesForMultipleEntitySelect => { const { mapToObjectRecordIdentifier } = useMapToObjectRecordIdentifier({ objectNameSingular, @@ -46,55 +33,21 @@ export const useFilteredSearchEntityQuery = ({ const selectedIdsFilter = { id: { in: selectedIds } }; const { loading: selectedRecordsLoading, records: selectedRecords } = - useFindManyRecords({ + useSearchRecords({ objectNameSingular, filter: selectedIdsFilter, - orderBy: [{ [orderByField]: sortOrder }], skip: !selectedIds.length, + searchInput: searchFilter, }); - const searchFilters = filters.map(({ fieldNames, filter }) => { - if (!isNonEmptyString(filter)) { - return undefined; - } - - const formattedFilters = fieldNames.reduce( - (previousValue: RecordGqlOperationFilter[], fieldName) => { - const [parentFieldName, subFieldName] = fieldName.split('.'); - - if (isNonEmptyString(subFieldName)) { - // Composite field - return [ - ...previousValue, - ...generateILikeFiltersForCompositeFields(filter, parentFieldName, [ - subFieldName, - ]), - ]; - } - - return [ - ...previousValue, - { - [fieldName]: { - ilike: `%${filter}%`, - }, - }, - ]; - }, - [], - ); - - return makeOrFilterVariables(formattedFilters); - }); - const { loading: filteredSelectedRecordsLoading, records: filteredSelectedRecords, - } = useFindManyRecords({ + } = useSearchRecords({ objectNameSingular, - filter: makeAndFilterVariables([...searchFilters, selectedIdsFilter]), - orderBy: [{ [orderByField]: sortOrder }], + filter: selectedIdsFilter, skip: !selectedIds.length, + searchInput: searchFilter, }); const notFilterIds = [...selectedIds, ...excludeRecordIds]; @@ -102,11 +55,11 @@ export const useFilteredSearchEntityQuery = ({ ? { not: { id: { in: notFilterIds } } } : undefined; const { loading: recordsToSelectLoading, records: recordsToSelect } = - useFindManyRecords({ + useSearchRecords({ objectNameSingular, - filter: makeAndFilterVariables([...searchFilters, notFilter]), + filter: notFilter, limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT, - orderBy: [{ [orderByField]: sortOrder }], + searchInput: searchFilter, }); return { diff --git a/packages/twenty-front/src/testing/graphqlMocks.ts b/packages/twenty-front/src/testing/graphqlMocks.ts index 1cb7c4b3aef5..d634890a5f9b 100644 --- a/packages/twenty-front/src/testing/graphqlMocks.ts +++ b/packages/twenty-front/src/testing/graphqlMocks.ts @@ -113,6 +113,52 @@ export const graphqlMocks = { }, }); }), + graphql.query('SearchWorkspaceMembers', () => { + return HttpResponse.json({ + data: { + searchWorkspaceMembers: { + edges: mockWorkspaceMembers.map((member) => ({ + node: { + ...member, + messageParticipants: { + edges: [], + __typename: 'MessageParticipantConnection', + }, + authoredAttachments: { + edges: [], + __typename: 'AttachmentConnection', + }, + authoredComments: { + edges: [], + __typename: 'CommentConnection', + }, + accountOwnerForCompanies: { + edges: [], + __typename: 'CompanyConnection', + }, + authoredActivities: { + edges: [], + __typename: 'ActivityConnection', + }, + favorites: { + edges: [], + __typename: 'FavoriteConnection', + }, + connectedAccounts: { + edges: [], + __typename: 'ConnectedAccountConnection', + }, + assignedActivities: { + edges: [], + __typename: 'ActivityConnection', + }, + }, + cursor: null, + })), + }, + }, + }); + }), graphql.query('FindManyViewFields', ({ variables }) => { const viewId = variables.filter.view.eq; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts index cfd570281187..378dfff97fc4 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts @@ -4,16 +4,19 @@ import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/int import { Record as IRecord, OrderByDirection, + RecordFilter, } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; import { IConnection } from 'src/engine/api/graphql/workspace-query-runner/interfaces/connection.interface'; import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { SearchResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { QUERY_MAX_RECORDS } from 'src/engine/api/graphql/graphql-query-runner/constants/query-max-records.constant'; +import { GraphqlQueryParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser'; import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { isDefined } from 'src/utils/is-defined'; @Injectable() export class GraphqlQuerySearchResolverService @@ -24,11 +27,19 @@ export class GraphqlQuerySearchResolverService private readonly featureFlagService: FeatureFlagService, ) {} - async resolve( + async resolve< + ObjectRecord extends IRecord = IRecord, + Filter extends RecordFilter = RecordFilter, + >( args: SearchResolverArgs, options: WorkspaceQueryRunnerOptions, ): Promise> { - const { authContext, objectMetadataItem, objectMetadataMap } = options; + const { + authContext, + objectMetadataItem, + objectMetadataMapItem, + objectMetadataMap, + } = options; const repository = await this.twentyORMGlobalManager.getRepositoryForWorkspace( @@ -39,7 +50,7 @@ export class GraphqlQuerySearchResolverService const typeORMObjectRecordsParser = new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); - if (!args.searchInput) { + if (!isDefined(args.searchInput)) { return typeORMObjectRecordsParser.createConnection({ objectRecords: [], objectName: objectMetadataItem.nameSingular, @@ -54,11 +65,27 @@ export class GraphqlQuerySearchResolverService const limit = args?.limit ?? QUERY_MAX_RECORDS; - const resultsWithTsVector = (await repository - .createQueryBuilder() - .where(`"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery(:searchTerms)`, { - searchTerms, - }) + const queryBuilder = repository.createQueryBuilder( + objectMetadataItem.nameSingular, + ); + const graphqlQueryParser = new GraphqlQueryParser( + objectMetadataMapItem.fields, + objectMetadataMap, + ); + + const queryBuilderWithFilter = graphqlQueryParser.applyFilterToBuilder( + queryBuilder, + objectMetadataMapItem.nameSingular, + args.filter ?? ({} as Filter), + ); + + const resultsWithTsVector = (await queryBuilderWithFilter + .andWhere( + searchTerms === '' + ? `"${SEARCH_VECTOR_FIELD.name}" IS NOT NULL` + : `"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery(:searchTerms)`, + searchTerms === '' ? {} : { searchTerms }, + ) .orderBy( `ts_rank("${SEARCH_VECTOR_FIELD.name}", to_tsquery(:searchTerms))`, 'DESC', @@ -84,6 +111,9 @@ export class GraphqlQuerySearchResolverService } private formatSearchTerms(searchTerm: string) { + if (searchTerm === '') { + return ''; + } const words = searchTerm.trim().split(/\s+/); const formattedWords = words.map((word) => { const escapedWord = word.replace(/[\\:'&|!()]/g, '\\$&'); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts index 219b185c451e..69bc97777b10 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface.ts @@ -48,8 +48,11 @@ export interface FindDuplicatesResolverArgs< data?: Data[]; } -export interface SearchResolverArgs { +export interface SearchResolverArgs< + Filter extends RecordFilter = RecordFilter, +> { searchInput?: string; + filter?: Filter; limit?: number; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts index 7e1755218730..b50047e62e2a 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/get-resolver-args.util.ts @@ -147,6 +147,10 @@ export const getResolverArgs = ( type: GraphQLInt, isNullable: true, }, + filter: { + kind: InputTypeDefinitionKind.Filter, + isNullable: true, + }, }; default: throw new Error(`Unknown resolver type: ${type}`); From 5a23d1eea8b61d026d2c7dbcfc87489a28d9f00a Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Fri, 18 Oct 2024 15:14:08 +0200 Subject: [PATCH 18/67] [sentry fix] handle undefined createdBy case (#7818) Fix sentry https://twenty-v7.sentry.io/issues/5998085857/?alert_rule_id=15135094&alert_type=issue&environment=prod¬ification_uuid=9a6c6c3d-6bd1-4c7f-bf27-8acb3571bbc3&project=4507072499810304&referrer=discord --- .../core-modules/actor/query-hooks/created-by.pre-query-hook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/twenty-server/src/engine/core-modules/actor/query-hooks/created-by.pre-query-hook.ts b/packages/twenty-server/src/engine/core-modules/actor/query-hooks/created-by.pre-query-hook.ts index 2a90843f1d69..0ef8bab573b7 100644 --- a/packages/twenty-server/src/engine/core-modules/actor/query-hooks/created-by.pre-query-hook.ts +++ b/packages/twenty-server/src/engine/core-modules/actor/query-hooks/created-by.pre-query-hook.ts @@ -101,7 +101,7 @@ export class CreatedByPreQueryHook implements WorkspaceQueryHookInstance { for (const datum of payload.data) { // Front-end can fill the source field - if (createdBy && (!datum.createdBy || !datum.createdBy.name)) { + if (createdBy && (!datum.createdBy || !datum.createdBy?.name)) { datum.createdBy = { ...createdBy, source: datum.createdBy?.source ?? createdBy.source, From 9c8eeeea9d01f28909fc13cadbaa7c4e92706462 Mon Sep 17 00:00:00 2001 From: martmull Date: Fri, 18 Oct 2024 16:13:12 +0200 Subject: [PATCH 19/67] Start twenty-server:worker when npx nx start (#7820) - start the worker service when launching `npx nx start` - update documentation --- package.json | 2 +- .../twenty-website/src/content/developers/local-setup.mdx | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index a4dc90df92ac..f86d4c1b9d08 100644 --- a/package.json +++ b/package.json @@ -347,7 +347,7 @@ "version": "0.2.1", "nx": {}, "scripts": { - "start": "npx nx run-many -t start -p twenty-server twenty-front" + "start": "npx nx run-many -t start worker -p twenty-server twenty-front" }, "workspaces": { "packages": [ diff --git a/packages/twenty-website/src/content/developers/local-setup.mdx b/packages/twenty-website/src/content/developers/local-setup.mdx index 283aba5a9614..b66ea2ee45aa 100644 --- a/packages/twenty-website/src/content/developers/local-setup.mdx +++ b/packages/twenty-website/src/content/developers/local-setup.mdx @@ -225,13 +225,14 @@ Setup your database with the following command: npx nx database:reset twenty-server ``` -Start the server and the frontend: +Start the server, the worker and the frontend services: ```bash npx nx start twenty-server +npx nx worker twenty-server npx nx start twenty-front ``` -Alternatively, you can start both applications at once: +Alternatively, you can start all services at once: ```bash npx nx start ``` From e50117e3b096bf252304da454f7199aedca7c35f Mon Sep 17 00:00:00 2001 From: NitinPSingh <71833171+NitinPSingh@users.noreply.github.com> Date: Fri, 18 Oct 2024 21:32:43 +0530 Subject: [PATCH 20/67] fix #7781 made kanban board title and checkbox 24px (#7815) # issue: #7781 - [x] titlechip to 24px - [x] checkbox to 24px ![Screenshot 2024-10-18 134759](https://github.com/user-attachments/assets/e9d347e3-41b8-4b0d-a072-d139ed982971) ![Screenshot 2024-10-18 134708](https://github.com/user-attachments/assets/8b83f6dd-96ac-4a4e-b6ae-85d3e2923fb9) --- .../src/modules/ui/input/components/Checkbox.tsx | 10 ++++++---- .../twenty-ui/src/display/chip/components/Chip.tsx | 2 +- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/twenty-front/src/modules/ui/input/components/Checkbox.tsx b/packages/twenty-front/src/modules/ui/input/components/Checkbox.tsx index fd3327c63f23..e19d3cb70976 100644 --- a/packages/twenty-front/src/modules/ui/input/components/Checkbox.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/Checkbox.tsx @@ -52,7 +52,10 @@ const StyledInputContainer = styled.div` cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; display: flex; - padding: ${({ theme }) => theme.spacing(1)}; + padding: ${({ theme, checkboxSize }) => + checkboxSize === CheckboxSize.Large + ? theme.spacing(1.5) + : theme.spacing(1.25)}; position: relative; ${({ hoverable, isChecked, theme, indeterminate, disabled }) => { if (!hoverable || disabled === true) return ''; @@ -126,10 +129,9 @@ const StyledInput = styled.input` } & + label > svg { - --padding: ${({ checkboxSize }) => - checkboxSize === CheckboxSize.Large ? '2px' : '1px'}; + --padding: 0px; --size: ${({ checkboxSize }) => - checkboxSize === CheckboxSize.Large ? '16px' : '12px'}; + checkboxSize === CheckboxSize.Large ? '20px' : '14px'}; height: var(--size); left: var(--padding); position: absolute; diff --git a/packages/twenty-ui/src/display/chip/components/Chip.tsx b/packages/twenty-ui/src/display/chip/components/Chip.tsx index 48795fd42551..3bf0cd9bb90d 100644 --- a/packages/twenty-ui/src/display/chip/components/Chip.tsx +++ b/packages/twenty-ui/src/display/chip/components/Chip.tsx @@ -66,7 +66,7 @@ const StyledContainer = withTheme(styled.div< display: inline-flex; justify-content: center; gap: ${({ theme }) => theme.spacing(1)}; - height: ${({ theme }) => theme.spacing(3)}; + height: ${({ theme }) => theme.spacing(4)}; max-width: ${({ maxWidth }) => maxWidth ? `calc(${maxWidth}px - 2 * var(--chip-horizontal-padding))` From 17b934e22b66562952ad7c2d53286d89eccc2d51 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Fri, 18 Oct 2024 18:36:01 +0200 Subject: [PATCH 21/67] Migrate to shipfox --- .github/workflows/ci-front.yaml | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml index 595081c0a973..506a6dce0e5e 100644 --- a/.github/workflows/ci-front.yaml +++ b/.github/workflows/ci-front.yaml @@ -43,32 +43,6 @@ jobs: - name: Front / Build storybook run: npx nx storybook:build twenty-front front-sb-test: - runs-on: ci-8-cores - timeout-minutes: 60 - needs: front-sb-build - strategy: - matrix: - storybook_scope: [pages, modules] - env: - REACT_APP_SERVER_BASE_URL: http://localhost:3000 - NX_REJECT_UNKNOWN_LOCAL_CACHE: 0 - steps: - - name: Fetch local actions - uses: actions/checkout@v4 - - name: Install dependencies - uses: ./.github/workflows/actions/yarn-install - - name: Install Playwright - run: cd packages/twenty-front && npx playwright install - - name: Front / Restore Storybook Task Cache - uses: ./.github/workflows/actions/task-cache - with: - tag: scope:frontend - tasks: storybook:build - - name: Front / Write .env - run: npx nx reset:env twenty-front - - name: Run storybook tests - run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }} - front-sb-test-shipfox: runs-on: shipfox-8vcpu-ubuntu-2204 timeout-minutes: 60 needs: front-sb-build @@ -95,7 +69,7 @@ jobs: - name: Run storybook tests run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }} front-sb-test-performance: - runs-on: ci-8-cores + runs-on: shipfox-8vcpu-ubuntu-2204 timeout-minutes: 60 env: REACT_APP_SERVER_BASE_URL: http://localhost:3000 From d4457d756cf847cd3615bd0f33fb708b481f6d6c Mon Sep 17 00:00:00 2001 From: Weiko Date: Fri, 18 Oct 2024 18:59:50 +0200 Subject: [PATCH 22/67] Fix custom index creation missing indexFieldMetadatas (#7832) ## Context Regression on custom index creation where indexFieldMetadatas were not saved properly in the DB. This is because we recently changed save() to upsert() in the indexMetadataService and upsert does not handle nesting insert properly. I'm suggesting another fix where we separate indexMetadata creation and index migration creation in 2 different functions. Since the goal was to be able to recreate the index after being deleted when we changed the tsvector expression and indexMetadata was actually not deleted, we didn't need to recreate that part (hence the upsert) and only needed to run a migration to create the actual index in the workspace schema. I've updated the different services and now only call createIndexMigration when we update a search vector expression. Note: this is also fixing the sync-metadata command when running on a workspace with a custom object (including the seeded workspace which has the 'rocket' custom object), failing due to the missing 'searchVector' indexFieldMetadata --- .../index-metadata/index-metadata.service.ts | 84 +++++++++++++------ .../relation-metadata.service.ts | 2 +- .../metadata-modules/search/search.service.ts | 4 +- 3 files changed, 62 insertions(+), 28 deletions(-) diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts index 1d75cbd7ad3f..6b8ff8fcb7c0 100644 --- a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { isDefined } from 'class-validator'; -import { InsertResult, Repository } from 'typeorm'; +import { Repository } from 'typeorm'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { @@ -28,7 +28,7 @@ export class IndexMetadataService { private readonly workspaceMigrationService: WorkspaceMigrationService, ) {} - async createIndex( + async createIndexMetadata( workspaceId: string, objectMetadata: ObjectMetadataEntity, fieldMetadataToIndex: Partial[], @@ -45,42 +45,76 @@ export class IndexMetadataService { const indexName = `IDX_${generateDeterministicIndexName([tableName, ...columnNames])}`; - let result: InsertResult; + let result: IndexMetadataEntity; - try { - result = await this.indexMetadataRepository.upsert( - { - name: indexName, - indexFieldMetadatas: fieldMetadataToIndex.map( - (fieldMetadata, index) => { - return { - fieldMetadataId: fieldMetadata.id, - order: index, - }; - }, - ), - workspaceId, - objectMetadataId: objectMetadata.id, - ...(isDefined(indexType) ? { indexType: indexType } : {}), - isCustom: isCustom, - }, - { - conflictPaths: ['workspaceId', 'name', 'objectMetadataId'], - skipUpdateIfNoValuesChanged: true, - }, + const existingIndex = await this.indexMetadataRepository.findOne({ + where: { + name: indexName, + workspaceId, + objectMetadataId: objectMetadata.id, + }, + }); + + if (existingIndex) { + throw new Error( + `Index ${indexName} on object metadata ${objectMetadata.nameSingular} already exists`, ); + } + + try { + result = await this.indexMetadataRepository.save({ + name: indexName, + indexFieldMetadatas: fieldMetadataToIndex.map( + (fieldMetadata, index) => ({ + fieldMetadataId: fieldMetadata.id, + order: index, + }), + ), + workspaceId, + objectMetadataId: objectMetadata.id, + ...(isDefined(indexType) ? { indexType } : {}), + isCustom, + }); } catch (error) { throw new Error( `Failed to create index ${indexName} on object metadata ${objectMetadata.nameSingular}`, ); } - if (!result.identifiers.length) { + if (!result) { throw new Error( `Failed to return saved index ${indexName} on object metadata ${objectMetadata.nameSingular}`, ); } + await this.createIndexCreationMigration( + workspaceId, + objectMetadata, + fieldMetadataToIndex, + isUnique, + isCustom, + indexType, + indexWhereClause, + ); + } + + async createIndexCreationMigration( + workspaceId: string, + objectMetadata: ObjectMetadataEntity, + fieldMetadataToIndex: Partial[], + isUnique: boolean, + isCustom: boolean, + indexType?: IndexType, + indexWhereClause?: string, + ) { + const tableName = computeObjectTargetTable(objectMetadata); + + const columnNames: string[] = fieldMetadataToIndex.map( + (fieldMetadata) => fieldMetadata.name as string, + ); + + const indexName = `IDX_${generateDeterministicIndexName([tableName, ...columnNames])}`; + const migration = { name: tableName, action: WorkspaceMigrationTableActionType.ALTER_INDEXES, diff --git a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts index 90291ad6522a..d9a1850a5df6 100644 --- a/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/relation-metadata/relation-metadata.service.ts @@ -149,7 +149,7 @@ export class RelationMetadataService extends TypeOrmQueryService Date: Sat, 19 Oct 2024 00:39:10 +0200 Subject: [PATCH 23/67] Refactoring show page (#7838) @ehconitin following your question I did a quick refactoring of the show page - we can push it much further but it would be better to start from this code than from main Edit: I will merge to avoid conflicts, this is very far from perfect but still much better than the mess we had before --- .../components/TimelineCreateButtonGroup.tsx | 2 +- .../record-show/components/FieldsCard.tsx | 188 ++++++++++ .../components/RecordShowContainer.tsx | 322 +----------------- .../record-show/components/SummaryCard.tsx | 100 ++++++ .../hooks/useRecordShowContainerActions.ts | 66 ++++ .../hooks/useRecordShowContainerData.ts | 57 ++++ .../hooks/useRecordShowContainerTabs.ts | 110 ++++++ ...Container.tsx => ShowPageSubContainer.tsx} | 213 ++++-------- .../ui/layout/tab/components/TabList.tsx | 2 +- 9 files changed, 600 insertions(+), 460 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerActions.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerData.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts rename packages/twenty-front/src/modules/ui/layout/show-page/components/{ShowPageRightContainer.tsx => ShowPageSubContainer.tsx} (59%) diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineCreateButtonGroup.tsx b/packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineCreateButtonGroup.tsx index 2889dfc77c0e..4e8ec1c647cc 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineCreateButtonGroup.tsx +++ b/packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineCreateButtonGroup.tsx @@ -3,7 +3,7 @@ import { IconCheckbox, IconNotes, IconPaperclip } from 'twenty-ui'; import { Button } from '@/ui/input/button/components/Button'; import { ButtonGroup } from '@/ui/input/button/components/ButtonGroup'; -import { TAB_LIST_COMPONENT_ID } from '@/ui/layout/show-page/components/ShowPageRightContainer'; +import { TAB_LIST_COMPONENT_ID } from '@/ui/layout/show-page/components/ShowPageSubContainer'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; export const TimelineCreateButtonGroup = ({ diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx new file mode 100644 index 000000000000..22f77e501dd8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-show/components/FieldsCard.tsx @@ -0,0 +1,188 @@ +import groupBy from 'lodash.groupby'; + +import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell'; +import { Note } from '@/activities/types/Note'; +import { Task } from '@/activities/types/Task'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; +import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox'; +import { PropertyBoxSkeletonLoader } from '@/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader'; +import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope'; +import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions'; +import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData'; +import { RecordDetailDuplicatesSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailDuplicatesSection'; +import { RecordDetailRelationSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSection'; +import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported'; +import { FieldMetadataType } from '~/generated/graphql'; +import { isDefined } from '~/utils/isDefined'; + +type FieldsCardProps = { + objectNameSingular: string; + objectRecordId: string; +}; + +export const FieldsCard = ({ + objectNameSingular, + objectRecordId, +}: FieldsCardProps) => { + const { + recordFromStore, + recordLoading, + objectMetadataItem, + labelIdentifierFieldMetadataItem, + isPrefetchLoading, + objectMetadataItems, + } = useRecordShowContainerData({ + objectNameSingular, + objectRecordId, + }); + + const { useUpdateOneObjectRecordMutation } = useRecordShowContainerActions({ + objectNameSingular, + objectRecordId, + recordFromStore, + }); + + const availableFieldMetadataItems = objectMetadataItem.fields + .filter( + (fieldMetadataItem) => + isFieldCellSupported(fieldMetadataItem, objectMetadataItems) && + fieldMetadataItem.id !== labelIdentifierFieldMetadataItem?.id, + ) + .sort((fieldMetadataItemA, fieldMetadataItemB) => + fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name), + ); + + const { inlineFieldMetadataItems, relationFieldMetadataItems } = groupBy( + availableFieldMetadataItems.filter( + (fieldMetadataItem) => + fieldMetadataItem.name !== 'createdAt' && + fieldMetadataItem.name !== 'deletedAt', + ), + (fieldMetadataItem) => + fieldMetadataItem.type === FieldMetadataType.Relation + ? 'relationFieldMetadataItems' + : 'inlineFieldMetadataItems', + ); + + const inlineRelationFieldMetadataItems = relationFieldMetadataItems?.filter( + (fieldMetadataItem) => + (objectNameSingular === CoreObjectNameSingular.Note && + fieldMetadataItem.name === 'noteTargets') || + (objectNameSingular === CoreObjectNameSingular.Task && + fieldMetadataItem.name === 'taskTargets'), + ); + + const boxedRelationFieldMetadataItems = relationFieldMetadataItems?.filter( + (fieldMetadataItem) => + objectNameSingular !== CoreObjectNameSingular.Note && + fieldMetadataItem.name !== 'noteTargets' && + objectNameSingular !== CoreObjectNameSingular.Task && + fieldMetadataItem.name !== 'taskTargets', + ); + const isReadOnly = objectMetadataItem.isRemote; + + return ( + <> + {isDefined(recordFromStore) && ( + <> + + {isPrefetchLoading ? ( + + ) : ( + <> + {inlineRelationFieldMetadataItems?.map( + (fieldMetadataItem, index) => ( + + + + ), + )} + {inlineFieldMetadataItems?.map((fieldMetadataItem, index) => ( + + + + ))} + + )} + + + {boxedRelationFieldMetadataItems?.map((fieldMetadataItem, index) => ( + + + + ))} + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx index 9b1e10601ab4..8e911edac10c 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx @@ -1,47 +1,10 @@ -import groupBy from 'lodash.groupby'; -import { useRecoilState, useRecoilValue } from 'recoil'; - -import { ActivityTargetsInlineCell } from '@/activities/inline-cell/components/ActivityTargetsInlineCell'; -import { Note } from '@/activities/types/Note'; -import { Task } from '@/activities/types/Task'; import { InformationBannerDeletedRecord } from '@/information-banner/components/deleted-record/InformationBannerDeletedRecord'; -import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon'; -import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem'; -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { formatFieldMetadataItemAsColumnDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsColumnDefinition'; -import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; -import { - FieldContext, - RecordUpdateHook, - RecordUpdateHookParams, -} from '@/object-record/record-field/contexts/FieldContext'; -import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; -import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox'; -import { PropertyBoxSkeletonLoader } from '@/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader'; -import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope'; -import { RecordDetailDuplicatesSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailDuplicatesSection'; -import { RecordDetailRelationSection } from '@/object-record/record-show/record-detail-section/components/RecordDetailRelationSection'; -import { recordLoadingFamilyState } from '@/object-record/record-store/states/recordLoadingFamilyState'; -import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; -import { recordStoreIdentifierFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifierSelector'; -import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported'; -import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; import { ShowPageContainer } from '@/ui/layout/page/ShowPageContainer'; -import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer'; -import { ShowPageRightContainer } from '@/ui/layout/show-page/components/ShowPageRightContainer'; -import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard'; -import { ShowPageSummaryCardSkeletonLoader } from '@/ui/layout/show-page/components/ShowPageSummaryCardSkeletonLoader'; -import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; -import { - FieldMetadataType, - FileFolder, - useUploadImageMutation, -} from '~/generated/graphql'; -import { isDefined } from '~/utils/isDefined'; -import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; + +import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData'; +import { useRecordShowContainerTabs } from '@/object-record/record-show/hooks/useRecordShowContainerTabs'; +import { ShowPageSubContainer } from '@/ui/layout/show-page/components/ShowPageSubContainer'; type RecordShowContainerProps = { objectNameSingular: string; @@ -58,261 +21,20 @@ export const RecordShowContainer = ({ isInRightDrawer = false, isNewRightDrawerItemLoading = false, }: RecordShowContainerProps) => { - const { objectMetadataItem } = useObjectMetadataItem({ + const { + recordFromStore, + objectMetadataItem, + isPrefetchLoading, + recordLoading, + } = useRecordShowContainerData({ objectNameSingular, + objectRecordId, }); - const { objectMetadataItems } = useObjectMetadataItems(); - - const { labelIdentifierFieldMetadataItem } = - useLabelIdentifierFieldMetadataItem({ - objectNameSingular, - }); - - const [recordLoading] = useRecoilState( - recordLoadingFamilyState(objectRecordId), - ); - - const [recordFromStore] = useRecoilState( - recordStoreFamilyState(objectRecordId), - ); - - const recordIdentifier = useRecoilValue( - recordStoreIdentifierFamilySelector({ - objectNameSingular, - recordId: objectRecordId, - }), - ); - const [uploadImage] = useUploadImageMutation(); - const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular }); - - const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => { - const updateEntity = ({ variables }: RecordUpdateHookParams) => { - updateOneRecord?.({ - idToUpdate: variables.where.id as string, - updateOneRecordInput: variables.updateOneRecordInput, - }); - }; - - return [updateEntity, { loading: false }]; - }; - - const onUploadPicture = async (file: File) => { - if (objectNameSingular !== 'person') { - return; - } - - const result = await uploadImage({ - variables: { - file, - fileFolder: FileFolder.PersonPicture, - }, - }); - - const avatarUrl = result?.data?.uploadImage; - - if (!avatarUrl || isUndefinedOrNull(updateOneRecord) || !recordFromStore) { - return; - } - - await updateOneRecord({ - idToUpdate: objectRecordId, - updateOneRecordInput: { - avatarUrl, - }, - }); - }; - - const availableFieldMetadataItems = objectMetadataItem.fields - .filter( - (fieldMetadataItem) => - isFieldCellSupported(fieldMetadataItem, objectMetadataItems) && - fieldMetadataItem.id !== labelIdentifierFieldMetadataItem?.id, - ) - .sort((fieldMetadataItemA, fieldMetadataItemB) => - fieldMetadataItemA.name.localeCompare(fieldMetadataItemB.name), - ); - - const { inlineFieldMetadataItems, relationFieldMetadataItems } = groupBy( - availableFieldMetadataItems.filter( - (fieldMetadataItem) => - fieldMetadataItem.name !== 'createdAt' && - fieldMetadataItem.name !== 'deletedAt', - ), - (fieldMetadataItem) => - fieldMetadataItem.type === FieldMetadataType.Relation - ? 'relationFieldMetadataItems' - : 'inlineFieldMetadataItems', - ); - - const inlineRelationFieldMetadataItems = relationFieldMetadataItems?.filter( - (fieldMetadataItem) => - (objectNameSingular === CoreObjectNameSingular.Note && - fieldMetadataItem.name === 'noteTargets') || - (objectNameSingular === CoreObjectNameSingular.Task && - fieldMetadataItem.name === 'taskTargets'), - ); - - const boxedRelationFieldMetadataItems = relationFieldMetadataItems?.filter( - (fieldMetadataItem) => - objectNameSingular !== CoreObjectNameSingular.Note && - fieldMetadataItem.name !== 'noteTargets' && - objectNameSingular !== CoreObjectNameSingular.Task && - fieldMetadataItem.name !== 'taskTargets', - ); - const { Icon, IconColor } = useGetStandardObjectIcon(objectNameSingular); - const isReadOnly = objectMetadataItem.isRemote; - const isMobile = useIsMobile() || isInRightDrawer; - const isPrefetchLoading = useIsPrefetchLoading(); - - const summaryCard = - !isNewRightDrawerItemLoading && isDefined(recordFromStore) ? ( - - - - } - avatarType={recordIdentifier?.avatarType ?? 'rounded'} - onUploadPicture={ - objectNameSingular === 'person' ? onUploadPicture : undefined - } - /> - ) : ( - - ); - - const fieldsBox = ( - <> - {isDefined(recordFromStore) && ( - <> - - {isPrefetchLoading ? ( - - ) : ( - <> - {inlineRelationFieldMetadataItems?.map( - (fieldMetadataItem, index) => ( - - - - ), - )} - {inlineFieldMetadataItems?.map((fieldMetadataItem, index) => ( - - - - ))} - - )} - - - {boxedRelationFieldMetadataItems?.map((fieldMetadataItem, index) => ( - - - - ))} - - )} - + const tabs = useRecordShowContainerTabs( + loading, + objectNameSingular as CoreObjectNameSingular, + isInRightDrawer, ); return ( @@ -324,23 +46,15 @@ export const RecordShowContainer = ({ /> )} - - {!isMobile && summaryCard} - {!isMobile && fieldsBox} - - } - fieldsBox={fieldsBox} loading={isPrefetchLoading || loading || recordLoading} + isNewRightDrawerItemLoading={isNewRightDrawerItemLoading} /> diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx new file mode 100644 index 000000000000..05b76a1e939c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-show/components/SummaryCard.tsx @@ -0,0 +1,100 @@ +import { useGetStandardObjectIcon } from '@/object-metadata/hooks/useGetStandardObjectIcon'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; +import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell'; +import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/InlineCellHotkeyScope'; +import { useRecordShowContainerActions } from '@/object-record/record-show/hooks/useRecordShowContainerActions'; +import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData'; +import { ShowPageSummaryCard } from '@/ui/layout/show-page/components/ShowPageSummaryCard'; +import { ShowPageSummaryCardSkeletonLoader } from '@/ui/layout/show-page/components/ShowPageSummaryCardSkeletonLoader'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; +import { FieldMetadataType } from '~/generated/graphql'; +import { isDefined } from '~/utils/isDefined'; + +type SummaryCardProps = { + objectNameSingular: string; + objectRecordId: string; + isNewRightDrawerItemLoading: boolean; + isInRightDrawer: boolean; +}; + +export const SummaryCard = ({ + objectNameSingular, + objectRecordId, + isNewRightDrawerItemLoading, + isInRightDrawer, +}: SummaryCardProps) => { + const { + recordFromStore, + recordLoading, + objectMetadataItem, + labelIdentifierFieldMetadataItem, + isPrefetchLoading, + recordIdentifier, + } = useRecordShowContainerData({ + objectNameSingular, + objectRecordId, + }); + + const { onUploadPicture, useUpdateOneObjectRecordMutation } = + useRecordShowContainerActions({ + objectNameSingular, + objectRecordId, + recordFromStore, + }); + + const { Icon, IconColor } = useGetStandardObjectIcon(objectNameSingular); + const isMobile = useIsMobile() || isInRightDrawer; + const isReadOnly = objectMetadataItem.isRemote; + + if (isNewRightDrawerItemLoading || !isDefined(recordFromStore)) { + return ; + } + + return ( + + + + } + avatarType={recordIdentifier?.avatarType ?? 'rounded'} + onUploadPicture={ + objectNameSingular === CoreObjectNameSingular.Person + ? onUploadPicture + : undefined + } + /> + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerActions.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerActions.ts new file mode 100644 index 000000000000..0188f48f696f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerActions.ts @@ -0,0 +1,66 @@ +import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { + RecordUpdateHook, + RecordUpdateHookParams, +} from '@/object-record/record-field/contexts/FieldContext'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { FileFolder } from '~/generated-metadata/graphql'; +import { useUploadImageMutation } from '~/generated/graphql'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; + +interface UseRecordShowContainerActionsProps { + objectNameSingular: string; + objectRecordId: string; + recordFromStore: ObjectRecord | null; +} + +export const useRecordShowContainerActions = ({ + objectNameSingular, + objectRecordId, + recordFromStore, +}: UseRecordShowContainerActionsProps) => { + const [uploadImage] = useUploadImageMutation(); + const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular }); + + const useUpdateOneObjectRecordMutation: RecordUpdateHook = () => { + const updateEntity = ({ variables }: RecordUpdateHookParams) => { + updateOneRecord?.({ + idToUpdate: variables.where.id as string, + updateOneRecordInput: variables.updateOneRecordInput, + }); + }; + + return [updateEntity, { loading: false }]; + }; + + const onUploadPicture = async (file: File) => { + if (objectNameSingular !== 'person') { + return; + } + + const result = await uploadImage({ + variables: { + file, + fileFolder: FileFolder.PersonPicture, + }, + }); + + const avatarUrl = result?.data?.uploadImage; + + if (!avatarUrl || isUndefinedOrNull(updateOneRecord) || !recordFromStore) { + return; + } + + await updateOneRecord({ + idToUpdate: objectRecordId, + updateOneRecordInput: { + avatarUrl, + }, + }); + }; + + return { + onUploadPicture, + useUpdateOneObjectRecordMutation, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerData.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerData.ts new file mode 100644 index 000000000000..15eeb056bab5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerData.ts @@ -0,0 +1,57 @@ +import { useLabelIdentifierFieldMetadataItem } from '@/object-metadata/hooks/useLabelIdentifierFieldMetadataItem'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; +import { recordLoadingFamilyState } from '@/object-record/record-store/states/recordLoadingFamilyState'; +import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { recordStoreIdentifierFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreIdentifierSelector'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; +import { useRecoilState, useRecoilValue } from 'recoil'; + +type UseRecordShowContainerDataProps = { + objectNameSingular: string; + objectRecordId: string; +}; + +export const useRecordShowContainerData = ({ + objectNameSingular, + objectRecordId, +}: UseRecordShowContainerDataProps) => { + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const { labelIdentifierFieldMetadataItem } = + useLabelIdentifierFieldMetadataItem({ + objectNameSingular, + }); + + const [recordLoading] = useRecoilState( + recordLoadingFamilyState(objectRecordId), + ); + + const [recordFromStore] = useRecoilState( + recordStoreFamilyState(objectRecordId), + ); + + const recordIdentifier = useRecoilValue( + recordStoreIdentifierFamilySelector({ + objectNameSingular, + recordId: objectRecordId, + }), + ); + + const isPrefetchLoading = useIsPrefetchLoading(); + + const { objectMetadataItems } = useObjectMetadataItems(); + + return { + recordFromStore, + recordLoading, + objectMetadataItem, + labelIdentifierFieldMetadataItem, + isPrefetchLoading, + recordIdentifier, + objectMetadataItems, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts new file mode 100644 index 000000000000..1d029f4877f8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts @@ -0,0 +1,110 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { + IconCalendarEvent, + IconCheckbox, + IconList, + IconMail, + IconNotes, + IconPaperclip, + IconSettings, + IconTimelineEvent, +} from 'twenty-ui'; + +export const useRecordShowContainerTabs = ( + loading: boolean, + targetObjectNameSingular: CoreObjectNameSingular, + isInRightDrawer: boolean, +) => { + const isMobile = useIsMobile(); + const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED'); + + const isWorkflow = + isWorkflowEnabled && + targetObjectNameSingular === CoreObjectNameSingular.Workflow; + const isWorkflowVersion = + isWorkflowEnabled && + targetObjectNameSingular === CoreObjectNameSingular.WorkflowVersion; + + const isCompanyOrPerson = [ + CoreObjectNameSingular.Company, + CoreObjectNameSingular.Person, + ].includes(targetObjectNameSingular); + const shouldDisplayCalendarTab = isCompanyOrPerson; + const shouldDisplayEmailsTab = isCompanyOrPerson; + + return [ + { + id: 'richText', + title: 'Note', + Icon: IconNotes, + hide: + loading || + (targetObjectNameSingular !== CoreObjectNameSingular.Note && + targetObjectNameSingular !== CoreObjectNameSingular.Task), + }, + { + id: 'fields', + title: 'Fields', + Icon: IconList, + hide: !(isMobile || isInRightDrawer), + }, + { + id: 'timeline', + title: 'Timeline', + Icon: IconTimelineEvent, + hide: isInRightDrawer || isWorkflow || isWorkflowVersion, + }, + { + id: 'tasks', + title: 'Tasks', + Icon: IconCheckbox, + hide: + targetObjectNameSingular === CoreObjectNameSingular.Note || + targetObjectNameSingular === CoreObjectNameSingular.Task || + isWorkflow || + isWorkflowVersion, + }, + { + id: 'notes', + title: 'Notes', + Icon: IconNotes, + hide: + targetObjectNameSingular === CoreObjectNameSingular.Note || + targetObjectNameSingular === CoreObjectNameSingular.Task || + isWorkflow || + isWorkflowVersion, + }, + { + id: 'files', + title: 'Files', + Icon: IconPaperclip, + hide: isWorkflow || isWorkflowVersion, + }, + { + id: 'emails', + title: 'Emails', + Icon: IconMail, + hide: !shouldDisplayEmailsTab, + }, + { + id: 'calendar', + title: 'Calendar', + Icon: IconCalendarEvent, + hide: !shouldDisplayCalendarTab, + }, + { + id: 'workflow', + title: 'Workflow', + Icon: IconSettings, + hide: !isWorkflow, + }, + { + id: 'workflowVersion', + title: 'Workflow Version', + Icon: IconSettings, + hide: !isWorkflowVersion, + }, + ]; +}; diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx similarity index 59% rename from packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx rename to packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx index 449963c0135b..017f38fd9280 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx @@ -8,32 +8,24 @@ import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableE import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading'; +import { FieldsCard } from '@/object-record/record-show/components/FieldsCard'; +import { SummaryCard } from '@/object-record/record-show/components/SummaryCard'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { Button } from '@/ui/input/button/components/Button'; import { ShowPageActivityContainer } from '@/ui/layout/show-page/components/ShowPageActivityContainer'; -import { TabList } from '@/ui/layout/tab/components/TabList'; +import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer'; +import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { WorkflowVersionVisualizer } from '@/workflow/components/WorkflowVersionVisualizer'; import { WorkflowVersionVisualizerEffect } from '@/workflow/components/WorkflowVersionVisualizerEffect'; import { WorkflowVisualizer } from '@/workflow/components/WorkflowVisualizer'; import { WorkflowVisualizerEffect } from '@/workflow/components/WorkflowVisualizerEffect'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import styled from '@emotion/styled'; import { useState } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; -import { - IconCalendarEvent, - IconCheckbox, - IconList, - IconMail, - IconNotes, - IconPaperclip, - IconSettings, - IconTimelineEvent, - IconTrash, -} from 'twenty-ui'; +import { IconTrash } from 'twenty-ui'; const StyledShowPageRightContainer = styled.div<{ isMobile: boolean }>` display: flex; @@ -89,145 +81,51 @@ const StyledContentContainer = styled.div<{ isInRightDrawer: boolean }>` export const TAB_LIST_COMPONENT_ID = 'show-page-right-tab-list'; -type ShowPageRightContainerProps = { +type ShowPageSubContainerProps = { + tabs: SingleTabProps[]; targetableObject: Pick< ActivityTargetableObject, 'targetObjectNameSingular' | 'id' >; - timeline?: boolean; - tasks?: boolean; - notes?: boolean; - emails?: boolean; - fieldsBox?: JSX.Element; - summaryCard?: JSX.Element; isInRightDrawer?: boolean; loading: boolean; + isNewRightDrawerItemLoading?: boolean; }; -export const ShowPageRightContainer = ({ +export const ShowPageSubContainer = ({ + tabs, targetableObject, - timeline, - tasks, - notes, - emails, loading, - fieldsBox, - summaryCard, isInRightDrawer = false, -}: ShowPageRightContainerProps) => { + isNewRightDrawerItemLoading = false, +}: ShowPageSubContainerProps) => { const { activeTabIdState } = useTabList( `${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`, ); const activeTabId = useRecoilValue(activeTabIdState); - const targetObjectNameSingular = - targetableObject.targetObjectNameSingular as CoreObjectNameSingular; - - const isCompanyOrPerson = [ - CoreObjectNameSingular.Company, - CoreObjectNameSingular.Person, - ].includes(targetObjectNameSingular); - - const isWorkflowEnabled = useIsFeatureEnabled('IS_WORKFLOW_ENABLED'); - const isWorkflow = - isWorkflowEnabled && - targetableObject.targetObjectNameSingular === - CoreObjectNameSingular.Workflow; - const isWorkflowVersion = - isWorkflowEnabled && - targetableObject.targetObjectNameSingular === - CoreObjectNameSingular.WorkflowVersion; - - const shouldDisplayCalendarTab = isCompanyOrPerson; - const shouldDisplayEmailsTab = emails && isCompanyOrPerson; - const isMobile = useIsMobile(); const isNewViewableRecordLoading = useRecoilValue( isNewViewableRecordLoadingState, ); - const tabs = [ - { - id: 'richText', - title: 'Note', - Icon: IconNotes, - hide: - loading || - (targetableObject.targetObjectNameSingular !== - CoreObjectNameSingular.Note && - targetableObject.targetObjectNameSingular !== - CoreObjectNameSingular.Task), - }, - { - id: 'fields', - title: 'Fields', - Icon: IconList, - hide: !(isMobile || isInRightDrawer), - }, - { - id: 'timeline', - title: 'Timeline', - Icon: IconTimelineEvent, - hide: !timeline || isInRightDrawer || isWorkflow || isWorkflowVersion, - }, - { - id: 'tasks', - title: 'Tasks', - Icon: IconCheckbox, - hide: - !tasks || - targetableObject.targetObjectNameSingular === - CoreObjectNameSingular.Note || - targetableObject.targetObjectNameSingular === - CoreObjectNameSingular.Task || - isWorkflow || - isWorkflowVersion, - }, - { - id: 'notes', - title: 'Notes', - Icon: IconNotes, - hide: - !notes || - targetableObject.targetObjectNameSingular === - CoreObjectNameSingular.Note || - targetableObject.targetObjectNameSingular === - CoreObjectNameSingular.Task || - isWorkflow || - isWorkflowVersion, - }, - { - id: 'files', - title: 'Files', - Icon: IconPaperclip, - hide: !notes || isWorkflow || isWorkflowVersion, - }, - { - id: 'emails', - title: 'Emails', - Icon: IconMail, - hide: !shouldDisplayEmailsTab, - }, - { - id: 'calendar', - title: 'Calendar', - Icon: IconCalendarEvent, - hide: !shouldDisplayCalendarTab, - }, - { - id: 'workflow', - title: 'Workflow', - Icon: IconSettings, - hide: !isWorkflow, - }, - { - id: 'workflowVersion', - title: 'Workflow Version', - Icon: IconSettings, - hide: !isWorkflowVersion, - }, - ]; + const summaryCard = ( + + ); + + const fieldsCard = ( + + ); + const renderActiveTabContent = () => { switch (activeTabId) { case 'timeline': @@ -251,10 +149,9 @@ export const ShowPageRightContainer = ({ case 'fields': return ( - {fieldsBox} + {fieldsCard} ); - case 'tasks': return ; case 'notes': @@ -307,28 +204,36 @@ export const ShowPageRightContainer = ({ ); return ( - - - - - {summaryCard} - - {renderActiveTabContent()} - - {isInRightDrawer && recordFromStore && !recordFromStore.deletedAt && ( - - - + <> + {!isMobile && !isInRightDrawer && ( + + {summaryCard} + {fieldsCard} + )} - + + + + + {(isMobile || isInRightDrawer) && summaryCard} + + {renderActiveTabContent()} + + {isInRightDrawer && recordFromStore && !recordFromStore.deletedAt && ( + + + + )} + + ); }; diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx index c66767a09039..8375cc0825fe 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx @@ -9,7 +9,7 @@ import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import { Tab } from './Tab'; -type SingleTabProps = { +export type SingleTabProps = { title: string; Icon?: IconComponent; id: string; From 29f903a83b540e56944bcbad1caf128e8781e2ca Mon Sep 17 00:00:00 2001 From: Prashant Acharya <125622593+prashant48653c@users.noreply.github.com> Date: Sat, 19 Oct 2024 14:43:20 +0545 Subject: [PATCH 24/67] Added new logo images (#7840) **Added new logo in different png format** Fixed Issue: Design a new logo for Twenty (300 points) #**7834** What tool did I used? I used logo.com as a design tool to design the logo. Logo I made: ![20-high-resolution-logo-black](https://github.com/user-attachments/assets/f041d22d-6d7f-4171-96b7-302a255e89e9) ![20-high-resolution-logo-white-transparent](https://github.com/user-attachments/assets/163f1b9d-cfa2-4d75-ba9d-9cb0ce54bf46) ![20-high-resolution-logo-black-transparent](https://github.com/user-attachments/assets/4648107d-c628-4a64-9bd1-94ab036c4b60) ![20-high-resolution-logo](https://github.com/user-attachments/assets/7735e623-b2e2-4484-b71c-5fc42be33362) --- ...0-high-resolution-logo-black-transparent.png | Bin 0 -> 44376 bytes .../logos/20-high-resolution-logo-black.png | Bin 0 -> 21963 bytes ...0-high-resolution-logo-white-transparent.png | Bin 0 -> 44370 bytes .../public/logos/20-high-resolution-logo.png | Bin 0 -> 20740 bytes 4 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 packages/twenty-front/public/logos/20-high-resolution-logo-black-transparent.png create mode 100644 packages/twenty-front/public/logos/20-high-resolution-logo-black.png create mode 100644 packages/twenty-front/public/logos/20-high-resolution-logo-white-transparent.png create mode 100644 packages/twenty-front/public/logos/20-high-resolution-logo.png diff --git a/packages/twenty-front/public/logos/20-high-resolution-logo-black-transparent.png b/packages/twenty-front/public/logos/20-high-resolution-logo-black-transparent.png new file mode 100644 index 0000000000000000000000000000000000000000..236da7815022be7f5882055778dca43461420312 GIT binary patch literal 44376 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 z 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¬veSl>h6#Rwdpkj~I&A_0hd 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%0Atq&e| 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 literal 0 HcmV?d00001 diff --git a/packages/twenty-front/public/logos/20-high-resolution-logo-black.png b/packages/twenty-front/public/logos/20-high-resolution-logo-black.png new file mode 100644 index 0000000000000000000000000000000000000000..db41f79831e503a772cebc727e5b7c6d9b248969 GIT binary patch literal 21963 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 Rib$PL`{^i?cx34G>{FF>mn{Nv`9X?QpnSaW zYWa24xPx16s?;?v5r=Xi1e_NDK&vh8JZV$8nCv%Gm~0gLG{b4R|0MDc zC+8PTHgy|m3R?i~49I`@D#NpMESkviskYBpB(!jTOz>5?Bclf%GOJZnCDlX8`(Pbhp zFueFEW?5_}aDruAhG#k}SUSmG1_Ex)0PLeRcHT7K3{JDFd$8kk((uw-o%Z9h{k2EN zn*+%m0>AdWQHy6@r0Q~kY3(Utl#Nz8N$;XRP1FTMv#{I%D%)eF!F&6&^j=|Zvj*}I zioRwuc$}(}D|fSI$n9?=!d1^ywkttnHrn_uBq2;5SO&7XRg_nrSCZgo#m^+a74b}Cwppkx?Y7nD zDJ6;PIPfj5Z`$bGH0sKut4$(#xbgCl_zM<{>u*Myw<|a8V8-_y`FFxbI$L^m;R9(XfFl|??)$F6BC?Nj&bFiXiQADEk0Tu`5 zzr3G}gzIV5`)!1ZMfpm>w<8sHnC}IVH4lF5WFUr2S}xb?*IQ|IPCfzO@#BuTC9c70 z(_CT1$hW$x(neF{^sM# zdRoMK2daO$&y_Ej9^zl_K0dn`r6h=aB9_7BD1k-QYNBk*I?r%IAO`2(Rvsp`$kkTP zMCeSJRs9_BhIxC>PsmBzp9=&ynX2x~p+*9{Y8e}`69OL;7d zn*VCI=oru5+iP(YkeDjS2-)t1-hlkS-~U74|8E2ew2sJR;*Fkw-(S}Zm_TLqeV7CU zB0mr`V)d7DiMY*JdeE)DVA(IeVShZbVck z&8{E&e!HkG52C&7_IXMOgm}eZYvuI3AUEn+ep9I}&rv%&7JTE{p8iZKP%YNCmNoG`MPSm z1GLsud!%w?k})*Sw*8~R?AVtw3$hSM;nIh3;g!PA^xEsjlHFp7=N>}A`bO2QX3rak z>ZCG!Cs&iOEPwxAgg|afjkis*xgsWU)(1Y4-K*i#&*eRv?!n-TZzbIq`y_UP68G2A z$^5Svj1PrRHl#@yByYZ1%WZk!D$b=>YkUIhB6?uEvwr@I+v?7-S2{;E$vb{WZ{0fX zC+xUQpH)x3FOnRSwrh#ux`H+(0l{d23kn;MD=i1c5SA6DVWOx<6PIsAF8j;#2IiOR z?B1xvE!wfjkwQj4FG6>Z-{Sm_-fq72)O1rO&C+t2Z&;GC%r}mdi|>_%Jah_$b!R6w zfKT;ivLA5=(Dv6>K%G8iBs1IZQ?HOr)`VB7jo)~}aYwMa*z3WE7SaJNAJ_erHW7i3 z@W1Efjt{~{e&oyU1e?9ht~@@ueG(V8N$)=+u#pF&4N>yy>a7l>n>2L$e#hyLq%*W+ z?`Mc6^yI|&|10gvAEA1`|HT_6Q3+Wl6=lov4w0o%VWey^6vnh!vJD@mF=R$ar8gl` zhLP-q2qR_?MUm_~*OqO>SjILQ^S$#oe1E;qea<&jS`sBy3!!`I*Aa z_`-XRua-R2HF?y_1bq$XqNKxK7hBSsHPG;S7N-=1yCm&Ro$@u-h~^7#BCM%nEzeie zehkThceY}-^TX%r9K3IY)Mn-G>bMoX9KdU$Lr%4|1O5&m&5N+KZ0Cd@Ot6)cFwd#V zALcdRw>rH4@!>J|`r-jI7MA_s1b{fmR1ZW&BM1{GL2D9C98&*GCG0$pf{Kgl;fRn4VZ_d8$AEYSlxb5_h4N497YSGG1C2I|mX5)$t7 zBg;nV&ObPJdP>cM01;kEpdu6LtL2LQbsO`5Wk;uBGXbHRo4M;_Sr?fP$d(6arvkWhNq22|wmgu8}n zw@Kc$l6hi{a4x)XKGE}1-vbT5Cc{PWxQk@EOd zho$CnycPU#r6mJ(BF6QD&qvfqVSt-xC);4#vsUTwQk?Y1fF0M%Wp*68k+1>Xr z@(wUjtwAd)T~?Whk?irNiMO|H7`KIHi?L%p>Ab)k>?XcTwzO7A@4aoG1?KRF8-}G` zBxY7t{sCYVXE~#@Za;=>IQ%m2%66j?XjNb5g~W}AIr0F=sv@d;or%GrCkrU5usTd; ztVjOjp4g8sKtsScH@ZaTl_}YbxQS0tYT_7Z())R~r-&e6HPTdjAKIEvSTFNRAV$A3 zL#}I8{hEetkJ_TR7jWVNnOXA2dQqLvYWreXB;mn*LF&n{1_OD66M)_#xpmPk<;a*@ z1OI6|>bCEgM!9^|P`|;{f2;)9VZdcZZE@oxgfc3ld#mSwyy0)bg)7UFlP7r^u4b*t zvpkQPr>78}`W7iU2NdFdD#vGzn)a_2<;kCnevT_&KoGaCADPB)9^26 zvn09jp}T4MpXK&kO4dc6C%8eu9SIol$cIk*s{Q!*N!*nl>rwQ}niMF30@{De0$_wN)v|1Z3FA?%KH0FKQI332eF+~Uhq@@Ma z$&P-MfXiyKZl;Js5K|xQvbGNj0zMA9FeI7XIZ!WE*R@kyH%t+7j!t;y(zXI0P&oG5 zRM!07p#)B_7G0)tu@ja{Ujc3xp{Ph>Cumt-FwM5-+Ng&;jPM+{tzi#44xCM<#AGNO zE}l-_iza)GRIaU{hV%6=0)q>3;5Q1wV+`8>6ATS3G#*+V-;6^$53TYvm~DuxsK>$S zC43_I9+I7|lEL02xXN&N_z3veRZsPv-u@rd?WA+aEBXny77cLNc@LXZE(U~$9rwM3 ze{hzbQ!7&a+f<_yZHJ4{qKc&zE&l`DCN;uns?&22F1wO7_dav1qPYeqB(nzU=c|vG zY`#D}{8XuL{k4i$@yCLN8MLI5As=vartl1i4S2V#Kk?=o72k*2*wi598}I-(bGrCJ zw^bv^5FOD_vBm#k72hlw@eQh5k4XUyXVAeZW7ESBf|8*&(k)$8!zOP~D)_A!pn_tc z#AL{3x`)anEVitRgEDZ@sX`q9&>x&rwl<=q(0dW=4KbONstIkYue!VA2$-vlHP_CQ z5vtIu42Ex;g9>r#Vh5?NR0T3`@Xn|(BO=%eKK7)+bOC17qGH%22udBl5i9_Fyv$hz zH~bsyp|-1v_?|`F6OVkSy~_GVJ0P6jEPV#$uWMkBJL%se2L2tkpHZ$`vs!)K3N^0! z!OyWHc5yWa9%qH<=BM4(5fUd)i9`QVm8=K@x2dGfjglv78J(zovYf-H1;qq;nw??c z+EL(kl>tqIVDd^5E6Iyv)7#77_T(uU#!-G1YaZZC5xl)tByrB>fhZ;sYln}y3a*uE z-P{v5zHF3EprQWMzF4-HsCbzC=`2w&Iq&A5_TN|R$-KaIB&R!i!I0?xdHKdfl~xk5 z`gP5)!M8H5%xLNPvGe;+TDb`Z_P^{dzZ`Kx zKZG#2_Q2 zPFm%iBu6k!u0j>3**U?3*-L-7r`({-b!l0?u3tuvGj(?+2I1}-)dhLkMclP2xC3Eho17v%ERdT zvGSQ=d3pw~_HW#2AeY4c?V%e@mETG_7&0+EpeR9Cms#;~zM#BL;UWu>7HPOmv)(qO z9dg)*8piJanInmgXOWlRKgPODw*T^g95k3v#ixk`aSWZ+TCwqvzw)AET1#K`b%|@e z)X!qy1CA{e9+MzEpM*;G`@TEke1);AQl-U%-t{>3&?K16ou^3f>zE}J1_lzMWO+r) zh^dSjeq4)KhZ*?tt>Wmz&qJg-$s!1p>9V%kImAOXHxu zU+&sbd1@YD)QW_t_gj`VF|4gY+x>@d?&$_S*0z47O$|i<6y(P4hC>xvY!1uOdOrJw z-y>9v<+WkWvuTeP0^;KxKc4yZB2_I1>)$j3Cb|0T+5y16;%%}Hp3uh25^($tn^60E z?j{6Lkqfa=H~HxWd1-tWPViARz@Fw@7zfC7iCJGB%zEA`7G+w zN0nde8g5@y^=9v5yz1XWY|7zUl<9LTdG4S?w`TP$0^?`RrFk|t>c?o(+3VDqP3D1L zQeDPS{w(AHj3P+p35CB20|D%R) zU6$>9ZS}=A1#kDa@)(&n$N?_`tYCxnJKFrRto@O$2AG5=GZ6hoxx>Un9vB|C{zhZ})(?zqQ${?^D_ zkuL#Sw6}yBNGZAyQhHxAGY7ZdYwR)9P#S8-_8rOK1&lW?T93aYS`#N~n2j2023#8E zaJ^jVuDhvje6OZQPLDyrlxx^R*`fuF13*S)@z?1J!DZ5%<>m`0J?2|awE|OBqa&-f z%z4Yj$vP?zU6ODW%9|SyC*Mg6=3+Vxx?~kR+Mnn{I9>7}G#|@y+`zs(j7(~zf?h9) zG3`r)3RIa3N@0#tqby7@bE&#Vtj6CR0ZlVO98;iK7&7I59O zFdsyQU)PN{R;3NWAq~s-eWidhr6B{@Jf#bxVAC_(DoK!9Ub;T&I4K3Z>sDMatS9-M zND#5FSM3S}l_j`2o^2t3mn8jloaaBXV7F1Pmp++3<<5P6^!7mn7WCv#RF0lOTd(Xj zZ`wmM4fABtl@c9O3&(yyxa!lE;!07YmXECyKKl5F55mSdly2R5Hx1>n3|`HFn+)p- zd~%*Sr(~l7a+!Gz%dY#ex=CazbV?|n=PGrr+NjasirNzoAU>Gw7>w=>u2~+1udYJ zKM+csz0*e$^PNe57Y$AIjEC>F_bb#Kv+GQ+I&rIRBX{pH-70>d)8L96)&mGvp|rcm zEHu$8GBwN-2}_VnePgzSCSWtOwhk5Dby#|jq{4Sk3|MN9i$H?=H|=n|z)eKyH)s{g zAw)oyo}}3S+UeeDwOaYSh1j5!PxCbh ztO?`}46gj=leuDWyg?xpZ?|XZ*1~Bv*n8U=Ckn_Ye@J)D=?=W->yTk~>hG-O_NOEm zN0hM*ONPk-g|;_$zgH&gok)l?F~{7x=-4;O(O`SU6gKf0Z?rmBILj<3ie_8-kJo}e zr8d}YHnmCD9vcjN&vq@o02$o(LR+Y0aaxTN^1N`sOdn6QFpY^50WM1S4ZHhh<4B0o ziE3hq9jXuR+F{8#y=o-{z*yK}oNu-$KI2k&k5jN1yFMn4V9#PaW0(Q+UA0z=*i&rn zXr5mhoZ*~;)U1q2_F)}vjk53j`*&@rCLNfEN@|g{;7aZe#Y<$b3b)elB?4`>%`Ba- zIn&L3Iz903vj!E$ZQH+;u8UkWZU4e^BF$EOp;%6}4;Ciw8%eTYg#pj`BYh0Io)QhaA8Jr(P#na3;tvl9poX&aqPhpGI8a3$HLTlPrGD+!S~2 zyi`5w>1L3hts5u?SB6Ts`*-}b7;m%l@TMLi`zYGZ zqboT@@^$_W#<*+5{jFwq%f-k_?L6i=5n7uK&);C?_e481o;=X~@?D5&*r;STQ8O#+ zqB`(9d%zotjYXbWkl1qY5UJn8ENn2eo2U*q33CCy+jbo2H)AbV#w;kh1ek{;Pov%& zdEE(_Ydl4`EO?tt_9%{JN>vVC7Ai4^6KW`$-I2#%?_JyBY%ZU#psNT2!L9w4x|C!c zQlRJcZi^Pn%t{CP({w1ENH(l8L!ETDi zf4eNfzH;TW3r}n#FCLDllhaW$K8q~??_T3*>RfT#fd?DazT0&)5yrQ;4ZUeg8w0L8 zVhdp3UK3Q1OF<_hwC)(y%a*-iM8}t!3hzHOlvBw#>dYdmZ=7DGH%^~abIvlQmctDn zY~d@O0dAU<^P?BuR{lM#b7tXTG;JRJGgf@1V`ZRALiM=u+q2cPb$74T&+uW-Ohz|4 zU4$vSY}NQwH?NI;5;@cpGvt_5!Q6xebRH9nJWL32!r~R&(B!7%YKl8tPqiOb#x5Iy5 z0hKAxu?dl2Yf1bz+I%1xdk$y0($IUwNbVHu#pW}ZF~5(eS_Si7T%7zD!cP^qU_X^O*J=G(&SO~wn>-wXLnIjT$x~nS6y8rEt z9esXD^-5A$N=qBmNJfpk*+J2+R*mAu-@4i9oh_L3f3|9m{`nJ7nfoeNXxZ53`xD13 zJtOmeGVf644y(F=2bk_p*p)WYVx;^)7PAvw!kv(U;e|W-{@JV7UJbphbI1Et!R7%Q zOJ^aE(=hb8$sdY-t?jV0*ZhEn;MkuXIOcATSdr7%(Cg8kFOp?|LLcYMtPaCrqsRC# z%HS^4bzBn{u>@b2Z5f3$KYi|2lYb9y69Z*#rTYm_q=z+ z{sEdZ*MrNlJkDceyn{Q2WDaDBZsltN*ToJ-#-3JV_a<=Y-JvhlRsM5f-{pS=-M;4k zqkc^5Eq&x8@OPLdPcpCM@V(Bimvcht-yIL_Ym)$~ z!)r4NIF8rkXit~sGE<>LLMVi_)mFidovvbK%0Z}i(1`9%*4^fEX32#nVUMvBC-zS<9i ztt2_4Z6;2zFkKwb+z-RWW!H2sFj*~z-6H?*w?qx5uU15%?>-G+fPfBH+Er64lakA~ GBmNI0RyfH3 literal 0 HcmV?d00001 diff --git a/packages/twenty-front/public/logos/20-high-resolution-logo.png b/packages/twenty-front/public/logos/20-high-resolution-logo.png new file mode 100644 index 0000000000000000000000000000000000000000..0dbe8de6cba14a3bdeb246f740ce57c3dbbaa05f GIT binary patch literal 20740 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 literal 0 HcmV?d00001 From 36e59d80c488bb46b9a3ab4303d0035222f3810f Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Sat, 19 Oct 2024 10:59:04 +0200 Subject: [PATCH 25/67] Use the correct color for workflow nodes label (#7829) Follow the design from the Figma about the color to use. Big up to @Bonapara for doing great job on the Figma! It was done on stream! Fixes #7058 --- .../workflow/components/WorkflowDiagramBaseStepNode.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramBaseStepNode.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramBaseStepNode.tsx index 8484a29e7eaf..9d8c6b39b303 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramBaseStepNode.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramBaseStepNode.tsx @@ -66,7 +66,9 @@ const StyledStepNodeLabel = styled.div<{ variant?: Variant }>` font-weight: ${({ theme }) => theme.font.weight.medium}; column-gap: ${({ theme }) => theme.spacing(2)}; color: ${({ variant, theme }) => - variant === 'placeholder' ? theme.font.color.extraLight : null}; + variant === 'placeholder' + ? theme.font.color.extraLight + : theme.font.color.primary}; `; const StyledSourceHandle = styled(Handle)` From 8368f14fb913c1aaea0c6173570a691bbd0b9887 Mon Sep 17 00:00:00 2001 From: Anis Hamal <73131641+AndrewHamal@users.noreply.github.com> Date: Sat, 19 Oct 2024 15:27:43 +0545 Subject: [PATCH 26/67] Bug Fix: Decreased border radius of badge and changed badge parent div padding to margin (#7835) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What does this PR do? Decreased the border-radius of the badge and changed the padding-top and padding-bottom of the badge parent div to margin-top and margin-bottom Fixes #7811 Screenshot 2024-10-19 at 12 27 49 AM Screenshot 2024-10-19 at 12 28 37 AM ## How should this be tested? Create any task, notes, or files. --------- Co-authored-by: ehconitin --- .../timelineActivities/components/EventsGroup.tsx | 6 +++--- .../ui/layout/show-page/components/ShowPageSubContainer.tsx | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx b/packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx index 430968f4f259..fe368ae9bab9 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx +++ b/packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx @@ -20,8 +20,8 @@ const StyledActivityGroup = styled.div` `; const StyledActivityGroupContainer = styled.div` - padding-bottom: ${({ theme }) => theme.spacing(2)}; - padding-top: ${({ theme }) => theme.spacing(2)}; + margin-bottom: ${({ theme }) => theme.spacing(3)}; + margin-top: ${({ theme }) => theme.spacing(3)}; position: relative; `; @@ -29,7 +29,7 @@ const StyledActivityGroupBar = styled.div` align-items: center; background: ${({ theme }) => theme.background.secondary}; border: 1px solid ${({ theme }) => theme.border.color.light}; - border-radius: ${({ theme }) => theme.border.radius.xl}; + border-radius: ${({ theme }) => theme.border.radius.md}; display: flex; flex-direction: column; height: 100%; diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx index 017f38fd9280..d8bf449d6afd 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx @@ -61,13 +61,13 @@ const StyledGreyBox = styled.div<{ isInRightDrawer: boolean }>` const StyledButtonContainer = styled.div` align-items: center; - bottom: 0; + background: ${({ theme }) => theme.background.secondary}; border-top: 1px solid ${({ theme }) => theme.border.color.light}; + bottom: 0; + box-sizing: border-box; display: flex; justify-content: flex-end; padding: ${({ theme }) => theme.spacing(2)}; - width: 100%; - box-sizing: border-box; position: absolute; width: 100%; `; From ac88840bf040a2a1bf825484f1909ca530330b39 Mon Sep 17 00:00:00 2001 From: Nabhag Motivaras <65061890+Nabhag8848@users.noreply.github.com> Date: Sat, 19 Oct 2024 20:52:47 +0530 Subject: [PATCH 27/67] fix: redis url to not be optional anymore (#7850) ## Description - `REDIS_URL` is required Redis Required ---- - Closes #7849 - Might be related #7768 --- - Wasn't gracefully reseting database ``` npx nx database:reset twenty-server ``` --- packages/twenty-server/.env.example | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/twenty-server/.env.example b/packages/twenty-server/.env.example index f43d3ed7bca3..0b520154c4eb 100644 --- a/packages/twenty-server/.env.example +++ b/packages/twenty-server/.env.example @@ -8,6 +8,8 @@ LOGIN_TOKEN_SECRET=replace_me_with_a_random_string_login REFRESH_TOKEN_SECRET=replace_me_with_a_random_string_refresh FILE_TOKEN_SECRET=replace_me_with_a_random_string_refresh SIGN_IN_PREFILLED=true +REDIS_URL=redis://localhost:6379 + # ———————— Optional ———————— # PORT=3000 @@ -50,7 +52,6 @@ SIGN_IN_PREFILLED=true # SENTRY_FRONT_DSN=https://xxx@xxx.ingest.sentry.io/xxx # LOG_LEVELS=error,warn # MESSAGE_QUEUE_TYPE=pg-boss -# REDIS_URL=redis://localhost:6379 # DEMO_WORKSPACE_IDS=REPLACE_ME_WITH_A_RANDOM_UUID # SERVER_URL=http://localhost:3000 # WORKSPACE_INACTIVE_DAYS_BEFORE_NOTIFICATION=30 From c5138df58c94a8a9f916d51004bebf8ca57d9a1b Mon Sep 17 00:00:00 2001 From: sateshcharan Date: Sun, 20 Oct 2024 11:27:05 +0530 Subject: [PATCH 28/67] oss.gg side-quest-gif-magic completed (#7873) ![image](https://github.com/user-attachments/assets/3238d111-9098-4e60-a287-87d7f13f2ace) --- oss-gg/twenty-side-quest/5-gif-magic.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/oss-gg/twenty-side-quest/5-gif-magic.md b/oss-gg/twenty-side-quest/5-gif-magic.md index 320ffa9015db..b55bc24fc838 100644 --- a/oss-gg/twenty-side-quest/5-gif-magic.md +++ b/oss-gg/twenty-side-quest/5-gif-magic.md @@ -34,4 +34,7 @@ Your turn 👇 » 16-October-2024 by Harsh Bhat » Link to gif: https://giphy.com/gifs/oss-twentycrm-mgoYSDrjIalUL7XJzm + +» 20-October-2024 by Satesh Charan +» Link to gif: https://giphy.com/gifs/rXjvGBrTqu7vvhEsvR --- From dc1fbc3315e4e4bcc2d0c51ac9e58b1eef0f5a90 Mon Sep 17 00:00:00 2001 From: Manish Kr Prasad <85901005+Naprila@users.noreply.github.com> Date: Sun, 20 Oct 2024 13:49:42 +0530 Subject: [PATCH 29/67] Created a meme on Twenty and posted on X (#7883) ### Points: 150 ### Proof: Link to Tweet: https://x.com/mkprasad_821/status/1847900277510123706 ![Screenshot 2024-10-20 at 12 49 12 PM](https://github.com/user-attachments/assets/ae47d070-3b98-46b7-ba89-ecce8c16ae9a) Co-authored-by: Apple --- oss-gg/twenty-side-quest/4-meme-magic.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/oss-gg/twenty-side-quest/4-meme-magic.md b/oss-gg/twenty-side-quest/4-meme-magic.md index 041c73ef081e..feacc0b857a7 100644 --- a/oss-gg/twenty-side-quest/4-meme-magic.md +++ b/oss-gg/twenty-side-quest/4-meme-magic.md @@ -34,4 +34,7 @@ Your turn 👇 » 16-October-2024 by Harsh Bhat » Link to Tweet: https://x.com/HarshBhatX/status/1844698253104709899 + +» 20-October-2024 by Naprila +» Link to Tweet: https://x.com/mkprasad_821/status/1847900277510123706 --- From f801f3aa9f4f3328e950ed403d46fc535d967a5e Mon Sep 17 00:00:00 2001 From: Manish Kr Prasad <85901005+Naprila@users.noreply.github.com> Date: Sun, 20 Oct 2024 13:50:39 +0530 Subject: [PATCH 30/67] oss.gg Tweet about your favourite feature in Twenty (#7880) Point: 50 Proof: Link: https://x.com/mkprasad_821/status/1847895747707953205 Screenshot 2024-10-20 at 12 31 07 PM Co-authored-by: Apple --- oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md b/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md index 07d2e067c708..21840593b1af 100644 --- a/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md +++ b/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md @@ -28,4 +28,7 @@ Your turn 👇 » 16-October-2024 by Harsh Bhat » Link to Tweet: https://x.com/HarshBhatX/status/1846075312691413066 + +» 20-October-2024 by Naprila +» Link to Tweet: https://x.com/mkprasad_821/status/1847895747707953205 --- From eccf0bf8ba68560fa5e01c8fdce92240e7ea0e3f Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Sun, 20 Oct 2024 20:20:19 +0200 Subject: [PATCH 31/67] Enforce front project structure through ESLINT (#7863) Fixes: https://github.com/twentyhq/twenty/issues/7329 --- .gitignore | 1 + nx.json | 2 +- package.json | 1 + packages/twenty-front/.eslintrc.cjs | 9 +- packages/twenty-front/folderStructure.json | 81 ++++++++++++++ packages/twenty-front/jest.config.ts | 4 +- packages/twenty-front/project.json | 4 +- .../blocks/{ => components}/FileBlock.tsx | 6 +- .../blocks/{schema.ts => constants/Schema.ts} | 4 +- .../{slashMenu.tsx => utils/getSlashMenu.ts} | 4 +- .../calendar/components/Calendar.tsx | 4 +- .../__stories__/Calendar.stories.tsx | 4 +- .../timelineCalendarEventFragment.ts | 3 +- ...imelineCalendarEventParticipantFragment.ts | 0 .../timelineCalendarEventWithTotalFragment.ts | 3 +- .../getTimelineCalendarEventsFromCompanyId.ts | 3 +- .../getTimelineCalendarEventsFromPersonId.ts | 3 +- .../activities/components/RichTextEditor.tsx | 4 +- .../emails/components/EmailThreads.tsx | 4 +- .../getTimelineThreadsFromCompanyId.test.ts | 0 .../getTimelineThreadsFromPersonId.test.ts | 0 .../queries/fragments/participantFragment.ts | 0 .../fragments/timelineThreadFragment.ts | 2 +- .../timelineThreadWithTotalFragment.ts | 3 +- .../getTimelineThreadsFromCompanyId.ts | 3 +- .../queries/getTimelineThreadsFromPersonId.ts | 2 +- ...efreshShowPageFindManyActivitiesQueries.ts | 2 +- .../activities/hooks/useUpsertActivity.ts | 2 +- .../activities/notes/hooks/useNotes.ts | 2 +- .../tasks/components/PageAddTaskButton.tsx | 2 +- .../__stories__/TaskGroups.stories.tsx | 0 .../__stories__/TaskList.stories.tsx | 0 .../activities/tasks/hooks/useTasks.ts | 2 +- .../components/EventList.tsx | 8 +- .../components/EventRow.tsx | 12 +- .../components/EventsGroup.tsx | 4 +- .../components/TimelineActivities.tsx | 8 +- .../components/TimelineCreateButtonGroup.tsx | 0 .../TimelineActivities.stories.tsx | 6 +- .../FindManyTimelineActivitiesOrderBy.ts | 0 .../contexts/TimelineActivityContext.ts | 0 .../__tests__/useTimelineActivities.test.tsx | 2 +- .../useLinkedObjectObjectMetadataItem.ts | 0 .../hooks/useLinkedObjectsTitle.ts | 0 .../hooks/useTimelineActivities.ts | 4 +- .../activity/components/EventRowActivity.tsx | 2 +- .../components/EventCardCalendarEvent.tsx | 0 .../components/EventRowCalendarEvent.tsx | 10 +- .../EventCardCalendarEvent.stories.tsx | 4 +- .../rows/components/EventCard.tsx | 0 .../rows/components/EventCardToggleButton.tsx | 0 .../components/EventIconDynamicComponent.tsx | 2 +- .../components/EventRowDynamicComponent.tsx | 10 +- .../main-object/components/EventFieldDiff.tsx | 6 +- .../components/EventFieldDiffContainer.tsx | 2 +- .../components/EventFieldDiffLabel.tsx | 0 .../components/EventFieldDiffValue.tsx | 0 .../components/EventFieldDiffValueEffect.tsx | 0 .../components/EventRowMainObject.tsx | 7 +- .../components/EventRowMainObjectUpdated.tsx | 12 +- .../EventRowMainObjectUpdated.stories.tsx | 4 +- .../message/components/EventCardMessage.tsx | 2 +- .../components/EventCardMessageNotShared.tsx | 0 .../message/components/EventRowMessage.tsx | 10 +- .../__stories__/EventCardMessage.stories.tsx | 6 +- .../objectShowPageTargetableObjectIdState.ts | 0 .../types/TimelineActivity.ts | 0 .../types/TimelineActivityLinkedObject.ts | 0 ...filterOutInvalidTimelineActivities.test.ts | 4 +- .../getTimelineActivityAuthorFullName.test.ts | 4 +- .../__tests__/groupEventsByMonth.test.ts | 0 .../filterOutInvalidTimelineActivities.ts | 2 +- ...lterTimelineActivityByLinkedObjectTypes.ts | 4 +- .../getTimelineActivityAuthorFullName.ts | 2 +- .../utils/groupEventsByMonth.ts | 2 +- .../modules/apollo/services/apollo.factory.ts | 2 +- ...rmat-title.test.ts => formatTitle.test.ts} | 2 +- .../apollo/utils/__tests__/utils.test.ts | 4 - .../utils/{format-title.ts => formatTitle.ts} | 0 .../apollo/utils/{index.ts => loggerLink.ts} | 2 +- .../src/modules/app/components/AppRouter.tsx | 4 +- .../app/components/AppRouterProviders.tsx | 2 +- .../useCreateAppRouter.tsx} | 6 +- .../{__test__ => __tests__}/useAuth.test.tsx | 0 .../useIsLogged.test.ts | 0 .../src/modules/auth/services/AuthService.ts | 2 +- .../passwordRegex.test.ts | 0 .../command-menu/components/CommandMenu.tsx | 2 +- .../useCommandMenu.test.tsx | 0 .../modules/favorites/hooks/useFavorites.ts | 2 +- ...ort-favorites.util.ts => sortFavorites.ts} | 0 .../ApolloMetadataClientProvider.tsx | 2 +- .../PreComputedChipGeneratorsProvider.tsx | 2 +- .../ApolloClientMetadataContext.ts | 0 .../PreComputedChipGeneratorsContext.ts | 0 .../ApolloMetadataClientMockedProvider.tsx | 2 +- .../hooks/useApolloMetadataClient.ts | 5 +- ...=> mapFieldMetadataToGraphQLQuery.test.ts} | 0 ...> mapObjectMetadataToGraphQLQuery.test.ts} | 0 .../object-record/hooks/useRecordChipData.ts | 2 +- .../ObjectFilterDropdownDateInput.tsx | 6 +- ...Label.test.tsx => getOperandLabel.test.ts} | 0 ...t.tsx => getOperandsForFilterType.test.ts} | 0 .../utils/getRelativeDateDisplayValue.ts | 2 +- ....test.tsx => turnSortsIntoOrderBy.test.ts} | 0 .../record-board/components/RecordBoard.tsx | 2 +- ...st.ts => getDraggedRecordPosition.test.ts} | 2 +- ...on.util.ts => getDraggedRecordPosition.ts} | 0 .../FieldContextProvider.tsx | 0 .../meta-types/hooks/useChipFieldDisplay.ts | 2 +- .../hooks/useRelationFromManyFieldDisplay.ts | 4 +- .../hooks/useRelationToOneFieldDisplay.ts | 4 +- .../__stories__/AddressFieldInput.stories.tsx | 4 +- .../__stories__/BooleanFieldInput.stories.tsx | 4 +- .../DateTimeFieldInput.stories.tsx | 4 +- .../__stories__/NumberFieldInput.stories.tsx | 4 +- .../__stories__/RatingFieldInput.stories.tsx | 4 +- .../RelationManyFieldInput.stories.tsx | 4 +- .../RelationToOneFieldInput.stories.tsx | 2 +- .../__stories__/TextFieldInput.stories.tsx | 4 +- ...ldButtonIcon.tsx => getFieldButtonIcon.ts} | 0 ...turnObjectDropdownFilterIntoQueryFilter.ts | 2 +- .../components/RecordIndexPageHeader.tsx | 6 +- .../components/RecordShowContainer.tsx | 2 +- ...dTableFetchedAllRecordsComponentStateV2.ts | 2 +- ...isRecordTableScrolledLeftComponentState.ts | 2 +- ...jectRecordsSpreasheetImportDialog.test.ts} | 3 +- .../hooks/useBuildAvailableFieldsForImport.ts | 2 +- ...OpenObjectRecordsSpreasheetImportDialog.ts | 2 +- .../buildRecordFromImportedStructuredRow.ts | 0 ...etSpreadSheetFieldValidationDefinitions.ts | 0 ...olumnDefinitionsFromObjectMetadata.test.ts | 27 +++++ .../utils/getRecordChipGenerators.ts | 2 +- .../opportunities/{ => types}/Opportunity.ts | 0 .../prefetch/constants/PrefetchConfig.ts | 4 +- ...ndAllFavoritesOperationSignatureFactory.ts | 0 .../findAllViewsOperationSignatureFactory.ts | 0 .../components/SettingsSkeletonLoader.tsx | 4 +- .../SettingsDataModelFieldPreviewCard.tsx | 2 +- .../components/SettingsDataModelOverview.tsx | 10 +- .../SettingsDataModelOverviewObject.tsx | 2 +- .../__tests__/calculateHandlePosition.test.ts | 0 .../calculateHandlePosition.ts | 0 .../components/SettingsObjectItemTableRow.tsx | 2 +- .../components/SettingsObjectSummaryCard.tsx | 2 +- .../SettingsDataModelObjectSummary.tsx | 2 +- .../SettingsDataModelObjectTypeTag.tsx | 0 .../SettingsObjectCoverImage.tsx | 4 +- .../SettingsObjectInactiveMenuDropDown.tsx | 0 ...ingsObjectInactiveMenuDropDown.stories.tsx | 0 ...ettingsDataModelObjectSettingsFormCard.tsx | 2 +- .../components/SettingsApiKeysTable.tsx | 2 +- ...st.ts => computeNewExpirationDate.test.ts} | 2 +- ...ation.test.ts => formatExpiration.test.ts} | 2 +- ...on-date.ts => computeNewExpirationDate.ts} | 0 ...rmat-expiration.ts => formatExpiration.ts} | 0 ...tion.tsx => useExpandedHeightAnimation.ts} | 0 .../components/SignInBackgroundMockPage.tsx | 10 +- .../{tests => __mocks__}/mockRsiValues.ts | 0 .../__stories__/MatchColumns.stories.tsx | 2 +- .../__stories__/SelectHeader.stories.tsx | 8 +- .../__stories__/SelectSheet.stories.tsx | 2 +- .../components/__stories__/Upload.stories.tsx | 2 +- .../__stories__/Validation.stories.tsx | 8 +- .../code-editor/components/CodeEditor.tsx | 12 +- .../codeEditorTheme.ts} | 0 .../date/components/InternalDatePicker.tsx | 2 +- .../components/RelativeDatePickerHeader.tsx | 9 +- .../RelativeDateDirectionSelectOptions.ts | 2 +- .../RelativeDateUnitSelectOptions.ts | 2 +- .../input/editor/components/BlockEditor.tsx | 6 +- .../editor/components/CustomAddBlockItem.tsx | 4 +- .../editor/components/CustomSideMenu.tsx | 4 +- ...=> getFirstNonEmptyLineOfRichText.test.ts} | 0 .../__stories__/DraggableItem.stories.tsx | 3 +- .../__stories__/DraggableList.stories.tsx | 5 +- .../page/{ => components}/BlankLayout.tsx | 0 .../page/{ => components}/DefaultLayout.tsx | 0 .../page/{ => components}/PageAddButton.tsx | 0 .../layout/page/{ => components}/PageBody.tsx | 0 .../page/{ => components}/PageContainer.tsx | 0 .../{ => components}/PageFavoriteButton.tsx | 0 .../page/{ => components}/PageHeader.tsx | 0 .../{ => components}/PageHotkeysEffect.tsx | 0 .../page/{ => components}/PagePanel.tsx | 0 .../{ => components}/RightDrawerContainer.tsx | 0 .../{ => components}/ShowPageContainer.tsx | 0 .../SubMenuTopBarContainer.tsx | 0 .../components/ShowPageSubContainer.tsx | 2 +- .../top-bar/{ => components}/TopBar.tsx | 0 .../page-title/{ => components}/PageTitle.tsx | 0 .../__tests__/useAvailableScopeId.test.tsx | 2 +- .../utils/createScopeInternalContext.ts | 3 +- ...isMobile.test.tsx => useIsMobile.test.tsx} | 0 ...lpha.ts => createComponentStateV2Alpha.ts} | 0 .../src/modules/views/components/ViewBar.tsx | 2 +- .../views/components/ViewBarPageTitle.tsx | 2 +- .../internal/usePersistViewFieldRecords.ts | 2 +- .../computeVariableDateViewFilterValue.ts | 2 +- .../utils}/resolveDateViewFilterValue.ts | 0 .../utils}/resolveFilterValue.ts | 2 +- .../utils}/resolveNumberViewFilterValue.ts | 0 ...sion.tsx => useActivateWorkflowVersion.ts} | 0 ...ion.tsx => useCreateNewWorkflowVersion.ts} | 0 .../{useCreateStep.tsx => useCreateStep.ts} | 0 ...on.tsx => useDeactivateWorkflowVersion.ts} | 0 ...eDeleteOneStep.tsx => useDeleteOneStep.ts} | 0 ...ion.tsx => useDeleteOneWorkflowVersion.ts} | 0 ...deCreation.tsx => useStartNodeCreation.ts} | 0 ...lection.tsx => useTriggerNodeSelection.ts} | 0 ...ep.tsx => useUpdateWorkflowVersionStep.ts} | 0 ...tsx => useUpdateWorkflowVersionTrigger.ts} | 0 ...kflowVersion.tsx => useWorkflowVersion.ts} | 0 ...n.tsx => useWorkflowWithCurrentVersion.ts} | 0 ...ertWorkflowWithCurrentVersionIsDefined.ts} | 0 .../fragments/workspaceMemberQueryFragment.ts | 0 .../mutations/addUserToWorkspace.ts | 0 .../addUserToWorkspaceByInviteToken.ts | 0 .../src/pages/not-found/NotFound.tsx | 2 +- .../pages/object-record/RecordIndexPage.tsx | 6 +- .../pages/object-record/RecordShowPage.tsx | 8 +- .../RecordShowPageBaseHeader.tsx | 2 +- .../object-record/RecordShowPageHeader.tsx | 2 +- .../src/pages/settings/Releases.tsx | 2 +- .../src/pages/settings/SettingsBilling.tsx | 2 +- .../src/pages/settings/SettingsProfile.tsx | 2 +- .../src/pages/settings/SettingsWorkspace.tsx | 2 +- .../settings/SettingsWorkspaceMembers.tsx | 2 +- .../settings/accounts/SettingsAccounts.tsx | 2 +- .../accounts/SettingsAccountsCalendars.tsx | 2 +- .../accounts/SettingsAccountsEmails.tsx | 2 +- .../settings/accounts/SettingsNewAccount.tsx | 2 +- .../crm-migration/SettingsCRMMigration.tsx | 2 +- .../settings/data-model/SettingsNewObject.tsx | 2 +- .../SettingsObjectDetailPageContent.tsx | 2 +- .../data-model/SettingsObjectEdit.tsx | 2 +- .../data-model/SettingsObjectFieldEdit.tsx | 2 +- .../SettingsObjectNewFieldConfigure.tsx | 2 +- .../SettingsObjectNewFieldSelect.tsx | 2 +- .../data-model/SettingsObjectOverview.tsx | 2 +- .../settings/data-model/SettingsObjects.tsx | 6 +- .../developers/SettingsDevelopers.tsx | 2 +- .../SettingsDevelopersApiKeyDetail.tsx | 6 +- .../api-keys/SettingsDevelopersApiKeysNew.tsx | 2 +- .../SettingsDevelopersWebhookDetail.tsx | 2 +- .../SettingsDevelopersWebhooksNew.tsx | 2 +- .../SettingsIntegrationDatabase.tsx | 2 +- ...tingsIntegrationEditDatabaseConnection.tsx | 2 +- ...ttingsIntegrationNewDatabaseConnection.tsx | 2 +- ...tingsIntegrationShowDatabaseConnection.tsx | 2 +- .../integrations/SettingsIntegrations.tsx | 2 +- .../components/SettingsAppearance.tsx | 2 +- .../SettingsServerlessFunctionDetail.tsx | 4 +- .../SettingsServerlessFunctions.tsx | 2 +- .../SettingsServerlessFunctionsNew.tsx | 2 +- .../decorators/ChipGeneratorsDecorator.tsx | 2 +- .../src/testing/decorators/PageDecorator.tsx | 2 +- .../testing/mock-data/timeline-activities.ts | 2 +- packages/twenty-front/vite.config.ts | 2 +- yarn.lock | 104 ++++++++++++++++++ 260 files changed, 500 insertions(+), 290 deletions(-) create mode 100644 packages/twenty-front/folderStructure.json rename packages/twenty-front/src/modules/activities/blocks/{ => components}/FileBlock.tsx (94%) rename packages/twenty-front/src/modules/activities/blocks/{schema.ts => constants/Schema.ts} (57%) rename packages/twenty-front/src/modules/activities/blocks/{slashMenu.tsx => utils/getSlashMenu.ts} (90%) rename packages/twenty-front/src/modules/activities/calendar/{ => graphql}/queries/fragments/timelineCalendarEventFragment.ts (84%) rename packages/twenty-front/src/modules/activities/calendar/{ => graphql}/queries/fragments/timelineCalendarEventParticipantFragment.ts (100%) rename packages/twenty-front/src/modules/activities/calendar/{ => graphql}/queries/fragments/timelineCalendarEventWithTotalFragment.ts (86%) rename packages/twenty-front/src/modules/activities/calendar/{ => graphql}/queries/getTimelineCalendarEventsFromCompanyId.ts (86%) rename packages/twenty-front/src/modules/activities/calendar/{ => graphql}/queries/getTimelineCalendarEventsFromPersonId.ts (86%) rename packages/twenty-front/src/modules/activities/emails/{ => graphql}/queries/__tests__/getTimelineThreadsFromCompanyId.test.ts (100%) rename packages/twenty-front/src/modules/activities/emails/{ => graphql}/queries/__tests__/getTimelineThreadsFromPersonId.test.ts (100%) rename packages/twenty-front/src/modules/activities/emails/{ => graphql}/queries/fragments/participantFragment.ts (100%) rename packages/twenty-front/src/modules/activities/emails/{ => graphql}/queries/fragments/timelineThreadFragment.ts (80%) rename packages/twenty-front/src/modules/activities/emails/{ => graphql}/queries/fragments/timelineThreadWithTotalFragment.ts (71%) rename packages/twenty-front/src/modules/activities/emails/{ => graphql}/queries/getTimelineThreadsFromCompanyId.ts (88%) rename packages/twenty-front/src/modules/activities/emails/{ => graphql}/queries/getTimelineThreadsFromPersonId.ts (87%) rename packages/twenty-front/src/modules/activities/tasks/{ => components}/__stories__/TaskGroups.stories.tsx (100%) rename packages/twenty-front/src/modules/activities/tasks/{ => components}/__stories__/TaskList.stories.tsx (100%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/components/EventList.tsx (86%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/components/EventRow.tsx (89%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/components/EventsGroup.tsx (92%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/components/TimelineActivities.tsx (91%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/components/TimelineCreateButtonGroup.tsx (100%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities/components}/__stories__/TimelineActivities.stories.tsx (89%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/constants/FindManyTimelineActivitiesOrderBy.ts (100%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/contexts/TimelineActivityContext.ts (100%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/hooks/__tests__/useTimelineActivities.test.tsx (95%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/hooks/useLinkedObjectObjectMetadataItem.ts (100%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/hooks/useLinkedObjectsTitle.ts (100%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/hooks/useTimelineActivities.ts (90%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/activity/components/EventRowActivity.tsx (96%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/calendar/components/EventCardCalendarEvent.tsx (100%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/calendar/components/EventRowCalendarEvent.tsx (77%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/calendar/components/__stories__/EventCardCalendarEvent.stories.tsx (90%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/components/EventCard.tsx (100%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/components/EventCardToggleButton.tsx (100%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/components/EventIconDynamicComponent.tsx (88%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/components/EventRowDynamicComponent.tsx (84%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/main-object/components/EventFieldDiff.tsx (88%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/main-object/components/EventFieldDiffContainer.tsx (91%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/main-object/components/EventFieldDiffLabel.tsx (100%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/main-object/components/EventFieldDiffValue.tsx (100%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/main-object/components/EventFieldDiffValueEffect.tsx (100%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/main-object/components/EventRowMainObject.tsx (91%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/main-object/components/EventRowMainObjectUpdated.tsx (84%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/main-object/components/__stories__/EventRowMainObjectUpdated.stories.tsx (90%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/message/components/EventCardMessage.tsx (98%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/message/components/EventCardMessageNotShared.tsx (100%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/message/components/EventRowMessage.tsx (79%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/rows/message/components/__stories__/EventCardMessage.stories.tsx (87%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/states/objectShowPageTargetableObjectIdState.ts (100%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/types/TimelineActivity.ts (100%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/types/TimelineActivityLinkedObject.ts (100%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/utils/__tests__/filterOutInvalidTimelineActivities.test.ts (96%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/utils/__tests__/getTimelineActivityAuthorFullName.test.ts (91%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/utils/__tests__/groupEventsByMonth.test.ts (100%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/utils/filterOutInvalidTimelineActivities.ts (95%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/utils/filterTimelineActivityByLinkedObjectTypes.ts (75%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/utils/getTimelineActivityAuthorFullName.ts (84%) rename packages/twenty-front/src/modules/activities/{timelineActivities => timeline-activities}/utils/groupEventsByMonth.ts (89%) rename packages/twenty-front/src/modules/apollo/utils/__tests__/{format-title.test.ts => formatTitle.test.ts} (91%) delete mode 100644 packages/twenty-front/src/modules/apollo/utils/__tests__/utils.test.ts rename packages/twenty-front/src/modules/apollo/utils/{format-title.ts => formatTitle.ts} (100%) rename packages/twenty-front/src/modules/apollo/utils/{index.ts => loggerLink.ts} (98%) rename packages/twenty-front/src/modules/app/{utils/createAppRouter.tsx => hooks/useCreateAppRouter.tsx} (95%) rename packages/twenty-front/src/modules/auth/hooks/{__test__ => __tests__}/useAuth.test.tsx (100%) rename packages/twenty-front/src/modules/auth/hooks/{__test__ => __tests__}/useIsLogged.test.ts (100%) rename packages/twenty-front/src/modules/auth/utils/{__test__ => __tests__}/passwordRegex.test.ts (100%) rename packages/twenty-front/src/modules/command-menu/hooks/{__test__ => __tests__}/useCommandMenu.test.tsx (100%) rename packages/twenty-front/src/modules/favorites/utils/{sort-favorites.util.ts => sortFavorites.ts} (100%) rename packages/twenty-front/src/modules/object-metadata/{context => contexts}/ApolloClientMetadataContext.ts (100%) rename packages/twenty-front/src/modules/object-metadata/{context => contexts}/PreComputedChipGeneratorsContext.ts (100%) rename packages/twenty-front/src/modules/object-metadata/utils/__tests__/{mapFieldMetadataToGraphQLQuery.test.tsx => mapFieldMetadataToGraphQLQuery.test.ts} (100%) rename packages/twenty-front/src/modules/object-metadata/utils/__tests__/{mapObjectMetadataToGraphQLQuery.test.tsx => mapObjectMetadataToGraphQLQuery.test.ts} (100%) rename packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/{getOperandLabel.test.tsx => getOperandLabel.test.ts} (100%) rename packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/{getOperandsForFilterType.test.tsx => getOperandsForFilterType.test.ts} (100%) rename packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/{turnSortsIntoOrderBy.test.tsx => turnSortsIntoOrderBy.test.ts} (100%) rename packages/twenty-front/src/modules/object-record/record-board/utils/__tests__/{get-dragged-record-position.util.test.ts => getDraggedRecordPosition.test.ts} (92%) rename packages/twenty-front/src/modules/object-record/record-board/utils/{get-dragged-record-position.util.ts => getDraggedRecordPosition.ts} (100%) rename packages/twenty-front/src/modules/object-record/record-field/meta-types/{__stories__ => components}/FieldContextProvider.tsx (100%) rename packages/twenty-front/src/modules/object-record/record-field/utils/{getFieldButtonIcon.tsx => getFieldButtonIcon.ts} (100%) rename packages/twenty-front/src/modules/object-record/spreadsheet-import/{__tests__/useOpenObjectRecordsSpreasheetImportDialog.test.tsx => hooks/__tests__/useOpenObjectRecordsSpreasheetImportDialog.test.ts} (98%) rename packages/twenty-front/src/modules/object-record/spreadsheet-import/{util => utils}/buildRecordFromImportedStructuredRow.ts (100%) rename packages/twenty-front/src/modules/object-record/spreadsheet-import/{util => utils}/getSpreadSheetFieldValidationDefinitions.ts (100%) create mode 100644 packages/twenty-front/src/modules/object-record/utils/__tests__/computeRecordBoardColumnDefinitionsFromObjectMetadata.test.ts rename packages/twenty-front/src/modules/opportunities/{ => types}/Opportunity.ts (100%) rename packages/twenty-front/src/modules/prefetch/{ => graphql}/operation-signatures/factories/findAllFavoritesOperationSignatureFactory.ts (100%) rename packages/twenty-front/src/modules/prefetch/{ => graphql}/operation-signatures/factories/findAllViewsOperationSignatureFactory.ts (100%) rename packages/twenty-front/src/modules/settings/data-model/graph-overview/{util => utils}/__tests__/calculateHandlePosition.test.ts (100%) rename packages/twenty-front/src/modules/settings/data-model/graph-overview/{util => utils}/calculateHandlePosition.ts (100%) rename packages/twenty-front/src/modules/settings/data-model/objects/{ => components}/SettingsDataModelObjectSummary.tsx (96%) rename packages/twenty-front/src/modules/settings/data-model/objects/{ => components}/SettingsDataModelObjectTypeTag.tsx (100%) rename packages/twenty-front/src/modules/settings/data-model/objects/{ => components}/SettingsObjectCoverImage.tsx (91%) rename packages/twenty-front/src/modules/settings/data-model/objects/{ => components}/SettingsObjectInactiveMenuDropDown.tsx (100%) rename packages/twenty-front/src/modules/settings/data-model/objects/{ => components}/__stories__/SettingsObjectInactiveMenuDropDown.stories.tsx (100%) rename packages/twenty-front/src/modules/settings/developers/utils/__tests__/{compute-new-expiration-date.test.ts => computeNewExpirationDate.test.ts} (96%) rename packages/twenty-front/src/modules/settings/developers/utils/__tests__/{format-expiration.test.ts => formatExpiration.test.ts} (99%) rename packages/twenty-front/src/modules/settings/developers/utils/{compute-new-expiration-date.ts => computeNewExpirationDate.ts} (100%) rename packages/twenty-front/src/modules/settings/developers/utils/{format-expiration.ts => formatExpiration.ts} (100%) rename packages/twenty-front/src/modules/settings/hooks/{useExpandedHeightAnimation.tsx => useExpandedHeightAnimation.ts} (100%) rename packages/twenty-front/src/modules/spreadsheet-import/{tests => __mocks__}/mockRsiValues.ts (100%) rename packages/twenty-front/src/modules/ui/input/code-editor/{theme/CodeEditorTheme.ts => utils/codeEditorTheme.ts} (100%) rename packages/twenty-front/src/modules/ui/input/editor/utils/__tests__/{getFirstNonEmptyLineOfRichText.test.tsx => getFirstNonEmptyLineOfRichText.test.ts} (100%) rename packages/twenty-front/src/modules/ui/layout/draggable-list/{ => components}/__stories__/DraggableItem.stories.tsx (92%) rename packages/twenty-front/src/modules/ui/layout/draggable-list/{ => components}/__stories__/DraggableList.stories.tsx (89%) rename packages/twenty-front/src/modules/ui/layout/page/{ => components}/BlankLayout.tsx (100%) rename packages/twenty-front/src/modules/ui/layout/page/{ => components}/DefaultLayout.tsx (100%) rename packages/twenty-front/src/modules/ui/layout/page/{ => components}/PageAddButton.tsx (100%) rename packages/twenty-front/src/modules/ui/layout/page/{ => components}/PageBody.tsx (100%) rename packages/twenty-front/src/modules/ui/layout/page/{ => components}/PageContainer.tsx (100%) rename packages/twenty-front/src/modules/ui/layout/page/{ => components}/PageFavoriteButton.tsx (100%) rename packages/twenty-front/src/modules/ui/layout/page/{ => components}/PageHeader.tsx (100%) rename packages/twenty-front/src/modules/ui/layout/page/{ => components}/PageHotkeysEffect.tsx (100%) rename packages/twenty-front/src/modules/ui/layout/page/{ => components}/PagePanel.tsx (100%) rename packages/twenty-front/src/modules/ui/layout/page/{ => components}/RightDrawerContainer.tsx (100%) rename packages/twenty-front/src/modules/ui/layout/page/{ => components}/ShowPageContainer.tsx (100%) rename packages/twenty-front/src/modules/ui/layout/page/{ => components}/SubMenuTopBarContainer.tsx (100%) rename packages/twenty-front/src/modules/ui/layout/top-bar/{ => components}/TopBar.tsx (100%) rename packages/twenty-front/src/modules/ui/utilities/page-title/{ => components}/PageTitle.tsx (100%) rename packages/twenty-front/src/modules/ui/utilities/responsive/hooks/__tests__/{isMobile.test.tsx => useIsMobile.test.tsx} (100%) rename packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/{createComponentStateV2_alpha.ts => createComponentStateV2Alpha.ts} (100%) rename packages/twenty-front/src/modules/views/{utils/view-filter-value => view-filter-value/utils}/computeVariableDateViewFilterValue.ts (83%) rename packages/twenty-front/src/modules/views/{utils/view-filter-value => view-filter-value/utils}/resolveDateViewFilterValue.ts (100%) rename packages/twenty-front/src/modules/views/{utils/view-filter-value => view-filter-value/utils}/resolveFilterValue.ts (91%) rename packages/twenty-front/src/modules/views/{utils/view-filter-value => view-filter-value/utils}/resolveNumberViewFilterValue.ts (100%) rename packages/twenty-front/src/modules/workflow/hooks/{useActivateWorkflowVersion.tsx => useActivateWorkflowVersion.ts} (100%) rename packages/twenty-front/src/modules/workflow/hooks/{useCreateNewWorkflowVersion.tsx => useCreateNewWorkflowVersion.ts} (100%) rename packages/twenty-front/src/modules/workflow/hooks/{useCreateStep.tsx => useCreateStep.ts} (100%) rename packages/twenty-front/src/modules/workflow/hooks/{useDeactivateWorkflowVersion.tsx => useDeactivateWorkflowVersion.ts} (100%) rename packages/twenty-front/src/modules/workflow/hooks/{useDeleteOneStep.tsx => useDeleteOneStep.ts} (100%) rename packages/twenty-front/src/modules/workflow/hooks/{useDeleteOneWorkflowVersion.tsx => useDeleteOneWorkflowVersion.ts} (100%) rename packages/twenty-front/src/modules/workflow/hooks/{useStartNodeCreation.tsx => useStartNodeCreation.ts} (100%) rename packages/twenty-front/src/modules/workflow/hooks/{useTriggerNodeSelection.tsx => useTriggerNodeSelection.ts} (100%) rename packages/twenty-front/src/modules/workflow/hooks/{useUpdateWorkflowVersionStep.tsx => useUpdateWorkflowVersionStep.ts} (100%) rename packages/twenty-front/src/modules/workflow/hooks/{useUpdateWorkflowVersionTrigger.tsx => useUpdateWorkflowVersionTrigger.ts} (100%) rename packages/twenty-front/src/modules/workflow/hooks/{useWorkflowVersion.tsx => useWorkflowVersion.ts} (100%) rename packages/twenty-front/src/modules/workflow/hooks/{useWorkflowWithCurrentVersion.tsx => useWorkflowWithCurrentVersion.ts} (100%) rename packages/twenty-front/src/modules/workflow/utils/{assertWorkflowWithCurrentVersionIsDefined.tsx => assertWorkflowWithCurrentVersionIsDefined.ts} (100%) rename packages/twenty-front/src/modules/workspace-member/{grapqhql => graphql}/fragments/workspaceMemberQueryFragment.ts (100%) rename packages/twenty-front/src/modules/workspace-member/{grapqhql => graphql}/mutations/addUserToWorkspace.ts (100%) rename packages/twenty-front/src/modules/workspace-member/{grapqhql => graphql}/mutations/addUserToWorkspaceByInviteToken.ts (100%) diff --git a/.gitignore b/.gitignore index c5bb33e003d1..0d87dfc1cdc6 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ .nx/installation .nx/cache +projectStructure.cache.json .pnp.* .yarn/* diff --git a/nx.json b/nx.json index 35b6e7501700..0030428ee966 100644 --- a/nx.json +++ b/nx.json @@ -113,7 +113,7 @@ "outputs": ["{projectRoot}/{options.output-dir}"], "options": { "cwd": "{projectRoot}", - "command": "storybook build", + "command": "VITE_DISABLE_ESLINT_CHECKER=true storybook build", "output-dir": "storybook-static", "config-dir": ".storybook" } diff --git a/package.json b/package.json index f86d4c1b9d08..a83f2180fa98 100644 --- a/package.json +++ b/package.json @@ -294,6 +294,7 @@ "eslint-plugin-jsx-a11y": "^6.8.0", "eslint-plugin-prefer-arrow": "^1.2.3", "eslint-plugin-prettier": "^5.1.2", + "eslint-plugin-project-structure": "^3.7.2", "eslint-plugin-react": "^7.33.2", "eslint-plugin-react-hooks": "^4.6.0", "eslint-plugin-react-refresh": "^0.4.4", diff --git a/packages/twenty-front/.eslintrc.cjs b/packages/twenty-front/.eslintrc.cjs index df4daf7633a4..790724178919 100644 --- a/packages/twenty-front/.eslintrc.cjs +++ b/packages/twenty-front/.eslintrc.cjs @@ -21,7 +21,14 @@ module.exports = { parserOptions: { project: ['packages/twenty-front/tsconfig.{json,*.json}'], }, - rules: {}, + plugins: ['project-structure'], + settings: { + 'project-structure/folder-structure-config-path': + 'packages/twenty-front/folderStructure.json', + }, + rules: { + 'project-structure/folder-structure': 'error', + }, }, ], }; diff --git a/packages/twenty-front/folderStructure.json b/packages/twenty-front/folderStructure.json new file mode 100644 index 000000000000..4dab3b2cda49 --- /dev/null +++ b/packages/twenty-front/folderStructure.json @@ -0,0 +1,81 @@ +{ + "$schema": "../../node_modules/eslint-plugin-project-structure/folderStructure.schema.json", + "regexParameters": { + "camelCase": "^[a-z]+([A-Za-z0-9]+)+" + }, + "structure": [ + { + "name": "packages", + "children": [ + { + "name": "twenty-front", + "children": [ + { "name": "*", "children": [] }, + { "name": "*" }, + { + "name": "src", + "children": [ + { "name": "*", "children": [] }, + { "name": "*" }, + { + "name": "modules", + "children": [ + { "ruleId": "moduleFolderRule" }, + { "name": "types", "ruleId": "doNotCheckLeafFolderRule" } + ] + } + ] + } + ] + } + ] + } + ], + "rules": { + "moduleFolderRule": { + "name": "^(?!utils$|hooks$|states$|types$|graphql$|components$|effect-components$|constants$|validation-schemas$|contexts$|scopes$|services$|errors$)[a-z][a-z0-9]**(?:-[a-z0-9]+)**$", + "folderRecursionLimit": 6, + "children": [ + { "ruleId": "moduleFolderRule" }, + { "name": "hooks", "ruleId": "hooksLeafFolderRule" }, + { "name": "utils", "ruleId": "utilsLeafFolderRule" }, + { "name": "states", "ruleId": "doNotCheckLeafFolderRule" }, + { "name": "types", "ruleId": "doNotCheckLeafFolderRule" }, + { "name": "graphql", "ruleId": "doNotCheckLeafFolderRule" }, + { "name": "components", "ruleId": "doNotCheckLeafFolderRule" }, + { "name": "effect-components", "ruleId": "doNotCheckLeafFolderRule" }, + { "name": "constants", "ruleId": "doNotCheckLeafFolderRule" }, + { "name": "validation-schemas", "ruleId": "doNotCheckLeafFolderRule" }, + { "name": "contexts", "ruleId": "doNotCheckLeafFolderRule" }, + { "name": "scopes", "ruleId": "doNotCheckLeafFolderRule" }, + { "name": "services", "ruleId": "doNotCheckLeafFolderRule" }, + { "name": "errors", "ruleId": "doNotCheckLeafFolderRule" } + ] + }, + "hooksLeafFolderRule": { + "folderRecursionLimit": 2, + "children": [ + { "name": "use{PascalCase}.(ts|tsx)" }, + { + "name": "__tests__", + "children": [{ "name": "use{PascalCase}.test.(ts|tsx)" }] + }, + { "name": "internal", "ruleId": "hooksLeafFolderRule" } + ] + }, + "doNotCheckLeafFolderRule": { + "folderRecursionLimit": 1, + "children": [{ "name": "*" }, { "name": "*", "children": [] }] + }, + "utilsLeafFolderRule": { + "folderRecursionLimit": 1, + "children": [ + { "name": "{camelCase}.ts" }, + { + "name": "__tests__", + "children": [{ "name": "{camelCase}.test.ts" }] + } + ] + } + } +} diff --git a/packages/twenty-front/jest.config.ts b/packages/twenty-front/jest.config.ts index 8ed7f398db4e..c3e8ab4148e7 100644 --- a/packages/twenty-front/jest.config.ts +++ b/packages/twenty-front/jest.config.ts @@ -25,9 +25,9 @@ const jestConfig: JestConfigWithTsJest = { extensionsToTreatAsEsm: ['.ts', '.tsx'], coverageThreshold: { global: { - statements: 60, + statements: 59, lines: 55, - functions: 50, + functions: 49, }, }, collectCoverageFrom: ['/src/**/*.ts'], diff --git a/packages/twenty-front/project.json b/packages/twenty-front/project.json index 3ed94b22f256..245fde1fac34 100644 --- a/packages/twenty-front/project.json +++ b/packages/twenty-front/project.json @@ -52,7 +52,9 @@ "reportUnusedDisableDirectives": "error" }, "configurations": { - "ci": { "eslintConfig": "{projectRoot}/.eslintrc-ci.cjs" }, + "ci": { + "eslintConfig": "{projectRoot}/.eslintrc-ci.cjs" + }, "fix": {} } }, diff --git a/packages/twenty-front/src/modules/activities/blocks/FileBlock.tsx b/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx similarity index 94% rename from packages/twenty-front/src/modules/activities/blocks/FileBlock.tsx rename to packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx index 680ac551f662..d1d308dcd4f2 100644 --- a/packages/twenty-front/src/modules/activities/blocks/FileBlock.tsx +++ b/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx @@ -8,9 +8,9 @@ import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; import { isDefined } from '~/utils/isDefined'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; -import { AttachmentIcon } from '../files/components/AttachmentIcon'; -import { AttachmentType } from '../files/types/Attachment'; -import { getFileType } from '../files/utils/getFileType'; +import { AttachmentIcon } from '../../files/components/AttachmentIcon'; +import { AttachmentType } from '../../files/types/Attachment'; +import { getFileType } from '../../files/utils/getFileType'; const StyledFileInput = styled.input` display: none; diff --git a/packages/twenty-front/src/modules/activities/blocks/schema.ts b/packages/twenty-front/src/modules/activities/blocks/constants/Schema.ts similarity index 57% rename from packages/twenty-front/src/modules/activities/blocks/schema.ts rename to packages/twenty-front/src/modules/activities/blocks/constants/Schema.ts index d6ea82eac19b..2584f3c7b8c5 100644 --- a/packages/twenty-front/src/modules/activities/blocks/schema.ts +++ b/packages/twenty-front/src/modules/activities/blocks/constants/Schema.ts @@ -1,8 +1,8 @@ import { BlockNoteSchema, defaultBlockSpecs } from '@blocknote/core'; -import { FileBlock } from './FileBlock'; +import { FileBlock } from '../components/FileBlock'; -export const blockSchema = BlockNoteSchema.create({ +export const BLOCK_SCHEMA = BlockNoteSchema.create({ blockSpecs: { ...defaultBlockSpecs, file: FileBlock, diff --git a/packages/twenty-front/src/modules/activities/blocks/slashMenu.tsx b/packages/twenty-front/src/modules/activities/blocks/utils/getSlashMenu.ts similarity index 90% rename from packages/twenty-front/src/modules/activities/blocks/slashMenu.tsx rename to packages/twenty-front/src/modules/activities/blocks/utils/getSlashMenu.ts index 34a161bab747..760a778b57b2 100644 --- a/packages/twenty-front/src/modules/activities/blocks/slashMenu.tsx +++ b/packages/twenty-front/src/modules/activities/blocks/utils/getSlashMenu.ts @@ -18,7 +18,7 @@ import { import { SuggestionItem } from '@/ui/input/editor/components/CustomSlashMenu'; -import { blockSchema } from './schema'; +import { BLOCK_SCHEMA } from '../constants/Schema'; const Icons: Record = { 'Heading 1': IconH1, @@ -35,7 +35,7 @@ const Icons: Record = { Emoji: IconMoodSmile, }; -export const getSlashMenu = (editor: typeof blockSchema.BlockNoteEditor) => { +export const getSlashMenu = (editor: typeof BLOCK_SCHEMA.BlockNoteEditor) => { const items: SuggestionItem[] = [ ...getDefaultReactSlashMenuItems(editor).map((x) => ({ ...x, diff --git a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx index 360eccbbcc2a..83f06303b74f 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx @@ -5,9 +5,9 @@ import { H3Title } from 'twenty-ui'; import { CalendarMonthCard } from '@/activities/calendar/components/CalendarMonthCard'; import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from '@/activities/calendar/constants/Calendar'; import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext'; +import { getTimelineCalendarEventsFromCompanyId } from '@/activities/calendar/graphql/queries/getTimelineCalendarEventsFromCompanyId'; +import { getTimelineCalendarEventsFromPersonId } from '@/activities/calendar/graphql/queries/getTimelineCalendarEventsFromPersonId'; import { useCalendarEvents } from '@/activities/calendar/hooks/useCalendarEvents'; -import { getTimelineCalendarEventsFromCompanyId } from '@/activities/calendar/queries/getTimelineCalendarEventsFromCompanyId'; -import { getTimelineCalendarEventsFromPersonId } from '@/activities/calendar/queries/getTimelineCalendarEventsFromPersonId'; import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader'; import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { useCustomResolver } from '@/activities/hooks/useCustomResolver'; diff --git a/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx b/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx index b2732df86544..eb4aa38eda72 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx @@ -1,10 +1,10 @@ import { getOperationName } from '@apollo/client/utilities'; import { Meta, StoryObj } from '@storybook/react'; -import { graphql, HttpResponse } from 'msw'; +import { HttpResponse, graphql } from 'msw'; import { ComponentDecorator } from 'twenty-ui'; import { Calendar } from '@/activities/calendar/components/Calendar'; -import { getTimelineCalendarEventsFromCompanyId } from '@/activities/calendar/queries/getTimelineCalendarEventsFromCompanyId'; +import { getTimelineCalendarEventsFromCompanyId } from '@/activities/calendar/graphql/queries/getTimelineCalendarEventsFromCompanyId'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { graphqlMocks } from '~/testing/graphqlMocks'; diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventFragment.ts b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/fragments/timelineCalendarEventFragment.ts similarity index 84% rename from packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventFragment.ts rename to packages/twenty-front/src/modules/activities/calendar/graphql/queries/fragments/timelineCalendarEventFragment.ts index d98c7bcf81b2..eb152294ccea 100644 --- a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventFragment.ts +++ b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/fragments/timelineCalendarEventFragment.ts @@ -1,7 +1,6 @@ +import { timelineCalendarEventParticipantFragment } from '@/activities/calendar/graphql/queries/fragments/timelineCalendarEventParticipantFragment'; import { gql } from '@apollo/client'; -import { timelineCalendarEventParticipantFragment } from '@/activities/calendar/queries/fragments/timelineCalendarEventParticipantFragment'; - export const timelineCalendarEventFragment = gql` fragment TimelineCalendarEventFragment on TimelineCalendarEvent { id diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventParticipantFragment.ts b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/fragments/timelineCalendarEventParticipantFragment.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventParticipantFragment.ts rename to packages/twenty-front/src/modules/activities/calendar/graphql/queries/fragments/timelineCalendarEventParticipantFragment.ts diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventWithTotalFragment.ts b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/fragments/timelineCalendarEventWithTotalFragment.ts similarity index 86% rename from packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventWithTotalFragment.ts rename to packages/twenty-front/src/modules/activities/calendar/graphql/queries/fragments/timelineCalendarEventWithTotalFragment.ts index 2a76f0f7fa41..58de733417c1 100644 --- a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/timelineCalendarEventWithTotalFragment.ts +++ b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/fragments/timelineCalendarEventWithTotalFragment.ts @@ -1,7 +1,6 @@ +import { timelineCalendarEventFragment } from '@/activities/calendar/graphql/queries/fragments/timelineCalendarEventFragment'; import { gql } from '@apollo/client'; -import { timelineCalendarEventFragment } from '@/activities/calendar/queries/fragments/timelineCalendarEventFragment'; - export const timelineCalendarEventWithTotalFragment = gql` fragment TimelineCalendarEventsWithTotalFragment on TimelineCalendarEventsWithTotal { totalNumberOfCalendarEvents diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromCompanyId.ts b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromCompanyId.ts similarity index 86% rename from packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromCompanyId.ts rename to packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromCompanyId.ts index e454e67452f3..c43d197e43ff 100644 --- a/packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromCompanyId.ts +++ b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromCompanyId.ts @@ -1,7 +1,6 @@ +import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/graphql/queries/fragments/timelineCalendarEventWithTotalFragment'; import { gql } from '@apollo/client'; -import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/queries/fragments/timelineCalendarEventWithTotalFragment'; - export const getTimelineCalendarEventsFromCompanyId = gql` query GetTimelineCalendarEventsFromCompanyId( $companyId: UUID! diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromPersonId.ts b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromPersonId.ts similarity index 86% rename from packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromPersonId.ts rename to packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromPersonId.ts index 7d9f221fbc78..3285fb475d23 100644 --- a/packages/twenty-front/src/modules/activities/calendar/queries/getTimelineCalendarEventsFromPersonId.ts +++ b/packages/twenty-front/src/modules/activities/calendar/graphql/queries/getTimelineCalendarEventsFromPersonId.ts @@ -1,7 +1,6 @@ +import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/graphql/queries/fragments/timelineCalendarEventWithTotalFragment'; import { gql } from '@apollo/client'; -import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/queries/fragments/timelineCalendarEventWithTotalFragment'; - export const getTimelineCalendarEventsFromPersonId = gql` query GetTimelineCalendarEventsFromPersonId( $personId: UUID! diff --git a/packages/twenty-front/src/modules/activities/components/RichTextEditor.tsx b/packages/twenty-front/src/modules/activities/components/RichTextEditor.tsx index c6842395f64f..21ac7228d3da 100644 --- a/packages/twenty-front/src/modules/activities/components/RichTextEditor.tsx +++ b/packages/twenty-front/src/modules/activities/components/RichTextEditor.tsx @@ -7,7 +7,6 @@ import { Key } from 'ts-key-enum'; import { useDebouncedCallback } from 'use-debounce'; import { v4 } from 'uuid'; -import { blockSchema } from '@/activities/blocks/schema'; import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity'; import { activityBodyFamilyState } from '@/activities/states/activityBodyFamilyState'; import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState'; @@ -27,6 +26,7 @@ import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { getFileType } from '../files/utils/getFileType'; +import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema'; import { Note } from '@/activities/types/Note'; import { Task } from '@/activities/types/Task'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; @@ -287,7 +287,7 @@ export const RichTextEditor = ({ const editor = useCreateBlockNote({ initialContent: initialBody, domAttributes: { editor: { class: 'editor' } }, - schema: blockSchema, + schema: BLOCK_SCHEMA, uploadFile: handleUploadAttachment, }); diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx index 8a3eef7ea33f..a46df19b7f1b 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx @@ -6,8 +6,8 @@ import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomRes import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { EmailThreadPreview } from '@/activities/emails/components/EmailThreadPreview'; import { TIMELINE_THREADS_DEFAULT_PAGE_SIZE } from '@/activities/emails/constants/Messaging'; -import { getTimelineThreadsFromCompanyId } from '@/activities/emails/queries/getTimelineThreadsFromCompanyId'; -import { getTimelineThreadsFromPersonId } from '@/activities/emails/queries/getTimelineThreadsFromPersonId'; +import { getTimelineThreadsFromCompanyId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromCompanyId'; +import { getTimelineThreadsFromPersonId } from '@/activities/emails/graphql/queries/getTimelineThreadsFromPersonId'; import { useCustomResolver } from '@/activities/hooks/useCustomResolver'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; diff --git a/packages/twenty-front/src/modules/activities/emails/queries/__tests__/getTimelineThreadsFromCompanyId.test.ts b/packages/twenty-front/src/modules/activities/emails/graphql/queries/__tests__/getTimelineThreadsFromCompanyId.test.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/emails/queries/__tests__/getTimelineThreadsFromCompanyId.test.ts rename to packages/twenty-front/src/modules/activities/emails/graphql/queries/__tests__/getTimelineThreadsFromCompanyId.test.ts diff --git a/packages/twenty-front/src/modules/activities/emails/queries/__tests__/getTimelineThreadsFromPersonId.test.ts b/packages/twenty-front/src/modules/activities/emails/graphql/queries/__tests__/getTimelineThreadsFromPersonId.test.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/emails/queries/__tests__/getTimelineThreadsFromPersonId.test.ts rename to packages/twenty-front/src/modules/activities/emails/graphql/queries/__tests__/getTimelineThreadsFromPersonId.test.ts diff --git a/packages/twenty-front/src/modules/activities/emails/queries/fragments/participantFragment.ts b/packages/twenty-front/src/modules/activities/emails/graphql/queries/fragments/participantFragment.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/emails/queries/fragments/participantFragment.ts rename to packages/twenty-front/src/modules/activities/emails/graphql/queries/fragments/participantFragment.ts diff --git a/packages/twenty-front/src/modules/activities/emails/queries/fragments/timelineThreadFragment.ts b/packages/twenty-front/src/modules/activities/emails/graphql/queries/fragments/timelineThreadFragment.ts similarity index 80% rename from packages/twenty-front/src/modules/activities/emails/queries/fragments/timelineThreadFragment.ts rename to packages/twenty-front/src/modules/activities/emails/graphql/queries/fragments/timelineThreadFragment.ts index d5728f23efaf..7d8f8ab9c351 100644 --- a/packages/twenty-front/src/modules/activities/emails/queries/fragments/timelineThreadFragment.ts +++ b/packages/twenty-front/src/modules/activities/emails/graphql/queries/fragments/timelineThreadFragment.ts @@ -1,6 +1,6 @@ import { gql } from '@apollo/client'; -import { participantFragment } from '@/activities/emails/queries/fragments/participantFragment'; +import { participantFragment } from '@/activities/emails/graphql/queries/fragments/participantFragment'; export const timelineThreadFragment = gql` fragment TimelineThreadFragment on TimelineThread { diff --git a/packages/twenty-front/src/modules/activities/emails/queries/fragments/timelineThreadWithTotalFragment.ts b/packages/twenty-front/src/modules/activities/emails/graphql/queries/fragments/timelineThreadWithTotalFragment.ts similarity index 71% rename from packages/twenty-front/src/modules/activities/emails/queries/fragments/timelineThreadWithTotalFragment.ts rename to packages/twenty-front/src/modules/activities/emails/graphql/queries/fragments/timelineThreadWithTotalFragment.ts index 89dc76d19b11..b5a8f351dacb 100644 --- a/packages/twenty-front/src/modules/activities/emails/queries/fragments/timelineThreadWithTotalFragment.ts +++ b/packages/twenty-front/src/modules/activities/emails/graphql/queries/fragments/timelineThreadWithTotalFragment.ts @@ -1,7 +1,6 @@ +import { timelineThreadFragment } from '@/activities/emails/graphql/queries/fragments/timelineThreadFragment'; import { gql } from '@apollo/client'; -import { timelineThreadFragment } from '@/activities/emails/queries/fragments/timelineThreadFragment'; - export const timelineThreadWithTotalFragment = gql` fragment TimelineThreadsWithTotalFragment on TimelineThreadsWithTotal { totalNumberOfThreads diff --git a/packages/twenty-front/src/modules/activities/emails/queries/getTimelineThreadsFromCompanyId.ts b/packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromCompanyId.ts similarity index 88% rename from packages/twenty-front/src/modules/activities/emails/queries/getTimelineThreadsFromCompanyId.ts rename to packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromCompanyId.ts index 589905550c7d..e999e676b94e 100644 --- a/packages/twenty-front/src/modules/activities/emails/queries/getTimelineThreadsFromCompanyId.ts +++ b/packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromCompanyId.ts @@ -1,7 +1,6 @@ +import { timelineThreadWithTotalFragment } from '@/activities/emails/graphql/queries/fragments/timelineThreadWithTotalFragment'; import { gql } from '@apollo/client'; -import { timelineThreadWithTotalFragment } from '@/activities/emails/queries/fragments/timelineThreadWithTotalFragment'; - export const getTimelineThreadsFromCompanyId = gql` query GetTimelineThreadsFromCompanyId( $companyId: UUID! diff --git a/packages/twenty-front/src/modules/activities/emails/queries/getTimelineThreadsFromPersonId.ts b/packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromPersonId.ts similarity index 87% rename from packages/twenty-front/src/modules/activities/emails/queries/getTimelineThreadsFromPersonId.ts rename to packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromPersonId.ts index 84cd7053791e..7f1877f112eb 100644 --- a/packages/twenty-front/src/modules/activities/emails/queries/getTimelineThreadsFromPersonId.ts +++ b/packages/twenty-front/src/modules/activities/emails/graphql/queries/getTimelineThreadsFromPersonId.ts @@ -1,6 +1,6 @@ import { gql } from '@apollo/client'; -import { timelineThreadWithTotalFragment } from '@/activities/emails/queries/fragments/timelineThreadWithTotalFragment'; +import { timelineThreadWithTotalFragment } from '@/activities/emails/graphql/queries/fragments/timelineThreadWithTotalFragment'; export const getTimelineThreadsFromPersonId = gql` query GetTimelineThreadsFromPersonId( diff --git a/packages/twenty-front/src/modules/activities/hooks/useRefreshShowPageFindManyActivitiesQueries.ts b/packages/twenty-front/src/modules/activities/hooks/useRefreshShowPageFindManyActivitiesQueries.ts index e054014aa2be..e3295d57e991 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useRefreshShowPageFindManyActivitiesQueries.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useRefreshShowPageFindManyActivitiesQueries.ts @@ -1,7 +1,7 @@ import { useRecoilValue } from 'recoil'; import { usePrepareFindManyActivitiesQuery } from '@/activities/hooks/usePrepareFindManyActivitiesQuery'; -import { objectShowPageTargetableObjectState } from '@/activities/timelineActivities/states/objectShowPageTargetableObjectIdState'; +import { objectShowPageTargetableObjectState } from '@/activities/timeline-activities/states/objectShowPageTargetableObjectIdState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts b/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts index 4c1c6e2499fc..76e91ec8bf53 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts @@ -4,7 +4,7 @@ import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB' import { useRefreshShowPageFindManyActivitiesQueries } from '@/activities/hooks/useRefreshShowPageFindManyActivitiesQueries'; import { isActivityInCreateModeState } from '@/activities/states/isActivityInCreateModeState'; import { isUpsertingActivityInDBState } from '@/activities/states/isCreatingActivityInDBState'; -import { objectShowPageTargetableObjectState } from '@/activities/timelineActivities/states/objectShowPageTargetableObjectIdState'; +import { objectShowPageTargetableObjectState } from '@/activities/timeline-activities/states/objectShowPageTargetableObjectIdState'; import { Note } from '@/activities/types/Note'; import { Task } from '@/activities/types/Task'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; diff --git a/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts b/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts index 04aa231d4c84..1a8248543747 100644 --- a/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts +++ b/packages/twenty-front/src/modules/activities/notes/hooks/useNotes.ts @@ -3,7 +3,7 @@ import { useRecoilState } from 'recoil'; import { useActivities } from '@/activities/hooks/useActivities'; import { currentNotesQueryVariablesState } from '@/activities/notes/states/currentNotesQueryVariablesState'; -import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timelineActivities/constants/FindManyTimelineActivitiesOrderBy'; +import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline-activities/constants/FindManyTimelineActivitiesOrderBy'; import { Note } from '@/activities/types/Note'; import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; diff --git a/packages/twenty-front/src/modules/activities/tasks/components/PageAddTaskButton.tsx b/packages/twenty-front/src/modules/activities/tasks/components/PageAddTaskButton.tsx index 265780072a7d..a7168d35ec85 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/PageAddTaskButton.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/PageAddTaskButton.tsx @@ -1,6 +1,6 @@ import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { PageAddButton } from '@/ui/layout/page/PageAddButton'; +import { PageAddButton } from '@/ui/layout/page/components/PageAddButton'; export const PageAddTaskButton = () => { const openCreateActivity = useOpenCreateActivityDrawer({ diff --git a/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskGroups.stories.tsx b/packages/twenty-front/src/modules/activities/tasks/components/__stories__/TaskGroups.stories.tsx similarity index 100% rename from packages/twenty-front/src/modules/activities/tasks/__stories__/TaskGroups.stories.tsx rename to packages/twenty-front/src/modules/activities/tasks/components/__stories__/TaskGroups.stories.tsx diff --git a/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskList.stories.tsx b/packages/twenty-front/src/modules/activities/tasks/components/__stories__/TaskList.stories.tsx similarity index 100% rename from packages/twenty-front/src/modules/activities/tasks/__stories__/TaskList.stories.tsx rename to packages/twenty-front/src/modules/activities/tasks/components/__stories__/TaskList.stories.tsx diff --git a/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts b/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts index ef210d328a25..2085284d130d 100644 --- a/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts +++ b/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts @@ -1,5 +1,5 @@ import { useActivities } from '@/activities/hooks/useActivities'; -import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timelineActivities/constants/FindManyTimelineActivitiesOrderBy'; +import { FIND_MANY_TIMELINE_ACTIVITIES_ORDER_BY } from '@/activities/timeline-activities/constants/FindManyTimelineActivitiesOrderBy'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { Task } from '@/activities/types/Task'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventList.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/components/EventList.tsx similarity index 86% rename from packages/twenty-front/src/modules/activities/timelineActivities/components/EventList.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/components/EventList.tsx index bf82cc2a42da..86b053536d88 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventList.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/components/EventList.tsx @@ -1,10 +1,10 @@ import styled from '@emotion/styled'; import { ReactElement } from 'react'; -import { EventsGroup } from '@/activities/timelineActivities/components/EventsGroup'; -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; -import { filterOutInvalidTimelineActivities } from '@/activities/timelineActivities/utils/filterOutInvalidTimelineActivities'; -import { groupEventsByMonth } from '@/activities/timelineActivities/utils/groupEventsByMonth'; +import { EventsGroup } from '@/activities/timeline-activities/components/EventsGroup'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; +import { filterOutInvalidTimelineActivities } from '@/activities/timeline-activities/utils/filterOutInvalidTimelineActivities'; +import { groupEventsByMonth } from '@/activities/timeline-activities/utils/groupEventsByMonth'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventRow.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/components/EventRow.tsx similarity index 89% rename from packages/twenty-front/src/modules/activities/timelineActivities/components/EventRow.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/components/EventRow.tsx index e046316132df..e2cd91889021 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventRow.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/components/EventRow.tsx @@ -2,13 +2,13 @@ import styled from '@emotion/styled'; import { useContext } from 'react'; import { useRecoilValue } from 'recoil'; -import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext'; +import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext'; -import { useLinkedObjectObjectMetadataItem } from '@/activities/timelineActivities/hooks/useLinkedObjectObjectMetadataItem'; -import { EventIconDynamicComponent } from '@/activities/timelineActivities/rows/components/EventIconDynamicComponent'; -import { EventRowDynamicComponent } from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent'; -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; -import { getTimelineActivityAuthorFullName } from '@/activities/timelineActivities/utils/getTimelineActivityAuthorFullName'; +import { useLinkedObjectObjectMetadataItem } from '@/activities/timeline-activities/hooks/useLinkedObjectObjectMetadataItem'; +import { EventIconDynamicComponent } from '@/activities/timeline-activities/rows/components/EventIconDynamicComponent'; +import { EventRowDynamicComponent } from '@/activities/timeline-activities/rows/components/EventRowDynamicComponent'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; +import { getTimelineActivityAuthorFullName } from '@/activities/timeline-activities/utils/getTimelineActivityAuthorFullName'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { beautifyPastDateRelativeToNow } from '~/utils/date-utils'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/components/EventsGroup.tsx similarity index 92% rename from packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/components/EventsGroup.tsx index fe368ae9bab9..590f5657c66c 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/components/EventsGroup.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/components/EventsGroup.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; -import { EventRow } from '@/activities/timelineActivities/components/EventRow'; -import { EventGroup } from '@/activities/timelineActivities/utils/groupEventsByMonth'; +import { EventRow } from '@/activities/timeline-activities/components/EventRow'; +import { EventGroup } from '@/activities/timeline-activities/utils/groupEventsByMonth'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; type EventsGroupProps = { diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineActivities.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineActivities.tsx similarity index 91% rename from packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineActivities.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineActivities.tsx index bbda464681f9..a938ae8aa1c7 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineActivities.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineActivities.tsx @@ -2,9 +2,9 @@ import styled from '@emotion/styled'; import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader'; import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; -import { EventList } from '@/activities/timelineActivities/components/EventList'; -import { TimelineCreateButtonGroup } from '@/activities/timelineActivities/components/TimelineCreateButtonGroup'; -import { useTimelineActivities } from '@/activities/timelineActivities/hooks/useTimelineActivities'; +import { EventList } from '@/activities/timeline-activities/components/EventList'; +import { TimelineCreateButtonGroup } from '@/activities/timeline-activities/components/TimelineCreateButtonGroup'; +import { useTimelineActivities } from '@/activities/timeline-activities/hooks/useTimelineActivities'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; import { @@ -46,7 +46,7 @@ export const TimelineActivities = ({ const isTimelineActivitiesEmpty = !timelineActivities || timelineActivities.length === 0; - if (loading) { + if (loading === true) { return ; } diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineCreateButtonGroup.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineCreateButtonGroup.tsx similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/components/TimelineCreateButtonGroup.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineCreateButtonGroup.tsx diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/__stories__/TimelineActivities.stories.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/components/__stories__/TimelineActivities.stories.tsx similarity index 89% rename from packages/twenty-front/src/modules/activities/timelineActivities/__stories__/TimelineActivities.stories.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/components/__stories__/TimelineActivities.stories.tsx index d04d8281c761..7c16632eea75 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/__stories__/TimelineActivities.stories.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/components/__stories__/TimelineActivities.stories.tsx @@ -1,9 +1,9 @@ import { Meta, StoryObj } from '@storybook/react'; -import { graphql, HttpResponse } from 'msw'; +import { HttpResponse, graphql } from 'msw'; import { ComponentDecorator } from 'twenty-ui'; -import { TimelineActivities } from '@/activities/timelineActivities/components/TimelineActivities'; -import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext'; +import { TimelineActivities } from '@/activities/timeline-activities/components/TimelineActivities'; +import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { mockedTimelineActivities } from '~/testing/mock-data/timeline-activities'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/constants/FindManyTimelineActivitiesOrderBy.ts b/packages/twenty-front/src/modules/activities/timeline-activities/constants/FindManyTimelineActivitiesOrderBy.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/constants/FindManyTimelineActivitiesOrderBy.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/constants/FindManyTimelineActivitiesOrderBy.ts diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/contexts/TimelineActivityContext.ts b/packages/twenty-front/src/modules/activities/timeline-activities/contexts/TimelineActivityContext.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/contexts/TimelineActivityContext.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/contexts/TimelineActivityContext.ts diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/__tests__/useTimelineActivities.test.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/hooks/__tests__/useTimelineActivities.test.tsx similarity index 95% rename from packages/twenty-front/src/modules/activities/timelineActivities/hooks/__tests__/useTimelineActivities.test.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/hooks/__tests__/useTimelineActivities.test.tsx index 2d1989cc68ea..158b628bee6a 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/__tests__/useTimelineActivities.test.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/hooks/__tests__/useTimelineActivities.test.tsx @@ -1,6 +1,6 @@ import { renderHook } from '@testing-library/react'; -import { useTimelineActivities } from '@/activities/timelineActivities/hooks/useTimelineActivities'; +import { useTimelineActivities } from '@/activities/timeline-activities/hooks/useTimelineActivities'; import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; jest.mock('@/object-record/hooks/useFindManyRecords', () => ({ diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useLinkedObjectObjectMetadataItem.ts b/packages/twenty-front/src/modules/activities/timeline-activities/hooks/useLinkedObjectObjectMetadataItem.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/hooks/useLinkedObjectObjectMetadataItem.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/hooks/useLinkedObjectObjectMetadataItem.ts diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useLinkedObjectsTitle.ts b/packages/twenty-front/src/modules/activities/timeline-activities/hooks/useLinkedObjectsTitle.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/hooks/useLinkedObjectsTitle.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/hooks/useLinkedObjectsTitle.ts diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useTimelineActivities.ts b/packages/twenty-front/src/modules/activities/timeline-activities/hooks/useTimelineActivities.ts similarity index 90% rename from packages/twenty-front/src/modules/activities/timelineActivities/hooks/useTimelineActivities.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/hooks/useTimelineActivities.ts index fb65053c151c..96c00233e6d8 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/useTimelineActivities.ts +++ b/packages/twenty-front/src/modules/activities/timeline-activities/hooks/useTimelineActivities.ts @@ -1,5 +1,5 @@ -import { useLinkedObjectsTitle } from '@/activities/timelineActivities/hooks/useLinkedObjectsTitle'; -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; +import { useLinkedObjectsTitle } from '@/activities/timeline-activities/hooks/useLinkedObjectsTitle'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getActivityTargetObjectFieldIdName'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/activity/components/EventRowActivity.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/activity/components/EventRowActivity.tsx similarity index 96% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/activity/components/EventRowActivity.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/activity/components/EventRowActivity.tsx index 1c1f34e43ade..24322ac7c598 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/activity/components/EventRowActivity.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/activity/components/EventRowActivity.tsx @@ -5,7 +5,7 @@ import { EventRowDynamicComponentProps, StyledEventRowItemAction, StyledEventRowItemColumn, -} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent'; +} from '@/activities/timeline-activities/rows/components/EventRowDynamicComponent'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { isNonEmptyString } from '@sniptt/guards'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/calendar/components/EventCardCalendarEvent.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/EventCardCalendarEvent.tsx similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/calendar/components/EventCardCalendarEvent.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/EventCardCalendarEvent.tsx diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/calendar/components/EventRowCalendarEvent.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/EventRowCalendarEvent.tsx similarity index 77% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/calendar/components/EventRowCalendarEvent.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/EventRowCalendarEvent.tsx index c1ffd1094ef3..a4172b8f4a90 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/calendar/components/EventRowCalendarEvent.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/EventRowCalendarEvent.tsx @@ -1,14 +1,14 @@ -import { useState } from 'react'; import styled from '@emotion/styled'; +import { useState } from 'react'; -import { EventCardCalendarEvent } from '@/activities/timelineActivities/rows/calendar/components/EventCardCalendarEvent'; -import { EventCard } from '@/activities/timelineActivities/rows/components/EventCard'; -import { EventCardToggleButton } from '@/activities/timelineActivities/rows/components/EventCardToggleButton'; +import { EventCardCalendarEvent } from '@/activities/timeline-activities/rows/calendar/components/EventCardCalendarEvent'; +import { EventCard } from '@/activities/timeline-activities/rows/components/EventCard'; +import { EventCardToggleButton } from '@/activities/timeline-activities/rows/components/EventCardToggleButton'; import { EventRowDynamicComponentProps, StyledEventRowItemAction, StyledEventRowItemColumn, -} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent'; +} from '@/activities/timeline-activities/rows/components/EventRowDynamicComponent'; type EventRowCalendarEventProps = EventRowDynamicComponentProps; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/calendar/components/__stories__/EventCardCalendarEvent.stories.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/__stories__/EventCardCalendarEvent.stories.tsx similarity index 90% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/calendar/components/__stories__/EventCardCalendarEvent.stories.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/__stories__/EventCardCalendarEvent.stories.tsx index b0c3fd7ca54c..8e8f4b3aaa0b 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/calendar/components/__stories__/EventCardCalendarEvent.stories.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/calendar/components/__stories__/EventCardCalendarEvent.stories.tsx @@ -1,8 +1,8 @@ import { Meta, StoryObj } from '@storybook/react'; -import { graphql, HttpResponse } from 'msw'; +import { HttpResponse, graphql } from 'msw'; import { ComponentDecorator } from 'twenty-ui'; -import { EventCardCalendarEvent } from '@/activities/timelineActivities/rows/calendar/components/EventCardCalendarEvent'; +import { EventCardCalendarEvent } from '@/activities/timeline-activities/rows/calendar/components/EventCardCalendarEvent'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventCard.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventCard.tsx similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventCard.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventCard.tsx diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventCardToggleButton.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventCardToggleButton.tsx similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventCardToggleButton.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventCardToggleButton.tsx diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventIconDynamicComponent.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventIconDynamicComponent.tsx similarity index 88% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventIconDynamicComponent.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventIconDynamicComponent.tsx index ecb7bc90d51f..6e4b49b8feea 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventIconDynamicComponent.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventIconDynamicComponent.tsx @@ -1,6 +1,6 @@ import { IconCirclePlus, IconEditCircle, IconTrash, useIcons } from 'twenty-ui'; -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; export const EventIconDynamicComponent = ({ diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventRowDynamicComponent.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventRowDynamicComponent.tsx similarity index 84% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventRowDynamicComponent.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventRowDynamicComponent.tsx index e5542cd35480..441d6598a841 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/components/EventRowDynamicComponent.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventRowDynamicComponent.tsx @@ -1,10 +1,10 @@ import styled from '@emotion/styled'; -import { EventRowActivity } from '@/activities/timelineActivities/rows/activity/components/EventRowActivity'; -import { EventRowCalendarEvent } from '@/activities/timelineActivities/rows/calendar/components/EventRowCalendarEvent'; -import { EventRowMainObject } from '@/activities/timelineActivities/rows/main-object/components/EventRowMainObject'; -import { EventRowMessage } from '@/activities/timelineActivities/rows/message/components/EventRowMessage'; -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; +import { EventRowActivity } from '@/activities/timeline-activities/rows/activity/components/EventRowActivity'; +import { EventRowCalendarEvent } from '@/activities/timeline-activities/rows/calendar/components/EventRowCalendarEvent'; +import { EventRowMainObject } from '@/activities/timeline-activities/rows/main-object/components/EventRowMainObject'; +import { EventRowMessage } from '@/activities/timeline-activities/rows/message/components/EventRowMessage'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiff.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiff.tsx similarity index 88% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiff.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiff.tsx index 94726465f201..f1a50f9128e7 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiff.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiff.tsx @@ -1,8 +1,8 @@ import styled from '@emotion/styled'; -import { EventFieldDiffLabel } from '@/activities/timelineActivities/rows/main-object/components/EventFieldDiffLabel'; -import { EventFieldDiffValue } from '@/activities/timelineActivities/rows/main-object/components/EventFieldDiffValue'; -import { EventFieldDiffValueEffect } from '@/activities/timelineActivities/rows/main-object/components/EventFieldDiffValueEffect'; +import { EventFieldDiffLabel } from '@/activities/timeline-activities/rows/main-object/components/EventFieldDiffLabel'; +import { EventFieldDiffValue } from '@/activities/timeline-activities/rows/main-object/components/EventFieldDiffValue'; +import { EventFieldDiffValueEffect } from '@/activities/timeline-activities/rows/main-object/components/EventFieldDiffValueEffect'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiffContainer.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffContainer.tsx similarity index 91% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiffContainer.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffContainer.tsx index 3b4cf60396f8..3a5b36ee6f44 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiffContainer.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffContainer.tsx @@ -1,4 +1,4 @@ -import { EventFieldDiff } from '@/activities/timelineActivities/rows/main-object/components/EventFieldDiff'; +import { EventFieldDiff } from '@/activities/timeline-activities/rows/main-object/components/EventFieldDiff'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiffLabel.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffLabel.tsx similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiffLabel.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffLabel.tsx diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiffValue.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValue.tsx similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiffValue.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValue.tsx diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiffValueEffect.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValueEffect.tsx similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventFieldDiffValueEffect.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventFieldDiffValueEffect.tsx diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObject.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx similarity index 91% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObject.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx index 053e9217bb66..448879073a1b 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObject.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObject.tsx @@ -1,11 +1,10 @@ -import styled from '@emotion/styled'; - import { EventRowDynamicComponentProps, StyledEventRowItemAction, StyledEventRowItemColumn, -} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent'; -import { EventRowMainObjectUpdated } from '@/activities/timelineActivities/rows/main-object/components/EventRowMainObjectUpdated'; +} from '@/activities/timeline-activities/rows/components/EventRowDynamicComponent'; +import { EventRowMainObjectUpdated } from '@/activities/timeline-activities/rows/main-object/components/EventRowMainObjectUpdated'; +import styled from '@emotion/styled'; type EventRowMainObjectProps = EventRowDynamicComponentProps; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObjectUpdated.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObjectUpdated.tsx similarity index 84% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObjectUpdated.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObjectUpdated.tsx index 30e6343bd708..cc2b7102e0d1 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/EventRowMainObjectUpdated.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/EventRowMainObjectUpdated.tsx @@ -1,14 +1,14 @@ -import { useState } from 'react'; import styled from '@emotion/styled'; +import { useState } from 'react'; -import { EventCard } from '@/activities/timelineActivities/rows/components/EventCard'; -import { EventCardToggleButton } from '@/activities/timelineActivities/rows/components/EventCardToggleButton'; +import { EventCard } from '@/activities/timeline-activities/rows/components/EventCard'; +import { EventCardToggleButton } from '@/activities/timeline-activities/rows/components/EventCardToggleButton'; import { StyledEventRowItemAction, StyledEventRowItemColumn, -} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent'; -import { EventFieldDiffContainer } from '@/activities/timelineActivities/rows/main-object/components/EventFieldDiffContainer'; -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; +} from '@/activities/timeline-activities/rows/components/EventRowDynamicComponent'; +import { EventFieldDiffContainer } from '@/activities/timeline-activities/rows/main-object/components/EventFieldDiffContainer'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/__stories__/EventRowMainObjectUpdated.stories.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/__stories__/EventRowMainObjectUpdated.stories.tsx similarity index 90% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/__stories__/EventRowMainObjectUpdated.stories.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/__stories__/EventRowMainObjectUpdated.stories.tsx index fe0b549d68da..d4a12a465643 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/__stories__/EventRowMainObjectUpdated.stories.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/main-object/components/__stories__/EventRowMainObjectUpdated.stories.tsx @@ -1,8 +1,8 @@ +import { EventRowMainObjectUpdated } from '@/activities/timeline-activities/rows/main-object/components/EventRowMainObjectUpdated'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; import { Meta, StoryObj } from '@storybook/react'; import { ComponentDecorator, RouterDecorator } from 'twenty-ui'; -import { EventRowMainObjectUpdated } from '@/activities/timelineActivities/rows/main-object/components/EventRowMainObjectUpdated'; -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/EventCardMessage.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventCardMessage.tsx similarity index 98% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/EventCardMessage.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventCardMessage.tsx index 899c0414e76b..7114bd427667 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/EventCardMessage.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventCardMessage.tsx @@ -4,7 +4,7 @@ import { OverflowingTextWithTooltip } from 'twenty-ui'; import { useEmailThread } from '@/activities/emails/hooks/useEmailThread'; import { EmailThreadMessage } from '@/activities/emails/types/EmailThreadMessage'; -import { EventCardMessageNotShared } from '@/activities/timelineActivities/rows/message/components/EventCardMessageNotShared'; +import { EventCardMessageNotShared } from '@/activities/timeline-activities/rows/message/components/EventCardMessageNotShared'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; import { useUpsertRecordsInStore } from '@/object-record/record-store/hooks/useUpsertRecordsInStore'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/EventCardMessageNotShared.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventCardMessageNotShared.tsx similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/EventCardMessageNotShared.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventCardMessageNotShared.tsx diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/EventRowMessage.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventRowMessage.tsx similarity index 79% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/EventRowMessage.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventRowMessage.tsx index 83513994517f..00bd68e93a2a 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/EventRowMessage.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/EventRowMessage.tsx @@ -1,14 +1,14 @@ -import { useState } from 'react'; import styled from '@emotion/styled'; +import { useState } from 'react'; -import { EventCard } from '@/activities/timelineActivities/rows/components/EventCard'; -import { EventCardToggleButton } from '@/activities/timelineActivities/rows/components/EventCardToggleButton'; +import { EventCard } from '@/activities/timeline-activities/rows/components/EventCard'; +import { EventCardToggleButton } from '@/activities/timeline-activities/rows/components/EventCardToggleButton'; import { EventRowDynamicComponentProps, StyledEventRowItemAction, StyledEventRowItemColumn, -} from '@/activities/timelineActivities/rows/components/EventRowDynamicComponent'; -import { EventCardMessage } from '@/activities/timelineActivities/rows/message/components/EventCardMessage'; +} from '@/activities/timeline-activities/rows/components/EventRowDynamicComponent'; +import { EventCardMessage } from '@/activities/timeline-activities/rows/message/components/EventCardMessage'; type EventRowMessageProps = EventRowDynamicComponentProps; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/__stories__/EventCardMessage.stories.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/__stories__/EventCardMessage.stories.tsx similarity index 87% rename from packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/__stories__/EventCardMessage.stories.tsx rename to packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/__stories__/EventCardMessage.stories.tsx index 3e8e08cd06dc..40d27298fd5d 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/message/components/__stories__/EventCardMessage.stories.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/message/components/__stories__/EventCardMessage.stories.tsx @@ -1,9 +1,9 @@ import { Meta, StoryObj } from '@storybook/react'; -import { graphql, HttpResponse } from 'msw'; +import { HttpResponse, graphql } from 'msw'; import { ComponentDecorator } from 'twenty-ui'; -import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext'; -import { EventCardMessage } from '@/activities/timelineActivities/rows/message/components/EventCardMessage'; +import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext'; +import { EventCardMessage } from '@/activities/timeline-activities/rows/message/components/EventCardMessage'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/states/objectShowPageTargetableObjectIdState.ts b/packages/twenty-front/src/modules/activities/timeline-activities/states/objectShowPageTargetableObjectIdState.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/states/objectShowPageTargetableObjectIdState.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/states/objectShowPageTargetableObjectIdState.ts diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/types/TimelineActivity.ts b/packages/twenty-front/src/modules/activities/timeline-activities/types/TimelineActivity.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/types/TimelineActivity.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/types/TimelineActivity.ts diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/types/TimelineActivityLinkedObject.ts b/packages/twenty-front/src/modules/activities/timeline-activities/types/TimelineActivityLinkedObject.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/types/TimelineActivityLinkedObject.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/types/TimelineActivityLinkedObject.ts diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts b/packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts similarity index 96% rename from packages/twenty-front/src/modules/activities/timelineActivities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts index 1dc11441002b..a685d15055c5 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts +++ b/packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/filterOutInvalidTimelineActivities.test.ts @@ -1,5 +1,5 @@ -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; -import { filterOutInvalidTimelineActivities } from '@/activities/timelineActivities/utils/filterOutInvalidTimelineActivities'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; +import { filterOutInvalidTimelineActivities } from '@/activities/timeline-activities/utils/filterOutInvalidTimelineActivities'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/utils/__tests__/getTimelineActivityAuthorFullName.test.ts b/packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/getTimelineActivityAuthorFullName.test.ts similarity index 91% rename from packages/twenty-front/src/modules/activities/timelineActivities/utils/__tests__/getTimelineActivityAuthorFullName.test.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/getTimelineActivityAuthorFullName.test.ts index 85ac636ea096..7b3d817c64e0 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/utils/__tests__/getTimelineActivityAuthorFullName.test.ts +++ b/packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/getTimelineActivityAuthorFullName.test.ts @@ -1,5 +1,5 @@ -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; -import { getTimelineActivityAuthorFullName } from '@/activities/timelineActivities/utils/getTimelineActivityAuthorFullName'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; +import { getTimelineActivityAuthorFullName } from '@/activities/timeline-activities/utils/getTimelineActivityAuthorFullName'; import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState'; describe('getTimelineActivityAuthorFullName', () => { diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/utils/__tests__/groupEventsByMonth.test.ts b/packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/groupEventsByMonth.test.ts similarity index 100% rename from packages/twenty-front/src/modules/activities/timelineActivities/utils/__tests__/groupEventsByMonth.test.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/utils/__tests__/groupEventsByMonth.test.ts diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/utils/filterOutInvalidTimelineActivities.ts b/packages/twenty-front/src/modules/activities/timeline-activities/utils/filterOutInvalidTimelineActivities.ts similarity index 95% rename from packages/twenty-front/src/modules/activities/timelineActivities/utils/filterOutInvalidTimelineActivities.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/utils/filterOutInvalidTimelineActivities.ts index 5613db9d48b0..96413c89cd4e 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/utils/filterOutInvalidTimelineActivities.ts +++ b/packages/twenty-front/src/modules/activities/timeline-activities/utils/filterOutInvalidTimelineActivities.ts @@ -1,4 +1,4 @@ -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/utils/filterTimelineActivityByLinkedObjectTypes.ts b/packages/twenty-front/src/modules/activities/timeline-activities/utils/filterTimelineActivityByLinkedObjectTypes.ts similarity index 75% rename from packages/twenty-front/src/modules/activities/timelineActivities/utils/filterTimelineActivityByLinkedObjectTypes.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/utils/filterTimelineActivityByLinkedObjectTypes.ts index 455ceca01c0a..781e85d8b67e 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/utils/filterTimelineActivityByLinkedObjectTypes.ts +++ b/packages/twenty-front/src/modules/activities/timeline-activities/utils/filterTimelineActivityByLinkedObjectTypes.ts @@ -1,5 +1,5 @@ -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; -import { TimelineActivityLinkedObject } from '@/activities/timelineActivities/types/TimelineActivityLinkedObject'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; +import { TimelineActivityLinkedObject } from '@/activities/timeline-activities/types/TimelineActivityLinkedObject'; export const filterTimelineActivityByLinkedObjectTypes = (linkedObjectTypes: TimelineActivityLinkedObject[]) => diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/utils/getTimelineActivityAuthorFullName.ts b/packages/twenty-front/src/modules/activities/timeline-activities/utils/getTimelineActivityAuthorFullName.ts similarity index 84% rename from packages/twenty-front/src/modules/activities/timelineActivities/utils/getTimelineActivityAuthorFullName.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/utils/getTimelineActivityAuthorFullName.ts index e97b27fa9450..4e141de6b3bd 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/utils/getTimelineActivityAuthorFullName.ts +++ b/packages/twenty-front/src/modules/activities/timeline-activities/utils/getTimelineActivityAuthorFullName.ts @@ -1,4 +1,4 @@ -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; import { CurrentWorkspaceMember } from '@/auth/states/currentWorkspaceMemberState'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/utils/groupEventsByMonth.ts b/packages/twenty-front/src/modules/activities/timeline-activities/utils/groupEventsByMonth.ts similarity index 89% rename from packages/twenty-front/src/modules/activities/timelineActivities/utils/groupEventsByMonth.ts rename to packages/twenty-front/src/modules/activities/timeline-activities/utils/groupEventsByMonth.ts index fa0779f538c3..cd5ce8a73364 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/utils/groupEventsByMonth.ts +++ b/packages/twenty-front/src/modules/activities/timeline-activities/utils/groupEventsByMonth.ts @@ -1,4 +1,4 @@ -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; import { isDefined } from '~/utils/isDefined'; export type EventGroup = { diff --git a/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts b/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts index 65f69f6d290e..6eca2c821fc6 100644 --- a/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts +++ b/packages/twenty-front/src/modules/apollo/services/apollo.factory.ts @@ -18,7 +18,7 @@ import { logDebug } from '~/utils/logDebug'; import { GraphQLFormattedError } from 'graphql'; import { ApolloManager } from '../types/apolloManager.interface'; -import { loggerLink } from '../utils'; +import { loggerLink } from '../utils/loggerLink'; const logger = loggerLink(() => 'Twenty'); diff --git a/packages/twenty-front/src/modules/apollo/utils/__tests__/format-title.test.ts b/packages/twenty-front/src/modules/apollo/utils/__tests__/formatTitle.test.ts similarity index 91% rename from packages/twenty-front/src/modules/apollo/utils/__tests__/format-title.test.ts rename to packages/twenty-front/src/modules/apollo/utils/__tests__/formatTitle.test.ts index 39773acb8dda..47d8dc2ae8e2 100644 --- a/packages/twenty-front/src/modules/apollo/utils/__tests__/format-title.test.ts +++ b/packages/twenty-front/src/modules/apollo/utils/__tests__/formatTitle.test.ts @@ -2,7 +2,7 @@ import { expect } from '@storybook/test'; import { OperationType } from '@/apollo/types/operation-type'; -import formatTitle from '../format-title'; +import formatTitle from '../formatTitle'; describe('formatTitle', () => { it('should correctly format the title', () => { diff --git a/packages/twenty-front/src/modules/apollo/utils/__tests__/utils.test.ts b/packages/twenty-front/src/modules/apollo/utils/__tests__/utils.test.ts deleted file mode 100644 index 0d87baac9971..000000000000 --- a/packages/twenty-front/src/modules/apollo/utils/__tests__/utils.test.ts +++ /dev/null @@ -1,4 +0,0 @@ -// More work needed here -describe.skip('loggerLink', () => { - it('should log the correct message', () => {}); -}); diff --git a/packages/twenty-front/src/modules/apollo/utils/format-title.ts b/packages/twenty-front/src/modules/apollo/utils/formatTitle.ts similarity index 100% rename from packages/twenty-front/src/modules/apollo/utils/format-title.ts rename to packages/twenty-front/src/modules/apollo/utils/formatTitle.ts diff --git a/packages/twenty-front/src/modules/apollo/utils/index.ts b/packages/twenty-front/src/modules/apollo/utils/loggerLink.ts similarity index 98% rename from packages/twenty-front/src/modules/apollo/utils/index.ts rename to packages/twenty-front/src/modules/apollo/utils/loggerLink.ts index b57f427cf192..174c5c3badda 100644 --- a/packages/twenty-front/src/modules/apollo/utils/index.ts +++ b/packages/twenty-front/src/modules/apollo/utils/loggerLink.ts @@ -4,7 +4,7 @@ import { isDefined } from '~/utils/isDefined'; import { logDebug } from '~/utils/logDebug'; import { logError } from '~/utils/logError'; -import formatTitle from './format-title'; +import formatTitle from './formatTitle'; const getGroup = (collapsed: boolean) => collapsed diff --git a/packages/twenty-front/src/modules/app/components/AppRouter.tsx b/packages/twenty-front/src/modules/app/components/AppRouter.tsx index d8985e676332..9e474de5ad44 100644 --- a/packages/twenty-front/src/modules/app/components/AppRouter.tsx +++ b/packages/twenty-front/src/modules/app/components/AppRouter.tsx @@ -1,4 +1,4 @@ -import { createAppRouter } from '@/app/utils/createAppRouter'; +import { useCreateAppRouter } from '@/app/hooks/useCreateAppRouter'; import { billingState } from '@/client-config/states/billingState'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { RouterProvider } from 'react-router-dom'; @@ -17,7 +17,7 @@ export const AppRouter = () => { return ( { const apolloMetadataClient = useContext(ApolloMetadataClientContext); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.tsx b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.ts similarity index 100% rename from packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.tsx rename to packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.ts diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.tsx b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.ts similarity index 100% rename from packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.tsx rename to packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.ts diff --git a/packages/twenty-front/src/modules/object-record/hooks/useRecordChipData.ts b/packages/twenty-front/src/modules/object-record/hooks/useRecordChipData.ts index 1958a09eb535..7ade7cb90699 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useRecordChipData.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useRecordChipData.ts @@ -1,4 +1,4 @@ -import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext'; +import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext'; import { generateDefaultRecordChipData } from '@/object-metadata/utils/generateDefaultRecordChipData'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { useContext } from 'react'; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx index 3961f28c836b..f464b8710f91 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx @@ -6,12 +6,12 @@ import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; import { getRelativeDateDisplayValue } from '@/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue'; import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; -import { computeVariableDateViewFilterValue } from '@/views/utils/view-filter-value/computeVariableDateViewFilterValue'; +import { computeVariableDateViewFilterValue } from '@/views/view-filter-value/utils/computeVariableDateViewFilterValue'; import { VariableDateViewFilterValueDirection, VariableDateViewFilterValueUnit, -} from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; -import { resolveFilterValue } from '@/views/utils/view-filter-value/resolveFilterValue'; +} from '@/views/view-filter-value/utils/resolveDateViewFilterValue'; +import { resolveFilterValue } from '@/views/view-filter-value/utils/resolveFilterValue'; import { useState } from 'react'; import { isDefined } from 'twenty-ui'; import { FieldMetadataType } from '~/generated-metadata/graphql'; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandLabel.test.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandLabel.test.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandLabel.test.tsx rename to packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandLabel.test.ts diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.tsx rename to packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/__tests__/getOperandsForFilterType.test.ts diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue.ts index fb59e540180b..9bbc69af3f52 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue.ts @@ -1,7 +1,7 @@ import { VariableDateViewFilterValueDirection, VariableDateViewFilterValueUnit, -} from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; +} from '@/views/view-filter-value/utils/resolveDateViewFilterValue'; import { plural } from 'pluralize'; import { capitalize } from '~/utils/string/capitalize'; export const getRelativeDateDisplayValue = ( diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.tsx b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.tsx rename to packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.ts diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx index e9487ea6eb23..01ca2843c3bb 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx @@ -11,7 +11,7 @@ import { useRecordBoardStates } from '@/object-record/record-board/hooks/interna import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; import { RecordBoardColumn } from '@/object-record/record-board/record-board-column/components/RecordBoardColumn'; import { RecordBoardScope } from '@/object-record/record-board/scopes/RecordBoardScope'; -import { getDraggedRecordPosition } from '@/object-record/record-board/utils/get-dragged-record-position.util'; +import { getDraggedRecordPosition } from '@/object-record/record-board/utils/getDraggedRecordPosition'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; diff --git a/packages/twenty-front/src/modules/object-record/record-board/utils/__tests__/get-dragged-record-position.util.test.ts b/packages/twenty-front/src/modules/object-record/record-board/utils/__tests__/getDraggedRecordPosition.test.ts similarity index 92% rename from packages/twenty-front/src/modules/object-record/record-board/utils/__tests__/get-dragged-record-position.util.test.ts rename to packages/twenty-front/src/modules/object-record/record-board/utils/__tests__/getDraggedRecordPosition.test.ts index 1359c2d597be..483c323f07c6 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/utils/__tests__/get-dragged-record-position.util.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/utils/__tests__/getDraggedRecordPosition.test.ts @@ -1,4 +1,4 @@ -import { getDraggedRecordPosition } from '../get-dragged-record-position.util'; +import { getDraggedRecordPosition } from '../getDraggedRecordPosition'; describe('getDraggedRecordPosition', () => { it('when both records defined and positive, should return the average of the two positions', () => { diff --git a/packages/twenty-front/src/modules/object-record/record-board/utils/get-dragged-record-position.util.ts b/packages/twenty-front/src/modules/object-record/record-board/utils/getDraggedRecordPosition.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/record-board/utils/get-dragged-record-position.util.ts rename to packages/twenty-front/src/modules/object-record/record-board/utils/getDraggedRecordPosition.ts diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/__stories__/FieldContextProvider.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/components/FieldContextProvider.tsx similarity index 100% rename from packages/twenty-front/src/modules/object-record/record-field/meta-types/__stories__/FieldContextProvider.tsx rename to packages/twenty-front/src/modules/object-record/record-field/meta-types/components/FieldContextProvider.tsx diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useChipFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useChipFieldDisplay.ts index 6451634ef5d7..6569e987c38f 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useChipFieldDisplay.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useChipFieldDisplay.ts @@ -1,7 +1,7 @@ import { isNonEmptyString } from '@sniptt/guards'; import { useContext } from 'react'; -import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext'; +import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext'; import { isFieldFullName } from '@/object-record/record-field/types/guards/isFieldFullName'; import { isFieldNumber } from '@/object-record/record-field/types/guards/isFieldNumber'; import { isFieldText } from '@/object-record/record-field/types/guards/isFieldText'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay.ts index 760a70b987ea..aec21e44b0d3 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationFromManyFieldDisplay.ts @@ -1,7 +1,7 @@ -import { useContext } from 'react'; import { isNonEmptyString } from '@sniptt/guards'; +import { useContext } from 'react'; -import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext'; +import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext'; import { generateDefaultRecordChipData } from '@/object-metadata/utils/generateDefaultRecordChipData'; import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationToOneFieldDisplay.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationToOneFieldDisplay.ts index 85afbd90743d..ff380aff3e81 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationToOneFieldDisplay.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useRelationToOneFieldDisplay.ts @@ -1,7 +1,7 @@ -import { useContext } from 'react'; import { isNonEmptyString } from '@sniptt/guards'; +import { useContext } from 'react'; -import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext'; +import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext'; import { generateDefaultRecordChipData } from '@/object-metadata/utils/generateDefaultRecordChipData'; import { useRecordFieldValue } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx index be1a3cefa206..2cda4b62cb05 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/AddressFieldInput.stories.tsx @@ -1,6 +1,6 @@ -import { useEffect } from 'react'; import { Decorator, Meta, StoryObj } from '@storybook/react'; import { expect, fn, userEvent, waitFor } from '@storybook/test'; +import { useEffect } from 'react'; import { useAddressField } from '@/object-record/record-field/meta-types/hooks/useAddressField'; import { FieldAddressDraftValue } from '@/object-record/record-field/types/FieldInputDraftValue'; @@ -11,7 +11,7 @@ import { import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider'; const AddressValueSetterEffect = ({ value, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx index 3b6f3300b530..764dca8fa480 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/BooleanFieldInput.stories.tsx @@ -1,12 +1,12 @@ -import { useEffect } from 'react'; import { Meta, StoryObj } from '@storybook/react'; import { expect, fn, userEvent, within } from '@storybook/test'; +import { useEffect } from 'react'; import { useSetRecoilState } from 'recoil'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { FieldMetadataType } from '~/generated/graphql'; -import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider'; import { BooleanFieldInput, BooleanFieldInputProps, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/DateTimeFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/DateTimeFieldInput.stories.tsx index 4c6c0d19a15f..c2155357971c 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/DateTimeFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/DateTimeFieldInput.stories.tsx @@ -1,11 +1,11 @@ -import { useEffect } from 'react'; import { Meta, StoryObj } from '@storybook/react'; import { expect, fn, userEvent, within } from '@storybook/test'; +import { useEffect } from 'react'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { FieldMetadataType } from '~/generated/graphql'; -import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider'; import { useDateTimeField } from '../../../hooks/useDateTimeField'; import { DateTimeFieldInput, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx index 79d32f974ae6..e0b8e85bd5ca 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/NumberFieldInput.stories.tsx @@ -1,12 +1,12 @@ -import { useEffect } from 'react'; import { Decorator, Meta, StoryObj } from '@storybook/react'; import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; +import { useEffect } from 'react'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { FieldMetadataType } from '~/generated/graphql'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; -import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider'; import { useNumberField } from '../../../hooks/useNumberField'; import { NumberFieldInput, NumberFieldInputProps } from '../NumberFieldInput'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RatingFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RatingFieldInput.stories.tsx index 5317c6ec36f5..dddbfbfc84d9 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RatingFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RatingFieldInput.stories.tsx @@ -1,13 +1,13 @@ -import { useEffect } from 'react'; import { Decorator, Meta, StoryObj } from '@storybook/react'; import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; +import { useEffect } from 'react'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { isDefined } from '~/utils/isDefined'; +import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider'; import { FieldRatingValue } from '../../../../types/FieldMetadata'; -import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; import { useRatingField } from '../../../hooks/useRatingField'; import { RatingFieldInput, RatingFieldInputProps } from '../RatingFieldInput'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationManyFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationManyFieldInput.stories.tsx index 97f8f7e206e7..3a4cd095e1fa 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationManyFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationManyFieldInput.stories.tsx @@ -1,5 +1,5 @@ -import { useEffect } from 'react'; import { Meta, StoryObj } from '@storybook/react'; +import { useEffect } from 'react'; import { useSetRecoilState } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; @@ -17,7 +17,7 @@ import { mockedWorkspaceMemberData, } from '~/testing/mock-data/users'; -import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider'; const RelationWorkspaceSetterEffect = () => { const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx index 09c405061fb7..6bb6f02d2b03 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationToOneFieldInput.stories.tsx @@ -25,7 +25,7 @@ import { mockedWorkspaceMemberData, } from '~/testing/mock-data/users'; -import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { FieldContextProvider } from '@/object-record/record-field/meta-types/components/FieldContextProvider'; import { RelationToOneFieldInput, RelationToOneFieldInputProps, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx index ef9bdced6a5c..ec17c5ff92a3 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/TextFieldInput.stories.tsx @@ -1,12 +1,12 @@ -import { useEffect } from 'react'; import { Decorator, Meta, StoryObj } from '@storybook/react'; import { expect, fn, userEvent, waitFor, within } from '@storybook/test'; +import { useEffect } from 'react'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { FieldMetadataType } from '~/generated/graphql'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; -import { FieldContextProvider } from '../../../__stories__/FieldContextProvider'; +import { FieldContextProvider } from '../../../components/FieldContextProvider'; import { useTextField } from '../../../hooks/useTextField'; import { TextFieldInput, TextFieldInputProps } from '../TextFieldInput'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/getFieldButtonIcon.tsx b/packages/twenty-front/src/modules/object-record/record-field/utils/getFieldButtonIcon.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/record-field/utils/getFieldButtonIcon.tsx rename to packages/twenty-front/src/modules/object-record/record-field/utils/getFieldButtonIcon.ts diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts index ff0199344948..345421f7ce95 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts @@ -25,7 +25,7 @@ import { } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput'; import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; import { applyEmptyFilters } from '@/object-record/record-filter/utils/applyEmptyFilters'; -import { resolveFilterValue } from '@/views/utils/view-filter-value/resolveFilterValue'; +import { resolveFilterValue } from '@/views/view-filter-value/utils/resolveFilterValue'; import { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns'; import { z } from 'zod'; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx index f167ad13f19d..60ab0fed8afc 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageHeader.tsx @@ -6,9 +6,9 @@ import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetada import { RecordIndexPageKanbanAddButton } from '@/object-record/record-index/components/RecordIndexPageKanbanAddButton'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState'; -import { PageAddButton } from '@/ui/layout/page/PageAddButton'; -import { PageHeader } from '@/ui/layout/page/PageHeader'; -import { PageHotkeysEffect } from '@/ui/layout/page/PageHotkeysEffect'; +import { PageAddButton } from '@/ui/layout/page/components/PageAddButton'; +import { PageHeader } from '@/ui/layout/page/components/PageHeader'; +import { PageHotkeysEffect } from '@/ui/layout/page/components/PageHotkeysEffect'; import { ViewType } from '@/views/types/ViewType'; import { useContext } from 'react'; import { capitalize } from '~/utils/string/capitalize'; diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx index 8e911edac10c..01e5f42d6592 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainer.tsx @@ -1,6 +1,6 @@ import { InformationBannerDeletedRecord } from '@/information-banner/components/deleted-record/InformationBannerDeletedRecord'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { ShowPageContainer } from '@/ui/layout/page/ShowPageContainer'; +import { ShowPageContainer } from '@/ui/layout/page/components/ShowPageContainer'; import { useRecordShowContainerData } from '@/object-record/record-show/hooks/useRecordShowContainerData'; import { useRecordShowContainerTabs } from '@/object-record/record-show/hooks/useRecordShowContainerTabs'; diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2.ts b/packages/twenty-front/src/modules/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2.ts index 9cd066b84e5e..a30216912c08 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2.ts @@ -1,5 +1,5 @@ import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext'; -import { createComponentStateV2_alpha } from '@/ui/utilities/state/component-state/utils/createComponentStateV2_alpha'; +import { createComponentStateV2_alpha } from '@/ui/utilities/state/component-state/utils/createComponentStateV2Alpha'; export const hasRecordTableFetchedAllRecordsComponentStateV2 = createComponentStateV2_alpha({ diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledLeftComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledLeftComponentState.ts index b51a7f63f48d..a11aa2eb789e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledLeftComponentState.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledLeftComponentState.ts @@ -1,5 +1,5 @@ import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext'; -import { createComponentStateV2_alpha } from '@/ui/utilities/state/component-state/utils/createComponentStateV2_alpha'; +import { createComponentStateV2_alpha } from '@/ui/utilities/state/component-state/utils/createComponentStateV2Alpha'; export const isRecordTableScrolledLeftComponentState = createComponentStateV2_alpha({ diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/__tests__/useOpenObjectRecordsSpreasheetImportDialog.test.tsx b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useOpenObjectRecordsSpreasheetImportDialog.test.ts similarity index 98% rename from packages/twenty-front/src/modules/object-record/spreadsheet-import/__tests__/useOpenObjectRecordsSpreasheetImportDialog.test.tsx rename to packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useOpenObjectRecordsSpreasheetImportDialog.test.ts index a7e47f8ba713..0eacdbb3d5ae 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/__tests__/useOpenObjectRecordsSpreasheetImportDialog.test.tsx +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/__tests__/useOpenObjectRecordsSpreasheetImportDialog.test.ts @@ -6,8 +6,9 @@ import { useRecoilValue } from 'recoil'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState'; +import { useOpenObjectRecordsSpreasheetImportDialog } from '@/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreasheetImportDialog'; + import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; -import { useOpenObjectRecordsSpreasheetImportDialog } from '../hooks/useOpenObjectRecordsSpreasheetImportDialog'; const companyId = 'cb2e9f4b-20c3-4759-9315-4ffeecfaf71a'; diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts index 260a223f1791..21cbdb3f9ee0 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport.ts @@ -4,7 +4,7 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels'; import { AvailableFieldForImport } from '@/object-record/spreadsheet-import/types/AvailableFieldForImport'; -import { getSpreadSheetFieldValidationDefinitions } from '@/object-record/spreadsheet-import/util/getSpreadSheetFieldValidationDefinitions'; +import { getSpreadSheetFieldValidationDefinitions } from '@/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions'; import { FieldMetadataType } from '~/generated-metadata/graphql'; export const useBuildAvailableFieldsForImport = () => { diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreasheetImportDialog.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreasheetImportDialog.ts index e5be43858442..d90dfff54842 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreasheetImportDialog.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/hooks/useOpenObjectRecordsSpreasheetImportDialog.ts @@ -1,7 +1,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords'; import { useBuildAvailableFieldsForImport } from '@/object-record/spreadsheet-import/hooks/useBuildAvailableFieldsForImport'; -import { buildRecordFromImportedStructuredRow } from '@/object-record/spreadsheet-import/util/buildRecordFromImportedStructuredRow'; +import { buildRecordFromImportedStructuredRow } from '@/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow'; import { useOpenSpreadsheetImportDialog } from '@/spreadsheet-import/hooks/useOpenSpreadsheetImportDialog'; import { SpreadsheetImportDialogOptions } from '@/spreadsheet-import/types'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/util/buildRecordFromImportedStructuredRow.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/spreadsheet-import/util/buildRecordFromImportedStructuredRow.ts rename to packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/buildRecordFromImportedStructuredRow.ts diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/util/getSpreadSheetFieldValidationDefinitions.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/spreadsheet-import/util/getSpreadSheetFieldValidationDefinitions.ts rename to packages/twenty-front/src/modules/object-record/spreadsheet-import/utils/getSpreadSheetFieldValidationDefinitions.ts diff --git a/packages/twenty-front/src/modules/object-record/utils/__tests__/computeRecordBoardColumnDefinitionsFromObjectMetadata.test.ts b/packages/twenty-front/src/modules/object-record/utils/__tests__/computeRecordBoardColumnDefinitionsFromObjectMetadata.test.ts new file mode 100644 index 000000000000..98137b1df480 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/__tests__/computeRecordBoardColumnDefinitionsFromObjectMetadata.test.ts @@ -0,0 +1,27 @@ +import { expect } from '@storybook/test'; + +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; +import { computeRecordBoardColumnDefinitionsFromObjectMetadata } from '../computeRecordBoardColumnDefinitionsFromObjectMetadata'; + +describe('computeRecordBoardColumnDefinitionsFromObjectMetadata', () => { + it('should correctly compute', () => { + const objectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'opportunity', + ); + + const stageField = objectMetadataItem?.fields.find( + (field) => field.name === 'stage', + ); + + if (!objectMetadataItem) { + throw new Error('Object metadata item not found'); + } + + const res = computeRecordBoardColumnDefinitionsFromObjectMetadata( + objectMetadataItem, + stageField?.id, + () => null, + ); + expect(res.length).toEqual(stageField?.options?.length); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/utils/getRecordChipGenerators.ts b/packages/twenty-front/src/modules/object-record/utils/getRecordChipGenerators.ts index fa56378afdd1..1ee8e850a2a3 100644 --- a/packages/twenty-front/src/modules/object-record/utils/getRecordChipGenerators.ts +++ b/packages/twenty-front/src/modules/object-record/utils/getRecordChipGenerators.ts @@ -1,7 +1,7 @@ import { ChipGeneratorPerObjectNameSingularPerFieldName, IdentifierChipGeneratorPerObject, -} from '@/object-metadata/context/PreComputedChipGeneratorsContext'; +} from '@/object-metadata/contexts/PreComputedChipGeneratorsContext'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getAvatarType } from '@/object-metadata/utils/getAvatarType'; import { getAvatarUrl } from '@/object-metadata/utils/getAvatarUrl'; diff --git a/packages/twenty-front/src/modules/opportunities/Opportunity.ts b/packages/twenty-front/src/modules/opportunities/types/Opportunity.ts similarity index 100% rename from packages/twenty-front/src/modules/opportunities/Opportunity.ts rename to packages/twenty-front/src/modules/opportunities/types/Opportunity.ts diff --git a/packages/twenty-front/src/modules/prefetch/constants/PrefetchConfig.ts b/packages/twenty-front/src/modules/prefetch/constants/PrefetchConfig.ts index f76ba6637925..dd38928fcf64 100644 --- a/packages/twenty-front/src/modules/prefetch/constants/PrefetchConfig.ts +++ b/packages/twenty-front/src/modules/prefetch/constants/PrefetchConfig.ts @@ -1,7 +1,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { RecordGqlOperationSignatureFactory } from '@/object-record/graphql/types/RecordGqlOperationSignatureFactory'; -import { findAllFavoritesOperationSignatureFactory } from '@/prefetch/operation-signatures/factories/findAllFavoritesOperationSignatureFactory'; -import { findAllViewsOperationSignatureFactory } from '@/prefetch/operation-signatures/factories/findAllViewsOperationSignatureFactory'; +import { findAllFavoritesOperationSignatureFactory } from '@/prefetch/graphql/operation-signatures/factories/findAllFavoritesOperationSignatureFactory'; +import { findAllViewsOperationSignatureFactory } from '@/prefetch/graphql/operation-signatures/factories/findAllViewsOperationSignatureFactory'; import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; export const PREFETCH_CONFIG: Record< diff --git a/packages/twenty-front/src/modules/prefetch/operation-signatures/factories/findAllFavoritesOperationSignatureFactory.ts b/packages/twenty-front/src/modules/prefetch/graphql/operation-signatures/factories/findAllFavoritesOperationSignatureFactory.ts similarity index 100% rename from packages/twenty-front/src/modules/prefetch/operation-signatures/factories/findAllFavoritesOperationSignatureFactory.ts rename to packages/twenty-front/src/modules/prefetch/graphql/operation-signatures/factories/findAllFavoritesOperationSignatureFactory.ts diff --git a/packages/twenty-front/src/modules/prefetch/operation-signatures/factories/findAllViewsOperationSignatureFactory.ts b/packages/twenty-front/src/modules/prefetch/graphql/operation-signatures/factories/findAllViewsOperationSignatureFactory.ts similarity index 100% rename from packages/twenty-front/src/modules/prefetch/operation-signatures/factories/findAllViewsOperationSignatureFactory.ts rename to packages/twenty-front/src/modules/prefetch/graphql/operation-signatures/factories/findAllViewsOperationSignatureFactory.ts diff --git a/packages/twenty-front/src/modules/settings/components/SettingsSkeletonLoader.tsx b/packages/twenty-front/src/modules/settings/components/SettingsSkeletonLoader.tsx index 812a5ca67b82..1e9403a3aa69 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsSkeletonLoader.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsSkeletonLoader.tsx @@ -1,6 +1,6 @@ import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; -import { PageBody } from '@/ui/layout/page/PageBody'; -import { PageHeader } from '@/ui/layout/page/PageHeader'; +import { PageBody } from '@/ui/layout/page/components/PageBody'; +import { PageHeader } from '@/ui/layout/page/components/PageHeader'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx index 360c9a300692..c052e01ffb71 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx @@ -4,7 +4,7 @@ import { SettingsDataModelFieldPreview, SettingsDataModelFieldPreviewProps, } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreview'; -import { SettingsDataModelObjectSummary } from '@/settings/data-model/objects/SettingsDataModelObjectSummary'; +import { SettingsDataModelObjectSummary } from '@/settings/data-model/objects/components/SettingsDataModelObjectSummary'; import { Card } from '@/ui/layout/card/components/Card'; import { CardContent } from '@/ui/layout/card/components/CardContent'; diff --git a/packages/twenty-front/src/modules/settings/data-model/graph-overview/components/SettingsDataModelOverview.tsx b/packages/twenty-front/src/modules/settings/data-model/graph-overview/components/SettingsDataModelOverview.tsx index e5bd42d0b4e3..42f4fabd50ed 100644 --- a/packages/twenty-front/src/modules/settings/data-model/graph-overview/components/SettingsDataModelOverview.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/graph-overview/components/SettingsDataModelOverview.tsx @@ -1,17 +1,17 @@ +import styled from '@emotion/styled'; import { useCallback, useState } from 'react'; import ReactFlow, { - applyEdgeChanges, - applyNodeChanges, Background, EdgeChange, + NodeChange, + applyEdgeChanges, + applyNodeChanges, getIncomers, getOutgoers, - NodeChange, useEdgesState, useNodesState, useReactFlow, } from 'reactflow'; -import styled from '@emotion/styled'; import { IconLock, IconLockOpen, @@ -24,7 +24,7 @@ import { import { SettingsDataModelOverviewEffect } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverviewEffect'; import { SettingsDataModelOverviewObject } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverviewObject'; import { SettingsDataModelOverviewRelationMarkers } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverviewRelationMarkers'; -import { calculateHandlePosition } from '@/settings/data-model/graph-overview/util/calculateHandlePosition'; +import { calculateHandlePosition } from '@/settings/data-model/graph-overview/utils/calculateHandlePosition'; import { Button } from '@/ui/input/button/components/Button'; import { IconButtonGroup } from '@/ui/input/button/components/IconButtonGroup'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/modules/settings/data-model/graph-overview/components/SettingsDataModelOverviewObject.tsx b/packages/twenty-front/src/modules/settings/data-model/graph-overview/components/SettingsDataModelOverviewObject.tsx index 85c473b2fe36..593503fd2b97 100644 --- a/packages/twenty-front/src/modules/settings/data-model/graph-overview/components/SettingsDataModelOverviewObject.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/graph-overview/components/SettingsDataModelOverviewObject.tsx @@ -8,7 +8,7 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { ObjectFieldRow } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverviewField'; -import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/SettingsDataModelObjectTypeTag'; +import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/components/SettingsDataModelObjectTypeTag'; import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel'; import { FieldMetadataType } from '~/generated/graphql'; import { capitalize } from '~/utils/string/capitalize'; diff --git a/packages/twenty-front/src/modules/settings/data-model/graph-overview/util/__tests__/calculateHandlePosition.test.ts b/packages/twenty-front/src/modules/settings/data-model/graph-overview/utils/__tests__/calculateHandlePosition.test.ts similarity index 100% rename from packages/twenty-front/src/modules/settings/data-model/graph-overview/util/__tests__/calculateHandlePosition.test.ts rename to packages/twenty-front/src/modules/settings/data-model/graph-overview/utils/__tests__/calculateHandlePosition.test.ts diff --git a/packages/twenty-front/src/modules/settings/data-model/graph-overview/util/calculateHandlePosition.ts b/packages/twenty-front/src/modules/settings/data-model/graph-overview/utils/calculateHandlePosition.ts similarity index 100% rename from packages/twenty-front/src/modules/settings/data-model/graph-overview/util/calculateHandlePosition.ts rename to packages/twenty-front/src/modules/settings/data-model/graph-overview/utils/calculateHandlePosition.ts diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectItemTableRow.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectItemTableRow.tsx index 27e1a97823cd..1de7ea88adad 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectItemTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectItemTableRow.tsx @@ -4,7 +4,7 @@ import { ReactNode } from 'react'; import { useIcons } from 'twenty-ui'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/SettingsDataModelObjectTypeTag'; +import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/components/SettingsDataModelObjectTypeTag'; import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel'; import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableRow } from '@/ui/layout/table/components/TableRow'; diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectSummaryCard.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectSummaryCard.tsx index 6161fbcdf3a3..22ae24fdb0c0 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectSummaryCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectSummaryCard.tsx @@ -6,7 +6,7 @@ import { useLastVisitedObjectMetadataItem } from '@/navigation/hooks/useLastVisi import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { SettingsSummaryCard } from '@/settings/components/SettingsSummaryCard'; -import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/SettingsDataModelObjectTypeTag'; +import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/components/SettingsDataModelObjectTypeTag'; import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel'; import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectSummary.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsDataModelObjectSummary.tsx similarity index 96% rename from packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectSummary.tsx rename to packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsDataModelObjectSummary.tsx index 931fb6990e09..270ef7a28d26 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectSummary.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsDataModelObjectSummary.tsx @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; import { OverflowingTextWithTooltip, useIcons } from 'twenty-ui'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/SettingsDataModelObjectTypeTag'; +import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/components/SettingsDataModelObjectTypeTag'; import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel'; export type SettingsDataModelObjectSummaryProps = { diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectTypeTag.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsDataModelObjectTypeTag.tsx similarity index 100% rename from packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectTypeTag.tsx rename to packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsDataModelObjectTypeTag.tsx diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/SettingsObjectCoverImage.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsObjectCoverImage.tsx similarity index 91% rename from packages/twenty-front/src/modules/settings/data-model/objects/SettingsObjectCoverImage.tsx rename to packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsObjectCoverImage.tsx index 87ce82a443ea..f2db34f27e38 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/SettingsObjectCoverImage.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsObjectCoverImage.tsx @@ -5,8 +5,8 @@ import { FloatingButton } from '@/ui/input/button/components/FloatingButton'; import { Card } from '@/ui/layout/card/components/Card'; import { SettingsPath } from '@/types/SettingsPath'; -import DarkCoverImage from '../assets/cover-dark.png'; -import LightCoverImage from '../assets/cover-light.png'; +import DarkCoverImage from '../../assets/cover-dark.png'; +import LightCoverImage from '../../assets/cover-light.png'; const StyledCoverImageContainer = styled(Card)` align-items: center; diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/SettingsObjectInactiveMenuDropDown.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsObjectInactiveMenuDropDown.tsx similarity index 100% rename from packages/twenty-front/src/modules/settings/data-model/objects/SettingsObjectInactiveMenuDropDown.tsx rename to packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsObjectInactiveMenuDropDown.tsx diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/__stories__/SettingsObjectInactiveMenuDropDown.stories.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/components/__stories__/SettingsObjectInactiveMenuDropDown.stories.tsx similarity index 100% rename from packages/twenty-front/src/modules/settings/data-model/objects/__stories__/SettingsObjectInactiveMenuDropDown.stories.tsx rename to packages/twenty-front/src/modules/settings/data-model/objects/components/__stories__/SettingsObjectInactiveMenuDropDown.stories.tsx diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard.tsx index eec82eb8e394..4b2782b69d39 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectSettingsFormCard.tsx @@ -6,11 +6,11 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem'; import { SettingsDataModelCardTitle } from '@/settings/data-model/components/SettingsDataModelCardTitle'; import { SettingsDataModelFieldPreviewCard } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard'; +import { SettingsDataModelObjectSummary } from '@/settings/data-model/objects/components/SettingsDataModelObjectSummary'; import { SettingsDataModelObjectIdentifiersForm, SettingsDataModelObjectIdentifiersFormValues, } from '@/settings/data-model/objects/forms/components/SettingsDataModelObjectIdentifiersForm'; -import { SettingsDataModelObjectSummary } from '@/settings/data-model/objects/SettingsDataModelObjectSummary'; import { Card } from '@/ui/layout/card/components/Card'; import { CardContent } from '@/ui/layout/card/components/CardContent'; diff --git a/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysTable.tsx b/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysTable.tsx index 0d1a9fc12660..3b47bad471f0 100644 --- a/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysTable.tsx +++ b/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysTable.tsx @@ -3,7 +3,7 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { SettingsApiKeysFieldItemTableRow } from '@/settings/developers/components/SettingsApiKeysFieldItemTableRow'; import { ApiFieldItem } from '@/settings/developers/types/api-key/ApiFieldItem'; import { ApiKey } from '@/settings/developers/types/api-key/ApiKey'; -import { formatExpirations } from '@/settings/developers/utils/format-expiration'; +import { formatExpirations } from '@/settings/developers/utils/formatExpiration'; import { Table } from '@/ui/layout/table/components/Table'; import { TableBody } from '@/ui/layout/table/components/TableBody'; import { TableHeader } from '@/ui/layout/table/components/TableHeader'; diff --git a/packages/twenty-front/src/modules/settings/developers/utils/__tests__/compute-new-expiration-date.test.ts b/packages/twenty-front/src/modules/settings/developers/utils/__tests__/computeNewExpirationDate.test.ts similarity index 96% rename from packages/twenty-front/src/modules/settings/developers/utils/__tests__/compute-new-expiration-date.test.ts rename to packages/twenty-front/src/modules/settings/developers/utils/__tests__/computeNewExpirationDate.test.ts index bd2a1db5bf7f..fa8d566e0cc2 100644 --- a/packages/twenty-front/src/modules/settings/developers/utils/__tests__/compute-new-expiration-date.test.ts +++ b/packages/twenty-front/src/modules/settings/developers/utils/__tests__/computeNewExpirationDate.test.ts @@ -1,4 +1,4 @@ -import { computeNewExpirationDate } from '@/settings/developers/utils/compute-new-expiration-date'; +import { computeNewExpirationDate } from '@/settings/developers/utils/computeNewExpirationDate'; jest.useFakeTimers().setSystemTime(new Date('2024-01-01T00:00:00.000Z')); diff --git a/packages/twenty-front/src/modules/settings/developers/utils/__tests__/format-expiration.test.ts b/packages/twenty-front/src/modules/settings/developers/utils/__tests__/formatExpiration.test.ts similarity index 99% rename from packages/twenty-front/src/modules/settings/developers/utils/__tests__/format-expiration.test.ts rename to packages/twenty-front/src/modules/settings/developers/utils/__tests__/formatExpiration.test.ts index c993984b0579..68731a78ce6d 100644 --- a/packages/twenty-front/src/modules/settings/developers/utils/__tests__/format-expiration.test.ts +++ b/packages/twenty-front/src/modules/settings/developers/utils/__tests__/formatExpiration.test.ts @@ -1,4 +1,4 @@ -import { formatExpiration } from '@/settings/developers/utils/format-expiration'; +import { formatExpiration } from '@/settings/developers/utils/formatExpiration'; jest.useFakeTimers().setSystemTime(new Date('2024-01-01T00:00:00.000Z')); diff --git a/packages/twenty-front/src/modules/settings/developers/utils/compute-new-expiration-date.ts b/packages/twenty-front/src/modules/settings/developers/utils/computeNewExpirationDate.ts similarity index 100% rename from packages/twenty-front/src/modules/settings/developers/utils/compute-new-expiration-date.ts rename to packages/twenty-front/src/modules/settings/developers/utils/computeNewExpirationDate.ts diff --git a/packages/twenty-front/src/modules/settings/developers/utils/format-expiration.ts b/packages/twenty-front/src/modules/settings/developers/utils/formatExpiration.ts similarity index 100% rename from packages/twenty-front/src/modules/settings/developers/utils/format-expiration.ts rename to packages/twenty-front/src/modules/settings/developers/utils/formatExpiration.ts diff --git a/packages/twenty-front/src/modules/settings/hooks/useExpandedHeightAnimation.tsx b/packages/twenty-front/src/modules/settings/hooks/useExpandedHeightAnimation.ts similarity index 100% rename from packages/twenty-front/src/modules/settings/hooks/useExpandedHeightAnimation.tsx rename to packages/twenty-front/src/modules/settings/hooks/useExpandedHeightAnimation.ts diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockPage.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockPage.tsx index 2b17a655549d..9b36b1541abd 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockPage.tsx +++ b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockPage.tsx @@ -3,11 +3,11 @@ import { IconBuildingSkyscraper } from 'twenty-ui'; import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; import { SignInBackgroundMockContainer } from '@/sign-in-background-mock/components/SignInBackgroundMockContainer'; -import { PageAddButton } from '@/ui/layout/page/PageAddButton'; -import { PageBody } from '@/ui/layout/page/PageBody'; -import { PageContainer } from '@/ui/layout/page/PageContainer'; -import { PageHeader } from '@/ui/layout/page/PageHeader'; -import { PageHotkeysEffect } from '@/ui/layout/page/PageHotkeysEffect'; +import { PageAddButton } from '@/ui/layout/page/components/PageAddButton'; +import { PageBody } from '@/ui/layout/page/components/PageBody'; +import { PageContainer } from '@/ui/layout/page/components/PageContainer'; +import { PageHeader } from '@/ui/layout/page/components/PageHeader'; +import { PageHotkeysEffect } from '@/ui/layout/page/components/PageHotkeysEffect'; const StyledTableContainer = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/tests/mockRsiValues.ts b/packages/twenty-front/src/modules/spreadsheet-import/__mocks__/mockRsiValues.ts similarity index 100% rename from packages/twenty-front/src/modules/spreadsheet-import/tests/mockRsiValues.ts rename to packages/twenty-front/src/modules/spreadsheet-import/__mocks__/mockRsiValues.ts diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/MatchColumns.stories.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/MatchColumns.stories.tsx index 47cb4e54d0b8..ac1a4da5acb8 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/MatchColumns.stories.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/MatchColumns.stories.tsx @@ -1,10 +1,10 @@ import { Meta } from '@storybook/react'; +import { mockRsiValues } from '@/spreadsheet-import/__mocks__/mockRsiValues'; import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper'; import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider'; import { MatchColumnsStep } from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep'; -import { mockRsiValues } from '@/spreadsheet-import/tests/mockRsiValues'; import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/SelectHeader.stories.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/SelectHeader.stories.tsx index a87f0ce4226c..ad33171faa3f 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/SelectHeader.stories.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/SelectHeader.stories.tsx @@ -1,13 +1,13 @@ import { Meta } from '@storybook/react'; +import { + headerSelectionTableFields, + mockRsiValues, +} from '@/spreadsheet-import/__mocks__/mockRsiValues'; import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper'; import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider'; import { SelectHeaderStep } from '@/spreadsheet-import/steps/components/SelectHeaderStep/SelectHeaderStep'; import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType'; -import { - headerSelectionTableFields, - mockRsiValues, -} from '@/spreadsheet-import/tests/mockRsiValues'; import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope'; const meta: Meta = { diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/SelectSheet.stories.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/SelectSheet.stories.tsx index e48542f1a089..57b5162793cb 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/SelectSheet.stories.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/SelectSheet.stories.tsx @@ -1,10 +1,10 @@ import { Meta } from '@storybook/react'; +import { mockRsiValues } from '@/spreadsheet-import/__mocks__/mockRsiValues'; import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper'; import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider'; import { SelectSheetStep } from '@/spreadsheet-import/steps/components/SelectSheetStep/SelectSheetStep'; import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType'; -import { mockRsiValues } from '@/spreadsheet-import/tests/mockRsiValues'; import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope'; const meta: Meta = { diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Upload.stories.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Upload.stories.tsx index 7f2b295fb545..0757b7e619d4 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Upload.stories.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Upload.stories.tsx @@ -1,10 +1,10 @@ import { Meta } from '@storybook/react'; +import { mockRsiValues } from '@/spreadsheet-import/__mocks__/mockRsiValues'; import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper'; import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider'; import { UploadStep } from '@/spreadsheet-import/steps/components/UploadStep/UploadStep'; import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType'; -import { mockRsiValues } from '@/spreadsheet-import/tests/mockRsiValues'; import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Validation.stories.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Validation.stories.tsx index 9126371d1dc2..58894c817ce0 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Validation.stories.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/__stories__/Validation.stories.tsx @@ -1,13 +1,13 @@ import { Meta } from '@storybook/react'; -import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper'; -import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider'; -import { ValidationStep } from '@/spreadsheet-import/steps/components/ValidationStep/ValidationStep'; import { editableTableInitialData, importedColums, mockRsiValues, -} from '@/spreadsheet-import/tests/mockRsiValues'; +} from '@/spreadsheet-import/__mocks__/mockRsiValues'; +import { ModalWrapper } from '@/spreadsheet-import/components/ModalWrapper'; +import { ReactSpreadsheetImportContextProvider } from '@/spreadsheet-import/components/ReactSpreadsheetImportContextProvider'; +import { ValidationStep } from '@/spreadsheet-import/steps/components/ValidationStep/ValidationStep'; import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope'; const meta: Meta = { diff --git a/packages/twenty-front/src/modules/ui/input/code-editor/components/CodeEditor.tsx b/packages/twenty-front/src/modules/ui/input/code-editor/components/CodeEditor.tsx index 1240a6d6025e..723b04a9f69b 100644 --- a/packages/twenty-front/src/modules/ui/input/code-editor/components/CodeEditor.tsx +++ b/packages/twenty-front/src/modules/ui/input/code-editor/components/CodeEditor.tsx @@ -1,11 +1,11 @@ -import Editor, { Monaco, EditorProps } from '@monaco-editor/react'; -import dotenv from 'dotenv'; -import { AutoTypings } from 'monaco-editor-auto-typings'; -import { editor, MarkerSeverity } from 'monaco-editor'; -import { codeEditorTheme } from '@/ui/input/code-editor/theme/CodeEditorTheme'; +import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages'; +import { codeEditorTheme } from '@/ui/input/code-editor/utils/codeEditorTheme'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages'; +import Editor, { EditorProps, Monaco } from '@monaco-editor/react'; +import dotenv from 'dotenv'; +import { MarkerSeverity, editor } from 'monaco-editor'; +import { AutoTypings } from 'monaco-editor-auto-typings'; import { isDefined } from '~/utils/isDefined'; const StyledEditor = styled(Editor)` diff --git a/packages/twenty-front/src/modules/ui/input/code-editor/theme/CodeEditorTheme.ts b/packages/twenty-front/src/modules/ui/input/code-editor/utils/codeEditorTheme.ts similarity index 100% rename from packages/twenty-front/src/modules/ui/input/code-editor/theme/CodeEditorTheme.ts rename to packages/twenty-front/src/modules/ui/input/code-editor/utils/codeEditorTheme.ts diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx index e7a330c81ff3..ebca2067fcdc 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/InternalDatePicker.tsx @@ -17,7 +17,7 @@ import { UserContext } from '@/users/contexts/UserContext'; import { VariableDateViewFilterValueDirection, VariableDateViewFilterValueUnit, -} from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; +} from '@/views/view-filter-value/utils/resolveDateViewFilterValue'; import { useContext } from 'react'; import 'react-datepicker/dist/react-datepicker.css'; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/RelativeDatePickerHeader.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/RelativeDatePickerHeader.tsx index 0a9328577dba..d970f3c825b1 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/RelativeDatePickerHeader.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/RelativeDatePickerHeader.tsx @@ -1,12 +1,13 @@ -import { RELATIVE_DATE_DIRECTION_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateDirectionSelectOptions'; -import { RELATIVE_DATE_UNITS_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions'; import { Select } from '@/ui/input/components/Select'; import { TextInput } from '@/ui/input/components/TextInput'; +import { RELATIVE_DATE_DIRECTION_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateDirectionSelectOptions'; +import { RELATIVE_DATE_UNITS_SELECT_OPTIONS } from '@/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions'; import { VariableDateViewFilterValueDirection, - variableDateViewFilterValuePartsSchema, VariableDateViewFilterValueUnit, -} from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; + variableDateViewFilterValuePartsSchema, +} from '@/views/view-filter-value/utils/resolveDateViewFilterValue'; + import styled from '@emotion/styled'; import { useEffect, useState } from 'react'; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateDirectionSelectOptions.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateDirectionSelectOptions.ts index d13926719f0f..3a066f722d86 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateDirectionSelectOptions.ts +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateDirectionSelectOptions.ts @@ -1,4 +1,4 @@ -import { VariableDateViewFilterValueDirection } from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; +import { VariableDateViewFilterValueDirection } from '@/views/view-filter-value/utils/resolveDateViewFilterValue'; type RelativeDateDirectionOption = { value: VariableDateViewFilterValueDirection; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions.ts index bf65953f63bc..832fc41f7cad 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions.ts +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions.ts @@ -1,4 +1,4 @@ -import { VariableDateViewFilterValueUnit } from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; +import { VariableDateViewFilterValueUnit } from '@/views/view-filter-value/utils/resolveDateViewFilterValue'; type RelativeDateUnit = { value: VariableDateViewFilterValueUnit; diff --git a/packages/twenty-front/src/modules/ui/input/editor/components/BlockEditor.tsx b/packages/twenty-front/src/modules/ui/input/editor/components/BlockEditor.tsx index f2a940de52bd..63af03fb0d32 100644 --- a/packages/twenty-front/src/modules/ui/input/editor/components/BlockEditor.tsx +++ b/packages/twenty-front/src/modules/ui/input/editor/components/BlockEditor.tsx @@ -5,8 +5,8 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { ClipboardEvent } from 'react'; -import { blockSchema } from '@/activities/blocks/schema'; -import { getSlashMenu } from '@/activities/blocks/slashMenu'; +import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema'; +import { getSlashMenu } from '@/activities/blocks/utils/getSlashMenu'; import { CustomSideMenu } from '@/ui/input/editor/components/CustomSideMenu'; import { CustomSlashMenu, @@ -14,7 +14,7 @@ import { } from '@/ui/input/editor/components/CustomSlashMenu'; interface BlockEditorProps { - editor: typeof blockSchema.BlockNoteEditor; + editor: typeof BLOCK_SCHEMA.BlockNoteEditor; onFocus?: () => void; onBlur?: () => void; onPaste?: (event: ClipboardEvent) => void; diff --git a/packages/twenty-front/src/modules/ui/input/editor/components/CustomAddBlockItem.tsx b/packages/twenty-front/src/modules/ui/input/editor/components/CustomAddBlockItem.tsx index 2eeb9862173e..02076198daed 100644 --- a/packages/twenty-front/src/modules/ui/input/editor/components/CustomAddBlockItem.tsx +++ b/packages/twenty-front/src/modules/ui/input/editor/components/CustomAddBlockItem.tsx @@ -1,9 +1,9 @@ -import { blockSchema } from '@/activities/blocks/schema'; +import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema'; import { useComponentsContext } from '@blocknote/react'; type CustomAddBlockItemProps = { - editor: typeof blockSchema.BlockNoteEditor; + editor: typeof BLOCK_SCHEMA.BlockNoteEditor; children: React.ReactNode; // Adding the children prop }; diff --git a/packages/twenty-front/src/modules/ui/input/editor/components/CustomSideMenu.tsx b/packages/twenty-front/src/modules/ui/input/editor/components/CustomSideMenu.tsx index 6f44e0ebc501..62a6443ceab9 100644 --- a/packages/twenty-front/src/modules/ui/input/editor/components/CustomSideMenu.tsx +++ b/packages/twenty-front/src/modules/ui/input/editor/components/CustomSideMenu.tsx @@ -1,4 +1,4 @@ -import { blockSchema } from '@/activities/blocks/schema'; +import { BLOCK_SCHEMA } from '@/activities/blocks/constants/Schema'; import { CustomAddBlockItem } from '@/ui/input/editor/components/CustomAddBlockItem'; import { CustomSideMenuOptions } from '@/ui/input/editor/components/CustomSideMenuOptions'; import { @@ -13,7 +13,7 @@ import styled from '@emotion/styled'; import { IconColorSwatch, IconPlus, IconTrash } from 'twenty-ui'; type CustomSideMenuProps = { - editor: typeof blockSchema.BlockNoteEditor; + editor: typeof BLOCK_SCHEMA.BlockNoteEditor; }; const StyledDivToCreateGap = styled.div` diff --git a/packages/twenty-front/src/modules/ui/input/editor/utils/__tests__/getFirstNonEmptyLineOfRichText.test.tsx b/packages/twenty-front/src/modules/ui/input/editor/utils/__tests__/getFirstNonEmptyLineOfRichText.test.ts similarity index 100% rename from packages/twenty-front/src/modules/ui/input/editor/utils/__tests__/getFirstNonEmptyLineOfRichText.test.tsx rename to packages/twenty-front/src/modules/ui/input/editor/utils/__tests__/getFirstNonEmptyLineOfRichText.test.ts diff --git a/packages/twenty-front/src/modules/ui/layout/draggable-list/__stories__/DraggableItem.stories.tsx b/packages/twenty-front/src/modules/ui/layout/draggable-list/components/__stories__/DraggableItem.stories.tsx similarity index 92% rename from packages/twenty-front/src/modules/ui/layout/draggable-list/__stories__/DraggableItem.stories.tsx rename to packages/twenty-front/src/modules/ui/layout/draggable-list/components/__stories__/DraggableItem.stories.tsx index a6b2cffc0e03..810be399c972 100644 --- a/packages/twenty-front/src/modules/ui/layout/draggable-list/__stories__/DraggableItem.stories.tsx +++ b/packages/twenty-front/src/modules/ui/layout/draggable-list/components/__stories__/DraggableItem.stories.tsx @@ -2,10 +2,9 @@ import { DragDropContext, Droppable } from '@hello-pangea/dnd'; import { Meta, StoryObj } from '@storybook/react'; import { ComponentDecorator, IconBell } from 'twenty-ui'; +import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem'; import { MenuItemDraggable } from '@/ui/navigation/menu-item/components/MenuItemDraggable'; -import { DraggableItem } from '../components/DraggableItem'; - const meta: Meta = { title: 'UI/Layout/DraggableList/DraggableItem', component: DraggableItem, diff --git a/packages/twenty-front/src/modules/ui/layout/draggable-list/__stories__/DraggableList.stories.tsx b/packages/twenty-front/src/modules/ui/layout/draggable-list/components/__stories__/DraggableList.stories.tsx similarity index 89% rename from packages/twenty-front/src/modules/ui/layout/draggable-list/__stories__/DraggableList.stories.tsx rename to packages/twenty-front/src/modules/ui/layout/draggable-list/components/__stories__/DraggableList.stories.tsx index 071d8a336bac..813481958f54 100644 --- a/packages/twenty-front/src/modules/ui/layout/draggable-list/__stories__/DraggableList.stories.tsx +++ b/packages/twenty-front/src/modules/ui/layout/draggable-list/components/__stories__/DraggableList.stories.tsx @@ -2,11 +2,10 @@ import { action } from '@storybook/addon-actions'; import { Meta, StoryObj } from '@storybook/react'; import { ComponentDecorator, IconBell } from 'twenty-ui'; +import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem'; +import { DraggableList } from '@/ui/layout/draggable-list/components/DraggableList'; import { MenuItemDraggable } from '@/ui/navigation/menu-item/components/MenuItemDraggable'; -import { DraggableItem } from '../components/DraggableItem'; -import { DraggableList } from '../components/DraggableList'; - const meta: Meta = { title: 'UI/Layout/DraggableList/DraggableList', component: DraggableList, diff --git a/packages/twenty-front/src/modules/ui/layout/page/BlankLayout.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/BlankLayout.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/layout/page/BlankLayout.tsx rename to packages/twenty-front/src/modules/ui/layout/page/components/BlankLayout.tsx diff --git a/packages/twenty-front/src/modules/ui/layout/page/DefaultLayout.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/layout/page/DefaultLayout.tsx rename to packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx diff --git a/packages/twenty-front/src/modules/ui/layout/page/PageAddButton.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/PageAddButton.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/layout/page/PageAddButton.tsx rename to packages/twenty-front/src/modules/ui/layout/page/components/PageAddButton.tsx diff --git a/packages/twenty-front/src/modules/ui/layout/page/PageBody.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/PageBody.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/layout/page/PageBody.tsx rename to packages/twenty-front/src/modules/ui/layout/page/components/PageBody.tsx diff --git a/packages/twenty-front/src/modules/ui/layout/page/PageContainer.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/PageContainer.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/layout/page/PageContainer.tsx rename to packages/twenty-front/src/modules/ui/layout/page/components/PageContainer.tsx diff --git a/packages/twenty-front/src/modules/ui/layout/page/PageFavoriteButton.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/PageFavoriteButton.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/layout/page/PageFavoriteButton.tsx rename to packages/twenty-front/src/modules/ui/layout/page/components/PageFavoriteButton.tsx diff --git a/packages/twenty-front/src/modules/ui/layout/page/PageHeader.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/PageHeader.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/layout/page/PageHeader.tsx rename to packages/twenty-front/src/modules/ui/layout/page/components/PageHeader.tsx diff --git a/packages/twenty-front/src/modules/ui/layout/page/PageHotkeysEffect.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/PageHotkeysEffect.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/layout/page/PageHotkeysEffect.tsx rename to packages/twenty-front/src/modules/ui/layout/page/components/PageHotkeysEffect.tsx diff --git a/packages/twenty-front/src/modules/ui/layout/page/PagePanel.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/PagePanel.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/layout/page/PagePanel.tsx rename to packages/twenty-front/src/modules/ui/layout/page/components/PagePanel.tsx diff --git a/packages/twenty-front/src/modules/ui/layout/page/RightDrawerContainer.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/RightDrawerContainer.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/layout/page/RightDrawerContainer.tsx rename to packages/twenty-front/src/modules/ui/layout/page/components/RightDrawerContainer.tsx diff --git a/packages/twenty-front/src/modules/ui/layout/page/ShowPageContainer.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/ShowPageContainer.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/layout/page/ShowPageContainer.tsx rename to packages/twenty-front/src/modules/ui/layout/page/components/ShowPageContainer.tsx diff --git a/packages/twenty-front/src/modules/ui/layout/page/SubMenuTopBarContainer.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/SubMenuTopBarContainer.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/layout/page/SubMenuTopBarContainer.tsx rename to packages/twenty-front/src/modules/ui/layout/page/components/SubMenuTopBarContainer.tsx diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx index d8bf449d6afd..3365a170e2a6 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx @@ -3,7 +3,7 @@ import { EmailThreads } from '@/activities/emails/components/EmailThreads'; import { Attachments } from '@/activities/files/components/Attachments'; import { Notes } from '@/activities/notes/components/Notes'; import { ObjectTasks } from '@/activities/tasks/components/ObjectTasks'; -import { TimelineActivities } from '@/activities/timelineActivities/components/TimelineActivities'; +import { TimelineActivities } from '@/activities/timeline-activities/components/TimelineActivities'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; diff --git a/packages/twenty-front/src/modules/ui/layout/top-bar/TopBar.tsx b/packages/twenty-front/src/modules/ui/layout/top-bar/components/TopBar.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/layout/top-bar/TopBar.tsx rename to packages/twenty-front/src/modules/ui/layout/top-bar/components/TopBar.tsx diff --git a/packages/twenty-front/src/modules/ui/utilities/page-title/PageTitle.tsx b/packages/twenty-front/src/modules/ui/utilities/page-title/components/PageTitle.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/utilities/page-title/PageTitle.tsx rename to packages/twenty-front/src/modules/ui/utilities/page-title/components/PageTitle.tsx diff --git a/packages/twenty-front/src/modules/ui/utilities/recoil-scope/scopes-internal/hooks/__tests__/useAvailableScopeId.test.tsx b/packages/twenty-front/src/modules/ui/utilities/recoil-scope/scopes-internal/hooks/__tests__/useAvailableScopeId.test.tsx index afb281b9a759..05a638e71727 100644 --- a/packages/twenty-front/src/modules/ui/utilities/recoil-scope/scopes-internal/hooks/__tests__/useAvailableScopeId.test.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/recoil-scope/scopes-internal/hooks/__tests__/useAvailableScopeId.test.tsx @@ -1,5 +1,5 @@ -import React from 'react'; import { renderHook } from '@testing-library/react'; +import React from 'react'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { createScopeInternalContext } from '@/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext'; diff --git a/packages/twenty-front/src/modules/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext.ts b/packages/twenty-front/src/modules/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext.ts index abf1b33900b4..02d447a7fd32 100644 --- a/packages/twenty-front/src/modules/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext.ts +++ b/packages/twenty-front/src/modules/ui/utilities/recoil-scope/scopes-internal/utils/createScopeInternalContext.ts @@ -1,6 +1,5 @@ -import { Context, createContext } from 'react'; - import { RecoilComponentStateKey } from '@/ui/utilities/state/component-state/types/RecoilComponentStateKey'; +import { Context, createContext } from 'react'; type ScopeInternalContext = Context; diff --git a/packages/twenty-front/src/modules/ui/utilities/responsive/hooks/__tests__/isMobile.test.tsx b/packages/twenty-front/src/modules/ui/utilities/responsive/hooks/__tests__/useIsMobile.test.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/utilities/responsive/hooks/__tests__/isMobile.test.tsx rename to packages/twenty-front/src/modules/ui/utilities/responsive/hooks/__tests__/useIsMobile.test.tsx diff --git a/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentStateV2_alpha.ts b/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentStateV2Alpha.ts similarity index 100% rename from packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentStateV2_alpha.ts rename to packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/createComponentStateV2Alpha.ts diff --git a/packages/twenty-front/src/modules/views/components/ViewBar.tsx b/packages/twenty-front/src/modules/views/components/ViewBar.tsx index 186f54b691ba..59642d58a301 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBar.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBar.tsx @@ -5,7 +5,7 @@ import { ObjectFilterDropdownButton } from '@/object-record/object-filter-dropdo import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope'; import { ObjectSortDropdownButton } from '@/object-record/object-sort-dropdown/components/ObjectSortDropdownButton'; import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading'; -import { TopBar } from '@/ui/layout/top-bar/TopBar'; +import { TopBar } from '@/ui/layout/top-bar/components/TopBar'; import { QueryParamsFiltersEffect } from '@/views/components/QueryParamsFiltersEffect'; import { QueryParamsViewIdEffect } from '@/views/components/QueryParamsViewIdEffect'; import { ViewBarEffect } from '@/views/components/ViewBarEffect'; diff --git a/packages/twenty-front/src/modules/views/components/ViewBarPageTitle.tsx b/packages/twenty-front/src/modules/views/components/ViewBarPageTitle.tsx index c7ab13c549b5..758a83b733be 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarPageTitle.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarPageTitle.tsx @@ -1,6 +1,6 @@ import { useParams } from 'react-router-dom'; -import { PageTitle } from '@/ui/utilities/page-title/PageTitle'; +import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { capitalize } from '~/utils/string/capitalize'; diff --git a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts index 15327a303579..fce290183f7c 100644 --- a/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts +++ b/packages/twenty-front/src/modules/views/hooks/internal/usePersistViewFieldRecords.ts @@ -1,5 +1,5 @@ -import { useCallback } from 'react'; import { useApolloClient } from '@apollo/client'; +import { useCallback } from 'react'; import { v4 } from 'uuid'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; diff --git a/packages/twenty-front/src/modules/views/utils/view-filter-value/computeVariableDateViewFilterValue.ts b/packages/twenty-front/src/modules/views/view-filter-value/utils/computeVariableDateViewFilterValue.ts similarity index 83% rename from packages/twenty-front/src/modules/views/utils/view-filter-value/computeVariableDateViewFilterValue.ts rename to packages/twenty-front/src/modules/views/view-filter-value/utils/computeVariableDateViewFilterValue.ts index 1b09bc91348b..d34e4b7f1b35 100644 --- a/packages/twenty-front/src/modules/views/utils/view-filter-value/computeVariableDateViewFilterValue.ts +++ b/packages/twenty-front/src/modules/views/view-filter-value/utils/computeVariableDateViewFilterValue.ts @@ -1,7 +1,7 @@ import { VariableDateViewFilterValueDirection, VariableDateViewFilterValueUnit, -} from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; +} from '@/views/view-filter-value/utils/resolveDateViewFilterValue'; export const computeVariableDateViewFilterValue = ( direction: VariableDateViewFilterValueDirection, diff --git a/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveDateViewFilterValue.ts b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveDateViewFilterValue.ts similarity index 100% rename from packages/twenty-front/src/modules/views/utils/view-filter-value/resolveDateViewFilterValue.ts rename to packages/twenty-front/src/modules/views/view-filter-value/utils/resolveDateViewFilterValue.ts diff --git a/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveFilterValue.ts b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveFilterValue.ts similarity index 91% rename from packages/twenty-front/src/modules/views/utils/view-filter-value/resolveFilterValue.ts rename to packages/twenty-front/src/modules/views/view-filter-value/utils/resolveFilterValue.ts index 34afbb46ad1a..c5c4e064507b 100644 --- a/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveFilterValue.ts +++ b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveFilterValue.ts @@ -1,7 +1,7 @@ import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; -import { resolveNumberViewFilterValue } from '@/views/utils/view-filter-value/resolveNumberViewFilterValue'; +import { resolveNumberViewFilterValue } from '@/views/view-filter-value/utils/resolveNumberViewFilterValue'; import { resolveDateViewFilterValue, ResolvedDateViewFilterValue, diff --git a/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveNumberViewFilterValue.ts b/packages/twenty-front/src/modules/views/view-filter-value/utils/resolveNumberViewFilterValue.ts similarity index 100% rename from packages/twenty-front/src/modules/views/utils/view-filter-value/resolveNumberViewFilterValue.ts rename to packages/twenty-front/src/modules/views/view-filter-value/utils/resolveNumberViewFilterValue.ts diff --git a/packages/twenty-front/src/modules/workflow/hooks/useActivateWorkflowVersion.tsx b/packages/twenty-front/src/modules/workflow/hooks/useActivateWorkflowVersion.ts similarity index 100% rename from packages/twenty-front/src/modules/workflow/hooks/useActivateWorkflowVersion.tsx rename to packages/twenty-front/src/modules/workflow/hooks/useActivateWorkflowVersion.ts diff --git a/packages/twenty-front/src/modules/workflow/hooks/useCreateNewWorkflowVersion.tsx b/packages/twenty-front/src/modules/workflow/hooks/useCreateNewWorkflowVersion.ts similarity index 100% rename from packages/twenty-front/src/modules/workflow/hooks/useCreateNewWorkflowVersion.tsx rename to packages/twenty-front/src/modules/workflow/hooks/useCreateNewWorkflowVersion.ts diff --git a/packages/twenty-front/src/modules/workflow/hooks/useCreateStep.tsx b/packages/twenty-front/src/modules/workflow/hooks/useCreateStep.ts similarity index 100% rename from packages/twenty-front/src/modules/workflow/hooks/useCreateStep.tsx rename to packages/twenty-front/src/modules/workflow/hooks/useCreateStep.ts diff --git a/packages/twenty-front/src/modules/workflow/hooks/useDeactivateWorkflowVersion.tsx b/packages/twenty-front/src/modules/workflow/hooks/useDeactivateWorkflowVersion.ts similarity index 100% rename from packages/twenty-front/src/modules/workflow/hooks/useDeactivateWorkflowVersion.tsx rename to packages/twenty-front/src/modules/workflow/hooks/useDeactivateWorkflowVersion.ts diff --git a/packages/twenty-front/src/modules/workflow/hooks/useDeleteOneStep.tsx b/packages/twenty-front/src/modules/workflow/hooks/useDeleteOneStep.ts similarity index 100% rename from packages/twenty-front/src/modules/workflow/hooks/useDeleteOneStep.tsx rename to packages/twenty-front/src/modules/workflow/hooks/useDeleteOneStep.ts diff --git a/packages/twenty-front/src/modules/workflow/hooks/useDeleteOneWorkflowVersion.tsx b/packages/twenty-front/src/modules/workflow/hooks/useDeleteOneWorkflowVersion.ts similarity index 100% rename from packages/twenty-front/src/modules/workflow/hooks/useDeleteOneWorkflowVersion.tsx rename to packages/twenty-front/src/modules/workflow/hooks/useDeleteOneWorkflowVersion.ts diff --git a/packages/twenty-front/src/modules/workflow/hooks/useStartNodeCreation.tsx b/packages/twenty-front/src/modules/workflow/hooks/useStartNodeCreation.ts similarity index 100% rename from packages/twenty-front/src/modules/workflow/hooks/useStartNodeCreation.tsx rename to packages/twenty-front/src/modules/workflow/hooks/useStartNodeCreation.ts diff --git a/packages/twenty-front/src/modules/workflow/hooks/useTriggerNodeSelection.tsx b/packages/twenty-front/src/modules/workflow/hooks/useTriggerNodeSelection.ts similarity index 100% rename from packages/twenty-front/src/modules/workflow/hooks/useTriggerNodeSelection.tsx rename to packages/twenty-front/src/modules/workflow/hooks/useTriggerNodeSelection.ts diff --git a/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionStep.tsx b/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionStep.ts similarity index 100% rename from packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionStep.tsx rename to packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionStep.ts diff --git a/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionTrigger.tsx b/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionTrigger.ts similarity index 100% rename from packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionTrigger.tsx rename to packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionTrigger.ts diff --git a/packages/twenty-front/src/modules/workflow/hooks/useWorkflowVersion.tsx b/packages/twenty-front/src/modules/workflow/hooks/useWorkflowVersion.ts similarity index 100% rename from packages/twenty-front/src/modules/workflow/hooks/useWorkflowVersion.tsx rename to packages/twenty-front/src/modules/workflow/hooks/useWorkflowVersion.ts diff --git a/packages/twenty-front/src/modules/workflow/hooks/useWorkflowWithCurrentVersion.tsx b/packages/twenty-front/src/modules/workflow/hooks/useWorkflowWithCurrentVersion.ts similarity index 100% rename from packages/twenty-front/src/modules/workflow/hooks/useWorkflowWithCurrentVersion.tsx rename to packages/twenty-front/src/modules/workflow/hooks/useWorkflowWithCurrentVersion.ts diff --git a/packages/twenty-front/src/modules/workflow/utils/assertWorkflowWithCurrentVersionIsDefined.tsx b/packages/twenty-front/src/modules/workflow/utils/assertWorkflowWithCurrentVersionIsDefined.ts similarity index 100% rename from packages/twenty-front/src/modules/workflow/utils/assertWorkflowWithCurrentVersionIsDefined.tsx rename to packages/twenty-front/src/modules/workflow/utils/assertWorkflowWithCurrentVersionIsDefined.ts diff --git a/packages/twenty-front/src/modules/workspace-member/grapqhql/fragments/workspaceMemberQueryFragment.ts b/packages/twenty-front/src/modules/workspace-member/graphql/fragments/workspaceMemberQueryFragment.ts similarity index 100% rename from packages/twenty-front/src/modules/workspace-member/grapqhql/fragments/workspaceMemberQueryFragment.ts rename to packages/twenty-front/src/modules/workspace-member/graphql/fragments/workspaceMemberQueryFragment.ts diff --git a/packages/twenty-front/src/modules/workspace-member/grapqhql/mutations/addUserToWorkspace.ts b/packages/twenty-front/src/modules/workspace-member/graphql/mutations/addUserToWorkspace.ts similarity index 100% rename from packages/twenty-front/src/modules/workspace-member/grapqhql/mutations/addUserToWorkspace.ts rename to packages/twenty-front/src/modules/workspace-member/graphql/mutations/addUserToWorkspace.ts diff --git a/packages/twenty-front/src/modules/workspace-member/grapqhql/mutations/addUserToWorkspaceByInviteToken.ts b/packages/twenty-front/src/modules/workspace-member/graphql/mutations/addUserToWorkspaceByInviteToken.ts similarity index 100% rename from packages/twenty-front/src/modules/workspace-member/grapqhql/mutations/addUserToWorkspaceByInviteToken.ts rename to packages/twenty-front/src/modules/workspace-member/graphql/mutations/addUserToWorkspaceByInviteToken.ts diff --git a/packages/twenty-front/src/pages/not-found/NotFound.tsx b/packages/twenty-front/src/pages/not-found/NotFound.tsx index bf3acc27aed8..4270c5718b4b 100644 --- a/packages/twenty-front/src/pages/not-found/NotFound.tsx +++ b/packages/twenty-front/src/pages/not-found/NotFound.tsx @@ -11,7 +11,7 @@ import { AnimatedPlaceholderErrorTitle, } from '@/ui/layout/animated-placeholder/components/ErrorPlaceholderStyled'; import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; -import { PageTitle } from '@/ui/utilities/page-title/PageTitle'; +import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle'; const StyledBackDrop = styled.div` align-items: center; diff --git a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx index 44eb6f203b25..dfefa674cdf1 100644 --- a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx @@ -9,9 +9,9 @@ import { RecordIndexPageHeader } from '@/object-record/record-index/components/R import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; import { useHandleIndexIdentifierClick } from '@/object-record/record-index/hooks/useHandleIndexIdentifierClick'; import { useCreateNewTableRecord } from '@/object-record/record-table/hooks/useCreateNewTableRecords'; -import { PageBody } from '@/ui/layout/page/PageBody'; -import { PageContainer } from '@/ui/layout/page/PageContainer'; -import { PageTitle } from '@/ui/utilities/page-title/PageTitle'; +import { PageBody } from '@/ui/layout/page/components/PageBody'; +import { PageContainer } from '@/ui/layout/page/components/PageContainer'; +import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle'; import { useRecoilCallback } from 'recoil'; import { capitalize } from '~/utils/string/capitalize'; diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx index 0ecb2cb569de..6c838400370e 100644 --- a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx @@ -1,14 +1,14 @@ import { useParams } from 'react-router-dom'; -import { TimelineActivityContext } from '@/activities/timelineActivities/contexts/TimelineActivityContext'; +import { TimelineActivityContext } from '@/activities/timeline-activities/contexts/TimelineActivityContext'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { RecordShowContainer } from '@/object-record/record-show/components/RecordShowContainer'; import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage'; import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect'; import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; -import { PageBody } from '@/ui/layout/page/PageBody'; -import { PageContainer } from '@/ui/layout/page/PageContainer'; -import { PageTitle } from '@/ui/utilities/page-title/PageTitle'; +import { PageBody } from '@/ui/layout/page/components/PageBody'; +import { PageContainer } from '@/ui/layout/page/components/PageContainer'; +import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle'; import { RecordShowPageWorkflowHeader } from '@/workflow/components/RecordShowPageWorkflowHeader'; import { RecordShowPageWorkflowVersionHeader } from '@/workflow/components/RecordShowPageWorkflowVersionHeader'; import { RecordShowPageBaseHeader } from '~/pages/object-record/RecordShowPageBaseHeader'; diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPageBaseHeader.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPageBaseHeader.tsx index a7577114c092..eb64af722938 100644 --- a/packages/twenty-front/src/pages/object-record/RecordShowPageBaseHeader.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordShowPageBaseHeader.tsx @@ -1,6 +1,6 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { PageFavoriteButton } from '@/ui/layout/page/PageFavoriteButton'; +import { PageFavoriteButton } from '@/ui/layout/page/components/PageFavoriteButton'; import { ShowPageAddButton } from '@/ui/layout/show-page/components/ShowPageAddButton'; import { ShowPageMoreButton } from '@/ui/layout/show-page/components/ShowPageMoreButton'; diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPageHeader.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPageHeader.tsx index 7f91f3001055..9808158ec885 100644 --- a/packages/twenty-front/src/pages/object-record/RecordShowPageHeader.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordShowPageHeader.tsx @@ -1,6 +1,6 @@ import { useRecordShowPage } from '@/object-record/record-show/hooks/useRecordShowPage'; import { useRecordShowPagePagination } from '@/object-record/record-show/hooks/useRecordShowPagePagination'; -import { PageHeader } from '@/ui/layout/page/PageHeader'; +import { PageHeader } from '@/ui/layout/page/components/PageHeader'; export const RecordShowPageHeader = ({ objectNameSingular, diff --git a/packages/twenty-front/src/pages/settings/Releases.tsx b/packages/twenty-front/src/pages/settings/Releases.tsx index 3429ac9893ac..9e20f9c9557f 100644 --- a/packages/twenty-front/src/pages/settings/Releases.tsx +++ b/packages/twenty-front/src/pages/settings/Releases.tsx @@ -9,7 +9,7 @@ import { visit } from 'unist-util-visit'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; type ReleaseNote = { diff --git a/packages/twenty-front/src/pages/settings/SettingsBilling.tsx b/packages/twenty-front/src/pages/settings/SettingsBilling.tsx index 5a8c90a81a31..c1f3f4aa51a0 100644 --- a/packages/twenty-front/src/pages/settings/SettingsBilling.tsx +++ b/packages/twenty-front/src/pages/settings/SettingsBilling.tsx @@ -19,7 +19,7 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Button } from '@/ui/input/button/components/Button'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { useSubscriptionStatus } from '@/workspace/hooks/useSubscriptionStatus'; import { diff --git a/packages/twenty-front/src/pages/settings/SettingsProfile.tsx b/packages/twenty-front/src/pages/settings/SettingsProfile.tsx index 0ff302645941..9e28b161164b 100644 --- a/packages/twenty-front/src/pages/settings/SettingsProfile.tsx +++ b/packages/twenty-front/src/pages/settings/SettingsProfile.tsx @@ -8,7 +8,7 @@ import { NameFields } from '@/settings/profile/components/NameFields'; import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePictureUploader'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; export const SettingsProfile = () => ( diff --git a/packages/twenty-front/src/pages/settings/SettingsWorkspace.tsx b/packages/twenty-front/src/pages/settings/SettingsWorkspace.tsx index 329c736f1834..f61342a4c586 100644 --- a/packages/twenty-front/src/pages/settings/SettingsWorkspace.tsx +++ b/packages/twenty-front/src/pages/settings/SettingsWorkspace.tsx @@ -7,7 +7,7 @@ import { NameField } from '@/settings/workspace/components/NameField'; import { ToggleImpersonate } from '@/settings/workspace/components/ToggleImpersonate'; import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader'; import { SettingsPath } from '@/types/SettingsPath'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { GithubVersionLink } from '@/ui/navigation/link/components/GithubVersionLink'; diff --git a/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx b/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx index 3ba11b7f22fb..033f103a9ff9 100644 --- a/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx +++ b/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx @@ -25,7 +25,7 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { IconButton } from '@/ui/input/button/components/IconButton'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { Table } from '@/ui/layout/table/components/Table'; import { TableHeader } from '@/ui/layout/table/components/TableHeader'; diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx index 5ff43e83219d..a827cd4a2533 100644 --- a/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx @@ -14,7 +14,7 @@ import { SettingsAccountsSettingsSection } from '@/settings/accounts/components/ import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; export const SettingsAccounts = () => { diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx index 2dc86e72e114..9e75f3297d15 100644 --- a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx @@ -2,7 +2,7 @@ import { SettingsAccountsCalendarChannelsContainer } from '@/settings/accounts/c import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; export const SettingsAccountsCalendars = () => { diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmails.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmails.tsx index 4e5732ef81b4..4cc777a62396 100644 --- a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmails.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmails.tsx @@ -2,7 +2,7 @@ import { SettingsAccountsMessageChannelsContainer } from '@/settings/accounts/co import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; export const SettingsAccountsEmails = () => ( diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsNewAccount.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsNewAccount.tsx index 35e90d9b2cf0..d46f4b99c7c7 100644 --- a/packages/twenty-front/src/pages/settings/accounts/SettingsNewAccount.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsNewAccount.tsx @@ -2,7 +2,7 @@ import { SettingsNewAccountSection } from '@/settings/accounts/components/Settin import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; export const SettingsNewAccount = () => { return ( diff --git a/packages/twenty-front/src/pages/settings/crm-migration/SettingsCRMMigration.tsx b/packages/twenty-front/src/pages/settings/crm-migration/SettingsCRMMigration.tsx index 4c4dbc807c64..36c6c7b9abb8 100644 --- a/packages/twenty-front/src/pages/settings/crm-migration/SettingsCRMMigration.tsx +++ b/packages/twenty-front/src/pages/settings/crm-migration/SettingsCRMMigration.tsx @@ -7,7 +7,7 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain import { SettingsReadDocumentationButton } from '@/settings/developers/components/SettingsReadDocumentationButton'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { useRecoilValue } from 'recoil'; diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx index 136bad77026b..6c368f0a0d47 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx @@ -17,7 +17,7 @@ import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; const newObjectFormSchema = settingsDataModelObjectAboutFormSchema; diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPageContent.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPageContent.tsx index 37fb2d9c0cd2..d973ab8f3cd2 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPageContent.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPageContent.tsx @@ -5,7 +5,7 @@ import { SettingsObjectSummaryCard } from '@/settings/data-model/object-details/ import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { Button } from '@/ui/input/button/components/Button'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState'; diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectEdit.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectEdit.tsx index 3cbdfe3bdc7e..06cc4999feab 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectEdit.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectEdit.tsx @@ -27,7 +27,7 @@ import { SettingsPath } from '@/types/SettingsPath'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Button } from '@/ui/input/button/components/Button'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; const objectEditFormSchema = z diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx index 651243747a2e..a08d6658cff7 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx @@ -32,7 +32,7 @@ import { SettingsPath } from '@/types/SettingsPath'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Button } from '@/ui/input/button/components/Button'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldConfigure.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldConfigure.tsx index 5e8893813fb3..682cb7123de7 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldConfigure.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldConfigure.tsx @@ -16,7 +16,7 @@ import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType import { AppPath } from '@/types/AppPath'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { View } from '@/views/types/View'; import { ViewType } from '@/views/types/ViewType'; diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldSelect.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldSelect.tsx index cfc683510006..793279f38836 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldSelect.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldSelect.tsx @@ -6,7 +6,7 @@ import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/Set import { SettingsObjectNewFieldSelector } from '@/settings/data-model/fields/forms/components/SettingsObjectNewFieldSelector'; import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; import { AppPath } from '@/types/AppPath'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { zodResolver } from '@hookform/resolvers/zod'; import { useEffect } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectOverview.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectOverview.tsx index 215ae748bc99..e807ea134bf9 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectOverview.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectOverview.tsx @@ -3,7 +3,7 @@ import { ReactFlowProvider } from 'reactflow'; import { SettingsDataModelOverview } from '@/settings/data-model/graph-overview/components/SettingsDataModelOverview'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; export const SettingsObjectOverview = () => { return ( diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjects.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjects.tsx index 7c7e280569d7..ba5fd0fd600b 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjects.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjects.tsx @@ -12,14 +12,14 @@ import { SettingsObjectMetadataItemTableRow, StyledObjectTableRow, } from '@/settings/data-model/object-details/components/SettingsObjectItemTableRow'; -import { SettingsObjectCoverImage } from '@/settings/data-model/objects/SettingsObjectCoverImage'; -import { SettingsObjectInactiveMenuDropDown } from '@/settings/data-model/objects/SettingsObjectInactiveMenuDropDown'; +import { SettingsObjectCoverImage } from '@/settings/data-model/objects/components/SettingsObjectCoverImage'; +import { SettingsObjectInactiveMenuDropDown } from '@/settings/data-model/objects/components/SettingsObjectInactiveMenuDropDown'; import { getObjectTypeLabel } from '@/settings/data-model/utils/getObjectTypeLabel'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { Button } from '@/ui/input/button/components/Button'; import { TextInput } from '@/ui/input/components/TextInput'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { SortableTableHeader } from '@/ui/layout/table/components/SortableTableHeader'; import { Table } from '@/ui/layout/table/components/Table'; diff --git a/packages/twenty-front/src/pages/settings/developers/SettingsDevelopers.tsx b/packages/twenty-front/src/pages/settings/developers/SettingsDevelopers.tsx index 35ce036694e1..a200a8445546 100644 --- a/packages/twenty-front/src/pages/settings/developers/SettingsDevelopers.tsx +++ b/packages/twenty-front/src/pages/settings/developers/SettingsDevelopers.tsx @@ -9,7 +9,7 @@ import { SettingsWebhooksTable } from '@/settings/developers/components/Settings import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { Button } from '@/ui/input/button/components/Button'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; const StyledButtonContainer = styled.div` diff --git a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail.tsx b/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail.tsx index 7a9651b6893a..8d45c760cb59 100644 --- a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail.tsx +++ b/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeyDetail.tsx @@ -15,8 +15,8 @@ import { ApiKeyInput } from '@/settings/developers/components/ApiKeyInput'; import { ApiKeyNameInput } from '@/settings/developers/components/ApiKeyNameInput'; import { apiKeyTokenState } from '@/settings/developers/states/generatedApiKeyTokenState'; import { ApiKey } from '@/settings/developers/types/api-key/ApiKey'; -import { computeNewExpirationDate } from '@/settings/developers/utils/compute-new-expiration-date'; -import { formatExpiration } from '@/settings/developers/utils/format-expiration'; +import { computeNewExpirationDate } from '@/settings/developers/utils/computeNewExpirationDate'; +import { formatExpiration } from '@/settings/developers/utils/formatExpiration'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; @@ -24,7 +24,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { Button } from '@/ui/input/button/components/Button'; import { TextInput } from '@/ui/input/components/TextInput'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { useGenerateApiKeyTokenMutation } from '~/generated/graphql'; diff --git a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx b/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx index 92951f0a5b43..93733b7cc64a 100644 --- a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx +++ b/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx @@ -14,7 +14,7 @@ import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { Select } from '@/ui/input/components/Select'; import { TextInput } from '@/ui/input/components/TextInput'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { useSetRecoilState } from 'recoil'; import { Key } from 'ts-key-enum'; diff --git a/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx b/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx index 006a4db6a88b..1a2316a0a317 100644 --- a/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx +++ b/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx @@ -21,7 +21,7 @@ import { Select } from '@/ui/input/components/Select'; import { TextArea } from '@/ui/input/components/TextArea'; import { TextInput } from '@/ui/input/components/TextInput'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useRecoilValue } from 'recoil'; diff --git a/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhooksNew.tsx b/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhooksNew.tsx index b473d3f28c76..4d0e7bb47abb 100644 --- a/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhooksNew.tsx +++ b/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhooksNew.tsx @@ -10,7 +10,7 @@ import { Webhook } from '@/settings/developers/types/webhook/Webhook'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { TextInput } from '@/ui/input/components/TextInput'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { isValidUrl } from '~/utils/url/isValidUrl'; diff --git a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationDatabase.tsx b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationDatabase.tsx index eb8ddd11f2cf..1b803f6355be 100644 --- a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationDatabase.tsx +++ b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationDatabase.tsx @@ -11,7 +11,7 @@ import { useSettingsIntegrationCategories } from '@/settings/integrations/hooks/ import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { AppPath } from '@/types/AppPath'; import { SettingsPath } from '@/types/SettingsPath'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; export const SettingsIntegrationDatabase = () => { diff --git a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationEditDatabaseConnection.tsx b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationEditDatabaseConnection.tsx index 92de8008e048..10c57a8bcb4f 100644 --- a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationEditDatabaseConnection.tsx +++ b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationEditDatabaseConnection.tsx @@ -2,7 +2,7 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain import { SettingsIntegrationEditDatabaseConnectionContainer } from '@/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContainer'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; export const SettingsIntegrationEditDatabaseConnection = () => { return ( diff --git a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx index e10f696446f6..017eeb5c279f 100644 --- a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx +++ b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx @@ -21,7 +21,7 @@ import { AppPath } from '@/types/AppPath'; import { SettingsPath } from '@/types/SettingsPath'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { CreateRemoteServerInput } from '~/generated-metadata/graphql'; diff --git a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationShowDatabaseConnection.tsx b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationShowDatabaseConnection.tsx index 76f1aefa24c8..2d2000d536c3 100644 --- a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationShowDatabaseConnection.tsx +++ b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationShowDatabaseConnection.tsx @@ -2,7 +2,7 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain import { SettingsIntegrationDatabaseConnectionShowContainer } from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionShowContainer'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; export const SettingsIntegrationShowDatabaseConnection = () => { return ( diff --git a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrations.tsx b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrations.tsx index bfd5db517c93..de44d7346c59 100644 --- a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrations.tsx +++ b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrations.tsx @@ -3,7 +3,7 @@ import { SettingsIntegrationGroup } from '@/settings/integrations/components/Set import { useSettingsIntegrationCategories } from '@/settings/integrations/hooks/useSettingsIntegrationCategories'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; export const SettingsIntegrations = () => { const integrationCategories = useSettingsIntegrationCategories(); diff --git a/packages/twenty-front/src/pages/settings/profile/appearance/components/SettingsAppearance.tsx b/packages/twenty-front/src/pages/settings/profile/appearance/components/SettingsAppearance.tsx index 85ca252abfba..891d69d957ad 100644 --- a/packages/twenty-front/src/pages/settings/profile/appearance/components/SettingsAppearance.tsx +++ b/packages/twenty-front/src/pages/settings/profile/appearance/components/SettingsAppearance.tsx @@ -4,7 +4,7 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { ColorSchemePicker } from '@/ui/input/color-scheme/components/ColorSchemePicker'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { useColorScheme } from '@/ui/theme/hooks/useColorScheme'; import { DateTimeSettings } from '~/pages/settings/profile/appearance/components/DateTimeSettings'; diff --git a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx index 2934066fd1a4..b6237754fdcc 100644 --- a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx +++ b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx @@ -14,7 +14,7 @@ import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { TabList } from '@/ui/layout/tab/components/TabList'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; @@ -23,8 +23,8 @@ import { useParams } from 'react-router-dom'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { IconCode, IconSettings, IconTestPipe } from 'twenty-ui'; import { usePreventOverlapCallback } from '~/hooks/usePreventOverlapCallback'; -import { isDefined } from '~/utils/isDefined'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; +import { isDefined } from '~/utils/isDefined'; const TAB_LIST_COMPONENT_ID = 'serverless-function-detail'; diff --git a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctions.tsx b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctions.tsx index 9cfd9b04ca42..1d240653c974 100644 --- a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctions.tsx +++ b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctions.tsx @@ -2,7 +2,7 @@ import { SettingsServerlessFunctionsTable } from '@/settings/serverless-function import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { Button } from '@/ui/input/button/components/Button'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; import { IconPlus } from 'twenty-ui'; diff --git a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionsNew.tsx b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionsNew.tsx index 3e43717544ce..cb52a0c020df 100644 --- a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionsNew.tsx +++ b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionsNew.tsx @@ -1,6 +1,6 @@ import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { useNavigate } from 'react-router-dom'; import { SettingsServerlessFunctionNewForm } from '@/settings/serverless-functions/components/SettingsServerlessFunctionNewForm'; diff --git a/packages/twenty-front/src/testing/decorators/ChipGeneratorsDecorator.tsx b/packages/twenty-front/src/testing/decorators/ChipGeneratorsDecorator.tsx index c107d5466387..e47d8bcabac7 100644 --- a/packages/twenty-front/src/testing/decorators/ChipGeneratorsDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/ChipGeneratorsDecorator.tsx @@ -1,7 +1,7 @@ import { Decorator } from '@storybook/react'; import { useMemo } from 'react'; -import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext'; +import { PreComputedChipGeneratorsContext } from '@/object-metadata/contexts/PreComputedChipGeneratorsContext'; import { getRecordChipGenerators } from '@/object-record/utils/getRecordChipGenerators'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; diff --git a/packages/twenty-front/src/testing/decorators/PageDecorator.tsx b/packages/twenty-front/src/testing/decorators/PageDecorator.tsx index 244772c809e6..7405490de3a9 100644 --- a/packages/twenty-front/src/testing/decorators/PageDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/PageDecorator.tsx @@ -14,9 +14,9 @@ import { RecoilRoot } from 'recoil'; import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect'; import { ApolloMetadataClientMockedProvider } from '@/object-metadata/hooks/__mocks__/ApolloMetadataClientMockedProvider'; import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; +import { DefaultLayout } from '@/ui/layout/page/components/DefaultLayout'; import { UserProviderEffect } from '@/users/components/UserProviderEffect'; import { ClientConfigProvider } from '~/modules/client-config/components/ClientConfigProvider'; -import { DefaultLayout } from '~/modules/ui/layout/page/DefaultLayout'; import { UserProvider } from '~/modules/users/components/UserProvider'; import { mockedApolloClient } from '~/testing/mockedApolloClient'; diff --git a/packages/twenty-front/src/testing/mock-data/timeline-activities.ts b/packages/twenty-front/src/testing/mock-data/timeline-activities.ts index 971ab159737c..a716ba032f76 100644 --- a/packages/twenty-front/src/testing/mock-data/timeline-activities.ts +++ b/packages/twenty-front/src/testing/mock-data/timeline-activities.ts @@ -1,4 +1,4 @@ -import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; +import { TimelineActivity } from '@/activities/timeline-activities/types/TimelineActivity'; export const mockedTimelineActivities: Array = [ { diff --git a/packages/twenty-front/vite.config.ts b/packages/twenty-front/vite.config.ts index 282f9d6dd202..a3a7af054b68 100644 --- a/packages/twenty-front/vite.config.ts +++ b/packages/twenty-front/vite.config.ts @@ -52,7 +52,7 @@ export default defineConfig(({ command, mode }) => { if (VITE_DISABLE_ESLINT_CHECKER !== 'true') { checkers['eslint'] = { lintCommand: - 'eslint . --report-unused-disable-directives --max-warnings 0 --config .eslintrc.cjs', + 'cd ../.. && eslint packages/twenty-front --report-unused-disable-directives --max-warnings 0 --config .eslintrc.cjs', }; } diff --git a/yarn.lock b/yarn.lock index 9d8eabefb6c3..475ba43b0b79 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17097,6 +17097,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/scope-manager@npm:8.10.0": + version: 8.10.0 + resolution: "@typescript-eslint/scope-manager@npm:8.10.0" + dependencies: + "@typescript-eslint/types": "npm:8.10.0" + "@typescript-eslint/visitor-keys": "npm:8.10.0" + checksum: 10c0/b8bb8635c4d6c00a3578d6265e3ee0f5d96d0c9dee534ed588aa411c3f4497fd71cce730c3ae7571e52453d955b191bc9edcc47c9af21a20c90e9a20f2371108 + languageName: node + linkType: hard + "@typescript-eslint/type-utils@npm:6.21.0": version: 6.21.0 resolution: "@typescript-eslint/type-utils@npm:6.21.0" @@ -17152,6 +17162,13 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/types@npm:8.10.0": + version: 8.10.0 + resolution: "@typescript-eslint/types@npm:8.10.0" + checksum: 10c0/f27dd43c8383e02e914a254257627e393dfc0f08b0f74a253c106813ae361f090271b2f3f2ef588fa3ca1329897d873da595bb5641fe8e3091b25eddca24b5d2 + languageName: node + linkType: hard + "@typescript-eslint/typescript-estree@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/typescript-estree@npm:5.62.0" @@ -17208,6 +17225,25 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/typescript-estree@npm:8.10.0": + version: 8.10.0 + resolution: "@typescript-eslint/typescript-estree@npm:8.10.0" + dependencies: + "@typescript-eslint/types": "npm:8.10.0" + "@typescript-eslint/visitor-keys": "npm:8.10.0" + debug: "npm:^4.3.4" + fast-glob: "npm:^3.3.2" + is-glob: "npm:^4.0.3" + minimatch: "npm:^9.0.4" + semver: "npm:^7.6.0" + ts-api-utils: "npm:^1.3.0" + peerDependenciesMeta: + typescript: + optional: true + checksum: 10c0/535a740fe25be0e28fe68c41e3264273d1e5169c9f938e08cc0e3415c357726f43efa44621960108c318fc3305c425d29f3223b6e731d44d67f84058a8947304 + languageName: node + linkType: hard + "@typescript-eslint/utils@npm:5.62.0, @typescript-eslint/utils@npm:^5.45.0": version: 5.62.0 resolution: "@typescript-eslint/utils@npm:5.62.0" @@ -17257,6 +17293,20 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/utils@npm:^8.8.0": + version: 8.10.0 + resolution: "@typescript-eslint/utils@npm:8.10.0" + dependencies: + "@eslint-community/eslint-utils": "npm:^4.4.0" + "@typescript-eslint/scope-manager": "npm:8.10.0" + "@typescript-eslint/types": "npm:8.10.0" + "@typescript-eslint/typescript-estree": "npm:8.10.0" + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 + checksum: 10c0/a21a2933517176abd00fcd5d8d80023e35dc3d89d5746bbac43790b4e984ab1f371117db08048bce7f42d54c64f4e0e35161149f8f34fd25a27bff9d1110fd16 + languageName: node + linkType: hard + "@typescript-eslint/visitor-keys@npm:5.62.0": version: 5.62.0 resolution: "@typescript-eslint/visitor-keys@npm:5.62.0" @@ -17287,6 +17337,16 @@ __metadata: languageName: node linkType: hard +"@typescript-eslint/visitor-keys@npm:8.10.0": + version: 8.10.0 + resolution: "@typescript-eslint/visitor-keys@npm:8.10.0" + dependencies: + "@typescript-eslint/types": "npm:8.10.0" + eslint-visitor-keys: "npm:^3.4.3" + checksum: 10c0/14721c4ac939640d5fd1ee1b6eeb07604b11a6017e319e21dcc71e7aac2992341fc7ae1992d977bad4433b6a1d0d1c0c279e6927316b26245f6e333f922fa458 + languageName: node + linkType: hard + "@ungap/structured-clone@npm:^1.2.0": version: 1.2.0 resolution: "@ungap/structured-clone@npm:1.2.0" @@ -22284,6 +22344,19 @@ __metadata: languageName: node linkType: hard +"comment-json@npm:^4.2.5": + version: 4.2.5 + resolution: "comment-json@npm:4.2.5" + dependencies: + array-timsort: "npm:^1.0.3" + core-util-is: "npm:^1.0.3" + esprima: "npm:^4.0.1" + has-own-prop: "npm:^2.0.0" + repeat-string: "npm:^1.6.1" + checksum: 10c0/e22f13f18fcc484ac33c8bc02a3d69c3f9467ae5063fdfb3df7735f83a8d9a2cab6a32b7d4a0c53123413a9577de8e17c8cc88369c433326799558febb34ef9c + languageName: node + linkType: hard + "common-ancestor-path@npm:^1.0.1": version: 1.0.1 resolution: "common-ancestor-path@npm:1.0.1" @@ -25551,6 +25624,19 @@ __metadata: languageName: node linkType: hard +"eslint-plugin-project-structure@npm:^3.7.2": + version: 3.7.2 + resolution: "eslint-plugin-project-structure@npm:3.7.2" + dependencies: + "@typescript-eslint/utils": "npm:^8.8.0" + comment-json: "npm:^4.2.5" + js-yaml: "npm:^4.1.0" + jsonschema: "npm:^1.4.1" + micromatch: "npm:^4.0.8" + checksum: 10c0/bb5d972cb2f24eceae0b5eefc7ccfaed1e0802977bc5ea33d3eb105521b590b5a69b36adcd3124ae67367af6b7798a05b17f06d085deaf9059e53b0839e3621e + languageName: node + linkType: hard + "eslint-plugin-react-hooks@npm:^4.5.0 || 5.0.0-canary-7118f5dd7-20230705": version: 5.0.0-canary-7118f5dd7-20230705 resolution: "eslint-plugin-react-hooks@npm:5.0.0-canary-7118f5dd7-20230705" @@ -32002,6 +32088,13 @@ __metadata: languageName: node linkType: hard +"jsonschema@npm:^1.4.1": + version: 1.4.1 + resolution: "jsonschema@npm:1.4.1" + checksum: 10c0/c3422d3fc7d33ff7234a806ffa909bb6fb5d1cd664bea229c64a1785dc04cbccd5fc76cf547c6ab6dd7881dbcaf3540a6a9f925a5956c61a9cd3e23a3c1796ef + languageName: node + linkType: hard + "jsonwebtoken@npm:9.0.2, jsonwebtoken@npm:^9.0.0": version: 9.0.2 resolution: "jsonwebtoken@npm:9.0.2" @@ -34581,6 +34674,16 @@ __metadata: languageName: node linkType: hard +"micromatch@npm:^4.0.8": + version: 4.0.8 + resolution: "micromatch@npm:4.0.8" + dependencies: + braces: "npm:^3.0.3" + picomatch: "npm:^2.3.1" + checksum: 10c0/166fa6eb926b9553f32ef81f5f531d27b4ce7da60e5baf8c021d043b27a388fb95e46a8038d5045877881e673f8134122b59624d5cecbd16eb50a42e7a6b5ca8 + languageName: node + linkType: hard + "microseconds@npm:0.2.0": version: 0.2.0 resolution: "microseconds@npm:0.2.0" @@ -44100,6 +44203,7 @@ __metadata: eslint-plugin-jsx-a11y: "npm:^6.8.0" eslint-plugin-prefer-arrow: "npm:^1.2.3" eslint-plugin-prettier: "npm:^5.1.2" + eslint-plugin-project-structure: "npm:^3.7.2" eslint-plugin-react: "npm:^7.33.2" eslint-plugin-react-hooks: "npm:^4.6.0" eslint-plugin-react-refresh: "npm:^0.4.4" From 44a843542c14e7f765114c20d3f2e700e63cf2d5 Mon Sep 17 00:00:00 2001 From: Manish Kr Prasad <85901005+Naprila@users.noreply.github.com> Date: Sun, 20 Oct 2024 23:51:23 +0530 Subject: [PATCH 32/67] Design a promotional poster for twenty. (#7896) ### Points: 300 ### Proof: Screenshot 2024-10-20 at 10 00 57 PM Co-authored-by: Apple --- .../1-design-promotional-poster-20-share.md | 1 + 1 file changed, 1 insertion(+) diff --git a/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md b/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md index 9f1f55ae767e..7bfb7d49382a 100644 --- a/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md +++ b/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md @@ -28,4 +28,5 @@ Your turn 👇 » 17-October-2024 by [Atharva Deshmukh](https://oss.gg/Atharva-3000) poster Link: [poster](https://x.com/0x_atharva/status/1846915861191577697) +» 20-October-2024 by [Naprila](https://oss.gg/Naprila) poster Link: [poster](https://x.com/mkprasad_821/status/1848037527921254625) --- From 8b5b0da77ff3199a91768491aeb4ea3dd787ec36 Mon Sep 17 00:00:00 2001 From: Manish Kr Prasad <85901005+Naprila@users.noreply.github.com> Date: Sun, 20 Oct 2024 23:53:24 +0530 Subject: [PATCH 33/67] Design/Create new Twenty logo, tweet your design. (#7892) ### Points: 300 ### Proof: Screenshot 2024-10-20 at 6 13 00 PM Co-authored-by: Apple --- oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md | 1 + 1 file changed, 1 insertion(+) diff --git a/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md b/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md index d67c49b64154..f29319349cbf 100644 --- a/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md +++ b/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md @@ -28,5 +28,6 @@ Your turn 👇 » 17-October-2024 by [shlok-py](https://oss.gg/shlok-py) Logo Link: [logo](https://drive.google.com/file/d/1BakHRLJul6DcNbLyeOXgJO9Ap4DpUxO9/view?usp=sharing) » tweet Link: [tweet](https://x.com/koirala_shlok/status/1846910669658247201) +» 20-October-2024 by [Naprila](https://oss.gg/Naprila) Logo Link: [logo](https://drive.google.com/file/d/105fWXNtOkOPkU31AV0FDZKOdrJ8XLwBb/view?usp=drivesdk) » tweet Link: [tweet](https://x.com/mkprasad_821/status/1847978789713695133) --- From b134f62da79a56e7a71407fad9abec83f75f67db Mon Sep 17 00:00:00 2001 From: Manish Kr Prasad <85901005+Naprila@users.noreply.github.com> Date: Sun, 20 Oct 2024 23:55:24 +0530 Subject: [PATCH 34/67] oss.gg: Created a gif about twenty and uploaded to Giphy (#7884) ### Points 150 ### Proof Link to tweet: https://x.com/mkprasad_821/status/1847917157956419690 Link to giphy: https://giphy.com/gifs/uiTAwFJ0BWQsQb7jbM Screenshot 2024-10-20 at 1 49 43 PM Co-authored-by: Apple --- oss-gg/twenty-side-quest/5-gif-magic.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/oss-gg/twenty-side-quest/5-gif-magic.md b/oss-gg/twenty-side-quest/5-gif-magic.md index b55bc24fc838..eab5d578430b 100644 --- a/oss-gg/twenty-side-quest/5-gif-magic.md +++ b/oss-gg/twenty-side-quest/5-gif-magic.md @@ -37,4 +37,7 @@ Your turn 👇 » 20-October-2024 by Satesh Charan » Link to gif: https://giphy.com/gifs/rXjvGBrTqu7vvhEsvR + +» 20-October-2024 by Naprila +» Link to gif: https://giphy.com/gifs/uiTAwFJ0BWQsQb7jbM --- From a09c5280ee5e2f21fa0a7005cb22fe93178cf841 Mon Sep 17 00:00:00 2001 From: sateshcharan Date: Sun, 20 Oct 2024 23:56:17 +0530 Subject: [PATCH 35/67] oss.gg - Side quest meme magic completed (#7879) ![image](https://github.com/user-attachments/assets/c2d52346-4fdb-49de-8b70-e3a1da3a7521) --------- Co-authored-by: Charles Bochet --- oss-gg/twenty-side-quest/4-meme-magic.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/oss-gg/twenty-side-quest/4-meme-magic.md b/oss-gg/twenty-side-quest/4-meme-magic.md index feacc0b857a7..7aa0a3b4a0a1 100644 --- a/oss-gg/twenty-side-quest/4-meme-magic.md +++ b/oss-gg/twenty-side-quest/4-meme-magic.md @@ -35,6 +35,11 @@ Your turn 👇 » 16-October-2024 by Harsh Bhat » Link to Tweet: https://x.com/HarshBhatX/status/1844698253104709899 + +» 20-October-2024 by Satesh Charan +» Link to Tweet: https://x.com/sateshcharans/status/1847760124267389357 + » 20-October-2024 by Naprila » Link to Tweet: https://x.com/mkprasad_821/status/1847900277510123706 + --- From f27b1169a1b0b64783f68862ad70c9c93e21e0e7 Mon Sep 17 00:00:00 2001 From: Poorvi Bajpai <150348534+poorvibajpai@users.noreply.github.com> Date: Sun, 20 Oct 2024 23:57:53 +0530 Subject: [PATCH 36/67] meme-magic #7875 created (#7878) Completed the sidequest of meme magic Co-authored-by: Charles Bochet --- oss-gg/twenty-side-quest/4-meme-magic.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oss-gg/twenty-side-quest/4-meme-magic.md b/oss-gg/twenty-side-quest/4-meme-magic.md index 7aa0a3b4a0a1..eb631acce03e 100644 --- a/oss-gg/twenty-side-quest/4-meme-magic.md +++ b/oss-gg/twenty-side-quest/4-meme-magic.md @@ -35,6 +35,8 @@ Your turn 👇 » 16-October-2024 by Harsh Bhat » Link to Tweet: https://x.com/HarshBhatX/status/1844698253104709899 +» 20-October-2024 by Poorvi Bajpai +» Link to Tweet: https://x.com/poorvi_bajpai/status/1847881362038308992 » 20-October-2024 by Satesh Charan » Link to Tweet: https://x.com/sateshcharans/status/1847760124267389357 From 35bb1a82bab5890d63ead458ec16c8ac31fb5edb Mon Sep 17 00:00:00 2001 From: Manish Kr Prasad <85901005+Naprila@users.noreply.github.com> Date: Sun, 20 Oct 2024 23:58:06 +0530 Subject: [PATCH 37/67] side quest: Like & Re-Tweet oss.gg Launch Tweet (#7877) Description: Liked & Tweeted @twentycrm on X Points: 50 Proof: Link: https://x.com/mkprasad_821/status/1847886807314120762 Screenshot 2024-10-20 at 11 56 54 AM --------- Co-authored-by: Apple --- oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md b/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md index a0e1421bfd99..7121fb7da638 100644 --- a/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md +++ b/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md @@ -46,3 +46,6 @@ Your turn 👇 » 16-October-2024 by Harsh Bhat » Link to Tweet: https://x.com/HarshBhatX/status/1846252536241508392 + +» 20-October-2024 by Naprila +» Link to Tweet: https://x.com/mkprasad_821/status/1847886807314120762 From cc4b060932ce6c05531c35b2ff7a74931203fdc0 Mon Sep 17 00:00:00 2001 From: BOHEUS <56270748+BOHEUS@users.noreply.github.com> Date: Sun, 20 Oct 2024 21:09:28 +0000 Subject: [PATCH 38/67] Typos in docs (#7898) --- .../developers/self-hosting/cloud-providers.mdx | 2 +- .../developers/self-hosting/self-hosting-var.mdx | 14 +++++++------- .../developers/self-hosting/upgrade-guide.mdx | 4 ++-- .../src/content/user-guide/objects/fields.mdx | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/twenty-website/src/content/developers/self-hosting/cloud-providers.mdx b/packages/twenty-website/src/content/developers/self-hosting/cloud-providers.mdx index 08e541a21a19..38cddead7ce9 100644 --- a/packages/twenty-website/src/content/developers/self-hosting/cloud-providers.mdx +++ b/packages/twenty-website/src/content/developers/self-hosting/cloud-providers.mdx @@ -11,7 +11,7 @@ This document is maintained by the community. It might contain issues. ## Kubernetes via Terraform and Manifests -Community-led documentation for Kubernetes deployment is available (here)[https://github.com/twentyhq/twenty/tree/main/packages/twenty-docker/k8s] +Community-led documentation for Kubernetes deployment is available [here](https://github.com/twentyhq/twenty/tree/main/packages/twenty-docker/k8s) ## Render diff --git a/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx b/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx index d679096f80a7..18a5ecd4ac85 100644 --- a/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx +++ b/packages/twenty-website/src/content/developers/self-hosting/self-hosting-var.mdx @@ -60,7 +60,7 @@ yarn command:prod cron:calendar:calendar-event-list-fetch ['REFRESH_TOKEN_COOL_DOWN', '1m', 'Refresh token cooldown'], ['FILE_TOKEN_SECRET', '', 'Secret used for the file tokens'], ['FILE_TOKEN_EXPIRES_IN', '1d', 'File token expiration time'], - ['API_TOKEN_EXPIRES_IN', '1000y', 'Api token expiration time'], + ['API_TOKEN_EXPIRES_IN', '1000y', 'API token expiration time'], ]}> ### Auth @@ -91,10 +91,10 @@ yarn command:prod cron:calendar:calendar-event-list-fetch ['EMAIL_FROM_NAME', 'John from YourDomain', 'Global name From: header used to send emails'], ['EMAIL_SYSTEM_ADDRESS', 'system@yourdomain.com', 'Email address used as a destination to send internal system notification'], ['EMAIL_DRIVER', 'logger', "Email driver: 'logger' (to log emails in console) or 'smtp'"], - ['EMAIL_SMTP_HOST', '', 'Email Smtp Host'], - ['EMAIL_SMTP_PORT', '', 'Email Smtp Port'], - ['EMAIL_SMTP_USER', '', 'Email Smtp User'], - ['EMAIL_SMTP_PASSWORD', '', 'Email Smtp Password'], + ['EMAIL_SMTP_HOST', '', 'Email SMTP Host'], + ['EMAIL_SMTP_PORT', '', 'Email SMTP Port'], + ['EMAIL_SMTP_USER', '', 'Email SMTP User'], + ['EMAIL_SMTP_PASSWORD', '', 'Email SMTP Password'], ]}> #### Email SMTP Server configuration examples @@ -143,7 +143,7 @@ yarn command:prod cron:calendar:calendar-event-list-fetch ['STORAGE_S3_ENDPOINT', '', 'Use if a different Endpoint is needed (for example Google)'], ['STORAGE_S3_ACCESS_KEY_ID', '', 'Optional depending on the authentication method'], ['STORAGE_S3_SECRET_ACCESS_KEY', '', 'Optional depending on the authentication method'], - ['STORAGE_LOCAL_PATH', '.local-storage', 'data path (local storage)'], + ['STORAGE_LOCAL_PATH', '.local-storage', 'Data path (local storage)'], ]}> ### Custom Code Execution @@ -201,7 +201,7 @@ This feature is WIP and is not yet useful for most users. ', 'Suport chat key'], + ['SUPPORT_FRONT_HMAC_KEY', '', 'Support chat key'], ['SUPPORT_FRONT_CHAT_ID', '', 'Support chat id'], ]}> diff --git a/packages/twenty-website/src/content/developers/self-hosting/upgrade-guide.mdx b/packages/twenty-website/src/content/developers/self-hosting/upgrade-guide.mdx index 2379fb2fc42e..3b4ff6123188 100644 --- a/packages/twenty-website/src/content/developers/self-hosting/upgrade-guide.mdx +++ b/packages/twenty-website/src/content/developers/self-hosting/upgrade-guide.mdx @@ -84,7 +84,7 @@ yarn command:prod upgrade-0.30 ``` The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas) -The `yarn command:prod upgrade-30` takes care of the data migration of all workspaces. +The `yarn command:prod upgrade-0.30` takes care of the data migration of all workspaces. # v0.30.0 to v0.31.0 @@ -97,7 +97,7 @@ yarn command:prod upgrade-0.31 ``` The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas) -The `yarn command:prod upgrade-31` takes care of the data migration of all workspaces. +The `yarn command:prod upgrade-0.31` takes care of the data migration of all workspaces. # v0.31.0 to v0.32.0 diff --git a/packages/twenty-website/src/content/user-guide/objects/fields.mdx b/packages/twenty-website/src/content/user-guide/objects/fields.mdx index 69f7c4a961df..7039db1d6095 100644 --- a/packages/twenty-website/src/content/user-guide/objects/fields.mdx +++ b/packages/twenty-website/src/content/user-guide/objects/fields.mdx @@ -72,7 +72,7 @@ Here's how you can do it: 2. To the right of the line, three vertically aligned dots symbolize a menu button. Click on this to unveil a dropdown list of options. -3. In the dropdown menu, find and click on the "deactivate" option. +3. In the dropdown menu, find and click on the "Deactivate" option. From d6810c3b428b6a9a7a035c681f35333c50d6a74f Mon Sep 17 00:00:00 2001 From: Ngan Phan Date: Sun, 20 Oct 2024 23:33:59 -0700 Subject: [PATCH 39/67] fix: Custom fields lacks empty tag (#7777) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR fixes this issue #7250 --------- Co-authored-by: Félix Malfait --- .../utils/__tests__/isFieldValueEmpty.test.ts | 12 ++++++++++++ .../record-field/utils/isFieldValueEmpty.ts | 13 ++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueEmpty.test.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueEmpty.test.ts index 07761aef8822..db247716ab9a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueEmpty.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/__tests__/isFieldValueEmpty.test.ts @@ -46,6 +46,18 @@ describe('isFieldValueEmpty', () => { fieldValue: { foo: 'bar' }, }), ).toBe(false); + expect( + isFieldValueEmpty({ + fieldDefinition: relationFieldDefinition, + fieldValue: [], + }), + ).toBe(true); + expect( + isFieldValueEmpty({ + fieldDefinition: relationFieldDefinition, + fieldValue: [{ id: '123' }], + }), + ).toBe(false); }); it('should return correct value for select field', () => { diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts index 93ee5eaa5458..e8e3ebe3fb86 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/isFieldValueEmpty.ts @@ -1,4 +1,4 @@ -import { isString } from '@sniptt/guards'; +import { isArray, isNonEmptyArray, isString } from '@sniptt/guards'; import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; @@ -58,7 +58,6 @@ export const isFieldValueEmpty = ({ isFieldNumber(fieldDefinition) || isFieldRating(fieldDefinition) || isFieldBoolean(fieldDefinition) || - isFieldRelation(fieldDefinition) || isFieldRawJson(fieldDefinition) || isFieldRichText(fieldDefinition) || isFieldPosition(fieldDefinition) @@ -73,11 +72,19 @@ export const isFieldValueEmpty = ({ ); } + if (isFieldRelation(fieldDefinition)) { + if (isArray(fieldValue)) { + return !isNonEmptyArray(fieldValue); + } + return isValueEmpty(fieldValue); + } + if (isFieldMultiSelect(fieldDefinition) || isFieldArray(fieldDefinition)) { return ( !isFieldArrayValue(fieldValue) || !isFieldMultiSelectValue(fieldValue, selectOptionValues) || - !isDefined(fieldValue) + !isDefined(fieldValue) || + !isNonEmptyArray(fieldValue) ); } From ae1d53aa2943255af72a24d10dba035ae3b74083 Mon Sep 17 00:00:00 2001 From: Rajeev Dewangan <63413883+rajeevDewangan@users.noreply.github.com> Date: Mon, 21 Oct 2024 12:07:00 +0530 Subject: [PATCH 40/67] Write-a-blog-post-about-Twenty (#7902) What side quest are you solving: Write a blog post about Twenty Description: Shared my experience using Twenty in a detailed blog post Points: 750 Proof: link : https://open.substack.com/pub/rajeevdewangan/p/our-experience-using-twenty-an-open?r=4lly3x&utm_campaign=post&utm_medium=web&showWelcomeOnShare=true ![Screenshot 2024-10-21 103524](https://github.com/user-attachments/assets/9475c262-8c5e-4a74-b2c7-e690e72daba4) --- Write-a-blog-post-about-Twenty | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 Write-a-blog-post-about-Twenty diff --git a/Write-a-blog-post-about-Twenty b/Write-a-blog-post-about-Twenty new file mode 100644 index 000000000000..86325c0af708 --- /dev/null +++ b/Write-a-blog-post-about-Twenty @@ -0,0 +1,2 @@ +Description: +Shared my experience using Twenty in a detailed blog post From f3ec6a759fb17df5e89c485ebf74afb7b673e46b Mon Sep 17 00:00:00 2001 From: Bhavesh Mishra <69065938+thefool76@users.noreply.github.com> Date: Mon, 21 Oct 2024 12:11:19 +0530 Subject: [PATCH 41/67] OSS.GG Content creation challenge (#7859) I have the content creation challenge of twenty I have published a detailed Youtube walkthrough to Twenty Dashboard and Created a Blog on Hashnode about Twenty Crm with step by step guide to use Twenty. Below are the task links 1. Create a YouTube Video about Twenty showcasing a specific way to use Twenty effectively. Points: 750 [Watch here](https://youtu.be/KuAycGuW698?si=TyKGVyrydLzof2RI) 2. Write a blog post about sharing your experience using Twenty in a detailed format on any platform. Points: 750 [Click here](https://k5lo7h.hashnode.dev/twenty-crm-a-fresh-start-for-modern-businesses) Total Points - 1500 --- .../1-create-youtube-video-about-20.md | 4 +++- .../twenty-content-challenges/2-write-blog-post-about-20.md | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/oss-gg/twenty-content-challenges/1-create-youtube-video-about-20.md b/oss-gg/twenty-content-challenges/1-create-youtube-video-about-20.md index 455b5e35bae3..2f8d1d1553ed 100644 --- a/oss-gg/twenty-content-challenges/1-create-youtube-video-about-20.md +++ b/oss-gg/twenty-content-challenges/1-create-youtube-video-about-20.md @@ -18,4 +18,6 @@ Your turn 👇 » 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) YouTube Link: [YouTube](https://twenty.com/) ---- \ No newline at end of file +» 19-October-2024 by [Thefool76](https://oss.gg/thefool76) YouTube Link: [YouTube](https://youtu.be/KuAycGuW698?si=q-YxcukbbYuO8BWf) + +--- diff --git a/oss-gg/twenty-content-challenges/2-write-blog-post-about-20.md b/oss-gg/twenty-content-challenges/2-write-blog-post-about-20.md index a4c4e6bee944..ca36278ff3f6 100644 --- a/oss-gg/twenty-content-challenges/2-write-blog-post-about-20.md +++ b/oss-gg/twenty-content-challenges/2-write-blog-post-about-20.md @@ -18,4 +18,6 @@ Your turn 👇 » 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) blog Link: [blog](https://twenty.com/) ---- \ No newline at end of file +» 19-October-2024 by [Thefool76](https://oss.gg/thefool76) blog Link: [blog](https://k5lo7h.hashnode.dev/twenty-crm-a-fresh-start-for-modern-businesses) + +--- From fc6748de0a2abc941cfc6f882fab0908caa03d7a Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Mon, 21 Oct 2024 11:51:54 +0200 Subject: [PATCH 42/67] Add modal to confirm workflow draft version overriding (#7758) In this PR: - Allow the `` to take additional buttons to display between the cancel and the confirm buttons. - Create a modal that's displayed when the user tries wants to use a workflow version as draft while a draft version already exists. The displayed modal contains a link to the current draft version and a button to confirm the overriding of the current draft version. A demo: https://github.com/user-attachments/assets/6349f418-1b11-45b3-9f5e-061ca74c2966 Closes twentyhq/private-issues#114 --- .../modal/components/ConfirmationModal.tsx | 8 ++- ...OverrideWorkflowDraftConfirmationModal.tsx | 62 +++++++++++++++++++ .../RecordShowPageWorkflowVersionHeader.tsx | 33 ++++++---- ...rideWorkflowDraftConfirmationModalState.ts | 7 +++ 4 files changed, 96 insertions(+), 14 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/components/OverrideWorkflowDraftConfirmationModal.tsx create mode 100644 packages/twenty-front/src/modules/workflow/states/openOverrideWorkflowDraftConfirmationModalState.ts diff --git a/packages/twenty-front/src/modules/ui/layout/modal/components/ConfirmationModal.tsx b/packages/twenty-front/src/modules/ui/layout/modal/components/ConfirmationModal.tsx index 04a80a1c827a..7b75faec18e5 100644 --- a/packages/twenty-front/src/modules/ui/layout/modal/components/ConfirmationModal.tsx +++ b/packages/twenty-front/src/modules/ui/layout/modal/components/ConfirmationModal.tsx @@ -25,6 +25,7 @@ export type ConfirmationModalProps = { confirmationPlaceholder?: string; confirmationValue?: string; confirmButtonAccent?: ButtonAccent; + AdditionalButtons?: React.ReactNode; }; const StyledConfirmationModal = styled(Modal)` @@ -33,7 +34,8 @@ const StyledConfirmationModal = styled(Modal)` height: auto; `; -const StyledCenteredButton = styled(Button)` +export const StyledCenteredButton = styled(Button)` + box-sizing: border-box; justify-content: center; margin-top: ${({ theme }) => theme.spacing(2)}; `; @@ -68,6 +70,7 @@ export const ConfirmationModal = ({ confirmationValue, confirmationPlaceholder, confirmButtonAccent = 'danger', + AdditionalButtons, }: ConfirmationModalProps) => { const [inputConfirmationValue, setInputConfirmationValue] = useState(''); @@ -138,6 +141,9 @@ export const ConfirmationModal = ({ title="Cancel" fullWidth /> + + {AdditionalButtons} + ; +}) => { + const [ + openOverrideWorkflowDraftConfirmationModal, + setOpenOverrideWorkflowDraftConfirmationModal, + ] = useRecoilState(openOverrideWorkflowDraftConfirmationModalState); + + const { updateOneRecord: updateOneWorkflowVersion } = + useUpdateOneRecord({ + objectNameSingular: CoreObjectNameSingular.WorkflowVersion, + }); + + const handleOverrideDraft = async () => { + await updateOneWorkflowVersion({ + idToUpdate: draftWorkflowVersionId, + updateOneRecordInput: workflowVersionUpdateInput, + }); + }; + + return ( + <> + { + setOpenOverrideWorkflowDraftConfirmationModal(false); + }} + variant="secondary" + title="Go to Draft" + fullWidth + /> + } + /> + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx index 79cbe7c27f77..e796c4a88cae 100644 --- a/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx +++ b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx @@ -1,13 +1,15 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; -import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { Button } from '@/ui/input/button/components/Button'; +import { OverrideWorkflowDraftConfirmationModal } from '@/workflow/components/OverrideWorkflowDraftConfirmationModal'; import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion'; import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion'; import { useDeactivateWorkflowVersion } from '@/workflow/hooks/useDeactivateWorkflowVersion'; import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion'; +import { openOverrideWorkflowDraftConfirmationModalState } from '@/workflow/states/openOverrideWorkflowDraftConfirmationModalState'; import { Workflow, WorkflowVersion } from '@/workflow/types/Workflow'; +import { useSetRecoilState } from 'recoil'; import { IconPencil, IconPlayerStop, IconPower, isDefined } from 'twenty-ui'; export const RecordShowPageWorkflowVersionHeader = ({ @@ -46,6 +48,8 @@ export const RecordShowPageWorkflowVersionHeader = ({ skip: !isDefined(workflowVersion), limit: 1, }); + const draftWorkflowVersion: WorkflowVersion | undefined = + draftWorkflowVersions[0]; const showUseAsDraftButton = !loadingDraftWorkflowVersions && @@ -57,7 +61,7 @@ export const RecordShowPageWorkflowVersionHeader = ({ workflowVersionRelatedWorkflowQuery.record.lastPublishedVersionId; const hasAlreadyDraftVersion = - !loadingDraftWorkflowVersions && draftWorkflowVersions.length > 0; + !loadingDraftWorkflowVersions && isDefined(draftWorkflowVersion); const isWaitingForWorkflowVersion = !isDefined(workflowVersion); @@ -65,10 +69,9 @@ export const RecordShowPageWorkflowVersionHeader = ({ const { deactivateWorkflowVersion } = useDeactivateWorkflowVersion(); const { createNewWorkflowVersion } = useCreateNewWorkflowVersion(); - const { updateOneRecord: updateOneWorkflowVersion } = - useUpdateOneRecord({ - objectNameSingular: CoreObjectNameSingular.WorkflowVersion, - }); + const setOpenOverrideWorkflowDraftConfirmationModal = useSetRecoilState( + openOverrideWorkflowDraftConfirmationModalState, + ); return ( <> @@ -80,13 +83,7 @@ export const RecordShowPageWorkflowVersionHeader = ({ disabled={isWaitingForWorkflowVersion} onClick={async () => { if (hasAlreadyDraftVersion) { - await updateOneWorkflowVersion({ - idToUpdate: draftWorkflowVersions[0].id, - updateOneRecordInput: { - trigger: workflowVersion.trigger, - steps: workflowVersion.steps, - }, - }); + setOpenOverrideWorkflowDraftConfirmationModal(true); } else { await createNewWorkflowVersion({ workflowId: workflowVersion.workflow.id, @@ -125,6 +122,16 @@ export const RecordShowPageWorkflowVersionHeader = ({ }} /> ) : null} + + {isDefined(workflowVersion) && isDefined(draftWorkflowVersion) ? ( + + ) : null} ); }; diff --git a/packages/twenty-front/src/modules/workflow/states/openOverrideWorkflowDraftConfirmationModalState.ts b/packages/twenty-front/src/modules/workflow/states/openOverrideWorkflowDraftConfirmationModalState.ts new file mode 100644 index 000000000000..1320a964207d --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/states/openOverrideWorkflowDraftConfirmationModalState.ts @@ -0,0 +1,7 @@ +import { createState } from 'twenty-ui'; + +export const openOverrideWorkflowDraftConfirmationModalState = + createState({ + key: 'openOverrideWorkflowDraftConfirmationModalState', + defaultValue: false, + }); From b914182b78b40acbaa8cb10209cee8c91e57f0d2 Mon Sep 17 00:00:00 2001 From: shubham yadav <126192924+yadavshubham01@users.noreply.github.com> Date: Mon, 21 Oct 2024 15:32:14 +0530 Subject: [PATCH 43/67] Update workflows to optimize CI processes (#7828) This Pull Request addresses the need to optimize our Continuous Integration (CI) workflows for Playwright tests and release processes. The changes implemented aim to reduce unnecessary resource usage by conditionally executing jobs based on relevant file changes and Implement https://github.com/tj-actions/changed-files step ## Changes logs - Updated `ci-test-docker-compose.yaml , ci-chrome-extension.yaml ` to check for changed files before running tests. - Updated `ci-front.yaml , ci-utils.yaml , ci-website.yaml , ci-server.yaml` to check for changed files before running tests. - Enhanced `playwright.yml` to skip unnecessary tests based on file changes. --- .github/workflows/ci-chrome-extension.yaml | 24 +++- .github/workflows/ci-front.yaml | 134 ++++++++++++++++-- .github/workflows/ci-server.yaml | 43 ++++-- .github/workflows/ci-test-docker-compose.yaml | 14 +- .github/workflows/ci-utils.yaml | 14 ++ .github/workflows/ci-website.yaml | 25 +++- .github/workflows/playwright.yml.bak | 16 +++ 7 files changed, 239 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ci-chrome-extension.yaml b/.github/workflows/ci-chrome-extension.yaml index a78be9415597..071104d31cb7 100644 --- a/.github/workflows/ci-chrome-extension.yaml +++ b/.github/workflows/ci-chrome-extension.yaml @@ -3,13 +3,9 @@ on: push: branches: - main - paths: - - 'package.json' - - 'packages/twenty-chrome-extension/**' + pull_request: - paths: - - 'package.json' - - 'packages/twenty-chrome-extension/**' + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -26,7 +22,23 @@ jobs: with: access_token: ${{ github.token }} - uses: actions/checkout@v4 + + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + package.json + packages/twenty-chrome-extension/** + - name: Install dependencies + if: steps.changed-files.outputs.changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Chrome Extension / Run build + if: steps.changed-files.outputs.changed == 'true' run: npx nx build twenty-chrome-extension + + - name: Mark as Valid if No Changes + if: steps.changed-files.outputs.changed != 'true' + run: | + echo "No relevant changes detected. Marking as valid." diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml index 506a6dce0e5e..d0a8a4c360ed 100644 --- a/.github/workflows/ci-front.yaml +++ b/.github/workflows/ci-front.yaml @@ -3,15 +3,9 @@ on: push: branches: - main - paths: - - 'package.json' - - 'packages/twenty-front/**' - - 'packages/twenty-ui/**' + pull_request: - paths: - - 'package.json' - - 'packages/twenty-front/**' - - 'packages/twenty-ui/**' + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -29,20 +23,81 @@ jobs: access_token: ${{ github.token }} - name: Fetch local actions uses: actions/checkout@v4 + + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + package.json + packages/twenty-front/** + packages/twenty-ui/** + + - name: Skip if no relevant changes + if: steps.changed-files.outputs.any_changed == 'false' + run: echo "No relevant changes. Skipping CI." + - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Diagnostic disk space issue + if: steps.changed-files.outputs.any_changed == 'true' run: df -h - name: Front / Restore Storybook Task Cache + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:frontend tasks: storybook:build - name: Front / Write .env + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx reset:env twenty-front - name: Front / Build storybook + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx storybook:build twenty-front front-sb-test: + + runs-on: ci-8-cores + timeout-minutes: 60 + needs: front-sb-build + strategy: + matrix: + storybook_scope: [pages, modules] + env: + REACT_APP_SERVER_BASE_URL: http://localhost:3000 + NX_REJECT_UNKNOWN_LOCAL_CACHE: 0 + steps: + - name: Fetch local actions + uses: actions/checkout@v4 + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + packages/twenty-front/** + - name: Skip if no relevant changes + if: steps.changed-files.outputs.any_changed == 'false' + run: echo "No relevant changes. Skipping CI." + + - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' + uses: ./.github/workflows/actions/yarn-install + - name: Install Playwright + if: steps.changed-files.outputs.any_changed == 'true' + run: cd packages/twenty-front && npx playwright install + - name: Front / Restore Storybook Task Cache + if: steps.changed-files.outputs.any_changed == 'true' + uses: ./.github/workflows/actions/task-cache + with: + tag: scope:frontend + tasks: storybook:build + - name: Front / Write .env + if: steps.changed-files.outputs.any_changed == 'true' + run: npx nx reset:env twenty-front + - name: Run storybook tests + if: steps.changed-files.outputs.any_changed == 'true' + run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }} + front-sb-test-shipfox: runs-on: shipfox-8vcpu-ubuntu-2204 timeout-minutes: 60 needs: front-sb-build @@ -55,18 +110,35 @@ jobs: steps: - name: Fetch local actions uses: actions/checkout@v4 + + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + packages/twenty-front/** + + - name: Skip if no relevant changes + if: steps.changed-files.outputs.any_changed == 'false' + run: echo "No relevant changes. Skipping CI." + - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Install Playwright + if: steps.changed-files.outputs.any_changed == 'true' run: cd packages/twenty-front && npx playwright install - name: Front / Restore Storybook Task Cache + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:frontend tasks: storybook:build - name: Front / Write .env + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx reset:env twenty-front - name: Run storybook tests + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }} front-sb-test-performance: runs-on: shipfox-8vcpu-ubuntu-2204 @@ -77,13 +149,28 @@ jobs: steps: - name: Fetch local actions uses: actions/checkout@v4 + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + packages/twenty-front/** + + - name: Skip if no relevant changes + if: steps.changed-files.outputs.any_changed == 'false' + run: echo "No relevant changes. Skipping CI." + - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Install Playwright + if: steps.changed-files.outputs.any_changed == 'true' run: cd packages/twenty-front && npx playwright install - name: Front / Write .env + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx reset:env twenty-front - name: Run storybook tests + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx storybook:serve-and-test:static:performance twenty-front front-chromatic-deployment: if: contains(github.event.pull_request.labels.*.name, 'run-chromatic') || github.event_name == 'push' @@ -97,19 +184,35 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 + + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + packages/twenty-front/** + + - name: Skip if no relevant changes + if: steps.changed-files.outputs.any_changed == 'false' + run: echo "No relevant changes. Skipping CI." + - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Front / Restore Storybook Task Cache + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:frontend tasks: storybook:build - name: Front / Write .env + if: steps.changed-files.outputs.any_changed == 'true' run: | cd packages/twenty-front touch .env echo "REACT_APP_SERVER_BASE_URL: $REACT_APP_SERVER_BASE_URL" >> .env - name: Publish to Chromatic + if: steps.changed-files.outputs.any_changed == 'true' run: npx nx run twenty-front:chromatic:ci front-task: runs-on: ubuntu-latest @@ -127,19 +230,34 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + packages/twenty-front/** + + - name: Skip if no relevant changes + if: steps.changed-files.outputs.any_changed == 'false' + run: echo "No relevant changes. Skipping CI." + - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Front / Restore ${{ matrix.task }} task cache + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:frontend tasks: ${{ matrix.task }} - name: Reset .env + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/nx-affected with: tag: scope:frontend tasks: reset:env - name: Run ${{ matrix.task }} task + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/nx-affected with: tag: scope:frontend diff --git a/.github/workflows/ci-server.yaml b/.github/workflows/ci-server.yaml index 074d63fdda40..101e1df3b47f 100644 --- a/.github/workflows/ci-server.yaml +++ b/.github/workflows/ci-server.yaml @@ -3,15 +3,9 @@ on: push: branches: - main - paths: - - 'package.json' - - 'packages/twenty-server/**' - - 'packages/twenty-emails/**' + pull_request: - paths: - - 'package.json' - - 'packages/twenty-server/**' - - 'packages/twenty-emails/**' + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -38,22 +32,35 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: 'package.json, packages/twenty-server/**, packages/twenty-emails/**' + - name: Install dependencies + if: steps.changed-files.outputs.changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Server / Restore Task Cache + if: steps.changed-files.outputs.changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:backend - name: Server / Run lint & typecheck + if: steps.changed-files.outputs.changed == 'true' uses: ./.github/workflows/actions/nx-affected with: tag: scope:backend tasks: lint,typecheck - name: Server / Build + if: steps.changed-files.outputs.changed == 'true' run: npx nx build twenty-server - name: Server / Write .env + if: steps.changed-files.outputs.changed == 'true' run: npx nx reset:env twenty-server - name: Worker / Run + if: steps.changed-files.outputs.changed == 'true' run: npx nx run twenty-server:worker:ci server-test: @@ -66,13 +73,23 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: 'package.json, packages/twenty-server/**, packages/twenty-emails/**' + - name: Install dependencies + if: steps.changed-files.outputs.changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Server / Restore Task Cache + if: steps.changed-files.outputs.changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:backend - name: Server / Run Tests + if: steps.changed-files.outputs.changed == 'true' uses: ./.github/workflows/actions/nx-affected with: tag: scope:backend @@ -100,13 +117,23 @@ jobs: uses: actions/checkout@v4 with: fetch-depth: 0 + + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: 'package.json, packages/twenty-server/**, packages/twenty-emails/**' + - name: Install dependencies + if: steps.changed-files.outputs.changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Server / Restore Task Cache + if: steps.changed-files.outputs.changed == 'true' uses: ./.github/workflows/actions/task-cache with: tag: scope:backend - name: Server / Run Integration Tests + if: steps.changed-files.outputs.changed == 'true' uses: ./.github/workflows/actions/nx-affected with: tag: scope:backend diff --git a/.github/workflows/ci-test-docker-compose.yaml b/.github/workflows/ci-test-docker-compose.yaml index 1496425c8511..2ff08a9e17f6 100644 --- a/.github/workflows/ci-test-docker-compose.yaml +++ b/.github/workflows/ci-test-docker-compose.yaml @@ -1,8 +1,7 @@ name: 'Test Docker Compose' on: pull_request: - paths: - - 'packages/twenty-docker/**' + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -13,8 +12,19 @@ jobs: steps: - name: Checkout uses: actions/checkout@v2 + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + packages/twenty-docker/** + docker-compose.yml + - name: Skip if no relevant changes + if: steps.changed-files.outputs.any_changed != 'true' + run: echo "No relevant changes detected. Marking as valid." - name: Run compose + if: steps.changed-files.outputs.any_changed == 'true' run: | echo "Patching docker-compose.yml..." # change image to localbuild using yq diff --git a/.github/workflows/ci-utils.yaml b/.github/workflows/ci-utils.yaml index fccfca98d8ab..6cbd99b28803 100644 --- a/.github/workflows/ci-utils.yaml +++ b/.github/workflows/ci-utils.yaml @@ -23,9 +23,16 @@ jobs: if: github.event.action != 'closed' steps: - uses: actions/checkout@v4 + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: 'packages/twenty-utils/**' - name: Install dependencies + if: steps.changed-files.outputs.changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Utils / Run Danger.js + if: steps.changed-files.outputs.changed == 'true' run: cd packages/twenty-utils && npx nx danger:ci env: DANGER_GITHUB_API_TOKEN: ${{ github.token }} @@ -35,9 +42,16 @@ jobs: if: github.event.action == 'closed' && github.event.pull_request.merged == true steps: - uses: actions/checkout@v4 + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: 'packages/twenty-utils/**' - name: Install dependencies + if: steps.changed-files.outputs.changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Run congratulate-dangerfile.js + if: steps.changed-files.outputs.changed == 'true' run: cd packages/twenty-utils && npx nx danger:congratulate env: DANGER_GITHUB_API_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci-website.yaml b/.github/workflows/ci-website.yaml index d79345f3bf53..ce39d66ca79b 100644 --- a/.github/workflows/ci-website.yaml +++ b/.github/workflows/ci-website.yaml @@ -3,13 +3,10 @@ on: push: branches: - main - paths: - - 'package.json' - - 'packages/twenty-website/**' + pull_request: - paths: - - 'package.json' - - 'packages/twenty-website/**' + + concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true @@ -27,13 +24,27 @@ jobs: - 5432:5432 steps: - uses: actions/checkout@v4 + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: 'package.json, packages/twenty-website/**' + - name: Install dependencies + if: steps.changed-files.outputs.changed == 'true' uses: ./.github/workflows/actions/yarn-install + - name: Website / Run migrations + if: steps.changed-files.outputs.changed == 'true' run: npx nx database:migrate twenty-website env: DATABASE_PG_URL: postgres://twenty:twenty@localhost:5432/default - name: Website / Build Website + if: steps.changed-files.outputs.changed == 'true' run: npx nx build twenty-website env: - DATABASE_PG_URL: postgres://twenty:twenty@localhost:5432/default \ No newline at end of file + DATABASE_PG_URL: postgres://twenty:twenty@localhost:5432/default + + - name: Mark as VALID + if: steps.changed-files.outputs.changed != 'true' # If no changes, mark as valid + run: echo "No relevant changes detected. CI is valid." \ No newline at end of file diff --git a/.github/workflows/playwright.yml.bak b/.github/workflows/playwright.yml.bak index cffb50287629..fc45955bc7ab 100644 --- a/.github/workflows/playwright.yml.bak +++ b/.github/workflows/playwright.yml.bak @@ -13,11 +13,27 @@ jobs: - uses: actions/setup-node@v4 with: node-version: lts/* + + - name: Check for changed files + id: changed-files + uses: tj-actions/changed-files@v11 + with: + files: | + packages/** # Adjust this to your relevant directories + playwright.config.ts # Include any relevant config files + + - name: Skip if no relevant changes + if: steps.changed-files.outputs.any_changed != 'true' + run: echo "No relevant changes detected. Marking as valid." + - name: Install dependencies + if: steps.changed-files.outputs.any_changed == 'true' run: npm install -g yarn && yarn - name: Install Playwright Browsers + if: steps.changed-files.outputs.any_changed == 'true' run: yarn playwright install --with-deps - name: Run Playwright tests + if: steps.changed-files.outputs.any_changed == 'true' run: yarn test:e2e companies - uses: actions/upload-artifact@v4 if: always() From e7eeb3b8201142ca963e717bb2f0659a3ab94795 Mon Sep 17 00:00:00 2001 From: Baptiste Devessier Date: Mon, 21 Oct 2024 12:04:44 +0200 Subject: [PATCH 44/67] Add Workflow Run show page (#7719) In this PR: - Display a workflow version visualizer for the version of the workflow the run was executed on. - Display the output of the run as code. https://github.com/user-attachments/assets/d617300a-bff4-4328-a35c-291dc86d81cf --- .../hooks/useRecordShowContainerTabs.ts | 29 ++- .../SettingsServerlessFunctionCodeEditor.tsx | 130 ++++++++++++++ ...sServerlessFunctionCodeEditorContainer.tsx | 11 ++ ...ettingsServerlessFunctionCodeEditorTab.tsx | 13 +- .../SettingsServerlessFunctionTestTab.tsx | 44 ++--- .../code-editor/components/CodeEditor.tsx | 165 ++++-------------- .../components/ShowPageSubContainer.tsx | 10 ++ .../WorkflowRunOutputVisualizer.tsx | 32 ++++ .../WorkflowRunVersionVisualizer.tsx | 29 +++ .../modules/workflow/hooks/useWorkflowRun.tsx | 16 ++ .../src/modules/workflow/types/Workflow.ts | 22 +++ .../display/icon/components/TablerIcons.ts | 1 + 12 files changed, 334 insertions(+), 168 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditor.tsx create mode 100644 packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditorContainer.tsx create mode 100644 packages/twenty-front/src/modules/workflow/components/WorkflowRunOutputVisualizer.tsx create mode 100644 packages/twenty-front/src/modules/workflow/components/WorkflowRunVersionVisualizer.tsx create mode 100644 packages/twenty-front/src/modules/workflow/hooks/useWorkflowRun.tsx diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts index 1d029f4877f8..a1d6a81d2e2f 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts +++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowContainerTabs.ts @@ -8,6 +8,7 @@ import { IconMail, IconNotes, IconPaperclip, + IconPrinter, IconSettings, IconTimelineEvent, } from 'twenty-ui'; @@ -26,6 +27,10 @@ export const useRecordShowContainerTabs = ( const isWorkflowVersion = isWorkflowEnabled && targetObjectNameSingular === CoreObjectNameSingular.WorkflowVersion; + const isWorkflowRun = + isWorkflowEnabled && + targetObjectNameSingular === CoreObjectNameSingular.WorkflowRun; + const isWorkflowRelated = isWorkflow || isWorkflowVersion || isWorkflowRun; const isCompanyOrPerson = [ CoreObjectNameSingular.Company, @@ -54,7 +59,7 @@ export const useRecordShowContainerTabs = ( id: 'timeline', title: 'Timeline', Icon: IconTimelineEvent, - hide: isInRightDrawer || isWorkflow || isWorkflowVersion, + hide: isInRightDrawer || isWorkflowRelated, }, { id: 'tasks', @@ -63,8 +68,7 @@ export const useRecordShowContainerTabs = ( hide: targetObjectNameSingular === CoreObjectNameSingular.Note || targetObjectNameSingular === CoreObjectNameSingular.Task || - isWorkflow || - isWorkflowVersion, + isWorkflowRelated, }, { id: 'notes', @@ -73,14 +77,13 @@ export const useRecordShowContainerTabs = ( hide: targetObjectNameSingular === CoreObjectNameSingular.Note || targetObjectNameSingular === CoreObjectNameSingular.Task || - isWorkflow || - isWorkflowVersion, + isWorkflowRelated, }, { id: 'files', title: 'Files', Icon: IconPaperclip, - hide: isWorkflow || isWorkflowVersion, + hide: isWorkflowRelated, }, { id: 'emails', @@ -102,9 +105,21 @@ export const useRecordShowContainerTabs = ( }, { id: 'workflowVersion', - title: 'Workflow Version', + title: 'Flow', Icon: IconSettings, hide: !isWorkflowVersion, }, + { + id: 'workflowRunOutput', + title: 'Output', + Icon: IconPrinter, + hide: !isWorkflowRun, + }, + { + id: 'workflowRunFlow', + title: 'Flow', + Icon: IconSettings, + hide: !isWorkflowRun, + }, ]; }; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditor.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditor.tsx new file mode 100644 index 000000000000..7641d8f46d38 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditor.tsx @@ -0,0 +1,130 @@ +import { SettingsServerlessFunctionCodeEditorContainer } from '@/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditorContainer'; +import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages'; +import { CodeEditor } from '@/ui/input/code-editor/components/CodeEditor'; +import { EditorProps, Monaco } from '@monaco-editor/react'; +import dotenv from 'dotenv'; +import { editor, MarkerSeverity } from 'monaco-editor'; +import { AutoTypings } from 'monaco-editor-auto-typings'; +import { isDefined } from '~/utils/isDefined'; + +export type File = { + language: string; + content: string; + path: string; +}; + +type SettingsServerlessFunctionCodeEditorProps = Omit< + EditorProps, + 'onChange' +> & { + currentFilePath: string; + files: File[]; + onChange: (value: string) => void; + setIsCodeValid: (isCodeValid: boolean) => void; +}; + +export const SettingsServerlessFunctionCodeEditor = ({ + currentFilePath, + files, + onChange, + setIsCodeValid, + height = 450, + options = undefined, +}: SettingsServerlessFunctionCodeEditorProps) => { + const { availablePackages } = useGetAvailablePackages(); + + const currentFile = files.find((file) => file.path === currentFilePath); + const environmentVariablesFile = files.find((file) => file.path === '.env'); + + const handleEditorDidMount = async ( + editor: editor.IStandaloneCodeEditor, + monaco: Monaco, + ) => { + if (files.length > 1) { + files.forEach((file) => { + const model = monaco.editor.getModel(monaco.Uri.file(file.path)); + if (!isDefined(model)) { + monaco.editor.createModel( + file.content, + file.language, + monaco.Uri.file(file.path), + ); + } + }); + + monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ + ...monaco.languages.typescript.typescriptDefaults.getCompilerOptions(), + moduleResolution: + monaco.languages.typescript.ModuleResolutionKind.NodeJs, + baseUrl: 'file:///src', + paths: { + 'src/*': ['file:///src/*'], + }, + allowSyntheticDefaultImports: true, + esModuleInterop: true, + noEmit: true, + target: monaco.languages.typescript.ScriptTarget.ESNext, + }); + + if (isDefined(environmentVariablesFile)) { + const environmentVariables = dotenv.parse( + environmentVariablesFile.content, + ); + + const environmentDefinition = ` + declare namespace NodeJS { + interface ProcessEnv { + ${Object.keys(environmentVariables) + .map((key) => `${key}: string;`) + .join('\n')} + } + } + + declare const process: { + env: NodeJS.ProcessEnv; + }; + `; + + monaco.languages.typescript.typescriptDefaults.addExtraLib( + environmentDefinition, + 'ts:process-env.d.ts', + ); + } + + await AutoTypings.create(editor, { + monaco, + preloadPackages: true, + onlySpecifiedPackages: true, + versions: availablePackages, + debounceDuration: 0, + }); + } + }; + + const handleEditorValidation = (markers: editor.IMarker[]) => { + for (const marker of markers) { + if (marker.severity === MarkerSeverity.Error) { + setIsCodeValid?.(false); + return; + } + } + setIsCodeValid?.(true); + }; + + return ( + isDefined(currentFile) && + isDefined(availablePackages) && ( + + + + ) + ); +}; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditorContainer.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditorContainer.tsx new file mode 100644 index 000000000000..4ad8afaee743 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditorContainer.tsx @@ -0,0 +1,11 @@ +import styled from '@emotion/styled'; + +const StyledEditorContainer = styled.div` + border: 1px solid ${({ theme }) => theme.border.color.medium}; + border-top: none; + border-radius: 0 0 ${({ theme }) => theme.border.radius.sm} + ${({ theme }) => theme.border.radius.sm}; +`; + +export const SettingsServerlessFunctionCodeEditorContainer = + StyledEditorContainer; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx index 5f8886871359..c1131c1b66f5 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionCodeEditorTab.tsx @@ -1,20 +1,23 @@ +import { + File, + SettingsServerlessFunctionCodeEditor, +} from '@/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditor'; +import { SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/settings/serverless-functions/constants/SettingsServerlessFunctionTabListComponentId'; import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { Button } from '@/ui/input/button/components/Button'; -import { CodeEditor, File } from '@/ui/input/code-editor/components/CodeEditor'; import { CoreEditorHeader } from '@/ui/input/code-editor/components/CodeEditorHeader'; import { Section } from '@/ui/layout/section/components/Section'; import { TabList } from '@/ui/layout/tab/components/TabList'; +import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import styled from '@emotion/styled'; import { useNavigate } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; import { Key } from 'ts-key-enum'; import { H2Title, IconGitCommit, IconPlayerPlay, IconRestore } from 'twenty-ui'; import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount'; -import { SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/settings/serverless-functions/constants/SettingsServerlessFunctionTabListComponentId'; -import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; -import { useRecoilValue } from 'recoil'; const StyledTabList = styled(TabList)` border-bottom: none; @@ -107,7 +110,7 @@ export const SettingsServerlessFunctionCodeEditorTab = ({ rightNodes={[ResetButton, PublishButton, TestButton]} /> {activeTabId && ( - onChange(activeTabId, newCodeValue)} diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab.tsx index b2d54cbc03f9..54a565215d4b 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTestTab.tsx @@ -2,6 +2,7 @@ import { Section } from '@/ui/layout/section/components/Section'; import { H2Title, IconPlayerPlay } from 'twenty-ui'; import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton'; +import { SettingsServerlessFunctionCodeEditorContainer } from '@/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditorContainer'; import { SettingsServerlessFunctionsOutputMetadataInfo } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsOutputMetadataInfo'; import { settingsServerlessFunctionCodeEditorOutputParamsState } from '@/settings/serverless-functions/states/settingsServerlessFunctionCodeEditorOutputParamsState'; import { settingsServerlessFunctionInputState } from '@/settings/serverless-functions/states/settingsServerlessFunctionInputState'; @@ -78,37 +79,30 @@ export const SettingsServerlessFunctionTestTab = ({ />, ]} /> - + + +
]} rightNodes={[]} /> - + + +
diff --git a/packages/twenty-front/src/modules/ui/input/code-editor/components/CodeEditor.tsx b/packages/twenty-front/src/modules/ui/input/code-editor/components/CodeEditor.tsx index 723b04a9f69b..dc846b9c0834 100644 --- a/packages/twenty-front/src/modules/ui/input/code-editor/components/CodeEditor.tsx +++ b/packages/twenty-front/src/modules/ui/input/code-editor/components/CodeEditor.tsx @@ -1,148 +1,51 @@ -import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages'; import { codeEditorTheme } from '@/ui/input/code-editor/utils/codeEditorTheme'; import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import Editor, { EditorProps, Monaco } from '@monaco-editor/react'; -import dotenv from 'dotenv'; -import { MarkerSeverity, editor } from 'monaco-editor'; -import { AutoTypings } from 'monaco-editor-auto-typings'; -import { isDefined } from '~/utils/isDefined'; - -const StyledEditor = styled(Editor)` - border: 1px solid ${({ theme }) => theme.border.color.medium}; - border-top: none; - border-radius: 0 0 ${({ theme }) => theme.border.radius.sm} - ${({ theme }) => theme.border.radius.sm}; -`; - -export type File = { - language: string; - content: string; - path: string; -}; +import Editor, { EditorProps } from '@monaco-editor/react'; +import { isDefined } from 'twenty-ui'; type CodeEditorProps = Omit & { - currentFilePath: string; - files: File[]; onChange?: (value: string) => void; - setIsCodeValid?: (isCodeValid: boolean) => void; }; export const CodeEditor = ({ - currentFilePath, - files, + value, + language, + onMount, onChange, - setIsCodeValid, + onValidate, height = 450, - options = undefined, + options, }: CodeEditorProps) => { const theme = useTheme(); - const { availablePackages } = useGetAvailablePackages(); - - const currentFile = files.find((file) => file.path === currentFilePath); - const environmentVariablesFile = files.find((file) => file.path === '.env'); - - const handleEditorDidMount = async ( - editor: editor.IStandaloneCodeEditor, - monaco: Monaco, - ) => { - monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme)); - monaco.editor.setTheme('codeEditorTheme'); - - if (files.length > 1) { - files.forEach((file) => { - const model = monaco.editor.getModel(monaco.Uri.file(file.path)); - if (!isDefined(model)) { - monaco.editor.createModel( - file.content, - file.language, - monaco.Uri.file(file.path), - ); + return ( + { + monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme)); + monaco.editor.setTheme('codeEditorTheme'); + + onMount?.(editor, monaco); + }} + onChange={(value) => { + if (isDefined(value)) { + onChange?.(value); } - }); - - monaco.languages.typescript.typescriptDefaults.setCompilerOptions({ - ...monaco.languages.typescript.typescriptDefaults.getCompilerOptions(), - moduleResolution: - monaco.languages.typescript.ModuleResolutionKind.NodeJs, - baseUrl: 'file:///src', - paths: { - 'src/*': ['file:///src/*'], + }} + onValidate={onValidate} + options={{ + overviewRulerLanes: 0, + scrollbar: { + vertical: 'hidden', + horizontal: 'hidden', }, - allowSyntheticDefaultImports: true, - esModuleInterop: true, - noEmit: true, - target: monaco.languages.typescript.ScriptTarget.ESNext, - }); - - if (isDefined(environmentVariablesFile)) { - const environmentVariables = dotenv.parse( - environmentVariablesFile.content, - ); - - const environmentDefinition = ` - declare namespace NodeJS { - interface ProcessEnv { - ${Object.keys(environmentVariables) - .map((key) => `${key}: string;`) - .join('\n')} - } - } - - declare const process: { - env: NodeJS.ProcessEnv; - }; - `; - - monaco.languages.typescript.typescriptDefaults.addExtraLib( - environmentDefinition, - 'ts:process-env.d.ts', - ); - } - - await AutoTypings.create(editor, { - monaco, - preloadPackages: true, - onlySpecifiedPackages: true, - versions: availablePackages, - debounceDuration: 0, - }); - } - }; - - const handleEditorValidation = (markers: editor.IMarker[]) => { - for (const marker of markers) { - if (marker.severity === MarkerSeverity.Error) { - setIsCodeValid?.(false); - return; - } - } - setIsCodeValid?.(true); - }; - - return ( - isDefined(currentFile) && - isDefined(availablePackages) && ( - value && onChange?.(value)} - onValidate={handleEditorValidation} - options={{ - ...options, - overviewRulerLanes: 0, - scrollbar: { - vertical: 'hidden', - horizontal: 'hidden', - }, - minimap: { - enabled: false, - }, - }} - /> - ) + minimap: { + enabled: false, + }, + ...options, + }} + /> ); }; diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx index 3365a170e2a6..6f2c0aa442c9 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSubContainer.tsx @@ -18,6 +18,8 @@ import { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPage import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; +import { WorkflowRunOutputVisualizer } from '@/workflow/components/WorkflowRunOutputVisualizer'; +import { WorkflowRunVersionVisualizer } from '@/workflow/components/WorkflowRunVersionVisualizer'; import { WorkflowVersionVisualizer } from '@/workflow/components/WorkflowVersionVisualizer'; import { WorkflowVersionVisualizerEffect } from '@/workflow/components/WorkflowVersionVisualizerEffect'; import { WorkflowVisualizer } from '@/workflow/components/WorkflowVisualizer'; @@ -182,6 +184,14 @@ export const ShowPageSubContainer = ({ /> ); + case 'workflowRunFlow': + return ( + + ); + case 'workflowRunOutput': + return ( + + ); default: return <>; } diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowRunOutputVisualizer.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowRunOutputVisualizer.tsx new file mode 100644 index 000000000000..1a49c030ac60 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowRunOutputVisualizer.tsx @@ -0,0 +1,32 @@ +import { CodeEditor } from '@/ui/input/code-editor/components/CodeEditor'; +import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun'; +import styled from '@emotion/styled'; +import { isDefined } from 'twenty-ui'; + +const StyledSourceCodeContainer = styled.div` + border: 1px solid ${({ theme }) => theme.border.color.medium}; + border-radius: ${({ theme }) => theme.border.radius.sm}; + margin: ${({ theme }) => theme.spacing(4)}; + overflow: hidden; +`; + +export const WorkflowRunOutputVisualizer = ({ + workflowRunId, +}: { + workflowRunId: string; +}) => { + const workflowRun = useWorkflowRun({ workflowRunId }); + if (!isDefined(workflowRun)) { + return null; + } + + return ( + + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowRunVersionVisualizer.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowRunVersionVisualizer.tsx new file mode 100644 index 000000000000..8d8f265c426c --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowRunVersionVisualizer.tsx @@ -0,0 +1,29 @@ +import { WorkflowVersionVisualizer } from '@/workflow/components/WorkflowVersionVisualizer'; +import { WorkflowVersionVisualizerEffect } from '@/workflow/components/WorkflowVersionVisualizerEffect'; +import { useWorkflowRun } from '@/workflow/hooks/useWorkflowRun'; +import { isDefined } from 'twenty-ui'; + +export const WorkflowRunVersionVisualizer = ({ + workflowRunId, +}: { + workflowRunId: string; +}) => { + const workflowRun = useWorkflowRun({ + workflowRunId, + }); + if (!isDefined(workflowRun)) { + return null; + } + + return ( + <> + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/hooks/useWorkflowRun.tsx b/packages/twenty-front/src/modules/workflow/hooks/useWorkflowRun.tsx new file mode 100644 index 000000000000..9bb6fa5642ed --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/hooks/useWorkflowRun.tsx @@ -0,0 +1,16 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; +import { WorkflowRun } from '@/workflow/types/Workflow'; + +export const useWorkflowRun = ({ + workflowRunId, +}: { + workflowRunId: string; +}) => { + const { record } = useFindOneRecord({ + objectNameSingular: CoreObjectNameSingular.WorkflowRun, + objectRecordId: workflowRunId, + }); + + return record; +}; diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts index 65b2e9a25a15..70e3ab197020 100644 --- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts +++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts @@ -84,6 +84,28 @@ export type WorkflowVersion = { __typename: 'WorkflowVersion'; }; +type StepRunOutput = { + id: string; + name: string; + type: string; + outputs: { + attemptCount: number; + result: object | undefined; + error: string | undefined; + }[]; +}; + +export type WorkflowRunOutput = { + steps: Record; +}; + +export type WorkflowRun = { + __typename: 'WorkflowRun'; + id: string; + workflowVersionId: string; + output: WorkflowRunOutput; +}; + export type Workflow = { __typename: 'Workflow'; id: string; diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index ffef2fdeda79..5244849f69b1 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -215,6 +215,7 @@ export { IconTimelineEvent, IconTool, IconTrash, + IconPrinter, IconUnlink, IconUpload, IconUser, From a5b2b3522f6e5664554f403cb4a1c3fdf081b7ff Mon Sep 17 00:00:00 2001 From: Thomas des Francs Date: Mon, 21 Oct 2024 12:25:25 +0200 Subject: [PATCH 45/67] Updated image to correct typo (#7907) Fixes https://github.com/twentyhq/twenty/issues/7899 --- .../user-guide/fields/deactivate-field.png | Bin 152797 -> 186133 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/packages/twenty-website/public/images/user-guide/fields/deactivate-field.png b/packages/twenty-website/public/images/user-guide/fields/deactivate-field.png index 03d263622ae321ed347003152977240d015c8348..4b55672d219766453bb88412a8ff7e385c30bccb 100644 GIT binary patch literal 186133 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È^AK0pvZr-N|- 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

RKp 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

{O-Ta*1ik5l+{@H^(K?y|B2@~HQ_ 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 literal 152797 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

Z)vT#(dDMp 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

S^^Hb8?#>enCzf|V{91Wi zgULB#Mx>}q@Wy+Ni7vKQFm64j(2P!959QR#!*pclRnQaEg!+-QNB)-@EmqY|!$O=| zd4`p4Qjf%DJJvP0@FB0nI#p5rPS}zO;3M#Ee#}Nfw_L}fPY>YJr(={ZfslV=Y4(; zhxX?DG^F_3Hoj7QTG9Vz(6@Zxk0F$I)vGSD;}HtmuKMQ)8Y)Lj8<{M^&VxIgq&DGa z&?!-Ffg4t7yfBRK$8TBZ!|fT9-QBUeuvU8u)+$omB({>r`%I6icN{w59Jy00Gl>gv z{>d><*~=c*^Fa~4!q`*Vr*sX>{(B5XI+U6uDik(iKm_WpDp~HVtWfh#@jP&ECt!;U z?L;u&*^c^|AT%kM9c>#o>{4J;-&B}&ldNFh|D<6iqK8Fh>1kdeZCp)mr5_eA#zQqz|_>xa$3Zq-b1>QR5S2$vdSWf+W z1Dd%F&2)yyoR>c3J?(XgHb&f$A^A%~Y^}UD3|IrSu>YJ`gMN(5`+Ch1z=k-0fi$czdkG#C}l9P2n0RQ9%9ID?O=4 zUn&_-)zA>ykfHzUCb`-8-pjn@3m><(ZyUybTI+TvO8h4#GdUvIdhZ4A7zs zuBtzJYl;|n>~=C#QB!U1N2XGyc5-2#Y3V$7DlkoIg0;!kIfUk*$FGIL18xL<$G7el zXbp%v30nB_Z1y!OvAsHY;igtbfWkJoi2wJxA&FfHSm*OfHW8Hur9v(jC^b9Rt;b59 z%|O)#@fs?iY3Cai#X(JwIN(>UUn^GjOopv|SsTjWiJ!Ji#(IcIto408|{Dvoj zr1s?mEDc|+gzcX*+`eD04Lm+QbIq@aNB+0>)!d&#K=Ot01`lgE8`U*+OB$FR|2?(f zWO}krztBG1Q$+bp4Zs#6vEICWQ5-IVZHrZLDm*GQhp)`=fDI5w3L7$z*|X8iS&6>* zqdn$7%l47LU)qqi^kR_m6m!U9^WrAs2B0-ql&br07LD|}%dA-$7(hqv>sqeq9K$*n z+XvjcN#9C%P8kSQAZm;$f_Vdso#Z4?YU}$plu?%OG3&O1sqU+;cap+a93O7&Uc#>T z#$ri}j1ow^N!aHXvh^kvQC4yXs?|4@Flr+01@o@|USKR%0A?aN9oaI47fxk`Bgy_} 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 G2>m}Fd3|&M From 2f72e22ff95ee77f4650b609c59d83dd307c938b Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Mon, 21 Oct 2024 13:16:01 +0200 Subject: [PATCH 46/67] Fix CI not running on main --- .github/workflows/ci-chrome-extension.yaml | 2 ++ .github/workflows/ci-front.yaml | 11 ++++++++--- .github/workflows/ci-website.yaml | 2 ++ 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci-chrome-extension.yaml b/.github/workflows/ci-chrome-extension.yaml index 071104d31cb7..15d99938b130 100644 --- a/.github/workflows/ci-chrome-extension.yaml +++ b/.github/workflows/ci-chrome-extension.yaml @@ -22,6 +22,8 @@ jobs: with: access_token: ${{ github.token }} - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Check for changed files id: changed-files diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml index d0a8a4c360ed..20c616e75ab1 100644 --- a/.github/workflows/ci-front.yaml +++ b/.github/workflows/ci-front.yaml @@ -23,6 +23,8 @@ jobs: access_token: ${{ github.token }} - name: Fetch local actions uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Check for changed files id: changed-files @@ -38,7 +40,7 @@ jobs: run: echo "No relevant changes. Skipping CI." - name: Install dependencies - if: steps.changed-files.outputs.any_changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Diagnostic disk space issue if: steps.changed-files.outputs.any_changed == 'true' @@ -56,7 +58,6 @@ jobs: if: steps.changed-files.outputs.any_changed == 'true' run: npx nx storybook:build twenty-front front-sb-test: - runs-on: ci-8-cores timeout-minutes: 60 needs: front-sb-build @@ -69,6 +70,8 @@ jobs: steps: - name: Fetch local actions uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Check for changed files id: changed-files uses: tj-actions/changed-files@v11 @@ -149,6 +152,8 @@ jobs: steps: - name: Fetch local actions uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Check for changed files id: changed-files uses: tj-actions/changed-files@v11 @@ -242,7 +247,7 @@ jobs: run: echo "No relevant changes. Skipping CI." - name: Install dependencies - if: steps.changed-files.outputs.any_changed == 'true' + if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install - name: Front / Restore ${{ matrix.task }} task cache if: steps.changed-files.outputs.any_changed == 'true' diff --git a/.github/workflows/ci-website.yaml b/.github/workflows/ci-website.yaml index ce39d66ca79b..1b015c2a34ed 100644 --- a/.github/workflows/ci-website.yaml +++ b/.github/workflows/ci-website.yaml @@ -24,6 +24,8 @@ jobs: - 5432:5432 steps: - uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Check for changed files id: changed-files uses: tj-actions/changed-files@v11 From eaab2d0dd257fb386c79646fd05e206fea252841 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Mon, 21 Oct 2024 13:16:01 +0200 Subject: [PATCH 47/67] Fix CI not running on main --- .github/workflows/ci-front.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml index 20c616e75ab1..f42b4d23b334 100644 --- a/.github/workflows/ci-front.yaml +++ b/.github/workflows/ci-front.yaml @@ -113,7 +113,8 @@ jobs: steps: - name: Fetch local actions uses: actions/checkout@v4 - + with: + fetch-depth: 0 - name: Check for changed files id: changed-files uses: tj-actions/changed-files@v11 From 40152d3b920316eb849d411ee78d1151aff646a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Mon, 21 Oct 2024 14:22:03 +0200 Subject: [PATCH 48/67] 7665 handle the select all case inside the action menu (#7742) Closes #7665 - Handle select all - Handle Filters --------- Co-authored-by: Charles Bochet --- .../components/DeleteRecordsActionEffect.tsx | 110 +++++++++++++----- .../components/ExportRecordsActionEffect.tsx | 31 ++--- .../ManageFavoritesActionEffect.tsx | 25 ++-- ...MultipleRecordsActionMenuEntriesSetter.tsx | 13 ++- .../RecordActionMenuEntriesSetter.tsx | 38 ++++-- .../SingleRecordActionMenuEntriesSetter.tsx | 13 ++- .../action-menu/components/ActionMenu.tsx | 30 +++++ .../action-menu/components/ActionMenuBar.tsx | 10 +- .../components/ActionMenuDropdown.tsx | 2 +- .../components/ActionMenuEffect.tsx | 12 +- .../__stories__/ActionMenuBar.stories.tsx | 9 +- .../__tests__/useExportRecordData.test.ts} | 2 +- .../hooks/useExportRecordData.ts} | 22 ++-- ...ontextStoreNumberOfSelectedRecordsState.ts | 6 + .../contextStoreTargetedRecordIdsState.ts | 6 - .../contextStoreTargetedRecordsRuleState.ts | 26 +++++ .../computeContextStoreFilters.test.ts | 77 ++++++++++++ .../utils/computeContextStoreFilters.ts | 42 +++++++ .../useObjectMetadataItemById.test.ts | 15 +-- .../hooks/useObjectMetadataItemById.ts | 6 +- .../record-board/components/RecordBoard.tsx | 2 +- ....ts => turnFiltersIntoQueryFilter.test.ts} | 18 +-- ...ilter.ts => turnFiltersIntoQueryFilter.ts} | 2 +- .../RecordIndexBoardDataLoaderEffect.tsx | 34 ++---- .../components/RecordIndexContainer.tsx | 31 +++-- ...textStoreNumberOfSelectedRecordsEffect.tsx | 66 +++++++++++ ...tainerContextStoreObjectMetadataEffect.tsx | 31 +++++ .../RecordIndexTableContainerEffect.tsx | 48 +++++--- .../hooks/useLoadRecordIndexBoard.ts | 4 +- .../hooks/useLoadRecordIndexBoardColumn.ts | 4 +- .../hooks/useLoadRecordIndexTable.ts | 4 +- .../components/RecordIndexOptionsDropdown.tsx | 7 +- .../RecordIndexOptionsDropdownContent.tsx | 24 ++-- ...leData.test.tsx => useRecordData.test.tsx} | 62 ++++++---- .../options/hooks/useDeleteTableData.ts | 43 ------- .../{useTableData.ts => useRecordData.ts} | 85 ++++---------- .../components/RecordTableInternalEffect.tsx | 25 +--- .../components/RecordTableWithWrappers.tsx | 5 +- .../hooks/internal/useLeaveTableFocus.ts | 15 +-- .../hooks/internal/useRecordTableStates.ts | 5 + .../hooks/internal/useSetRecordTableData.ts | 17 ++- .../record-table/hooks/useRecordTable.ts | 2 + .../components/RecordTableCellCheckbox.tsx | 11 +- .../RecordTableHeaderCheckboxColumn.tsx | 1 - .../unselectedRowIdsComponentSelector.ts | 23 ++++ .../SignInBackgroundMockContainer.tsx | 10 +- .../views/utils/getQueryVariablesFromView.ts | 4 +- .../pages/object-record/RecordIndexPage.tsx | 4 + .../RecordShowPageContextStoreEffect.tsx | 26 ++++- .../object-record/RecordShowPageEffect.tsx | 15 --- .../testing/jest/JestContextStoreSetter.tsx | 49 ++++++++ ...taAndApolloMocksAndContextStoreWrapper.tsx | 37 ++++++ 52 files changed, 785 insertions(+), 424 deletions(-) create mode 100644 packages/twenty-front/src/modules/action-menu/components/ActionMenu.tsx rename packages/twenty-front/src/modules/{object-record/record-index/options/hooks/__tests__/useExportTableData.test.ts => action-menu/hooks/__tests__/useExportRecordData.test.ts} (99%) rename packages/twenty-front/src/modules/{object-record/record-index/options/hooks/useExportTableData.ts => action-menu/hooks/useExportRecordData.ts} (91%) create mode 100644 packages/twenty-front/src/modules/context-store/states/contextStoreNumberOfSelectedRecordsState.ts delete mode 100644 packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordIdsState.ts create mode 100644 packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsRuleState.ts create mode 100644 packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts create mode 100644 packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts rename packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/{turnObjectDropdownFilterIntoQueryFilter.test.ts => turnFiltersIntoQueryFilter.test.ts} (97%) rename packages/twenty-front/src/modules/object-record/record-filter/utils/{turnObjectDropdownFilterIntoQueryFilter.ts => turnFiltersIntoQueryFilter.ts} (99%) create mode 100644 packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx create mode 100644 packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect.tsx rename packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/{useTableData.test.tsx => useRecordData.test.tsx} (86%) delete mode 100644 packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts rename packages/twenty-front/src/modules/object-record/record-index/options/hooks/{useTableData.ts => useRecordData.ts} (71%) create mode 100644 packages/twenty-front/src/modules/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector.ts delete mode 100644 packages/twenty-front/src/pages/object-record/RecordShowPageEffect.tsx create mode 100644 packages/twenty-front/src/testing/jest/JestContextStoreSetter.tsx create mode 100644 packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper.tsx diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/DeleteRecordsActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/DeleteRecordsActionEffect.tsx index 89243ead97c5..dfa609fc0547 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/DeleteRecordsActionEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/DeleteRecordsActionEffect.tsx @@ -1,51 +1,91 @@ import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; -import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; -import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; +import { useFavorites } from '@/favorites/hooks/useFavorites'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { DELETE_MAX_COUNT } from '@/object-record/constants/DeleteMaxCount'; -import { useDeleteTableData } from '@/object-record/record-index/options/hooks/useDeleteTableData'; +import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; +import { useFetchAllRecordIds } from '@/object-record/hooks/useFetchAllRecordIds'; +import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { useCallback, useEffect, useState } from 'react'; import { useRecoilValue } from 'recoil'; -import { IconTrash } from 'twenty-ui'; +import { IconTrash, isDefined } from 'twenty-ui'; export const DeleteRecordsActionEffect = ({ position, + objectMetadataItem, }: { position: number; + objectMetadataItem: ObjectMetadataItem; }) => { const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); - const contextStoreTargetedRecordIds = useRecoilValue( - contextStoreTargetedRecordIdsState, + const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] = + useState(false); + + const { resetTableRowSelection } = useRecordTable({ + recordTableId: objectMetadataItem.namePlural, + }); + + const { deleteManyRecords } = useDeleteManyRecords({ + objectNameSingular: objectMetadataItem.nameSingular, + }); + + const { favorites, deleteFavorite } = useFavorites(); + + const contextStoreNumberOfSelectedRecords = useRecoilValue( + contextStoreNumberOfSelectedRecordsState, + ); + + const contextStoreTargetedRecordsRule = useRecoilValue( + contextStoreTargetedRecordsRuleState, ); - const contextStoreCurrentObjectMetadataId = useRecoilValue( - contextStoreCurrentObjectMetadataIdState, + const graphqlFilter = computeContextStoreFilters( + contextStoreTargetedRecordsRule, + objectMetadataItem, ); - const { objectMetadataItem } = useObjectMetadataItemById({ - objectId: contextStoreCurrentObjectMetadataId, + const { fetchAllRecordIds } = useFetchAllRecordIds({ + objectNameSingular: objectMetadataItem.nameSingular, + filter: graphqlFilter, }); - const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] = - useState(false); + const handleDeleteClick = useCallback(async () => { + const recordIdsToDelete = await fetchAllRecordIds(); - const { deleteTableData } = useDeleteTableData({ - objectNameSingular: objectMetadataItem?.nameSingular ?? '', - recordIndexId: objectMetadataItem?.namePlural ?? '', - }); + resetTableRowSelection(); - const handleDeleteClick = useCallback(() => { - deleteTableData(contextStoreTargetedRecordIds); - }, [deleteTableData, contextStoreTargetedRecordIds]); + for (const recordIdToDelete of recordIdsToDelete) { + const foundFavorite = favorites?.find( + (favorite) => favorite.recordId === recordIdToDelete, + ); - const isRemoteObject = objectMetadataItem?.isRemote ?? false; + if (foundFavorite !== undefined) { + deleteFavorite(foundFavorite.id); + } + } + + await deleteManyRecords(recordIdsToDelete, { + delayInMsBetweenRequests: 50, + }); + }, [ + deleteFavorite, + deleteManyRecords, + favorites, + fetchAllRecordIds, + resetTableRowSelection, + ]); - const numberOfSelectedRecords = contextStoreTargetedRecordIds.length; + const isRemoteObject = objectMetadataItem.isRemote; const canDelete = - !isRemoteObject && numberOfSelectedRecords < DELETE_MAX_COUNT; + !isRemoteObject && + isDefined(contextStoreNumberOfSelectedRecords) && + contextStoreNumberOfSelectedRecords < DELETE_MAX_COUNT && + contextStoreNumberOfSelectedRecords > 0; useEffect(() => { if (canDelete) { @@ -62,17 +102,19 @@ export const DeleteRecordsActionEffect = ({ handleDeleteClick()} deleteButtonText={`Delete ${ - numberOfSelectedRecords > 1 ? 'Records' : 'Record' + contextStoreNumberOfSelectedRecords > 1 ? 'Records' : 'Record' }`} /> ), @@ -80,14 +122,18 @@ export const DeleteRecordsActionEffect = ({ } else { removeActionMenuEntry('delete'); } + + return () => { + removeActionMenuEntry('delete'); + }; }, [ - canDelete, addActionMenuEntry, - removeActionMenuEntry, - isDeleteRecordsModalOpen, - numberOfSelectedRecords, + canDelete, + contextStoreNumberOfSelectedRecords, handleDeleteClick, + isDeleteRecordsModalOpen, position, + removeActionMenuEntry, ]); return null; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ExportRecordsActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ExportRecordsActionEffect.tsx index d7b50ddaf0d3..bd5ce07cf817 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ExportRecordsActionEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ExportRecordsActionEffect.tsx @@ -1,38 +1,27 @@ import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; -import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { displayedExportProgress, - useExportTableData, -} from '@/object-record/record-index/options/hooks/useExportTableData'; + useExportRecordData, +} from '@/action-menu/hooks/useExportRecordData'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + import { useEffect } from 'react'; -import { useRecoilValue } from 'recoil'; import { IconFileExport } from 'twenty-ui'; export const ExportRecordsActionEffect = ({ position, + objectMetadataItem, }: { position: number; + objectMetadataItem: ObjectMetadataItem; }) => { const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); - const contextStoreCurrentObjectMetadataId = useRecoilValue( - contextStoreCurrentObjectMetadataIdState, - ); - - const { objectMetadataItem } = useObjectMetadataItemById({ - objectId: contextStoreCurrentObjectMetadataId, - }); - - const baseTableDataParams = { + const { progress, download } = useExportRecordData({ delayMs: 100, - objectNameSingular: objectMetadataItem?.nameSingular ?? '', - recordIndexId: objectMetadataItem?.namePlural ?? '', - }; - - const { progress, download } = useExportTableData({ - ...baseTableDataParams, - filename: `${objectMetadataItem?.nameSingular}.csv`, + objectMetadataItem, + recordIndexId: objectMetadataItem.namePlural, + filename: `${objectMetadataItem.nameSingular}.csv`, }); useEffect(() => { diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ManageFavoritesActionEffect.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ManageFavoritesActionEffect.tsx index e9767b034203..572bc239395a 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ManageFavoritesActionEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/ManageFavoritesActionEffect.tsx @@ -1,8 +1,7 @@ import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; -import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; import { useFavorites } from '@/favorites/hooks/useFavorites'; -import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { useEffect } from 'react'; import { useRecoilValue } from 'recoil'; @@ -10,30 +9,28 @@ import { IconHeart, IconHeartOff, isDefined } from 'twenty-ui'; export const ManageFavoritesActionEffect = ({ position, + objectMetadataItem, }: { position: number; + objectMetadataItem: ObjectMetadataItem; }) => { const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); - const contextStoreTargetedRecordIds = useRecoilValue( - contextStoreTargetedRecordIdsState, - ); - const contextStoreCurrentObjectMetadataId = useRecoilValue( - contextStoreCurrentObjectMetadataIdState, + const contextStoreTargetedRecordsRule = useRecoilValue( + contextStoreTargetedRecordsRuleState, ); const { favorites, createFavorite, deleteFavorite } = useFavorites(); - const selectedRecordId = contextStoreTargetedRecordIds[0]; + const selectedRecordId = + contextStoreTargetedRecordsRule.mode === 'selection' + ? contextStoreTargetedRecordsRule.selectedRecordIds[0] + : undefined; const selectedRecord = useRecoilValue( - recordStoreFamilyState(selectedRecordId), + recordStoreFamilyState(selectedRecordId ?? ''), ); - const { objectMetadataItem } = useObjectMetadataItemById({ - objectId: contextStoreCurrentObjectMetadataId, - }); - const foundFavorite = favorites?.find( (favorite) => favorite.recordId === selectedRecordId, ); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter.tsx index 69bfd3305094..ad47a1ee179f 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter.tsx @@ -1,13 +1,22 @@ import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect'; import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; const actionEffects = [ExportRecordsActionEffect, DeleteRecordsActionEffect]; -export const MultipleRecordsActionMenuEntriesSetter = () => { +export const MultipleRecordsActionMenuEntriesSetter = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { return ( <> {actionEffects.map((ActionEffect, index) => ( - + ))} ); diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx index 75267e445d49..acf4a9bed732 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter.tsx @@ -1,20 +1,44 @@ import { MultipleRecordsActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter'; import { SingleRecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; +import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; +import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; import { useRecoilValue } from 'recoil'; export const RecordActionMenuEntriesSetter = () => { - const contextStoreTargetedRecordIds = useRecoilValue( - contextStoreTargetedRecordIdsState, + const contextStoreNumberOfSelectedRecords = useRecoilValue( + contextStoreNumberOfSelectedRecordsState, ); - if (contextStoreTargetedRecordIds.length === 0) { + const contextStoreCurrentObjectMetadataId = useRecoilValue( + contextStoreCurrentObjectMetadataIdState, + ); + + const { objectMetadataItem } = useObjectMetadataItemById({ + objectId: contextStoreCurrentObjectMetadataId ?? '', + }); + + if (!objectMetadataItem) { + throw new Error( + `Object metadata item not found for id ${contextStoreCurrentObjectMetadataId}`, + ); + } + + if (!contextStoreNumberOfSelectedRecords) { return null; } - if (contextStoreTargetedRecordIds.length === 1) { - return ; + if (contextStoreNumberOfSelectedRecords === 1) { + return ( + + ); } - return ; + return ( + + ); }; diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx index 4b61fa58eadb..9c4b1d528f21 100644 --- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx +++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx @@ -1,8 +1,13 @@ import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect'; import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect'; import { ManageFavoritesActionEffect } from '@/action-menu/actions/record-actions/components/ManageFavoritesActionEffect'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -export const SingleRecordActionMenuEntriesSetter = () => { +export const SingleRecordActionMenuEntriesSetter = ({ + objectMetadataItem, +}: { + objectMetadataItem: ObjectMetadataItem; +}) => { const actionEffects = [ ManageFavoritesActionEffect, ExportRecordsActionEffect, @@ -11,7 +16,11 @@ export const SingleRecordActionMenuEntriesSetter = () => { return ( <> {actionEffects.map((ActionEffect, index) => ( - + ))} ); diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenu.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenu.tsx new file mode 100644 index 000000000000..92cda27cc9d8 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenu.tsx @@ -0,0 +1,30 @@ +import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter'; +import { ActionMenuBar } from '@/action-menu/components/ActionMenuBar'; +import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals'; +import { ActionMenuDropdown } from '@/action-menu/components/ActionMenuDropdown'; +import { ActionMenuEffect } from '@/action-menu/components/ActionMenuEffect'; +import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; +import { useRecoilValue } from 'recoil'; + +export const ActionMenu = ({ actionMenuId }: { actionMenuId: string }) => { + const contextStoreCurrentObjectMetadataId = useRecoilValue( + contextStoreCurrentObjectMetadataIdState, + ); + + return ( + <> + {contextStoreCurrentObjectMetadataId && ( + + + + + + + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx index 258683347919..2fd2937408c0 100644 --- a/packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx @@ -4,7 +4,7 @@ import { ActionMenuBarEntry } from '@/action-menu/components/ActionMenuBarEntry' import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionBarHotkeyScope } from '@/action-menu/types/ActionBarHotKeyScope'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; import { BottomBar } from '@/ui/layout/bottom-bar/components/BottomBar'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -19,8 +19,8 @@ const StyledLabel = styled.div` `; export const ActionMenuBar = () => { - const contextStoreTargetedRecordIds = useRecoilValue( - contextStoreTargetedRecordIdsState, + const contextStoreNumberOfSelectedRecords = useRecoilValue( + contextStoreNumberOfSelectedRecordsState, ); const actionMenuId = useAvailableComponentInstanceIdOrThrow( @@ -42,9 +42,7 @@ export const ActionMenuBar = () => { scope: ActionBarHotkeyScope.ActionBar, }} > - - {contextStoreTargetedRecordIds.length} selected: - + {contextStoreNumberOfSelectedRecords} selected: {actionMenuEntries.map((entry, index) => ( ))} diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuDropdown.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenuDropdown.tsx index 18ebdac7667e..c05df9b758a2 100644 --- a/packages/twenty-front/src/modules/action-menu/components/ActionMenuDropdown.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/ActionMenuDropdown.tsx @@ -64,7 +64,7 @@ export const ActionMenuDropdown = () => { return ( { - const contextStoreTargetedRecordIds = useRecoilValue( - contextStoreTargetedRecordIdsState, + const contextStoreNumberOfSelectedRecords = useRecoilValue( + contextStoreNumberOfSelectedRecordsState, ); const actionMenuId = useAvailableComponentInstanceIdOrThrow( @@ -26,17 +26,17 @@ export const ActionMenuEffect = () => { ); useEffect(() => { - if (contextStoreTargetedRecordIds.length > 0 && !isDropdownOpen) { + if (contextStoreNumberOfSelectedRecords > 0 && !isDropdownOpen) { // We only handle opening the ActionMenuBar here, not the Dropdown. // The Dropdown is already managed by sync handlers for events like // right-click to open and click outside to close. openActionBar(); } - if (contextStoreTargetedRecordIds.length === 0) { + if (contextStoreNumberOfSelectedRecords === 0 && isDropdownOpen) { closeActionBar(); } }, [ - contextStoreTargetedRecordIds, + contextStoreNumberOfSelectedRecords, openActionBar, closeActionBar, isDropdownOpen, diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBar.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBar.stories.tsx index 34d709d1685d..b34462d8fb3c 100644 --- a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBar.stories.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBar.stories.tsx @@ -5,7 +5,8 @@ import { RecoilRoot } from 'recoil'; import { ActionMenuBar } from '@/action-menu/components/ActionMenuBar'; import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState'; import { userEvent, waitFor, within } from '@storybook/test'; import { IconCheckbox, IconTrash } from 'twenty-ui'; @@ -20,7 +21,11 @@ const meta: Meta = { (Story) => ( { - set(contextStoreTargetedRecordIdsState, ['1', '2', '3']); + set(contextStoreTargetedRecordsRuleState, { + mode: 'selection', + selectedRecordIds: ['1', '2', '3'], + }); + set(contextStoreNumberOfSelectedRecordsState, 3); set( actionMenuEntriesComponentState.atomFamily({ instanceId: 'story-action-menu', diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useExportTableData.test.ts b/packages/twenty-front/src/modules/action-menu/hooks/__tests__/useExportRecordData.test.ts similarity index 99% rename from packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useExportTableData.test.ts rename to packages/twenty-front/src/modules/action-menu/hooks/__tests__/useExportRecordData.test.ts index 0494fd32f023..65fa9ba2e29c 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useExportTableData.test.ts +++ b/packages/twenty-front/src/modules/action-menu/hooks/__tests__/useExportRecordData.test.ts @@ -7,7 +7,7 @@ import { displayedExportProgress, download, generateCsv, -} from '../useExportTableData'; +} from '../useExportRecordData'; jest.useFakeTimers(); diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts b/packages/twenty-front/src/modules/action-menu/hooks/useExportRecordData.ts similarity index 91% rename from packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts rename to packages/twenty-front/src/modules/action-menu/hooks/useExportRecordData.ts index 532b8e0aa59b..8fa6d6f5981c 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts +++ b/packages/twenty-front/src/modules/action-menu/hooks/useExportRecordData.ts @@ -4,10 +4,11 @@ import { useMemo } from 'react'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/record-index/options/constants/ExportTableDataDefaultPageSize'; import { useProcessRecordsForCSVExport } from '@/object-record/record-index/options/hooks/useProcessRecordsForCSVExport'; + import { - useTableData, - UseTableDataOptions, -} from '@/object-record/record-index/options/hooks/useTableData'; + UseRecordDataOptions, + useRecordData, +} from '@/object-record/record-index/options/hooks/useRecordData'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { RelationDefinitionType } from '~/generated-metadata/graphql'; @@ -134,21 +135,22 @@ const downloader = (mimeType: string, generator: GenerateExport) => { export const csvDownloader = downloader('text/csv', generateCsv); -type UseExportTableDataOptions = Omit & { +type UseExportTableDataOptions = Omit & { filename: string; }; -export const useExportTableData = ({ +export const useExportRecordData = ({ delayMs, filename, maximumRequests = 100, - objectNameSingular, + objectMetadataItem, pageSize = EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE, recordIndexId, viewType, }: UseExportTableDataOptions) => { - const { processRecordsForCSVExport } = - useProcessRecordsForCSVExport(objectNameSingular); + const { processRecordsForCSVExport } = useProcessRecordsForCSVExport( + objectMetadataItem.nameSingular, + ); const downloadCsv = useMemo( () => @@ -160,10 +162,10 @@ export const useExportTableData = ({ [filename, processRecordsForCSVExport], ); - const { getTableData: download, progress } = useTableData({ + const { getTableData: download, progress } = useRecordData({ delayMs, maximumRequests, - objectNameSingular, + objectMetadataItem, pageSize, recordIndexId, callback: downloadCsv, diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreNumberOfSelectedRecordsState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreNumberOfSelectedRecordsState.ts new file mode 100644 index 000000000000..fb1b3544d320 --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/states/contextStoreNumberOfSelectedRecordsState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const contextStoreNumberOfSelectedRecordsState = createState({ + key: 'contextStoreNumberOfSelectedRecordsState', + defaultValue: 0, +}); diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordIdsState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordIdsState.ts deleted file mode 100644 index df0c3451172c..000000000000 --- a/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordIdsState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createState } from 'twenty-ui'; - -export const contextStoreTargetedRecordIdsState = createState({ - key: 'contextStoreTargetedRecordIdsState', - defaultValue: [], -}); diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsRuleState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsRuleState.ts new file mode 100644 index 000000000000..7f71377c3186 --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsRuleState.ts @@ -0,0 +1,26 @@ +import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; +import { createState } from 'twenty-ui'; + +type ContextStoreTargetedRecordsRuleSelectionMode = { + mode: 'selection'; + selectedRecordIds: string[]; +}; + +type ContextStoreTargetedRecordsRuleExclusionMode = { + mode: 'exclusion'; + excludedRecordIds: string[]; + filters: Filter[]; +}; + +export type ContextStoreTargetedRecordsRule = + | ContextStoreTargetedRecordsRuleSelectionMode + | ContextStoreTargetedRecordsRuleExclusionMode; + +export const contextStoreTargetedRecordsRuleState = + createState({ + key: 'contextStoreTargetedRecordsRuleState', + defaultValue: { + mode: 'selection', + selectedRecordIds: [], + }, + }); diff --git a/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts b/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts new file mode 100644 index 000000000000..689d7287d4da --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/utils/__tests__/computeContextStoreFilters.test.ts @@ -0,0 +1,77 @@ +import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; +describe('computeContextStoreFilters', () => { + const personObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', + )!; + + it('should work for selection mode', () => { + const contextStoreTargetedRecordsRule: ContextStoreTargetedRecordsRule = { + mode: 'selection', + selectedRecordIds: ['1', '2', '3'], + }; + + const filters = computeContextStoreFilters( + contextStoreTargetedRecordsRule, + personObjectMetadataItem, + ); + + expect(filters).toEqual({ + id: { + in: ['1', '2', '3'], + }, + }); + }); + + it('should work for exclusion mode', () => { + const contextStoreTargetedRecordsRule: ContextStoreTargetedRecordsRule = { + mode: 'exclusion', + filters: [ + { + id: 'name-filter', + variant: 'default', + fieldMetadataId: personObjectMetadataItem.fields.find( + (field) => field.name === 'name', + )!.id, + value: 'John', + displayValue: 'John', + displayAvatarUrl: undefined, + operand: ViewFilterOperand.Contains, + definition: { + fieldMetadataId: personObjectMetadataItem.fields.find( + (field) => field.name === 'name', + )!.id, + label: 'Name', + iconName: 'person', + type: 'TEXT', + }, + }, + ], + excludedRecordIds: ['1', '2', '3'], + }; + + const filters = computeContextStoreFilters( + contextStoreTargetedRecordsRule, + personObjectMetadataItem, + ); + + expect(filters).toEqual({ + and: [ + { + name: { + ilike: '%John%', + }, + }, + { + not: { + id: { + in: ['1', '2', '3'], + }, + }, + }, + ], + }); + }); +}); diff --git a/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts new file mode 100644 index 000000000000..26727fbc26ee --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts @@ -0,0 +1,42 @@ +import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; +import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter'; +import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; + +export const computeContextStoreFilters = ( + contextStoreTargetedRecordsRule: ContextStoreTargetedRecordsRule, + objectMetadataItem: ObjectMetadataItem, +) => { + let queryFilter: RecordGqlOperationFilter | undefined; + + if (contextStoreTargetedRecordsRule.mode === 'exclusion') { + queryFilter = makeAndFilterVariables([ + turnFiltersIntoQueryFilter( + contextStoreTargetedRecordsRule.filters, + objectMetadataItem?.fields ?? [], + ), + contextStoreTargetedRecordsRule.excludedRecordIds.length > 0 + ? { + not: { + id: { + in: contextStoreTargetedRecordsRule.excludedRecordIds, + }, + }, + } + : undefined, + ]); + } + if (contextStoreTargetedRecordsRule.mode === 'selection') { + queryFilter = + contextStoreTargetedRecordsRule.selectedRecordIds.length > 0 + ? { + id: { + in: contextStoreTargetedRecordsRule.selectedRecordIds, + }, + } + : undefined; + } + + return queryFilter; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemById.test.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemById.test.ts index ceff2e45541a..01cdbc405d6d 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemById.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemById.test.ts @@ -33,16 +33,11 @@ describe('useObjectMetadataItemById', () => { expect(objectMetadataItem?.id).toBe(opportunityObjectMetadata.id); }); - it('should return null when invalid ID is provided', async () => { - const { result } = renderHook( - () => useObjectMetadataItemById({ objectId: 'invalid-id' }), - { + it('should throw an error when invalid ID is provided', async () => { + expect(() => + renderHook(() => useObjectMetadataItemById({ objectId: 'invalid-id' }), { wrapper: Wrapper, - }, - ); - - const { objectMetadataItem } = result.current; - - expect(objectMetadataItem).toBeNull(); + }), + ).toThrow(`Object metadata item not found for id invalid-id`); }); }); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemById.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemById.ts index 72c559364226..1783ea61fd1b 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemById.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemById.ts @@ -6,7 +6,7 @@ import { isDefined } from '~/utils/isDefined'; export const useObjectMetadataItemById = ({ objectId, }: { - objectId: string | null; + objectId: string; }) => { const objectMetadataItems = useRecoilValue(objectMetadataItemsState); @@ -15,9 +15,7 @@ export const useObjectMetadataItemById = ({ ); if (!isDefined(objectMetadataItem)) { - return { - objectMetadataItem: null, - }; + throw new Error(`Object metadata item not found for id ${objectId}`); } return { diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx index 01ca2843c3bb..592f2d7b4af4 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx @@ -66,7 +66,7 @@ export const RecordBoard = () => { useListenClickOutsideByClassName({ classNames: ['record-board-card'], - excludeClassNames: ['bottom-bar', 'context-menu'], + excludeClassNames: ['bottom-bar', 'action-menu-dropdown'], callback: resetRecordSelection, }); diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnObjectDropdownFilterIntoQueryFilter.test.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnFiltersIntoQueryFilter.test.ts similarity index 97% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnObjectDropdownFilterIntoQueryFilter.test.ts rename to packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnFiltersIntoQueryFilter.test.ts index 6486ca29b92e..e8778a3f89bb 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnObjectDropdownFilterIntoQueryFilter.test.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnFiltersIntoQueryFilter.test.ts @@ -1,5 +1,5 @@ import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; -import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter'; +import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { getCompaniesMock } from '~/testing/mock-data/companies'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; @@ -16,7 +16,7 @@ const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find( jest.useFakeTimers().setSystemTime(new Date('2020-01-01')); -describe('turnObjectDropdownFilterIntoQueryFilter', () => { +describe('turnFiltersIntoQueryFilter', () => { it('should work as expected for single filter', () => { const companyMockNameFieldMetadataId = companyMockObjectMetadataItem.fields.find( @@ -37,7 +37,7 @@ describe('turnObjectDropdownFilterIntoQueryFilter', () => { }, }; - const result = turnObjectDropdownFilterIntoQueryFilter( + const result = turnFiltersIntoQueryFilter( [nameFilter], companyMockObjectMetadataItem.fields, ); @@ -88,7 +88,7 @@ describe('turnObjectDropdownFilterIntoQueryFilter', () => { }, }; - const result = turnObjectDropdownFilterIntoQueryFilter( + const result = turnFiltersIntoQueryFilter( [nameFilter, employeesFilter], companyMockObjectMetadataItem.fields, ); @@ -173,7 +173,7 @@ describe('should work as expected for the different field types', () => { }, }; - const result = turnObjectDropdownFilterIntoQueryFilter( + const result = turnFiltersIntoQueryFilter( [ addressFilterContains, addressFilterDoesNotContain, @@ -554,7 +554,7 @@ describe('should work as expected for the different field types', () => { }, }; - const result = turnObjectDropdownFilterIntoQueryFilter( + const result = turnFiltersIntoQueryFilter( [ phonesFilterContains, phonesFilterDoesNotContain, @@ -754,7 +754,7 @@ describe('should work as expected for the different field types', () => { }, }; - const result = turnObjectDropdownFilterIntoQueryFilter( + const result = turnFiltersIntoQueryFilter( [ emailsFilterContains, emailsFilterDoesNotContain, @@ -908,7 +908,7 @@ describe('should work as expected for the different field types', () => { }, }; - const result = turnObjectDropdownFilterIntoQueryFilter( + const result = turnFiltersIntoQueryFilter( [ dateFilterIsAfter, dateFilterIsBefore, @@ -1023,7 +1023,7 @@ describe('should work as expected for the different field types', () => { }, }; - const result = turnObjectDropdownFilterIntoQueryFilter( + const result = turnFiltersIntoQueryFilter( [ employeesFilterIsGreaterThan, employeesFilterIsLessThan, diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnFiltersIntoQueryFilter.ts similarity index 99% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts rename to packages/twenty-front/src/modules/object-record/record-filter/utils/turnFiltersIntoQueryFilter.ts index 345421f7ce95..0e3c69d7c0b8 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnFiltersIntoQueryFilter.ts @@ -31,7 +31,7 @@ import { z } from 'zod'; // TODO: break this down into smaller functions and make the whole thing immutable // Especially applyEmptyFilters -export const turnObjectDropdownFilterIntoQueryFilter = ( +export const turnFiltersIntoQueryFilter = ( rawUIFilters: Filter[], fields: Pick[], ): RecordGqlOperationFilter | undefined => { diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx index 354abcf09dab..9e8358f9e096 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardDataLoaderEffect.tsx @@ -2,8 +2,7 @@ import { useCallback, useEffect } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; @@ -121,32 +120,23 @@ export const RecordIndexBoardDataLoaderEffect = ({ const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector()); - const setContextStoreTargetedRecordIds = useSetRecoilState( - contextStoreTargetedRecordIdsState, + const setContextStoreTargetedRecords = useSetRecoilState( + contextStoreTargetedRecordsRuleState, ); - const setContextStoreCurrentObjectMetadataItem = useSetRecoilState( - contextStoreCurrentObjectMetadataIdState, - ); - - useEffect(() => { - setContextStoreTargetedRecordIds(selectedRecordIds); - }, [selectedRecordIds, setContextStoreTargetedRecordIds]); - useEffect(() => { - setContextStoreTargetedRecordIds(selectedRecordIds); - setContextStoreCurrentObjectMetadataItem(objectMetadataItem?.id); + setContextStoreTargetedRecords({ + mode: 'selection', + selectedRecordIds: selectedRecordIds, + }); return () => { - setContextStoreTargetedRecordIds([]); - setContextStoreCurrentObjectMetadataItem(null); + setContextStoreTargetedRecords({ + mode: 'selection', + selectedRecordIds: [], + }); }; - }, [ - objectMetadataItem?.id, - selectedRecordIds, - setContextStoreCurrentObjectMetadataItem, - setContextStoreTargetedRecordIds, - ]); + }, [selectedRecordIds, setContextStoreTargetedRecords]); return <>; }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx index 9aecee3e6160..a28d2f26ac08 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx @@ -22,12 +22,8 @@ import { RecordFieldValueSelectorContextProvider } from '@/object-record/record- import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider'; -import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter'; -import { ActionMenuBar } from '@/action-menu/components/ActionMenuBar'; -import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals'; -import { ActionMenuDropdown } from '@/action-menu/components/ActionMenuDropdown'; -import { ActionMenuEffect } from '@/action-menu/components/ActionMenuEffect'; -import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { ActionMenu } from '@/action-menu/components/ActionMenu'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; import { ViewBar } from '@/views/components/ViewBar'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { ViewField } from '@/views/types/ViewField'; @@ -106,6 +102,10 @@ export const RecordIndexContainer = () => { [columnDefinitions, setTableColumns], ); + const setContextStoreTargetedRecordsRule = useSetRecoilState( + contextStoreTargetedRecordsRuleState, + ); + return ( @@ -119,7 +119,7 @@ export const RecordIndexContainer = () => { optionsDropdownButton={ } @@ -135,6 +135,13 @@ export const RecordIndexContainer = () => { setRecordIndexFilters( mapViewFiltersToFilters(view.viewFilters, filterDefinitions), ); + setContextStoreTargetedRecordsRule((prev) => ({ + ...prev, + filters: mapViewFiltersToFilters( + view.viewFilters, + filterDefinitions, + ), + })); setTableSorts( mapViewSortsToSorts(view.viewSorts, sortDefinitions), ); @@ -179,15 +186,7 @@ export const RecordIndexContainer = () => { /> )} - - - - - - - + diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx new file mode 100644 index 000000000000..2a538c542af1 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect.tsx @@ -0,0 +1,66 @@ +import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useFindManyParams } from '@/object-record/record-index/hooks/useLoadRecordIndexTable'; +import { useContext, useEffect } from 'react'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; + +export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect = + () => { + const setContextStoreNumberOfSelectedRecords = useSetRecoilState( + contextStoreNumberOfSelectedRecordsState, + ); + + const contextStoreTargetedRecordsRule = useRecoilValue( + contextStoreTargetedRecordsRuleState, + ); + + const { objectNamePlural } = useContext(RecordIndexRootPropsContext); + + const { objectNameSingular } = useObjectNameSingularFromPlural({ + objectNamePlural, + }); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const findManyRecordsParams = useFindManyParams( + objectMetadataItem?.nameSingular ?? '', + objectMetadataItem?.namePlural ?? '', + ); + + const { totalCount } = useFindManyRecords({ + ...findManyRecordsParams, + recordGqlFields: { + id: true, + }, + filter: computeContextStoreFilters( + contextStoreTargetedRecordsRule, + objectMetadataItem, + ), + limit: 1, + skip: contextStoreTargetedRecordsRule.mode === 'selection', + }); + + useEffect(() => { + if (contextStoreTargetedRecordsRule.mode === 'selection') { + setContextStoreNumberOfSelectedRecords( + contextStoreTargetedRecordsRule.selectedRecordIds.length, + ); + } + if (contextStoreTargetedRecordsRule.mode === 'exclusion') { + setContextStoreNumberOfSelectedRecords(totalCount ?? 0); + } + }, [ + contextStoreTargetedRecordsRule, + setContextStoreNumberOfSelectedRecords, + totalCount, + ]); + + return null; + }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect.tsx new file mode 100644 index 000000000000..c94611836a1b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerContextStoreObjectMetadataEffect.tsx @@ -0,0 +1,31 @@ +import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; +import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useContext, useEffect } from 'react'; +import { useSetRecoilState } from 'recoil'; + +export const RecordIndexContainerContextStoreObjectMetadataEffect = () => { + const setContextStoreCurrentObjectMetadataItem = useSetRecoilState( + contextStoreCurrentObjectMetadataIdState, + ); + const { objectNamePlural } = useContext(RecordIndexRootPropsContext); + + const { objectNameSingular } = useObjectNameSingularFromPlural({ + objectNamePlural, + }); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + useEffect(() => { + setContextStoreCurrentObjectMetadataItem(objectMetadataItem.id); + + return () => { + setContextStoreCurrentObjectMetadataItem(null); + }; + }, [objectMetadataItem.id, setContextStoreCurrentObjectMetadataItem]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx index da407d91e5a9..ba541ca1a67c 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexTableContainerEffect.tsx @@ -1,8 +1,7 @@ import { useContext, useEffect } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; @@ -24,18 +23,12 @@ export const RecordIndexTableContainerEffect = () => { selectedRowIdsSelector, setOnToggleColumnFilter, setOnToggleColumnSort, + hasUserSelectedAllRowsState, + unselectedRowIdsSelector, } = useRecordTable({ recordTableId: recordIndexId, }); - const setContextStoreTargetedRecordIds = useSetRecoilState( - contextStoreTargetedRecordIdsState, - ); - - const setContextStoreCurrentObjectMetadataItem = useSetRecoilState( - contextStoreCurrentObjectMetadataIdState, - ); - const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, }); @@ -50,8 +43,6 @@ export const RecordIndexTableContainerEffect = () => { setAvailableTableColumns(columnDefinitions); }, [columnDefinitions, setAvailableTableColumns]); - const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); - const handleToggleColumnFilter = useHandleToggleColumnFilter({ objectNameSingular, viewBarId, @@ -82,19 +73,38 @@ export const RecordIndexTableContainerEffect = () => { ); }, [setRecordCountInCurrentView, setOnEntityCountChange]); + const setContextStoreTargetedRecords = useSetRecoilState( + contextStoreTargetedRecordsRuleState, + ); + const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState); + const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); + const unselectedRowIds = useRecoilValue(unselectedRowIdsSelector()); + useEffect(() => { - setContextStoreTargetedRecordIds(selectedRowIds); - setContextStoreCurrentObjectMetadataItem(objectMetadataItem?.id); + if (hasUserSelectedAllRows) { + setContextStoreTargetedRecords({ + mode: 'exclusion', + excludedRecordIds: unselectedRowIds, + filters: [], + }); + } else { + setContextStoreTargetedRecords({ + mode: 'selection', + selectedRecordIds: selectedRowIds, + }); + } return () => { - setContextStoreTargetedRecordIds([]); - setContextStoreCurrentObjectMetadataItem(null); + setContextStoreTargetedRecords({ + mode: 'selection', + selectedRecordIds: [], + }); }; }, [ - objectMetadataItem?.id, + hasUserSelectedAllRows, selectedRowIds, - setContextStoreCurrentObjectMetadataItem, - setContextStoreTargetedRecordIds, + setContextStoreTargetedRecords, + unselectedRowIds, ]); return <>; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts index 297f1dcf8088..3f5ce71ed09b 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts @@ -5,7 +5,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; -import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter'; +import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter'; import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields'; import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; @@ -44,7 +44,7 @@ export const useLoadRecordIndexBoard = ({ const recordIndexFilters = useRecoilValue(recordIndexFiltersState); const recordIndexSorts = useRecoilValue(recordIndexSortsState); - const requestFilters = turnObjectDropdownFilterIntoQueryFilter( + const requestFilters = turnFiltersIntoQueryFilter( recordIndexFilters, objectMetadataItem?.fields ?? [], ); diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts index 02485fe0d78b..e77545fdcf6f 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoardColumn.ts @@ -5,7 +5,7 @@ import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadata import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; -import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter'; +import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter'; import { useRecordBoardRecordGqlFields } from '@/object-record/record-index/hooks/useRecordBoardRecordGqlFields'; import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState'; @@ -35,7 +35,7 @@ export const useLoadRecordIndexBoardColumn = ({ const recordIndexFilters = useRecoilValue(recordIndexFiltersState); const recordIndexSorts = useRecoilValue(recordIndexSortsState); - const requestFilters = turnObjectDropdownFilterIntoQueryFilter( + const requestFilters = turnFiltersIntoQueryFilter( recordIndexFilters, objectMetadataItem?.fields ?? [], ); diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts index df178df4c4fd..f39ebc77915b 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts @@ -5,7 +5,7 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { turnSortsIntoOrderBy } from '@/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy'; -import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter'; +import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter'; import { useRecordTableRecordGqlFields } from '@/object-record/record-index/hooks/useRecordTableRecordGqlFields'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; @@ -27,7 +27,7 @@ export const useFindManyParams = ( const tableFilters = useRecoilValue(tableFiltersState); const tableSorts = useRecoilValue(tableSortsState); - const filter = turnObjectDropdownFilterIntoQueryFilter( + const filter = turnFiltersIntoQueryFilter( tableFilters, objectMetadataItem?.fields ?? [], ); diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdown.tsx b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdown.tsx index 25e53d8cc5b1..3c2f5b2bae3e 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdown.tsx @@ -1,3 +1,4 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { RecordIndexOptionsDropdownButton } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdownButton'; import { RecordIndexOptionsDropdownContent } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdownContent'; import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId'; @@ -7,13 +8,13 @@ import { ViewType } from '@/views/types/ViewType'; type RecordIndexOptionsDropdownProps = { viewType: ViewType; - objectNameSingular: string; + objectMetadataItem: ObjectMetadataItem; recordIndexId: string; }; export const RecordIndexOptionsDropdown = ({ recordIndexId, - objectNameSingular, + objectMetadataItem, viewType, }: RecordIndexOptionsDropdownProps) => { return ( @@ -26,7 +27,7 @@ export const RecordIndexOptionsDropdown = ({ dropdownComponents={ } diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx index a884eda8582b..9396d30da07c 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx @@ -14,10 +14,12 @@ import { import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular'; import { useHandleToggleTrashColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleTrashColumnFilter'; import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId'; + import { displayedExportProgress, - useExportTableData, -} from '@/object-record/record-index/options/hooks/useExportTableData'; + useExportRecordData, +} from '@/action-menu/hooks/useExportRecordData'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard'; import { useRecordIndexOptionsForTable } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForTable'; import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; @@ -44,14 +46,14 @@ type RecordIndexOptionsMenu = 'fields' | 'hiddenFields'; type RecordIndexOptionsDropdownContentProps = { recordIndexId: string; - objectNameSingular: string; + objectMetadataItem: ObjectMetadataItem; viewType: ViewType; }; export const RecordIndexOptionsDropdownContent = ({ viewType, recordIndexId, - objectNameSingular, + objectMetadataItem, }: RecordIndexOptionsDropdownContentProps) => { const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(); @@ -68,7 +70,7 @@ export const RecordIndexOptionsDropdownContent = ({ }; const { objectNamePlural } = useObjectNamePluralFromSingular({ - objectNameSingular: objectNameSingular, + objectNameSingular: objectMetadataItem.nameSingular, }); const settingsUrl = getSettingsPagePath(SettingsPath.ObjectDetail, { @@ -92,7 +94,7 @@ export const RecordIndexOptionsDropdownContent = ({ const { handleToggleTrashColumnFilter, toggleSoftDeleteFilterState } = useHandleToggleTrashColumnFilter({ - objectNameSingular, + objectNameSingular: objectMetadataItem.nameSingular, viewBarId: recordIndexId, }); @@ -104,7 +106,7 @@ export const RecordIndexOptionsDropdownContent = ({ isCompactModeActive, setAndPersistIsCompactModeActive, } = useRecordIndexOptionsForBoard({ - objectNameSingular, + objectNameSingular: objectMetadataItem.nameSingular, recordBoardId: recordIndexId, viewBarId: recordIndexId, }); @@ -126,12 +128,12 @@ export const RecordIndexOptionsDropdownContent = ({ : handleColumnVisibilityChange; const { openObjectRecordsSpreasheetImportDialog } = - useOpenObjectRecordsSpreasheetImportDialog(objectNameSingular); + useOpenObjectRecordsSpreasheetImportDialog(objectMetadataItem.nameSingular); - const { progress, download } = useExportTableData({ + const { progress, download } = useExportRecordData({ delayMs: 100, - filename: `${objectNameSingular}.csv`, - objectNameSingular, + filename: `${objectMetadataItem.nameSingular}.csv`, + objectMetadataItem, recordIndexId, viewType, }); diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useTableData.test.tsx b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useRecordData.test.tsx similarity index 86% rename from packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useTableData.test.tsx rename to packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useRecordData.test.tsx index aa9f392782f0..9747c2c4e9e1 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useTableData.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useRecordData.test.tsx @@ -1,6 +1,6 @@ import { renderHook, waitFor } from '@testing-library/react'; import { act } from 'react'; -import { percentage, sleep, useTableData } from '../useTableData'; +import { percentage, sleep, useRecordData } from '../useRecordData'; import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; @@ -11,7 +11,7 @@ import { ViewType } from '@/views/types/ViewType'; import { MockedResponse } from '@apollo/client/testing'; import gql from 'graphql-tag'; import { useRecoilValue } from 'recoil'; -import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { getJestMetadataAndApolloMocksAndContextStoreWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; const defaultResponseData = { @@ -127,9 +127,16 @@ const mocks: MockedResponse[] = [ }, ]; -const WrapperWithResponse = getJestMetadataAndApolloMocksWrapper({ - apolloMocks: mocks, -}); +const WrapperWithResponse = getJestMetadataAndApolloMocksAndContextStoreWrapper( + { + apolloMocks: mocks, + contextStoreTargetedRecordsRule: { + mode: 'selection', + selectedRecordIds: [], + }, + contextStoreCurrentObjectMetadataNameSingular: 'person', + }, +); const graphqlEmptyResponse = [ { @@ -145,28 +152,41 @@ const graphqlEmptyResponse = [ }, ]; -const WrapperWithEmptyResponse = getJestMetadataAndApolloMocksWrapper({ - apolloMocks: graphqlEmptyResponse, -}); +const WrapperWithEmptyResponse = + getJestMetadataAndApolloMocksAndContextStoreWrapper({ + apolloMocks: graphqlEmptyResponse, + contextStoreTargetedRecordsRule: { + mode: 'selection', + selectedRecordIds: [], + }, + contextStoreCurrentObjectMetadataNameSingular: 'person', + }); -describe('useTableData', () => { +describe('useRecordData', () => { const recordIndexId = 'people'; - const objectNameSingular = 'person'; + const objectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', + ); + if (!objectMetadataItem) { + throw new Error('Object metadata item not found'); + } describe('data fetching', () => { it('should handle no records', async () => { const callback = jest.fn(); const { result } = renderHook( () => - useTableData({ + useRecordData({ recordIndexId, - objectNameSingular, + objectMetadataItem, pageSize: 30, callback, delayMs: 0, viewType: ViewType.Kanban, }), - { wrapper: WrapperWithEmptyResponse }, + { + wrapper: WrapperWithEmptyResponse, + }, ); await act(async () => { @@ -182,9 +202,9 @@ describe('useTableData', () => { const callback = jest.fn(); const { result } = renderHook( () => - useTableData({ + useRecordData({ recordIndexId, - objectNameSingular, + objectMetadataItem, callback, pageSize: 30, @@ -211,9 +231,9 @@ describe('useTableData', () => { recordIndexId, ); return { - tableData: useTableData({ + tableData: useRecordData({ recordIndexId, - objectNameSingular, + objectMetadataItem, callback, pageSize: 30, maximumRequests: 100, @@ -223,7 +243,7 @@ describe('useTableData', () => { useRecordBoardHook: useRecordBoard(recordIndexId), kanbanFieldName: useRecoilValue(kanbanFieldNameState), kanbanData: useRecordIndexOptionsForBoard({ - objectNameSingular, + objectNameSingular: objectMetadataItem.nameSingular, recordBoardId: recordIndexId, viewBarId: recordIndexId, }), @@ -304,9 +324,9 @@ describe('useTableData', () => { recordIndexId, ); return { - tableData: useTableData({ + tableData: useRecordData({ recordIndexId, - objectNameSingular, + objectMetadataItem, callback, pageSize: 30, maximumRequests: 100, @@ -316,7 +336,7 @@ describe('useTableData', () => { setKanbanFieldName: useRecordBoard(recordIndexId), kanbanFieldName: useRecoilValue(kanbanFieldNameState), kanbanData: useRecordIndexOptionsForBoard({ - objectNameSingular, + objectNameSingular: objectMetadataItem.nameSingular, recordBoardId: recordIndexId, viewBarId: recordIndexId, }), diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts deleted file mode 100644 index 345e11453892..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useDeleteTableData.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { useFavorites } from '@/favorites/hooks/useFavorites'; -import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; -import { UseTableDataOptions } from '@/object-record/record-index/options/hooks/useTableData'; -import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; - -type UseDeleteTableDataOptions = Pick< - UseTableDataOptions, - 'objectNameSingular' | 'recordIndexId' ->; - -export const useDeleteTableData = ({ - objectNameSingular, - recordIndexId, -}: UseDeleteTableDataOptions) => { - const { resetTableRowSelection } = useRecordTable({ - recordTableId: recordIndexId, - }); - - const { deleteManyRecords } = useDeleteManyRecords({ - objectNameSingular, - }); - const { favorites, deleteFavorite } = useFavorites(); - - const deleteRecords = async (recordIdsToDelete: string[]) => { - resetTableRowSelection(); - - for (const recordIdToDelete of recordIdsToDelete) { - const foundFavorite = favorites?.find( - (favorite) => favorite.recordId === recordIdToDelete, - ); - - if (foundFavorite !== undefined) { - deleteFavorite(foundFavorite.id); - } - } - - await deleteManyRecords(recordIdsToDelete, { - delayInMsBetweenRequests: 50, - }); - }; - - return { deleteTableData: deleteRecords }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordData.ts similarity index 71% rename from packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts rename to packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordData.ts index 98294115c5d2..7c65a53105a9 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordData.ts @@ -1,18 +1,21 @@ -import { useEffect, useMemo, useState } from 'react'; +import { useEffect, useState } from 'react'; import { useRecoilValue } from 'recoil'; -import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { isDefined } from '~/utils/isDefined'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; +import { useFindManyParams } from '@/object-record/record-index/hooks/useLoadRecordIndexTable'; import { EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE } from '@/object-record/record-index/options/constants/ExportTableDataDefaultPageSize'; import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard'; import { ViewType } from '@/views/types/ViewType'; -import { useFindManyParams } from '../../hooks/useLoadRecordIndexTable'; export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); @@ -21,10 +24,10 @@ export const percentage = (part: number, whole: number): number => { return Math.round((part / whole) * 100); }; -export type UseTableDataOptions = { +export type UseRecordDataOptions = { delayMs: number; maximumRequests?: number; - objectNameSingular: string; + objectMetadataItem: ObjectMetadataItem; pageSize?: number; recordIndexId: string; callback: ( @@ -40,15 +43,15 @@ type ExportProgress = { displayType: 'percentage' | 'number'; }; -export const useTableData = ({ +export const useRecordData = ({ + objectMetadataItem, delayMs, maximumRequests = 100, - objectNameSingular, pageSize = EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE, recordIndexId, callback, viewType = ViewType.Table, -}: UseTableDataOptions) => { +}: UseRecordDataOptions) => { const [isDownloading, setIsDownloading] = useState(false); const [inflight, setInflight] = useState(false); const [pageCount, setPageCount] = useState(0); @@ -57,15 +60,10 @@ export const useTableData = ({ }); const [previousRecordCount, setPreviousRecordCount] = useState(0); - const { - visibleTableColumnsSelector, - selectedRowIdsSelector, - tableRowIdsState, - hasUserSelectedAllRowsState, - } = useRecordTableStates(recordIndexId); + const { visibleTableColumnsSelector } = useRecordTableStates(recordIndexId); const { hiddenBoardFields } = useRecordIndexOptionsForBoard({ - objectNameSingular, + objectNameSingular: objectMetadataItem.nameSingular, recordBoardId: recordIndexId, viewBarId: recordIndexId, }); @@ -76,61 +74,21 @@ export const useTableData = ({ (column) => column.metadata.fieldName === kanbanFieldMetadataName, ); const columns = useRecoilValue(visibleTableColumnsSelector()); - const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); - - const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState); - const tableRowIds = useRecoilValue(tableRowIdsState); - // user has checked select all and then unselected some rows - const userHasUnselectedSomeRows = - hasUserSelectedAllRows && selectedRowIds.length < tableRowIds.length; - - const hasSelectedRows = - selectedRowIds.length > 0 && - !(hasUserSelectedAllRows && selectedRowIds.length === tableRowIds.length); + const contextStoreTargetedRecordsRule = useRecoilValue( + contextStoreTargetedRecordsRuleState, + ); - const unselectedRowIds = useMemo( - () => - userHasUnselectedSomeRows - ? tableRowIds.filter((id) => !selectedRowIds.includes(id)) - : [], - [userHasUnselectedSomeRows, tableRowIds, selectedRowIds], + const queryFilter = computeContextStoreFilters( + contextStoreTargetedRecordsRule, + objectMetadataItem, ); const findManyRecordsParams = useFindManyParams( - objectNameSingular, + objectMetadataItem.nameSingular, recordIndexId, ); - const selectedFindManyParams = { - ...findManyRecordsParams, - filter: { - ...findManyRecordsParams.filter, - id: { - in: selectedRowIds, - }, - }, - }; - - const unselectedFindManyParams = { - ...findManyRecordsParams, - filter: { - ...findManyRecordsParams.filter, - not: { - id: { - in: unselectedRowIds, - }, - }, - }, - }; - - const usedFindManyParams = - hasSelectedRows && !userHasUnselectedSomeRows - ? selectedFindManyParams - : userHasUnselectedSomeRows - ? unselectedFindManyParams - : findManyRecordsParams; - const { findManyRecords, totalCount, @@ -138,7 +96,8 @@ export const useTableData = ({ fetchMoreRecordsWithPagination, loading, } = useLazyFindManyRecords({ - ...usedFindManyParams, + ...findManyRecordsParams, + filter: queryFilter, limit: pageSize, }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx index 0dff4b429dca..828897b3bb1e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableInternalEffect.tsx @@ -1,41 +1,22 @@ import { Key } from 'ts-key-enum'; -import { SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID } from '@/object-record/record-table/constants/SoftFocusClickOutsideListenerId'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; -import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; -import { - ClickOutsideMode, - useListenClickOutsideByClassName, -} from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; +import { useListenClickOutsideByClassName } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; type RecordTableInternalEffectProps = { recordTableId: string; - tableBodyRef: React.RefObject; }; export const RecordTableInternalEffect = ({ recordTableId, - tableBodyRef, }: RecordTableInternalEffectProps) => { const { leaveTableFocus, resetTableRowSelection, useMapKeyboardToSoftFocus } = useRecordTable({ recordTableId }); useMapKeyboardToSoftFocus(); - const { useListenClickOutside } = useClickOutsideListener( - SOFT_FOCUS_CLICK_OUTSIDE_LISTENER_ID, - ); - - useListenClickOutside({ - refs: [tableBodyRef], - callback: () => { - leaveTableFocus(); - }, - mode: ClickOutsideMode.compareHTMLRef, - }); - useScopedHotkeys( [Key.Escape], () => { @@ -46,9 +27,9 @@ export const RecordTableInternalEffect = ({ useListenClickOutsideByClassName({ classNames: ['entity-table-cell'], - excludeClassNames: ['bottom-bar', 'context-menu'], + excludeClassNames: ['bottom-bar', 'action-menu-dropdown'], callback: () => { - resetTableRowSelection(); + leaveTableFocus(); }, }); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx index b7a46e64829d..43bc9c76cf2f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx @@ -87,10 +87,7 @@ export const RecordTableWithWrappers = ({ onDragSelectionChange={setRowSelected} /> - + diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts index 0a52eb083239..fc3f386432aa 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts @@ -6,21 +6,19 @@ import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotV import { TableHotkeyScope } from '../../types/TableHotkeyScope'; +import { useResetTableRowSelection } from '@/object-record/record-table/hooks/internal/useResetTableRowSelection'; import { useCloseCurrentTableCellInEditMode } from './useCloseCurrentTableCellInEditMode'; import { useDisableSoftFocus } from './useDisableSoftFocus'; -import { useSetHasUserSelectedAllRows } from './useSetAllRowSelectedState'; export const useLeaveTableFocus = (recordTableId?: string) => { const disableSoftFocus = useDisableSoftFocus(recordTableId); const closeCurrentCellInEditMode = useCloseCurrentTableCellInEditMode(recordTableId); - const setHasUserSelectedAllRows = useSetHasUserSelectedAllRows(recordTableId); - - const selectAllRows = useSetHasUserSelectedAllRows(recordTableId); - const { isSoftFocusActiveState } = useRecordTableStates(recordTableId); + const resetTableRowSelection = useResetTableRowSelection(recordTableId); + return useRecoilCallback( ({ snapshot }) => () => { @@ -33,6 +31,8 @@ export const useLeaveTableFocus = (recordTableId?: string) => { .getLoadable(currentHotkeyScopeState) .getValue(); + resetTableRowSelection(); + if (!isSoftFocusActive) { return; } @@ -43,15 +43,12 @@ export const useLeaveTableFocus = (recordTableId?: string) => { closeCurrentCellInEditMode(); disableSoftFocus(); - setHasUserSelectedAllRows(false); - selectAllRows(false); }, [ closeCurrentCellInEditMode, disableSoftFocus, isSoftFocusActiveState, - selectAllRows, - setHasUserSelectedAllRows, + resetTableRowSelection, ], ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts index 106b1174de02..af9bc7f80209 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts @@ -19,6 +19,7 @@ import { allRowsSelectedStatusComponentSelector } from '@/object-record/record-t import { hiddenTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/hiddenTableColumnsComponentSelector'; import { numberOfTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/numberOfTableColumnsComponentSelector'; import { selectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/selectedRowIdsComponentSelector'; +import { unselectedRowIdsComponentSelector } from '@/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector'; import { visibleTableColumnsComponentSelector } from '@/object-record/record-table/states/selectors/visibleTableColumnsComponentSelector'; import { softFocusPositionComponentState } from '@/object-record/record-table/states/softFocusPositionComponentState'; import { tableColumnsComponentState } from '@/object-record/record-table/states/tableColumnsComponentState'; @@ -134,6 +135,10 @@ export const useRecordTableStates = (recordTableId?: string) => { selectedRowIdsComponentSelector, scopeId, ), + unselectedRowIdsSelector: extractComponentReadOnlySelector( + unselectedRowIdsComponentSelector, + scopeId, + ), visibleTableColumnsSelector: extractComponentReadOnlySelector( visibleTableColumnsComponentSelector, scopeId, diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts index 79deb4693a2c..3bb5dc6ea08a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts @@ -46,17 +46,16 @@ export const useSetRecordTableData = ({ const recordIds = newRecords.map((record) => record.id); if (!isDeeplyEqual(currentRowIds, recordIds)) { - set(tableRowIdsState, recordIds); - } - - if (hasUserSelectedAllRows) { - for (const rowId of recordIds) { - set(isRowSelectedFamilyState(rowId), true); + if (hasUserSelectedAllRows) { + for (const rowId of recordIds) { + set(isRowSelectedFamilyState(rowId), true); + } } - } - set(numberOfTableRowsState, totalCount ?? 0); - onEntityCountChange(totalCount); + set(tableRowIdsState, recordIds); + set(numberOfTableRowsState, totalCount ?? 0); + onEntityCountChange(totalCount); + } }, [ numberOfTableRowsState, diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts index 6cad63df5448..ffd741ec7e75 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts @@ -42,6 +42,7 @@ export const useRecordTable = (props?: useRecordTableProps) => { isRecordTableInitialLoadingState, tableLastRowVisibleState, selectedRowIdsSelector, + unselectedRowIdsSelector, onToggleColumnFilterState, onToggleColumnSortState, pendingRecordIdState, @@ -223,6 +224,7 @@ export const useRecordTable = (props?: useRecordTableProps) => { setSoftFocusPosition, isSomeCellInEditModeState, selectedRowIdsSelector, + unselectedRowIdsSelector, setHasUserSelectedAllRows, setOnToggleColumnFilter, setOnToggleColumnSort, diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx index 459211537244..597d38e61dc7 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellCheckbox.tsx @@ -1,9 +1,7 @@ import styled from '@emotion/styled'; import { useCallback, useContext } from 'react'; -import { useRecoilValue } from 'recoil'; import { RecordTableRowContext } from '@/object-record/record-table/contexts/RecordTableRowContext'; -import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { RecordTableTd } from '@/object-record/record-table/record-table-cell/components/RecordTableTd'; import { useSetCurrentRowSelected } from '@/object-record/record-table/record-table-row/hooks/useSetCurrentRowSelected'; import { Checkbox } from '@/ui/input/components/Checkbox'; @@ -21,19 +19,16 @@ const StyledContainer = styled.div` export const RecordTableCellCheckbox = () => { const { isSelected } = useContext(RecordTableRowContext); - const { recordId } = useContext(RecordTableRowContext); - const { isRowSelectedFamilyState } = useRecordTableStates(); const { setCurrentRowSelected } = useSetCurrentRowSelected(); - const currentRowSelected = useRecoilValue(isRowSelectedFamilyState(recordId)); const handleClick = useCallback(() => { - setCurrentRowSelected(!currentRowSelected); - }, [currentRowSelected, setCurrentRowSelected]); + setCurrentRowSelected(!isSelected); + }, [isSelected, setCurrentRowSelected]); return ( - + ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx index 5bcfd65d67cd..912789351078 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx @@ -37,7 +37,6 @@ export const RecordTableHeaderCheckboxColumn = () => { setHasUserSelectedAllRows(true); selectAllRows(); } else { - setHasUserSelectedAllRows(false); resetTableRowSelection(); } }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector.ts b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector.ts new file mode 100644 index 000000000000..37621eacc0fa --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-table/states/selectors/unselectedRowIdsComponentSelector.ts @@ -0,0 +1,23 @@ +import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; +import { tableRowIdsComponentState } from '@/object-record/record-table/states/tableRowIdsComponentState'; +import { createComponentReadOnlySelector } from '@/ui/utilities/state/component-state/utils/createComponentReadOnlySelector'; + +export const unselectedRowIdsComponentSelector = + createComponentReadOnlySelector({ + key: 'unselectedRowIdsComponentSelector', + get: + ({ scopeId }) => + ({ get }) => { + const rowIds = get(tableRowIdsComponentState({ scopeId })); + + return rowIds.filter( + (rowId) => + get( + isRowSelectedComponentFamilyState({ + scopeId, + familyKey: rowId, + }), + ) === false, + ); + }, + }); diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx index 1ce2f5affe9c..6c538fe36209 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx +++ b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx @@ -1,11 +1,9 @@ import styled from '@emotion/styled'; -import { RecordIndexOptionsDropdown } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdown'; import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers'; import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect'; import { ViewBar } from '@/views/components/ViewBar'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; -import { ViewType } from '@/views/types/ViewType'; const StyledContainer = styled.div` display: flex; @@ -26,13 +24,7 @@ export const SignInBackgroundMockContainer = () => { {}} - optionsDropdownButton={ - - } + optionsDropdownButton={<>} /> { + + diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPageContextStoreEffect.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPageContextStoreEffect.tsx index a9a5514e285d..080b6d5a48d4 100644 --- a/packages/twenty-front/src/pages/object-record/RecordShowPageContextStoreEffect.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordShowPageContextStoreEffect.tsx @@ -1,5 +1,6 @@ import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; +import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useEffect } from 'react'; import { useParams } from 'react-router-dom'; @@ -10,8 +11,8 @@ export const RecordShowPageContextStoreEffect = ({ }: { recordId: string; }) => { - const setContextStoreTargetedRecordIds = useSetRecoilState( - contextStoreTargetedRecordIdsState, + const setContextStoreTargetedRecordsRule = useSetRecoilState( + contextStoreTargetedRecordsRuleState, ); const setContextStoreCurrentObjectMetadataId = useSetRecoilState( @@ -24,19 +25,32 @@ export const RecordShowPageContextStoreEffect = ({ objectNameSingular: objectNameSingular ?? '', }); + const setContextStoreNumberOfSelectedRecords = useSetRecoilState( + contextStoreNumberOfSelectedRecordsState, + ); + useEffect(() => { - setContextStoreTargetedRecordIds([recordId]); + setContextStoreTargetedRecordsRule({ + mode: 'selection', + selectedRecordIds: [recordId], + }); setContextStoreCurrentObjectMetadataId(objectMetadataItem?.id); + setContextStoreNumberOfSelectedRecords(1); return () => { - setContextStoreTargetedRecordIds([]); + setContextStoreTargetedRecordsRule({ + mode: 'selection', + selectedRecordIds: [], + }); setContextStoreCurrentObjectMetadataId(null); + setContextStoreNumberOfSelectedRecords(0); }; }, [ recordId, - setContextStoreTargetedRecordIds, + setContextStoreTargetedRecordsRule, setContextStoreCurrentObjectMetadataId, objectMetadataItem?.id, + setContextStoreNumberOfSelectedRecords, ]); return null; diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPageEffect.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPageEffect.tsx deleted file mode 100644 index e40a00da25ee..000000000000 --- a/packages/twenty-front/src/pages/object-record/RecordShowPageEffect.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; -import { useEffect } from 'react'; -import { useSetRecoilState } from 'recoil'; - -export const RecordShowPageEffect = ({ recordId }: { recordId: string }) => { - const setContextStoreTargetedRecordIds = useSetRecoilState( - contextStoreTargetedRecordIdsState, - ); - - useEffect(() => { - setContextStoreTargetedRecordIds([recordId]); - }, [recordId, setContextStoreTargetedRecordIds]); - - return null; -}; diff --git a/packages/twenty-front/src/testing/jest/JestContextStoreSetter.tsx b/packages/twenty-front/src/testing/jest/JestContextStoreSetter.tsx new file mode 100644 index 000000000000..866ebe143e2e --- /dev/null +++ b/packages/twenty-front/src/testing/jest/JestContextStoreSetter.tsx @@ -0,0 +1,49 @@ +import { ReactNode, useEffect, useState } from 'react'; +import { useSetRecoilState } from 'recoil'; + +import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; +import { + ContextStoreTargetedRecordsRule, + contextStoreTargetedRecordsRuleState, +} from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; + +export const JestContextStoreSetter = ({ + contextStoreTargetedRecordsRule = { + mode: 'selection', + selectedRecordIds: [], + }, + contextStoreCurrentObjectMetadataNameSingular = '', + children, +}: { + contextStoreTargetedRecordsRule?: ContextStoreTargetedRecordsRule; + contextStoreCurrentObjectMetadataNameSingular?: string; + children: ReactNode; +}) => { + const setContextStoreTargetedRecordsRule = useSetRecoilState( + contextStoreTargetedRecordsRuleState, + ); + const setContextStoreCurrentObjectMetadataId = useSetRecoilState( + contextStoreCurrentObjectMetadataIdState, + ); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: contextStoreCurrentObjectMetadataNameSingular, + }); + + const contextStoreCurrentObjectMetadataId = objectMetadataItem.id; + + const [isLoaded, setIsLoaded] = useState(false); + useEffect(() => { + setContextStoreTargetedRecordsRule(contextStoreTargetedRecordsRule); + setContextStoreCurrentObjectMetadataId(contextStoreCurrentObjectMetadataId); + setIsLoaded(true); + }, [ + setContextStoreTargetedRecordsRule, + setContextStoreCurrentObjectMetadataId, + contextStoreTargetedRecordsRule, + contextStoreCurrentObjectMetadataId, + ]); + + return isLoaded ? <>{children} : null; +}; diff --git a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper.tsx b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper.tsx new file mode 100644 index 000000000000..e674d4282114 --- /dev/null +++ b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper.tsx @@ -0,0 +1,37 @@ +import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { MockedResponse } from '@apollo/client/testing'; +import { ReactNode } from 'react'; +import { MutableSnapshot } from 'recoil'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { JestContextStoreSetter } from '~/testing/jest/JestContextStoreSetter'; + +export const getJestMetadataAndApolloMocksAndContextStoreWrapper = ({ + apolloMocks, + onInitializeRecoilSnapshot, + contextStoreTargetedRecordsRule, + contextStoreCurrentObjectMetadataNameSingular, +}: { + apolloMocks: + | readonly MockedResponse, Record>[] + | undefined; + onInitializeRecoilSnapshot?: (snapshot: MutableSnapshot) => void; + contextStoreTargetedRecordsRule?: ContextStoreTargetedRecordsRule; + contextStoreCurrentObjectMetadataNameSingular?: string; +}) => { + const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks, + onInitializeRecoilSnapshot, + }); + return ({ children }: { children: ReactNode }) => ( + + + {children} + + + ); +}; From 784770dfe8422f293c0ffca2ed445c6721b8ce53 Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Mon, 21 Oct 2024 14:23:57 +0200 Subject: [PATCH 49/67] Disable Github runners front CIs --- .github/workflows/ci-front.yaml | 46 +-------------------------------- 1 file changed, 1 insertion(+), 45 deletions(-) diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml index f42b4d23b334..0d9a5b4dc6ed 100644 --- a/.github/workflows/ci-front.yaml +++ b/.github/workflows/ci-front.yaml @@ -58,7 +58,7 @@ jobs: if: steps.changed-files.outputs.any_changed == 'true' run: npx nx storybook:build twenty-front front-sb-test: - runs-on: ci-8-cores + runs-on: shipfox-8vcpu-ubuntu-2204 timeout-minutes: 60 needs: front-sb-build strategy: @@ -82,50 +82,6 @@ jobs: if: steps.changed-files.outputs.any_changed == 'false' run: echo "No relevant changes. Skipping CI." - - name: Install dependencies - if: steps.changed-files.outputs.any_changed == 'true' - uses: ./.github/workflows/actions/yarn-install - - name: Install Playwright - if: steps.changed-files.outputs.any_changed == 'true' - run: cd packages/twenty-front && npx playwright install - - name: Front / Restore Storybook Task Cache - if: steps.changed-files.outputs.any_changed == 'true' - uses: ./.github/workflows/actions/task-cache - with: - tag: scope:frontend - tasks: storybook:build - - name: Front / Write .env - if: steps.changed-files.outputs.any_changed == 'true' - run: npx nx reset:env twenty-front - - name: Run storybook tests - if: steps.changed-files.outputs.any_changed == 'true' - run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }} - front-sb-test-shipfox: - runs-on: shipfox-8vcpu-ubuntu-2204 - timeout-minutes: 60 - needs: front-sb-build - strategy: - matrix: - storybook_scope: [pages, modules] - env: - REACT_APP_SERVER_BASE_URL: http://localhost:3000 - NX_REJECT_UNKNOWN_LOCAL_CACHE: 0 - steps: - - name: Fetch local actions - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - name: Check for changed files - id: changed-files - uses: tj-actions/changed-files@v11 - with: - files: | - packages/twenty-front/** - - - name: Skip if no relevant changes - if: steps.changed-files.outputs.any_changed == 'false' - run: echo "No relevant changes. Skipping CI." - - name: Install dependencies if: steps.changed-files.outputs.any_changed == 'true' uses: ./.github/workflows/actions/yarn-install From 28c99cbc64786afb1d2b7d03ab0e34b671a33316 Mon Sep 17 00:00:00 2001 From: Prashant Kumar <38308359+Pk9697@users.noreply.github.com> Date: Mon, 21 Oct 2024 18:02:19 +0530 Subject: [PATCH 50/67] fix: use + {isXMLMetadataValid() && ( + + )} + + +

+ + + + + + + + + + + +
+ + ); +}; diff --git a/packages/twenty-front/src/modules/settings/security/components/SettingsSecurityOptionsList.tsx b/packages/twenty-front/src/modules/settings/security/components/SettingsSecurityOptionsList.tsx new file mode 100644 index 000000000000..7e4b3b22d651 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/components/SettingsSecurityOptionsList.tsx @@ -0,0 +1,62 @@ +import { IconLink } from 'twenty-ui'; +import { SettingsOptionCardContent } from '@/settings/components/SettingsOptionCardContent'; +import { Card } from '@/ui/layout/card/components/Card'; +import styled from '@emotion/styled'; +import { Toggle } from '@/ui/input/components/Toggle'; +import { useUpdateWorkspaceMutation } from '~/generated/graphql'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useRecoilState } from 'recoil'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; + +const StyledToggle = styled(Toggle)` + margin-left: auto; +`; + +export const SettingsSecurityOptionsList = () => { + const { enqueueSnackBar } = useSnackBar(); + + const [currentWorkspace, setCurrentWorkspace] = useRecoilState( + currentWorkspaceState, + ); + + const [updateWorkspace] = useUpdateWorkspaceMutation(); + + const handleChange = async (value: boolean) => { + try { + if (!currentWorkspace?.id) { + throw new Error('User is not logged in'); + } + await updateWorkspace({ + variables: { + input: { + isPublicInviteLinkEnabled: value, + }, + }, + }); + setCurrentWorkspace({ + ...currentWorkspace, + isPublicInviteLinkEnabled: value, + }); + } catch (err: any) { + enqueueSnackBar(err?.message, { + variant: SnackBarVariant.Error, + }); + } + }; + + return ( + + + handleChange(!currentWorkspace?.isPublicInviteLinkEnabled) + } + > + + + + ); +}; diff --git a/packages/twenty-front/src/modules/settings/security/components/SettingsSecuritySSORowDropdownMenu.tsx b/packages/twenty-front/src/modules/settings/security/components/SettingsSecuritySSORowDropdownMenu.tsx new file mode 100644 index 000000000000..fa619ef2cd1d --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/components/SettingsSecuritySSORowDropdownMenu.tsx @@ -0,0 +1,102 @@ +/* @license Enterprise */ + +import { IconArchive, IconDotsVertical, IconTrash } from 'twenty-ui'; + +import { useDeleteSSOIdentityProvider } from '@/settings/security/hooks/useDeleteSSOIdentityProvider'; +import { useUpdateSSOIdentityProvider } from '@/settings/security/hooks/useUpdateSSOIdentityProvider'; +import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { UnwrapRecoilValue } from 'recoil'; +import { SsoIdentityProviderStatus } from '~/generated/graphql'; +import { isDefined } from '~/utils/isDefined'; + +type SettingsSecuritySSORowDropdownMenuProps = { + SSOIdp: UnwrapRecoilValue[0]; +}; + +export const SettingsSecuritySSORowDropdownMenu = ({ + SSOIdp, +}: SettingsSecuritySSORowDropdownMenuProps) => { + const dropdownId = `settings-account-row-${SSOIdp.id}`; + + const { enqueueSnackBar } = useSnackBar(); + + const { closeDropdown } = useDropdown(dropdownId); + + const { deleteSSOIdentityProvider } = useDeleteSSOIdentityProvider(); + const { updateSSOIdentityProvider } = useUpdateSSOIdentityProvider(); + + const handleDeleteSSOIdentityProvider = async ( + identityProviderId: string, + ) => { + const result = await deleteSSOIdentityProvider({ + identityProviderId, + }); + if (isDefined(result.errors)) { + enqueueSnackBar('Error deleting SSO Identity Provider', { + variant: SnackBarVariant.Error, + duration: 2000, + }); + } + }; + + const toggleSSOIdentityProviderStatus = async ( + identityProviderId: string, + ) => { + const result = await updateSSOIdentityProvider({ + id: identityProviderId, + status: + SSOIdp.status === 'Active' + ? SsoIdentityProviderStatus.Inactive + : SsoIdentityProviderStatus.Active, + }); + if (isDefined(result.errors)) { + enqueueSnackBar('Error editing SSO Identity Provider', { + variant: SnackBarVariant.Error, + duration: 2000, + }); + } + }; + + return ( + + } + dropdownComponents={ + + + { + toggleSSOIdentityProviderStatus(SSOIdp.id); + closeDropdown(); + }} + /> + { + handleDeleteSSOIdentityProvider(SSOIdp.id); + closeDropdown(); + }} + /> + + + } + /> + ); +}; diff --git a/packages/twenty-front/src/modules/settings/security/graphql/mutations/createOIDCSSOIdentityProvider.ts b/packages/twenty-front/src/modules/settings/security/graphql/mutations/createOIDCSSOIdentityProvider.ts new file mode 100644 index 000000000000..e0cd4b6dd323 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/graphql/mutations/createOIDCSSOIdentityProvider.ts @@ -0,0 +1,15 @@ +/* @license Enterprise */ + +import { gql } from '@apollo/client'; + +export const CREATE_OIDC_SSO_IDENTITY_PROVIDER = gql` + mutation CreateOIDCIdentityProvider($input: SetupOIDCSsoInput!) { + createOIDCIdentityProvider(input: $input) { + id + type + issuer + name + status + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/security/graphql/mutations/createSAMLSSOIdentityProvider.ts b/packages/twenty-front/src/modules/settings/security/graphql/mutations/createSAMLSSOIdentityProvider.ts new file mode 100644 index 000000000000..3729b4f504a9 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/graphql/mutations/createSAMLSSOIdentityProvider.ts @@ -0,0 +1,15 @@ +/* @license Enterprise */ + +import { gql } from '@apollo/client'; + +export const CREATE_SAML_SSO_IDENTITY_PROVIDER = gql` + mutation CreateSAMLIdentityProvider($input: SetupSAMLSsoInput!) { + createSAMLIdentityProvider(input: $input) { + id + type + issuer + name + status + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/security/graphql/mutations/deleteSSOIdentityProvider.ts b/packages/twenty-front/src/modules/settings/security/graphql/mutations/deleteSSOIdentityProvider.ts new file mode 100644 index 000000000000..d9153d33d909 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/graphql/mutations/deleteSSOIdentityProvider.ts @@ -0,0 +1,11 @@ +/* @license Enterprise */ + +import { gql } from '@apollo/client'; + +export const DELETE_SSO_IDENTITY_PROVIDER = gql` + mutation DeleteSSOIdentityProvider($input: DeleteSsoInput!) { + deleteSSOIdentityProvider(input: $input) { + identityProviderId + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/security/graphql/mutations/editSSOIdentityProvider.ts b/packages/twenty-front/src/modules/settings/security/graphql/mutations/editSSOIdentityProvider.ts new file mode 100644 index 000000000000..78a83a3b53fa --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/graphql/mutations/editSSOIdentityProvider.ts @@ -0,0 +1,15 @@ +/* @license Enterprise */ + +import { gql } from '@apollo/client'; + +export const EDIT_SSO_IDENTITY_PROVIDER = gql` + mutation EditSSOIdentityProvider($input: EditSsoInput!) { + editSSOIdentityProvider(input: $input) { + id + type + issuer + name + status + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/security/graphql/queries/getWorkspaceSSOIdentitiesProviders.ts b/packages/twenty-front/src/modules/settings/security/graphql/queries/getWorkspaceSSOIdentitiesProviders.ts new file mode 100644 index 000000000000..0fdd9701e722 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/graphql/queries/getWorkspaceSSOIdentitiesProviders.ts @@ -0,0 +1,15 @@ +/* @license Enterprise */ + +import { gql } from '@apollo/client'; + +export const LIST_WORKSPACE_SSO_IDENTITY_PROVIDERS = gql` + query ListSSOIdentityProvidersByWorkspaceId { + listSSOIdentityProvidersByWorkspaceId { + type + id + name + issuer + status + } + } +`; diff --git a/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useCreateSSOIdentityProvider.test.tsx b/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useCreateSSOIdentityProvider.test.tsx new file mode 100644 index 000000000000..50b71e727b2b --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useCreateSSOIdentityProvider.test.tsx @@ -0,0 +1,94 @@ +/* @license Enterprise */ + +import { renderHook } from '@testing-library/react'; +import { ReactNode } from 'react'; +import { RecoilRoot } from 'recoil'; + +import { useCreateSSOIdentityProvider } from '@/settings/security/hooks/useCreateSSOIdentityProvider'; + +const mutationOIDCCallSpy = jest.fn(); +const mutationSAMLCallSpy = jest.fn(); + +jest.mock('~/generated/graphql', () => ({ + useCreateOidcIdentityProviderMutation: () => [mutationOIDCCallSpy], + useCreateSamlIdentityProviderMutation: () => [mutationSAMLCallSpy], +})); + +const Wrapper = ({ children }: { children: ReactNode }) => ( + {children} +); + +describe('useCreateSSOIdentityProvider', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('create OIDC sso identity provider', async () => { + const OIDCParams = { + type: 'OIDC' as const, + name: 'test', + clientID: 'test', + clientSecret: 'test', + issuer: 'test', + }; + renderHook( + () => { + const { createSSOIdentityProvider } = useCreateSSOIdentityProvider(); + createSSOIdentityProvider(OIDCParams); + }, + { wrapper: Wrapper }, + ); + + // eslint-disable-next-line unused-imports/no-unused-vars + const { type, ...input } = OIDCParams; + expect(mutationOIDCCallSpy).toHaveBeenCalledWith({ + onCompleted: expect.any(Function), + variables: { + input, + }, + }); + }); + it('create SAML sso identity provider', async () => { + const SAMLParams = { + type: 'SAML' as const, + name: 'test', + metadata: 'test', + certificate: 'test', + id: 'test', + issuer: 'test', + ssoURL: 'test', + }; + renderHook( + () => { + const { createSSOIdentityProvider } = useCreateSSOIdentityProvider(); + createSSOIdentityProvider(SAMLParams); + }, + { wrapper: Wrapper }, + ); + + // eslint-disable-next-line unused-imports/no-unused-vars + const { type, ...input } = SAMLParams; + expect(mutationOIDCCallSpy).not.toHaveBeenCalled(); + expect(mutationSAMLCallSpy).toHaveBeenCalledWith({ + onCompleted: expect.any(Function), + variables: { + input, + }, + }); + }); + it('throw error if provider is not SAML or OIDC', async () => { + const OTHERParams = { + type: 'OTHER' as const, + }; + renderHook( + async () => { + const { createSSOIdentityProvider } = useCreateSSOIdentityProvider(); + await expect( + // @ts-expect-error - It's expected to throw an error + createSSOIdentityProvider(OTHERParams), + ).rejects.toThrowError(); + }, + { wrapper: Wrapper }, + ); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useDeleteSSOIdentityProvider.test.tsx b/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useDeleteSSOIdentityProvider.test.tsx new file mode 100644 index 000000000000..48b5e101918c --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useDeleteSSOIdentityProvider.test.tsx @@ -0,0 +1,40 @@ +/* @license Enterprise */ + +import { renderHook } from '@testing-library/react'; +import { ReactNode } from 'react'; +import { RecoilRoot } from 'recoil'; + +import { useDeleteSSOIdentityProvider } from '@/settings/security/hooks/useDeleteSSOIdentityProvider'; + +const mutationDeleteSSOIDPCallSpy = jest.fn(); + +jest.mock('~/generated/graphql', () => ({ + useDeleteSsoIdentityProviderMutation: () => [mutationDeleteSSOIDPCallSpy], +})); + +const Wrapper = ({ children }: { children: ReactNode }) => ( + {children} +); + +describe('useDeleteSsoIdentityProvider', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('delete SSO identity provider', async () => { + renderHook( + () => { + const { deleteSSOIdentityProvider } = useDeleteSSOIdentityProvider(); + deleteSSOIdentityProvider({ identityProviderId: 'test' }); + }, + { wrapper: Wrapper }, + ); + + expect(mutationDeleteSSOIDPCallSpy).toHaveBeenCalledWith({ + onCompleted: expect.any(Function), + variables: { + input: { identityProviderId: 'test' }, + }, + }); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useEditSSOIdentityProvider.test.tsx b/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useEditSSOIdentityProvider.test.tsx new file mode 100644 index 000000000000..f253f10cb432 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/hooks/__tests__/useEditSSOIdentityProvider.test.tsx @@ -0,0 +1,49 @@ +/* @license Enterprise */ + +import { renderHook } from '@testing-library/react'; +import { ReactNode } from 'react'; +import { RecoilRoot } from 'recoil'; + +import { useUpdateSSOIdentityProvider } from '@/settings/security/hooks/useUpdateSSOIdentityProvider'; +import { SsoIdentityProviderStatus } from '~/generated/graphql'; + +const mutationEditSSOIDPCallSpy = jest.fn(); + +jest.mock('~/generated/graphql', () => { + const actual = jest.requireActual('~/generated/graphql'); + return { + useEditSsoIdentityProviderMutation: () => [mutationEditSSOIDPCallSpy], + SsoIdentityProviderStatus: actual.SsoIdentityProviderStatus, + }; +}); + +const Wrapper = ({ children }: { children: ReactNode }) => ( + {children} +); + +describe('useEditSsoIdentityProvider', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Deactivate SSO identity provider', async () => { + const params = { + id: 'test', + status: SsoIdentityProviderStatus.Inactive, + }; + renderHook( + () => { + const { updateSSOIdentityProvider } = useUpdateSSOIdentityProvider(); + updateSSOIdentityProvider(params); + }, + { wrapper: Wrapper }, + ); + + expect(mutationEditSSOIDPCallSpy).toHaveBeenCalledWith({ + onCompleted: expect.any(Function), + variables: { + input: params, + }, + }); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/security/hooks/useCreateSSOIdentityProvider.ts b/packages/twenty-front/src/modules/settings/security/hooks/useCreateSSOIdentityProvider.ts new file mode 100644 index 000000000000..b7dd56f1b13e --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/hooks/useCreateSSOIdentityProvider.ts @@ -0,0 +1,63 @@ +/* @license Enterprise */ + +import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state'; +import { useSetRecoilState } from 'recoil'; +import { + CreateOidcIdentityProviderMutationVariables, + CreateSamlIdentityProviderMutationVariables, + useCreateOidcIdentityProviderMutation, + useCreateSamlIdentityProviderMutation, +} from '~/generated/graphql'; + +export const useCreateSSOIdentityProvider = () => { + const [createOidcIdentityProviderMutation] = + useCreateOidcIdentityProviderMutation(); + const [createSamlIdentityProviderMutation] = + useCreateSamlIdentityProviderMutation(); + + const setSSOIdentitiesProviders = useSetRecoilState( + SSOIdentitiesProvidersState, + ); + + const createSSOIdentityProvider = async ( + input: + | ({ + type: 'OIDC'; + } & CreateOidcIdentityProviderMutationVariables['input']) + | ({ + type: 'SAML'; + } & CreateSamlIdentityProviderMutationVariables['input']), + ) => { + if (input.type === 'OIDC') { + // eslint-disable-next-line unused-imports/no-unused-vars + const { type, ...params } = input; + return await createOidcIdentityProviderMutation({ + variables: { input: params }, + onCompleted: (data) => { + setSSOIdentitiesProviders((existingProvider) => [ + ...existingProvider, + data.createOIDCIdentityProvider, + ]); + }, + }); + } else if (input.type === 'SAML') { + // eslint-disable-next-line unused-imports/no-unused-vars + const { type, ...params } = input; + return await createSamlIdentityProviderMutation({ + variables: { input: params }, + onCompleted: (data) => { + setSSOIdentitiesProviders((existingProvider) => [ + ...existingProvider, + data.createSAMLIdentityProvider, + ]); + }, + }); + } else { + throw new Error('Invalid IdpType'); + } + }; + + return { + createSSOIdentityProvider, + }; +}; diff --git a/packages/twenty-front/src/modules/settings/security/hooks/useDeleteSSOIdentityProvider.ts b/packages/twenty-front/src/modules/settings/security/hooks/useDeleteSSOIdentityProvider.ts new file mode 100644 index 000000000000..a140444631cb --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/hooks/useDeleteSSOIdentityProvider.ts @@ -0,0 +1,40 @@ +/* @license Enterprise */ + +import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state'; +import { useSetRecoilState } from 'recoil'; +import { + DeleteSsoIdentityProviderMutationVariables, + useDeleteSsoIdentityProviderMutation, +} from '~/generated/graphql'; + +export const useDeleteSSOIdentityProvider = () => { + const [deleteSsoIdentityProviderMutation] = + useDeleteSsoIdentityProviderMutation(); + + const setSSOIdentitiesProviders = useSetRecoilState( + SSOIdentitiesProvidersState, + ); + + const deleteSSOIdentityProvider = async ({ + identityProviderId, + }: DeleteSsoIdentityProviderMutationVariables['input']) => { + return await deleteSsoIdentityProviderMutation({ + variables: { + input: { identityProviderId }, + }, + onCompleted: (data) => { + setSSOIdentitiesProviders((SSOIdentitiesProviders) => + SSOIdentitiesProviders.filter( + (identityProvider) => + identityProvider.id !== + data.deleteSSOIdentityProvider.identityProviderId, + ), + ); + }, + }); + }; + + return { + deleteSSOIdentityProvider, + }; +}; diff --git a/packages/twenty-front/src/modules/settings/security/hooks/useUpdateSSOIdentityProvider.ts b/packages/twenty-front/src/modules/settings/security/hooks/useUpdateSSOIdentityProvider.ts new file mode 100644 index 000000000000..07baaaae6a7a --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/hooks/useUpdateSSOIdentityProvider.ts @@ -0,0 +1,40 @@ +/* @license Enterprise */ + +import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProviders.state'; +import { useSetRecoilState } from 'recoil'; +import { + EditSsoIdentityProviderMutationVariables, + useEditSsoIdentityProviderMutation, +} from '~/generated/graphql'; + +export const useUpdateSSOIdentityProvider = () => { + const [editSsoIdentityProviderMutation] = + useEditSsoIdentityProviderMutation(); + + const setSSOIdentitiesProviders = useSetRecoilState( + SSOIdentitiesProvidersState, + ); + + const updateSSOIdentityProvider = async ( + payload: EditSsoIdentityProviderMutationVariables['input'], + ) => { + return await editSsoIdentityProviderMutation({ + variables: { + input: payload, + }, + onCompleted: (data) => { + setSSOIdentitiesProviders((SSOIdentitiesProviders) => + SSOIdentitiesProviders.map((identityProvider) => + identityProvider.id === data.editSSOIdentityProvider.id + ? data.editSSOIdentityProvider + : identityProvider, + ), + ); + }, + }); + }; + + return { + updateSSOIdentityProvider, + }; +}; diff --git a/packages/twenty-front/src/modules/settings/security/states/SSOIdentitiesProviders.state.ts b/packages/twenty-front/src/modules/settings/security/states/SSOIdentitiesProviders.state.ts new file mode 100644 index 000000000000..76dc7cfdfbde --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/states/SSOIdentitiesProviders.state.ts @@ -0,0 +1,11 @@ +/* @license Enterprise */ + +import { SSOIdentityProvider } from '@/settings/security/types/SSOIdentityProvider'; +import { createState } from 'twenty-ui'; + +export const SSOIdentitiesProvidersState = createState< + Omit[] +>({ + key: 'SSOIdentitiesProvidersState', + defaultValue: [], +}); diff --git a/packages/twenty-front/src/modules/settings/security/types/SSOIdentityProvider.ts b/packages/twenty-front/src/modules/settings/security/types/SSOIdentityProvider.ts new file mode 100644 index 000000000000..fe7226c9d2c4 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/types/SSOIdentityProvider.ts @@ -0,0 +1,18 @@ +/* @license Enterprise */ + +import { SSOIdentitiesProvidersParamsSchema } from '@/settings/security/validation-schemas/SSOIdentityProviderSchema'; +import { z } from 'zod'; +import { IdpType, SsoIdentityProviderStatus } from '~/generated/graphql'; + +export type SSOIdentityProvider = { + __typename: 'SSOIdentityProvider'; + id: string; + type: IdpType; + issuer: string; + name?: string | null; + status: SsoIdentityProviderStatus; +}; + +export type SettingSecurityNewSSOIdentityFormValues = z.infer< + typeof SSOIdentitiesProvidersParamsSchema +>; diff --git a/packages/twenty-front/src/modules/settings/security/utils/__tests__/parseSAMLMetadataFromXMLFile.test.ts b/packages/twenty-front/src/modules/settings/security/utils/__tests__/parseSAMLMetadataFromXMLFile.test.ts new file mode 100644 index 000000000000..e1d79168e823 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/utils/__tests__/parseSAMLMetadataFromXMLFile.test.ts @@ -0,0 +1,39 @@ +/* @license Enterprise */ + +import { parseSAMLMetadataFromXMLFile } from '../parseSAMLMetadataFromXMLFile'; + +describe('parseSAMLMetadataFromXMLFile', () => { + it('should parse SAML metadata from XML file', () => { + const xmlString = ` + + + + + test + + + + urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress + + + +`; + const result = parseSAMLMetadataFromXMLFile(xmlString); + expect(result).toEqual({ + success: true, + data: { + entityID: 'https://test.com', + ssoUrl: 'https://test.com', + certificate: 'test', + }, + }); + }); + it('should return error if XML is invalid', () => { + const xmlString = 'invalid xml'; + const result = parseSAMLMetadataFromXMLFile(xmlString); + expect(result).toEqual({ + success: false, + error: new Error('Error parsing XML'), + }); + }); +}); diff --git a/packages/twenty-front/src/modules/settings/security/utils/getColorBySSOIdentityProviderStatus.ts b/packages/twenty-front/src/modules/settings/security/utils/getColorBySSOIdentityProviderStatus.ts new file mode 100644 index 000000000000..94fd86f95dd0 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/utils/getColorBySSOIdentityProviderStatus.ts @@ -0,0 +1,13 @@ +/* @license Enterprise */ + +import { ThemeColor } from 'twenty-ui'; +import { SsoIdentityProviderStatus } from '~/generated/graphql'; + +export const getColorBySSOIdentityProviderStatus: Record< + SsoIdentityProviderStatus, + ThemeColor +> = { + Active: 'green', + Inactive: 'gray', + Error: 'red', +}; diff --git a/packages/twenty-front/src/modules/settings/security/utils/guessSSOIdentityProviderIconByUrl.ts b/packages/twenty-front/src/modules/settings/security/utils/guessSSOIdentityProviderIconByUrl.ts new file mode 100644 index 000000000000..f8582577f999 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/utils/guessSSOIdentityProviderIconByUrl.ts @@ -0,0 +1,13 @@ +/* @license Enterprise */ + +import { IconComponent, IconGoogle, IconKey } from 'twenty-ui'; + +export const guessSSOIdentityProviderIconByUrl = ( + url: string, +): IconComponent => { + if (url.includes('google')) { + return IconGoogle; + } + + return IconKey; +}; diff --git a/packages/twenty-front/src/modules/settings/security/utils/parseSAMLMetadataFromXMLFile.ts b/packages/twenty-front/src/modules/settings/security/utils/parseSAMLMetadataFromXMLFile.ts new file mode 100644 index 000000000000..2e4fdf294b2d --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/utils/parseSAMLMetadataFromXMLFile.ts @@ -0,0 +1,59 @@ +/* @license Enterprise */ + +import { z } from 'zod'; + +const validator = z.object({ + entityID: z.string().url(), + ssoUrl: z.string().url(), + certificate: z.string().min(1), +}); + +export const parseSAMLMetadataFromXMLFile = ( + xmlString: string, +): + | { success: true; data: z.infer } + | { success: false; error: unknown } => { + try { + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(xmlString, 'application/xml'); + + if (xmlDoc.getElementsByTagName('parsererror').length > 0) { + throw new Error('Error parsing XML'); + } + + const entityDescriptor = xmlDoc.getElementsByTagName( + 'md:EntityDescriptor', + )?.[0]; + const idpSSODescriptor = xmlDoc.getElementsByTagName( + 'md:IDPSSODescriptor', + )?.[0]; + const keyDescriptor = xmlDoc.getElementsByTagName('md:KeyDescriptor')[0]; + const keyInfo = keyDescriptor.getElementsByTagName('ds:KeyInfo')[0]; + const x509Data = keyInfo.getElementsByTagName('ds:X509Data')[0]; + const x509Certificate = x509Data + .getElementsByTagName('ds:X509Certificate')?.[0] + .textContent?.trim(); + + const singleSignOnServices = Array.from( + idpSSODescriptor.getElementsByTagName('md:SingleSignOnService'), + ).map((service) => ({ + Binding: service.getAttribute('Binding'), + Location: service.getAttribute('Location'), + })); + + const result = { + ssoUrl: singleSignOnServices.find((singleSignOnService) => { + return ( + singleSignOnService.Binding === + 'urn:oasis:names:tc:SAML:2.0:bindings:HTTP-Redirect' + ); + })?.Location, + certificate: x509Certificate, + entityID: entityDescriptor?.getAttribute('entityID'), + }; + + return { success: true, data: validator.parse(result) }; + } catch (error) { + return { success: false, error }; + } +}; diff --git a/packages/twenty-front/src/modules/settings/security/utils/sSOIdentityProviderDefaultValues.ts b/packages/twenty-front/src/modules/settings/security/utils/sSOIdentityProviderDefaultValues.ts new file mode 100644 index 000000000000..a5358e948b86 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/utils/sSOIdentityProviderDefaultValues.ts @@ -0,0 +1,25 @@ +/* @license Enterprise */ + +import { SettingSecurityNewSSOIdentityFormValues } from '@/settings/security/types/SSOIdentityProvider'; +import { IdpType } from '~/generated/graphql'; + +export const sSOIdentityProviderDefaultValues: Record< + IdpType, + () => SettingSecurityNewSSOIdentityFormValues +> = { + SAML: () => ({ + type: 'SAML', + ssoURL: '', + name: '', + id: crypto.randomUUID(), + certificate: '', + issuer: '', + }), + OIDC: () => ({ + type: 'OIDC', + name: '', + clientID: '', + clientSecret: '', + issuer: '', + }), +}; diff --git a/packages/twenty-front/src/modules/settings/security/validation-schemas/SSOIdentityProviderSchema.ts b/packages/twenty-front/src/modules/settings/security/validation-schemas/SSOIdentityProviderSchema.ts new file mode 100644 index 000000000000..adfd8680f590 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/security/validation-schemas/SSOIdentityProviderSchema.ts @@ -0,0 +1,34 @@ +/* @license Enterprise */ + +import { z } from 'zod'; + +export const SSOIdentitiesProvidersOIDCParamsSchema = z + .object({ + type: z.literal('OIDC'), + clientID: z.string().optional(), + clientSecret: z.string().optional(), + }) + .required(); + +export const SSOIdentitiesProvidersSAMLParamsSchema = z + .object({ + type: z.literal('SAML'), + id: z.string().optional(), + ssoURL: z.string().url().optional(), + certificate: z.string().optional(), + }) + .required(); + +export const SSOIdentitiesProvidersParamsSchema = z + .discriminatedUnion('type', [ + SSOIdentitiesProvidersOIDCParamsSchema, + SSOIdentitiesProvidersSAMLParamsSchema, + ]) + .and( + z + .object({ + name: z.string().min(1), + issuer: z.string().url().optional(), + }) + .required(), + ); diff --git a/packages/twenty-front/src/modules/types/SettingsPath.ts b/packages/twenty-front/src/modules/types/SettingsPath.ts index 96efe89cdb7a..2d7b9ebdcbc2 100644 --- a/packages/twenty-front/src/modules/types/SettingsPath.ts +++ b/packages/twenty-front/src/modules/types/SettingsPath.ts @@ -30,6 +30,9 @@ export enum SettingsPath { IntegrationDatabaseConnection = 'integrations/:databaseKey/:connectionId', IntegrationEditDatabaseConnection = 'integrations/:databaseKey/:connectionId/edit', IntegrationNewDatabaseConnection = 'integrations/:databaseKey/new', + Security = 'security', + NewSSOIdentityProvider = 'security/sso/new', + EditSSOIdentityProvider = 'security/sso/:identityProviderId', DevelopersNewWebhook = 'webhooks/new', DevelopersNewWebhookDetail = 'webhooks/:webhookId', Releases = 'releases', diff --git a/packages/twenty-front/src/modules/ui/input/button/components/MainButton.tsx b/packages/twenty-front/src/modules/ui/input/button/components/MainButton.tsx index 333eaeb2fb34..cb9dbd27191e 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/MainButton.tsx +++ b/packages/twenty-front/src/modules/ui/input/button/components/MainButton.tsx @@ -77,6 +77,7 @@ const StyledButton = styled.button< justify-content: center; outline: none; padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(3)}; + max-height: ${({ theme }) => theme.spacing(8)}; width: ${({ fullWidth, width }) => fullWidth ? '100%' : width ? `${width}px` : 'auto'}; ${({ theme, variant, disabled }) => { diff --git a/packages/twenty-front/src/modules/ui/input/components/Toggle.tsx b/packages/twenty-front/src/modules/ui/input/components/Toggle.tsx index cd45fdca1fba..f6c28cc2f571 100644 --- a/packages/twenty-front/src/modules/ui/input/components/Toggle.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/Toggle.tsx @@ -39,7 +39,7 @@ const StyledCircle = styled(motion.span)<{ export type ToggleProps = { id?: string; value?: boolean; - onChange?: (value: boolean) => void; + onChange?: (value: boolean, e?: React.MouseEvent) => void; color?: string; toggleSize?: ToggleSize; className?: string; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts index 7a04238d1e53..dfdd4082bbfa 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching.ts @@ -6,11 +6,24 @@ import { AppPath } from '@/types/AppPath'; import { useGenerateJwtMutation } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; import { sleep } from '~/utils/sleep'; +import { useSSO } from '@/auth/sign-in-up/hooks/useSSO'; +import { + SignInUpStep, + signInUpStepState, +} from '@/auth/states/signInUpStepState'; +import { availableSSOIdentityProvidersState } from '@/auth/states/availableWorkspacesForSSO'; +import { useAuth } from '@/auth/hooks/useAuth'; export const useWorkspaceSwitching = () => { const setTokenPair = useSetRecoilState(tokenPairState); const [generateJWT] = useGenerateJwtMutation(); + const { redirectToSSOLoginPage } = useSSO(); const currentWorkspace = useRecoilValue(currentWorkspaceState); + const setAvailableWorkspacesForSSOState = useSetRecoilState( + availableSSOIdentityProvidersState, + ); + const setSignInUpStep = useSetRecoilState(signInUpStepState); + const { signOut } = useAuth(); const switchWorkspace = async (workspaceId: string) => { if (currentWorkspace?.id === workspaceId) return; @@ -28,10 +41,34 @@ export const useWorkspaceSwitching = () => { throw new Error('could not create token'); } - const { tokens } = jwt.data.generateJWT; - setTokenPair(tokens); - await sleep(0); // This hacky workaround is necessary to ensure the tokens stored in the cookie are updated correctly. - window.location.href = AppPath.Index; + if ( + jwt.data.generateJWT.reason === 'WORKSPACE_USE_SSO_AUTH' && + 'availableSSOIDPs' in jwt.data.generateJWT + ) { + if (jwt.data.generateJWT.availableSSOIDPs.length === 1) { + redirectToSSOLoginPage(jwt.data.generateJWT.availableSSOIDPs[0].id); + } + + if (jwt.data.generateJWT.availableSSOIDPs.length > 1) { + await signOut(); + setAvailableWorkspacesForSSOState( + jwt.data.generateJWT.availableSSOIDPs, + ); + setSignInUpStep(SignInUpStep.SSOWorkspaceSelection); + } + + return; + } + + if ( + jwt.data.generateJWT.reason !== 'WORKSPACE_USE_SSO_AUTH' && + 'authTokens' in jwt.data.generateJWT + ) { + const { tokens } = jwt.data.generateJWT.authTokens; + setTokenPair(tokens); + await sleep(0); // This hacky workaround is necessary to ensure the tokens stored in the cookie are updated correctly. + window.location.href = AppPath.Index; + } }; return { switchWorkspace }; diff --git a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts index 8cdb26be8a17..5ca3faa9ac31 100644 --- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts +++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts @@ -24,6 +24,7 @@ export const USER_QUERY_FRAGMENT = gql` inviteHash allowImpersonation activationStatus + isPublicInviteLinkEnabled featureFlags { id key diff --git a/packages/twenty-front/src/modules/workspace-invitation/hooks/__tests__/useCreateWorkspaceInvitation.test.tsx b/packages/twenty-front/src/modules/workspace-invitation/hooks/__tests__/useCreateWorkspaceInvitation.test.tsx new file mode 100644 index 000000000000..a14f17012d82 --- /dev/null +++ b/packages/twenty-front/src/modules/workspace-invitation/hooks/__tests__/useCreateWorkspaceInvitation.test.tsx @@ -0,0 +1,36 @@ +import { renderHook } from '@testing-library/react'; +import { ReactNode } from 'react'; +import { RecoilRoot } from 'recoil'; +import { useCreateWorkspaceInvitation } from '@/workspace-invitation/hooks/useCreateWorkspaceInvitation'; + +const mutationSendInvitationsCallSpy = jest.fn(); + +jest.mock('~/generated/graphql', () => ({ + useSendInvitationsMutation: () => [mutationSendInvitationsCallSpy], +})); + +const Wrapper = ({ children }: { children: ReactNode }) => ( + {children} +); + +describe('useCreateWorkspaceInvitation', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Send invitations', async () => { + const invitationParams = { emails: ['test@twenty.com'] }; + renderHook( + () => { + const { sendInvitation } = useCreateWorkspaceInvitation(); + sendInvitation(invitationParams); + }, + { wrapper: Wrapper }, + ); + + expect(mutationSendInvitationsCallSpy).toHaveBeenCalledWith({ + onCompleted: expect.any(Function), + variables: invitationParams, + }); + }); +}); diff --git a/packages/twenty-front/src/modules/workspace-invitation/hooks/__tests__/useDeleteWorkspaceInvitation.test.tsx b/packages/twenty-front/src/modules/workspace-invitation/hooks/__tests__/useDeleteWorkspaceInvitation.test.tsx new file mode 100644 index 000000000000..5075d7225cda --- /dev/null +++ b/packages/twenty-front/src/modules/workspace-invitation/hooks/__tests__/useDeleteWorkspaceInvitation.test.tsx @@ -0,0 +1,38 @@ +import { renderHook } from '@testing-library/react'; +import { ReactNode } from 'react'; +import { RecoilRoot } from 'recoil'; +import { useDeleteWorkspaceInvitation } from '@/workspace-invitation/hooks/useDeleteWorkspaceInvitation'; + +const mutationDeleteWorspaceInvitationCallSpy = jest.fn(); + +jest.mock('~/generated/graphql', () => ({ + useDeleteWorkspaceInvitationMutation: () => [ + mutationDeleteWorspaceInvitationCallSpy, + ], +})); + +const Wrapper = ({ children }: { children: ReactNode }) => ( + {children} +); + +describe('useDeleteWorkspaceInvitation', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Delete Workspace Invitation', async () => { + const params = { appTokenId: 'test' }; + renderHook( + () => { + const { deleteWorkspaceInvitation } = useDeleteWorkspaceInvitation(); + deleteWorkspaceInvitation(params); + }, + { wrapper: Wrapper }, + ); + + expect(mutationDeleteWorspaceInvitationCallSpy).toHaveBeenCalledWith({ + onCompleted: expect.any(Function), + variables: params, + }); + }); +}); diff --git a/packages/twenty-front/src/modules/workspace-invitation/hooks/__tests__/useResendWorkspaceInvitation.test.tsx b/packages/twenty-front/src/modules/workspace-invitation/hooks/__tests__/useResendWorkspaceInvitation.test.tsx new file mode 100644 index 000000000000..97456ca39c1d --- /dev/null +++ b/packages/twenty-front/src/modules/workspace-invitation/hooks/__tests__/useResendWorkspaceInvitation.test.tsx @@ -0,0 +1,38 @@ +import { renderHook } from '@testing-library/react'; +import { ReactNode } from 'react'; +import { RecoilRoot } from 'recoil'; +import { useResendWorkspaceInvitation } from '@/workspace-invitation/hooks/useResendWorkspaceInvitation'; + +const mutationResendWorspaceInvitationCallSpy = jest.fn(); + +jest.mock('~/generated/graphql', () => ({ + useResendWorkspaceInvitationMutation: () => [ + mutationResendWorspaceInvitationCallSpy, + ], +})); + +const Wrapper = ({ children }: { children: ReactNode }) => ( + {children} +); + +describe('useResendWorkspaceInvitation', () => { + afterEach(() => { + jest.clearAllMocks(); + }); + + it('Resend Workspace Invitation', async () => { + const params = { appTokenId: 'test' }; + renderHook( + () => { + const { resendInvitation } = useResendWorkspaceInvitation(); + resendInvitation(params); + }, + { wrapper: Wrapper }, + ); + + expect(mutationResendWorspaceInvitationCallSpy).toHaveBeenCalledWith({ + onCompleted: expect.any(Function), + variables: params, + }); + }); +}); diff --git a/packages/twenty-front/src/modules/workspace-invitation/hooks/useCreateWorkspaceInvitation.ts b/packages/twenty-front/src/modules/workspace-invitation/hooks/useCreateWorkspaceInvitation.ts index 6894f9b700e4..5ee4a97fb7c2 100644 --- a/packages/twenty-front/src/modules/workspace-invitation/hooks/useCreateWorkspaceInvitation.ts +++ b/packages/twenty-front/src/modules/workspace-invitation/hooks/useCreateWorkspaceInvitation.ts @@ -1,6 +1,8 @@ import { useSetRecoilState } from 'recoil'; -import { useSendInvitationsMutation } from '~/generated/graphql'; -import { SendInvitationsMutationVariables } from '../../../generated/graphql'; +import { + useSendInvitationsMutation, + SendInvitationsMutationVariables, +} from '~/generated/graphql'; import { workspaceInvitationsState } from '../states/workspaceInvitationsStates'; export const useCreateWorkspaceInvitation = () => { diff --git a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts index f0346d505548..5573956fddbe 100644 --- a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts +++ b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts @@ -13,5 +13,6 @@ export type FeatureFlagKey = | 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED' | 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED' | 'IS_ANALYTICS_V2_ENABLED' + | 'IS_SSO_ENABLED' | 'IS_UNIQUE_INDEXES_ENABLED' | 'IS_ARRAY_AND_JSON_FILTER_ENABLED'; diff --git a/packages/twenty-front/src/pages/auth/Invite.tsx b/packages/twenty-front/src/pages/auth/Invite.tsx index 7260a6b59041..f14b7ea751ef 100644 --- a/packages/twenty-front/src/pages/auth/Invite.tsx +++ b/packages/twenty-front/src/pages/auth/Invite.tsx @@ -91,25 +91,7 @@ export const Invite = () => { fullWidth /> - - By using Twenty, you agree to the{' '} - - Terms of Service - {' '} - and{' '} - - Privacy Policy - - . - + ) : ( diff --git a/packages/twenty-front/src/pages/auth/SSOWorkspaceSelection.tsx b/packages/twenty-front/src/pages/auth/SSOWorkspaceSelection.tsx new file mode 100644 index 000000000000..6d3e604a5893 --- /dev/null +++ b/packages/twenty-front/src/pages/auth/SSOWorkspaceSelection.tsx @@ -0,0 +1,69 @@ +/* @license Enterprise */ + +import { FooterNote } from '@/auth/sign-in-up/components/FooterNote'; +import { HorizontalSeparator } from '@/auth/sign-in-up/components/HorizontalSeparator'; +import { useSSO } from '@/auth/sign-in-up/hooks/useSSO'; +import { availableSSOIdentityProvidersState } from '@/auth/states/availableWorkspacesForSSO'; +import { guessSSOIdentityProviderIconByUrl } from '@/settings/security/utils/guessSSOIdentityProviderIconByUrl'; +import { MainButton } from '@/ui/input/button/components/MainButton'; +import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName'; +import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; + +const StyledContentContainer = styled.div` + margin-bottom: ${({ theme }) => theme.spacing(8)}; + margin-top: ${({ theme }) => theme.spacing(4)}; +`; + +const StyledTitle = styled.h2` + color: ${({ theme }) => theme.font.color.primary}; + font-size: ${({ theme }) => theme.font.size.md}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + margin: 0; +`; + +export const SSOWorkspaceSelection = () => { + const availableSSOIdentityProviders = useRecoilValue( + availableSSOIdentityProvidersState, + ); + + const { redirectToSSOLoginPage } = useSSO(); + + const availableWorkspacesForSSOGroupByWorkspace = + availableSSOIdentityProviders.reduce( + (acc, idp) => { + acc[idp.workspace.id] = [...(acc[idp.workspace.id] ?? []), idp]; + return acc; + }, + {} as Record, + ); + + return ( + <> + + {Object.values(availableWorkspacesForSSOGroupByWorkspace).map( + (idps) => ( + <> + + {idps[0].workspace.displayName ?? DEFAULT_WORKSPACE_NAME} + + + {idps.map((idp) => ( + <> + redirectToSSOLoginPage(idp.id)} + Icon={guessSSOIdentityProviderIconByUrl(idp.issuer)} + fullWidth + /> + + + ))} + + ), + )} + + + + ); +}; diff --git a/packages/twenty-front/src/pages/auth/SignInUp.tsx b/packages/twenty-front/src/pages/auth/SignInUp.tsx index 719e665baf7c..d9eefac9ed39 100644 --- a/packages/twenty-front/src/pages/auth/SignInUp.tsx +++ b/packages/twenty-front/src/pages/auth/SignInUp.tsx @@ -4,15 +4,14 @@ import { useRecoilValue } from 'recoil'; import { Logo } from '@/auth/components/Logo'; import { Title } from '@/auth/components/Title'; import { SignInUpForm } from '@/auth/sign-in-up/components/SignInUpForm'; -import { - SignInUpMode, - SignInUpStep, - useSignInUp, -} from '@/auth/sign-in-up/hooks/useSignInUp'; +import { SignInUpMode, useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp'; import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn'; import { isDefined } from '~/utils/isDefined'; +import { SignInUpStep } from '@/auth/states/signInUpStepState'; +import { IconLockCustom } from '@ui/display/icon/components/IconLock'; +import { SSOWorkspaceSelection } from './SSOWorkspaceSelection'; export const SignInUp = () => { const { form } = useSignInUpForm(); @@ -27,6 +26,9 @@ export const SignInUp = () => { ) { return 'Welcome to Twenty'; } + if (signInUpStep === SignInUpStep.SSOWorkspaceSelection) { + return 'Choose SSO connection'; + } return signInUpMode === SignInUpMode.SignIn ? 'Sign in to Twenty' : 'Sign up to Twenty'; @@ -39,10 +41,18 @@ export const SignInUp = () => { return ( <> - + {signInUpStep === SignInUpStep.SSOWorkspaceSelection ? ( + + ) : ( + + )} {title} - + {signInUpStep === SignInUpStep.SSOWorkspaceSelection ? ( + + ) : ( + + )} ); }; diff --git a/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx b/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx index 033f103a9ff9..6d60faf1bb24 100644 --- a/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx +++ b/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx @@ -148,17 +148,18 @@ export const SettingsWorkspaceMembers = () => { ]} > - {currentWorkspace?.inviteHash && ( -
- - -
- )} + {currentWorkspace?.inviteHash && + currentWorkspace?.isPublicInviteLinkEnabled && ( +
+ + +
+ )}
{ + return ( + } + links={[ + { + children: 'Workspace', + href: getSettingsPagePath(SettingsPath.Workspace), + }, + { children: 'Security' }, + ]} + > + +
+ + +
+
+ + +
+
+
+ ); +}; diff --git a/packages/twenty-front/src/pages/settings/security/SettingsSecuritySSOIdentifyProvider.tsx b/packages/twenty-front/src/pages/settings/security/SettingsSecuritySSOIdentifyProvider.tsx new file mode 100644 index 000000000000..2f7cc0a079a8 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/security/SettingsSecuritySSOIdentifyProvider.tsx @@ -0,0 +1,86 @@ +/* @license Enterprise */ + +import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; +import SettingsSSOIdentitiesProvidersForm from '@/settings/security/components/SettingsSSOIdentitiesProvidersForm'; +import { useCreateSSOIdentityProvider } from '@/settings/security/hooks/useCreateSSOIdentityProvider'; +import { SettingSecurityNewSSOIdentityFormValues } from '@/settings/security/types/SSOIdentityProvider'; +import { sSOIdentityProviderDefaultValues } from '@/settings/security/utils/sSOIdentityProviderDefaultValues'; +import { SSOIdentitiesProvidersParamsSchema } from '@/settings/security/validation-schemas/SSOIdentityProviderSchema'; +import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; +import { SettingsPath } from '@/types/SettingsPath'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { useEffect } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { useNavigate } from 'react-router-dom'; + +export const SettingsSecuritySSOIdentifyProvider = () => { + const navigate = useNavigate(); + + const { enqueueSnackBar } = useSnackBar(); + const { createSSOIdentityProvider } = useCreateSSOIdentityProvider(); + + const formConfig = useForm({ + mode: 'onChange', + resolver: zodResolver(SSOIdentitiesProvidersParamsSchema), + defaultValues: Object.values(sSOIdentityProviderDefaultValues).reduce( + (acc, fn) => ({ ...acc, ...fn() }), + {}, + ), + }); + + const selectedType = formConfig.watch('type'); + + useEffect( + () => + formConfig.reset({ + ...sSOIdentityProviderDefaultValues[selectedType](), + name: formConfig.getValues('name'), + }), + [formConfig, selectedType], + ); + + const handleSave = async () => { + try { + await createSSOIdentityProvider(formConfig.getValues()); + navigate(getSettingsPagePath(SettingsPath.Security)); + } catch (error) { + enqueueSnackBar((error as Error).message, { + variant: SnackBarVariant.Error, + }); + } + }; + + return ( + navigate(getSettingsPagePath(SettingsPath.Security))} + onSave={handleSave} + /> + } + links={[ + { + children: 'Workspace', + href: getSettingsPagePath(SettingsPath.Workspace), + }, + { + children: 'Security', + href: getSettingsPagePath(SettingsPath.Security), + }, + { children: 'New' }, + ]} + > + + + + + ); +}; diff --git a/packages/twenty-front/src/testing/mock-data/config.ts b/packages/twenty-front/src/testing/mock-data/config.ts index 1ed65869a7ca..6e8ade28b55f 100644 --- a/packages/twenty-front/src/testing/mock-data/config.ts +++ b/packages/twenty-front/src/testing/mock-data/config.ts @@ -6,7 +6,9 @@ export const mockedClientConfig: ClientConfig = { signUpDisabled: false, chromeExtensionId: 'MOCKED_EXTENSION_ID', debugMode: false, + analyticsEnabled: true, authProviders: { + sso: false, google: true, password: true, magicLink: false, diff --git a/packages/twenty-front/src/testing/mock-data/users.ts b/packages/twenty-front/src/testing/mock-data/users.ts index cc483bd1e570..b8af2e37a0cb 100644 --- a/packages/twenty-front/src/testing/mock-data/users.ts +++ b/packages/twenty-front/src/testing/mock-data/users.ts @@ -40,6 +40,7 @@ export const mockDefaultWorkspace: Workspace = { domainName: 'twenty.com', inviteHash: 'twenty.com-invite-hash', logo: workspaceLogoUrl, + isPublicInviteLinkEnabled: true, allowImpersonation: true, activationStatus: WorkspaceActivationStatus.Active, featureFlags: [ diff --git a/packages/twenty-server/.env.example b/packages/twenty-server/.env.example index 0b520154c4eb..0ea9a607a946 100644 --- a/packages/twenty-server/.env.example +++ b/packages/twenty-server/.env.example @@ -37,6 +37,7 @@ REDIS_URL=redis://localhost:6379 # AUTH_GOOGLE_CLIENT_SECRET=replace_me_with_google_client_secret # AUTH_GOOGLE_CALLBACK_URL=http://localhost:3000/auth/google/redirect # AUTH_GOOGLE_APIS_CALLBACK_URL=http://localhost:3000/auth/google-apis/get-access-token +# AUTH_SSO_ENABLED=false # SERVERLESS_TYPE=local # STORAGE_TYPE=local # STORAGE_LOCAL_PATH=.local-storage @@ -74,3 +75,5 @@ REDIS_URL=redis://localhost:6379 # MUTATION_MAXIMUM_AFFECTED_RECORDS=100 # CHROME_EXTENSION_ID=bggmipldbceihilonnbpgoeclgbkblkp # PG_SSL_ALLOW_SELF_SIGNED=true +# SESSION_STORE_SECRET=replace_me_with_a_random_string_session +# ENTERPRISE_KEY=replace_me_with_a_valid_enterprise_key diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index c5778aa6493d..82fca3b04d00 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -23,12 +23,15 @@ "@nestjs/cache-manager": "^2.2.1", "@nestjs/devtools-integration": "^0.1.6", "@nestjs/graphql": "patch:@nestjs/graphql@12.1.1#./patches/@nestjs+graphql+12.1.1.patch", + "@node-saml/passport-saml": "^5.0.0", "@ptc-org/nestjs-query-graphql": "patch:@ptc-org/nestjs-query-graphql@4.2.0#./patches/@ptc-org+nestjs-query-graphql+4.2.0.patch", "@revertdotdev/revert-react": "^0.0.21", "@sentry/nestjs": "^8.30.0", "cache-manager": "^5.4.0", "cache-manager-redis-yet": "^4.1.2", "class-validator": "patch:class-validator@0.14.0#./patches/class-validator+0.14.0.patch", + "connect-redis": "^7.1.1", + "express-session": "^1.18.1", "graphql-middleware": "^6.1.35", "handlebars": "^4.7.8", "jsdom": "~22.1.0", @@ -42,8 +45,10 @@ "lodash.uniqby": "^4.7.0", "monaco-editor": "^0.51.0", "monaco-editor-auto-typings": "^0.4.5", + "openid-client": "^5.7.0", "passport": "^0.7.0", "psl": "^1.9.0", + "redis": "^4.7.0", "ts-morph": "^24.0.0", "tsconfig-paths": "^4.2.0", "typeorm": "patch:typeorm@0.3.20#./patches/typeorm+0.3.20.patch", @@ -53,6 +58,7 @@ "devDependencies": { "@nestjs/cli": "10.3.0", "@nx/js": "18.3.3", + "@types/express-session": "^1.18.0", "@types/lodash.differencewith": "^4.5.9", "@types/lodash.isempty": "^4.4.7", "@types/lodash.isequal": "^4.5.8", @@ -64,6 +70,7 @@ "@types/lodash.uniq": "^4.5.9", "@types/lodash.uniqby": "^4.7.9", "@types/lodash.upperfirst": "^4.3.7", + "@types/openid-client": "^3.7.0", "@types/react": "^18.2.39", "@types/unzipper": "^0", "rimraf": "^5.0.5", diff --git a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts index 97b34a8844c7..b3d984156eb8 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/core/feature-flags.ts @@ -60,6 +60,11 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: true, }, + { + key: FeatureFlagKey.IsSSOEnabled, + workspaceId: workspaceId, + value: true, + }, { key: FeatureFlagKey.IsGmailSendEmailScopeEnabled, workspaceId: workspaceId, diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1727181198403-addWorkspaceSSOIdentityProvider.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1727181198403-addWorkspaceSSOIdentityProvider.ts new file mode 100644 index 000000000000..413e9c57adc4 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/1727181198403-addWorkspaceSSOIdentityProvider.ts @@ -0,0 +1,66 @@ +/* @license Enterprise */ + +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddWorkspaceSSOIdentityProvider1727181198403 + implements MigrationInterface +{ + name = 'AddWorkspaceSSOIdentityProvider1727181198403'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + CREATE TYPE "core"."idp_type_enum" AS ENUM('OIDC', 'SAML'); + `); + + await queryRunner.query(` + CREATE TABLE "core"."workspaceSSOIdentityProvider" ( + "id" uuid DEFAULT uuid_generate_v4() PRIMARY KEY, + "name" varchar NULL, + "workspaceId" uuid NOT NULL, + "createdAt" timestamptz DEFAULT now() NOT NULL, + "updatedAt" timestamptz DEFAULT now() NOT NULL, + "type" "core"."idp_type_enum" DEFAULT 'OIDC' NOT NULL, + "issuer" varchar NOT NULL, + "ssoURL" varchar NULL, + "clientID" varchar NULL, + "clientSecret" varchar NULL, + "certificate" varchar NULL, + "fingerprint" varchar NULL, + "status" varchar DEFAULT 'Active' NOT NULL + ); + `); + + await queryRunner.query(` + ALTER TABLE "core"."workspaceSSOIdentityProvider" + ADD CONSTRAINT "FK_workspaceId" + FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") + ON DELETE CASCADE; + `); + + await queryRunner.query(` + ALTER TABLE "core"."workspaceSSOIdentityProvider" ADD CONSTRAINT "CHK_OIDC" CHECK ( + ("type" = 'OIDC' AND "clientID" IS NOT NULL AND "clientSecret" IS NOT NULL) OR "type" = 'SAML' + ) + `); + await queryRunner.query(` + ALTER TABLE "core"."workspaceSSOIdentityProvider" ADD CONSTRAINT "CHK_SAML" CHECK ( + ("type" = 'SAML' AND "ssoURL" IS NOT NULL AND "certificate" IS NOT NULL) OR "type" = 'OIDC' + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(` + ALTER TABLE "core"."workspaceSSOIdentityProvider" + DROP CONSTRAINT "FK_workspaceId"; + `); + + await queryRunner.query(` + DROP TABLE "core"."workspaceSSOIdentityProvider"; + `); + + await queryRunner.query(` + DROP TYPE "core"."idp_type_enum"; + `); + } +} diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1728986317196-addIsPublicInviteLinkEnabledOnWorkspace.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1728986317196-addIsPublicInviteLinkEnabledOnWorkspace.ts new file mode 100644 index 000000000000..38177a558e7f --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/1728986317196-addIsPublicInviteLinkEnabledOnWorkspace.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddIsPublicInviteLinkEnabledOnWorkspace1728986317196 + implements MigrationInterface +{ + name = 'AddIsPublicInviteLinkEnabledOnWorkspace1728986317196'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."workspace" ADD "isPublicInviteLinkEnabled" boolean NOT NULL DEFAULT true`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "core"."workspace" DROP COLUMN "isPublicInviteLinkEnabled"`, + ); + } +} diff --git a/packages/twenty-server/src/database/typeorm/typeorm.service.ts b/packages/twenty-server/src/database/typeorm/typeorm.service.ts index 46b546d65ee5..73466982d07b 100644 --- a/packages/twenty-server/src/database/typeorm/typeorm.service.ts +++ b/packages/twenty-server/src/database/typeorm/typeorm.service.ts @@ -13,6 +13,7 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; +import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; @Injectable() export class TypeORMService implements OnModuleInit, OnModuleDestroy { @@ -36,6 +37,7 @@ export class TypeORMService implements OnModuleInit, OnModuleDestroy { BillingSubscription, BillingSubscriptionItem, PostgresCredentials, + WorkspaceSSOIdentityProvider, ], metadataTableName: '_typeorm_generated_columns_and_materialized_views', ssl: environmentService.get('PG_SSL_ALLOW_SELF_SIGNED') diff --git a/packages/twenty-server/src/engine/core-modules/app-token/app-token.entity.ts b/packages/twenty-server/src/engine/core-modules/app-token/app-token.entity.ts index 998fa634a757..38f17ddaa7fc 100644 --- a/packages/twenty-server/src/engine/core-modules/app-token/app-token.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/app-token/app-token.entity.ts @@ -22,6 +22,7 @@ export enum AppTokenType { AuthorizationCode = 'AUTHORIZATION_CODE', PasswordResetToken = 'PASSWORD_RESET_TOKEN', InvitationToken = 'INVITATION_TOKEN', + OIDCCodeVerifier = 'OIDC_CODE_VERIFIER', } @Entity({ name: 'appToken', schema: 'core' }) diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts index 62b215f2691b..43d4d7312e4e 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.exception.ts @@ -17,4 +17,6 @@ export enum AuthExceptionCode { INVALID_DATA = 'INVALID_DATA', INTERNAL_SERVER_ERROR = 'INTERNAL_SERVER_ERROR', OAUTH_ACCESS_DENIED = 'OAUTH_ACCESS_DENIED', + SSO_AUTH_FAILED = 'SSO_AUTH_FAILED', + USE_SSO_AUTH = 'USE_SSO_AUTH', } diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts index 708472826d10..6a102e8b360c 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts @@ -27,7 +27,13 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module'; import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module'; +import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module'; +import { SSOAuthController } from 'src/engine/core-modules/auth/controllers/sso-auth.controller'; +import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; +import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity'; +import { SamlAuthStrategy } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; +import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module'; import { AuthResolver } from './auth.resolver'; @@ -43,7 +49,14 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; WorkspaceManagerModule, TypeORMModule, TypeOrmModule.forFeature( - [Workspace, User, AppToken, FeatureFlagEntity], + [ + Workspace, + User, + AppToken, + FeatureFlagEntity, + WorkspaceSSOIdentityProvider, + KeyValuePair, + ], 'core', ), HttpModule, @@ -52,7 +65,9 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; WorkspaceModule, OnboardingModule, WorkspaceDataSourceModule, + WorkspaceInvitationModule, ConnectedAccountModule, + WorkspaceSSOModule, FeatureFlagModule, ], controllers: [ @@ -60,11 +75,13 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; MicrosoftAuthController, GoogleAPIsAuthController, VerifyAuthController, + SSOAuthController, ], providers: [ SignInUpService, AuthService, JwtAuthStrategy, + SamlAuthStrategy, AuthResolver, TokenService, GoogleAPIsService, diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts index 033210af120a..2e470589ef07 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts @@ -24,6 +24,11 @@ import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator'; import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; import { UserAuthGuard } from 'src/engine/guards/user-auth.guard'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; +import { + GenerateJWTOutput, + GenerateJWTOutputWithAuthTokens, + GenerateJWTOutputWithSSOAUTH, +} from 'src/engine/core-modules/auth/dto/generateJWT.output'; import { ChallengeInput } from './dto/challenge.input'; import { ImpersonateInput } from './dto/impersonate.input'; @@ -159,18 +164,41 @@ export class AuthResolver { return authorizedApp; } - @Mutation(() => AuthTokens) + @Mutation(() => GenerateJWTOutput) @UseGuards(WorkspaceAuthGuard, UserAuthGuard) async generateJWT( @AuthUser() user: User, @Args() args: GenerateJwtInput, - ): Promise { - const token = await this.tokenService.generateSwitchWorkspaceToken( + ): Promise { + const result = await this.tokenService.switchWorkspace( user, args.workspaceId, ); - return token; + if (result.useSSOAuth) { + return { + success: true, + reason: 'WORKSPACE_USE_SSO_AUTH', + availableSSOIDPs: result.availableSSOIdentityProviders.map( + (identityProvider) => ({ + ...identityProvider, + workspace: { + id: result.workspace.id, + displayName: result.workspace.displayName, + }, + }), + ), + }; + } + + return { + success: true, + reason: 'WORKSPACE_AVAILABLE_FOR_SWITCH', + authTokens: await this.tokenService.generateSwitchWorkspaceToken( + user, + result.workspace, + ), + }; } @Mutation(() => AuthTokens) diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts new file mode 100644 index 000000000000..18b9dbb4d6bf --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts @@ -0,0 +1,161 @@ +/* @license Enterprise */ + +import { + Controller, + Get, + Post, + Req, + Res, + UseFilters, + UseGuards, +} from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { generateServiceProviderMetadata } from '@node-saml/node-saml'; +import { Response } from 'express'; +import { Repository } from 'typeorm'; + +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; +import { AuthRestApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-rest-api-exception.filter'; +import { OIDCAuthGuard } from 'src/engine/core-modules/auth/guards/oidc-auth.guard'; +import { SAMLAuthGuard } from 'src/engine/core-modules/auth/guards/saml-auth.guard'; +import { SSOProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/sso-provider-enabled.guard'; +import { AuthService } from 'src/engine/core-modules/auth/services/auth.service'; +import { TokenService } from 'src/engine/core-modules/auth/token/services/token.service'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; +import { + IdentityProviderType, + WorkspaceSSOIdentityProvider, +} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; +import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; +import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service'; + +@Controller('auth') +@UseFilters(AuthRestApiExceptionFilter) +export class SSOAuthController { + constructor( + private readonly tokenService: TokenService, + private readonly authService: AuthService, + private readonly workspaceInvitationService: WorkspaceInvitationService, + private readonly environmentService: EnvironmentService, + private readonly userWorkspaceService: UserWorkspaceService, + private readonly ssoService: SSOService, + @InjectRepository(WorkspaceSSOIdentityProvider, 'core') + private readonly workspaceSSOIdentityProviderRepository: Repository, + ) {} + + @Get('saml/metadata/:identityProviderId') + @UseGuards(SSOProviderEnabledGuard) + async generateMetadata(@Req() req: any): Promise { + return generateServiceProviderMetadata({ + wantAssertionsSigned: false, + issuer: this.ssoService.buildIssuerURL({ + id: req.params.identityProviderId, + type: IdentityProviderType.SAML, + }), + callbackUrl: this.ssoService.buildCallbackUrl({ + type: IdentityProviderType.SAML, + }), + }); + } + + @Get('oidc/login/:identityProviderId') + @UseGuards(SSOProviderEnabledGuard, OIDCAuthGuard) + async oidcAuth() { + // As this method is protected by OIDC Auth guard, it will trigger OIDC SSO flow + return; + } + + @Get('saml/login/:identityProviderId') + @UseGuards(SSOProviderEnabledGuard, SAMLAuthGuard) + async samlAuth() { + // As this method is protected by SAML Auth guard, it will trigger SAML SSO flow + return; + } + + @Get('oidc/callback') + @UseGuards(SSOProviderEnabledGuard, OIDCAuthGuard) + async oidcAuthCallback(@Req() req: any, @Res() res: Response) { + try { + const loginToken = await this.generateLoginToken(req.user); + + return res.redirect( + this.tokenService.computeRedirectURI(loginToken.token), + ); + } catch (err) { + // TODO: improve error management + res.status(403).send(err.message); + } + } + + @Post('saml/callback') + @UseGuards(SSOProviderEnabledGuard, SAMLAuthGuard) + async samlAuthCallback(@Req() req: any, @Res() res: Response) { + try { + const loginToken = await this.generateLoginToken(req.user); + + return res.redirect( + this.tokenService.computeRedirectURI(loginToken.token), + ); + } catch (err) { + // TODO: improve error management + res.status(403).send(err.message); + res.redirect(`${this.environmentService.get('FRONT_BASE_URL')}/verify`); + } + } + + private async generateLoginToken({ + user, + identityProviderId, + }: { + identityProviderId?: string; + user: { email: string } & Record; + }) { + const identityProvider = + await this.workspaceSSOIdentityProviderRepository.findOne({ + where: { id: identityProviderId }, + relations: ['workspace'], + }); + + if (!identityProvider) { + throw new AuthException( + 'Identity provider not found', + AuthExceptionCode.INVALID_DATA, + ); + } + + const invitation = + await this.workspaceInvitationService.getOneWorkspaceInvitation( + identityProvider.workspaceId, + user.email, + ); + + if (invitation) { + await this.authService.signInUp({ + ...user, + workspacePersonalInviteToken: invitation.value, + workspaceInviteHash: identityProvider.workspace.inviteHash, + fromSSO: true, + }); + } + + const isUserExistInWorkspace = + await this.userWorkspaceService.checkUserWorkspaceExistsByEmail( + user.email, + identityProvider.workspaceId, + ); + + if (!isUserExistInWorkspace) { + throw new AuthException( + 'User not found in workspace', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } + + return this.tokenService.generateLoginToken(user.email); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/dto/generateJWT.output.ts b/packages/twenty-server/src/engine/core-modules/auth/dto/generateJWT.output.ts new file mode 100644 index 000000000000..cc27d8c6c000 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/dto/generateJWT.output.ts @@ -0,0 +1,43 @@ +import { Field, ObjectType, createUnionType } from '@nestjs/graphql'; + +import { AuthTokens } from 'src/engine/core-modules/auth/dto/token.entity'; +import { FindAvailableSSOIDPOutput } from 'src/engine/core-modules/sso/dtos/find-available-SSO-IDP.output'; + +@ObjectType() +export class GenerateJWTOutputWithAuthTokens { + @Field(() => Boolean) + success: boolean; + + @Field(() => String) + reason: 'WORKSPACE_AVAILABLE_FOR_SWITCH'; + + @Field(() => AuthTokens) + authTokens: AuthTokens; +} + +@ObjectType() +export class GenerateJWTOutputWithSSOAUTH { + @Field(() => Boolean) + success: boolean; + + @Field(() => String) + reason: 'WORKSPACE_USE_SSO_AUTH'; + + @Field(() => [FindAvailableSSOIDPOutput]) + availableSSOIDPs: Array; +} + +export const GenerateJWTOutput = createUnionType({ + name: 'GenerateJWT', + types: () => [GenerateJWTOutputWithAuthTokens, GenerateJWTOutputWithSSOAUTH], + resolveType(value) { + if (value.reason === 'WORKSPACE_AVAILABLE_FOR_SWITCH') { + return GenerateJWTOutputWithAuthTokens; + } + if (value.reason === 'WORKSPACE_USE_SSO_AUTH') { + return GenerateJWTOutputWithSSOAUTH; + } + + return null; + }, +}); diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.guard.ts new file mode 100644 index 000000000000..d7b8de1e8a47 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.guard.ts @@ -0,0 +1,73 @@ +/* @license Enterprise */ + +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +import { Issuer } from 'openid-client'; + +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; +import { OIDCAuthStrategy } from 'src/engine/core-modules/auth/strategies/oidc.auth.strategy'; +import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; + +@Injectable() +export class OIDCAuthGuard extends AuthGuard('openidconnect') { + constructor(private readonly ssoService: SSOService) { + super(); + } + + private getIdentityProviderId(request: any): string { + if (request.params.identityProviderId) { + return request.params.identityProviderId; + } + + if ( + request.query.state && + typeof request.query.state === 'string' && + request.query.state.startsWith('{') && + request.query.state.endsWith('}') + ) { + const state = JSON.parse(request.query.state); + + return state.identityProviderId; + } + + throw new Error('Invalid OIDC identity provider params'); + } + + async canActivate(context: ExecutionContext): Promise { + try { + const request = context.switchToHttp().getRequest(); + + const identityProviderId = this.getIdentityProviderId(request); + + const identityProvider = + await this.ssoService.findSSOIdentityProviderById(identityProviderId); + + if (!identityProvider) { + throw new AuthException( + 'Identity provider not found', + AuthExceptionCode.INVALID_DATA, + ); + } + + const issuer = await Issuer.discover(identityProvider.issuer); + + new OIDCAuthStrategy( + this.ssoService.getOIDCClient(identityProvider, issuer), + identityProvider.id, + ); + + return (await super.canActivate(context)) as boolean; + } catch (err) { + if (err instanceof AuthException) { + return false; + } + + // TODO AMOREAUX: trigger sentry error + return false; + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/saml-auth.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/saml-auth.guard.ts new file mode 100644 index 000000000000..fba753a0727f --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/saml-auth.guard.ts @@ -0,0 +1,48 @@ +/* @license Enterprise */ + +import { ExecutionContext, Injectable } from '@nestjs/common'; +import { AuthGuard } from '@nestjs/passport'; + +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; +import { SamlAuthStrategy } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy'; +import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; + +@Injectable() +export class SAMLAuthGuard extends AuthGuard('saml') { + constructor(private readonly sSOService: SSOService) { + super(); + } + + async canActivate(context: ExecutionContext) { + try { + const request = context.switchToHttp().getRequest(); + + const RelayState = + 'RelayState' in request.body ? JSON.parse(request.body.RelayState) : {}; + + request.params.identityProviderId = + request.params.identityProviderId ?? RelayState.identityProviderId; + + if (!request.params.identityProviderId) { + throw new AuthException( + 'Invalid SAML identity provider', + AuthExceptionCode.INVALID_DATA, + ); + } + + new SamlAuthStrategy(this.sSOService); + + return (await super.canActivate(context)) as boolean; + } catch (err) { + if (err instanceof AuthException) { + return false; + } + + // TODO AMOREAUX: trigger sentry error + return false; + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/sso-provider-enabled.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/sso-provider-enabled.guard.ts new file mode 100644 index 000000000000..ce1d6b11a72d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/sso-provider-enabled.guard.ts @@ -0,0 +1,27 @@ +/* @license Enterprise */ + +import { CanActivate, Injectable } from '@nestjs/common'; + +import { Observable } from 'rxjs'; + +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; + +@Injectable() +export class SSOProviderEnabledGuard implements CanActivate { + constructor(private readonly environmentService: EnvironmentService) {} + + canActivate(): boolean | Promise | Observable { + if (!this.environmentService.get('ENTERPRISE_KEY')) { + throw new AuthException( + 'Enterprise key must be defined to use SSO', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } + + return true; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts index bba83839a315..0092499d2b41 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts @@ -35,7 +35,6 @@ import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-u import { TokenService } from 'src/engine/core-modules/auth/token/services/token.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; -import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -43,7 +42,6 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; export class AuthService { constructor( private readonly tokenService: TokenService, - private readonly userService: UserService, private readonly signInUpService: SignInUpService, @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts index 6e0e96203876..c5cb98f0c659 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts @@ -225,23 +225,45 @@ export class SignInUpService { email, }) { if (!workspacePersonalInviteToken && !workspaceInviteHash) { - throw new Error('No invite token or hash provided'); + throw new AuthException( + 'No invite token or hash provided', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); } - if (!workspacePersonalInviteToken && workspaceInviteHash) { - return ( - (await this.workspaceRepository.findOneBy({ - inviteHash: workspaceInviteHash, - })) ?? undefined + const workspace = await this.workspaceRepository.findOneBy({ + inviteHash: workspaceInviteHash, + }); + + if (!workspace) { + throw new AuthException( + 'Workspace not found', + AuthExceptionCode.WORKSPACE_NOT_FOUND, ); } - const appToken = await this.userWorkspaceService.validateInvitation( - workspacePersonalInviteToken, - email, - ); + if (!workspacePersonalInviteToken && !workspace.isPublicInviteLinkEnabled) { + throw new AuthException( + 'Workspace does not allow public invites', + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } + + if (workspacePersonalInviteToken && workspace.isPublicInviteLinkEnabled) { + try { + await this.userWorkspaceService.validateInvitation( + workspacePersonalInviteToken, + email, + ); + } catch (err) { + throw new AuthException( + err.message, + AuthExceptionCode.FORBIDDEN_EXCEPTION, + ); + } + } - return appToken?.workspace; + return workspace; } private async activateOnboardingForNewUser( diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/oidc.auth.strategy.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/oidc.auth.strategy.ts new file mode 100644 index 000000000000..493e3972d3c7 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/oidc.auth.strategy.ts @@ -0,0 +1,86 @@ +/* @license Enterprise */ + +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; + +import { + Strategy, + StrategyOptions, + StrategyVerifyCallbackReq, +} from 'openid-client'; + +import { + AuthException, + AuthExceptionCode, +} from 'src/engine/core-modules/auth/auth.exception'; + +@Injectable() +export class OIDCAuthStrategy extends PassportStrategy( + Strategy, + 'openidconnect', +) { + constructor( + private client: StrategyOptions['client'], + sessionKey: string, + ) { + super({ + params: { + scope: 'openid email profile', + code_challenge_method: 'S256', + }, + client, + usePKCE: true, + passReqToCallback: true, + sessionKey, + }); + } + + async authenticate(req: any, options: any) { + return super.authenticate(req, { + ...options, + state: JSON.stringify({ + identityProviderId: req.params.identityProviderId, + }), + }); + } + + validate: StrategyVerifyCallbackReq<{ + identityProviderId: string; + user: { + email: string; + firstName?: string | null; + lastName?: string | null; + }; + }> = async (req, tokenset, done) => { + try { + const state = JSON.parse( + 'query' in req && + req.query && + typeof req.query === 'object' && + 'state' in req.query && + req.query.state && + typeof req.query.state === 'string' + ? req.query.state + : '{}', + ); + + const userinfo = await this.client.userinfo(tokenset); + + if (!userinfo || !userinfo.email) { + return done( + new AuthException('Email not found', AuthExceptionCode.INVALID_DATA), + ); + } + + const user = { + email: userinfo.email, + ...(userinfo.given_name ? { firstName: userinfo.given_name } : {}), + ...(userinfo.family_name ? { lastName: userinfo.family_name } : {}), + }; + + done(null, { user, identityProviderId: state.identityProviderId }); + } catch (err) { + done(err); + } + }; +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/saml.auth.strategy.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/saml.auth.strategy.ts new file mode 100644 index 000000000000..c1514c8f9977 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/saml.auth.strategy.ts @@ -0,0 +1,98 @@ +/* @license Enterprise */ + +import { Injectable } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; + +import { + MultiSamlStrategy, + MultiStrategyConfig, + PassportSamlConfig, + SamlConfig, + VerifyWithRequest, +} from '@node-saml/passport-saml'; +import { AuthenticateOptions } from '@node-saml/passport-saml/lib/types'; +import { isEmail } from 'class-validator'; +import { Request } from 'express'; + +import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; + +@Injectable() +export class SamlAuthStrategy extends PassportStrategy( + MultiSamlStrategy, + 'saml', +) { + constructor(private readonly sSOService: SSOService) { + super({ + getSamlOptions: (req, callback) => { + this.sSOService + .findSSOIdentityProviderById(req.params.identityProviderId) + .then((identityProvider) => { + if ( + identityProvider && + this.sSOService.isSAMLIdentityProvider(identityProvider) + ) { + const config: SamlConfig = { + entryPoint: identityProvider.ssoURL, + issuer: this.sSOService.buildIssuerURL(identityProvider), + callbackUrl: this.sSOService.buildCallbackUrl(identityProvider), + idpCert: identityProvider.certificate, + wantAssertionsSigned: false, + // TODO: Improve the feature by sign the response + wantAuthnResponseSigned: false, + signatureAlgorithm: 'sha256', + }; + + return callback(null, config); + } + + // TODO: improve error management + return callback(new Error('Invalid SAML identity provider')); + }) + .catch((err) => { + // TODO: improve error management + return callback(err); + }); + }, + passReqToCallback: true, + } as PassportSamlConfig & MultiStrategyConfig); + } + + authenticate(req: Request, options: AuthenticateOptions) { + super.authenticate(req, { + ...options, + additionalParams: { + RelayState: JSON.stringify({ + identityProviderId: req.params.identityProviderId, + }), + }, + }); + } + + validate: VerifyWithRequest = async (request, profile, done) => { + if (!profile) { + return done(new Error('Profile is must be provided')); + } + + const email = profile.email ?? profile.mail ?? profile.nameID; + + if (!isEmail(email)) { + return done(new Error('Invalid email')); + } + + const result: { + user: Record; + identityProviderId?: string; + } = { user: { email } }; + + if ( + 'RelayState' in request.body && + typeof request.body.RelayState === 'string' + ) { + const RelayState = JSON.parse(request.body.RelayState); + + result.identityProviderId = RelayState.identityProviderId; + } + + done(null, result); + }; +} diff --git a/packages/twenty-server/src/engine/core-modules/auth/token/services/token.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/token/services/token.service.spec.ts index 777b1febde7f..7bc44ffd001f 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/token/services/token.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/token/services/token.service.spec.ts @@ -17,6 +17,7 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { EmailService } from 'src/engine/core-modules/email/email.service'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; import { TokenService } from './token.service'; @@ -50,6 +51,12 @@ describe('TokenService', () => { send: jest.fn(), }, }, + { + provide: SSOService, + useValue: { + send: jest.fn(), + }, + }, { provide: getRepositoryToken(User, 'core'), useValue: { diff --git a/packages/twenty-server/src/engine/core-modules/auth/token/services/token.service.ts b/packages/twenty-server/src/engine/core-modules/auth/token/services/token.service.ts index c740cb9a0613..d323ba83168e 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/token/services/token.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/token/services/token.service.ts @@ -46,6 +46,7 @@ import { } from 'src/engine/core-modules/workspace/workspace.entity'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; @Injectable() export class TokenService { @@ -60,6 +61,7 @@ export class TokenService { @InjectRepository(Workspace, 'core') private readonly workspaceRepository: Repository, private readonly emailService: EmailService, + private readonly sSSOService: SSOService, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} @@ -341,10 +343,7 @@ export class TokenService { }; } - async generateSwitchWorkspaceToken( - user: User, - workspaceId: string, - ): Promise { + async switchWorkspace(user: User, workspaceId: string) { const userExists = await this.userRepository.findBy({ id: user.id }); if (!userExists) { @@ -356,7 +355,7 @@ export class TokenService { const workspace = await this.workspaceRepository.findOne({ where: { id: workspaceId }, - relations: ['workspaceUsers'], + relations: ['workspaceUsers', 'workspaceSSOIdentityProviders'], }); if (!workspace) { @@ -377,12 +376,44 @@ export class TokenService { ); } + if (workspace.workspaceSSOIdentityProviders.length > 0) { + return { + useSSOAuth: true, + workspace, + availableSSOIdentityProviders: + await this.sSSOService.listSSOIdentityProvidersByWorkspaceId( + workspaceId, + ), + } as { + useSSOAuth: true; + workspace: Workspace; + availableSSOIdentityProviders: Awaited< + ReturnType< + typeof this.sSSOService.listSSOIdentityProvidersByWorkspaceId + > + >; + }; + } + + return { + useSSOAuth: false, + workspace, + } as { + useSSOAuth: false; + workspace: Workspace; + }; + } + + async generateSwitchWorkspaceToken( + user: User, + workspace: Workspace, + ): Promise { await this.userRepository.save({ id: user.id, defaultWorkspace: workspace, }); - const token = await this.generateAccessToken(user.id, workspaceId); + const token = await this.generateAccessToken(user.id, workspace.id); const refreshToken = await this.generateRefreshToken(user.id); return { diff --git a/packages/twenty-server/src/engine/core-modules/auth/token/token.module.ts b/packages/twenty-server/src/engine/core-modules/auth/token/token.module.ts index ea104687291f..42d65621e528 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/token/token.module.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/token/token.module.ts @@ -11,6 +11,7 @@ import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; +import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module'; @Module({ imports: [ @@ -19,6 +20,7 @@ import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-s TypeORMModule, DataSourceModule, EmailModule, + WorkspaceSSOModule, ], providers: [TokenService, JwtAuthStrategy], exports: [TokenService], diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts index 6001a90f4ade..00e2c3cb4304 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts @@ -15,6 +15,9 @@ class AuthProviders { @Field(() => Boolean) microsoft: boolean; + + @Field(() => Boolean) + sso: boolean; } @ObjectType() diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts index f6ba1aaf4abd..9f2660876568 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.resolver.ts @@ -16,6 +16,7 @@ export class ClientConfigResolver { magicLink: false, password: this.environmentService.get('AUTH_PASSWORD_ENABLED'), microsoft: this.environmentService.get('AUTH_MICROSOFT_ENABLED'), + sso: this.environmentService.get('AUTH_SSO_ENABLED'), }, billing: { isBillingEnabled: this.environmentService.get('IS_BILLING_ENABLED'), diff --git a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts index af651c18c5d2..00cb30716f39 100644 --- a/packages/twenty-server/src/engine/core-modules/core-engine.module.ts +++ b/packages/twenty-server/src/engine/core-modules/core-engine.module.ts @@ -40,6 +40,7 @@ import { WorkflowTriggerApiModule } from 'src/engine/core-modules/workflow/workf import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module'; import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; import { WorkspaceEventEmitterModule } from 'src/engine/workspace-event-emitter/workspace-event-emitter.module'; +import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module'; import { AnalyticsModule } from './analytics/analytics.module'; import { ClientConfigModule } from './client-config/client-config.module'; @@ -61,6 +62,7 @@ import { FileModule } from './file/file.module'; UserModule, WorkspaceModule, WorkspaceInvitationModule, + WorkspaceSSOModule, PostgresCredentialsModule, WorkflowTriggerApiModule, WorkspaceEventEmitterModule, @@ -117,6 +119,7 @@ import { FileModule } from './file/file.module'; UserModule, WorkspaceModule, WorkspaceInvitationModule, + WorkspaceSSOModule, ], }) export class CoreEngineModule {} diff --git a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts index 03b0d234e1fe..77d54025fda6 100644 --- a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts @@ -225,6 +225,15 @@ export class EnvironmentVariables { @ValidateIf((env) => env.AUTH_GOOGLE_ENABLED) AUTH_GOOGLE_CALLBACK_URL: string; + @CastToBoolean() + @IsOptional() + @IsBoolean() + AUTH_SSO_ENABLED = false; + + @IsString() + @IsOptional() + ENTERPRISE_KEY: string; + // Custom Code Engine @IsEnum(ServerlessDriverType) @IsOptional() @@ -443,6 +452,9 @@ export class EnvironmentVariables { @CastToPositiveNumber() CACHE_STORAGE_TTL: number = 3600 * 24 * 7; + @ValidateIf((env) => env.ENTERPRISE_KEY) + SESSION_STORE_SECRET: string; + @CastToBoolean() CALENDAR_PROVIDER_GOOGLE_ENABLED = false; diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts index 58282f9deca4..c78a1bf066db 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/enums/feature-flag-key.enum.ts @@ -10,6 +10,7 @@ export enum FeatureFlagKey { IsMessageThreadSubscriberEnabled = 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED', IsQueryRunnerTwentyORMEnabled = 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED', IsWorkspaceFavoriteEnabled = 'IS_WORKSPACE_FAVORITE_ENABLED', + IsSSOEnabled = 'IS_SSO_ENABLED', IsGmailSendEmailScopeEnabled = 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED', IsAnalyticsV2Enabled = 'IS_ANALYTICS_V2_ENABLED', IsUniqueIndexesEnabled = 'IS_UNIQUE_INDEXES_ENABLED', diff --git a/packages/twenty-server/src/engine/core-modules/session-storage/session-storage.module-factory.ts b/packages/twenty-server/src/engine/core-modules/session-storage/session-storage.module-factory.ts new file mode 100644 index 000000000000..8f5099ba5bd9 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/session-storage/session-storage.module-factory.ts @@ -0,0 +1,66 @@ +import { Logger } from '@nestjs/common'; + +import { createClient } from 'redis'; +import RedisStore from 'connect-redis'; +import session from 'express-session'; + +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { CacheStorageType } from 'src/engine/core-modules/cache-storage/types/cache-storage-type.enum'; +import { MessageQueueDriverType } from 'src/engine/core-modules/message-queue/interfaces'; + +export const getSessionStorageOptions = ( + environmentService: EnvironmentService, +): session.SessionOptions => { + const cacheStorageType = environmentService.get('CACHE_STORAGE_TYPE'); + + const SERVER_URL = environmentService.get('SERVER_URL'); + + const sessionStorage = { + secret: environmentService.get('SESSION_STORE_SECRET'), + resave: false, + saveUninitialized: false, + cookie: { + secure: !!(SERVER_URL && SERVER_URL.startsWith('https')), + maxAge: 1000 * 60 * 30, // 30 minutes + }, + }; + + switch (cacheStorageType) { + case CacheStorageType.Memory: { + Logger.warn( + 'Memory session storage is not recommended for production. Prefer Redis.', + ); + + return sessionStorage; + } + case CacheStorageType.Redis: { + const connectionString = environmentService.get('REDIS_URL'); + + if (!connectionString) { + throw new Error( + `${CacheStorageType.Redis} session storage requires REDIS_URL to be defined, check your .env file`, + ); + } + + const redisClient = createClient({ + url: connectionString, + }); + + redisClient.connect().catch((err) => { + throw new Error(`Redis connection failed: ${err}`); + }); + + return { + ...sessionStorage, + store: new RedisStore({ + client: redisClient, + prefix: 'engine:session:', + }), + }; + } + default: + throw new Error( + `Invalid session-storage (${cacheStorageType}), check your .env file`, + ); + } +}; diff --git a/packages/twenty-server/src/engine/core-modules/sso/dtos/delete-sso.input.ts b/packages/twenty-server/src/engine/core-modules/sso/dtos/delete-sso.input.ts new file mode 100644 index 000000000000..4a0e1002df57 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sso/dtos/delete-sso.input.ts @@ -0,0 +1,12 @@ +/* @license Enterprise */ + +import { Field, InputType } from '@nestjs/graphql'; + +import { IsUUID } from 'class-validator'; + +@InputType() +export class DeleteSsoInput { + @Field(() => String) + @IsUUID() + identityProviderId: string; +} diff --git a/packages/twenty-server/src/engine/core-modules/sso/dtos/delete-sso.output.ts b/packages/twenty-server/src/engine/core-modules/sso/dtos/delete-sso.output.ts new file mode 100644 index 000000000000..0857ac3a4bf9 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sso/dtos/delete-sso.output.ts @@ -0,0 +1,9 @@ +/* @license Enterprise */ + +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class DeleteSsoOutput { + @Field(() => String) + identityProviderId: string; +} diff --git a/packages/twenty-server/src/engine/core-modules/sso/dtos/edit-sso.input.ts b/packages/twenty-server/src/engine/core-modules/sso/dtos/edit-sso.input.ts new file mode 100644 index 000000000000..617183633712 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sso/dtos/edit-sso.input.ts @@ -0,0 +1,19 @@ +/* @license Enterprise */ + +import { Field, InputType } from '@nestjs/graphql'; + +import { IsString, IsUUID } from 'class-validator'; + +import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type'; +import { SSOIdentityProviderStatus } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; + +@InputType() +export class EditSsoInput { + @Field(() => String) + @IsUUID() + id: string; + + @Field(() => SSOIdentityProviderStatus) + @IsString() + status: SSOConfiguration['status']; +} diff --git a/packages/twenty-server/src/engine/core-modules/sso/dtos/edit-sso.output.ts b/packages/twenty-server/src/engine/core-modules/sso/dtos/edit-sso.output.ts new file mode 100644 index 000000000000..35209c642e86 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sso/dtos/edit-sso.output.ts @@ -0,0 +1,27 @@ +/* @license Enterprise */ + +import { Field, ObjectType } from '@nestjs/graphql'; + +import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type'; +import { + IdentityProviderType, + SSOIdentityProviderStatus, +} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; + +@ObjectType() +export class EditSsoOutput { + @Field(() => String) + id: string; + + @Field(() => IdentityProviderType) + type: string; + + @Field(() => String) + issuer: string; + + @Field(() => String) + name: string; + + @Field(() => SSOIdentityProviderStatus) + status: SSOConfiguration['status']; +} diff --git a/packages/twenty-server/src/engine/core-modules/sso/dtos/find-available-SSO-IDP.input.ts b/packages/twenty-server/src/engine/core-modules/sso/dtos/find-available-SSO-IDP.input.ts new file mode 100644 index 000000000000..3cd5c91df79f --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sso/dtos/find-available-SSO-IDP.input.ts @@ -0,0 +1,13 @@ +/* @license Enterprise */ + +import { Field, InputType } from '@nestjs/graphql'; + +import { IsEmail, IsNotEmpty } from 'class-validator'; + +@InputType() +export class FindAvailableSSOIDPInput { + @Field(() => String) + @IsNotEmpty() + @IsEmail() + email: string; +} diff --git a/packages/twenty-server/src/engine/core-modules/sso/dtos/find-available-SSO-IDP.output.ts b/packages/twenty-server/src/engine/core-modules/sso/dtos/find-available-SSO-IDP.output.ts new file mode 100644 index 000000000000..3c62fdbcac37 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sso/dtos/find-available-SSO-IDP.output.ts @@ -0,0 +1,39 @@ +/* @license Enterprise */ + +import { Field, ObjectType } from '@nestjs/graphql'; + +import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type'; +import { + IdentityProviderType, + SSOIdentityProviderStatus, +} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; + +@ObjectType() +class WorkspaceNameAndId { + @Field(() => String, { nullable: true }) + displayName?: string | null; + + @Field(() => String) + id: string; +} + +@ObjectType() +export class FindAvailableSSOIDPOutput { + @Field(() => IdentityProviderType) + type: SSOConfiguration['type']; + + @Field(() => String) + id: string; + + @Field(() => String) + issuer: string; + + @Field(() => String) + name: string; + + @Field(() => SSOIdentityProviderStatus) + status: SSOConfiguration['status']; + + @Field(() => WorkspaceNameAndId) + workspace: WorkspaceNameAndId; +} diff --git a/packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.input.ts b/packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.input.ts new file mode 100644 index 000000000000..e0adc9645f26 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.input.ts @@ -0,0 +1,12 @@ +/* @license Enterprise */ + +import { Field, InputType } from '@nestjs/graphql'; + +import { IsString } from 'class-validator'; + +@InputType() +export class GetAuthorizationUrlInput { + @Field(() => String) + @IsString() + identityProviderId: string; +} diff --git a/packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.output.ts b/packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.output.ts new file mode 100644 index 000000000000..d0c78e37ddc6 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sso/dtos/get-authorization-url.output.ts @@ -0,0 +1,17 @@ +/* @license Enterprise */ + +import { Field, ObjectType } from '@nestjs/graphql'; + +import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type'; + +@ObjectType() +export class GetAuthorizationUrlOutput { + @Field(() => String) + authorizationURL: string; + + @Field(() => String) + type: SSOConfiguration['type']; + + @Field(() => String) + id: string; +} diff --git a/packages/twenty-server/src/engine/core-modules/sso/dtos/setup-sso.input.ts b/packages/twenty-server/src/engine/core-modules/sso/dtos/setup-sso.input.ts new file mode 100644 index 000000000000..29890f184ae0 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sso/dtos/setup-sso.input.ts @@ -0,0 +1,50 @@ +/* @license Enterprise */ + +import { Field, InputType } from '@nestjs/graphql'; + +import { IsOptional, IsString, IsUrl, IsUUID } from 'class-validator'; + +import { IsX509Certificate } from 'src/engine/core-modules/sso/dtos/validators/x509.validator'; + +@InputType() +class SetupSsoInputCommon { + @Field(() => String) + @IsString() + name: string; + + @Field(() => String) + @IsString() + @IsUrl({ protocols: ['http', 'https'] }) + issuer: string; +} + +@InputType() +export class SetupOIDCSsoInput extends SetupSsoInputCommon { + @Field(() => String) + @IsString() + clientID: string; + + @Field(() => String) + @IsString() + clientSecret: string; +} + +@InputType() +export class SetupSAMLSsoInput extends SetupSsoInputCommon { + @Field(() => String) + @IsUUID() + id: string; + + @Field(() => String) + @IsUrl({ protocols: ['http', 'https'] }) + ssoURL: string; + + @Field(() => String) + @IsX509Certificate() + certificate: string; + + @Field(() => String, { nullable: true }) + @IsOptional() + @IsString() + fingerprint?: string; +} diff --git a/packages/twenty-server/src/engine/core-modules/sso/dtos/setup-sso.output.ts b/packages/twenty-server/src/engine/core-modules/sso/dtos/setup-sso.output.ts new file mode 100644 index 000000000000..b5b4ee076039 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sso/dtos/setup-sso.output.ts @@ -0,0 +1,27 @@ +/* @license Enterprise */ + +import { Field, ObjectType } from '@nestjs/graphql'; + +import { SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type'; +import { + IdentityProviderType, + SSOIdentityProviderStatus, +} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; + +@ObjectType() +export class SetupSsoOutput { + @Field(() => String) + id: string; + + @Field(() => IdentityProviderType) + type: string; + + @Field(() => String) + issuer: string; + + @Field(() => String) + name: string; + + @Field(() => SSOIdentityProviderStatus) + status: SSOConfiguration['status']; +} diff --git a/packages/twenty-server/src/engine/core-modules/sso/dtos/validators/x509.validator.ts b/packages/twenty-server/src/engine/core-modules/sso/dtos/validators/x509.validator.ts new file mode 100644 index 000000000000..22486aa3eb76 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sso/dtos/validators/x509.validator.ts @@ -0,0 +1,52 @@ +/* @license Enterprise */ + +import * as crypto from 'crypto'; + +import { + registerDecorator, + ValidationOptions, + ValidatorConstraint, + ValidatorConstraintInterface, +} from 'class-validator'; + +@ValidatorConstraint({ async: false }) +export class IsX509CertificateConstraint + implements ValidatorConstraintInterface +{ + validate(value: any) { + if (typeof value !== 'string') { + return false; + } + + try { + const cleanCert = value.replace( + /-----BEGIN CERTIFICATE-----|-----END CERTIFICATE-----|\n|\r/g, + '', + ); + + const der = Buffer.from(cleanCert, 'base64'); + + const cert = new crypto.X509Certificate(der); + + return cert instanceof crypto.X509Certificate; + } catch (error) { + return false; + } + } + + defaultMessage() { + return 'The string is not a valid X509 certificate'; + } +} + +export function IsX509Certificate(validationOptions?: ValidationOptions) { + return function (object: object, propertyName: string) { + registerDecorator({ + target: object.constructor, + propertyName: propertyName, + options: validationOptions, + constraints: [], + validator: IsX509CertificateConstraint, + }); + }; +} diff --git a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts new file mode 100644 index 000000000000..7b2148d23000 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts @@ -0,0 +1,327 @@ +/* @license Enterprise */ + +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { Issuer } from 'openid-client'; +import { Repository } from 'typeorm'; + +import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator'; +import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service'; +import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { FindAvailableSSOIDPOutput } from 'src/engine/core-modules/sso/dtos/find-available-SSO-IDP.output'; +import { + SSOException, + SSOExceptionCode, +} from 'src/engine/core-modules/sso/sso.exception'; +import { + OIDCConfiguration, + SAMLConfiguration, + SSOConfiguration, +} from 'src/engine/core-modules/sso/types/SSOConfigurations.type'; +import { + IdentityProviderType, + OIDCResponseType, + WorkspaceSSOIdentityProvider, +} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; +import { User } from 'src/engine/core-modules/user/user.entity'; + +@Injectable() +// eslint-disable-next-line @nx/workspace-inject-workspace-repository +export class SSOService { + constructor( + @InjectRepository(FeatureFlagEntity, 'core') + private readonly featureFlagRepository: Repository, + @InjectRepository(WorkspaceSSOIdentityProvider, 'core') + private readonly workspaceSSOIdentityProviderRepository: Repository, + @InjectRepository(User, 'core') + private readonly userRepository: Repository, + private readonly environmentService: EnvironmentService, + @InjectCacheStorage(CacheStorageNamespace.EngineWorkspace) + private readonly cacheStorageService: CacheStorageService, + ) {} + + private async isSSOEnabled(workspaceId: string) { + const isSSOEnabledFeatureFlag = await this.featureFlagRepository.findOneBy({ + workspaceId, + key: FeatureFlagKey.IsSSOEnabled, + value: true, + }); + + if (!isSSOEnabledFeatureFlag?.value) { + throw new SSOException( + `${FeatureFlagKey.IsSSOEnabled} feature flag is disabled`, + SSOExceptionCode.SSO_DISABLE, + ); + } + } + + async createOIDCIdentityProvider( + data: Pick< + WorkspaceSSOIdentityProvider, + 'issuer' | 'clientID' | 'clientSecret' | 'name' + >, + workspaceId: string, + ) { + try { + await this.isSSOEnabled(workspaceId); + + if (!data.issuer) { + throw new SSOException( + 'Invalid issuer URL', + SSOExceptionCode.INVALID_ISSUER_URL, + ); + } + + const issuer = await Issuer.discover(data.issuer); + + if (!issuer.metadata.issuer) { + throw new SSOException( + 'Invalid issuer URL from discovery', + SSOExceptionCode.INVALID_ISSUER_URL, + ); + } + + const identityProvider = + await this.workspaceSSOIdentityProviderRepository.save({ + type: IdentityProviderType.OIDC, + clientID: data.clientID, + clientSecret: data.clientSecret, + issuer: issuer.metadata.issuer, + name: data.name, + workspaceId, + }); + + return { + id: identityProvider.id, + type: identityProvider.type, + name: identityProvider.name, + status: identityProvider.status, + issuer: identityProvider.issuer, + }; + } catch (err) { + if (err instanceof SSOException) { + return err; + } + + return new SSOException( + 'Unknown SSO configuration error', + SSOExceptionCode.UNKNOWN_SSO_CONFIGURATION_ERROR, + ); + } + } + + async createSAMLIdentityProvider( + data: Pick< + WorkspaceSSOIdentityProvider, + 'ssoURL' | 'certificate' | 'fingerprint' | 'id' + >, + workspaceId: string, + ) { + await this.isSSOEnabled(workspaceId); + + const identityProvider = + await this.workspaceSSOIdentityProviderRepository.save({ + ...data, + type: IdentityProviderType.SAML, + workspaceId, + }); + + return { + id: identityProvider.id, + type: identityProvider.type, + name: identityProvider.name, + issuer: this.buildIssuerURL(identityProvider), + status: identityProvider.status, + }; + } + + async findAvailableSSOIdentityProviders(email: string) { + const user = await this.userRepository.findOne({ + where: { email }, + relations: [ + 'workspaces', + 'workspaces.workspace', + 'workspaces.workspace.workspaceSSOIdentityProviders', + ], + }); + + if (!user) { + throw new SSOException('User not found', SSOExceptionCode.USER_NOT_FOUND); + } + + return user.workspaces.flatMap((userWorkspace) => + ( + userWorkspace.workspace + .workspaceSSOIdentityProviders as Array + ).reduce((acc, identityProvider) => { + if (identityProvider.status === 'Inactive') return acc; + + acc.push({ + id: identityProvider.id, + name: identityProvider.name ?? 'Unknown', + issuer: identityProvider.issuer, + type: identityProvider.type, + status: identityProvider.status, + workspace: { + id: userWorkspace.workspaceId, + displayName: userWorkspace.workspace.displayName, + }, + }); + + return acc; + }, [] as Array), + ); + } + + async findSSOIdentityProviderById(identityProviderId?: string) { + // if identityProviderId is not provide, typeorm return a random idp instead of undefined + if (!identityProviderId) return undefined; + + return (await this.workspaceSSOIdentityProviderRepository.findOne({ + where: { id: identityProviderId }, + })) as (SSOConfiguration & WorkspaceSSOIdentityProvider) | undefined; + } + + buildCallbackUrl( + identityProvider: Pick, + ) { + const callbackURL = new URL(this.environmentService.get('SERVER_URL')); + + callbackURL.pathname = `/auth/${identityProvider.type.toLowerCase()}/callback`; + + return callbackURL.toString(); + } + + buildIssuerURL( + identityProvider: Pick, + ) { + return `${this.environmentService.get('SERVER_URL')}/auth/${identityProvider.type.toLowerCase()}/login/${identityProvider.id}`; + } + + private isOIDCIdentityProvider( + identityProvider: WorkspaceSSOIdentityProvider, + ): identityProvider is OIDCConfiguration & WorkspaceSSOIdentityProvider { + return identityProvider.type === IdentityProviderType.OIDC; + } + + isSAMLIdentityProvider( + identityProvider: WorkspaceSSOIdentityProvider, + ): identityProvider is SAMLConfiguration & WorkspaceSSOIdentityProvider { + return identityProvider.type === IdentityProviderType.SAML; + } + + getOIDCClient( + identityProvider: WorkspaceSSOIdentityProvider, + issuer: Issuer, + ) { + if (!this.isOIDCIdentityProvider(identityProvider)) { + throw new SSOException( + 'Invalid Identity Provider type', + SSOExceptionCode.INVALID_IDP_TYPE, + ); + } + + return new issuer.Client({ + client_id: identityProvider.clientID, + client_secret: identityProvider.clientSecret, + redirect_uris: [this.buildCallbackUrl(identityProvider)], + response_types: [OIDCResponseType.CODE], + }); + } + + async getAuthorizationUrl(identityProviderId: string) { + const identityProvider = + (await this.workspaceSSOIdentityProviderRepository.findOne({ + where: { + id: identityProviderId, + }, + })) as WorkspaceSSOIdentityProvider & SSOConfiguration; + + if (!identityProvider) { + throw new SSOException( + 'Identity Provider not found', + SSOExceptionCode.USER_NOT_FOUND, + ); + } + + return { + id: identityProvider.id, + authorizationURL: this.buildIssuerURL(identityProvider), + type: identityProvider.type, + }; + } + + async listSSOIdentityProvidersByWorkspaceId(workspaceId: string) { + return (await this.workspaceSSOIdentityProviderRepository.find({ + where: { workspaceId }, + select: ['id', 'name', 'type', 'issuer', 'status'], + })) as Array< + Pick< + WorkspaceSSOIdentityProvider, + 'id' | 'name' | 'type' | 'issuer' | 'status' + > + >; + } + + async deleteSSOIdentityProvider( + identityProviderId: string, + workspaceId: string, + ) { + const identityProvider = + await this.workspaceSSOIdentityProviderRepository.findOne({ + where: { + id: identityProviderId, + workspaceId, + }, + }); + + if (!identityProvider) { + throw new SSOException( + 'Identity Provider not found', + SSOExceptionCode.IDENTITY_PROVIDER_NOT_FOUND, + ); + } + + await this.workspaceSSOIdentityProviderRepository.delete({ + id: identityProvider.id, + }); + + return { identityProviderId: identityProvider.id }; + } + + async editSSOIdentityProvider( + payload: Partial, + workspaceId: string, + ) { + const ssoIdp = await this.workspaceSSOIdentityProviderRepository.findOne({ + where: { + id: payload.id, + workspaceId, + }, + }); + + if (!ssoIdp) { + throw new SSOException( + 'Identity Provider not found', + SSOExceptionCode.IDENTITY_PROVIDER_NOT_FOUND, + ); + } + + const result = await this.workspaceSSOIdentityProviderRepository.save({ + ...ssoIdp, + ...payload, + }); + + return { + id: result.id, + type: result.type, + issuer: result.issuer, + name: result.name, + status: result.status, + }; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/sso/sso.exception.ts b/packages/twenty-server/src/engine/core-modules/sso/sso.exception.ts new file mode 100644 index 000000000000..ec5520c5a371 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sso/sso.exception.ts @@ -0,0 +1,20 @@ +/* @license Enterprise */ + +import { CustomException } from 'src/utils/custom-exception'; + +export class SSOException extends CustomException { + code: SSOExceptionCode; + constructor(message: string, code: SSOExceptionCode) { + super(message, code); + } +} + +export enum SSOExceptionCode { + USER_NOT_FOUND = 'USER_NOT_FOUND', + INVALID_SSO_CONFIGURATION = 'INVALID_SSO_CONFIGURATION', + IDENTITY_PROVIDER_NOT_FOUND = 'IDENTITY_PROVIDER_NOT_FOUND', + INVALID_ISSUER_URL = 'INVALID_ISSUER_URL', + INVALID_IDP_TYPE = 'INVALID_IDP_TYPE', + UNKNOWN_SSO_CONFIGURATION_ERROR = 'UNKNOWN_SSO_CONFIGURATION_ERROR', + SSO_DISABLE = 'SSO_DISABLE', +} diff --git a/packages/twenty-server/src/engine/core-modules/sso/sso.module.ts b/packages/twenty-server/src/engine/core-modules/sso/sso.module.ts new file mode 100644 index 000000000000..fc7fe9979987 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sso/sso.module.ts @@ -0,0 +1,24 @@ +/* @license Enterprise */ + +import { Module } from '@nestjs/common'; + +import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; + +import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity'; +import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; +import { SSOResolver } from 'src/engine/core-modules/sso/sso.resolver'; +import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; +import { User } from 'src/engine/core-modules/user/user.entity'; + +@Module({ + imports: [ + NestjsQueryTypeOrmModule.forFeature( + [WorkspaceSSOIdentityProvider, User, AppToken, FeatureFlagEntity], + 'core', + ), + ], + exports: [SSOService], + providers: [SSOService, SSOResolver], +}) +export class WorkspaceSSOModule {} diff --git a/packages/twenty-server/src/engine/core-modules/sso/sso.resolver.ts b/packages/twenty-server/src/engine/core-modules/sso/sso.resolver.ts new file mode 100644 index 000000000000..e6e492b5b90f --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sso/sso.resolver.ts @@ -0,0 +1,97 @@ +/* @license Enterprise */ + +import { UseGuards } from '@nestjs/common'; +import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; + +import { SSOProviderEnabledGuard } from 'src/engine/core-modules/auth/guards/sso-provider-enabled.guard'; +import { DeleteSsoInput } from 'src/engine/core-modules/sso/dtos/delete-sso.input'; +import { DeleteSsoOutput } from 'src/engine/core-modules/sso/dtos/delete-sso.output'; +import { EditSsoInput } from 'src/engine/core-modules/sso/dtos/edit-sso.input'; +import { EditSsoOutput } from 'src/engine/core-modules/sso/dtos/edit-sso.output'; +import { FindAvailableSSOIDPInput } from 'src/engine/core-modules/sso/dtos/find-available-SSO-IDP.input'; +import { FindAvailableSSOIDPOutput } from 'src/engine/core-modules/sso/dtos/find-available-SSO-IDP.output'; +import { GetAuthorizationUrlInput } from 'src/engine/core-modules/sso/dtos/get-authorization-url.input'; +import { GetAuthorizationUrlOutput } from 'src/engine/core-modules/sso/dtos/get-authorization-url.output'; +import { + SetupOIDCSsoInput, + SetupSAMLSsoInput, +} from 'src/engine/core-modules/sso/dtos/setup-sso.input'; +import { SetupSsoOutput } from 'src/engine/core-modules/sso/dtos/setup-sso.output'; +import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; +import { SSOException } from 'src/engine/core-modules/sso/sso.exception'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator'; +import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; + +@Resolver() +export class SSOResolver { + constructor(private readonly sSOService: SSOService) {} + + @UseGuards(WorkspaceAuthGuard, SSOProviderEnabledGuard) + @Mutation(() => SetupSsoOutput) + async createOIDCIdentityProvider( + @Args('input') setupSsoInput: SetupOIDCSsoInput, + @AuthWorkspace() { id: workspaceId }: Workspace, + ): Promise { + return this.sSOService.createOIDCIdentityProvider( + setupSsoInput, + workspaceId, + ); + } + + @UseGuards(SSOProviderEnabledGuard) + @Mutation(() => [FindAvailableSSOIDPOutput]) + async findAvailableSSOIdentityProviders( + @Args('input') input: FindAvailableSSOIDPInput, + ): Promise> { + return this.sSOService.findAvailableSSOIdentityProviders(input.email); + } + + @UseGuards(SSOProviderEnabledGuard) + @Query(() => [FindAvailableSSOIDPOutput]) + async listSSOIdentityProvidersByWorkspaceId( + @AuthWorkspace() { id: workspaceId }: Workspace, + ) { + return this.sSOService.listSSOIdentityProvidersByWorkspaceId(workspaceId); + } + + @Mutation(() => GetAuthorizationUrlOutput) + async getAuthorizationUrl( + @Args('input') { identityProviderId }: GetAuthorizationUrlInput, + ) { + return this.sSOService.getAuthorizationUrl(identityProviderId); + } + + @UseGuards(WorkspaceAuthGuard, SSOProviderEnabledGuard) + @Mutation(() => SetupSsoOutput) + async createSAMLIdentityProvider( + @Args('input') setupSsoInput: SetupSAMLSsoInput, + @AuthWorkspace() { id: workspaceId }: Workspace, + ): Promise { + return this.sSOService.createSAMLIdentityProvider( + setupSsoInput, + workspaceId, + ); + } + + @UseGuards(WorkspaceAuthGuard, SSOProviderEnabledGuard) + @Mutation(() => DeleteSsoOutput) + async deleteSSOIdentityProvider( + @Args('input') { identityProviderId }: DeleteSsoInput, + @AuthWorkspace() { id: workspaceId }: Workspace, + ) { + return this.sSOService.deleteSSOIdentityProvider( + identityProviderId, + workspaceId, + ); + } + + @UseGuards(WorkspaceAuthGuard, SSOProviderEnabledGuard) + @Mutation(() => EditSsoOutput) + async editSSOIdentityProvider( + @Args('input') input: EditSsoInput, + @AuthWorkspace() { id: workspaceId }: Workspace, + ) { + return this.sSOService.editSSOIdentityProvider(input, workspaceId); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/sso/types/SSOConfigurations.type.ts b/packages/twenty-server/src/engine/core-modules/sso/types/SSOConfigurations.type.ts new file mode 100644 index 000000000000..ea41aded6d08 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sso/types/SSOConfigurations.type.ts @@ -0,0 +1,28 @@ +/* @license Enterprise */ + +import { + IdentityProviderType, + SSOIdentityProviderStatus, +} from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; + +type CommonSSOConfiguration = { + id: string; + issuer: string; + name?: string; + status: SSOIdentityProviderStatus; +}; + +export type OIDCConfiguration = { + type: IdentityProviderType.OIDC; + clientID: string; + clientSecret: string; +} & CommonSSOConfiguration; + +export type SAMLConfiguration = { + type: IdentityProviderType.SAML; + ssoURL: string; + certificate: string; + fingerprint?: string; +} & CommonSSOConfiguration; + +export type SSOConfiguration = OIDCConfiguration | SAMLConfiguration; diff --git a/packages/twenty-server/src/engine/core-modules/sso/workspace-sso-identity-provider.entity.ts b/packages/twenty-server/src/engine/core-modules/sso/workspace-sso-identity-provider.entity.ts new file mode 100644 index 000000000000..b860353314c1 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/sso/workspace-sso-identity-provider.entity.ts @@ -0,0 +1,110 @@ +/* @license Enterprise */ + +import { ObjectType, registerEnumType } from '@nestjs/graphql'; + +import { IDField } from '@ptc-org/nestjs-query-graphql'; +import { + Column, + CreateDateColumn, + Entity, + JoinColumn, + ManyToOne, + PrimaryGeneratedColumn, + Relation, + UpdateDateColumn, +} from 'typeorm'; + +import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; + +export enum IdentityProviderType { + OIDC = 'OIDC', + SAML = 'SAML', +} + +export enum OIDCResponseType { + // Only Authorization Code is used for now + CODE = 'code', + ID_TOKEN = 'id_token', + TOKEN = 'token', + NONE = 'none', +} + +registerEnumType(IdentityProviderType, { + name: 'IdpType', +}); + +export enum SSOIdentityProviderStatus { + Active = 'Active', + Inactive = 'Inactive', + Error = 'Error', +} + +registerEnumType(SSOIdentityProviderStatus, { + name: 'SSOIdentityProviderStatus', +}); + +@Entity({ name: 'workspaceSSOIdentityProvider', schema: 'core' }) +@ObjectType('WorkspaceSSOIdentityProvider') +export class WorkspaceSSOIdentityProvider { + // COMMON + @IDField(() => UUIDScalarType) + @PrimaryGeneratedColumn('uuid') + id: string; + + @Column() + name: string; + + @Column({ + type: 'enum', + enum: SSOIdentityProviderStatus, + default: SSOIdentityProviderStatus.Active, + }) + status: SSOIdentityProviderStatus; + + @ManyToOne( + () => Workspace, + (workspace) => workspace.workspaceSSOIdentityProviders, + { + onDelete: 'CASCADE', + }, + ) + @JoinColumn({ name: 'workspaceId' }) + workspace: Relation; + + @Column() + workspaceId: string; + + @CreateDateColumn({ type: 'timestamptz' }) + createdAt: Date; + + @UpdateDateColumn({ type: 'timestamptz' }) + updatedAt: Date; + + @Column({ + type: 'enum', + enum: IdentityProviderType, + default: IdentityProviderType.OIDC, + }) + type: IdentityProviderType; + + @Column() + issuer: string; + + // OIDC + @Column({ nullable: true }) + clientID?: string; + + @Column({ nullable: true }) + clientSecret?: string; + + // SAML + @Column({ nullable: true }) + ssoURL?: string; + + @Column({ nullable: true }) + certificate?: string; + + @Column({ nullable: true }) + fingerprint?: string; +} diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts index 4f26a8b0e0df..3810176f3592 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts @@ -126,7 +126,7 @@ export class UserWorkspaceService extends TypeOrmQueryService { throw new Error('Invalid invitation token'); } - if (appToken.context?.email !== email) { + if (!appToken.context?.email && appToken.context?.email !== email) { throw new Error('Email does not match the invitation'); } diff --git a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts index 8c193efefaa6..7e7f8cf1f79b 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts @@ -36,7 +36,7 @@ export class WorkspaceInvitationService { private readonly onboardingService: OnboardingService, ) {} - private async getOneWorkspaceInvitation(workspaceId: string, email: string) { + async getOneWorkspaceInvitation(workspaceId: string, email: string) { return await this.appTokenRepository .createQueryBuilder('appToken') .where('"appToken"."workspaceId" = :workspaceId', { @@ -160,7 +160,7 @@ export class WorkspaceInvitationService { }, }); - if (!appToken || !appToken.context || !('email' in appToken.context)) { + if (!appToken || !appToken.context?.email) { throw new WorkspaceInvitationException( 'Invalid appToken', WorkspaceInvitationExceptionCode.INVALID_INVITATION, diff --git a/packages/twenty-server/src/engine/core-modules/workspace/dtos/update-workspace-input.ts b/packages/twenty-server/src/engine/core-modules/workspace/dtos/update-workspace-input.ts index 75b40376c38c..f56a8a54c6ff 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/dtos/update-workspace-input.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/dtos/update-workspace-input.ts @@ -24,6 +24,11 @@ export class UpdateWorkspaceInput { @IsOptional() inviteHash?: string; + @Field({ nullable: true }) + @IsBoolean() + @IsOptional() + isPublicInviteLinkEnabled?: boolean; + @Field({ nullable: true }) @IsBoolean() @IsOptional() diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts index b9948f623b2c..ac5c38287104 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts @@ -19,6 +19,7 @@ import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-p import { PostgresCredentials } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.entity'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { User } from 'src/engine/core-modules/user/user.entity'; +import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; export enum WorkspaceActivationStatus { ONGOING_CREATION = 'ONGOING_CREATION', @@ -92,6 +93,10 @@ export class Workspace { @Column({ default: true }) allowImpersonation: boolean; + @Field() + @Column({ default: true }) + isPublicInviteLinkEnabled: boolean; + @OneToMany(() => FeatureFlagEntity, (featureFlag) => featureFlag.workspace) featureFlags: Relation; @@ -118,6 +123,12 @@ export class Workspace { ) allPostgresCredentials: Relation; + @OneToMany( + () => WorkspaceSSOIdentityProvider, + (workspaceSSOIdentityProviders) => workspaceSSOIdentityProviders.workspace, + ) + workspaceSSOIdentityProviders: Relation; + @Field() @Column({ default: 1 }) metadataVersion: number; diff --git a/packages/twenty-server/src/engine/middlewares/graphql-hydrate-request-from-token.middleware.ts b/packages/twenty-server/src/engine/middlewares/graphql-hydrate-request-from-token.middleware.ts index 7375c64a8e4b..4ea5ba05ffcf 100644 --- a/packages/twenty-server/src/engine/middlewares/graphql-hydrate-request-from-token.middleware.ts +++ b/packages/twenty-server/src/engine/middlewares/graphql-hydrate-request-from-token.middleware.ts @@ -54,6 +54,8 @@ export class GraphQLHydrateRequestFromTokenMiddleware 'UpdatePasswordViaResetToken', 'IntrospectionQuery', 'ExchangeAuthorizationCode', + 'GetAuthorizationUrl', + 'FindAvailableSSOIdentityProviders', ]; if ( diff --git a/packages/twenty-server/src/main.ts b/packages/twenty-server/src/main.ts index b00532747154..469845f41103 100644 --- a/packages/twenty-server/src/main.ts +++ b/packages/twenty-server/src/main.ts @@ -2,12 +2,15 @@ import { ValidationPipe } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; import { NestExpressApplication } from '@nestjs/platform-express'; +import session from 'express-session'; import bytes from 'bytes'; import { useContainer } from 'class-validator'; import { graphqlUploadExpress } from 'graphql-upload'; import { LoggerService } from 'src/engine/core-modules/logger/logger.service'; import { ApplyCorsToExceptions } from 'src/utils/apply-cors-to-exceptions'; +import { getSessionStorageOptions } from 'src/engine/core-modules/session-storage/session-storage.module-factory'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { AppModule } from './app.module'; import './instrument'; @@ -23,6 +26,7 @@ const bootstrap = async () => { snapshot: process.env.DEBUG_MODE === 'true', }); const logger = app.get(LoggerService); + const environmentService = app.get(EnvironmentService); // TODO: Double check this as it's not working for now, it's going to be heplful for durable trees in twenty "orm" // // Apply context id strategy for durable trees @@ -59,6 +63,11 @@ const bootstrap = async () => { // Create the env-config.js of the front at runtime generateFrontConfig(); + // Enable session - Today it's used only for SSO + if (environmentService.get('AUTH_SSO_ENABLED')) { + app.use(session(getSessionStorageOptions(environmentService))); + } + await app.listen(process.env.PORT ?? 3000); }; diff --git a/packages/twenty-ui/src/display/icon/assets/lock.svg b/packages/twenty-ui/src/display/icon/assets/lock.svg new file mode 100644 index 000000000000..6fd1e546430e --- /dev/null +++ b/packages/twenty-ui/src/display/icon/assets/lock.svg @@ -0,0 +1,19 @@ + + + + + + + + + + + + + + + + + + + diff --git a/packages/twenty-ui/src/display/icon/components/IconLock.tsx b/packages/twenty-ui/src/display/icon/components/IconLock.tsx new file mode 100644 index 000000000000..32053e4663f3 --- /dev/null +++ b/packages/twenty-ui/src/display/icon/components/IconLock.tsx @@ -0,0 +1,13 @@ +import { useTheme } from '@emotion/react'; + +import IconLockRaw from '@ui/display/icon/assets/lock.svg?react'; +import { IconComponentProps } from '@ui/display/icon/types/IconComponent'; + +type IconLockCustomProps = Pick; + +export const IconLockCustom = (props: IconLockCustomProps) => { + const theme = useTheme(); + const size = props.size ?? theme.icon.size.lg; + + return ; +}; diff --git a/packages/twenty-ui/src/display/index.ts b/packages/twenty-ui/src/display/index.ts index bdccc2a098dc..3bab15bcf604 100644 --- a/packages/twenty-ui/src/display/index.ts +++ b/packages/twenty-ui/src/display/index.ts @@ -14,6 +14,7 @@ export * from './icon/components/IconAddressBook'; export * from './icon/components/IconGmail'; export * from './icon/components/IconGoogle'; export * from './icon/components/IconGoogleCalendar'; +export * from './icon/components/IconLock'; export * from './icon/components/IconMicrosoft'; export * from './icon/components/IconRelationManyToOne'; export * from './icon/components/IconTwentyStar'; diff --git a/yarn.lock b/yarn.lock index b6ed9d913b45..615c20e4f3b0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7705,6 +7705,40 @@ __metadata: languageName: node linkType: hard +"@node-saml/node-saml@npm:^5.0.0": + version: 5.0.0 + resolution: "@node-saml/node-saml@npm:5.0.0" + dependencies: + "@types/debug": "npm:^4.1.12" + "@types/qs": "npm:^6.9.11" + "@types/xml-encryption": "npm:^1.2.4" + "@types/xml2js": "npm:^0.4.14" + "@xmldom/is-dom-node": "npm:^1.0.1" + "@xmldom/xmldom": "npm:^0.8.10" + debug: "npm:^4.3.4" + xml-crypto: "npm:^6.0.0" + xml-encryption: "npm:^3.0.2" + xml2js: "npm:^0.6.2" + xmlbuilder: "npm:^15.1.1" + xpath: "npm:^0.0.34" + checksum: 10c0/50a7aab94d410c0b1169eb5b0cf13ac964281a88d6fc155345e82afb2d6ccc159db90ebffa89c3d348fc233c0558af8d2b7b11f0ce8e65f90cd8297c0d274c1a + languageName: node + linkType: hard + +"@node-saml/passport-saml@npm:^5.0.0": + version: 5.0.0 + resolution: "@node-saml/passport-saml@npm:5.0.0" + dependencies: + "@node-saml/node-saml": "npm:^5.0.0" + "@types/express": "npm:^4.17.21" + "@types/passport": "npm:^1.0.16" + "@types/passport-strategy": "npm:^0.2.38" + passport: "npm:^0.7.0" + passport-strategy: "npm:^1.0.0" + checksum: 10c0/bbe72899ce26bb830147f53c44f7399e459ec852c6b5837b5e03e9652def53a62cd3a39ef0a27024ab616f8630d198a25481c729c25e52375f506e3825b930dd + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -15681,7 +15715,7 @@ __metadata: languageName: node linkType: hard -"@types/debug@npm:^4.0.0": +"@types/debug@npm:^4.0.0, @types/debug@npm:^4.1.12": version: 4.1.12 resolution: "@types/debug@npm:4.1.12" dependencies: @@ -15831,7 +15865,16 @@ __metadata: languageName: node linkType: hard -"@types/express@npm:*, @types/express@npm:^4.17.13, @types/express@npm:^4.7.0": +"@types/express-session@npm:^1.18.0": + version: 1.18.0 + resolution: "@types/express-session@npm:1.18.0" + dependencies: + "@types/express": "npm:*" + checksum: 10c0/a41a1fcc4a433c71e2a7ffbac82bc7fb5ad436757a9d27fd30ae4656dee137d244f04de9ad756b566be136cf82f6a75e9ca55ac6c260396e74d1931021b09621 + languageName: node + linkType: hard + +"@types/express@npm:*, @types/express@npm:^4.17.13, @types/express@npm:^4.17.21, @types/express@npm:^4.7.0": version: 4.17.21 resolution: "@types/express@npm:4.17.21" dependencies: @@ -16551,6 +16594,15 @@ __metadata: languageName: node linkType: hard +"@types/openid-client@npm:^3.7.0": + version: 3.7.0 + resolution: "@types/openid-client@npm:3.7.0" + dependencies: + openid-client: "npm:*" + checksum: 10c0/16f9bb3516e427ff580f664a329cfdbbe1dc7658c1aad08b6a45581c23588230a9b75e9fe5295316117a38f76c13e2d44c587a439cdaaae580f5f6059fbb435a + languageName: node + linkType: hard + "@types/parse-json@npm:^4.0.0": version: 4.0.2 resolution: "@types/parse-json@npm:4.0.2" @@ -16607,7 +16659,7 @@ __metadata: languageName: node linkType: hard -"@types/passport-strategy@npm:*": +"@types/passport-strategy@npm:*, @types/passport-strategy@npm:^0.2.38": version: 0.2.38 resolution: "@types/passport-strategy@npm:0.2.38" dependencies: @@ -16617,7 +16669,7 @@ __metadata: languageName: node linkType: hard -"@types/passport@npm:*": +"@types/passport@npm:*, @types/passport@npm:^1.0.16": version: 1.0.16 resolution: "@types/passport@npm:1.0.16" dependencies: @@ -16692,6 +16744,13 @@ __metadata: languageName: node linkType: hard +"@types/qs@npm:^6.9.11": + version: 6.9.16 + resolution: "@types/qs@npm:6.9.16" + checksum: 10c0/a4e871b80fff623755e356fd1f225aea45ff7a29da30f99fddee1a05f4f5f33485b314ab5758145144ed45708f97e44595aa9a8368e9bbc083932f931b12dbb6 + languageName: node + linkType: hard + "@types/range-parser@npm:*": version: 1.2.7 resolution: "@types/range-parser@npm:1.2.7" @@ -16981,6 +17040,24 @@ __metadata: languageName: node linkType: hard +"@types/xml-encryption@npm:^1.2.4": + version: 1.2.4 + resolution: "@types/xml-encryption@npm:1.2.4" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/33191fc1a8ef6b81108f438d3f3bc8aac987cb68eaab8f70653a1e231c903de7998f961078345fa5444f2681513c47d452e039bd438d66ebaebd4b907194175d + languageName: node + linkType: hard + +"@types/xml2js@npm:^0.4.14": + version: 0.4.14 + resolution: "@types/xml2js@npm:0.4.14" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/06776e7f7aec55a698795e60425417caa7d7db3ff680a7b4ccaae1567c5fec28ff49b9975e9a0d74ff4acb8f4a43730501bbe64f9f761d784c6476ba4db12e13 + languageName: node + linkType: hard + "@types/yargs-parser@npm:*": version: 21.0.3 resolution: "@types/yargs-parser@npm:21.0.3" @@ -17910,6 +17987,20 @@ __metadata: languageName: node linkType: hard +"@xmldom/is-dom-node@npm:^1.0.1": + version: 1.0.1 + resolution: "@xmldom/is-dom-node@npm:1.0.1" + checksum: 10c0/138d5e74441b16f065ce360d81737673986d93f14d5bb09b1e3bcfc2b18fae70b86beb9b7bfbffe916dd36b3bdab012acaa81cc0b49450acadfd66978b62827f + languageName: node + linkType: hard + +"@xmldom/xmldom@npm:^0.8.10, @xmldom/xmldom@npm:^0.8.5": + version: 0.8.10 + resolution: "@xmldom/xmldom@npm:0.8.10" + checksum: 10c0/c7647c442502720182b0d65b17d45d2d95317c1c8c497626fe524bda79b4fb768a9aa4fae2da919f308e7abcff7d67c058b102a9d641097e9a57f0b80187851f + languageName: node + linkType: hard + "@xobotyi/scrollbar-width@npm:^1.9.5": version: 1.9.5 resolution: "@xobotyi/scrollbar-width@npm:1.9.5" @@ -22564,6 +22655,15 @@ __metadata: languageName: node linkType: hard +"connect-redis@npm:^7.1.1": + version: 7.1.1 + resolution: "connect-redis@npm:7.1.1" + peerDependencies: + express-session: ">=1" + checksum: 10c0/eeb9e275176d1ef973c808df7c860c80300dfdecdee1a8ca20524fc4e37ccb2206923b07f17501fb7235cde73e9db9e06dff79ef17a54e5a57f35db247ec99fb + languageName: node + linkType: hard + "consola@npm:^2.15.0": version: 2.15.3 resolution: "consola@npm:2.15.3" @@ -22654,6 +22754,13 @@ __metadata: languageName: node linkType: hard +"cookie-signature@npm:1.0.7": + version: 1.0.7 + resolution: "cookie-signature@npm:1.0.7" + checksum: 10c0/e7731ad2995ae2efeed6435ec1e22cdd21afef29d300c27281438b1eab2bae04ef0d1a203928c0afec2cee72aa36540b8747406ebe308ad23c8e8cc3c26c9c51 + languageName: node + linkType: hard + "cookie@npm:0.5.0, cookie@npm:^0.5.0": version: 0.5.0 resolution: "cookie@npm:0.5.0" @@ -22668,6 +22775,13 @@ __metadata: languageName: node linkType: hard +"cookie@npm:0.7.2": + version: 0.7.2 + resolution: "cookie@npm:0.7.2" + checksum: 10c0/9596e8ccdbf1a3a88ae02cf5ee80c1c50959423e1022e4e60b91dd87c622af1da309253d8abdb258fb5e3eacb4f08e579dc58b4897b8087574eee0fd35dfa5d2 + languageName: node + linkType: hard + "cookiejar@npm:^2.1.4": version: 2.1.4 resolution: "cookiejar@npm:2.1.4" @@ -23974,7 +24088,7 @@ __metadata: languageName: node linkType: hard -"depd@npm:2.0.0": +"depd@npm:2.0.0, depd@npm:~2.0.0": version: 2.0.0 resolution: "depd@npm:2.0.0" checksum: 10c0/58bd06ec20e19529b06f7ad07ddab60e504d9e0faca4bd23079fac2d279c3594334d736508dc350e06e510aba5e22e4594483b3a6562ce7c17dd797f4cc4ad2c @@ -25414,7 +25528,7 @@ __metadata: languageName: node linkType: hard -"escape-html@npm:~1.0.3": +"escape-html@npm:^1.0.3, escape-html@npm:~1.0.3": version: 1.0.3 resolution: "escape-html@npm:1.0.3" checksum: 10c0/524c739d776b36c3d29fa08a22e03e8824e3b2fd57500e5e44ecf3cc4707c34c60f9ca0781c0e33d191f2991161504c295e98f68c78fe7baa6e57081ec6ac0a3 @@ -26185,6 +26299,22 @@ __metadata: languageName: node linkType: hard +"express-session@npm:^1.18.1": + version: 1.18.1 + resolution: "express-session@npm:1.18.1" + dependencies: + cookie: "npm:0.7.2" + cookie-signature: "npm:1.0.7" + debug: "npm:2.6.9" + depd: "npm:~2.0.0" + on-headers: "npm:~1.0.2" + parseurl: "npm:~1.3.3" + safe-buffer: "npm:5.2.1" + uid-safe: "npm:~2.1.5" + checksum: 10c0/7999f128df1528430044c97bb1aac95093afaee86c5fa54b2890c4aad9898d79745301f8c90c2df057d6dfe7af7f8ee220340bf5eb53dca5eff37e52cc2fbec7 + languageName: node + linkType: hard + "express@npm:4.18.2": version: 4.18.2 resolution: "express@npm:4.18.2" @@ -31429,7 +31559,7 @@ __metadata: languageName: node linkType: hard -"jose@npm:^4.11.4": +"jose@npm:^4.11.4, jose@npm:^4.15.9": version: 4.15.9 resolution: "jose@npm:4.15.9" checksum: 10c0/4ed4ddf4a029db04bd167f2215f65d7245e4dc5f36d7ac3c0126aab38d66309a9e692f52df88975d99429e357e5fd8bab340ff20baab544d17684dd1d940a0f4 @@ -36389,6 +36519,13 @@ __metadata: languageName: node linkType: hard +"object-hash@npm:^2.2.0": + version: 2.2.0 + resolution: "object-hash@npm:2.2.0" + checksum: 10c0/1527de843926c5442ed61f8bdddfc7dc181b6497f725b0e89fcf50a55d9c803088763ed447cac85a5aa65345f1e99c2469ba679a54349ef3c4c0aeaa396a3eb9 + languageName: node + linkType: hard + "object-hash@npm:^3.0.0": version: 3.0.0 resolution: "object-hash@npm:3.0.0" @@ -36521,6 +36658,13 @@ __metadata: languageName: node linkType: hard +"oidc-token-hash@npm:^5.0.3": + version: 5.0.3 + resolution: "oidc-token-hash@npm:5.0.3" + checksum: 10c0/d0dc0551406f09577874155cc83cf69c39e4b826293d50bb6c37936698aeca17d4bcee356ab910c859e53e83f2728a2acbd041020165191353b29de51fbca615 + languageName: node + linkType: hard + "on-finished@npm:2.4.1": version: 2.4.1 resolution: "on-finished@npm:2.4.1" @@ -36632,6 +36776,18 @@ __metadata: languageName: node linkType: hard +"openid-client@npm:*, openid-client@npm:^5.7.0": + version: 5.7.0 + resolution: "openid-client@npm:5.7.0" + dependencies: + jose: "npm:^4.15.9" + lru-cache: "npm:^6.0.0" + object-hash: "npm:^2.2.0" + oidc-token-hash: "npm:^5.0.3" + checksum: 10c0/02e42c66415581262c0372e178dba2bc958f1b5cfd2eb502b4f71b7718fc11dfac37b12117b1c73cff5dc80f5871cd830e175aae95ae212fbd353f3efa1de091 + languageName: node + linkType: hard + "optimism@npm:^0.18.0": version: 0.18.0 resolution: "optimism@npm:0.18.0" @@ -38910,6 +39066,13 @@ __metadata: languageName: node linkType: hard +"random-bytes@npm:~1.0.0": + version: 1.0.0 + resolution: "random-bytes@npm:1.0.0" + checksum: 10c0/71e7a600e0976e9ebc269793a0577d47b965fa678fcc9e9623e427f909d1b3669db5b3a178dbf61229f0724ea23dba64db389f0be0ba675c6a6b837c02f29b8f + languageName: node + linkType: hard + "randombytes@npm:^2.0.0, randombytes@npm:^2.0.1, randombytes@npm:^2.0.5, randombytes@npm:^2.1.0": version: 2.1.0 resolution: "randombytes@npm:2.1.0" @@ -39894,7 +40057,7 @@ __metadata: languageName: node linkType: hard -"redis@npm:^4.6.13": +"redis@npm:^4.6.13, redis@npm:^4.7.0": version: 4.7.0 resolution: "redis@npm:4.7.0" dependencies: @@ -41013,6 +41176,13 @@ __metadata: languageName: node linkType: hard +"sax@npm:>=0.6.0": + version: 1.4.1 + resolution: "sax@npm:1.4.1" + checksum: 10c0/6bf86318a254c5d898ede6bd3ded15daf68ae08a5495a2739564eb265cd13bcc64a07ab466fb204f67ce472bb534eb8612dac587435515169593f4fffa11de7c + languageName: node + linkType: hard + "saxes@npm:^6.0.0": version: 6.0.0 resolution: "saxes@npm:6.0.0" @@ -43934,10 +44104,12 @@ __metadata: "@nestjs/cli": "npm:10.3.0" "@nestjs/devtools-integration": "npm:^0.1.6" "@nestjs/graphql": "patch:@nestjs/graphql@12.1.1#./patches/@nestjs+graphql+12.1.1.patch" + "@node-saml/passport-saml": "npm:^5.0.0" "@nx/js": "npm:18.3.3" "@ptc-org/nestjs-query-graphql": "patch:@ptc-org/nestjs-query-graphql@4.2.0#./patches/@ptc-org+nestjs-query-graphql+4.2.0.patch" "@revertdotdev/revert-react": "npm:^0.0.21" "@sentry/nestjs": "npm:^8.30.0" + "@types/express-session": "npm:^1.18.0" "@types/lodash.differencewith": "npm:^4.5.9" "@types/lodash.isempty": "npm:^4.4.7" "@types/lodash.isequal": "npm:^4.5.8" @@ -43949,11 +44121,14 @@ __metadata: "@types/lodash.uniq": "npm:^4.5.9" "@types/lodash.uniqby": "npm:^4.7.9" "@types/lodash.upperfirst": "npm:^4.3.7" + "@types/openid-client": "npm:^3.7.0" "@types/react": "npm:^18.2.39" "@types/unzipper": "npm:^0" cache-manager: "npm:^5.4.0" cache-manager-redis-yet: "npm:^4.1.2" class-validator: "patch:class-validator@0.14.0#./patches/class-validator+0.14.0.patch" + connect-redis: "npm:^7.1.1" + express-session: "npm:^1.18.1" graphql-middleware: "npm:^6.1.35" handlebars: "npm:^4.7.8" jsdom: "npm:~22.1.0" @@ -43967,8 +44142,10 @@ __metadata: lodash.uniqby: "npm:^4.7.0" monaco-editor: "npm:^0.51.0" monaco-editor-auto-typings: "npm:^0.4.5" + openid-client: "npm:^5.7.0" passport: "npm:^0.7.0" psl: "npm:^1.9.0" + redis: "npm:^4.7.0" rimraf: "npm:^5.0.5" ts-morph: "npm:^24.0.0" tsconfig-paths: "npm:^4.2.0" @@ -44678,6 +44855,15 @@ __metadata: languageName: node linkType: hard +"uid-safe@npm:~2.1.5": + version: 2.1.5 + resolution: "uid-safe@npm:2.1.5" + dependencies: + random-bytes: "npm:~1.0.0" + checksum: 10c0/ec96862e859fd12175f3da7fda9d1359a2cf412fd521e10837cbdc6d554774079ce252f366981df9401283841c8924782f6dbee8f82a3a81f805ed8a8584595d + languageName: node + linkType: hard + "uid2@npm:0.0.x": version: 0.0.4 resolution: "uid2@npm:0.0.4" @@ -46752,6 +46938,28 @@ __metadata: languageName: node linkType: hard +"xml-crypto@npm:^6.0.0": + version: 6.0.0 + resolution: "xml-crypto@npm:6.0.0" + dependencies: + "@xmldom/is-dom-node": "npm:^1.0.1" + "@xmldom/xmldom": "npm:^0.8.10" + xpath: "npm:^0.0.33" + checksum: 10c0/1a9d8be4cc7a4c618fa413b8ef30f11cda9ae81f20bc03e84c51f6c61383168a9915f8c3a26061e2053e58807b76d3a13726338f7bc0d8c45285fbb1da296293 + languageName: node + linkType: hard + +"xml-encryption@npm:^3.0.2": + version: 3.0.2 + resolution: "xml-encryption@npm:3.0.2" + dependencies: + "@xmldom/xmldom": "npm:^0.8.5" + escape-html: "npm:^1.0.3" + xpath: "npm:0.0.32" + checksum: 10c0/fcad4244f76c9b849f4168e6712c96281badb25e5ebeaae3da1e837386440527f33f3452b529949794d16072d12b0f9fa0405052445c9ce52b9311f557eb0dcb + languageName: node + linkType: hard + "xml-formatter@npm:^2.6.1": version: 2.6.1 resolution: "xml-formatter@npm:2.6.1" @@ -46775,6 +46983,16 @@ __metadata: languageName: node linkType: hard +"xml2js@npm:^0.6.2": + version: 0.6.2 + resolution: "xml2js@npm:0.6.2" + dependencies: + sax: "npm:>=0.6.0" + xmlbuilder: "npm:~11.0.0" + checksum: 10c0/e98a84e9c172c556ee2c5afa0fc7161b46919e8b53ab20de140eedea19903ed82f7cd5b1576fb345c84f0a18da1982ddf65908129b58fc3d7cbc658ae232108f + languageName: node + linkType: hard + "xml@npm:^1.0.1": version: 1.0.1 resolution: "xml@npm:1.0.1" @@ -46782,6 +47000,20 @@ __metadata: languageName: node linkType: hard +"xmlbuilder@npm:^15.1.1": + version: 15.1.1 + resolution: "xmlbuilder@npm:15.1.1" + checksum: 10c0/665266a8916498ff8d82b3d46d3993913477a254b98149ff7cff060d9b7cc0db7cf5a3dae99aed92355254a808c0e2e3ec74ad1b04aa1061bdb8dfbea26c18b8 + languageName: node + linkType: hard + +"xmlbuilder@npm:~11.0.0": + version: 11.0.1 + resolution: "xmlbuilder@npm:11.0.1" + checksum: 10c0/74b979f89a0a129926bc786b913459bdbcefa809afaa551c5ab83f89b1915bdaea14c11c759284bb9b931e3b53004dbc2181e21d3ca9553eeb0b2a7b4e40c35b + languageName: node + linkType: hard + "xmlchars@npm:^2.2.0": version: 2.2.0 resolution: "xmlchars@npm:2.2.0" @@ -46789,6 +47021,27 @@ __metadata: languageName: node linkType: hard +"xpath@npm:0.0.32": + version: 0.0.32 + resolution: "xpath@npm:0.0.32" + checksum: 10c0/3743ab91a8ec1b5eac1f27ddf2fbf696fcde8ce487215becde1502b85a309dcd1b0baeaac1ee7a730aea4787d049b67ae89e8aedbe03a5a07a71e62ec296d9de + languageName: node + linkType: hard + +"xpath@npm:^0.0.33": + version: 0.0.33 + resolution: "xpath@npm:0.0.33" + checksum: 10c0/ac2c04142c0f38e75f0d899b6818b08a0e8163aab5d6fd8a292f31a6e925ab08ee48feb1f447049c5bbcb8926b7241c79d1d4a51386e6f6f2d76ac5784917b9d + languageName: node + linkType: hard + +"xpath@npm:^0.0.34": + version: 0.0.34 + resolution: "xpath@npm:0.0.34" + checksum: 10c0/88335108884ca164421f7fed048ef1a18ab3f7b1ae446b627fd3f51fc2396dcce798601c5e426de3bbd55d5940b84cf2326c75cd76620c1b49491283b85de17a + languageName: node + linkType: hard + "xss@npm:^1.0.8": version: 1.0.15 resolution: "xss@npm:1.0.15" From 4407b1aaa2c626bc6a1f9238207b6b673524d1ec Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Tue, 22 Oct 2024 00:18:12 +0530 Subject: [PATCH 66/67] Minor page header fix (#7927) Screenshot 2024-10-22 at 00 03 03 Screenshot 2024-10-22 at 00 04 33 --- .../src/modules/ui/layout/page/components/PageHeader.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/PageHeader.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/PageHeader.tsx index 82a85687e499..8cdb9cfea7f2 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/components/PageHeader.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/components/PageHeader.tsx @@ -19,7 +19,7 @@ import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; export const PAGE_BAR_MIN_HEIGHT = 40; -const StyledTopBarContainer = styled.div<{ width?: number }>` +const StyledTopBarContainer = styled.div` align-items: center; background: ${({ theme }) => theme.background.noisy}; color: ${({ theme }) => theme.font.color.primary}; @@ -31,7 +31,6 @@ const StyledTopBarContainer = styled.div<{ width?: number }>` padding: ${({ theme }) => theme.spacing(2)}; padding-left: 0; padding-right: ${({ theme }) => theme.spacing(3)}; - width: ${({ width }) => (width ? `${width}px` : '100%')}; @media (max-width: ${MOBILE_VIEWPORT}px) { width: 100%; @@ -91,7 +90,6 @@ type PageHeaderProps = { navigateToNextRecord?: () => void; Icon?: IconComponent; children?: ReactNode; - width?: number; }; export const PageHeader = ({ @@ -105,7 +103,6 @@ export const PageHeader = ({ navigateToNextRecord, Icon, children, - width, }: PageHeaderProps) => { const isMobile = useIsMobile(); const theme = useTheme(); @@ -114,7 +111,7 @@ export const PageHeader = ({ ); return ( - + {!isMobile && !isNavigationDrawerExpanded && ( From 5e2df81211271f89d15d840ff502124a500fd1df Mon Sep 17 00:00:00 2001 From: ZiaCodes <72739794+Khaan25@users.noreply.github.com> Date: Tue, 22 Oct 2024 00:28:01 +0500 Subject: [PATCH 67/67] fix: hidden settings menu affects settings layout (#7769) This PR fixes #6746 --------- Co-authored-by: Charles Bochet --- .../app/components/AppRouterProviders.tsx | 4 +- .../GoToHotkeyItemEffect.tsx | 2 +- .../effect-components/GotoHotkeysEffect.tsx | 19 ---------- .../GotoHotkeysEffectsProvider.tsx | 37 +++++++++++++++++++ .../components/AppNavigationDrawer.tsx | 14 +------ .../hooks/__tests__/useGoToHotkeys.test.tsx | 4 +- .../utilities/hotkey/hooks/useGoToHotkeys.ts | 13 ++++++- 7 files changed, 56 insertions(+), 37 deletions(-) delete mode 100644 packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx create mode 100644 packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffectsProvider.tsx diff --git a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx index 80b02508f736..df7bb083f9dc 100644 --- a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx +++ b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx @@ -1,6 +1,6 @@ import { ApolloProvider } from '@/apollo/components/ApolloProvider'; import { CommandMenuEffect } from '@/app/effect-components/CommandMenuEffect'; -import { GotoHotkeys } from '@/app/effect-components/GotoHotkeysEffect'; +import { GotoHotkeysEffectsProvider } from '@/app/effect-components/GotoHotkeysEffectsProvider'; import { PageChangeEffect } from '@/app/effect-components/PageChangeEffect'; import { AuthProvider } from '@/auth/components/AuthProvider'; import { ChromeExtensionSidecarEffect } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarEffect'; @@ -45,7 +45,7 @@ export const AppRouterProviders = () => { - + diff --git a/packages/twenty-front/src/modules/app/effect-components/GoToHotkeyItemEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/GoToHotkeyItemEffect.tsx index a0b545302501..d6f9f70d7a72 100644 --- a/packages/twenty-front/src/modules/app/effect-components/GoToHotkeyItemEffect.tsx +++ b/packages/twenty-front/src/modules/app/effect-components/GoToHotkeyItemEffect.tsx @@ -6,7 +6,7 @@ export const GoToHotkeyItemEffect = (props: { }) => { const { hotkey, pathToNavigateTo } = props; - useGoToHotkeys(hotkey, pathToNavigateTo); + useGoToHotkeys({ key: hotkey, location: pathToNavigateTo }); return <>; }; diff --git a/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx deleted file mode 100644 index 202b58b963e5..000000000000 --- a/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx +++ /dev/null @@ -1,19 +0,0 @@ -import { GoToHotkeyItemEffect } from '@/app/effect-components/GoToHotkeyItemEffect'; -import { useNonSystemActiveObjectMetadataItems } from '@/object-metadata/hooks/useNonSystemActiveObjectMetadataItems'; -import { useGoToHotkeys } from '@/ui/utilities/hotkey/hooks/useGoToHotkeys'; - -export const GotoHotkeys = () => { - const { nonSystemActiveObjectMetadataItems } = - useNonSystemActiveObjectMetadataItems(); - - // Hardcoded since settings is static - useGoToHotkeys('s', '/settings/profile'); - - return nonSystemActiveObjectMetadataItems.map((objectMetadataItem) => ( - - )); -}; diff --git a/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffectsProvider.tsx b/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffectsProvider.tsx new file mode 100644 index 000000000000..44267f5c34e3 --- /dev/null +++ b/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffectsProvider.tsx @@ -0,0 +1,37 @@ +import { GoToHotkeyItemEffect } from '@/app/effect-components/GoToHotkeyItemEffect'; +import { useNonSystemActiveObjectMetadataItems } from '@/object-metadata/hooks/useNonSystemActiveObjectMetadataItems'; +import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; +import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState'; +import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; +import { useGoToHotkeys } from '@/ui/utilities/hotkey/hooks/useGoToHotkeys'; +import { useLocation } from 'react-router-dom'; +import { useRecoilCallback } from 'recoil'; + +export const GotoHotkeysEffectsProvider = () => { + const { nonSystemActiveObjectMetadataItems } = + useNonSystemActiveObjectMetadataItems(); + + const location = useLocation(); + + useGoToHotkeys({ + key: 's', + location: '/settings/profile', + preNavigateFunction: useRecoilCallback( + ({ set }) => + () => { + set(isNavigationDrawerExpandedState, true); + set(navigationDrawerExpandedMemorizedState, true); + set(navigationMemorizedUrlState, location.pathname + location.search); + }, + [location.pathname, location.search], + ), + }); + + return nonSystemActiveObjectMetadataItems.map((objectMetadataItem) => ( + + )); +}; diff --git a/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx b/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx index 258473603420..b286b4e91d63 100644 --- a/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx +++ b/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx @@ -1,5 +1,4 @@ -import { useEffect } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { SettingsNavigationDrawerItems } from '@/settings/components/SettingsNavigationDrawerItems'; @@ -9,13 +8,11 @@ import { NavigationDrawerProps, } from '@/ui/navigation/navigation-drawer/components/NavigationDrawer'; -import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI'; import { useIsSettingsDrawer } from '@/navigation/hooks/useIsSettingsDrawer'; import { AdvancedSettingsToggle } from '@/ui/navigation/link/components/AdvancedSettingsToggle'; -import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; import { MainNavigationDrawerItems } from './MainNavigationDrawerItems'; export type AppNavigationDrawerProps = { @@ -25,11 +22,8 @@ export type AppNavigationDrawerProps = { export const AppNavigationDrawer = ({ className, }: AppNavigationDrawerProps) => { - const isMobile = useIsMobile(); const isSettingsDrawer = useIsSettingsDrawer(); - const setIsNavigationDrawerExpanded = useSetRecoilState( - isNavigationDrawerExpandedState, - ); + const currentWorkspace = useRecoilValue(currentWorkspaceState); const drawerProps: NavigationDrawerProps = isSettingsDrawer @@ -48,10 +42,6 @@ export const AppNavigationDrawer = ({ footer: , }; - useEffect(() => { - setIsNavigationDrawerExpanded(!isMobile); - }, [isMobile, setIsNavigationDrawerExpanded]); - return ( { it('should navigate on hotkey trigger', () => { const { result } = renderHook(() => { - useGoToHotkeys('a', '/three'); + useGoToHotkeys({ key: 'a', location: '/three' }); const setHotkeyScope = useSetHotkeyScope(); diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGoToHotkeys.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGoToHotkeys.ts index aeb485b4c02f..d8e62312cfeb 100644 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGoToHotkeys.ts +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useGoToHotkeys.ts @@ -5,13 +5,24 @@ import { AppHotkeyScope } from '../types/AppHotkeyScope'; import { useSequenceHotkeys } from './useSequenceScopedHotkeys'; -export const useGoToHotkeys = (key: Keys, location: string) => { +type GoToHotkeysProps = { + key: Keys; + location: string; + preNavigateFunction?: () => void; +}; + +export const useGoToHotkeys = ({ + key, + location, + preNavigateFunction, +}: GoToHotkeysProps) => { const navigate = useNavigate(); useSequenceHotkeys( 'g', key, () => { + preNavigateFunction?.(); navigate(location); }, AppHotkeyScope.Goto,