From 03dd7527b70205712e8abd628b23088e03b66555 Mon Sep 17 00:00:00 2001 From: Harshit Singh <73997189+harshit078@users.noreply.github.com> Date: Wed, 16 Oct 2024 21:22:32 +0530 Subject: [PATCH 001/123] fix: Array data type accepts whitespace as input (#7707) ## Description - This PR fixes the issue #7593 Co-authored-by: bosiraphael --- .../meta-types/input/components/MultiItemFieldInput.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx index 7e3e93ec2c..e416e50ce8 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx @@ -18,6 +18,7 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis import { FieldMetadataType } from '~/generated-metadata/graphql'; import { moveArrayItem } from '~/utils/array/moveArrayItem'; import { toSpliced } from '~/utils/array/toSpliced'; +import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly'; const StyledDropdownMenu = styled(DropdownMenu)` left: -1px; @@ -190,7 +191,11 @@ export const MultiItemFieldInput = ({ }) : undefined } - onChange={(event) => handleOnChange(event.target.value)} + onChange={(event) => + handleOnChange( + turnIntoEmptyStringIfWhitespacesOnly(event.target.value), + ) + } onEnter={handleSubmitInput} rightComponent={ Date: Wed, 16 Oct 2024 21:50:39 +0530 Subject: [PATCH 002/123] Fix: Remove Deleted filter not reflecting issue (#7676) ## PR Summary This Pull request fixes #7626 Adding Deleted filter from option will add filter label as "Deleted" in tableFiltersState, But on click of "Remove Deleted filter" "Deleted at" is used for finding tableFilter id, which results in tableFilter id as undefined. --- .../empty-state/components/RecordTableEmptyStateSoftDelete.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete.tsx index 71eb045abd..a025916be6 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateSoftDelete.tsx @@ -28,7 +28,7 @@ export const RecordTableEmptyStateSoftDelete = () => { deleteCombinedViewFilter( tableFilters.find( (filter) => - filter.definition.label === 'Deleted at' && + filter.definition.label === 'Deleted' && filter.operand === 'isNotEmpty', )?.id ?? '', ); From f26c65fd41b75c8291ada3c0152f7298c9922dfe Mon Sep 17 00:00:00 2001 From: Charles Bochet Date: Thu, 17 Oct 2024 11:50:43 +0200 Subject: [PATCH 003/123] 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 e90e7012fe..52ca3c825f 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 004/123] 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 bab186af52..6d5ab0751d 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 f16c6fe436..26679a18fc 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 005/123] 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 7cedf04348..c5778aa649 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 191dc9edf4..7e0fcbf9de 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 0000000000..8829699154 --- /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 0000000000..22ebbd7bf3 --- /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 0000000000..d12a94ecf4 --- /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 0000000000..31e18b25fe --- /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 d3400e4a26..9d8eabefb6 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 006/123] 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 feeba5aabc..4b61fa58ea 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 007/123] =?UTF-8?q?[=F0=9F=95=B9=EF=B8=8F]=20Twenty=20Desi?= =?UTF-8?q?gn=20Challenges:=20New=20twenty=20logo=20by=20Shlok-py=20(#7790?= =?UTF-8?q?)?= 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 fca791fc04..d67c49b641 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 008/123] 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 78edaedd3d..9f1f55ae76 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 009/123] [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 b8fb82f4c8..e768984fbd 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 a33b017977..0000000000 --- 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 10b5cc9e20..deb2ba3ee5 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 ed8ad716b6..de8dec76a9 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 bd25d66eb4..0000000000 --- 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 5b635d3bcc..9f1937cae2 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 0000000000..3b1a46f89a --- /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 01f262c8f2..0e0134578e 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 cbbeb216f0..99b4f0b1e1 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 a196db0861..5515abb9a6 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 440a6484e6..fc96379633 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 99a573235c..77a4507188 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 d8080b3cd5..60da5e4673 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 5d03268b84..023b087691 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 6056af6ac6..baab9d5003 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 50e65547e4..45a8c87a84 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 0f89ba5491..2508ff628a 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 63c7f9eec4..1273e624b0 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 e17fd5b285..0a6858940c 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 f58e702e55..ea410fabc9 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 0000000000..02d3d54b05 --- /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 a250550f4b..db17b067bb 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 8100a885d8..58f9b3ea80 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 45c190c536..1271455c0c 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 714bf06bb6..85ec6e2a50 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 30d309dc32..8cd6b76a34 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 eb13fedbac..5f25d3ffa1 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 80d1458ab6..afeb568c4c 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 9eebe96a0d..82099405c9 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 28b981e22d..7a8454a12f 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 0000000000..a9fa0f88f4 --- /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 0000000000..7d9be33624 --- /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 0000000000..64b5fa8c2f --- /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 0000000000..6d403e20d2 --- /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 0000000000..1debda9ff4 --- /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 0000000000..0a7ecd6f8f --- /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 0000000000..f864a62857 --- /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 0000000000..749ed8c12d --- /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 0000000000..28196dce80 --- /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 0000000000..b72c8aeaa4 --- /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 0000000000..76ef636a0d --- /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 0000000000..549f1d3011 --- /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 0000000000..da309385f7 --- /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 0000000000..d00c81ecb1 --- /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 0000000000..982aff7267 --- /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 0000000000..514b67bb32 --- /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 0000000000..c39ccae7c5 --- /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 0000000000..71b9ee4862 --- /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 0000000000..5c38ebfeb4 --- /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 0000000000..4865fb8d31 --- /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 0000000000..45188e8aca --- /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 0000000000..8965c5006f --- /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 0000000000..0f63d73d71 --- /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 0000000000..8c45c0c7e2 --- /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 0000000000..1bf7385157 --- /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 0000000000..76f9d7b1ec --- /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 0000000000..d9af7a1c6a --- /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 0000000000..b87ec60f1d --- /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 0000000000..b84def1a81 --- /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 0000000000..fe1e96c4b6 --- /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 0000000000..7f5b2e6f6a --- /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 0000000000..716c4a8433 --- /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 0000000000..d5a93db25e --- /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 0000000000..ddf55a1a49 --- /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 0000000000..6307d8ae6b --- /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 0000000000..86bd6df809 --- /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 0000000000..b12d780dfe --- /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 0000000000..efc76b4043 --- /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 0000000000..9e8d50619e --- /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 e54e855d31..b9d5cb4930 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 900fd3de5c..016341966b 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 a5ef6a5f96..3e5c72fec0 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 0585687639..24b28bc5b6 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 8caa942b2b..e76c2f12fd 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 fc29b1d4c2..850d24cf87 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 122a8c398f..29cf849985 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 7c4224b69a..aaf181bf38 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 0000000000..6859b52abe --- /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 0000000000..cf3a7d113e --- /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 0000000000..a1c0450f02 --- /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 5ef7a415d8..63fd94d81b 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 0000000000..70604c0093 --- /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 0000000000..ed477b1a77 --- /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 0000000000..2bfe3c158e --- /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 0000000000..f3cfd765b2 --- /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 0000000000..f664a40882 --- /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 0000000000..4062e9319f --- /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 0000000000..752e9aca0c --- /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 0000000000..1a6972a841 --- /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 0000000000..21b3e88971 --- /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 0000000000..688ae91999 --- /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 0000000000..cf78272412 --- /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 0000000000..123de9b6b1 --- /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 010/123] 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 52ca3c825f..595081c0a9 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 011/123] 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 1b55e6a327..55c2a77f3c 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 012/123] 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 a4c9f5306f..3d513dc1ef 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 9a1148447a..30cf3a37a3 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 013/123] 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 75f3a982e9..35d3ccc08d 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 014/123] 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 9b50515b10..ca1ef939ab 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 0372ba0714..59bee20064 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 06684997a4..360c9a3006 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 f6b8aaa0ab..931fb6990e 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 015/123] 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 2387ff9d31..62b215f269 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 6ae9b11d74..c674569d43 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 0000000000..008e7d1103 --- /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 dd9fbf17f2..f4675888b2 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 0dd3a5cc12..eba73f8e05 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 016/123] [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 | 158 +++++++----------- ...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, 159 insertions(+), 126 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 cf10621140..b975799fd4 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 0000000000..8c7f1e3ced --- /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 fb17b64307..b17ad4c310 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 5e666e0cf5..9960670d1b 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 { 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 { useRecoilValue } from 'recoil'; -import { useIcons } from 'twenty-ui'; +import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle'; -import { NavigationDrawerItemsCollapsedContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer'; +import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection'; +import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; +import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; + 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 sortedStandardObjectMetadataItems = [...objectMetadataItems] + .filter((item) => ORDERED_STANDARD_OBJECTS.includes(item.nameSingular)) + .sort((objectMetadataItemA, objectMetadataItemB) => { + const indexA = ORDERED_STANDARD_OBJECTS.indexOf( + objectMetadataItemA.nameSingular, ); - 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) => ( - - ))} - + const indexB = ORDERED_STANDARD_OBJECTS.indexOf( + objectMetadataItemB.nameSingular, ); + if (indexA === -1 || indexB === -1) { + return objectMetadataItemA.nameSingular.localeCompare( + objectMetadataItemB.nameSingular, + ); + } + return indexA - indexB; }); - }; + + 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 2127db1fc6..91a22ca5ab 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 8fbd853a56..33e9ef1527 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 19575ff0d4..3a4d42541d 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 d98fc54709..65944df37e 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 2ba9850332..afc7fe8037 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 b6f7d2103a..1d83d1ddd9 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 017/123] 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 7eb55d60ae..2330ec4728 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 018/123] 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 5950c81d5c..7f053fc6b1 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 7a7de0807f..ae13d831fb 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 9eccbeb98e..ed06d3f0ee 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 e702acefa4..9a060b0d7b 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 0000000000..50c0f5c89c --- /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 0000000000..a1c7f2af3b --- /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 2b641f302a..b267622bf0 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 6433495789..9445068a5f 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 0000000000..4caee3aedf --- /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 b503ef826e..e38b018df4 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 01bad17167..d6d914d836 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 0000000000..c96d9f2f88 --- /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 f32bdbb933..09293fbb8e 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 f6aebb4377..7519d0cb40 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 0000000000..40925c5d38 --- /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 eb2e359fff..9626c6712e 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; `; -export const SettingsDeveloppersWebhookUsageGraph = () => { +const StyledTitleContainer = styled.div` + align-items: flex-start; + display: flex; + justify-content: space-between; +`; + +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 3615066a43..f6ba1aaf4a 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 019/123] 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 15d371f9f4..202b58b963 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 9960670d1b..f90a160b57 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 1d2d1ecd74..0000000000 --- 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 175f84554f..7c1f901625 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 be1cba3816..f0cbf686df 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 263b70decf..6b2409af45 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 06ba43f92e..9d69733852 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 1cb7c4b3ae..d634890a5f 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 cfd5702811..378dfff97f 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 219b185c45..69bc97777b 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 7e17552187..b50047e62e 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 020/123] [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 2a90843f1d..0ef8bab573 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 021/123] 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 a4dc90df92..f86d4c1b9d 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 283aba5a96..b66ea2ee45 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 022/123] 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 fd3327c63f..e19d3cb709 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 48795fd425..3bf0cd9bb9 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 023/123] 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 595081c0a9..506a6dce0e 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 024/123] 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 1d75cbd7ad..6b8ff8fcb7 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; + + 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.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, - }, - ); + 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 90291ad652..d9a1850a5d 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 025/123] 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 2889dfc77c..4e8ec1c647 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 0000000000..22f77e501d --- /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 9b1e10601a..8e911edac1 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 0000000000..05b76a1e93 --- /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 0000000000..0188f48f69 --- /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 0000000000..15eeb056ba --- /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 0000000000..1d029f4877 --- /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 449963c013..017f38fd92 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 c66767a090..8375cc0825 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 026/123] 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 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 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 027/123] 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 8484a29e7e..9d8c6b39b3 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 028/123] 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 430968f4f2..fe368ae9ba 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 017f38fd92..d8bf449d6a 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 029/123] 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 f43d3ed7bc..0b520154c4 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 030/123] 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 320ffa9015..b55bc24fc8 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 031/123] 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 041c73ef08..feacc0b857 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 032/123] 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 07d2e067c7..21840593b1 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 033/123] 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 c5bb33e003..0d87dfc1cd 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 35b6e75017..0030428ee9 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 f86d4c1b9d..a83f2180fa 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 df4daf7633..7907241789 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 0000000000..4dab3b2cda --- /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 8ed7f398db..c3e8ab4148 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 3ed94b22f2..245fde1fac 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 680ac551f6..d1d308dcd4 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 d6ea82eac1..2584f3c7b8 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 34a161bab7..760a778b57 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 360eccbbcc..83f06303b7 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 b2732df865..eb4aa38eda 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 d98c7bcf81..eb152294cc 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 2a76f0f7fa..58de733417 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 e454e67452..c43d197e43 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 7d9f221fbc..3285fb475d 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 c6842395f6..21ac7228d3 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 8a3eef7ea3..a46df19b7f 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 d5728f23ef..7d8f8ab9c3 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 89dc76d19b..b5a8f351da 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 589905550c..e999e676b9 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 84cd705379..7f1877f112 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 e054014aa2..e3295d57e9 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 4c1c6e2499..76e91ec8bf 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 04aa231d4c..1a82485437 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 265780072a..a7168d35ec 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 ef210d328a..2085284d13 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 bf82cc2a42..86b053536d 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 e046316132..e2cd918890 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 fe368ae9ba..590f5657c6 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 bbda464681..a938ae8aa1 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 d04d8281c7..7c16632eea 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 2d1989cc68..158b628bee 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 fb65053c15..96c00233e6 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 1c1f34e43a..24322ac7c5 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 c1ffd1094e..a4172b8f4a 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 b0c3fd7ca5..8e8f4b3aaa 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 ecb7bc90d5..6e4b49b8fe 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 e5542cd354..441d6598a8 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 94726465f2..f1a50f9128 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 3b4cf60396..3a5b36ee6f 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 053e9217bb..448879073a 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 30e6343bd7..cc2b7102e0 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 fe0b549d68..d4a12a4656 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 899c0414e7..7114bd4276 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 8351399451..00bd68e93a 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 3e8e08cd06..40d27298fd 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 1dc1144100..a685d15055 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 85ac636ea0..7b3d817c64 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 5613db9d48..96413c89cd 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 455ceca01c..781e85d8b6 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 e97b27fa94..4e141de6b3 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 fa0779f538..cd5ce8a733 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 65f69f6d29..6eca2c821f 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 39773acb8d..47d8dc2ae8 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 0d87baac99..0000000000 --- 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 b57f427cf1..174c5c3bad 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 d8985e6763..9e474de5ad 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 1958a09eb5..7ade7cb906 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 3961f28c83..f464b8710f 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 fb59e54018..9bbc69af3f 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 e9487ea6eb..01ca2843c3 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 1359c2d597..483c323f07 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 6451634ef5..6569e987c3 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 760a70b987..aec21e44b0 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 85afbd9074..ff380aff3e 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 be1a3cefa2..2cda4b62cb 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 3b6f3300b5..764dca8fa4 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 4c6c0d19a1..c215535797 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 79d32f974a..e0b8e85bd5 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 5317c6ec36..dddbfbfc84 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 97f8f7e206..3a4cd095e1 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 09c405061f..6bb6f02d2b 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 ef9bdced6a..ec17c5ff92 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 ff01993449..345421f7ce 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 f167ad13f1..60ab0fed8a 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 8e911edac1..01e5f42d65 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 9cd066b84e..a30216912c 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 b51a7f63f4..a11aa2eb78 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 a7e47f8ba7..0eacdbb3d5 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 260a223f17..21cbdb3f9e 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 e5be438584..d90dfff548 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 0000000000..98137b1df4 --- /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 fa56378afd..1ee8e850a2 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 f76ba66379..dd38928fcf 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 812a5ca67b..1e9403a3aa 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 360c9a3006..c052e01ffb 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 e5bd42d0b4..42f4fabd50 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 85c473b2fe..593503fd2b 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 27e1a97823..1de7ea88ad 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 6161fbcdf3..22ae24fdb0 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 931fb6990e..270ef7a28d 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 87ce82a443..f2db34f27e 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 eec82eb8e3..4b2782b69d 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 0d1a9fc126..3b47bad471 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 bd2a1db5bf..fa8d566e0c 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 c993984b05..68731a78ce 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 2b17a65554..9b36b1541a 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 47cb4e54d0..ac1a4da5ac 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 a87f0ce422..ad33171faa 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 e48542f1a0..57b5162793 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 7f2b295fb5..0757b7e619 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 9126371d1d..58894c817c 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 1240a6d602..723b04a9f6 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 e7a330c81f..ebca2067fc 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 0a9328577d..d970f3c825 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 d13926719f..3a066f722d 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 bf65953f63..832fc41f7c 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 f2a940de52..63af03fb0d 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 2eeb986217..02076198da 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 6f44e0ebc5..62a6443cea 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 a6b2cffc0e..810be399c9 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 071d8a336b..813481958f 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 d8bf449d6a..3365a170e2 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 afb281b9a7..05a638e717 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 abf1b33900..02d447a7fd 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 186f54b691..59642d58a3 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 c7ab13c549..758a83b733 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 15327a3035..fce290183f 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 1b09bc9134..d34e4b7f1b 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 34afbb46ad..c5c4e06450 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 bf3acc27ae..4270c5718b 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 44eb6f203b..dfefa674cd 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 0ecb2cb569..6c83840037 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 a7577114c0..eb64af7229 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 7f91f30010..9808158ec8 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 3429ac9893..9e20f9c955 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 5a8c90a81a..c1f3f4aa51 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 0ff3026459..9e28b16116 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 329c736f18..f61342a4c5 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 3ba11b7f22..033f103a9f 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 5ff43e8321..a827cd4a25 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 2dc86e72e1..9e75f3297d 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 4e5732ef81..4cc777a623 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 35e90d9b2c..d46f4b99c7 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 4c4dbc807c..36c6c7b9ab 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 136bad7702..6c368f0a0d 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 37fb2d9c0c..d973ab8f3c 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 3cbdfe3bdc..06cc4999fe 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 651243747a..a08d6658cf 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 5e8893813f..682cb7123d 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 cfc6835100..793279f388 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 215ae748bc..e807ea134b 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 7c7e280569..ba5fd0fd60 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 35ce036694..a200a84455 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 7a9651b689..8d45c760cb 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 92951f0a5b..93733b7cc6 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 006a4db6a8..1a2316a0a3 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 b473d3f28c..4d0e7bb47a 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 eb8ddd11f2..1b803f6355 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 92de8008e0..10c57a8bcb 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 e10f696446..017eeb5c27 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 76f1aefa24..2d2000d536 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 bfd5db517c..de44d7346c 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 85ca252abf..891d69d957 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 2934066fd1..b6237754fd 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 9cfd9b04ca..1d240653c9 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 3e43717544..cb52a0c020 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 c107d54663..e47d8bcaba 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 244772c809..7405490de3 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 971ab15973..a716ba032f 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 282f9d6dd2..a3a7af054b 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 9d8eabefb6..475ba43b0b 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 034/123] 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 9f1f55ae76..7bfb7d4938 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 035/123] 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 d67c49b641..f29319349c 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 036/123] 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 b55bc24fc8..eab5d57843 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 037/123] 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 feacc0b857..7aa0a3b4a0 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 038/123] 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 7aa0a3b4a0..eb631acce0 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 039/123] 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 a0e1421bfd..7121fb7da6 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 040/123] 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 08e541a21a..38cddead7c 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 d679096f80..18a5ecd4ac 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 2379fb2fc4..3b4ff61231 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 69f7c4a961..7039db1d60 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 041/123] 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 07761aef88..db247716ab 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 93ee5eaa54..e8e3ebe3fb 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 042/123] 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 0000000000..86325c0af7 --- /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 043/123] 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 455b5e35ba..2f8d1d1553 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 a4c4e6bee9..ca36278ff3 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 044/123] 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 04a80a1c82..7b75faec18 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 79cbe7c27f..e796c4a88c 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 0000000000..1320a96420 --- /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 045/123] 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 a78be94155..071104d31c 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 506a6dce0e..d0a8a4c360 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 074d63fdda..101e1df3b4 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 1496425c85..2ff08a9e17 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 fccfca98d8..6cbd99b288 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 d79345f3bf..ce39d66ca7 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 cffb502876..fc45955bc7 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 046/123] 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 | 167 ++++-------------- .../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, 335 insertions(+), 169 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 1d029f4877..a1d6a81d2e 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 0000000000..7641d8f46d --- /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 0000000000..4ad8afaee7 --- /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 5f88868713..c1131c1b66 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 b2d54cbc03..54a565215d 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 723b04a9f6..dc846b9c08 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), - ); - } - }); - - 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) && ( - value && onChange?.(value)} - onValidate={handleEditorValidation} - options={{ - ...options, - overviewRulerLanes: 0, - scrollbar: { - vertical: 'hidden', - horizontal: 'hidden', - }, - minimap: { - enabled: false, - }, - }} - /> - ) + { + monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme)); + monaco.editor.setTheme('codeEditorTheme'); + + onMount?.(editor, monaco); + }} + onChange={(value) => { + if (isDefined(value)) { + onChange?.(value); + } + }} + onValidate={onValidate} + options={{ + overviewRulerLanes: 0, + scrollbar: { + vertical: 'hidden', + horizontal: 'hidden', + }, + 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 3365a170e2..6f2c0aa442 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 0000000000..1a49c030ac --- /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 0000000000..8d8f265c42 --- /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 0000000000..9bb6fa5642 --- /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 65b2e9a25a..70e3ab1970 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 ffef2fdeda..5244849f69 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 047/123] 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 zgS&0#IiFI5t%aWQ3tb^Zbk`Hq3_K_d8faXdxF=#IXp^W=(Q0}B1E^SU?o`GkfHmKS zs=FV6RE+Lk4<^9_qa0dza(CFLBwvV~W4>nBBq={XMoQ@gM|s*^iUst?{v4tNHV+)H zpCF*EyOXboqSCShD`8F%r;G2) zd@Vi{>a&&P3?8s3bDNU02&gBIw6WVx5LZMgEmEnA-VlPuOrXr1a=$)kgz~wnKiZuE zP9lPW)W<-t1nA9CrLl9_rb>+ndNnLx?pQ5QIpqsz545wQ-2&SjKMU|l!yU6UdCWE2>%uPn@BmNqW(DV`4(m&U1z=U)`nUaFTY<~ z7>eJ>s{ZzR9Rx4KIHm(^5%V-;xYAT-g{U$-+vRk8jTt<*;w7Uk_BLyltHn;&AL0cF zu2f*{bFop?#>Jidgo9TGM`FAMB)6JM*o219lWsGxSBVRa3q@aRzwJW@FI2xDdPt^4 ztjRy!Sh*-YVB)Ca6BvGzZJ6$avr#8qL0k^I&C9MCtJ5Ay2T$^shI8+bS1Q_R=R1wR z0WGR3Z!}J8TJF?o`G>+b2OV`JuV38}-0hXFHX8#PJT@(z=0)gz>wiS9cfD}XX7nDy z6v~}xFJ6YkQ;UrTlw@D>Bb-$R37Lg>pNkvp<>|byC;9%u&evyfr`*hp!-kDpg7PfP zF+8^I{v^K3Dn3}k@CQkzv7^O~{jAM4vykI9jfZTU2n_{L6DK_eg3c!_0)Br4j!?17 zWYITkRuqqzo%K0w$2^s!i>$aF)@rvyj=K4KXoVBkOussu zkgSwfF0^LDUBSrT{inmqF=dq<`Rn)iY}iO}a`v}?t6Qug&$7MI?r-z+>Yx`pDPaT8zlgAF2|P}heoyndVsbY>w| z4B0mVLtv;JYCY_Chi9zJ0iiHi(lnR4E-;KdoJ||4M}>-um$1oiNNmwd)6fV4xHHOw z84Cb>mZ9B>6+6VV>@F?~1&rl7$JWUMnajJupKaN~z-P;nA&m&u?iQNPD_emm zNClmGIn-VHYJ1wulM zie!uDMwaC-FkMTIr-%LsIcT1v(~<`ADIb@G@sjlkbdhOUkCO6AtyGZ;Ur~R}MRD^9 zn2hEE*1k&yRM>8=yfKjA@K?0(9o2bkMH(8#9I2d1ac_jDC()@%MLe2lmp8b$YGa$|FiRxH&%)AvY6CLCj|59+|*Z)d-@VlwbG9r>F#7hgtvq}5pvNP4x zusLBrBfxRLmKQg7x53CxRa3+0I8Z;<;cC-}Y;y%01gW2r7-z;4%+rMp{F3L)6VpG? zZ2@IertULGqqJWac@Yh}&}6FZcd9eBxVnnAvtvh-`In;?+4XhSxDx;s!N;D_ z{rBJE+P+@Mo?eek{yA~goOIsZBhXydo%q2} zS)rEs;2$se1^aNPyA`O8eK`8@S?y`u}+{wleHltGX8 zdmD2D8?|f2@s~t|Q~wL7=fSzdoPWA5NkEunGd!C7>ASha?YPOyMT>u|I zb&=eQNLZHVg|RzH`Q-suL3`@fe_!V$SH)rD%^D5unwOt%E2K8mma<9)&*uPJ!}c?^ zqkbcWfAe7d5$*4xcO~21{0N1UuEeFpAuNTJ&;@+`|pH&bp{?2dt|RF zM6%{L2er?AxDI&)w1uYOM#<&UuTryo*h>v5b`RJ>JmR-#v;u6^=TTL9UzmtVGF9c^axk1@?Xqso8Bb^Qy*&rTN}Y17F-hwPFqpoB}PtCO%< zcy|0?PNgSRG5Z(T5{Dv2eswzWOf*g(*I8-b?gu$t(=(4? z{u3^J>4m+?wGqk#jCKMSq-gWS76 z@~K!KkeEG^(fPr^p5ofa7IQ6N^u4Whg9E89oLH?szkS_7pOoT64`O!?XeRQ6C(p%w%0c|Bkc0?f3j2hv zL$ZPh9rpxfu$kF?6kOI9R9Y@G>QkF<>by*{IvTlyLcisCQy}7`Yg_T!o_r9oRi@aw zlep^-#7p!jkFD)#NAW#eF^>6Ip=gv>&C56KynNv}}a=KJ~_ z#t_kx!ncKazRv&QaNjskkgMGP)uasl$Ddc3jPBev{j+{5BcT@awZ%ODdA;a&1CmS= zk>QipnD&q(nU07n{m!~NOC#O+W(e~=HFC!pJ!cURmsD?8U(>2Nmd&cyN1(-Kh7I z_brEEpaoQSK8RWLPGR>4QweY_=_v!_hw+QHez#j#EPVxK0_LOkYy*mKBv7F!UAm+5 zkDQj;EF#?E2Njkr!6Bssa?&%jcmdOxqW=bUhS2(_h=vl14Cz1-`wI*q4UGV`zSEQ4 zQN3H4;&nmTJVk{uH%)?t+6HYBGs2xZF$!GBpkSp*AzdI(;}Ls8(w*#;*oUCiP#IGo zLywxqyuNQTYn8(?K7Kd;fp3f#ZjlN}$N!wfeB?BFUrAEbemS}qqgc57(H%Y|GYK?d zC}lio1$Q_4w1;k^I;MA~1MMeeo_h=jPvRp~m2Dz^RoSD`u#1SAKHjL#T{44owma6Q zGxZ3WF(w|@5o5XAw#}xP1Cp;_-hJelnf0{plACdLY@Azzqf~9JzRh>osDi9}pj+^? zR}66u7@H}8!Z~`@H&x?g%z$y_Q6W?(mS{=hI$8LK!XPZ z2dk@csBI4gh@=V&RA|i(v4F8%TGa-u*GGv2qNXVsY2xH1ika2BMSn(k_kFPiaPLlx zBZaK8YYXx&ZSU!0Y5hvUdh?dD1~~JMk$t}nYfx2=O;7|VT)LMA!!FCu?UVP>OLU5@ zIv(lii^#EA`i`#U(as)?yiB^d zaD=SB3lFmd zMB*J%3F^$_1KJFDXRf8PEY>q3ZkI`G^19mj5jxrXr9a+P^N)Y@eG1L7 zBDCnX6^5uzSOvPD$awPlGO(7YD}*%3|B8{X)Z z7Tf3`k8kz2UT#i*{cQ!S=+lBdxbwnyA0u>#wcp5#$I7R`^!1|pSZ80f<=&xxZN@oU zkNIXD$e%7zy|J{@&J-uoD$y!cmghh=%d5Y43Pj4cxoJsfl8~W6r4f1*0LarJkRuYf z;Qgzn$)Ngl!_}!*Ow#$VKZ0ADp9y-%5dbnSl*Dp2-2v{Y8%z2WjQ}VPGh<1)`}A&n zv9&>^D$IfpesOVH(AFnk;5VjkydMe1R;9rmwI@8%!4L3_Kcd|d7P$L@-I9Ah4`GFv zHAp^-#!h2(7@6-ioR03iO)8PhDODYGmro3|y7nLy$}m>_tH=ruBD-*h>$f<14ahw* zO_u@>(m&PrttKJ6#T=J!UQ45ERZHYdPFy>Nd>6K`Ur*R#9+hdEX{%vWWx5UD*yZIv zJCyY*t_Zb=fYNVCM$U2`BeheH4EBu#iI4l$wtim?h25E}&18!RUP5#XHd97z*zGwW zHWcV1dY)?$us1HJ_H6!N<^@ooOBDSc7IJSyE&Wf;%k11i^w?M@WKb!U`B@JGxd7-* z>-p^V>FlQI#^SYoGm_Lkd$({6>ap`@HmwmDN%)2hHh@(|K}yy}XaI+jnAZ><0EKDzrcc;sIR^ zPEHEEep9_&JYhRivFBUvzDG->M{-XMWl8lhV|e}`4K0G5tZ<>3UE z3R1oE94|`%>lXBVWBRFlcZa+%l#TM!wcyaw?K^%T!t5H^<^dT<(>$|}e1W`w)}Y;h zyy8JQn7`Y^HF4L{ir3N#gsqD|SCl4$Ek+-V;|6Q&W3{R!8(yb;H!t-t zEj}{YL-hW{NUpNp!I<~~NS2ONs6_eJ_yW17yP($fJ9&mW{{Zb`7H#X~^N!w{2Zn3& z?ls2^^nbO)cB655MFur)?v!poXCbx@U`tF3u>$C$$_%*=4NFIa`Zm?$gtIRTz!lOx z59QMtX8vLx zjUjNpDn`2=d%E)SX+0=f8hW#4VE!<#?XWe^nQ$3(JyAm8EUaTc`+DQAQ=z}gB$%&i z%{b@H@i%MKL__z%7&)>dCwO3Tg4L)Rn;DTL<*s0OYE1e-m}u>lm_yCjQqH^sh)@Z z_!7>m-`)y{MI1@`-Oe4nSA4Fq+N)sG_Dpki8Bdb+EMysW-gL<@RCpz(1;geUVk06t#AyI1Ks?2~>;h;b=Vw@n_ z{j*j(k4IVILqZl(aOlA@Nha&nWQO3M;}4HJScipnPWJL!IQx%)JIDWCORWC~7~dD^ zJe-fm9&u2YL-x*HEh26|ZMxQZ5v6qcnE9VlG|Yss0^g`YWpVNXGI?E46%uYP%B-^( z*D2!P(XI9}?rNs$1kKgVL`6=j^u$&LeCrFx17Gn~Fsx`f5wviR`&e6t?aq?s*Dbs5 zcM>1g^d@S5{09`Udr)oGKx~w|EtTakIGJD*+{(Lqv#~_N18lC1PWCz$Q?9sbZ|4Hd zVTSp zX<2>><}^F^ggQU3qkh< z^ob__#~Rl%pQio13$*Ije7s#XI8Su=fKhD5$EC`AZN~9qGj*9qo~q(-$f1*u*s2oFl-hI zy@oS(Gr8#%)(ex9S*&28IWs>D4+LBq?!enKT4g59%DM@6p|7bQ)z5H2GLUn|_7H~s!(a@f9p zA!RvP0t$~upH44GwPp9imbKtvp33Dw8H&I|#u}Nn3Ozr{reNal-sn?N83J$ZF!!9Q zhSf_a)7SMo+BdqdtE$K_Fhjfz2$(58Qdl&OEoTnF(QJCK^-H-a)^oHYk?Q(~1HwE_ zbc^o)n@S($rX;hCv!mE|3dvESt5Zk^>PN zGWq~0undc+?Zl;k$N;_eYoXH%L7)zG==t1XuOhubsQQwP^d$kH!?zuBeuCKt;%mDO zD7f%9h>U94sz2R62fOXsvwh~V+17xVq#U^KAJ?&~X$!>ke_z_4Dg+=8ANIQ1OG2z1 zmT1@{Eq=6?uND4><5_w4hE()b%a-b`Ak|>j3Tr23+L+$J%)=M*;+m6U9^dfrFjpWz zZ}!(KdVM~$I<52$`H|**NB!3omv}{aqq>>w0&$)7)Vw(f+`e(Ov&!V2Y78am?sY+s z#q+z)PCKehW=*~KrFq5)ztk1>2`^jmxw^DQOPpLSdih6-WDUh|R+>s0+CeG2Ejmwi zm{E_L|E1yIN#SFVNGgio#YKxsU=#1N-tC};5mXR>uCy-h5vl_^cXHmy9nS@pw<&gd zE%Zx;rgTVPl^Ci-g3%TNy&z?vN8Z6`?g7b%%2|QiE%m487n0H>Abl%EkCNcNUaH10 zUc_Czv(&D}=t#3&`PGJN`-?zi1}~8HXv1}b%7(VSe<9~^nu3OPa0+90qwhS7tvCD} zA97~ob74f9P+U!o?e>ijsm`|P+?3LYsz-MN<9W?|vfDpJ(`Gn!jmm6EV7Z6hTFqNA z(W2Cs|L4xf%;-Zfr)w@E+O%zN~Ksy6jnE#yXYteoGrMk1zsyO^N{g zt-ZyB44%hcZd{w-+sVv@cmEOHgv?pWL z=>%BW>ik!g=cWK?F1d?k3?%rlGk{%Nwt^0_AFczIu;1_N6^dj}|IYr#|Mxq$idHqK z|1V9k-2jNdV62Mz9f1H{YmZyT{Gn2#!~JRo;A_mMB)jK&Ybn|cIhh@Q{Hyp)q%56Cn^#-1gt49v>`9GFVJI(_cwF`kbJqDE{Or>?oN;3)L8#VC3kJj?AAr#UEeiDk$_}yi2yf4wOnL zlDR(asAeJa{WnONMGY->>|*DlQ4QbO(VdYV6CWzsE?a@MUyJ6Y$6qEDlAVsXjg3s+ z2*1(jITbh@CGz)K;^ZHvn%!_R*s*VQbdtfPoLIxycR)g*-2=nY*iIt{`)cbgo#{Sv zDJEmMc}Uwe|2!biF6ShL$45xY=a-%AW8lnh`q@aUW56n2_Y;fmWKVNicc$3SUyuKD z>P$X&)rojhuTHIyWG3pusu#^Cehu`jxg)Q51@5+W%xvi}_=IwLEL5vUIIyk6;ZeUN^ zX#}e6LHxZ@_+a(DI){{R9r?0GjJ%&s8>Ec8lD(AuHb6x?+J0{IVOPGEu!iLK3-@4y zW7v+bP9G?pI5gKTuPoxMo;|~_PfdtXH)YUnb6z4Q^EX$9=6|unR02BP)}8c!Bcm@q zZie0+V|rKQHRR|({8ixiR>4($I@;Y^oLdrI|APG^h$EP(xSNqb5|zv>9Q-2<4M@=q zKkOG$(@5nxXFq6mjoWBX{h=t4Sx>FaZ=dHj7EuQ)zL#Y2k~MdODqE6yn58N{N>Y6L zHW?DLS-qG0_rSJX)EoUxzNl8HaNaD7PHIf17p8lb^?JM{92#7Qz?DAJ1^U#5+6v`q z6mj2GuX4su)^x}J+W5&zy#prqoRgMJF!cQ$0j(T;`%5KLxJrg^Fy8Un+GHX zNBw1x?%{L^)#kwa4|D(6o-CF!$@9x#qoyLeekadu){#c(0s5)VVOhbJR_mEtzLw3w z{gaJ}Fc(7^_CIQ)*Fp{dF{JD9n_+JmOdUOle|riaw5Xh1kLa=?jfLoY+f&1dXE&vj z|IX+tTDVtHEiX=17V4A@lDTHA9ld{rZtjtwt`23JAv@5+lNgfi3cNZwnIF|NphDQl zMeT|&UBS5T&>JQ9F$>uJPpa4tDeM0XD7j zQ^R;eT*h1dQ!z{M=%rpO-k_8tPt%lQK=<*#`7KVK*@1%E<6)Ytn$Zf?da0bJ)sN-= zoY-$OMgDNoj8!uY_XvI2O6B?BxsfAue=+)(qA82C7V-eJvru;DWOeg6$%pkypIWwF z^eJT1r>{z`34L_@*(JX25f+xtR*(gP;7EMW>Rtt$=%7L}T@3j!i2& zsxkEM4~g{`#P9Vvw(IiWLv~y*dD%y(`d0l1y*hfdBU{I@p9wCcWNz?h^6yVtzZCvv zAy04%&L~uao^GyagKg*bwt!9|4lY?)_Ai`(KiXwn`WP6L@5iF5F7R^1`SajCzfx=t z&e;#thH+|BVbUn{CYV7v|2}$1d3Jn$B1|onxg{)8P-L&H1brrB^w!JiSVCZ`(PA6a z&jdVQ^T%tQo~jpCNYMpV{X)c{cbx=YJ+`;a3w6um;g(%-4)<3-uWcP!3Y>S`2+x7- zT`1vl*yAQ!y4!P^99mx;SPSpDK*U`Sz0asf0Ym^LO6Nb`$O?xf%bumj;Z$R5>A_IO zTW5tz5BxtJy8rZXq0X&kA3g=~t7L}S7<&^;mnY7 z5C#d9W$g4|_V<8m@H4wlF$+z9M&onh6tOaIi|3-KX@r$%5!Q))$8vbee&MU3jYFh% zL(QaZgeAvtihRCcr{pA{%+hq4p34mE)BpE7#Oo_y#+Zi<_t6tqw_o3$Ue0fcQFuNq z`xV8`(J#P|`#pd^Qp&9wF67LfnUUpBHSGjq&3V)!Hww5F%?wn?iUQH#gTl@CLf+=H zDFxoio)CuvljlbLc$mZ`@QJrq`1a=RA$!xAK5{JVSzj#w{`+nf z5R%329lVsLkEr0H&r}(5ede$J z2Ga-^s_%dQ{1BHVx_nBz%@AgTWkG`#w5W}Eb6C!~d53rRbsHQ$_KNoQ{?n)adys}- zo6?9WX0!I~>zQ7WZT@+bXGzi0Pp9A1$oE^Q`qE+KwoaoW&MTm!f9s3Z%ITV`fkI}% zk93-Ba@aI1*W_5h>22$`6q^Nmlo!t|SGNb|EA;Q%w__~!ClJWuIP8Mf#O!mNjzLh2 zqD=7b#vtfdTf>W`Kmu;zwi935j9l(!oulM4BIf9`waGM zZ=pO|h4)9zg*;EwATd$ZuG}YdNNhA}?C~s`tO{+MqtrIy(IqltRa*O-D&p_9pyN~q zzFh#T+(nN}$r6M%uTzT|Pcm1%d%>3HXq*@ZTpjEwy;vbb3Rxc+V!J)Iuk|fPF%&sh z(xn}s*9*PGvo_Sk&~awci95|gx@IGcNLC4|KEL^%$O0qNPjZfIkq3usu58V`J2L`~E?wg;Zu`(sfQucg^nX`1pP+pqup9a4D& zxd$X`CwJ0hN(TLXsvYld{7@E0RrNkM!)#g&ScYPh*3NwPYqEoM@VgCeV>6HDz5#u1 zMj(oszl#P;*h=34Ag4lwhsQ<7g|E#m1uF6>BnJ*F?G1-LsH57aDFJsKG4of+MtBXI zgoM>fizEf)3zf{<=2~29}TBt%E@Nes-{;Y=pk*3|}xPHf5^LEvfjw!o+dHn4XQq9GP z2J!!1;%qTB^D>?8Xtf(+Z$?!plRrQIXVLA%t6iAu;3-`aF0LXeh0!5ErtoJ0b(uiX zPvHV&C8mzGw7jdAhYQ>Slq}QB+vKC{BA)yt>VC7!I#|*Ur^7xRyu+5D{r^UZ zuH5a{`4P=|J}vDwXe`0dGF$3v+EDQ_=B<1E5N!2TSEx>*?lsqFuqNCizOSoL=u@?+ z-C_Uo$JZwCIUq5H%PvKvP>*@M5z$JY=u)F~`6}>*8sNpkU5~@ z5OmaMj+R6@f%w`B4-PGc3D5~Niq8>%ZaeCyi!MlBCQ8}MGi%R6kaTFr(VoABRXeMG zN207TsC4?caP_y@aFc`9)IWg7WN(KEXxTuLGJ1`wh3**}R`WXXL-AcmH8=-FiaZ;^ z^;eX}-g*2K_%h#FWNg0~(Ck9(91nd}d`NxDpyW64z4Y$|6&!B}p`1I4Cyr|gI}`sza0s?UZcghNKpCX-K1|68}kuTS}S2}f)5-zmgB)mkOJ zX3V5&(sTHZT4U7k_k;XC`m2#rO5Jbc_(tIjuBfNwXpe~F8>@P9R#P%>tGrW+*9Rxs z75tM}h5r1`C66@noEX0C);qU!>V4)m;7~g&u>sni7W=zNhzq2oYri$`s-RL4oLv2$ zQ@7KHxm#$~J5&C+y5tZ`K!ArotpJ)3+e4{G@8x!8LL4#A9G?vjkV|@-^7ys&_a1nm zg5B^JIyu+mpD5-9d6VZ3@gxF8N4{&`^_xxe$o0o7|2&=sG@}2TX3~;B)arHZG=-8p z{S~x#o^jehLGgCz-2mQgJy_{jq&c8J;EKPWvR!CeR%t|6EvKK752m}`JS~IL^@T_wlP3^f6gk4 zuj)tl_C6Or?!Ai@M(+%zNw4ZJz3N#%6xjFLP~YoH9yH?!{_kkSN~d5`cGdP?Xg;0W zAhAXk<&e)3$=@MH^ZDcIPMNl5Zvgmgz@rQEk9Cx(g%l<9><}9f$-gA^P~OngK10s}uMWDdSebaCT4aL2Dl({A@6zS7!noAUD2d|3M4q<<)h*ee=5cNX&x zpG=>o5z49j4`*Qd=+1%~@i>bEkfMg!DAdGVtMkx-tqIHC*IgY61iAn~O>i;n8M{Cq zDHXR=l?W)A%7iVZX3=|;ohMGnf-U|E&Vp1}4tJ}3ZW~jdig_s5m%jU^GBLA7JDOJB zal65zx@8KJzj~UdXq-&@!gKJ0sDpORV6BJqayUkQQ7N;Q3mhyL$RQ-aX%QJ01 z3VE};H7uBJA)j;4?{|>BVyF=qIGjRTuJXdoV~@>ZdwR8B-!5P7>*_o^SSTm92yyDc zbHXy(>r9(t;9gzH)_pJRj_%#zO_&+yJf0kkVS=&~RHQ4I_eKSDo^!T7VNEd0eeVoS ziD#h^9AyZ5Q*F{z`FCo>`U~tXiN3?1WAHvKnB4q&cgKwnsh6s5P`nMq}xDUbYcmERO5rl=!vSfn7Gqrzo~L8Lz<`^|ci)JAaEt*31^1}g9HS0PCsfU1Jpv*`;y z3OA0Nni^+4%5xvgc)!)NsXvd?jQQnM=*MB~o6pqB(|RwPqTM07y_;c<@;tLEjIN_r z_=+2&y6Y8D2|#CnD=P4ni2-XPol~OP$PO1BHFbT14}IkD4k>_)qIE0>2ts^#1=@b$ zTF&wPetVb-5S(3DYcD>f_7LD)$5;gI^pqXi0wEVQEVC!~v$*@1$!px3gjsie6k{Y8 zOn)PAvPmr+cQ;TmjsIjWiyJZUn=mAxKwLl5%ZqQBM5A0-6_JWmm}h26tEW{Hwr@qc z>#A^OR>in;pI?lk5v^4UM}hZiH*4MZXSRcq^B(oRBG82;pi{Q-YJ2oyJ=V0w4Ax)* zv<;u$tEMM1j(URM$2B?Fur9`U4a-l+LCz0R**dE}`6m`Wv;M2{fCs@+Fq(8uWB&`F z3T`|D-e~xBAsN(+X6*8Zp6-od&_Iq`NgbSY;bdp_ptb(g zjjS(wcJCALVOOri@~~F`65rMv(V{~`J*KBXOA{O89*BX`zJ zNyKl1O`04)t~Q*a$2$dxtQzLe><-cD2T^nyQ?zzs=~IkyDRRB}sShBR`P1UPdSS8z zys2%T&}`ZtRrRLn#qBE_@}0&nk*^mo&6J<0&v})@>Q|`uwC*!sm&y7dMPXUz9lYI@ zR6gKX)`Z3;j#$fK-SXogmYwmI4I5YNptE<#?m@1&S=pi)hJ>@y00&|3MNo>2cgV7s zb!KDONkI&+E8Pyk-@>N!VE7w@IYY+UIQ)K(AYcEP_Sz&0!!Zc?3A&5!_aM zUFL!9r-~{-{qFdHItkU+ctU2T?+WwQM3%D_Rs`zkYlM&3jwKlwn`GdGP1T7-{m>6yjo2KF3uQ(O7?uQ^UAy zJB)@fX$VB9hRb05$Lp`&QdR$ih%oyAAxo{5J@$5FOL;!p1=M{PC_KZqWD)6}*8`c!v$BK4CoHp73AjrV;z-H8O6N-LR!E%6oYOEzERC zFns1VqGCi;1AL4(ZGev8T{@pDWF@`B4xA@B1iGKrpy2U=fcdm5mXX9QRjtBI#z8Qp z!5%r~@r7f!3|=c-v_cD@j1%aUL3kxr?C}b}=tF@G^)B5Oz@rlDXbpNp< zXH7hSEhBd;bSz#CBwxjmq^K{nfDV0JkWLHT$(uwdGxc(6(z6-IlMvC#PeXN1Av1O8 z0)PeL{*PH&)h6q%OKz!z>!UgVr_K9twA$=?P}Q-dd^|)vsPd1?VA_>Ij8_+(qMV%= z6EvC%aUH-%Nhh0{ijiO$ygA-^%kVokN8KQ;D+9jnXwQY%-I)H`4741%J7Bz~1LwY{ zb^n0!`pNmH?B}k}+fE4t*~rWgyvNyHvCs?Ji91)y2{uYH$QBQ5I5qw#<=N4+F4FxKimMMvt6e??4OWt3z$!UX z`nz;!4&0k`hFrwWOO>7Q*AaZV~uj zBBcI)ZguC=_P|8M>)v#^=xF<@{%Hj)w$l#a)#r`NX$1LxX<*Z5gw9|%*zg9jy%nkFJ2~GJuB#Wk9|N8A|F?qk|aA-2ZMnR%G{X4C#!3w?GOt(xe z&HmHu`}t*hE%K(S4G^zt_>Zxl@ zUAA2vn6`;|!$7_EE4paJ4{?U*F1!UBbM_+{2j2C{Yvzb zjFH`0zSTCh$#0~^WZnZe{s6iZL^npK$N^%g#AmWIyFF-XAKUA(Q&7Drp)ibOS>HOdug{A!4z()nzuAqlz^>QZ;>QDB8JWi ztbxml#G|2Y@+%}Nd=C2>r8eZ*9*S0ARz6MNs3T2Gm9-n!&nVam{C?oi604uSUVw!E z`R^iU)#=D%e$6V|VF)>`$yk6si)N_;0(l86AVkIs1SpduX>IQbNVW2FUly@nt;@ub z*m7cRVDIa_N!AxG+4fH&ZpMnk7Zf!nK7hE{BHtCek4u!6oFc-i!IFioi}pRz>XWhB zoJ5lvfOIHN=5NinqP;1jp?ko|-;I2DGW3VelBjYT0U_E9R`<0h1;vTv`aWo;M~?ve zpS!TjGso^^;Rk66uo8X0S-~zF7(@SS0Hg8;9QA4Og(U_CW5)VtqLm*^Kl#$C)5M@% z*!Rs{525VmrhlcuNbdmebxDYt?@}~Ddq-qQJ7Gd4Wfh zB0;m5!H$M0n}M44N%7ovs_Ij|J)5Hsoo~RJBty>f7b1z33;Xz2J#7jVR$7Aie0SZ3 zw4J(5JA0qQHwEUKHW`ZcZE;lclmW~XQY+Yo`;rrv_=?~EnnH9;_s)vWD6l#n9?Bx` z+wwx{LN=%nPN?I4qu}ZzTnv<(z~$3_HNAGZOsy7u=7c;<*-yMG%BmUb(2@6I$1TB& zftN>Niw8I#m=Da~%*=0YNh#fK&D@wm$_e^qid-Y~pzA2je$ZOazCP88{TYR#?6L-AH*XHxzlr5f+p@u z5`HHJ9Omora#Ialt7-z8u{L|Uc0RJPAxnY~mrJIRKAa*HOp)|R+zHvjU7I14M*(67puPv(XjOhwhh^v34h!2+WO`KUE*$*c`kvB4 zz>IUP^m2K)4K5PjA|DxwcX%xl9qg!&yGP^)-siXU zh1s_drk`ODdc4bz0W2q~3G znK1m~21&!*v5G_3ZbnHydz*l;D10kE70o!+0m)tRWVvmZN-V-V$^WR)IU@Hr6;;?dg0gNmx&W1*fpde6a7pwa=06vZJ4b z6WEd3-{vw8ZTYTMkP2F|f%EtXutloixv6e4a2$jLgH>7v0)a9p*<zEW zbvaC(tA6V-Urju0&|nbAs-?r+xhejTMFUln2Y)|hYEO9D)|SvW_-&3sXgaORC+1|Y z+_E|A0#3tUg{xl1UPysDtxaui#9eiA@U%1gCbga995{Gh<;rFZLjw4*n6X`{4Et;^ zBu83`K2eH~Q{AnSiUU68yZlRPa;(6B*nU&GgKYEnG?g|5vl?Gz2A^Gq6HR3#jzfxl z@i3Z2pvmrAQA-_^eLd&|Fqm@N;G(53+w*a~7uTb#T$)VF2wXeu!je`8+8^G-mwxVIgNOdJ`w5P?;93eC1nB{`>r#g+TH1D z#??eY|ATFLoJlj*D=eMA_bEfKFg|IW^BKo^g2lt=e1HB>o19bPDipf=p^IiwabO`2 z|2@66)_;M1#s3Bw&`4wksFtY;lz98}(F7{~tX(EGL!dvlb+ zd;5#+w#kTta4 znWf|=o!(A}#$M&?dug!&Kf7!f4x2kAtJsa1(lXQ*scd#gt&}a_--8#c%k5BJCwkaj zPO8wYwAHW!HMKs;N(2Z*!eBpJb`%`c1q}=~^>#zrh*wLQX^p*GEi@`-0aS_qB)3Q+ z4*^<7a>8-Dib9imKJ68S%Wc2JCH2y!D5O7^AZE%RtVxldI*|dw7sJaGe#}s*x^`s- zd$C^j(wY<6G@ZOa#!+pJ`NRcc&<34u!?wm!=$hAR?#nv+kH4OMvqH}E>>8c*LtTRR zpM-q932f;>acc;7b*InsxXKcaaWZ14ie)(L^$BjHxRnc z07#fUuv##$W&uX+A%G?!8~#o!m}<1A3;N#8yu3(GwsWBLIEq5BF-jnulsEeDLX*FF z&ZPcH3xFMDNTUXRMox!Uk`|D6vRc)Z#X;T^i1gR=e+2XaH)^>s;{g>Gr3uQ}VNmjN zpHbL>L`B4H4b$s+@015~SX2zA-eCQV%_YcCmy5Wx*^otZ&$^N?2-CR^r4lr!-=p5i zGH~!mva^8(CagmcCmaVHaGq+Qr-Z*CMqy59kdKUYEUdkbZ;!UmPumnjPU=H2#1)S-NBo7k;I8$ZJnFL=#TmVI7;)MteIkY}qgw4z z{^hD$m56ul?`QFy18@&os2@&wqk`41e;eJ`>2?(OR6*CJH0#Dil;DT!)iS>?F|@ue zGy~6Ky9UfS&O#pV^g1ha*A!ULl54tE8q=Iwrp{vk(LV>4ePKQmaC?B;U0*u`c6~=v zT2iS?|2@2;wD`)|_3jY^vU&X?h$ocrfH z&@E0O-ZE!Gsl#tVL-VkQEb4T-1)5;EH}8<6 zN5S_ibeU}7)Zu`CP8TYCyYiPB01J=(PRlwU!mYCy&1L{^bZ7QY0!hMO&d3fHcQp=$^U=|;LixF2_wBaL0@PSVORw3}|dyOUE8ey;P9W=nZT7CFJ}KG;gI1llT5|%Vh6%!Q%)`lj#<*oosZu z37DQK=NSwU?V@xJppog1o`pz?_8xB(58DF|U=Y;1?tRcu^zsK@zF0Fk%K%1TS}U2M z$8!HD0U`3h0M{YC^436)r zo>aV46e9|N;9v}N;{~rqb{=(%cc#95Yk74|plo~^%s@_^GMv@}*#p|XgnAT%@s0D{H844vZSC?l z``ye*<%C0EEYum<3~b8&{1s_7!7gH!b<^D_2;ablDx+@ zt6f(N48w5pQlO#86s7J%;`3!Q5j~ZOU}2lJ+@owzu{oi^?*K-he}%6R3`-#d@^1L_ z3JQXx6=1E^faTrg7aN&jY5V1^AG16CTcjn%M2qYS6j-`VmA+Fv;S_Xd7bwkQ>mAz+LU`Z2-E>YMTsh|K8CYj99IUPQ& z!u_sO{AO;a9&U^>Xz!20hD-dXPk=#0Zi~p&hRWYpw^4QuZ_jq~+<~{N-Be-2v%^Ad zA&~PU5{GS|3ILY`^qC=;Ig_wyD~4;zzT+ERPxhF zHjTc8WdOE_kAB@c7jfTC31R@@DuU(d0v~y0AAZrBlLiy{$FT{aVE)R_ykzf)_2R6% zo5W=0uhZrFW{IQ@YXj;61%5CNIv+$)|R@sXx+J)FElZ z%)0H{C++P~R-|DpXeP`r>y@XgxRF6veqTfsFtrAYr0=%5%;+Q}dEIU*L6_7?mrJd| zRp7*7;><%4q?O~0#oLVwXW?Dxt}I7D?ZnTw26qL=Nv{Kk2|L9c{@d@S$WQDBw>;i) zY4amXhK1ew1CS32udAgI)ev$2BUI#FdDAtogj0cg#V^zThb!8L{3orx z8*Xu_2>q_Ak=8~9SFs74j9)heg!-L=OczjwpRT*j*x2reqi=obMriqkPGc?{N@>YnFGqy*aiDii!b~z%;&7L=Hko{EYIAgK?7@JQerwJ_4J(Wr5L!)u&F1T$f47e7*AY>7+r;!Zn1~ck{aTy zM8GKnEm&-Fs1#DvkL0|2BOU-0^)#4S2t#Ek`smHxH5j>L2KSVAWh$J7*UF;W&ED^7 zUyF!>PHsr-0mGML8k+ypZ#32*w~CH5jgKcX$l-Ade>hH8<;5AF;nv_*9oYYhAKvf| z_^W8Wkt#@`(>5|LJ%=tHIk^(CDd4tB8ab{>lgw@}9$%!W+WPDqAKGJ7UA;LP`%s-e zfTu9gylZf1llT=;Mb7gu=y%PNPsI#Yy?;zl8F_4H^V0peds;OZ9wy{%XKHN|Pm3(+ zm2Qt|9~?8zsG%T&B%|vuM%EBC5H0y?lZ=P5y19JzPsieSkl~Rs#o|8$D04Pyu1t9n1z9dZsp|cOoo(G~uN%Kk^*N0Wm6JFVy2)G|p zqUI{Sk~r?x5+yF9MCTE3vS2{Hh!zV~!Cb%~#VY{<#3zX@GV?a>Q^TWn3t|o$G~DZ=nbU?FT}VcY&l6WMqotLOmI@xqYuZBhFrQ5FZO#; zmX{5}@B6C~dFnJwa0ou%Pd)j<-0#6{O9>g2Q&$sJAeskh@db;t|pPWs#nVys%t$jeU}p; z8Y0Z#@IvtU1RI4=$`}~aq)X1Gs(HvNSxXWpE3lmO z=9M*f^HHbPLk@IOq&>>xiq^1jGt>Fid$O282(=ky@Do{sqi-Fk;J zy^7f@H>m|nLeId`oKs*>h2K5=!IMz|OPaKc<|!6FbNnn%oojN&a~x@l6>Oe5d!K2p zvZ*=_oGy#L1kcF+g}Lm8BcuV`yRl70)|->S-$UqzwBLhDUZFI{B7#0<`MU>=u@+v7 zR`j7A>Hw2Me$KZT!5BjdCojh6t(K^|WGn=9@@}c7(Jizr(S6SQ4W_2Xlbx0k~32LZFni5r2xKo*a4hc-)U6Fs27X3vLzpZ3~MJ;e)T|F*>LgHB#Ya1ZEekoK52|pgw zc)E1K)pgaxg`y%D$;kd@m#9W1saf{k@8TY$pY=oRvM-VnIbQFR3_9_IM+dkAnO6su z-8kxVsBD=ilGGF?Q?J4cb%%Vuigk7PXCodkN-PqCPr~_46?Y!ueVl8O1!K$@;b6GMV;0AKs&a1Vd%ZD&DnP`mKahUTuJ3R`XTQpG#G{GCfyY| zTnX&uQR4NN(8oxA=DqJ1qUo4{h%xk$EX52*PLjGBznilnKFDf-C2C@d{L#i8<$?tg zgVXR_zQb348tWnHKYhYE=ENZ60|x@ne)-u&qFb(^zo|D;_pq!aDSPHEj5VqNN7~CB zveu)JQgQ)Xw)(hqBt8QEfXJmfO2{pvwTPUG9FyE!y!bj_tmn{=DpL8zXP$H9)eD-4 z9}A0rKBi+ar{#>Smk_@ge&$9u_I&csNp$h1vUQ+$)Y`bK^v?*^Tev=*eBcWdvC6Ez z-f($?{SOK--RqUg9lI0=HY@~#zeHL4JFIQ*j$a_=h_Ee3QG=jteElkxN6f?8D$D|{ zvrk#-Bpzbe&MO%KHi_cvB{z%W8^mtzAwOUb^&qCaL&NPy@ zerOX)mP>}o@}Xn&58AIqm+_ax6if6VdSxtJge@{rdEZ~gYV_QaICqfD-z|=I>o>rr_;o&w~nWVdgB^Q*Tn&qn!!gM@t>2AgG8E}$gc7AS_a zsoK)9niQ!$4#!=OU%!{Udd#03IY{7?64`6*C4;Zk*R!N?F~HZ&a>z|lWodKI-ObvK z+t>2vvfXQz>(b~|B@aUnLEfQp31b>eOu9i7iH$MCvnP+jSc{>Z?884;z#oo&XEGdd z1`y&xM@2y7Fhxm~p_pb8b*7kB1t{;x>L0R+up`4_9vI$paZQ~`VY}1+NW8&?m#*mX zpMk@Z+<~?x-+aaK>&Hkc4rtT}red$Kd{X}`_VLQ2sRe@8p1SB^hGqCmQ0s4)+xhd0 zNXMQ#)I9=3*b!{e7a;_>>*!w7%X3bcd8O7FBqdMhF<#B!mBhC5q|^20xm0>*x#Q1z8esTZZyQ}H$(7vhaId9%tffewC=*{mO zGz|f=+L$LvMG^(qKr8khPhRlZRUn$qw$yUufA14S62%0CCwMZb!}o864s<72>vcYf z)i>1FUPd=~R0Fu`(7Lb8qHg4ICOIJiWbGBwE+6s9N8%Nzs65x=%_w%Kh$NN4GM{jH zKj)Vp(N4W|TUgQ#s?Za>;Q&(|1?C_#-gePkRM=1&%Y<#JZhY^8EulFU!W+7L=~apy zrD1q|5it5&9ewz&!kC4gUkp{!5x*ot38ggIV z4_^4H``!1CAZyHQ>Yo7D=~kwN)OE!!>Rw(=gDi4`+=5gDV{5%MHtjbKp76=9FCjcy z#86wr*I+A+Nw-sh#A@f)9h!(}l<~MeMKQEE_2qOlX6=tzNsw$@?ebQ5YyUPsduZBC zkx8YCH?%y+SrTczHTi7z|6f0|p|hw&R+~xJox{1SIni16Gj~^gGP5*|bP&`^91XWh ze$!TWvU|#FfzGX6JdnO-$sk7#f3UN93s~8qp%`@Bo5qKF0h5WSZX~2Jl$AHnY7#j;+Q?*U5Sh7vTGT43p_n^9r%4K0GGE6yT@@TYfL$qc^)14&^PemM?JAn+4m zI%9=V40efJDGwijGL~Zu8hy|WlcEz1lb|_68#L9onv-M8AU#vIe`s{LRn}W$b~U{^ zN9%gK?d=t6l_tv9@{U$(S-wk(eoyq`xZs`e^5A4sJ3i-xd z>`CP^B67}qM?a)Z^KT2(>)39ZsK8$}mQj8#!?7goOUI(pdNFR%C_$+y=25LvGWg$< ziUMxIy%gM+atanL7ygQYGlYCQ$j?$kNqD9PM?H{<*NHJnD--l%1*034d^T`#7$%z; z&_UZL`SN3w_x!G*{atZnp6Uy5_yUC%r>xOy9~o+uciHQnV`d83#YiX~66`W#X8Gtl zYxj<=kBz~wgv6&9a3_u-n@8-OcwPTZKw#LyVg6O_S%Y5+OQLda2roz{nYS4hJ8Q|! zF)w1qaFR>jXDAhUXKBCGK+SM`WA|P^cQUscaC?>Hvni*ad-J2*#Z+j(vlL3Fwv~vd z`k$Z$d_dy+ab+D%b6VlPA5x-T9ACPS{xa9UrS#^{^U!9`-_=G6=u;>`eNOxK);}kX z;?(4mPQHk9BwI19l6$DyR$6m%SP5Eup;u!l0DKO4Zd~Kkoe) zdAYDYcPybdgAkXlE8iCUzmzg0{CN&yjjoX^2{G0M8^0d{4 z6OR5C3)E@i4)0P%Oe0XOCIb`xe{$+27a)P|gT~^XbU7=azDvoq9`kpsgJh!fiv3in z=rCi zyJ&fCX4R+`_T)I5Gyx?(QQ#X1e>zRH`7{;;A;{Mt~%$Euycg|-Z5h=f(m=n^axwct?>IQO&5u3 z`M!f!I5tZcrYXej*PZ#qWJy+1ufE0&(9Y1}13?wz=*3-2#}Y|}SY_Br)F;regHC&uBDqWd;~livcjtH$QPu&w(X#OG>wn8@>~B*;Z!xa@+GwW7sRy zIozfA_*!RDFO1c{aA=tC!wJqGlCCoaUrT53*^~Pl9R~D1nm=UrN=0%H{wRTr|_^~ms8bO#8bbnw@(bGTQ zHbcUgHA>r5!omc3$O?A&ok#T-uMg{jM#I1mx}0T?B{ZVbRI4kFH9QJ37I)?BWqoV@ zb8FcayAO+eNb%@2idK<^;tnKOV_93_Xtkt6JLbu)&D&DvgXy95Kd@*Tc}l{> zV3>DS|IK*4>T1K`1%yvjNTnD*((cCYstQNy1vR4y$pflZ-g6D`=Il_rjZebj?-#p9 zUruzP?h3|seE{;!g%JLGchEpReVhy^2ifm;Bf_Jw1nnr?7?aPVM*;a8$L*KE)jI%x zoA6nsTqHl&DfKH-?BS@ndz3mAz$cZNB+$FZdBswMCzPRUm`4JMV8B@H1HZo_ptSog zd1F+k_j6N?jJc`SD`JTKGCb>N0_)PZlC%t&X3X~H?l?~)^TxJ>-6U98&=+%ysx;05 z*Z)+6z{e0jk>#(QswXpg?G&U_0}dBH}T z67o3_X!#7&tu@~ZlcZsU!@jcwZjn|0jv@gk=J0eP@&Ehxd+F~Fh9Tj}EJ$vi-sUv) zj*E`8S^A<>GbNh|9cqc~Z;%G;sE;(sMJC>ou*B8hK4#L8cPPcg167p{Nqyerr(>bx zuf$88Y1yV&Wv|``WU`Ss3F4AErD~2AhqQS;ilCk_Ad9}5pg`apYPqo?DTH5@=KBI-N2IoHgBZJ3Jq(XhmbhJv`4Gu0|nJScW*LzP-K2(Wp?LPIPY`^!jT z#oNr{Jv<5&4)5}80fP7vA3KN4<4)jl-2GoHP`|}9I3?+aJS6+Rx#TOH@NLW3-sZ-xE@7=Wc?3z2^WYf9@OL)qTVk zckxgv%96U)#22qewj@E}p1?YQeI6O+Nla?=i;b70Qz@{B-Bsv|hg~Vb?M9`=+)cTS zfp)K6>rJIx+bYx?BovMKtu%gaN+l%ZW{)vX^@W7*p{Ft!YIfPZ2ID+71>eMv3(o&{ zzPk|#$Lsl$ zE612^wl0o1w^0zj=Rb|+TNmyQ+(k|mBqieDsLSOl!_{(#CXKRm`VhKI#^6~Lu-(l> z^aVphi=NTwt6$#XEZZYnM2)hzUEk=-tr839rDngVEnpeI3!+tP2H>;8ZMmR0mG9MI zZkq$>inM>?Jtg@GVpGYO8ljFK{)JPB7Db=&lR>wT=VMqQm94*htM1ab!(kC6ekQakmH2!i2o(CLAvXLd&i>0Prg3Y#y_s65@d6Ew<@HYHuYb_Otp}@pHVfO71oLqFY(v%vN%Q3&~Os}QSp-%`0ur<-ZF1$BEP2NqRjP&spZ=4lYMtNokojMXUs>50r}%-i!t*pZ2Fcq)nY zLjENZPX00Df~~_P(K(Ui!5uT2m(k-i&bG-r;r6`a$XdwdQtcJ^$jgBJ-x6fgP!>Nj z@s-&3^F4nPN;wom-Y5ZrzF5E8)nPevdSLovij8~Co0l(vyTh9_&Q^XdL$!6n&<+zt z$EM|N&-b15d;~LHt(r7jnZ&lFOh)^>JtA{%fx+{vdY3O^zb>s<&M_BsLj&z)+7ZRr zxei>Fujl(r4!yy3HEkm?(PS+{qhbxNNR7J#RAOuuxiSTsmu783 zB&*WPwP4h!TO+Q9mc2jI@TqE?t%M63)NUpW9oTLJp~X=o;$g>y6tfS%jWAWFO6ANq zsg#KO-)S%k1u!7=`4gO}{W#v-ZV6ad2q$_x6&xbOt%EwT*@2ArLPbz;=eH8=+}uHO zeYAIDv_h8I(2IUlBqayDbp_+A(5rY)Bu#bCyImt`{$fiSG~O~mBW1azldU5NZ8M!g z_WLVrDctQZglJDD!pNd-ugaJ6dqXl%h3=asLb z|K>l#o)Az-bXAsRI(RdfI%A4#j&DxZ*c%^dvC{b4j>KRy?b*NZDU4E}$ttGDODyi< zS;8}?Y=8HyJ$%G`yx-GtC4v=Sn1j04NNB{J`7DbWDvu}IEh8>e`Mh{I^Bo|)p0WXW zEYf=Vx9g7RJron1cjgfA1gC5II|&wddCNv0fX(p%t(ZywvfLky!5HDsr$r<AS6?V2u+lw&x=$=?2x?v*m}Jo{Lo#RKpcENTK#Qf zKVCA{RQv|}7$ORrcAYZNIR{Ff^zDHOd4r;d0AIcF0Ft)uR3_U>!P(}(sLJqorpD-h1)7xLKKIr~6ThIT#ChV2pTO__h9&$LeJndzMaP6gO z%frvD+)MP~ncUs{PaW__AD>4X(W3LeooZcH%ZhM(15!m2yo^80d2oa;EMBG!o$vCU zxBpNCw|h|;6-}CH@$sgrYW}|#raHXXF=IkPyuQS|{qq9N%-+J&8HbD9=r@@2&TH|? zWvck4ojqVR{xY_tZL@o5?St|T^@Bg3XHGTB3p=z?maQA`SP?dmMd>6k^WA>&aw}en|H4)K{1p3G(q2jzDUG;bROcAyYiRTD?v3}# z)}*)tkhXtqJ6|LqZ#q?$_Fz-P{^!LinVg|(1YP4@iOGFnw9<>U@a$D4ElR9tK7|%Z zmlM%v5wiw{WV&VaM9=IhIMEV@x-?lU`Yz1P;UbHhkUj{CiaO@T2yINdx5IYc)WdCF zXh3jBakcJS&Ptpw|2^|m#Uf$r@(XX6xgdNemWFA@!Qvq_i)&1?--3DUJCoNH6@?ke z`LA%;-jKM$pv7fQI(WpjU$*lccwy~@aa^Cwfe-y^8`JVX?|+LUiiJmM)G=A0(|dU- zG`2QdRBlzt4p!Bsy8nA)hw?N9FngwV9ra2W1>CIf3Ko9}bK_}9uOxuEEm zxp?Y#53Fg)z&qbs$3TYp3g&iCeoJa~*=$#{+ZLr|31X;hccvwk5yJ>+tO{hw2L0cY z^HW0CFQ#e zE#}Pg6zt_1rQS^Zj6;~%1gek5u6S=0oXfpT>L=+T zpH;>1$LG|>x=uq>BANc19nL(X=oJWxzmJU=6X;4v!%M4s2{~n3J~fZ}PI$}=x^wZ)CTVYbOoDDCCblPc{(CI|8Jf|aCXc@H>O!I=h3-zWPWE*{bGWpb2ix|YC8`iKEkm1 zDWinbpXhfx5OYu;? zLV`4Vmq;cb?E%J@56&&vlwvcP449LverI5l=Pl(HfpYpfzihP|$?&iDKHt?W=&=!i zTk1ngc&d_(+-yD@=%);4jmqX2QN0yBPC@?{GzS%gBg$j>ex5a@5fV+be;XXO>+ddl zotBah zdp;YU*!P7ki*6#x%lq&9@Y(!1XNqi&u@6iK(hH1g_B%v&g(1(YT(W)O(C45@qSvos z{LCNUO*HebF z8Cz3Ux02u7%3XP!+m2W(g4Y3wMD^1`k?grlr(bU}-sl)Y?quWTd+`5Wnh~9Olz)$) zFUEK-d}Ml6r0&BuV>*Kl<4BH%=Ol!w{(Utk0!ly4zj$~whaMc?wJ3%rvcZN<7TVH~ zsz?daJ*Ac=PfXq;sdROJ$ZZhcI(f$Tu7C zE`rboCTI=9Yt#!5 zzBiFzWH)M5MokXF-FAikmF&3Yor>92GY)$tQbH%itl@qF1Zpf&@xPfy>!m(es-yRw z6agFC+JjlAnd2F(*)E-1r-(&A-&0oj%{XMdFgv8zYxpygQ#N9u==st8<*6$SUz4Cs zRI6FgQ@@VUQT&cp(dCIkmHGCLBhlp)Gsre=mJcpw2`ihrCwFQ)x+#`KSPDr!D8uq| z|71DtWPHJ_d}qwNvi3v#jWtA7U+ECP(e;l)V*Q;aS<(N}^*eLnK)c^2Fo_YQ$v75> zThgAYfk2+@0K^nG2l;${v&Fr}4la42%k1kPCb@UC5jCFOV$-xKb$PvgZjlTKYcGnf z$Zz!Iy6LrAT5%5N=9XJ6a$z;Sr$q}bh0XrJ3qx}tXm*(&?G{e%-e$vosW#B`CdDy| zK(fQnq-3r77Nu%ZM8wOgf-6m2V?Pkpz-Yn+!It{(sq+mgZC^UrlR8YnAi@ek?{BlY z#!qa&G&$rZS2QaL?&xLezwlvj!S>FBs8a%B>=ZO z|G1XQ(@_!JNr}3BU(P-5Wxe}n_gVFP@`0^+B$Er7ggRZQ49S=elYEEyp`KM~f|URW z+G{|P6>3Tkit{)wwV*6ir2RW4pAS!|QRkXSgjT{>8m?P7&VL*Ds?D% zNWJdCJNS|m3uAD?nXbBW2bT8o-J)`nt#E`viy;4pib-!Ym8cNgsA`TC?nMu7mo`DLWRt} zq|p|v8suurtN#LcY^O+Rm4uDSsaWWxm*#7U;E^lBwP(aCAg2Z-^=qJu$Q}GM3esQ-i@l)^nZgl$eSYQ9TlrS@IGQGZC z?Vn^bdw0OlKnR1{WoheF`r)(iI5CeT{8CHqSUKr0eqnOJ?}U zC13OVeP0;cRQ6~3A@RKpPJL%gC|D%8lT8a2I~1l3_!%qQ7eR3>EBQbI5HjJ z1aaZN|Cie!TH9urpDlWid65KP_bd)VC1p7M&lqQU8%I9ax9It)6CYrjJ?ZQLqJFK- z{1<=(>au?t-bFwOy$7htbC1FC0!0sO3_c86;1LvynbpXjrml-U-*PHV%oafWe5Jk# z40Po9b0rZa2)(xu>dI%tl5{beNfZgLw#?NNM|gP;fIDRtBs`e4l}2*eeZk zH(Vu**b|T;gXVnJ@@cD)s*s}0wnDEIJv4aiDO37hYs2DFVDYN2R zg`gRO+6gw=ZJw!6B;YNxE1cH&-4`xAqGNA`SC2 z-g^tJ{eb*$$k=opW|bS=#A_qLOSPJalUDtc*5a3JQ|mQ-Hd_kN(5=?hZiFuPL6bS< z0^6nd*CV*)_t|Yd$UD1!(HC0h8Aak6oER%IYz?N?^N!NDLnqLAkN=%st!XA$W#({W z?^0k;sq2k3?C8kJKDoN`MiA6k+nqH66_)XFoX7!@(qWpGe6w{^#y;sR*81mTNW zo!wUra@*qVz`mAJ9~D!w4r!_T(zXcIb2gPv>{TE8KC!PMk*|U(X({aXFJu^H62EMZ z+1#1@;4tr-74nrvRIA&6Vx^&H2Dg@i)A6VWzC)i-I;F?`zklWzI~R@YLh7+3hha=-WV%Zx~4IJ!!;Z|Yg|PHkvq)7-~4una(B{sH7QTd}%D z_w0w~H@CokY1{R{A02Vr$*0Hw)I^WAT-x<1g#EqZoF%Cit9&*ZXSr*ymq+xGR|K3X zY2ad>dFMt7U<2c$B^q;F`zLXq>^Vn#NF;Ze@pUn{eVtpW`M(piU_`t$5a9gd8?;+8 z&q;(DWxF~AD#EN9lh?4MH_6lD6BiltAjkU*vq;7LOiK>mK}G>~;POWH-BZB2NSzUq zG8;G}^bkaBhQ@X%D`Ztt z2RQ|4gks^{I(3ZbO2y6v z;-DJ-!=4*)GnL`}aO1lVMLGwDu_?kv0O43GZT`?HW#j&`&B&}UY%`iZrWg70o!7R7 zr*wU$0#6Mmz1X0In=VY`brt-#Fy*P5BIU>Lc&!*C7&vs!EX2xJ&+)}@Pqqqw8jY@` zVBzvN2J7AO{wr*@&aU_0XGCXs;(yY67KOFg<=@NCTk(=#AWaVQvd+*i(sDCru)=aE z&bYbr1CHGKi1^{(J=omWj6C?##VCL{XFB>^5$ebDDIChh3{~m2ONGm?Lg_L>W4^~SL;>n zpZVGM;w+JgaL#R&)&INO$=RGla@Gtc+wQKJ7K4kw1mu#d?GX7BU?^B|K+04nVBsfHQEU4hW1%2^O`H0X1B$WJfR?-TqEVfAb3Z z-g(C2L67rc-@&ixx~a|k&FM0ZR24%)vxx;l5|CP~Kg!!Ezuzg(h~&O8Z_R!}xK^C+ zSM=o7(AuN)M~0_gadF;MqE~S77Eu>H#E`xO2eATjzTZTM${N&--&s*shs%6MTfJVU zqI0J6&e?syeIl^(U}s#+k>eZiZ-{J08ZXs5D(YQ7h)Vyl3p`ekRxkJwdFZAm>eHJd z+jrGZk*eVZjZxnNY|(^y!jr(V2acCb(UbPGHP(vL_4U;;(2E)OvwI-9QEDH{H>~{P z$mo+^?w(Xzds6PBeHzS~sYxQ;{+MZ+oeKT}6pl%+h}7ebJKYxPiTqe|c&Ivg>`EQc zZoW#VWmxOfIcK>kCR(DdyW7KQi$g2W`J?_In*7xrpp{OMi^_{{r~OPa^>*qvWnsrt##lk zVO=CWMw>E?&yDKGk}VP|{&4-ufB$^yx#}6_fUjlWiy*#lSSg9SWlbXt{qSuf?mRwz zV&(dezoDt;U4onGQ%@yjzy~{zdO=oDU858BK$Qdsw|wm_YB$W z3k}^Pbdb^Fx2~hKn~L!bJt0x~(au;{M5Px{Q=g`S4ry&SZc1#PU7(ExHz>1ZW_lki zmj6-)BwD2>Zr50)t$Skn%A4<(Y40oXqW`dH84)Vnuj%=1e9{}{=_Z(#3|M$W=eu?H zzDvtuCA4)4K|Cs^dPy7F(zu@dL1};BM50GiuK@hP@k9(J+X9}ue(VsGg^eN^yMPfi z9^12k#X4y6z!CrACFTkVMQ8=pUbD~wmHeXP`t zdph9oW~W_W^0OScp1XQ}u-smPgHmuhz3-8f+Od3#bY30B;4|jKb`_Rcc0DVFC7xH&h1`Ye0s0IN~OdTU+;HI8ipBIp^GncC!o4nG={WoJ5etxm*^k^pd!ji;sBe7)J zu0O==fZuZYz0PF2etVm6zcq~-$_AKy)D0{artuw{?;yB@Q^n11ok*9Ra7ndMSG_L2 z`W~-DCELw=VH_h%pQgcUc$y!8S6|?L-QC5UNZZZNw%Nazt(W#j|FRQlwjHE=me8i{ zyKOIP^6H-H2xqU~eayR9!Vt$FB3k@AoNdEet$P3J0v5&3sNQm-F0oD|Z2u zUy6|jH%hxX4s{I%lzM;d4~;$oGzf>b_oD!9i2a65{&TYt24ulwCAZ80=;88+^zM`kJ=HjCx#`gahrjczpYq9x!&66 z=d6{+d*1f&kBRe6>Qt-sA@qjFxx7L?HC+v{dzLAjT?g43W4dIEV%X(-$(3zloeL1l zG`{^2&Ejj${k}j}b?RBml5G~_%{tfIq8D0V-=hxBgpSqQQOU)pbom6&eu5lFvv#%9^N zk+pA7;-QY=L1bH1x~(Ah8Os-2Gq_Dh(PcOU_)EWW%=KuD8!F) zOfmGb{{N~^W3j#yK$rJq->G}KZGQjb)Ly0O)*N8_lHt8@nl=0_saZr-IP|x}zTF0w zChC)A{^yES!e&Wg5jOjQK?l|Qi8MW3HPF?gn55e1W1Oij4V@Jdgb)x@?H zvSmfSIQN1*apyY)>oVUA&88NPMAeFo&5+g4p1co))wd+0-h?|!+ zlMb-k3h?A{;oi-(fqQkIUT|tnkQcW7T>U05xe_n5LI3zz%)7oD=3=DCmVALSM3Clb_LXw9Z3SYV+ygaq`KYr%I;waFhUuI7UxKuseGW?+*?O^TM1`n*J6* zmvKb9=QW07w(<{+4!kq<)-#?RyUm^>W$+{0G$T z7y{^){C;;OCh*#GcC>`9U}lLwy3);($%8MM1t1m{E5W~BPZQsjQkjsG$mA2O!hh;u z8a_>D{;7kfc9W6WAlbyCxZ`l2eq6G){+hqSVjV!<aaF_*f4QN}JCp{XNtun6u8mBUlA_L{CHP~!^_z^ zd-yfSzRWfBaz7gtw1PDo{eVFxA;SQy1Q|H**G|FcBRmWy%CPtct{#BKZqesB(#US= z5Rly4zf@{gUR-q#dfIy~*$X8>y@{eQD4J4lzW3mXF{Iv06fM(jbtxa7{Wj>xS)5@g z@|0T*5d*Z7v8z?n==OeY_of-4d4kDi6kdY(Pk!^w{ya|;J#1$DP;y)G;SUZkSVMlF zMbA`45!lk|#&M!a`Y5Ai5jnW}FvOKI`25o+zW;QwAM2MxC%4-6VROQ}0jsgntwK8} zR8xF*~VGDrd7&RuGwq3lPmw#a@!=kU1=Ys~ed>Qq#O%$0vmpX2M!<;5tyMc|j* zAG?M}{umO*qiGGhSUyE!*K3J$mtg57uEwDyaf0X+QBsX>yg-%7(EHQqZb!R1!SUfL zdhsfH0}x?!XD>9Rl<@nsn0qy{|F~ook^Ow;&>9n<_I4164Jyf3B|l$_lb>B(Iujm#BI4fSZC{_1R|h0wa0=TaeaR48HkU+fmpHq=;`#lt##88hP|?>} ziKBprpD*KD&nG{`IvJU-N@J6(#ymLqo@SI9M>{>TcQXL?o%cQhC^5=g7I&p=SEWA_ zINna|RqAa$voE4i8}`U*;o*veVt!z^bHLoigj0soRr=N%y~&I~j2=@TN;gVNSXS|U zN06(Ax3`{AOrrJVSVvq+LSZ)9+pVEQhHggtwXVDAA?*yudD%&OI6a&f0$2q+`ZMyp zzVzV){a2qhyndA5gxFfu71YW_H(s4Y!N;FEq+wE{WGL$>U-ILHvmaHf=q6 ztL&T#WAb=$oW#*kx{=lq5Ha(GV#-&B(9AKjaHU@I7*2uTefD)K-TrKx6S%eS%a}yF zeJ6$UAKre7+*F3Y`1>wmJuf~L-TIC;y3*0JM#BOTeMf@e8?lRBw{BVyTO-c9LXMHT zDF%MrA8l*c;!E%4a+r1yE7O-q0g7%SIw4d~kDlt0AcOLh)tweXE=pPM(bH181XpR> z(2Vo_`>>j2k)j^lo95<~>`lIUWrwe}xxiyw&CNbPR3DeGGR#R$j@L#(HQ9+{8Jjk$ zca7#UO2->*;4E2cyh#RiOft!-A*HP@L-PASBZbIhAhMI+t4S1YvbujGh41F4&u$KW z$Tl-nIx4v4a`03`(4e3-)2jBGk*8|iS7+bZAv~M*VYY4GQXozFtw0g#%@jyihp7;6 zEuj{=kq@|>Lk34_BdMwe?eNQ9jI{Qgi+%&dzX|l)`on_A%DgclS7U$z8Og*EBZ)4E z4+1^9hWq(`xk6qghHU?+#Fp!Q_Yc@FaL4DquJb$&a#9n{M9;k|9^?pi;23tQ zC6lWwsZJf6gVh^X8vn{(TVkTLA2a8z?2;R*0X2&2FdR(6K9cdXI1!l&cx`EX<8IsP z@p3uyt)mc`p8tLxF8mYO5f*oI@Df{9RGUd(_&#WSuyXu0LQPYZ3kZ0PMqDEz>N{EO z3|hw_`@e6-sj#v|X`b8cI5hi%1r2v_YyI*RXsig1-4jy$rPo0vAC)q`IG>w~o zNZpw4uuw~F6;}nLB%Q}+_|7?UlJGKAc%+RY$JHNy}@`=wj z#9k_@XI3(p(#j^QyBE{!oKBEF-+1~3TSO)-5m%eA`u)Lf!tPClQ}Nl$MnGw zT!H^{dRu8^?1xSr$2MySc}yJNUC-_bCFAx2g5t>Zmt^=cS3H<`gdC)Ff$kP;W|Gr1 z3cwO<;hw#4@t|(XpC5=zOw!_!*)WY>exJN-QmxdL9QCf{i^-KQm}L9U^#yKL!_JWi z#mk(!Bm)P1nJ%wMzB`Ufg8YOL&J)vDYc*SFwAwj`mW~caPv+ZCdbMGbbT%G?z46%l zmE&W+>rWWR-!l+XSKKW#MgD})VKPg*m5@-IxocagC0#9LOeBhTu?(1u`yT51)Wd#MM1bTgMv&OPQ9&XvnqJZqR%$H+;M0&Q~-n^^;Jaw^|8`VlLaJ*TifR zU9~q>iJspt_iV9hY(AZ>4NOR@xUePWujrJyK|z>pd~L#X{YkreJU{?e|M}^+I18`Rna5!kiEwB3*5}-Qb#q}E zJyNRHuhpg|*Pn|_UD|t$^U3dt7->YUR0@vS8~aPP!?F zP2t4wf$V=IeCn>{J$2&JuXOC>*Qoxy6Kg5G{h-fo;`~@BNGJ6a04SSu+>dXH9*kOc z%9W1oiXlq!$B+A<>KGsN>}oV`fy>qPU3{PF0eJrNyqN5NlCMJR150b@#WPFr#lzDO zMLWg#7ElwU?%ik3M5(&UaC5E3KJcoC^K36_;_bZPlJByG=_|EK(7BJT8`;d;as%5M z@#!E=sN@+BvJ&%a$i-jhuK!86ag6t##bn3C(XLDT(^}OTl(RK@aRO{ta6O{BaF*NvqPOo0d?Z?eMgL~q)_Kxp>Xvhlx+|DZL%nz`g|o`&ZB@8iJi2PQd^~9JF>D(Gg|OWqphPz& zR8VJ*tety8RzAgg_iMnUQL7hrxgnxs{A1{>mX7ip4xVD(VanRwJ7mgS={SNeS4*jN zb+QRMb4dlo?Z_zB(^bmLA~jk{rYH^2>1WQAAxMf|oo*b6lW}nr-0$;5(Vd52=r-4T zrWTpd^~DBnGvYc@qZ2=$5bn=et+I2+mC^bA=???sgg+-0dH>nK)ucER5N{0O>Id(+ z!S?lzo4SmDzywvGCusz9gWjI!#COP`3B@p*W=2twc*P5t^44!Qov1SzQ>-=I%D?jYw1MK<>wg7@bE{DJ55<#$apc|W`rEu6~+}}4bAu|6N zpE#?V3y8hyWjs%*C;tJx;_ODOB|>v`wc>+T~Fa~Y(i z6rM9==(xg0*uv|V_s`R<<`L(Qdg3f%zafa64SvF4k5IS>9CliYHNaT)KTz2S;cPoY z^ZZH=E?CW9x&A!Dv2-}Fo_LqW{p#&*jo!nou+y^99b4SM%i$~)qD=PQONBERPF$N< z*B6v6Li6=C0%OW_Twf95*$?k?mu0*I$lMyZhV|;hULDATH8Iu)^ytO0eW#X+@WO}u z7}>?Inn>f)M&qpugN--rwP27HUJ|RF%&ynHwK!&;E}vklD#8zsYspAW!!B0%dOS1P zKo4FYmdAxYtD=b=OdfCr>D!k(CYC+B5c(KJMt~+Hc>=PYo67zrhetsX^kbD7A^B8{ ze41Rc^gorJdxwxdo_wA@l2=+C(Q;}B!Q(qj;@%=uzxs~3G(z=*tp;CzFPMcBJGEp) zJ!c|x1uNTl7>!fdJZ}gWjx+s@?Sns{D;eOA*UP&HB@Or6Zc4;P2CU_;uwwwIs8zXM zd)dJKcO6h03uS>5@|0j@fXq|*(gW)LDSG>!|7+wSPjm1g%BIRZIbuMB@oPSJ)0hIG zS-07p#2?|a<}P26-~jQ$=ed5?QALDc8lQjCpa^c>FBFUBWxhDRp3f=MQ7=#!RL|l5e0(rJH3zVe|1dr_vLMuUK}lA(29{ z(3=h_kzF-_u(VOko7#Rj5-d322}?<~N~bLu62OWv(Sq-!yT18;emE!=Ih{&9`E@Kz3 znazXlCW;N3kf$jh4X)n;RC132uI^d5h_>W#_5o-6Fkl(v&t2j&aHHoTut9O9Y{xhEH3* z*uS5Hz9~a)HGCcL=zx;+ZvJD4OEgm(u7S>!79{CSlI4?gwZMYLP_^>Tw;te6Z!Kj) z53s_crX6p6T6cTisWwZIh`ZfRjK7KBjzKFn26cpy8MmS7qMG!`F!Q2p67d|vCcGS> z&nQ6-oEvmR`=e0hCN5**o99$O|BfUDQBD(m)$$aCpM4s2l&hZMS%xS~{cfg<{OR9! z<~aV{r_fxBt_pUckjWKYF0K!&b9ZH*E{YscCVKS(zeGy@9rol$=qthc7rM6Q9B-by zkhe)Uw|OrVzVcrWgpPnrOnyNg;hv8$&XZ7>v!VhU*%$uNyk)_gG zn8@&uYZQ+ybpf%KWXcr#tNVmI2Jx@|E~61Vww`z43HaQnp8#D0&vPL}4(53MzYj*0 zXB>;;v1`Yg$e8~efr%%g$@qXpg><`M(!b)h<>s&7-#wm)bTyc?4g^(X_AI_u$nRVX za<+zy^QGbcbDt}hJl0KpA<B0kD~`Vla+O&XrSizuppq=6l_>33WagAUv$Qa> zOD~)0%^9{nb#1kyN?FrWex;14J`g*^iZZH6RU3_bsr(BwFnRfI}=*3Uz1^Ejk3=i~0fh}QzIW#FHdv@X=6J+Bdlexfl4@4P~O=PkMt z(%pB+W({BO%ZtFyMOdFA~rw_&qXOae`)-$N#fUq6nR9$K|M3 zHSSE?Rhxz_7b6dADCTdy=|&%+OTq4-$*`40{Fk?=_qUGj_RgXa{q#-|6-> zhHbMAPd$C>cLCJb3Ri1ZC`?r5w5e_%8W4jp_ zt8~(iOJ$eQ3(nJKc1FM(4kgci!e4zUkPNYG@J5pYiw8VPb) zmLg(^qrLvFXGpa3f>%R@&UGFq(PyWk62xm|=WG%XJ|RG@a|*B9bKQypi6(oFk5PhTJ^C7U>eh6s+1Fe!`(5$Bj3#L|qVwGGN}e2x zsR65M%8D)O)(>8MRt25OSg|@kf9|ajTwXOA5HGnNO(_OypoR3N(`|nupk>Pkv$L8A zrYW6KFD77(6nhszbv|8K?qcE76a}uvows*Hdsg?&Ysf83%ADFInNpR$7fW= zQ((bs`J1-0$5UP z8XkOEjp07|JS&Xv-=&0t;Vxm8U=?Nd2}I{*UT5=z)yA7N#JLSEX68|RQE+ufP zED`fa2=6*3TjIWeDyHkDaPPTtv{{`kg&w?I{e1W*1gJzUQ!wB;O>lE^T#>8+MRVY1 zG~23ciFb7&_|TkIU|r(binh{%%(EGJ^nuv+BOyAn_CtM(#Q;o z(#EZw(|Ss>zkOh%CgVCKQ8vdb6dbcDB{nzVGFe(NSH&Ldj}u|^)ZA`nKqStVV3J5& zpAMu#=!Uly3;tD|{%m|K67x@9oL2s#Yn_qxW+aA?UNM-yaWYU*pIw0xXC!?Vd(bS$6G5h^e z>x5m*8c~LE@b!X@oaLmNVtLL@b}09LH^D@-ZBD3uElwi>l*QMblwNtYcog3#@6GKz z#mhM-o^gpluV#3Dx1BE@d{2Jw=N&2K%-JgLJHzo5%+_j)uSE`yXeKzBWe;_zFt);8 zgpuMuWEOQ%V(sQaQxe)H)SxKYd^4)KK05UaXwDvL~6x$8#Z8t?y4lRa4mr5(zo$B*J@}VkK$r;+mR8H*4r4n5JVXVu2gi zd^~u}SJwg0ep7hb0F5V|=B_D5#O09?bmaD^S1R5$3WUeYZD%gi)NQP3L{|Q;W()s{ zV*EkSw@r~v1`Cos=+e)5T4I*Mtjg?ji5HSorWI9_flOcRJhZJKagZ2Jak3w>X^2vw zVpQCm$lDQMa`f@{zpqkF%TsyU$404IGFJX@#nWLj6Wg=r2F3w z06@?pYb<`)avZu~z#Wes9}yE$oi-4>&qxviTE(r2-xQ0U1H}BdGl33c<=>+i)<|PX z+FS<*B2JbKY>qN!yNqg9fO+#dlJ1oVX%zlA??=LURsm-n)cs{GD*5fw5(|1A4Ab6M zmwVglrMiX=zw>$M1dP)@obYABkpWkpWWnuFoY$fX4 zz2oe3_|+J4yl>XXD#+wGnl-=SsNm@2n>cOg{Az{ZAn6#+jzhD|3nUslbk#K^>wP#R zTaDmuWgUjB5b{_leEz1ecK9}pA@*Rg{wS~J)i>G^RLNXi;lW6CRdICkQxOEt<;fDSIa!5#IEIYli-F(7+l7s1=G@@3u*vY> z=&aNV3rron%2faCHS>D>_PY#~uywxb^|+s~uufOjXLFHbPlL`RW)hs*Owu=(;D)7q zaHt)=orGu-%SH4M56(K8f;tDH{mHyYT3=eLhTXiS$FVd!8eEk`TAGE;iIYlx+~^Lu zOmR^LJ8F+Dmil*3SWC%+n=7mSQREOaCFpOq1jj%5?e5EtY7G(X3)g||Tnt?m6bfxC z6p;@zpSo=y?M}H%MhA3TCiZ<9QTP4gXMMgH5EAXzP+_!B4_>FPJbnID!AaVSGRXUS zk_T%d%+Td(1n!#Xvcm%_#e#y;wb5FW6Ay9|+xmdwtc6T^w=rUao=QehD`(Sp6Ug3I zBvT?deGSFIoZ1v85Bm}pRq%C%X0sYZqg&keVv5o<8HZMb^t;SX(i4=w0CRdVJ43;* zq3KI=2CFQ=5j|eNr+gz%0UOhDdJOuZpNxlgOh$1hjCqk7nC0DkZC5Ly>Zip*M)z)k z+O6&1Q7RpCQZJaa64cc%)NmU(8Ka;c##}w+HTpa@4igdtP05|J)Ppkl@OZVUvo&ew zJ}|62W={u`El3DMhti+AnX$F;HdVJvxc(&7*yWiluxzq8MS9J5g(5kFMVM*1Qa&?aWKilO(4IM(Qf+S8KwYDD!v1cqlHEfZ z>8%S>YLC-0kK9imSf_dQ4LkAT~O1VaU;s(dV!bx0)vhK^v{P2rgV7d;~3wLuh<} zmqY~oJv}QqbNV&Wuw1g)r)Z|ifeU$J4NzD2+UW`(;(_T_@j3ya-uH8<_E~^nqG^f- zJz{!tBJY4foq~|XJ9W6P?&dr10?baslb52~1p7MDz=FvJEuD`ZgUr1AxLjbzvU?q~ zsd&YGd2Rt8xCvg$Vy4+h^*KL_l=$lbENQtl^4Ee2w|l<7qCcOfhI3_C?aF{qtw-%&zB z!Upy!mnT_891!jIG`8%S)Xbo|FpdYBj?#(2Pk3pQl3f~V=UuYs7MN!Zke|pup={2Z zpsuPPUoSe3SP)3!&9%h`5J*&anGqkvy8z9HJ*r_<8yb{mRRV}rl>zyRQ1?1Fn}F_V z-0dE_Y<4x2z9$neQRZv4D3Q}uGRfva<<%k9Q0GTwt@SC|OnrTip}WRVsueh2)fh*? z{l>en*~_ak`FvgNoJC35jO6jV&s194pc0%BwCDWngT*E(r*}?RZ%Rh5!iL}*$M}eM z2V;}-#76Xqh4pSXTG*fa);54db>QCV6q3Y@NJh}!aMKRX%n3mY$D}c&OW&A{BrKS3nPPm zDwNMzWkRB|t8J%a(#;xw@ zoGUUe<57X#rRf*y+g?e1VOix>zn2@c%++Tk!HEG?gbx0lGo{{Aoo?f*Uiq8Tmi{2H zs(pLsN@1mct#}Z%w!MH&2|8rtC}DHM!?1tHEXl#f*;3TA?C>ThHEHU`ABI-tS>CCA zWQxpWYd3WrqhB{3+K1IST5sUxJY3Kzpk{}p@7VpA=^YP4YZ%QHnr(y>xx>|{iX?0t zX5~6!rHlwuwk1-2N;2wV#K0H}jv6*OimuZ9DDh@_T}}=g3Arq)p?t2Yx=VN0d4V{- zQuSrCwXZD$x9pM&utp8!lv0QZfx0?W3~|dBS;h?Cr!(Vle!ZG-X9=5dmJm=^e+v=;kI;4PP%niyRjsKeO~j16 z>tK_5has`= zt*H}hM25?Zzvvn^5clPr<#_VBKbJ{&u@Mg&ck$pM1Ur97#Ff}-98jK6_jtTsD7Qs* z)a4wI^%8q`oP=rzqy()hJ1jq7y5B;L{K)pOgwA+TcU*84UX$AL^!(GQXWP2I=P1?M zLik#9C7W=aoNsU@W%P&8)zc_4Vp+yllh z6l?;0uFn_Xa1jmz^MZw=XIOsZ&!xs-5gyb;mYwANCGLFM_imlWAPCC>z5F+xi!*5Z_dEEv7@CJ?Vo>|KG(MkjaoO;a`M;}9 z9$76qZ&nRXslS%zcH_UxkkkpQTSn5y+iRQuhp#c1)v>qY(}$HLorf$z0-Y zc@kCyGtliB@veZ)%;&ICmPCn{Js;LZMc9^nSrqoOJseYqbsdx7CdEezo=J6N?eZbwVQxf4$>+Vn4ddkK}tje`%sJ|OII_H6tWT1%sDI;C(%_G)I_np%oa<)wGB(+J z$(-?PLN<6tsh)t4`3(LU!tswO&TTFGI1)E zc%e^K7$=j<#2o*v0DsxZZ+zmd7WZ6j^iB@8%N@dS^&hhPqE|M)HpoYgLWer}9-fUs zlQYsy`$Em0ed>Souc z10S_Fws?;xZPCe{ZiP1|-(`2+1FaFA#ERDP>$F*8(*=-Nsv2F@OVPX}p~I6PAjP*N zpt0d)|7YAjPuRLse=Aq)(_RbQ&EMZTMsZyqWq@#7=L2blZ(Tq!c9$0&(81p>*Lkbe zcv5r_ZYearW$XM8&7)Pi#&`RvVLUGforrXwjVbaVO7VKP>j!3vlm!I%LMl&W|47*f zE1&7et~^DGaeOmv?OXd}6w@ko+R`U3mO=a%Dz`zp@9(8AXPvgEgr+AQKDjbGGxM1LM^TQD7>O5$R3=d+Wp6Tn^=KD8y_W%d;bvTv2x&7+Yn&T$}J?#-osG9SEApm&E1uC!Z3`{rir?}54bSzWbZ zRnWh(+q`%=5W|M#o)W#y_h6t+Z(W8QjAo?9EGqDaJS`^6uqM2QE>D2O%~dATd8+b8 zCSa*^m8a;YmK(%T3(O-}#FoJlHJ>VH6|Xy@X}#4nG+-96EYth!ipCD#ao{6~!w-=; z=|x`wiw^ zp(!mBC2zBkq}aC43j@Qnf9xcbn4un9{PjO};kjl3+^bddG&YvERCI940w;*ruBn*5 zljZ0OZ8kY)ul#`Hu4AF?#SEtCV8mwZsaRCU6tHi-6MR|Tsk9Qxef6QP&PN&2_Hvyo zU<5dduG~ZuEj4BJ;#s$JO0?PM_gAf`688qc+tk&=ykCsU322a~p-NG3q7t*X^}Zq0nY057kQ9{sXrAc;cdEZ1Qz`2776cSohmx zjVp#50+e!;i&W0h*?v6A@#Rl^1X|fQn^h`RW^cWyUyHS~?1~T^amg1pJ4w>r4VA9T zNsg!eivH#v5w8LTGCsy;nSB>~hx}A2n^xz+()OkdX5Si37TxBQ=th7~w8R?(or~eV z;22F1yVxwHb}78^4 z;@B$SFwY!Y>7eNiV$&zFVRyp(7Z+)-*?X$}A`&F!W6L9tVd(-v06De)n;5Vf;JQNM zE}rF(>y%Q)zd0F~(ad1?|5nid)Bp6;W)t;eBI`6=OWW7i*aI~8TO&14cAP4i9pu7v z@A9qyV0jjEH?-7)N`=E|>=yzi2_^ z7cU8J51eF{+o1%WwVNs_PKJtVw65j7fvTKFFc*hql-z)a$KiD(UNnaCNN4MS?~mRR z(3(s=Rl+;hn=ZOTxGk8#6<{VUmJ=l}eT430FY;sH%j)K>DinA19hLZoG1lVi5uqRX zES98H^JD?Hi1wSwVe1C}wK8&x-&MtIz}+sMYbkbo@FnO0tdc8d&7by2hxHkmrBL4& zm5Y6tu~6;JphDE2t}?rK(JORSv|k;K_pkEV97`&`4bxdEX61k~t;H8cPM)>4jQPU> ziRyduXNzf~@IjYFX?w{5!G2l_J|IW7H0p!{q}i6sEp4JP#%2+itA}z%s#~$nDv_!| zfcaVWs_l20l3eJ14dxNvueLUQ-H6$&f?<#A4^tNhi}2aXjDlxKj;f6*71h>P26vwP zf%|p-+U{~;9x5q)@0pbpYtG?^Gq)Vwt-+JhJLwtMPH8H118)tOuxo#Xh%maX#7WQ- z(}lg)EG-aO4Q4&G3EF-RYZ*HJeBg0mUbJSgyl}nA6EZX*#HAw5d#`3cH(mR%pQo+` zZ~NP-S#L%W>2t;59BgrOz>|iLI`N!)zQzeHf z_XUGiTe&;bR&U}%h5FKObB&`-D>lU47r-dRbEii-;k+s%!Or%|kY@VaN4J$7zIho9 zcaVyN8$`IE0u@rXsl576gGi3rLwNcER{dAwSSu}Tq(x>^-tm{%TOm*)|ENmYR1D5y zGghmoSu!_{AHG3I-3k~UBHYb+f#|0i_YhDsH4=3#OngPZp3@%yL_$V4MW%5RzZ(x|pq ztWauA!&x;qyq|FlS}N|Mk$wd+9V=lHJ_?miiM$Z?h{CX=UuD}{6k04*chf%YT9r;j z?c8JRc1}%ur#h#SMk+n!J^D;u(mBUiob-C)fG|Z_kulH$-!Wes2|>yeU4+Lr?c*(1 zwH&!?NKAzPGDbZzH4_E3#2a3uT>~=c?fs)aTX{w!AaG>3Z72X=Y|jDmG);#gTHHUT zb^wvWxet^0=el0%s zIxeR+&jVnH;-0f_qL<@OdNkTut^G_SWvJiQ@&;pRnOWZJCb{TMkODfq^#ONM2iM7J(LLAuI(o;B=eFNt069TMO;%(2NzAkeG6hM&0rrE9_g(6wm+o<^hzG=8}aiiJ)gh_H2>ZCm)s_ z80^x&Yq0I@d|-?zt;s25^MNR@XGnqu+@28dxO`d*^R|`e;io4$?M*L7>KvjH9UUl> z2Cg#XzuNG*0iusGZMeBI{)fUg^hMkSv|=wofo<5_9(jM=Z z&s5rw0>30GSIev}2^l(dA@=Dj(ltHUMr^W916V`NNXos1WDWd$B1$qXX;*r-xC(d- zQFq{^WBJc3wR5#61Ws6Z*DF z&u^Rf{vLy}(ZAS&aO)2{oBtvJslu$P61pc;MC)z9$EYH%4OzyV`X{o4e!q;%HUyF> za-tNHW%*0Qk&IOdyS`ibS=&&AvEsm{^`{xj9Uh2JwD=fDxY}JmwlT^Z)G_41w=22g zzp)ZU8(e&+C0ib=?6Bd<%|%1#Uh7doNY*R|NtH;W*DTDca!zYc`GgC%5bn_IVI810 zn8sHo4A&Xnxws!JH&bbDZF^K&d_Fn)aJg;B()hd#85rE*&1Ey`Vu_w&Jybo61v37W)-Z8j*D|yOH zG<(PXts%RT*d}3!%1VTcYYDCWN*o_(Kp50z&$-TOBBIAJZB`J>xDvLKKzqgYnYM;{ z(QuOFv@8LSYBvXu04$$bC$in;b9lk#_Sog17x~$*V}bOK_##;^EtH&T9c z{fR{yPGX?MQ(lfaW`Nj##Vq)#>n_6Qjt4Wbu!u_`C|U{?JK7_utx-~65&zji2|<4r zkB5rf;F@E&B8F&IecELgwyqiwQIs+3Zld)zNflYDT^?-jp_znV zw>0qqz`LX;w*e`WoNVN{6fSV9U9lsQ)1Q-ljYjDxh;SSQ0a?#fMRZSa_vMC&WypDT zl@E<*EC_rRjp5(tVOOO+FYwVKLUqJq=wZ}&J$EM*E`#!K!1p~WL12> zq@JhYW#3t!;_Jf%5IWhkt$5EekvXUz4!+ahP0I5};KTSBUlt#NqK}g7`5}RZnbb5I z06{?hJMF%@Ii$r^WB(@9&I`tt&xcNWRK_Y^%s3T4En-x3L(qQ(=jjz3o^7U1;s(H(ioyhf%by?P`aG+`J1H#Au6qC@XQd&w3r4*Wm*} zQ7f_Nra7BX=Blcxi;XJ~x4viYVuwfhr^NG-lto#ds@~m@0jc<|HAz<+I&Pysrf?B= z;mRE|T8uDb#EADTC2@f@dCKFX;mZ^!dC>jn!%mv1QwdkQM{#_NwpOlxNF>;euKux| z`pAWq`!4BP)N7@Rd>F|vMTN)#+9B&E-{(Z#WlUm8cl3VnOl-JHH&(?}7e}=dQR8w; zzR0+P5M+GciQ4(5d->tiPKD?c7b=!$H_j79k_%ujscNSB5hpON{ifQkA@pBgIiUaEbNf9D=)Arlt2mBStEx)7yqt zpkTX^?|f7w5p8y6#`r|ANs?qLi6$4_zHb3gi{@)HS4OW$@7*pRi!2!&xhkc^CeW4d%V zI`nQVZTt9Ij&BdSpiJEtM>8=ji7ASyqjL>(pI46;Z}q~SX(VMuz5`GOs=2GM9sWsC z*N#SWbKjA5(?w;Vv;XN|at_L0a@Rs!FM+*s03Mbiihx_8QYrzBtQL`=etPX!O)zujw|lyI)8(nnMx*ys9Hrxd5t zO8yzdBU5&*UUyC==FK1*U?W|Xm_)=7)^S4&Y3d1!oMVi|i|vfYBcSC;VX16&T|r5YmC!Xr?wqhaZpe)O}@3$@mb zYbVwXAZCD4r1_Yz{e7S<==)nlH7^snMyb3=Z#)LXrHK0 z%1tVLp`4BZk1WQ8G*uO88mCfLN}g1#=iy&O$$AC}XBy#yMa<;r*0#~F`@j<-vZ)PdoHw_JL1a$EnZ1`V<{icYVqMz z><%5<3Nb!7EHj7$6Q`ld&bCf`l`tOV`lko_q@KsgO^us8jS+hUKzKxD{U&#qaFbqI zG2w_CzPC;UlqssWy@}QcO5TBNpH&r-H8E|kKj1^|ae_a>K3+-9B|>DV8-1u^^ai!^ zJ(+|lWF$fNCABGU)X+&r9)%}g1S_^lXA^ucewwzs)rBX)B=4&uInKLV z_`Lg0TZ}k47JWw~5%rLFs+us(dOPkKLR#f&=6h06kGs#wX5)TQ?_vx`y5Ev{U8GD{ zwu$+uW)G_Fiz|(lb2Z5Qw11xEG+-mzsB;INE)x6dI)COX@{@D5OV6w8a{pPXF?197 zvPl_9GE7fEai{C4(%v)$Zy0tx=En5ccKQ01Z^=DvwY9@u3-L6k)e&jid0>ygUosAW zX8sOAj#dIPC@Y_|dGC%~QtCV40qF_O5+4rH1oR^`3fQ%|A6Ixm%B%w~|7AgYrm|9w zzxozf|CU{o?bI>qE_V<1yq<;sYx?C;_TcI@=2ZS9H(^fC!{|?-DQ4uvqvnX$V&*s} zs`Q;-*W8g;$EY(s5Vmns`ZM%MAsL z?=^pF>_N_(0tCG$7WY(b4Gw#WqR9s^!2LBoE1{Nz&a#h{iJfTLN1I!_Qf}uC+)Ys6 zS?OqYjo&kGOocrDwl{oAP#JiFHJ;0sd`GiScu`a}fDiKdcKJqKODTo9A;g#_USgNS z3GHF_<=~!-;VSW6?v(o<;6^(H^0RzvNGK5Ru$z|#r?3$Oy7xB00ywtbNM+RDvHCenrEa%#gPxcs{#-i)WgPF+&=Lcl@|8uCgm zZ(`x4?Hn$n$yiYw9du&mj&$zPnq8kaT3lKrntNvbb%%ea>>1CQbs@ltur|%}3*J56 zZ7GIOuI6-{w;wcg{_6m?1dYvVH(0gsRs4FuRw{D<8E?(w3IO36WzWv@5|kbPOTB%& zR$TUeG8H*)Lc!*({3ZpF<3Qr-^ZFlOTzFl}&9d;#&*1){cb-@CdJoSB!VyZ#Ge8FG zdSUZ8KzI#d0#&FLg)V=)NTo2MBU7VOuT|!?8${F6-jD4haP7Ty5CkaPd9v~nX$!|S zL8q?FzU_3BBR?t|>7P$5HauCmBtR#0`qCv+A&)t=i;@cOnDHD~DpvY%WlaPe$4$6s zeSXb&qS`^v_|yNxwQmb=p*D-JV= z%L@Gl2zz%0FbO>~4BgG;47G?arxdBR51sYoXc3>IJaLe8E|K`bYh6-h z&-IUGflSR2!?J4T;!~L_Ndiy>2uRxg(lO}7r9BH$OWY&n^j7DvaLrt6yiwi1!dgnj z>Y)6yimDywHl-F3<`?EFZcJj>8}TIa$#lco!+E#1cf%iDcZau=uolum&Yt5buo@me zj)G4WP6n7k_+#(Q8iJg`nI<$K`<_gT^}XBKZOB zzr6Vl2IKRQqb0-(^H`r9S1;e9H@S+-xd*t@qodELRjZWWrCS0 zr$LKq;eqPht1b2<|IB^a9K4SWRWBlT>pVEu{v0ZM7l*vw-L7RQcV{6W-qxYvDn938 zo_E7`aD%k*wtVSxMs?_{xxwnr5caGe-yfYB+d^5rSi;GR0fdikJmS| z{x74Vo$x)Y1BnfpZR|wI;ZX@5!FxgyrJ&CmA~{(?R?;DQwf@$+%VvS2^QO{${k5_O z(Uk!Kdq!%%2h!uzllO+V6E(FwBH#xZuEuWerkz_yBX$;d+`-pYKgE?y&GYIjQyyoz zcn(pOS+PJHJ~G1Zi8<{JE$Q`o$T>@C`IJNFUOtwYRy3xmTYb++NYD+mQItOamBf*5O20WdmPhemw3=F>kX>*^7PWwfW}j~8H#Qtw&ZrbejCs2?xCYwv zEQb3b6U^;ASA>cs(*(2{WUmd&D1gho`c`x=d22(ciK^j1$LY=E2^ag1Fr9*90UARt z&~>@f%A!5?sBfL{`@fwvpInA)J)3H`Cpy?f}~48es0$^V(GbC z%)a~nM5$xixnTv;>9zfSS@@~i9r8G7yT=)nS! zdt;+fq)S!I>3rcU!~pnON*lHr#k`y~`$X|(vXM?>t-TWN>6}&m>9=Ct&WCA%fVngJ ztW-*3{A8Y77#q-K*l$@r>cd>$+Y`ed|F{TwJqQscd``*Rz!4U36t7xTUMC~L z`Y(1M!7!i%Z-JUBCPnOJPKdz(Hlupim<*rFmzYnxpH=|e0VHcT%y9n?FH`q)OG^EG z;hO-#bU1h}yo9mf6qp~!^+JJu(THG=WYTkx}m-zTZ4p0HLjhnjjtKbSm=bPV+ zsn$#Ss1#hKKjiaf_j%0|tE}J%yfdvc!~1`gS_Tfjlx{rcG~I@LE!5OX|BH)P{PsFW zkvwEJr=`-*mn&KxB<<|O_e4KHYb}&(e!&j0H~G;&3|5{WnbI5?`5n;ptbYlY)7YMRR0-%t7f z&e~o4MOFFxnRFPmu0U*Od!zC=jg{f?GQia;iQOE086^>Ras98#>QC4%7+2$Fc~8og z$cT=P5q3A>7a?x0dlbORVb-4pwmkO*CVI{0HKTAF5SgA1ZLAYF&WAHi>Ba!%kuNzn zQ`yAX!``F3#;Km~AoaF<_=v_yAuMuz#uzE8ljN~{UA3h48_&nsbLfjq;p94BMwM^d zk;;H5hE$?HkNa4pt7D^(NacBR{LjEm(&O@9Oxj{l?2C{`b0n0cOr+}})KUK*ZmnJX z*PcC3<5%J+u0>Uv+j$9(pU}yQ2RZq@4d~yD=Gv<*viaqisk&<=JyU7$WrFHuxj4D) z39~`m_x|CY!Lq1o78DY(h3(^4rS7d3$hsE&V8fQPssnd;`L6FP(@_bSby-WHeUe?a zv!)*?=(1(510(bBZxGmbRR_jBLu-^?R@vg_te|lf4K_Z2?lmq~26uIzdqD{a$YN=O zA|%d9QzrOPYHIF(-znd?AG8icyk7n)1=&)hNv8WMH@81$1^d^Sg#CF?qgI&kI!!bq z74r;Rr{6J#^B^qdkjw|M9c8;yaTAouY&t*oAk3KwN2q+8(z5cB%O!gbOQX`+Hwlux_uNy2pU`K^tidB-$!#xfgZX36^~cJbUG|?1B`;h&Vb+*bh_}0r z;m?JF`Z1Af%lkwYvk0M90D=bRE9M0mUw2XYf?fh^VOv}C&X>ntxEu7O+#or@?6X@+ zp+T;2Rh!$%<;{8P!K3SWV4k*kXnIjzRFxTgowZnTA~~3~{p-rvio>%)WUnk}@~&o_ z{2Ly`cj){*iiyL4DwXY=G)C!F4aH&4P35ZR(1F?owJ4oYq$gRU22v!7C$AXn`H*A@|f;+@tuns8z$yC0)97fRJ>~xJAE!p;Rj(@jDmE3x5arhujY}=e^bRsiZD$J` z5%48u+_g9lD2dnha;SgwmmqBMmd^L^cl3ls)kSczzxR5MxSP$<8mlZ2@_k`(lfjJE zY2L9uSftobn-oMO*CNd$jFd%sQW%&4z-D zsQislyMap|&wj)_8lK@M35jy9jH*d1&;MUu9N78Y4b6BsE{iJU>+-3Ez%_EYB1djeObAj_eS|6uAK|H3oE16+@D zJS!H9@pV2NyV69mG`>`NzKrc_0b|kgCm;i-dRn_badSjOMC+4Hy%XS{s;8!Aft>y{OZfPH;kS2=4A~!QDN$HSY9V z?05G$d%t(zpfZ*4)jCL~C9rSAGcBylWaS0{<^h}%S->r-N4!lI@? zp9NyMZ4z@he7#C))fPSBhOu@{daN%i=Ro2Sb7f=+FHsC4oc`q}`OywdE+m@I0Y?PYH%$?{h#LXP7?M($h$sNGD1eq#8bX0#{R+PBR2KqP z9Ld%3y!xd&IILdY+{Aw|T+O2$HaCrSeAP-1pvZtRQ~@v%Or?)O3A754LC^3kGQh66uyo=0_5&LVrXL<{*9 zmV}vMF)Of5mt^EztUW)t}}foM3Q1Ef7d&6lHCt&FwGd~ z73vv7`caKGme4-uI~S*@v~ThBZmmGoSS4xYaURK=mjF0%>dEwJz$Iza*@kt<+;(u3 zxJ9&q)m6-bRO$oOjg2i?N$%(euX;7@4TK|D|h)a#U9 zq)$yOv6{u^QzZJd7Cp{Q?Z)ZS0kyZE9>KsF;^A|2pl5Y|NDm*EjRN zHLZ*Sb}qpTnu*++4A;k!V1AiP&$(x-t6pauj2M}mX**1rA3NqS1HY_!xZ}WK>TN~1 zd;>0wFn!PuYt45X7~az@yR8t8MRIY&V*oAqIJ|=@@Dbo{{w#&j#$ntCyBhy3d%>^C7ET*!bsGXy1fNqeD`{y?8Vgr(6=;k4Z1N zIT@EH=UUDo_SBg)!e8mLX31l_IPD(!czn3lV71as?j_kAEx2qrx04AAlvZ92jy<+K7pJ~-1H7P<-fFf2 z_~ts!+X=;WQYpf~oo6*z?a=Zui5^&^Cyu&&07IOZRIQa!&Isv{iS&Z0 z*H<>fW1Y*{nx4niw8vWWM#Sml`%!CEJWPll-B**SY|R`g3`hKhadby9~*69}hu{U#e= z-qC_k2e~}uXltz#;3Bm&+fH9+^%=g-dB6k{B+J0MaZi5^n!B2VIDI1X>m!GpW$;Yd z1Fac6g2y#m7QY^zUqs_9y8QD?|B&=UNa!TeZ+-XuIrX}QcB{t~#I5$?(k3PkeQln% z3>F1hplY#0XSLV**?UPdyq4rFZ#*SQai4<(zAkXb1$Js_VZ-I?+n8jQArlW%>#5)>y;}XY+NJM&leL zBUK(AW;sLWtJFL>|L{-R8h(<}!=#p$nSVRq=XF1o!K3|||L4L5=w_jv-vFX-ck$Ig zf4z^`&IV}iU$a^J2$vFc#r51QS!ZJt&G|WqHVVZy99a#ASwp&)f@SC+>TuSb3H|*g zo)(vRU>=6ARMV%qu?Ye^-z?!m2PR5V*! zKUIGwyghbIR5-9@N1Ga^)u$#NP_j*#VUM)_nO^sSW5^NCgH&8kEYpV<@pY8QWBETNVNBPoSDjt#^=7##S#Gu8^`Ka0hqWlTrC6d=8YiYAJ*lqb^Mp(2;5(rE=W^> zj6>|sqIH?=X8^EQ)%mDq3Vnq1v_WO;=_D@7d2)`}m~8Hz8q0jMmGy$8@K_>gjCdvg zgLt&dvTJ)(ziJbt|El)TfN;W*%yzfNHmR%aee+tGIjaUQ%s_S~sXn8GGn>0#!Cog= zU2^D<$HyO=;Szw?uQ`GL5AkxfY&@+|qF1l-$(zbEz7hJ7eFq7hgo>#kwi|Ez@0@{n%*75{KW_@dITHB^eu%TpnY$-3%>cbzn zMOl;V0jBHD3)S#26KInln~9A@9L||T58QXj5PyG`W-(=wMj$|7`t~1FCohU4+;=Au zpT$fhP^;^S_N>dUnL-b6FK66apq$Vm;5&sSUa9>{==yf zon>$J)aJyb(L265<2g5$(&8EH$Wpd2@ozrkP!c@YzqUkLB%2Oi6(Bgjzzmm$R34T! z*Rh6BR?JwgVm->tms$i!34U@tU<5mbW?qm%B$kM*{kpU|KvKl?N~U#Mo>ChUhZK1a z^@Qn*PctwB1k)^&uHDR~r`b+%b8!U1Vhy?(#6|y+Sv-Hs2*VUg{iLs@L%+?2nvY0m zy7lz8EK6G#swN$xKbkXrJWN_Vn>#2a6ibb&o4uzDqsSAONsJFQzX#RP5G~3-eH2vB zyC7d8kIoz8&b%(y5V0}CW1r2wo zfgmgugCC%*rG#9uC!C^a8*Ykj+E-Dq%of z4UUbXQT$ZVSL^k9SID%aP4V;VL8iKv!9o#Tpxb zL16ov0Kv#WaO9h)h(wH!J@f>02dsoJXSVKpfPbFIRcyMW4wDTC(3CpOgKD$$5mC!E zqJ*e`4qD7I8SM(@1JSetRRcVfMQd&dP!KqH?q>p{w}Zt|*kEzr$&L?{3DSLTn|TOTh#(1_Jp&qb(E@*4%k$aI^Fd0DZ-(qac+Gev1ql^Y}G~i)m@FHoXT`N0193~v!FEO@)WEEV}m7B|) zGzWQXg8E?wehvhnKeHB&Eu;H~^C~l!JDd7X7*_npO)(ypmN@>bE1A07@FtMfkY@;u z>R56@K?;ioS?3a}%OgPIla1=(m-k25MB{SzHBq`8UnXC0pWPZ)|L@!PMfCF- zg=)525O75{4!`M;k?i_x;=98WzDd=?z-$aCbWd1aUq4Z^Gi=(Zo z!Ra5*ATah4dppK4=bLOr5hT0CQ<*;5W`(^qQlUWY)6*lyP93-Klir}`=3Wy)K0+jU zh8Q6Qo8fN|_ZPua;AN1u&|s9JIy1S}Dv_CIY~hMfTkg-znjQY5KnhcAGJeXjY{ymA zHmH-=^m@adP{Lr}C+tr0zY79_}kNXZXcv?gR=WSO!J{os;Mioa3Ak!7lrZ7jYF z!FldGd^N*)|Au$|Yi)?rVsFKk ztlnnv4{b?pWR;J6!+VQrQ#?l41Dk77fgn+=@!eFVL~f(R$y33PEXmxT4Oyja{%>BC zm#O?p-P;foWNe}3q6-Nc*rdMJh<+Zs&OzOP_Nx>9HmLHpK(zWhAvJ>RptfP6N#C>7 zsGtgwELJOwrDg2CY18m7>vvB4GtH$?*pxLbPYr%kHo)0n6 zwo0scL0v>f?Rihk*#NmQl*)w>L2N^`N{U+;+m_0CR4DtF5e(@HXy_@cOznrlUvmEI zZ?*9;FL6i$J2c5=L)*4?X5bfbVC?V*Va8@>fL@NrdK3PlPg z7TQC#5t;Z9(|}HlEDm3G6TzIR5z=^!W_1YVzn1YZ#X>#uG^aIj$Vk8+adu^gr=IW| zjQ1sIKWv&SexzZi1e5L#qjBwB@^4sblzy&g|G_SOOOIQCZs5I^9kYolt(l2oMYenX z5MM`?y~)4C_V;+=?{7p+3bu916rq66`{$aezWO;Mp56;Ur+c7$g}?Ik-5#O4{uDE5|Sc+SW}YPRbZgffP$bUwLP%wdYGQ6eaQ8byFE zf5_15IOY2v)}_Cs58b956U)iSZP5CSdx+Ef*s*K|wCz{1+fNkiSa(V$6A^O5MvzLh z$Da$`{fiz>&_9v}DiebqC)>fh_Hz>7E) zzo42^soyGc>CFSJ*Bd_xp`>5%n~4&M$NYXZB4jgh`DO4tdiq+p^7NexV6l)Sl+H6X z;H*OJQAK1Zt&XnXaLSL8c>h?yJhe56{R=k0HjCo5 zYO|~I0&LGeCx1X86S!!s+MtvLU9cgX{RS@d`ba@EIcQPxHtXiqr(54NcfCRmxrcq! z&Y2P_yp_YOA?{#*31L1tSX#jkC}e*x!pm}Sk`ejRefU%!N|?g;n$W!KalAlgi0Lr$ zit}b5=HWz6ie2{gW$l}wnIc^$iAi;(y=KV#HJ^r48hVpW^E`rsNRi6lw5xwD?%TM) z*6cJ@3H^PztK_iEcm%DaQo2;zL7p|b?~l|E498z-`2o%{mDTSmjak>c0hqBg-Gi~4 z_H3s88s`)f+a=wb!T;GMV1iB(4-7{|+MgS@Ei`fXjJ#r)6cLZxbpW2;a?`q%0|&~R z5O>q~7S13FN2U-X)jky@3c~6(jcj77Tkk zLKCeOhhqyABJF}qWwkV8+o-T+?y_SV_~ZjpX;KkY@DF2V_jZZQ10d3U+fL*?S?v>t zIOfbZIzCxOgaL2_|J|5mjvxBs$7RZ33ai~24rREBbK*|5C0^G4TxWBv)Jq}Ljk;OY z*OyuQj(+#)lB7aS3WozaU#p<;g+=5J|0|lnr%MUCltc4O$d-u{&{bnzXTS+k2GfTs zq(B*A#Gp6zggPAlK6fb?z)ITpa1U6Qd^4@;bd{19{{5fRDN(P&RCPG#r*{jn09x7V zO8056aRZ~6{8V~i29hP!^Roud!`4O4eU%FKw>@3?36(IXEYB!Xox6XCh6y30+24%I*Jrh8a~{ z9zjT#%pfDR^j={CcQxhW0M#fo2OZa?>JG9h5n`>RU|!W(`#-7Xen3kFKSzxT!teQ`1Qot-%2Td)*=9{%5*2R^-(&_1&L?3g+tZj^BDI?fYb z1t7LMK9I%D5#m{g$CPN_sLF>L1oZ;MQXxi?*ZfV2mbVNeu(98f$jC(h`QuY)bh+Kn z$XHarXrYMeLK2&+kh*H zmDG@+IwL|{0VDplu`XSyN|V-epaUfzU=Uhc`nJQFp;=3k-3Q=mO$uEVD%t`OX}!}D zfah0wHy{I=45Nv5pU(Lq13LfyPand}Slq)FM|8TaXqD0j2JyI|67)Q0TT}Kf=f;Hp z*ZVYPmlfwZ)e{&XEUM>aoTlRs$1M@iY)_DkRKW3FN=WQBcP`y@e}wESdM~~(T#W$4 zdK1epS>9O`@Uo8&R$z35Ip`u~WQ**kVI=Vdx&8AYC4d+B_uKRA-XE6Ru>e$OJ1^}t zP4}Lb5$rqo6kW(RU0)lAbbshS&FI_m5V_9kI?t$%*xGq4nH87TSzY*y$H5l(bedWf z&p>eAPjkXSl+8=bUt>`(Mp;s+bmNkM2Cgn5wmfdL>R&ag|IAgHr=7}} z53c1PF=#QUwpR~Brdf7rJzJb>bNQ1NkO~GGI_JWi5sd@w)xdxFfF{Tvf3-q9lCgR$ z45@7+LMv4%!!Ko)+h&$y|h8WtXopa%*9xHFm} zqTmksQ2wvHFhA;3vi*M16+Df_rd5wl{<*dc$qwE(V|vS)&X*l@mxYs8hTT9GUrcXd zUt!@|tfAHls`8raY}A8?4!}Uh!DOazijPf}z!W3=0}YKSrUdfiFwgLGaXcTStG|*k z5Cg^3%0)$OrG5%<+n3jGVp+kaR8X$;?Bh(IAaUQz0;USEd=Z}mJLh`81>AkQSZ0vA ze%@>aULi;F49MOM-%ED)K>F++r5_%kDSrQ!rgAqgpoh8Dqbuu&+oSQQMW&7mXjP&2 zsbDcptXsESYE5)PrqGm06v8r-Dnv7&zp^2l7UJg+dmGdoR=L%XmPp*?X*Ho_C@L%@ z*Fgox`Uklah4;b+K-pJx`~C3;z%6-aOGvFYb{;egdn7W{KJ|6!D&!ieD-ken`v@&U ztG9i;YLRLh2)=OT!2mSqF}06r#Gw~Z#2q1|g^4}|OqtC|SO(7*I2OXkc6{LZeo&of z`qitk_Lt`IV-HC@4GiqxH6o(|hb3lnx?h3&pkF9ys8VOJHUQl2h%{9g;SX5qp$p#{ z*on{D=a&OL2@Nf*njiKX$l?wOH7yD*5e3-wbMVd}Z0Zb=;NTj0HD^6i4yoEzJu$$`8n6J!(Hl& zE=@+@0UQZb6f@W@!pc3PftMtV8sZv2kM`oaJY|(e@^JaAp%XiNQ6Aj#*CW@G#!j^! zjbs)$_qAws@ZPHVc!QrJV?uEyKH+JVj(xrGhRu~HRWqO?jLwEon4}8jM|NN$G{Ba2 z2jXCTJ)3jIJQ>J?reFh#qR-?6i05H3F~ELMT~8(4$$<~e?GMc_g3pP5`AESA1F*I^ z?3bv8kmKvviwSDuh+_ICLt(wRdz}|hV?jWEzLo#!aw^qyeWu*Y*|Cq%oGDKx2RPs~ zn4$)b{!ggsrH^QS! zc}q(5T3eM`o1aa=O2cw@0-FNal%-C@%;mN@^I1zyXkn(tKa~{GQ-F(QGb-PdMg}i~ zz7t1)dD=)8gCLlqp&cB^N`Yh(-tD+WO`!Q%r&Xm=H z3N)K9!JB>Ibn4ym@0-?Q&4cxFBRJL1;X`5H;SL_PFmrOPc1YxuBW7=uSfx`9uy+b! zAF_XM0M2G4bO1^aop0ws0QQHyTGB3~v@gHk=8U~ypuOy|84*~>eQ9ZRRtTUc{;UV~ z!>?)NF(VZZHv**xqh{Fv;t>Jc!d-=_5w>WcOjft>!a+S!-3f6uO()lF6uif@ZPTsW z&$8*(gv%+h#gqjTeNh1&pVf>_NsC13dTnBAXbM%=8mMi`lz1!}4|l00u*?=v6wSY0 zGlzZGZBmM`9(MCR`dcop2&YmZ|+&fwODHvGmUbZw|#&E{6f-%8$t05 zke%?`u2H54MJ4%ReDb`cInT2o!=fFVx{coZi_~hSO$6vSlosIuD)d^eMMjCQiH+Z7 zv;5H~q$|k%LOJ1xHd9iL^a_{;DU^e!i0I;sb2-s7q2JaVW>SOOToWJbNNK$VVUAn7L zEvU!P++#d(64fYRL;ST?{LD{kHls68if!@LRAvxQ*Z8mgPu6s_at3RBYTUmKn-PT8 zp-U0oqoMHzxW|Ad)(hbhnPe{pQ*2~F{5;OihTS8IV7*W`^<82$91V;cXjxkL+Z(p+J zYpN^Jp7GHA$MWxAxmuxzVq$j@QhHp5TBxAJeHKG%6|TLsj93nb52>ujVkA#Vo>>El zs8peIccM7ux7q)Ig zraAKgG)jrmD-!7Zxfky!bqUHBE8kn#j!+0ch?QzG2 zP!g5944Y`B^8Mg^^hM5t=*p4)tEZ?7MB`_90Ohh!*sfyi#UNNVPtYP#xpuBY0G)1E zZH=%?x&&KO7!8#zu2T3U)K~KUNbwQ_u83s=#xuwfdyH~!BakQSSofHb?Rk3W@a_f)Bk?k%+bNH}d-4 zrz)HS`O`w(AOYb%rn{Ee;3~eTVYUGQ`hjUlDnM16fR|wJa$TeS9a3*>S|}8T8Rl?M zk6g_}smmTk=*sRs0R+9nh*|?g!2^Q3g5p5UxF}(9r9`~V=v3^r_MY&Sqi2t zI70lP+MK?}udP4O^CM1Jrm!d6+w}Tab+o$v0RKt5H|~Gzjl@e(uC$@p`05oPgh5r@5KO-(y>; z{d(Q!n~z_OOmir8OOecuk;8&o&qikaOAyqrd7y#N%^cZ`$*sf#YdXyb0g&}_G|M|a zLI*h_%G_>-KKCSj2!ZhtNAyu9JDjLjK*uO2CR$ZSzjAUL616c%(<19a4a8}&_cE}^ z_yiZaiN1(B5zfwd#aF3+gkcoz-ZCx^clh?hbQH!z_PomLQHlb#O8YeseJC0qe7tfw zUOG7${!%xHHIxD<6DhCJ#c=`qVfwic8sm|3Qy;ZMo=2!VEd1w$LN*a1=)hj{z?GR6 z6`CZ>C~B`KQXd6()H;D>h3^F7Qs%G(*?Ej|N~{taj;a0Q%7fJzv1BnGTnpTtkaDi$x9^Dx?u)3EGTdk-!vaU-TZdkpRW#*b!Ez)kH->D;FoLZWtv#r z{VcpNJJ__S6H{l+QWxUn>-Hzl6vS;`d(&c~C3plF*8XyyBw&P_Ezz-Px}NDrF+sf7 z3y9Mx(WEclo56AnHxOP<;h2y_P`6S4^_y7`aZ1wHzfZHJKWXvh%8`4uZ(Ih<5+ZGi zO#VY2u5a_|&r?>WK3IzL0yUEY$^u75Ldy+hZ^^9?A z%p|oC>`D1C-%)qr&g|xD;9U@HOpRTrloaNlvghZOiI1sw`+dh=qmK#16$V8hk4L(! zZK3dX#H9C&yIiJFge_NW2VVew+ipjuG<5C|0S+1~jUoy`x`Bast7i}5=#dmZt{xDF zG|b-ijMH;T8p7M)q)rb=Qks2_Z~;sx!ijuJJ;{mk0Di)XqoEVs{S$sV22P!_nHmhD z59M=7^{Cb9M#Jf5V*_H=D@vnG5dh1-74orVu!XY%z2AS!Z!c=s?&My79&B2PwSt~_IIcfa zeMK7;4LXio=8Zq)3o&Rb&HE8Wt>n%0BA8I;YD};%tDS32<1lS^76e4U2k_TYb?5nI zP!`qn&W%fA8Oh$)sH=*Fk+=BdEauU1Un6uImC|LO;q#gi#H5t_NgEM(jRD09mqG7Y zUqeaow`p7>xp66ugAnmO^*m#{lD>S(%rF4c9z`pJbX@DK(iKuHzUua-rOqV7m9{72 z#+TOzo2IUka_yPh?h3dE8l)1ipxAEIfn5u)itBLiIN}vd2FfImsgC@U=lnBSkcDc& zYPXbm*&p!SQTwIrS2?4xk%KMBf7|?F1r#MV(TOGDxs1W}+m4%f6#@)`lJyDS2h0c> zHj?+Ss0!|ovJqFQ)M>_C92WVvfmr-I$;N7gZgS~7Z16>fNM{p(on-FxL=tBap1lnG zMob9a$@z+SnuBUBYxeY0yqVI^ehm!Lgd4}ZeE(A?;M!`GA$}#Zd9=4nP8t z;^Vs6{^(RM=0l_&vCc0R6@>x`Wmt%Vp#+7YplkMA>K+z=p-bz(gD=7`&9*pec53X= zM-e&Lj{k~8`>l*buHtC0MXvg|`m#E6Ju>#!j}}ILSFibGbo(5Eieq}$jv|671n#|J z(J7oo$P0RR{2`UQ8(Rs^JxqVFHFa|o*JDBuD}LGqN-E*#ZRHkK7eu;bq+4JBHs15HO)(vZVC;p8=K3jEB3nq#| zh50kmqke!8cAoj5@QFk8#KR)zt1+aoyF3zY>LHnI0E9G}zztv>+XyUP3^M1I!kYeg zD0l>WEI!}=LW_etB8PgX!`p}2|5rTE8b~$>BnkAZzYhrUTO@6tCnrJ`e3iXC8K)P? zbFMbRXVw#OZAGoR|ixCr>-=9rzlYb^USO=75&tiP{-wX*8(Y$4h z6yyC=QY!`5XD}S*o7Ddo1I#Ob+rceSUo67yCe)q7&_#UxV}D9)y&I&rL5@=8s8{Rz z;qKm-NK*kkg-J4X=_X`2HgtmlG^O}=+)YYj*ksWs;YM<6Y@ggwQ<6|R=Y2zfP~if8 zO?l7F0Z9Fn{!ZeOCkR3Y_}dNBw%MtY#uRI?$*5`i*mqWdl+dYa*-!=W58>!toL#QJ zZmP>juq0q$MNY?v+#O1NC{}mflv7FAqIMf(-*ta8vXLnxKkL&Rk*Q#8r$sR`$*B-nE) zBn@*=YBHp~2>v|OQPNfb)h75Ty@>8Uk{HOD>HL~&)w%^Y08*E&zUN|~XK$5z#NL?0 zt8fjn{wVkHHHDHW658ZY?mx~Z5h)^UAxv?dBrtGhVK4-Fm=Yx7tX+B|Z-SB91ScpY zq9Z`-ZH0rD^6ZDU<3Qvy(kYAs@A5tZeS#Q!VF&u;1}rUxXtLAfI_j7{Re~$412So3 zx&Nct$)BB%kRG=z-HJ&W$?`2bITM!PtBf*n^aTR2Q;z5&yhcKj~Nz@lNjLC`o_all7jm%@e7-T63!zFnvz2) zbvHvb^$a5PYFenbCe`DWp#8E2?=v}Z`UXEjL;Iv-6;R2;pr2xt`11z4>nZU;foleY z%sveh&h-kqckG!{zavl5AiwaS8023Z#9!HSSjl4LHme5Cq)#PW9})pYG63hZ(HgW$ zZo!Kku!TS%i5W0eK=Y9lpUCbd6SullaAvVcLJg#qNL9-?E@7`fJjPsVB=I0~>aGw!DC7m~4Qtx8+b^6dp#xV4)COJV8*c)A6Cp zei80Q@f-e{Y(Yg@rG}?FwvcKqy=)0T`zSO4sWZ}VU6IeJ%lx>F_ z6y%gCsT9EK`)SUi%HV;D!rw(6Z|$HLW^s}my>SR?KZtXLGfjV3-sO?-Rf67nR${!O zTMELD!V*DbuOqldG2j-KSC_E^e&b$5*q*wKE33 zdpa*kZ;_U<-=nMH_@|T*bDaB^`Ah(M%|f&y)56z-BApqBS5Dl|om6z7&s|8H-pb!C z7%i9g_X`w+S5U!ALWS#)aQ@Z3Yrp}gveaA87l+=s%rAl6?07yTTZWqUZ1juA9>6y1 z*5`iqs3BidUj4{W3s&!dk@Zz*gpCX8U}zNT#r~uM+2nB`gp~Jp!gX_YzrG4Y%CqKN zIO`>4H-!Fti6}V3yHHa&#hVd4dQU3D7MfLk--W4~Nlo9;>|d7AAdQKK@~~bvkgDv* z&%=hEk2`ZoFu_}mxljLklK?vE0U$GcrWeBNLzl!f9yZEx<1{Gw(PO;%($6z6{z_s( z2z#cBAP673R2(d-P4EWgbRyl->?qr7y4Yc)S&6dG4HUsnjMnGd|AZ0e04Cokez9Z| z_pbV@Kzty57l-K*HinCQgxNZ`=@qv7#~2{SyVeXghi==#NN}`L7KXEKEUrOSu9O^8 z&TxOJTIu+PM`TO4(-wj~<*a>0FxX`<;=cLhE)hr*^6{H~cgaMXxO%(no0-}lCA6$t^N|FmEV6)y9^K6nf^xE$vE_2P8>xrc%%LeqBJ z%gPguC@dkr-msSf>nqVJ^PM%kznpShj!z*MSR2tLrif!y|K4ro$B=YNqE@=e1cb$D z!?n$pWuSb(vTo6;__`J#Uaik~nBQjxuC(?VzxR4=ePNk05mf#|KckARm578$&Lz}s z5Sb$LlDJ>Z;1j2yZQ zs7#VQPK+B=YL8=-6H+b{(`rX zvsWdPb$a%luPh0LF7CZ{Gk7t1t@=ZZH>)AQElz)t^U~*L+Xqjm%&4+9Kl727tQAj7 zGO$IA4E6=VzKdEj-Bj-G6*jbABp=FIc&D;9KbDFB>zEQt9Pc&TnH&1GDlR2WCsMP% zWIa50M@N)^EMR}1m^Rc69lV|T6R4oHjL0)j z{&L~Z&nFGWeML9?<{1Q&v#TDZ8d#iW!&sbZ@enZibNAP@Hg512r|V~H8RxEOwW(>V z2!Rmoc~0U8gzrhE5x+|&bA+7RdJz|27b&^q9GAPc#)bM@$}<`=D4>l#CA6QbuiEw} z{weSc(L&?)bJB8q_~?D0=*i}Kh7Z})|CX8*X8un7_vtdMlhNjOCj9$aTUF-|Pp$1Y zH+X{gj3o7K47vBojPymev1+P#MO1sI&nxW@Y~p#U4;AW7@@dk`oL-i%q@6b-;LV;d zGoGhTN6bu@qqJzcX6(IB#-193LxC|DMAUFg?7d~zQVeOWhKxb(MAkss{s=R^dSi3> zYWhOK)$vyLX2*&B+q?lpHRUZ+W7EjTG#NMX^qzA*l3IlykNwZP13#ON>ffSxZ|J5x zJ+(t_j8pxNLvo5m=xAwa(_z2ilbuX&J=K!1oiSIpl=GxPb0CO(4(EqDhuf>Gf8A(x z_W{jl+72t2nvgkV)8dajyr6m%NskWcz=7lro-DdZ&AqzUwmMll@{B_%bNuYL^ev{R zNpt6ue6bh?|MurO$bIvNYa*_BN5Cg2qbL2;@2$0xd6}C}1lFp`57UMvtRX~iqMqig z$%!q5H*bkJ@J4f065&SNEO@lU%sy8;%j-m$ds}~Iq$B6?mdr3cl#~6e3Y_#jsqfCK zq(q7ZStoz=uTR6P^~4dhwKA1({xc0aS!O9V+jFUQbfc9O{+PzFm|iZ>d$73nyyX30 z+myqM2Dv4NY@G^xvci0W<`-Y$oCeD&Kps#~7xkyKPH|hknCBZ?u37c$uzuEMY#s8) z;6#Bhd=*^m?CfV+S41aMx7JmsukQ!wcqhVc9s&*ub4fj<&t)R z0!oHExmEYmTN%h6dF#{k>3Nd)?!!LsQ2s2Z*0wUJscj+3^?-R4*oCMqb?+m~5V1ePcWdkF)&lb(e)Sa9mfEhY&G?bqJBk^4Ru2vT$+tsU zHpY?6vlQ?0vN|K(2MeN)KEUe47Zb-9ibYK3!Sg!(LaXnW6L`<4V+ECaq{S%2K!$z^ z&b$%Nt5j3R#>O6l<*oqNKhLhYFEy6qClQ$tl)8*Ns_s_i^ec0^SzqUGmUf2Lu)5AL z=d?~1OXLY?*SwdWk*`IQ8DT3Mjaxh@np}tKE66E5v3x~(QIs+ysap&taw^m;PG~dZ zRhPjsuTc9jR%>i+WMs6d2JVa|!tvUVGkIRrf3POM{Sk1k{t7)=9%EbZu+5JAibUxN zW+pLdOnylt;gP$8od~taqt;oomhlf&+-Z5uZ~f8ZH*~amELL4KhoRmNc97$xrnXTH zS7rn9(v0oS<8nTw>^3bl_uD)2Le^U{+R}E(Vdxm*EaC_P<}|q*DK_+2utB=h{HQ!E z<4PXkjkOYK%M87CwYO#9R3pYr`Y2~O(3pvT|aaTGv^PkOl57AcZs>Mr38cvZu?d6Vhk+Ki_R`OXz@fia@xI?Y*H}vtrL=DPM^?D}imI(X|Db8FbKeoSc16mXP{MK36BQuqakM^ePCSuZRfYg2bUXqXTYXmmt$Fi{fqb0iM-Dhp9kxgv~cU3 z+U5+tIUf~2lM8EuemyF^hlwGxsx`gixI;us%y*SDcSfJ72il`}BHfe7dnrG;BPXe8 ztB?ntyV2~5Jmo_!R~of(+lNHN1D^!FcTZc=43vtgaTSFDhC#0z+aEaz88bZet&!&< zz-g-~NFZM&V_BW86}j$zp6|ByD<73L|3OoR`{Nt^<>HTOob$zw9%Il8 zQDNnqr62Yu(zl!y&6%f9wb6XyjVILJXe;juH~gIGEX9S-iSi(g&@BkMD>y4s#e0?R zOexzR=zYfu*{>ZtLQ0{+({f0YN@E9`reGEL0J=W9Hb0KrqQrEHRyQM8C z(!}S?*4VFl(-=5iz$vrqU=PmjN>aSX_TE%1NmOLDJkOhf{$&2uX-wT6va|M>dNWy8 zk&qPYR!BB|@!R_=fxYiN!FH&Fn#{xxW-UO~(3bgOczs$aNg!91Q56se1BLU`1IObtECS*#zT|{ z%5AWqsRCXU`Kn62SoLK}|99xYT6uI%9NZDzrZ2&LP*6~4`SxiIVQd5U`6%P5+76*v zAEk)(Y7WT6tX=0kcd>4sZbohUnG?4WT-p=)Ra9zuo;U>*2%hLdad`VgR1JZ?P`-{!}zazu2 z>^T`24H(mPOQ5p`HHr3LozZ?6X47zveuAf91v7M`=_?}YeYN1+84Bx;=i3F=b`0{L z(3{1`O&&cYm0AdN!R1hIZ|SqU_Q}*kz)k&}2%@yK*EwdQ7I{imsKERE&sgR>5Gwrq zN*2v4qV75ME7e+$p_2Z6Q()lLMqfRo1^KX^qe!P1(G zF|q;-4Sz<3)FtQr#h!rpe;dqDwkG46b!$^o-z)vF?r+dH8GSQSCI^|D^&D${oi23*P-X zP~3*zkz73gjUhu`dTpS8k|-8dqv7k>#ZA9?J}MS-b2c(D!X@XtO=haz_e3&o>yglv z+dZMnYdYVi50Robs-LCz^XixkC`@GJGd;ACrRY zKtd8qotAw8MKwYmNHF$z)}URh-r=!>1HC^32i zev{XB=_B{8vcIV#h0VWS7%*aOXD`fMt;glaB(h5F$Gq)#4=Z~DaA`NlAaKmR7a@em z9_$Z(g^GF;36QLBpt7N*aUT0KY1d!4GxXq6~zrV;a88C%JrUF)mRI zYI<1aDVEio1Cr0=Lf-&J22ViFiC8_O*BkrK0QK{)aVY+ttlaCP*GrN6Y{INcwqIa-KP zF`J&#d<3azN^9JnMSDN80iM+VH_jezC!)PBPrgMHL2cwZpU&T0{P1ipyw@*gZFn`S z9C5#`JIa~Wwl_`3+LGILXWsZq4T0?b%k=`)qvuRf+ywOQz7VGV4CU=Powr(Uscl7= zD?mNlegy_HJ;HyVwtxK(Y0^{Dw?Rw4ZKOmBqdWHE z64!d_G;e*RMSWrKHWE``qA=v|TrxD!hD>#B5sMvCn^vs3mbX|nsTV50WQ?SbNRY)|Gu5VsvWKvDbFW4Jv zN59*Sp}imFkL$lNzn|@TKku#)$&>DV3>L7~F8N!p{qyf!kic~Fy_~gu*8PXW?gX+| z=BNM-dkIKfqJ7CUbd zC`LbcIQ0#v7AqjHxRJm88N6+Pc$0mIS`|_xeDr+i{ZvkFlfnJ}?KNQ9iIhBZUdSq* zufL*w`_x`|uj;u(m*%g$PPu!ZLED%=L*a)}Wg|AHuIt%;|Fag^c-mX&pd1vYtlxTa zDkcP{o1-Rl_xyX2ibm&_jOS2xx~2TAEA)oIp+_EN%}LVL{No? z`T!qQH{>w`axBK!7wZnY=N-GYW8zY;N&VYEeOJNT4H+EZ-aETZ^|M2s zmu4qj-ox*hR#;pbveemfGiF|!s{Tf&HR_?ZB-U6~h6mE>=})D*$;K%XCVpfxe1~6U z>AlD?j7g&;QuWB&h>u6nCK?}VE=y@NbgnvKCl0?dqfAbgaL8>Jg4!*{kE5KdecJS(RkcY;*kf7(3}aE`>D_MA&oUaLG^y-=wz z)h^}yYPI-dbuoc%>@!Nq=fMe8OE=#r>AKO_UKv;!`H*jt69{6Dk1_H1J0F86v%R0L z=Ik&ag^prY@+B__g8%CzXf|K?x|iBU4{l0uk-5RjuoN?_2aI^rW50FXn{wc#pJv-s za#3!UH}2>?&xXGjM->_9tgvIgJt7i1HKTO)_dEZBOPIF=);X$ z$JmR9UN7Hil{eu13?vqgTSXp&A>eM-=S|kO6^54}>i->^{5khL-j_M#tt;1%7xMA7 zYtPvZnJc?lUIfsilB0-4>QkX5n96zjt3P%9fqC0&a~4zrv1a`J3dbkVk82VrH;wxG zg@yg^^j#L2RK4FTBn5=EsqyX(Kl)SLm*G;Hn$!~Ig0$Z3B=r7oCxlt~DRHJ=lTrBK zBP^M|=he(oXT*V|I=LB|*R=QAEQ4)YP0bG~v{ zw+c}(E`d5l|_o26ni?}iTcSk7taF0ABABki=j`rEfZrR_8)?#}D(<5?I+Ck}}Gs$9L`^h0wh zSa`dgEu*_GbO?kGE#2};E(~wiE)H;;cBhWJyIl#J(Brxd^lB#oCd;=QKUD-OwP3j;CTMCfA zml#KaZc;1hwzqRGPKFA2{*ahqU;FHXbH2Xg# zZ^mt2@vyS5LFgbUtaIKA#Yydt-;=asJ((egfH^)tXm?((U*c}e1fd@YECl}f=#6@q z?RP%h6yiXAr{gu4bN%H&aiW@Nef*DYEiX60W+v-Z)%99@Do3tR0~$xElU;W3Tq}Bg zBh4@_g*Q~WMwuV4WQ%gVi4kn-7_;hmSof4c?{B9f6(!SRbc)mU8E-Y$u;9qgztzSa zt%i%w&COX%9aHu#G~CYGMGn8v=GJ_&Z0a^_kc&fOFP|0$u`+%wS*4dXB#HHh6@9KM z`m*tVSUT^3r1$Ut*X?Gvq26wJOLNq<+(~IJT#cJsDHoX{b<@al5?;m}>zrXm0KY;h)oacF*=Xt%J6Naz%TnPEPk1JfM z{Ub<&D&_q16nezH;83_z$cq>Bbo(k5YKznhf0yc(Rrw=%rMMI>xyjzCZTqM~b6#Ua zw($HraFWO3&`f#gA7P&>WX*HtUHuBJc1n)9Gvn1e0YtqyX6RhOf~crS*q9B&>w*xoqM1OZvU!4 zRpASjtH6V~g?tetvwbe8!yQTlI!{b6&7%M7)S>? zg!EOd!2Y%8Pcr^{k88I;hDo=a~c>ZL1&tW_=e+XX;&{5gJ z=68O?fyaZ|h~mGC77pzi2?yt4Jgy!|*wZk!%p8_EBwxPuNKh0RKoUzD*+Z%(s?o%K zI;rVSKM8AA4bZB6s!xBuy3n@sPx)h(8P7A-_E;Z=j6!$W%c*I9KAAEuTwz&aij0!RMM_?I!?E}q}@Xco+Z1`!Mf@3KI+}Qp_isQ z^b}l)BEIuBXE!771+E5nFd$*#TDkb*%SqQ=mIi;DvL)@5Kol9N$?#DC*gdA1`C(ImCgOl(?EcL#P)&s-g0=(@``efE|XD9xu*@>YbP%*j~LA*gkP_W zGP!XP7PJgxmV@JcOrvi0jmLXu)Jdj7zy@NSkCnXOHmJbxGCah#`qAz3l5eyuxu#mA z6mahh#8+c@oh>NN^coB9tSzod1wa;lY0vpn;9>+fKeE*nHS#x1Ig8l&oPoc$h2;ZY zV%+y<)=>nl`m}(aB99n5%zj|-gf05rq0CFcukzkuwkcAUhpV}NWcp_oNm-oYj;&j= zZfAen02nRUOy7zeRT10H29CT~=PIM{^CYvu{~b;S2`RDY>$*j5io9&`jO-v}8OU0H z_nIPAJHY|;VXlRsCPSspTfxAI6f>!X)@+piTRUgN7-qnIfp zWwr8HsIIrkk=c`f*$z@k2$##8jUM+4VoQU_{qJ|=eG?yOu~W97F#}7f3Ec;|@~FGT z&C6C9)60nTU5@0UOZ}D4a?m~vitG-;`sF8vJiZ%shN%O}y4(pObh{r=)(x(Cy>L0LTy!dze`cHP?fFb6 zQyNjca;haS>S1-3X*XaBc^>h5>L0-nvWGln$Lt!Dg9Whuiocu_WsgQNOa$q0+F`v3{He- zY}Iqj3-E5T7j598>7*57m!@%s9J{ko72`dbsi_({-~VJx$6xC(jU*<%%m&?+5wb z)>qXJFh2oYvXpn#=?X!?dKGvo&5?o*joxA+Qw6V=ekC3_vuasY#26TQqA+d0{tf(g z{yNxr65uu_MV~0g6rNVX1r=e}?^o>xS+;@h*Pjly-}O7cgZcUHEe7l&ALvRl7a_== zB48TmU4lzeOLf?lj*-8fu~?!FKL6om$N4uhW-5u7c+O;=VL{MLs$S}`$pT3wv<(h! zdo?!kGAf3k#7By-M{8nBT0|8u@by zfA*PbJ5%TYHCtZ>^;riR{wJTM?KZZ#E>%Y33f;gCmIe+Z!F4jxS-;bo1LCCZYR+cK z9n;dm3_dc_5vDq15Vf@-DZ%?9Z8IG8WE1W!W|+$aX&5v8K%IcX7Se(&&9w~BY`hV% zt9`9m#5Eau5gYkNrs3g4h*rbOQ?kvP(?jWUMkiJb(bw^T=5`}9fZfO;S5f4Jh0cT$ zC#y(@^~vgmTwm`s7}|Hwl{C#kQxon6GVaQDIW7doc1O>hR09f)Hb`A3YSQyXh=!;x z5=6Wq+USoghXygPq9lcL^kD$7G*e_#*w&1!Ur+EQkb5xtZ%M$2T%x1&vOCGQpKLh@ z^TkZl@8Ei8ueoWH{B2&Ia!Zpxp03Df_LWO({Yif3A5*#vM9uoPl3;>xlip?uyO zRpG!Afy@(-5MWbZ@S+OLi5|)ZxcSL&ncuH~+}yA4eOrNOynxB$0V-K;S`0=zu(_UK z`Yea1JN*=Aifm}bGOiMj0OC7Kx>nJ>;!J$ap2Kn5*u>|A)|+E4QHo2hM>pDZ)45-` zUlZ6*B!kAsOZ)n%Xi5j|(MVzC44{uNM8bGVY{pf+QU_RmUk3Y53`2L|4UH@?Tlm_%*AUcNd*F z^#}T5+0=!0T6ySu&MS7(5~es97m=CesV;1A*alB&-TCm4)BPqAU9_5}bQN2C!VY!@ zHWw$Y#)jP85y5uT(qE?TIqn-YwqseZ2Y95w(V@7iU)}RNE3C2L1IMU0BnK}7Uep&y zrSK^hbF~xB!K_SS>VYbGHWnHb5tE*q7|-CWqdu)57Lr-n%9>f*1ZE;6fhx%imSkt}<*c zuNou5e!sG@o=;0vgIodO={`)(Q1?1^!>f}-Gx{aO28%RJr|LcK@(xGsXkneS5}`Z} zkFDkT4+Usv<`9X3yXU=QzOqnt*+8Qh&;ZcUWdK%}2ar;Me--Zh1Hk?U8FdoE+M+h^ zrnm04=%C(P1pkYKLK!Cq}2bsbYQxw8ZAFMmY)4631_{9f_$cvlN@y29-GtYSEi4>{MLJ*TJVr|eHsrWgu=r3(!h0iknr<%&|)oQ`iy@)EoR?6onv zGUsLIn>V@9;2bD(ya=Z<01719lJd%O+pskhU5n1F9)gI12gJiri#dg1GsF@h?^EEV zs!!7{&V%apYnU{Od-NZDXDOKobGBaj8X$SY%^Cqf1>$8_?;8Nzme3uYvcdKL4!S?| zGVR1}8p475c?c}+ku7(!b(7oPm~wcOu{ zH95<~`(rOqgNnsHHq!A0`lJU9f8EMGFjW!x#I}Pc{nnL(IrVbVL0U8DLB&6~1K)_I z@+Oyr6qpW-*?J?gFgRd^7>&Xe5nEmIWg6qc4TxR6=h3?!z2T5T!# zl&}>3z}n&qv-Smj3$v7O+Q*z4%7e0E*GnCf_TsWxa}8m-YaL-M^OW zvl=y5gU=ROwgZoX&b}TT>Lh|eCYP2pfchosZ4oQJx-2s>GRtP&oAT*$$_jyA5^F1j zAmNeiD=fps>grZ3?sVf%;rvyL4ul#&VpC+~K{=(oSJ~xmm^J_Cd!8DD1M(usnqx%( zjDc212(W=U9fFrJ!O6onPhqZ20aR?@URM?T#6RYBjglS+vBad^=3}z~E&8Tz`JG|n zzI@3(H$1N>4?ENhJS&-Z-70lqbODj>W)>{~rcS8;1e{Y~>~?c*M*u-_Jp5JX_bH-E zzX?Xps=+Q8h31UY>&)=KDlZ=}mF|6$IQ^0TfdN!T z&C*AU?9>KLeVLPz&n2O?`*w;WTvS)+G9d%mcHte|_1u z;04n(QoE_0#zOc~due&$vsalidH0{y{6Uw|E~48dl|Wy1T&!udVZTW4otZ6##yODg zIk*stoB1N*z9d#J^>gRhOk2k1RHYyC{!3Vd`7Z~qvGEkc8j?UVTe2-|@d1*}R012tcC-*NPKtSG9IzST?~>&1Ncn<5bLqj{ zUr#C@Doj^?hYjBmy~!cQizv-Y4`zRnUf3%$pDOUTM&*$yfIe>NdyOlpCrsSKiVVQU z_&Fd)k-BjA^A{mVS<_-hz5nVx5w?O_u4BAp^uAtMh4BfHBMxOw{!Jj2Z58ZN`M1$+ ziva#P2kig#RX0K;xe0_Fs_$|2d{-Gw5!W^YrYh7%rz89%>um~d@4{n5(yVz&46%$_ zFQN==V4t9qMF?*p5PXH8F6J81(^GX{DkJ|LeMUEvy?4?jQay}$s&?_3uFf%tdRyTc z@*)qR99ZB|M2qNMIRtqwD|O!avRUx4uq4?>(s$i8N{#>W^PpCy+a`T;l2Rut)^wa6 zoOO`HFMn(fXP-BL?cqeK>%-@tl>b+#f7SPUXNL6Q6eDWO{V~CfsRzt*YcWKw*wiAv zgB#2rEDydIkH>2kO0+2GSZ9P3AS?xYj~HoMV9%~s$$@(9 zKtcJ@q2{o;y62!a7G)ca^a`GNHCgJB78&PxKEmW=Q^cmV0D@VVnyZQFCR*)p?6kTH40N)}nDa~NECA7s9 zl?}I^T$hv(0ew9OS5wYC9x}KhFNRlMGvRz+aHb3QAsCu#M5ZdhyXi7At>im_C<81z zfedI-|1y#ftc!@|wPiny?yv1$Yr+B7^K4FvUH+y3w}}8wC8SYLcdj|QfL7ZM=v(&H z$1r=hu$|hyky_#{RA;(BvRKBJLv}JtEaxh$yyUYDZ6g}fdXQ+p{k9$dR-6Dpu+(Ai z;$d><2!VZ|<=n7FV=Xzi zfhbkS?Ly|SdJ%M0HcmKZ#@r2Cq8uMwU65%&)hMDVeIX(`RY5uJ#9$3~c;SNY<+?u@Mzrt9DjWc78Cb}n zJN=@iFvB=E#eR zi76!%PKP=WM8_7j$QM+#&3))Usi^!Q<}qMviPw$(3mVcLv&vi>6JpE4g0WOSW98z| zT+pwlg#{Y>f%+IV0Gq`u5ztW?@$s??>3N^}(mN`j0E@@GjC6_ardKy4f^vJH8mcS1 zcHGm=ygBl%Hi&_&AQUhU0;Ap|$@IYg@imInjCO7iY&P8xR%0!oBVaLn2qUU(CdX(? z0z}|x->+EVdVTGM>-YQ2w=PLg3W3~UEmj}f4Bf{rA&uO2iS&;1GWTYB*}hqqEXrU4 zd+~@ljZ78fkL4dEBAAAxhy3U5LHVdC=LDoL@OJjvug$cEmk#|#)2=R_=ca0r`;K#R z<1O>!c43Ln-hSRbu>WK>bijdWx+w>Js07ML^7Ix~1TrqapFX8>%+mWTdg@~4xfV3* zn;Ba5>CzD_>SeJ@7FyX&KW&4tK9<>(j7j=jgX zX7gonEo#FH?%$S7kX<34{=Q(!iMUWZijsDUTi(JOPT3}zVGr`9UQbFC<7uF-U?f_m znyQ>bh6;|9Lo`XBgBII!6pwTFA;b;rC ztwnE3BkZJxE=2-fJ;g4`n{a)qHZ0&!VMqu%g^%qkUJ%z?%zrSKkc1j{YqjC4F`o={ z0+M{^t6R_XnG4hAwKJ#W#S+%#C!os|UN>>oUTJ0VJ$<1gzu8IR5-V)IFP+>cE)Rh_)zO%z#kscrmt=uF@h(aM4@;Zz{dZ??8NUM&)qlH>i8r=5Mpb! zPohWFlvOr_*hW)L7(L~4z|~KuN{3Z??Kkg>wZHEpmU*#isE#0_xfmX-Gyy0BATy{~ zy$LOUHSV{Xng6`{cKO<^6M^?R=_$)*=>WbO7vGWpnhu|D0VOK3o^w%xk#N2TYf;`KQH(_~fVvI8d(%;7eJ+{Sp z#|5rLod{>@3d5W(!q^wOl?PZ87GJ2kVLMQ7!0mDtWr538&Ppv@EqIeaWrDH=oV#Hl zBnU}*?^1%~JJwJ5hC~OlDDHllNp6V!!}swUzd;`;E&~=z7h$t8(P812kh;-aPv++N z7z+rD;wQqgT=oduMacE1VP$fLO`VsSlFD-M7spQrWK~+>SK_ru=z42#VS{oCH-4c8 zjL45ULWuZ*;hWD#6?Cl&0Nqjwsv>%3bIM?74-F?KtX+=2`=E{Xrl|x!v89WQqOAko z5~^W2V~Hr~9qSsW7;|G)ejAQZxN_1Nts}&)17U`kkMF>bx&C!&2&X&Op@6-}O=x8{-nICXUtX}srnLpFuGpM1=3kt=K3xDwXq+FUb zRC|4tVewrsHnyi&RoJLMv1}qSkLL)Yt2}QU)H&~NVLj1xYckrr!0toI^uNw=C+fC6 z+IX4JkT7aDD)Suc@7aQ%ANXIgU4iIp#1viov@APDI zacFzr4DhS;Jb$z`ebtyf+I+WPVB`1j;njSEd`gDG1w5YtR~(?TB=Wzbq`XBRp>!hxy1 zedo2cQuD}MLG!Ht(+tR53(YjMMP9_@{*sQ_fCXn-N6ausD)@~%hiK+Xt@O#Wgc8(& zj!BMp+lH^hK7)IhKxw^s=k(=HUfw`vSjkGd1Fw-! zT!zzycHrximI3(Si0nC|n%;c>ywi1KVM%-5V`B2um7x_(p%|MBbX^)O9sUHMq6^7i zR`MgfWQIum&!UrgYRP(mL$D)IC<<+!6~^;RTS+*yTgE$_RU@Zp>^qXUVIv|PgjE05 zs(qH^N@i{xAQ`XLmqeN8c4R6v!6LdI$hT23@Ba{#nyMOxk@tP;waff#Wx8wNn7&=? zGC8UEkc)6>AHlE4n3c$q+~{Ru^4j$#dRKz}Uq7E7GjRq#y7(59A##&fynDeo>+{W8{k>gIKRyaA zT31@Qz9torR+%e20O*6tQ}RU{qVi=)a)7j8>AR-7QA&qT-`THo69n|Sf3$UKYw<|H zlTM2%Qqv_#jPGM%^3K`L*5#y=_3_MRpip2c$ppYT5#k!eWn((MZ-&2x5s=Cz_buwb zGcxbZJ5kjSFxC_@Ac{_U5ylX0YOa_tDn>~eGDAcfIRoFp;W#s^ zcG6v_6a$XZ0^;Vb^UPPPg0SV&LiLA7a2ii<#yqbCj$yU z6$PxME&11c=35OOoI)qqWGe1G&tJwH#duI96{#SEov9V(9x?9^^&0zXr~lh27wAM$ z`_UGGeRcU+$D)PJx8J;NiJg|2`zMmq_-5DY3idIN{kM2WFmL0|EAZ_|qX^D;8@?Dp ztBcR-%1%|FV~Ib?(w40*7;Sw)zQiNz(JN~7ted$FVrhSLuZd$juhQ{o9Mj@BhaZ7n zGV&d$oThLe+DIF=vCSMB^2^?TB+Nh7290Dn*P(^=3k8vC+v`?7cfVPjV|{6;RFn{q zo9T(l62T{&inI-ndJI(UVW_iiG)R7EHs!Ye5o6OX^t*cv)*kCiOzsgnj`I13rAf^w z17jM?AGj;a>Zifc6aKE^@Vp4`#$*SiEWx7%$Z*=^t-0AD_&}tXabDtg_!fpd<7Tew zggp!nPv%=v7b6GkKiuvl8dq-uk= zU5cgT`#eg52&*i^MjXK3<+md~{2fqrZ?1E#aT+kWsWV7ueh-^P{IL)KR36G@&1(Ol zQHgK#-GWE5KT&}-5n|8T;jrChiL4bdUee$o%c@}+mh*FJ(LJAkCHT z!(Z8E1aC|7fk^k@O}i@$)2R_IAdz4s0Nw&z&DM;eL+?`l(5cTzIVBUb+8tlwFe3ha zD5_%T41GaOC`Y>FEWBFOYP6a9C?dyr+$MK)S0`(k+r-_}ac$sZcQqKBJ+uCb*;|W(WQN`3C@{@w;Og4nFKlWDY?UA zN&#ulZ`Kk?5v^-)pkG^{M3#XD?uE zl%j1Zuv`DR8Wlt(g|WSs;u6`Td4-dqgD>GJC76jHjjN^xN4;=5HGRf*EQ22H6%T3G z?c3R4S=IrUvBSjSdj14ImB`6<#rv&iO!Fgz&S?=mU$$`k0CK?mnR;-iZ?IA!7%dHC zyZ4y(as~4hPGWcq7TG>W0S94uvbVo47Y4Wn`LigKwHRWNmr*r;!Ifa={x)Lf0d&6%jur`O7xn1B`C#ig0U&VzOq7IZlO#vpAde%>9gt6{buz~Cejyk z4;44OyVaKIZ@?*~F~2$~^(F8x>)t7Gm}%iY`y_@}ko`Rc5+WQlfqj239ais+&hz3o z*!?~Ci2f0u?q%^sGx&iBWs?4Vi8B0QrK6>YoI(yQ=QNRa=tdl)^LP(s+eh~~^!BPV z8b{#!B=PjZsqTrs+|WU`$$zdVf`6R%2_;S~St)BD8HHr}*--^e#rX z=nnoyYddCRyQlnCQ6?X^y1-d#@^4w2kp3NXq&;;yMBa>|EW0vXn8zP9X#XjAqikk@ z`*KV(kOsT;kTYP8EYN`Q)A`07KhIyAq~J+|_M2sKhY;V;s&Qne@MeT!6rRkNw;&gF z6_Sf~U4p?!&zBh~zqB6ho%c2vz$tWWE`Y;_iHg=j%gBSdB6tTj{4e2~?sCs)9t%5I zyy~pvAbxui#xcR3<*Y9HfdO-h-wu~>sL};+-wWu83S#mu(`J3 zfX5=f{VRZ}l?;?70mc%OWg{j;-%0=ym`aRv*a-ngKYjXy>VU#d;;@jCt})v(*O_IBt6SDtPwvwWCK&M^_2!>3GMk2REnx97zpH_@~z= z|2UO&Z|wYijPFSKp*by+=d0KR{iu)sjeI*qJ6_13W@KfDJ2h5bW+W_r{5Tc48saE1 z4GL~HjU*Qz>9jIW1)jk%xwL9m?2B88&nu^$r#e05v!lc-2&oYw@?ADkXLaQyNdX9 z>WFX}d}@FX$Cz(zw_c`3#Q`Kam3=v5N*P&`%N4iLr-j%c8t+$I%mc6TsK7k|{mlII zTG&g=%`^XL117J(R>5Lm_D$UjU9`_g7Yz%|066t>UW5g~WeZD{;2J=OUQR3#(j!bX zCewEo$btwN8wMhX;)Zek=C~xEJ!sfMSP=Zvggq=yb17cM`{PDB)?VLuV_lH z*yw>KCM{bC^E7esCi&N|rdNj4BX)CS;-y*VdR19C=?KO{ABWhpwocv~A>5v&b{Do+ zqoNmEHysDpg0`D$S%L@!#PYNaJ;&-?g?nTk$eS9T7G;~2sz2M=(Hjcu{Zijpcc2T^ z&h2oQD=^uf3nP*pt#!=)|z4Wv5_rL>vhyY zxFP3)71Mph&Dgv%4v6Q!z-i;J|NBI&nb&tLbK{y4DE9(So1AvQQwV`u%V&4|2EF2! z#k&f?m8zpO7km0cUwvV+1_P{KN5Mvmcn1M

{O-Ta*1ik5l+{@H^(K?y|B2@~HQ_ z)|V_DU)hV);kB17Lrr_--pF6o%un>%T6 zv>`Q3%b{>;6ZSrAtpPfhr&P2m7vs2VfTma25%<){S#TiW#<@d7zBbkKGISGgWH*Fg z6p^0pYYYx+nyvj#9>qjPT9_m`vHiX3lX}%g2b^;?&F7wimqPwiRsB9c?>>$M$t8j~ z55lKGEKb{nD^q#~dN1+p(;Uk}TO}cuu8P#}f%GKNh|K;k|6+J^_sr@2ZpDjBdTVTR zlB9MQ?K;iMVMstO4IAz-aCZ+9hBn^9sTEik$}stU>!D zCs3a6gYs@&*%-7v-r&Ij5~|khCbIx+WeZTLxXvf|&geNxd?*a-sP~9A&A^VU%rU?B ztJQo;Gehfi;fcBinb(de)++&6!uf{x%n@T41Ce~kUlypbbZW0RAJ_*4yA2JsCwmeT z!7376tuLVKjq7VR{Sql8!%X-3$h-wTMHV(M)rCUqdLMeO-zcLude~cHW>JURG2JTa zuy$*4mV_Z-H`OERy&2RPS!F`Pzu)Gq*YpGSSp@*&8`eqDUS!q`Y!2)j+bpa6I1AON zoHl6zK%e&hF>0B=OWlCy-Z@iwF${3*y|XrWIjm~m zlz#h&t|pHAd=r@yf(hHMa7h2IOP%rk!Fl~T;O;8F%pE`AF;(~Ha0SCY(iHpd%?h3qN= zch1{&+y>t#dXfQyJo7B^wu>W(rqy}t-EJNDuDBbBp?)#86)+bQmVcES-LlC0V#%Gc zLO7*l3`*31`bKzG{V4l$WDck+v#HQFC+abDR(y{2!HK&HITi0@x>qhL#qhL$t_!fT zlz0I1%K=3frwB+)FId=PAR|TGPxHznC#o0L3;?~>NbwNus{R1|E>J(#CsS%z@qlKKctPkGnfJ?yQQTn8T+3mbk{##QfWCwc>jCGV}(X^}je%@?Q zHiX&B#0PhK{M~eOPd54%spP4%_qB-??=RY?P(xyekcd?2X?kd)gE}e3q zgWO(HPh3@FBsJf!^XsV@{)lQe|J__aW$fn3h+ zBewSCatb1?HfnvPX)P#pxCS5a;zTcjTE45P9eWL7r^eOdm| zA}X~J`TK#aoAK1QUh>89;o(PWmr`v zpb&Nwya`k6iEccqt;7aqcYmk22u~&3#;|);MMf*t(?GV=p1XsbQ%Z6#*{8OgM3PB$ z-*Vdjb@HanqgET8_%OWmp4~_3?f?O7psZrK%G@mLx*nY@!16u^=csAvK!?wC9o|D* z(h>9iKVrhg^8T#4#OzAu&re`+Ms&#Cc*#1$b-#HDZkW>71zf6u|8--;3Cp!vUm_+F z6+CRw3>=pAC6;Z4N+#NB4YSu39`hqecK~%y2a!Y++-6_U+QUB8Qs*CO*xx;hiKC4h zc4uBfXjkn|i3Q8IT{an1?ruDHKU~V$BO*Fj&MFwiLQ09u&r)5PW4a^H+D?Bcf7C&- z>b6eRTK2r~z{ z7bPl*LJ{$4cc!M5pkw#%eN~khqa4CS8<9^oj#4D^ePCi&-d-pl9v^$}hr{wcuBPLI zZvEo1aQT5HOO6i5dN~0icG#47Bb7J@erKy-D}%nE9Bsm9Y!aXN1wz>8`4oN%V}$cQ&AoYH07>wq3`feF<{3pQUh zjc^lOogU*dY5h?XQ|fHxdl;O`L0`9Jb390{=5%qcsRaXP`g>>IDmdiah=t8& zct@{6#)z$2$7i!(j3M}Z(C2#b;kYCnS#5Nh%b}kA`vSg^)T2?pg;j&KH`MnbtZK^} z5?#i;aJq;JmTpvqM*U4nC|GmPFuinm4-m*w!9WKvOVp#Lef?bo$FePA@{U9#t zk~uuQV}ZD823=H|pwMTL<~A`Gw!UL(%fu9mA-l)9YKN_Mov;>J3JKtUL})hleb89x z(yuV-z%D}#wVm8LZb#pHfX}-q**)>R6q&7stuj>}LAL_;}r{}>w~mF74I=+nU&4vFg!7FB4W}$HP3C6wx^yojEb71OejC4o zKm(}q^bBh@rGrLxzE)9Ku%Gc}Ql2`=N$TuN70?Ay=XnD*hQYT-`mTQA-XfWHoread zX;SO@#vvmT8xGjZ+h|O3homuBL z^QPC@FEa!$MyNZ5`u}{C{hVmSpmslTq$AM<`RfQ5UvB0LsOzRvaFLV_%b~j2thVe* zdl-G8m$iJ~R=07QxD}Jc%29|EKg#k!m;}3jD^xV0&VO^&MH^0 zuFI@RmtNdG)COs)UATe`oU+BiO0fvN!NpXTEU(psdDOa-0I{H)&Xghv}0!QTOxu9tj~iRqwqi zS#lOnJ_n6SMEUR@bn>Qz_o2$qB$vzNi0C3{hnZ=Rw!J7dZ6JrtP@VxCGw{rphHZm)v1BNa&e8e9cT#3yb&0fG3M)ygY zbA&kkx#S>&KY;$^Fr~8%Qs#3;^ojdoeOXml(K#=7-0*NDW*bW;a^KtU10S>BbRw_n z@d0H#Cx}Ji#&OK!H-2G6)Wh-UA`kJ4LnrA=0N$D&JFH^zy{a(1SpvfaYq_+4-Z9Ik z5NSB_a|3*-loJ)Gyv&C%dg-bAstEwYh@3Ywj7{A@TWZ{y|N zJXS^U-)#dmncZ-CquPDC=Tsf8nXGD?=bji8b_e}Wm|eVI!L#q$kNb|>N=u{OW|$is zFiY&efhHIuIkcrpfiit`aiz3TIn_FCC1#rg*?5R z5xVy^y`dk8YT8HRIYD3}e#ifvct`ZO}KTl)n*rA}g&&v%MWi(;=NZ5;#+ZBHsYkOCpvbdVuC?;^~#eelY!)C2Yvu__p6SJYqW}0{|^-Gm-y#cpn1N{WZOVQebV^PRsWg!Mp+hpSTuO zu)1$(l1{2`T=sx4XmCGgUGI(Bm(_C*TUQEH*m!pOaS3Wj0K}!-CnL7&3lqG(#q&NJ zn&0|E3O3`In5EL{&vq30qeHc!1qmWVL9|+0dG6zLe>cv*ukd<*f8?u6?etRKaqmI3 zYkcaG+c6%B zVx4TYtaV_|`J-9-f4x*`<6>BIyI=QuRA|=h&BBE>-#@^W{?^f+DztwL?j9NJDX#3Y zjf6<=rb?)zSBkfC!k)!eG`)_sTszjd{GC~4Fn79tZ@E$Dk6=^S3ebh^wApq>(4PRi zrClo&-?e~$4y?EyU1qL9>KamWnj@~sETOljmcS2L8FNJin0`>CJY+eQCu!-!)PQtH zy`(;maX-meIT_HD-TmG0^!+nAz7?6k^(U##SO5O9M{i>jTh^oX!xD|i!bwl9)G`nJ zT5Pmu|F5-K5Hdee(RqlV4^0x8S%r$MfKcDabG_kb6_GR51wiH^RI(0z;V1|(+N-6Q zMPz5s&i$+$2FkHT?A#5u8lWtbT$gAjCdOQx5# z2s9RX8-Zf4F{m&8!8xa73kuI0P?i$|`B)1dTId4H;kQ=$q( zp~2|f1Ck!=kVReQ@<73n#!A04CzlhJ%rtf9I=4jNXtgb_zkiaWS{~*o7T>@T;Qbxf zGW^?DQvF%@*HIM#E=%jfgOM*9#ba%;r|GwMOlKXH%QCGdejcdueK7XNYZr(3Ig2={ zjF`$E-(7{D+tsW~JO)pyJ2dJiQr@k6D2eRSMwz#WFzVOYwdn8K7dvw#{5O$vRc&VZ z>^bnr&ce9Illl0g@hD}j((?L4YkXys<8vo7)v@Y`e-R6tS&nvAHD`m&YDbq)M9e6J zypGVsQ53JhH^WUsg~jQvgosxcpF#8ramVPW$+EETABxX@3E>DNL#c2p%uU#f$v2LL zIA|MA0I6%-IfD){UE=xdf_%ZwePIoB)SQ>JBl+!qohZhMzQWAO663q{^;PFCKL7Ne zf{P1g%aN-6d;1n78)6i$I7w*3p#AdY!z(|Yf=esjP4Q_wS|6BZ@${2ON85^t;u-_Qsu``w)!cnc%pFL_26$6zeUw#;4EM6@&M{??7}jTdvx6Qjo{o<_(Ug`@%cyhI)(4Scn;kxBRf#!P!)b4 zdtkx$OH_bFtMfvm0PJ4T#scf9z7RywP_Yr{Z{GGG;|DgebJu;dhPdw`#wI8FFZ7|< zHHC;g)sq0|MTys7?ogf$=@3 z4he~Gi|}HOJ+%>zBf*BacsT>5L)xKpPzt*7Qpg;&PCx3dPC-=NqVp~t>J+`ssUa9Q ze@I_`YS9kjp7w5_c@y%`hRzmh(!l$l^;cL(FZ zIF=Z=r3-uw2bZhY-!KkZ3QIbU8)h9+Uv-$%Ktw75=A#)XK9fLs1(4M}-S)U_udkdx z%DXDw`c%H$aYV|{U*3qd5{2ZbSu7jLu9rCZ(^Y(LZ+5AR-PI%JB91LHksNWNa3w5d zi4g4cOS(MiwLb%olNiX)XpTw_08lcT{J3t2YU~MYBmP9+4Vw39t(f`C7j8LQ zW4dpj#H_N~?ceV(-K>c;Dv9J79`gnFF;P~`F z$;L{mnb@lX=xWMlAEy5*ik~j~qzA@Jte|De=#=Hq;oOW3SNu0fWE^Wcvd~GMDZhXP zVWjW8KLoA*x^QODqcnoO1#B--#+7&yskX>Lfl)rK*hfgaimkvORvV*%v-N3#9!*`u z)-_%Ffpnj4gE3XYzAoS}97P#N7;5IS6Uf2%Fn+x`BzG4h_>t(=P1lsu6K-WYY?EY3 z=iv$sdLf7($Ubnx#DGEKQOF6m&lx97Y}-hJpVzK<<=rBdj`YxQ&f&?gB>k~Br|=m; zNi74XSIGW-2E-`{{jd1K>~h5Q@{J5QO4(aiS$6HSnRd|F_!5G19PP-71efpMcHf2H zg^3LIk=Q<^6`SsaZ(Zprwr^@Zg-gl15_tg$LtlCxf($P{?PxsQe2gP@V84tW?p?EC}HmJj#QQi;6O>JZ7>vJKh&Ni z?Iu}WO6z%DhVgFj{{w_-YdgJn)l2ILV8O7ne5>h0)OOPFoNO+I>L=%+XG_!&I)f4{c)X5q6q%R;@>}B-l zw5P0?;66TA?e~~`8xNbLSV|7%;sW-D4ro|PXFdb;t%TxP+fc4w!z!3c2)%FL*wlIY z!{ce=@*N3HuJ5dUY`auV3a2$nF8=7bM*0ArL?wUMK1_P-W};b|`yz~`r`9ne7lz6M z?N2*ppx2;xJ7SKZ9awZ(K3&|i-|cQ|ZT@)l9FbPS?w}P^9s(=VD@`Y-XVa4#uG)?M zRop#}<~L+Ex~o6;nR))th6KPA;g)@l^gYLg-o5kD?9P-X2~Y3nFr%q%lkR&3mdIzP z(O8g8RRKR=#DCJpLV18+5b*zxrSptxDt)`Yqoa-}Fcw6RG7cT2DAGc+%z$+1B80#Q zlR=t5=p~~Fh(eGqB}4~8Cn%jzBAtLH8cL)~AchcXC?ODXp5y<0zXOZ4PIB(+-q+s0 zjhpIZjI?$n0J!MM-#{in!aVFWfIexYV!rF3-doXmH^>e*`q@F8`R9wEJ!IGgg}FbB zdr_4rfPd8W9^>Yi^uB69>&%i|jF+i9MjmS0--|@bGxG=v00sp>j+Jzm$H^16VO4ow ziN(>;+}bgDd_PY%?h8z66Ik&GHe%}RRN$X2?X6D-NlMNeZ{;lCFw*A&oxElmlGe8M zY8A|w$AenVEYaNPy-ZjwP#a=;W4`a?y@~NLP4RNhW6R=ttmfWapIMRI%sFsLwm@h~ zthN!He9w29G+V>PjI%Mj*+YNOO93}ACFt5FkiSSFI&G{*eLnyWB8zzIT(ZCPSd(W5 z`hnQafEhz7uW!IiP z$8~)tJzr9;l+(B27z&0@A_k}DwCL!tY>59hEY(Q8*19t(3G?ahUz191Q79ip^+)Z| z6Y^8vZ=o*~hC0nbKKl1u?k$oooljL+p5CHq_jp$?BI_c-EQ(pt01;#w=!)QL z-fkB_riTGFm?YUt$DM;b#`rVTyMIrh@%XDExpNo@&}H4U7FU*A!&^ttIpy`BO?V!8 zOQmVKSV+Dp{_6y-4iwv(>laT$T&oO%RW8g-EdT_N%iU z-xkDzDz2Rw;V?j7Q#sGO z_M5tHOqYiXBDDy2n-kNL0_I*n8VIH7Fuk5_$KcmJ^hy%pI&D{ogLBU$MB)1-*GF|O z*Gs&)i-$)>>e;IeEt7{ziNaV6`>I;2lo)CC!(~HCm3tUet-lyvE-j1^e;79t*$p=| zp8YNTW5r6ur`17kcCKn*4E_vgpfMRTUIjgH^Bs9rZs@mZ;a`iokjS&%yau^GM!lP9 z(=s(JC z`L=htN(^zn=%e93C&Zdh)3%gf-q=fj6TA^TqND$CM!vjx@-YWnE)W9WeviLk2LG{qo6xg7u%h>Ey8%T5SxDmTXav9BZ|sx-DolBJKK12#MX9GhFThOkH;jVKH zyOBi-3S#XGf1FIW_zqgNyL6TG*Qtiq_Z}aHc`t$Q6>7+GLrv75CudcFS+MOJ^hO~Q zu9)zRCv09kWCJmL)Qx0WZw;xc7r3Q8#04BU#oZGJKvGrtt5;CMh4|Zj)5a**}BO;#fm<#A^246dQ zGvJrnZvp9v&I0X4MInrjzWaaw3Hostg37)#*_}FS7n&C%8WOE5-{_rlqwd`P7#j() zzub!4hw2#vxz+N9WiRHoGR4mlm0Cd?oXx(u;1Qcls{VjQ$%*cQ()7~k!s9BRZ`FZH zfZZ_@w53ezw6aJKQ5vDO<78|Ixoba%IO zB1k}=W(=h1E(rR3Pva_@rEk7${yKczvs$E#_n53W6gJ`}bcOVF-DNpR-x+ z{DZBms;&RGrdhUTd~CIj(fWfn3*Lo2;KaF!qoHQ#k_PlZ5qTe1Z@Re7^E)lX{uP?1 zI}M1E{D1>ZwRjfzhXT849L4?9o|Xj6gxJ()L#C@`q$(5-H%k z>3F~1W;^(l*M6zEc1KpBoK?rQwBzLAH!Mfl9q|PV7(nrEJwZJQP%aq zlqou=A&1SJHzYUIdjYt0+)ra#IomZ|qNMGeMD#H6#Fd@W$8LW=xXaMgP?M5a+F#nv z6u*{5ygWH7gISo6Aqh7(I*<|b#GhtG0kjP5I$dqz#@5Hx6hZm#k!)~vuNl~>e21Xb zlf~ec#NhpkK-`3X^6sml>V-U(1<)bo*;AyxfRiD1Ss!6w*P>%@ci5vq4f|lTZr%g6 z^2Adf^ql|#Rb9vFbF21{ksot)UaJ%aC{5dLnaMG%*@iNaU_Hglog?})5$|e}T-cy+ zxxOQ~AkGEFW^k}LtQ`}2QCvjLlUaW5sc@PT111N00XR>1o9`^219BtCd!cF9t$(C3@UX~Vo44Q2j3PTNBtTF87}8&rv3YN9anTl7 zXK-XYg0(D}&$R2C4{f47(2U)!tpBAsq96RQD~WE;*7_FLN4CW*iBDPbEuS#<5KZf3 z{iHoBMTwnno(5O9x|8z@Z|1Zt21Q2kYTz#G?>mt$+^N# z)_xur>=IrpyFu5Lf3vX>31s|nZqIIw87kua==ta53o$1$X$x252vqF*6H-P*w8SfY zL1=jExKR~b3d<0Dq88UbFL=3Brlo$J#@xnhW2DNA+Z-+uJ=4VTqt3D0M_1@os0Yly zD!>`*yq2bcrtf1;o%F4yrrb2&5v#B|g^ml( zFV-8kFsV*!#yIl-IF8Xd(~xce@~%~v9;eoEVXQGqsm8_}E_eaCSpo4ozVD^IUv54( z%-j^8;kiqf84LS9Ap-|FCE?Ei*VTKkc`%G~^G*kk`o{g1Yt6C|y%^>Y9T*sE|M^I4 z2BH4c=3Y4N6JF_^g23zXcW~FG6*Px>oOO&wtd-c`~rzo#DQ07w_ov%UZRVHa(tt z4%FqJ>da1PeAb*4>fXAZAqTnxB-$4(^Pf&FD_R3+-7Z#go9`ZGF`ozH*UmLaK4Fdq z3Mop>6CjRX-&jM&w4_HbXEW}~b3~PJcz4O6gbe%oX#HrLFc#gOH~$vDV@DE_JN$m; zx?#bx00(Ond5da@^yOkSh8DoTe>k~U8*Sih;$dJ4;{NOzeiv@El<#Gqhy?>pOA)K= zL0lXD-wneh!x|7b0;t3aJyNcvB-7-Z!N8sjZ@uVCU~TcG0C1y8uzAcxfW{p^(^f+H zTp#??&lj7|17Kh)>`b_aJ3gMO--!SWKke7JuoeT*fwH)geLq1sXhRY&iTI&UUdL{L zDiDQsNGoV!i`0rW9iVwK-eFvOsn8i``%nUP4>^>%EL<}=Dj*&hZ5*%xpbUAi7i7AN zdH4?}<@RA-?-wr&E^zmFiQ~eV3hZnj~vQKef(y)>37Sf*CkeLdCoVR5GaCnQ8#tMc%sZeyE zfxE#=1e$n|)8>%VMC%`tE!HN%vXL2wp_)gUdD8R{;+kB1ZIfqN4Lg4&s6uGD+fX}o z2nFDJ(yb*k!7reJV4@kpl!;&KRggjvBSu?`PZ)WNO>CC0v^Xv(R$hhK9>+yVLO&)rn;+Mo7rprw+=OhI%-pRxZoyKE6^ zacF}sO_|!7>g&sXMf~yBpW5(3$^XmBX6qTaM@fBh2*~h$ zF3NY{5C7q8t3V4>T5x!~siWeurZCLVEqGJksD;92`Y!H8sAE^J*Mz>A2@kEq(u0bJ z_TKyK$(h#6*Y2+d?Qa!14OOuMxBVFR?fKLjH>dxG=;0G@iI&zMCr6j5lSk~_4`=h? z`rugEaVDIeLlK`qgsanCJY$axdatgK(^m}oy6U{)o8>4A?wTLAZH`#L*OZnC_#fNU z(l`kBAzRePaYTxLa51>YmBKNBbcK@5^Invx=+q zmtEuDUw&+Rzw!9U&!0$%cF+&+M$5X(_A({IcbUO*MeB3|BY|Dg-qx9%+Y4loq_RG- ztBa13qs$_MBhQ@!e5$Tex2N;>)h7}c8CwKVk~VvfB~BK7&=$IR?zQ|>+ib4xd+Ctl zgXrHKH6&&%Nr~i(?MIg>pPTh(9Bkhmovll0HzHP)nLH_55ZTOs@i4V#NnBP*l{}TN zs<|Ca%zLa9J2bEaC|d6CB1|^RV#+k|>)fxIYZoW>(1k%SLi{H!=GzRmj4r=o9#Ic| z4qhmhw%bmeDe!{Ph?Cc=6Mq&ahD1blK8x&aBjGZyDs^g6#}o5-$Ov^(R+`FQ?N5TA zOsEzG;_oVY=x6wCX)O1MZKji=S~iAN$u0v?a5wj_U}invIXG9-5u6NsRZw#DXM_bQC3Agrsyvu$ z39;yY)@bCBT9du{&j;+@Vv>nI{>jl9R~8^fU<^zpBxHQi_iKv^t~fLI=Ddq*Yn=pT znRZrmytKeeGQn(~nsYeWklSr62aIebyAn%(CX_KuU0biEdX+&tz4f1-(14Oi#>wH( z2TJO3bKYTx*g5#qSPbtI5i=`#{e6nwk>G*H=fc%r5^W*)p_iQdU3i%V?VsGmP?S~5 z%36K5g~)+o>V>}-CAL?Kd(CDY%+e?}B0&lx;$=-*9qA#HN0GDW>3m&%!0Pc>Sa%@9`ibVT)6+-2MnC*Mz8tjem)rlGD#N8{R*?R>)M5FL_n4xMufMiG z%=rx|FV~CeOL3(D5BQojU@Fja;>vRT8H2}8L2{t0F!ZTY{&KH*;kThd#O~nTz?oQF zH$tfD3RJyn(%oiCB(%_tl^e19-0AJ}+D8pr_txaDau4WLf`X;bt2#v3=HHd7ih?aD!@G}GU3Gjw%xyp^HUcc zA4ohiuNusGLb@U$F3o47D2_Uid$BeYCD{z&i;n7Y#GXL!yd62nK)3KLss=Ba zoZM5Lm><-Z3?;A&bRE%OEiCY+Oq8FuFksjrYZLCz2JZ0H1xrEsRGg8pzbj(BuXga? zj2^%xWDbOWsQ<-)pcK>0mK{r1OG^?}w&_zE_oQVm=QSAGIp?4yw*V);Iz4H=ZLbYH z%v_{R_i8t!J=>Jh7Jftw;lOlV5?K>1TfoTEjD))LsagU4v@pXuvu4wx%nx&rL8oJy zV29k3{<8{pQW>+&Y|JKV#mlQK6OjH+#2CK>;#yYk42B5mE+Zkjk-bPZirVns&{pre z#b)7uLC5oUeW#&Z>lpLu#6NWfxX!KUqNb^bb!1r^N*728Wg{<0`&$vxHs-?j-n8@G zuK0KK0TpUy%<**)%#dWpRGYZw?MkcP22Is!2N}>XvavJ2mj0}aeYVF3EI838JF(s6+sIB1~BQcO!eZ(xgYl+hvhmhr%V>qTF-Y<0e z2o905@O5g2{Tl6{T+g|;rwm~zELA$vC5}i1S8Msq{Na%&(9{lqs=**pnoONnM*RSn%>R?A>{O;0^X+?gqsxX^4om5?o2a$|Yw zq;w9*4iL*_Ws;+N`nPs%L+kiwbfgEC-;~{)*hhG2mOkL%_@83WK>iU16A(72&$=I+DzS@s^D1*+IHRFZ)!4MUf)QF z9##Cl1>U$ubw>lGt`BWPNrnbfzHL0<>uC5XU#|@oC@X_@;S;BaNeu$BpT;e;t+;KmI(X#F<-x^mzgZDyFja{Iz~+EfjxvdQ4&uIP|Rd$Pgo5(I|rZ- zu{UxgqH+~%0+ldIHR)%R&}YF1-a6i2Pj`Q!t}|(VhxBS1HdG69l^L-sPMD{D+`B~V z@7KTlMN)maFZ-kyN<1zqfM^oyL=@+zQSQI|5q`rEszNG^qEM-iCU0g0set&*ybdK~ zR$zlSieX5{a3&t_zT1bL{L_Lo>e=RHYSu|pB%3RiXR1nGvf}1!FMwg)V%-EHPy{S5 zDjt|^s?&)hm+AF5aJ3B&B!qsBRKToEHZR@34Od_OY8Ld_*as}}A?8pn&C~{lT8cjE zj|r`;uzDlp8j2Uelh)<*p*@)NxN|1nYfMCN=imHcq+1_Qr@G%w%hVliF%p13^dIwg z*7=OV#zY-l#cE}A?F`TLIR>a#-KbO>39G6!ZOYktfJvl#q z`q;P8`w=`5>$tamQ_a=p%o;i}NI<4=&WP3A8>|0Ag z0{P@f#3gL{B9p?TY;P39b7|g%4VPi#=)VuI_8AIPk+O~oVw7YBF^q@b{>;w*Jc=~Og#Zfw}a zEqf@#?(NdapLJS3s*&>>`ZBdVBEuy6DkLIn>p6HQbYtb67V{dRCh%M-oZb3*2p+6z z;bmJTVA|H*WOCPqCz{(aBQx3vc|J|Qg#E%)33}wuMF~~7M;VR5j$7j^nxs#9BWiiH z3`12Sc-^+xXRc5etb?rhm|-N?YYW{8sp-7Sb*CAR?6Q2Y_i3#+jY8?H>l&n9Bq!5- zm);ihxzl2VEy zh`)AZ_@Q-UsgEV@CaEO){1sJKvCd~|ZuB;U=vEK^f!cl@ght#K~8 z;Y5VFF|k(HAVs`tCgV}(ufbUJ+_K(d?q!k+ts~(O-Fuw!q-S2NqcD#cW(BUQr;v%- zo10hxfuyUpMutDaiX$z@fsQi>0_1Ares^=7E6w=U#7GwlZFG7~-HunJL(c6S(T*y@ zMf9(lLaa*>_B?_5Y=FrZ{~shsvRUcQMBRyMy^-x-0KL!e`oprq$}qMPeFeT&}@IX$hGEng{Hw%&1DW*Ch*+%{6Uj|~{NN4%>X%_SL`^EU@%o1Sb zogC&8$@yP|c7v0(`+C?n4x}N);beN9%$^W~c`aon4#Wo^cfVLYLKYL*XEueRqX9||d@l1g(Wy2>+x4iTyU5D3_A8~Q6DU}aGS;fh2Ar$C-% z7!;d7DjCLoX=wZlmf2LMuUxNX9bxgcQF~mg*TlQSB+%8%dY;mB5e1T0ou+JdP~!EE zKFbHBQpVRE*0=QMH#dW_*V?XI0*$q8CYq9gH)`3!>9 zya^LjK4M<2N%4KB6hbw|OjSf0ck+cl*P1h_*( zo-71xR)^*IwaGet3$J+uLEyAFPpzOPJF-r0(>%1{gkaM~<7roE^yP+8pW5v<-P|E^ z&+|KRg9iF`zSzwj>{i|c?zP6i>n+2^jq{dubx86T{QF}S1IKWgTmD3$t zj39MNLycb)R3FhB)*x9QylC(IqosJH5%(Xfh^5@6X870QH)yF=Q0V=K!o?=HslROs z#8buMQsXi{R@lLOr_}BQUnr6qncw*`|D>`6BKDe}@euMfJz%`_TClF^qUAm;BfOWq z1}M&8Sr!I&=W{47QMfntCZqSicfCfhK;0xE(Ke}vC}3^7dGJGoR%SQslAD@fhHf(| z3tl;pr`DrRe>lAJ<1i=SDz4P1PPNBZtiAN*+kyHASh`%GAra~N?cZmt7%M3e5?tLj zE^HQThR9uSni{$22^ydQG7Iw9fqc8(>FbKGiHj^&rbMiB* z^GNK@=72PzasGkef1MZmEU^X z!Jiwk+s}sKT9mS8t5Uzk{~pZ)K=|1LK>en*Mfh+F^v#k*?0oK}ul}}t4cm6ne0aeu ztU;t$*UdV}S-MfH@!Y7f>mlI6gAJoeFb%&9U!ryHQ16sW(f17haLQ4*wUilmc9pwNtFA&t7Ii|4gZ$7lBcKGb7kz zTl*%$K>@ntvEPkS0sHwjfyY%_oEL@cu_X0}S^lRim=XL~Zo_~Ue%$c}a{}iF)ix@& zo;gZZ`7H6^>dcO;u=l2}KELd-+4s@A zHk#k;_@+FY$F2XAL9!^Vtkg(ThK)%bLi@v>qWzfq&h3?`yIhvn=RhsWON&O;ct}or zv00GDC&M2ef!Xzc_fMu4vsz=(43e)Ko)n?T3*NpXtsi#0FTV{=Zj+#B8GY0w+dHYUHF1a_ap zM&;wcc>a&QI-te{F9e=!pl42L4Xv z1z3Qe09$@IUSh&JV_=ICL|OQD*r?kLWJ&XpW{>rQQrMj$GY2ltITa{_jNQqo1Iq@L zo8!Ows@ffUsKgVypE+KhVJI`Rzgj$Ms+?H$MN78*KQq!m?W+ItjoDw86`_wFBU1l8 zo1R(k5&|O_3nT@Gv9Zy_pX|ay#X z$F)d}yo`F9O1ry>-d8=eFDQhru;u zE7>u98dgL;0I?J0YvtB!E3p^C{P##j8K5`FxWQ-U1>ol0&3RE-rFHYau77KI;Tv!M zA(!#X{5wtX2)!)f#Ic;es&(w77KPi0Hw74+@zGe7X%=`Kr~-MSlR^s|e<$)c zLhbYI4FMJbjWp9~XUr=@(ewrSqR;8sakq!gq=OD{xigF@3&7uS%K^J$xa5%0z0D_0 z^;`@sI8UjzxfOD#2VSH6t*DxzRHl00eB{#J`$RyBGdK=Ggf*6`1wTbkkI4!nBgCcNQQGAM;)u7)fZps_xus||TJaPB>wB4^`acrRexxewK3 zAAY+zFqjho5(6CKvSOL;wBh`{+?84j>eh!F3P3&h2k`@})JwN8+3Krf*4PbWmGTLQanO<(g$TZ*7nG#l3*Oo39DFwtLLldf^3=DN>gBSS< zu~Fg;aG}@HahaWV;t4eS8i|nTzwh@mT$e%(B(llkGYERt5W9)a&f=t58g-->(lR#Wey%>Qy;pm)ub#o5WdNLxfG{lrNq~2PEFW}(1A0P)iUo`yr zUCOQVxKrg<314MevCrH&UpCL~)sU>#iO9gkrL?7ZpRgq%syWsE__`*+US=@8Tdu5o zF7TpwoKgoiF-2!oT2x#TrIyAdke|U`#9!ZQ_FB)$W*;GpUV4xbZ6;4(c+*gZM6$1i zb$*ludE)`~(h|GgRM^$;%9H5_4^PV!A?7cnQRSesZne;kd*7B(kILC^hP1RBa=}>Q zoMl?%Z*KDHNnV&Ch30*Bdqk(55#%zxl93$bWGHRjKF3rqJk~SB+J;Zf;Gdr?t70r_~xC!(Des0`rwgE-b*_O{vp5 ztRJ9o*WouJjb4+BdFrt8pK|MJ5W58j|7Brn_;cLF_2d`!>n`35wk=fr4=SIEj72!T zqP$ug)2nXTtDceyE#k4y@>Qlo;s6Erqd><)z9s*q{SJEnm%*v0Horwjd}RP_F)7Pe z)K4G67Jy8*;~1@4c&nsW9rjD}l=slf+-9l;5w001ld={KF~fPOa8gv*1@!L1BkCn^C`ha#jhw5eRC6m)Vpuu+OJI*<|6bn0xOLGomPswtL%#0*v4k@qBP8+^99r89+&r1(ZKccaH04t{6TDo( z(D_i^=mZP6V$`HB5BTS0g=`NctgHx=4#4yIg-r!mBi#=zAaLEUXjXjXe}PXj5=O-7 z>Ro!b&0pkEHA#Sjv5{|0nqQs5ma-$=5tEN`3PKBf3!y0^A+lCAfYo&_>7 z%0|50pP+kYTO0b;NZ*VXE*mx@ z)UlaG(WyLm{JtV^*u4Btr-U=Z+mMXPwO*5(2#p<%`+OXE7Gx}PC7SOjZWu_f!7(S^ z8rT#!jA&YLD1aIOl>j#&rOg+@lNt+4&-hgJ;>(1ly32dQUINr{_jZ#!-h<_Lp_ko@ z&u`xMcN^+|e4P7F?H&BS&$A952X0?9K6L#DEIr(F*wTa3Mm-z8 z!?#qRChKp_b+Ba-=Ky;HQ*P~^=_Y$}l4bFlH#dNsKurecpZQN$A0P0VBB?biwyGVC9^Rq#wmFN4BRt?@c%OzXl^f6jdy zast}UCxl4w+aFrRdm{`dNOmp)z9iF2dN*yF+lmmq(MoQ&;b~0z2iS{4iQ|}#1`{gE zRxd)AY(L}+FWB$60F+Ca`Ct*VXT%H8Q2%r#;!eii9Q}PuRufaq(AfOWW^k)+&sOW2 zT!dv9ZY^ z($vjj5~Ty&IBv1QE-{6Mq{b%K`A6TdPPU=ij+h&+9V43$075PxDJ4w~1OodOk#Rbd zXF1}}8Ot34Rh;BTp4%{>>h=oVc6hB*%V%t7NR;fz^mB>dfx1$+rpFYrhB>v;Op15Z z^b*KwH11UeFXZFH%9#CfD@BCLan1%TDCh&WCnvn3k^XAUak|n*VDgKH5)3VIX4ou} zQiZTNEI~kAr6-Cz&+EwJ`pP@o|GWA%u%Gk#0+5f7Ib9UAYG^37zX}%;1|i3L#Fz;~*pT1d(b&%LIcWEG7n{4K&06K@D$?T>Rr&3K)IM`u|EMe z2t3(ejo&${*KmVbT1DnHL;gD5X;m_b)LJSJ32(^%xL^%g!Z}84Iy1}T%^0Xb)BTWQ z#5ssHHUgX_@jGCdQZ&EJQOXP_@^l<^Ll zz>(0XZ0PthPo+XqM=zNPHTynJ@md%EIZhR7BbeymM7)OZ&ZkHbbG$sh%9UkFNL+4J z3z@_XY%%HedBW+!w`70o(XK4ShwYfl*k{|6cewzrxnx+YuHA`W+UB)}L!uw0YQh~0+{M7u`(yfXlEqKJyFjMHL)wC5S6n)IR&pA|sIek`gd)8kt zZ~0|+e$+^9?L4p`1p55vntK{j^LL$%Gl3l_oVx_%f=!<8NvQ6ZcMOgc1x1O^fF)VX ziQ%h)QKfsiJOYy1L30|rA_e!?a^gA8W~#gYe_}FxKNHYP!G#_h@_n_po;hL=+jxsu zHW1IBgAdeh*2DLJ201Jb>qYs1LuiUeY+et67Iq;^GucyPo?zr<7`QRW0WU6XS2r=& z_cVQ3M-t|jVI0k59U_o9FjSlCKXi=z5#bJ|9SR~kmi!9+ivr{hq^1uAUg0Qf-RNw0 z^M@DsisPjN0tYV|awWF%ebL7x9Vem3ZaD5r4JcamE5JYN#q{;)LW`_TXCHeM7Mt`^ z4o7b;6-h$NP}>!Nfy@I0L^0-ncLthDu;48Z(s3)K-EDQE)c%`w3su5LH*4mJN@@X&*Dtmcp z5%kx7H{M$CS(`U@R>SUr)0aj^z4oT@m=b!nMfpol%M3onEb&Wd|D8O&=R^1sBYN}B zjMei?6N4Fte)Gtohf$^VGW}px9Dhp^#hzpNOul!5`O%HnBl9J`T10NPj1sfA*9iLh zv>#)Ey=enqJyiTX7%BYWQ12NDjRiIK91x4Mnc_Heb>2|Xm(RU+BEA}`8=)$uKwebX5B1bl z&iUlDuj!e%S$J}1W>`WFE6Y}CA#5NFd>cOQ5Xrr9=|lEY&`IJb8k6YRUQ<$M#$Ee7 zk?!OpV$*^jip?luE-RU`|JNUftxvOdpAE6(W60p#QhU#mp4*f;1yb*McYT;VWhh5z zs^ld2vwaFy!ShX3GMATDV!*nfZX}}|SCa~2N}G7juGXF{h7J@MW1tB%nGI_&+diD~ zGUXBEQ4|V8P zR692dR5~G3#@ffI`csd}WSM$Npie#>@U;Amz$wWnF zU-Q9ZbXmWjC`!GH7Zzz}0L_o9ZEHdxPt8~)>kOG}wiy~}s~@vqs|P7h@H)0&>VMC^ zDsi(A{A$SK#QH!P0hgJ8UB1#>Hm>PHMH+^lDei8eVl!U)X}yt0JzR6&Uk9ymA?@v9 z7lFoeHtM(?7`kF|3KPE&J%mu1+x~=TgxZS6)d01C&qg0|Yb6~u?0ZOvtA7|f;(W{?X%|=u7md3RVDUN{9H}d^pBEV?Si~$-SRXjWlP7G^-E1);LHW~J z9dJ!}W8+)U(n|F{@IwI?4S%6?$YDurD|Lc9&$X?O>5UL;Iw4WrmpjKS;3)ivi?byS zky$`!s)~^56ed756AO<_)qfmOBv8k#?+U3CFz-3()->vPo-Az|^F{pQPp>lPtvgSL z*xZvbCnPiSsX8Eu3m>MvyR`7A)npUq5@$hj0_=-Zc5}opm|hc)wBCO(a`(acoCAw! zqls6vlpQS}Tvz!f@@LZp{L`wLEoZ8;%_pSxc4p(JpJ+*CWcMy5xY0zaQz>V7hVqfZy_H|C7Vs( zWdk-(q8Y}++tu3OKKp<|jww!6ib+!C!`wFa@cd*|^nmM|rUtL{^~zOvp91Cvb^hZn zFusS`KLjd*vH&L65cK5eFRYTJ^qzi+bk3L;5K7&5YX=w~5en|YV7=z?jT3!#45t0J zi`f@ygT9YXFqZ+5?2X>7TgwLRFKkwR-WB^(>66OehPq4JV{fEFh5S+r(0%!meWVW6 z)ZOIVV-o>&#r1oAn!a(2@Hj?%O$a0p^%M;$whj#a8GUNMbJ>G}Bnf~`bBkYlW9L__ z@7$K;&%9|BvcdsI8ug+xNB&Mi{CeoF0ToJI3cpn!$r|~Tg=vv?v+b-n)_5SNUj3xR z&6PEE!LNRQvQHPCAhdB>JAA@3k#0H3$^T35eo%#ccj3;kSJG=*{-c>6^excryPEapXLp>>$g?GKlMjA&rsRBL zOGqDf{?`-cQb3Uz&H%$vYBowppfB?2(ZXNI{R$Z)7t+ z%d~k?;SGgBjs@z*Pk#lo=zj_QJ1sUYbaz@DUY56uz`WKyZM2mtv}b-*mAmZ=7iuq= z=$V8KN z@*+C#mDxOzY(5UV5_};W#q*rFI0X(2)dp8$-nloyuJ8Xe8Qt|n6iqL_G1}_D%QmR@ zkAUfhXsF(r-cg<2laeXq@=IPme3@}aTQ}b zMcImQJW#PR*&$Ak7_PiB zYsXkUd$qwez9CHQ8UBZ4-F`_|5O+Q;2el^hq29IM zh;PPz!;^a6lR={?>*>nyN!d{$l{tEa<*GpGKqs;`P_>xsIjg+hy# zQlNMX#jUuz6$tKLC>~Or;uI*w-Q9yb1b4SkG`PDKhd|)V?|b-59`4FRa@SpVX7-%D z=bU|(C7%gYP?HW8P(2UHE#*K8YgXnNv5=zJ%AYW(xML{AV4OtePIUBT^}g4j*0sfE zxUUWdfsSH^Kfa#`f@}I3nQH&h8YTXqMCzNF8P4kO3a60TB4+5thHbo>oJQJ?7uqJ^ zyl|9V$AAzCrG1nx6e;=^1-ySr)&b{gLh+m#{+n^yB5a`N9Zmp%MdG@Ck$>3A&{ShZ zz>orZr6T%~{j(qUJ$KD3yR~YQ&BK%0R*2Cpfx_ovnBM-r?_}rjInm(eZ3&`Zp212SHEhHR#!=xc2{mkDUk zrb5SEl&=A|5?{ewI**(uwm%j2o^pi=29X0w=hiF4+WN&R{<>6ck7wTVW~jWFZcY+M z4r*q!lp`82X@}mCV|-3+%--VrVXxRY)jLfW3ztZnCebSTpF?Y1tR)_8y5;v9rPxN7#7gg zEc=D)yCY=ny^`+=aj6~f%V*GSf@J}vOvRLXkpt;^YiT=Q+J~Ha#mY@hf6>SC`tWJO zPQi-ioH^#@hgj^-m^~(GdcX#+=By@SJ*2J8z#;q zZ=)P{s1T5bhzAcij=WrbDInEp1I^o9i)jST&9+vsZ@X&nHICM7#gw zk?ubY#+e z3eE!aeZA$5yempcFT5Nv%ah-Dp_{4Lujj^e5D|aR>FuYwccR76I@L^b-3q0|zl$3J zl~$$*h?Y`8SKs*`Xd`k7Uu-KM6rksBSAi#5R1DQj!q}hKB*?1jnwv6_Q(nLx=+GZV zoMljudK`3UxVdrU3>*q=(A5uByD4aa+lhL>8BeYrUbp;gn9=i*@f+$pl7M9oX;JHx z7ayLQbQcBK>m8>&3Q#3`+;OAEr387HG?l9FcOhQe^UT-sQt}8w zfy;+Oi)b#Dc_;gr$WcQi8dJO`81iW6V0lDpIBB|cFX{w>gF_p>2EKR& zY2fy1bpV4Lmoj)LM64>e)iDT`8n)Aw%}yqUH-g(?-pXxk z4umzg*G2+45J>cy9oe2o4_hE!W!3^Q$DyAGPSf!FVDoyNr~;)nT$aeBtQAv(dCqyK z2Bm0p68da9=r6nSjMD|K&H0*3u-;3Bj0MvcH=<3V4i&)rIlFjw8I0+)S*pHN)H2uV z3Uo~djIC@`N&>Gge?iJs!>{bUR&E#N{{OA#Dx?$W%4p?wPL+2Cfiaz%WZ60Y#|pxl z+J-X0 za-lFFAZUEqxY%S?f0d+av5Z5h3?T1LlfBY<0I~JUJY}U;8vmJIS5&aS0GY9RNBiPn z7QZ-Jipn_2_SVh{tlOPXNaor>yh`Ns*n%^Bc;4Rk37}xOM*PP;mU@)9)I6$BQoW}j z`ic~d`sc1h?oV=jIU^E-b%H7T#I&dwK;&91XYahOuy6kveMV;0aj9vTNYwb}*nI1j zWPK$5vrg{aXK!SFE}m841G>RNNv}2t&6n;JLzsEgk*J7?GPAeenLmjxC@YcIugG0M z*U{2I!*S%6cpgUc!$>U}AS}`*JVB`nKz!;@r>-CsMm5mM$#{cnTiY4?bg;Tw*Kt2R z_Vlla1TN;XF(~$QZUgNE+Kpk_Y`*q}P-Si=2EI4IxQ2={nB(0D^loLHL#Y z^N{pRtu;ZrM)N!9&yGt6^pwG{?4`yK9l;RiP2U{BW+84kcEZWbH^p4RWS1|_`jf`| z&JZ|h%uk&R4|S^w1{}%{Mo9Q9FCVUW=vfH#_znF%1<>}(R`WTDM~f0anP+Xv69JPz zu0IVPAqBe*(WQXf=&~b4Kb_W1Z?-j0#}bpXQbG&1z{AMq3tKbhRqj?*{_M(sk@*s% z%r%yc0bUZ3pbx%xGC$wzd)V*s{L7JS54nSWv z3Vs0bIo*sgYOCk#0qhbox*4e)&g+znd*3y~FcS|F*}oGuT7J>OAh_)+0C6BwRYsMD z>s*1kYAxFYR^&tl_7q9{9@Tn<&t+;QGbLR0B4Qp;+XQe2`ZJiOhW?jvmi5kC5W?NTvfvtQuW zM>zCGEs66-sD#BVNNlxFZZ&_+b-G(xg z)>3A7e!$d07V>lo8Ct*q>53{1_GYF~<22pyLOncJ6AG496#fA2ONZagb}hhv|1?qz zq5;PeRxk^d+LAg!WsjlzcSTrMysJk+?%QWR@(*^GN_O2-qd9Z|#S|{+{O*H#mB)51 zW@LXhH8Mr={O^z_it*HzUiN0JO-dAE^|#Ec*$}9ch;u8YvXdSu3S)-Au+@KD#3xF- z-D$R?lgp^t$?yGbs%Rb%2=1QcnpO@JMFMniWu}y$3>iPFVMD=lLcd@3o7>>5M)){L zw+b2jfxdgov|e;RB0K1COeTllF8rnDNOan&or1>ud;;9`y*Ha_;6ARw6EtW=ckApiS(}-lxa`1L>xgU0Pm~?SIgjvj-3arks|m0 zr|i%N|A(Nu(d4rq5!h>FW`U>uNo2U0XVz+Q&n+-70qTxH@epebEPaWt=k0>S<$V9@|>sm$q%xtSTOEOX>e za%Gxz-5&!hWyuxJ^UH{v>(1xf=hZFm#+L;+^%y`Q|EiSKMq~>WkvcQw6vA~xK&1JP z7p^j?_7tJ*BbCFzYzkd|XFpe77G0GL0czoM@2;rHtCya;Sb2~aQ=_J}HpwU0|15IT z#|ubgt@zgaX1+(gCWhTz0H9>#uP2xqud8^hh`Vd>qGyR4>8_XG3 z4=mQT;c!VqjA;6*rV@AHPiAA!a@J~JeR%}XSVRz|2Rc$nNTQ-ApMl^!J;xk&x7=~} zYby25R9V^IXZ`n{u%7htC5CvVE=2TP*m0*TZ_tRhh<4Sg3PrC~@B&adNq_GjnYHQdvm>uWPQ$c^(#-CeTYzYZ#+(xsQALAi^T%Md9W~Z}LbWz*Al38cFKGEwG z`ZmY$NrBqFjl= zmk;o$b(rr7>~X5|epiLy9+okzf%>sQXYRk2)(*6lOn>+2dds`$D$%VbvO^ni+(#TO zTb}Yb@3|b`XKehXh#C$AO(=W4IsPXQKFuT4ZXCu#6B8M^(9P}mUf4T6Q>ka6&f_;v z@RIRlsJ=HGsg8(!O=l;#%-IiZ{~Lx2jVXhtTg4zHq1DyPr~>%7)(5MsoqT_HH>c)o zrq#@5c&|`Boyr-3o#OvesZO4IskP-m1dEVraMF@qaNVNUw&NZR&%Q3F?Pz;`4BNE< z#ZN1|8D(L-h*h;pcl#n7SF3x6n9D;7V_|I2^BCv~4VfAkj|w%m;X1$iy5l87y4;aM z@D17SaskzAMV$8R;8|@ZWuWun=ksYh(0)+V_fF$+iXAak^~4?ftii{$o_){dw`+>m zHeHM5%nMbWGIn|R_^!?=Z638Ty|1cFD0yyktezly@7qwHTJhy1)a`T&JB+mB-)o^;v6(JwM%a0|FjJE~~A(g;Ej*Nk6IupA1s@ zd1DscO-*Pq2>T&HBnLYDo15mE!39^RUoSIXvEv`h8o&N<;bTqjO}WHdC};UuShn}( zAciBoG4K4n^Q@aKvLsCvva?t1_)D5I)4$>DhZT$}tWMQ?R=e!gxg6+solpsV7I9@M z^LZp|Kuvst4yd!=#yG8lxpD!2MH}Rs8@^^FIrP~a?sH51&>?UdDrU0E&UD$6itSmk%d^T$p zXpbKI7n@zpG)E3cs&&QcEllOvPnCz|6cj{0Zc=VasK#6lSXOTDnWL{c%ca~rbh(9+ z={ip_Xnth+*V<5o8FtQ^8mu>Ji)7lEU)$x)ENyAQ8>$f zjQ87wtx_V+$Hl&`Ki?k&Jsa>n--|u(;XV6#SrO2{w7?s{1SLCieH&z`0*v{VWKP)k z(k)OnhSfZwyk^UX&U9R*4HCq4{4qq{lJLd5tXS0^Om1qsa|RaB;P20`Urjo%4cJel zPwXvcm!4_<%=V=!aVh(uv*uiO=)Jf1(C)M|5+iZ*jXTIw1K=~<`p5z6u-8yfW2iv4 zaDF`AY}}EJPw!C|0-{gS_PrOelZsXwF!1(Wk+yyy{d3gs)mgu+o#><$<_;l z3M_UuWam;zfomh#IZm`f;|Tsjx-ujjHHxE)360q)xQeeWK&|~x_!rWH z+>x8GBZI6l@V6(^?5X;)oEaGBt*T=rkBlXVMX8K%L&Ommm^)n`aT5+jO)FKynPnX~ zYCf8|@>}}1n{Mx05PI%Npn{9@k;?aOqipM!Q=p#=+7R2}uX^!B1+{KX4Z&;atDe6h zFYqks;*%)B&gg*l;4$7J^FB^@r`@i3 z^23!(s@Dd}@fra5B!BI0mSz0Ngvd}02_VQ zOfH4n*zP6!PJ}pSd5y|5UsN!&{B{p~n_kSr*&F_O?O!j|lY={wk_AtX%!s>_<9}Rq z({4@8>KTv*M}!vMT2|A2W+woPFn}oS?!GL&6LCqQrU#?(v)N*0D|PRcc)T0r2qK$1 z&S*C-xW)3-8Q<`Idc3E3rJ*@5s{#g@=HAkVEseR^j77W>cYVAYHFnwG087*JCb|D^WK7+qTBwayx|$ z;37Vo{AaORG=if35NAv2*_iGYre9iXh@{lOakqXU)RkC z7)&V&2nI#)U#nvB0mENUgiF>NMnYN)`b#q%e&?J0pHMR|@q>DJ|I+PAc+)?YufDy- z@H--M5S6BaB~T^MlP+=Mq-yq6<|YP&Ue=EA1Mg9a{v2l$`7lG}(nJ}i@5Gaa;+b0X z8N!fV4{4h28pm8D!1iGB#;>Ezj$zt@Df*>a-#JT$2__P=?5759JD(8_hwBy{s;EPw zTYdjTI%G*%bd@2De|!zc^aAP#0l>;upvrv`dJgS1DM0L4vZlKqjZ!h}{zN^?B?k&P z$OlQ4fC*5k4)LuQ4x`K8bpHO6k+dz(_j>{BzVK3WRMS(Fvn1e+ah%15qA*N0}-H4PJyR$Xe7K> zI6pfa&g>@rZgwMzuFQQXBfOV$&^*i{Gw36Gcw%d4O!fl#kCF%J+4F|S;|grSU30Zk z=DdSyPLHFC#?phTu1RQT0ytT*opMHEk-#FJvlK{hkGAmtwob63XY z_WLZa9Qp#oXe)>Hh%k&HWpfRby$z|oDcv78d*6$jyFeK`{T$BoZ=q-DP+}zc^ zD;ff`fGJO|r;XbR{^N545Vm;$OL@ZV(^?6aDn>=( zf1-vSHsW)xq_-h~dH4$_==%3Dr2F<@ETI1&A(zvW)dHdzEj}7KEISQz&~L12+;K2!cML1 z7mga7DNcREC@_oCSq46auM^J7I1B6* z10(!IWwl8cZ9oro&O~s7-3J%Wmy=yb0wd*xmKiV+87#SUM!CPu&gs?KomM_MCH@`f znP=YgS*@;n(9Guo2K!0pHNe^F2BC1T~>>sBTNx5s|ne44Wdk|VpC;TyTbV2zqRm)gW!C+L!r2Z;L z?NylPpbeD{UXhU(+C@8S2kIPIH%*kOCa%V(Z}5UdN#kkiz68y3V7!3J7>RxelLzPD z8#5O=LO>JW2zykjbH0F3F}2Js;l9eThTg!h!jj{!yKO`FY+&Nh6Sml&-xK(ak32HT zT-u=DlufbtiLICblRqLVQq@B0w+chl^w@VOZ@Mf*0L14*}5Z^}fJw5dWFMt;%f?qMZ~6_WiMn=$y3L0$WM;C>%%3#WSFy=OkKow5hUiaq0esiApeK0=U3sWyS( zRER%I>*3^yUxyAAN(uZHGME-srRXeo^H~2I;IT>h#yMU#y2L;0OfS8|Day!P>dPSB zEq#Pbe<0^96UBbU^aa~?6PO^^nTax$Q^rHfD9;sH^BlS#P1ScRx;}>-;%HI$emLnX zCj|U@8Y|lM0ms5 zm(A$8TRh+l{URZiG;ZP?VLOgL5$NNntZpRZf;|Di8NR07>tW=iQ8Am*eM2gjQtN2I zFNQyNFqdzcGg!z;1Zao;RVug~R>66f#Wwyo%>s2cAD&~ucJ4$zW z-JU8wJUpxk|Li1s{Y$1FJtn?8F0@f=yMJa)9~T?QT1#0lrtV_uFFLSDQy~K4ZFX}z zU1rcJRCP0Y4^p%n#RQ8^2GLB1NJlp%F$TEg>v9If0Pqan5g!OorBmFZvJXcRnEV{veX!^Lx6?#@iER zvb)}BE@KCqGhY#i63Q6 z9u{Gn&1P2^#kLItRQCtx|FAlIw<7tX4*gT&C2YQ>Eah0%3(O9%p_h^#CQfn^yBdB= z@lCm~1aZ8Wnn(5uOr=oj2hJ@P3{noMgG&RVC=FW9ck*}g2FdYK|J1+R*SSKos}t5WEP|IEf5&qIN2!Rq|TP{FF?XDv2s?Kw{L z-S_C^k7rydRyqD)wT$(K<+Ce_;z!6L)jO*T}iXI(h) zEuSEVucD7&yV#X)?u7&O&CNmO)-`@%u6x{OI2Q$z=3o>3$BxNw6^P0pj*o}>E>+sL z`4)5$clv`Qqv{miDh)F_)usT%+3}BSXr>T*v~0{oq*xc9$Eh6BAJ~+%oLQ&RdzFcVm!-I;?-XwT!Qfy4y& zB%O__&mUU#{Ye=h^!jp*LkOU)HOKTpA0R@J!3xU+lGUhKL~bb3s9gCFvl)J<&B4x3 zsT$$UpI!GU#(X`hZDL;e^Sx|FHPr!~7NJ5$FX!|ytJ*vht7xa}yaO9r3RX#V14n_M zK4Xdktgu(yvQ$fz=|WT{e37Qcs6wajA>V&8g$HmlttdMVVm#0VyVx&4b2Om1YV_OJ zS$dV&FAaE@3@0}xZ<8*+pA8H;l*IGAlu)Q<>K#cJ${KOD_ zNlqGszg<$41t0R7#lk1ID&jd==sznXJK5h^j4OoM`%txnhENh~%-K4(*w7jXHmfJ) z2_MJ`I8z(bFjr6;YfchM?E^PDz+#+y!SJ^C_Y(FR*N4N_jh|H-CK4Bliqtt&At|Dv z6Onq7SG-#DjSX)-v^(d%#?xME?mnVPVZfXVn?tJ%s(6(A9NKl9-8Q^t+%vn?zvrEr z)-KL5FkPor8Ld6g*P#!El5W|2WQc8smRHz&FrkdJuBHRoQ@8TBDY-`IWO#Aj)R5FJlpyXKkjOWxI z+C+mf}kIx-BL zl+fl;M0An*hxkW`V%VlT`vMupsO{Q*9)7wh8%skIh*H=lI z`ky^Vxc2G~FpPS0&%U&i=S->ZRhO@2ho$Q#2zCV5M+ghNjx~30yIu8?GluImyyIT^ z9GRBpJI3P!r)J_$IKHM@y#KG27=v87R3RdLU!Oa$FqWR_r{I8HU=+z$9C-n-@W#I2 z8w4k<$x_~yGvC%LtB{4r%+#bVns zN_(3!`@@D!o94UQk{)v5++XZqJaMl+caF^>RJAL93}QW7A~E;i9IH8Z&yl9M=V^>< zVvH$8A3MTW`btSDv<|EAguGSLuEgn$^bttAwdMSVx?K$1cG@(r)%$hsOeJXY9V*q3 z<~-E@L1Gc@2PbEO!D07{;q$-jiz?$r=@y5*1*< zlF6J&g>4z5CWSAW#-61^}8JZB`i7!j$PlOKF&yECEWABHETj-VKcxx7Tv|A;;dx(k}FLX|QDB`(I zDp}iF)`gb~`;n^Oq{IiyDymh<7)d}b_m+M3)h4WtZZ)6Z?u1?@3TihxNUmH7_4BBD z3C}%tWe*JQO!A(e^(ye{cdxa3-4;&1=9H#+{Ubs)Mvl_Me4eK;$kS%(izSW*qA0Nk zGWQhcHPV4$q*}6=ZAy1%~ayJd0p(HLM^D#yln_?@F%5c2!4&zupK6i&@WlEyv{}d zPP0pUvWVDZ_p1c**?FRTanAgbtJeCAn^3{{K5I|c3)@LNTZQQ~e7eB2-f&~8GgDGI z9`KKFrZBBfq_&&O)B> zZX_ZVjPT_&tnxUtsr*`L06m!={uwa;pAL0vOr)H3n`N@7anzTK{ z17W0h26$#6$*FyC@oo?AXq%~nr6}bDgc6)r!sp{~9K$2|d0P%wu)w@nSyf}Cx?Om} zk4QauIM$H!ZT%MpWslw1F(pPSyE;t=ZiDWEkyqcURzxR#6)@O5bJfB1)|PYbO8qz% zI)r+5a(tnB2z?y($>NfW{(Dl_cbwFvd3hh!*fr*x!73*I8vD-l4;xvj7bfKxg;XJ9 zg?7t@`8fH~C~%SKQBWXL_lt-$?j+8BBH)8A1TGm#UGbN}b#8qzDBclNrJ3f%h?-jM zv(j6FUl8ORE1*|@XS9`7o?!H6>*sFUhoz9)DcernRt(7Cx%6$_q3+Yol0DVjhr#o1 zMGE0|`E85^TiQq$T`w=MP7;iO=MQ2-Tf)!5VvnZoo1yf7_OiTA8ECZ?*{{`=c_=Hs zai-_Rr+Ye5!F>LCv;`A27b|9SJyl#w4qgT6KQ@y@rStjN6FCR18Yn#$GL*hf1X@$2 zM?^rhZ2HGL=g4ckuTlLJStu1phKbNYR~jSlY-#&sTuvw(3*URZt5TBT`K87nWvWQa z+J&F32aIwM&PR$vsQK>MSXqyyJ99!LO)-jifBW)@WtOiQ$KU%YK{FqQbI`K5I8B8 zzTwsy6ccgNV1yH?6Xa1A87E+@@AYM}4kgEg<1x-}OQhJYL3N(Z!`^L8G{%JhlMr(C zZE<$q(oN;2sf6?0j?5k%Pn@~zFY@4kl7ujSH9GDH@hn|&Auhr|0fYTTxxL?)dlOU$ z5d%{)SgHchqm(hl!Tv781-JS|A4}mjfPk=()MjQ%TukOad1${W!Eo2^i&YGN6oO!B zv{Iy2+Y~tVZYEVGlS={;A6|R4$a)h&OP^M}WbWyc@w~(C=V2}8j%2);$wGGspRK_P zQ$=;{sixS;*B2^Vzn^;UBsxm`q_~In#Gdba-x5jx_CD0eCguEAByj3qJm;7x1hWf7 z;^|%y=GqU;sz#R_yJkkM=AN!%nbPuVo~3uaN(U6M(n!)le2=_vd`~;Qtg&}4QN`C> zk^e@Hbq2LYgKad;YW9ghsdX4IC(WB=a+9leJx-ov(yvQUL+9KmyeX9aE4+s5OpDQS ziTvI#8-Yd;B6>8^cjN`w8@B*UU0 z@&!29TTdLoB6El`+MSUvOi@cFu?cbxA14gV zq>%Do#da@pW27Ro$^7*Fe=a}-gvxoo+V$$&cLDl|-UB`|P20j6g;NWyf9PL1N{f8i zS)57`OjliuNV49j!L_KEV^Q5|gUI25$07r9o)A(&tO*TFDx~d21OC_wVUzC`jor3G z=a@D{Y+4iOnN}b{Yc>(D9RzbaRP!mOJ~AEuWK|#GxL}thO+ICfr^VN5qLO9a6dLC4 zKuKf;2NnFc%ZrVXoN-!|YLWqo;T?skUv_V_mQXyV7HQIO( zY@j@GqTVGM(eW4QF{LNcNkYD(^b4d578PaDqP9oC6l?zTU|tJ3eFpCSaSmsHNAmf@ zR$6g;_Yv$pAi;=SZ^+Z2nm_vj6OBnM`>u~(U6f0dW@%Hm$0Hd(mW|X0GQ=6M?}@j? zd+6{FP8uok>ixFOG7N{LSU3^$uvWwgdES&j%3`Y;AbjvaQo!s4e>L1aC5~NnoB0GZeX9{WT&)#D{|K zEf_v2qVuz|5hL%r$j8^eQ8&n=GpT4e7ck}23foC&&R>Y`zbOtv8V4gLr`BZ(am-5! z{^d&iVkF>7adIyi)(M}eA~VzCmToq@yGP2X zN(J#(*6_FqI!0}yPxp$wMHp>qc||6N$Wz8_HK z_}z9rXV6=yn2ej<`{AOGKn2?bK9!71Beg4h*R+s+SvzhH5~?_5X5&{#$G}TMOkVJb z=zNtTM6zwXc9tYCrmX}B+Z1;r36$QE8rG9D$)RD|Y6%@ao_5U5cL3xDkeYkgr9Pz| z8lWLUF~Q}&6j)Ug!`et|sX>>F_(VpUA`Xp=culvQ`nh}wE?|cCmeDxL#|~&5PFxW>YjeJ8 z+Z7+Uaw->zYZ$N~i%_of&crFMpH0XiH>p#XGHWk#ZBp15yzB|Wl0p|`S0>rN7w6W~ z_SEwYUT$2G3{2A_hff@JJ>$n)v>o=R@8L%*bFUO3HV-!7u77mIXHmR-va&zv<)CF 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 048/123] 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 071104d31c..15d99938b1 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 d0a8a4c360..20c616e75a 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 ce39d66ca7..1b015c2a34 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 049/123] 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 20c616e75a..f42b4d23b3 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 050/123] 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 | 116 ++++++++++++------ .../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, 788 insertions(+), 427 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 89243ead97..dfa609fc05 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 contextStoreCurrentObjectMetadataId = useRecoilValue( - contextStoreCurrentObjectMetadataIdState, - ); - - const { objectMetadataItem } = useObjectMetadataItemById({ - objectId: contextStoreCurrentObjectMetadataId, - }); - const [isDeleteRecordsModalOpen, setIsDeleteRecordsModalOpen] = useState(false); - const { deleteTableData } = useDeleteTableData({ - objectNameSingular: objectMetadataItem?.nameSingular ?? '', - recordIndexId: objectMetadataItem?.namePlural ?? '', + const { resetTableRowSelection } = useRecordTable({ + recordTableId: objectMetadataItem.namePlural, }); - const handleDeleteClick = useCallback(() => { - deleteTableData(contextStoreTargetedRecordIds); - }, [deleteTableData, contextStoreTargetedRecordIds]); + const { deleteManyRecords } = useDeleteManyRecords({ + objectNameSingular: objectMetadataItem.nameSingular, + }); - const isRemoteObject = objectMetadataItem?.isRemote ?? false; + const { favorites, deleteFavorite } = useFavorites(); - const numberOfSelectedRecords = contextStoreTargetedRecordIds.length; + const contextStoreNumberOfSelectedRecords = useRecoilValue( + contextStoreNumberOfSelectedRecordsState, + ); + + const contextStoreTargetedRecordsRule = useRecoilValue( + contextStoreTargetedRecordsRuleState, + ); + + const graphqlFilter = computeContextStoreFilters( + contextStoreTargetedRecordsRule, + objectMetadataItem, + ); + + const { fetchAllRecordIds } = useFetchAllRecordIds({ + objectNameSingular: objectMetadataItem.nameSingular, + filter: graphqlFilter, + }); + + const handleDeleteClick = useCallback(async () => { + const recordIdsToDelete = await fetchAllRecordIds(); + + 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, + }); + }, [ + deleteFavorite, + deleteManyRecords, + favorites, + fetchAllRecordIds, + resetTableRowSelection, + ]); + + 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 d7b50ddaf0..bd5ce07cf8 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 e9767b0342..572bc23939 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 69bfd33050..ad47a1ee17 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 75267e445d..acf4a9bed7 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 4b61fa58ea..9c4b1d528f 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 0000000000..92cda27cc9 --- /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 2586833479..2fd2937408 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 18ebdac766..c05df9b758 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 34d709d168..b34462d8fb 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 0494fd32f0..65fa9ba2e2 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 532b8e0aa5..8fa6d6f598 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 0000000000..fb1b3544d3 --- /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 df0c345117..0000000000 --- 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 0000000000..7f71377c31 --- /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 0000000000..689d7287d4 --- /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 0000000000..26727fbc26 --- /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 ceff2e4554..01cdbc405d 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 72c5593642..1783ea61fd 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 01ca2843c3..592f2d7b4a 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 6486ca29b9..e8778a3f89 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 345421f7ce..0e3c69d7c0 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 354abcf09d..9e8358f9e0 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 setContextStoreCurrentObjectMetadataItem = useSetRecoilState( - contextStoreCurrentObjectMetadataIdState, + const setContextStoreTargetedRecords = useSetRecoilState( + contextStoreTargetedRecordsRuleState, ); 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 9aecee3e61..a28d2f26ac 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 0000000000..2a538c542a --- /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 0000000000..c94611836a --- /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 da407d91e5..ba541ca1a6 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 297f1dcf80..3f5ce71ed0 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 02485fe0d7..e77545fdcf 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 df178df4c4..f39ebc7791 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 25e53d8cc5..3c2f5b2bae 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 a884eda858..9396d30da0 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 aa9f392782..9747c2c4e9 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 345e114538..0000000000 --- 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 98294115c5..7c65a53105 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); + const contextStoreTargetedRecordsRule = useRecoilValue( + contextStoreTargetedRecordsRuleState, + ); - // 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 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 0dff4b429d..828897b3bb 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 b7a46e6482..43bc9c76cf 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 0a52eb0832..fc3f386432 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 106b1174de..af9bc7f802 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 79deb4693a..3bb5dc6ea0 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 6cad63df54..ffd741ec7e 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 4592115372..597d38e61d 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 5bcfd65d67..9127893510 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 0000000000..37621eacc0 --- /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 1ce2f5affe..6c538fe362 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 a9a5514e28..080b6d5a48 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 e40a00da25..0000000000 --- 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 0000000000..866ebe143e --- /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 0000000000..e674d42821 --- /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 051/123] 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 f42b4d23b3..0d9a5b4dc6 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 052/123] 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 0000000000..7e4b3b22d6 --- /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 0000000000..fa619ef2cd --- /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 0000000000..e0cd4b6dd3 --- /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 0000000000..3729b4f504 --- /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 0000000000..d9153d33d9 --- /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 0000000000..78a83a3b53 --- /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 0000000000..0fdd9701e7 --- /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 0000000000..50b71e727b --- /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 0000000000..48b5e10191 --- /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 0000000000..f253f10cb4 --- /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 0000000000..b7dd56f1b1 --- /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 0000000000..a140444631 --- /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 0000000000..07baaaae6a --- /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 0000000000..76dc7cfdfb --- /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 0000000000..fe7226c9d2 --- /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 0000000000..e1d79168e8 --- /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 0000000000..94fd86f95d --- /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 0000000000..f8582577f9 --- /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 0000000000..2e4fdf294b --- /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 0000000000..a5358e948b --- /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 0000000000..adfd8680f5 --- /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 96efe89cdb..2d7b9ebdcb 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 333eaeb2fb..cb9dbd2719 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 cd45fdca1f..f6c28cc2f5 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 7a04238d1e..dfdd4082bb 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 8cdb26be8a..5ca3faa9ac 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 0000000000..a14f17012d --- /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 0000000000..5075d7225c --- /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 0000000000..97456ca39c --- /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 6894f9b700..5ee4a97fb7 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 f0346d5055..5573956fdd 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 7260a6b590..f14b7ea751 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 0000000000..6d3e604a58 --- /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 719e665baf..d9eefac9ed 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 033f103a9f..6d60faf1bb 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 0000000000..2f7cc0a079 --- /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 1ed65869a7..6e8ade28b5 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 cc483bd1e5..b8af2e37a0 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 0b520154c4..0ea9a607a9 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 c5778aa649..82fca3b04d 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 97b34a8844..b3d984156e 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 0000000000..413e9c57ad --- /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 0000000000..38177a558e --- /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 46b546d65e..73466982d0 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 998fa634a7..38f17ddaa7 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 62b215f269..43d4d7312e 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 708472826d..6a102e8b36 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 033210af12..2e470589ef 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 0000000000..18b9dbb4d6 --- /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 0000000000..cc27d8c6c0 --- /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 0000000000..d7b8de1e8a --- /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 0000000000..fba753a072 --- /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 0000000000..ce1d6b11a7 --- /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 bba83839a3..0092499d2b 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 6e0e962038..c5cb98f0c6 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'); - } - - if (!workspacePersonalInviteToken && workspaceInviteHash) { - return ( - (await this.workspaceRepository.findOneBy({ - inviteHash: workspaceInviteHash, - })) ?? undefined + throw new AuthException( + 'No invite token or hash provided', + AuthExceptionCode.FORBIDDEN_EXCEPTION, ); } - const appToken = await this.userWorkspaceService.validateInvitation( - workspacePersonalInviteToken, - email, - ); + const workspace = await this.workspaceRepository.findOneBy({ + inviteHash: workspaceInviteHash, + }); - return appToken?.workspace; + if (!workspace) { + throw new AuthException( + 'Workspace not found', + AuthExceptionCode.WORKSPACE_NOT_FOUND, + ); + } + + 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 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 0000000000..493e3972d3 --- /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 0000000000..c1514c8f99 --- /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 777b1febde..7bc44ffd00 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 c740cb9a06..d323ba8316 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 ea10468729..42d65621e5 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 6001a90f4a..00e2c3cb43 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 f6ba1aaf4a..9f26608765 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 af651c18c5..00cb30716f 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 03b0d234e1..77d54025fd 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 58282f9dec..c78a1bf066 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 0000000000..8f5099ba5b --- /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 0000000000..4a0e1002df --- /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 0000000000..0857ac3a4b --- /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 0000000000..6171836337 --- /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 0000000000..35209c642e --- /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 0000000000..3cd5c91df7 --- /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 0000000000..3c62fdbcac --- /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 0000000000..e0adc9645f --- /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 0000000000..d0c78e37dd --- /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 0000000000..29890f184a --- /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 0000000000..b5b4ee0760 --- /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 0000000000..22486aa3eb --- /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 0000000000..7b2148d230 --- /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 0000000000..ec5520c5a3 --- /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 0000000000..fc7fe99799 --- /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 0000000000..e6e492b5b9 --- /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 0000000000..ea41aded6d --- /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 0000000000..b860353314 --- /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 4f26a8b0e0..3810176f35 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 8c193efefa..7e7f8cf1f7 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 75b40376c3..f56a8a54c6 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 b9948f623b..ac5c382871 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 7375c64a8e..4ea5ba05ff 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 b005327471..469845f411 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 0000000000..6fd1e54643 --- /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 0000000000..32053e4663 --- /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 bdccc2a098..3bab15bcf6 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 b6ed9d913b..615c20e4f3 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 068/123] 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 82a85687e4..8cdb9cfea7 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 069/123] 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 80b02508f7..df7bb083f9 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 a0b5453025..d6f9f70d7a 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 202b58b963..0000000000 --- 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 0000000000..44267f5c34 --- /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 2584736034..b286b4e91d 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 aeb485b4c0..d8e62312cf 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, From 34ef2d3d6d44945f7fc2aec9465ee0525465fc83 Mon Sep 17 00:00:00 2001 From: Shyamsunder Tard <130305690+shyamsundertard@users.noreply.github.com> Date: Tue, 22 Oct 2024 01:12:37 +0530 Subject: [PATCH 070/123] Left Padding removed in Settings Page Tabs (#7730) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fix: #7100 The `TabList` component, located in [Tablist](packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx), wraps the Tabs and defines the padding, and is used in multiple places. The left padding for the Emails and Calendars sections of the Accounts in Settings has been removed ( list appear when there are multiple connected accounts ). However, the padding on the Record detail page remains unchanged. To address this, prop of css styles is added to `Tablist`, allowing for the padding of the `TabList` component to be adjusted as required. Additional styles can also be applied as per requirements individually for Emails and Calendar section. Screenshot 2024-10-16 at 5 06 26 AM Screenshot 2024-10-16 at 5 49 18 AM Screenshot 2024-10-16 at 6 22 30 AM --------- Co-authored-by: Charles Bochet --- .../ui/layout/show-page/components/ShowPageSubContainer.tsx | 1 + .../src/modules/ui/layout/tab/components/TabList.tsx | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) 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 6f2c0aa442..29a53033ed 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 @@ -41,6 +41,7 @@ const StyledShowPageRightContainer = styled.div<{ isMobile: boolean }>` const StyledTabListContainer = styled.div` align-items: center; + padding-left: ${({ theme }) => theme.spacing(2)}; border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`}; box-sizing: border-box; display: flex; 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 8375cc0825..eac1de4268 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 @@ -31,7 +31,6 @@ const StyledContainer = styled.div` display: flex; gap: ${({ theme }) => theme.spacing(2)}; height: 40px; - padding-left: ${({ theme }) => theme.spacing(2)}; user-select: none; `; From 25010174f0a5d216ad004384b1c86a9e39d4059e Mon Sep 17 00:00:00 2001 From: Muhammad Abdullah <118609756+abdullah5361k@users.noreply.github.com> Date: Tue, 22 Oct 2024 00:47:17 +0500 Subject: [PATCH 071/123] =?UTF-8?q?Adjust=20the=20line=20height=20and=20ex?= =?UTF-8?q?pand=20the=20maximum=20height=20of=20the=20plac=E2=80=A6=20(#77?= =?UTF-8?q?64)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes: #7757 ## What does this PR do? We increased the line height from md to lg and the max height of the placeholder subtitle text from 2.4 to 2.8 to ensure that letters are no longer slightly cut off in the placeholder in Functions. ![twenty-placeholder-text](https://github.com/user-attachments/assets/1cfed3c4-6bae-4200-9516-4e1295da170a) ## How should this be tested? 1. Log in 2. Go to Settings 3. Toggle "Advanced" settings 4. Go to Functions --------- Co-authored-by: Charles Bochet --- .../components/EmptyPlaceholderStyled.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/twenty-front/src/modules/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled.tsx b/packages/twenty-front/src/modules/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled.tsx index d088ec7e53..e942ef6c99 100644 --- a/packages/twenty-front/src/modules/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled.tsx +++ b/packages/twenty-front/src/modules/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled.tsx @@ -26,7 +26,7 @@ const StyledEmptyTextContainer = styled.div` align-items: center; display: flex; flex-direction: column; - gap: ${({ theme }) => theme.spacing(3)}; + gap: ${({ theme }) => theme.spacing(2)}; justify-content: center; text-align: center; width: 100%; @@ -46,8 +46,8 @@ const StyledEmptySubTitle = styled.div` color: ${({ theme }) => theme.font.color.tertiary}; font-size: ${({ theme }) => theme.font.size.sm}; font-weight: ${({ theme }) => theme.font.weight.regular}; - line-height: ${({ theme }) => theme.text.lineHeight.md}; - max-height: 2.4em; + line-height: ${({ theme }) => theme.text.lineHeight.lg}; + max-height: 2.8em; overflow: hidden; width: 50%; `; From b45511c955b20e9ddc1bd0025c72cd5451a83b59 Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 21:53:43 +0200 Subject: [PATCH 072/123] Migrate to twenty-ui - navigation/breadcrumb (#7793) ### Description - Move breadcrumb components to `twenty-ui` \ \ \ Fixes #7534 --------- Co-authored-by: gitstart-twenty Co-authored-by: Charles Bochet --- ...grationDatabaseConnectionShowContainer.tsx | 8 +++----- ...tegrationEditDatabaseConnectionContent.tsx | 16 +++++++--------- .../components/SubMenuTopBarContainer.tsx | 8 ++------ .../__stories__/Breadcrumb.stories.tsx | 4 +--- packages/twenty-front/tsup.ui.index.tsx | 19 +++++++++---------- packages/twenty-ui/src/index.ts | 1 + .../breadcrumb}/components/Breadcrumb.tsx | 0 packages/twenty-ui/src/navigation/index.ts | 1 + 8 files changed, 24 insertions(+), 33 deletions(-) rename packages/{twenty-front/src/modules/ui/navigation/bread-crumb => twenty-ui/src/navigation/breadcrumb}/components/Breadcrumb.tsx (100%) create mode 100644 packages/twenty-ui/src/navigation/index.ts diff --git a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionShowContainer.tsx b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionShowContainer.tsx index 1e2b75d7c2..7e294fff97 100644 --- a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionShowContainer.tsx +++ b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionShowContainer.tsx @@ -1,14 +1,12 @@ -import { Section } from '@react-email/components'; -import { useNavigate } from 'react-router-dom'; -import { H2Title } from 'twenty-ui'; - import { useDeleteOneDatabaseConnection } from '@/databases/hooks/useDeleteOneDatabaseConnection'; import { SettingsIntegrationDatabaseConnectionSummaryCard } from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionSummaryCard'; import { SettingsIntegrationDatabaseTablesListCard } from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseTablesListCard'; import { useDatabaseConnection } from '@/settings/integrations/database-connection/hooks/useDatabaseConnection'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; +import { Section } from '@react-email/components'; +import { useNavigate } from 'react-router-dom'; +import { Breadcrumb, H2Title } from 'twenty-ui'; export const SettingsIntegrationDatabaseConnectionShowContainer = () => { const navigate = useNavigate(); diff --git a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContent.tsx b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContent.tsx index b1b3ae1a03..1a4d1a5d38 100644 --- a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContent.tsx +++ b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContent.tsx @@ -1,11 +1,3 @@ -import { zodResolver } from '@hookform/resolvers/zod'; -import { Section } from '@react-email/components'; -import pick from 'lodash.pick'; -import { FormProvider, useForm } from 'react-hook-form'; -import { useNavigate } from 'react-router-dom'; -import { H2Title } from 'twenty-ui'; -import { z } from 'zod'; - import { useUpdateOneDatabaseConnection } from '@/databases/hooks/useUpdateOneDatabaseConnection'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; @@ -21,7 +13,13 @@ import { SettingsPath } from '@/types/SettingsPath'; import { Info } from '@/ui/display/info/components/Info'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { Section } from '@react-email/components'; +import pick from 'lodash.pick'; +import { FormProvider, useForm } from 'react-hook-form'; +import { useNavigate } from 'react-router-dom'; +import { Breadcrumb, H2Title } from 'twenty-ui'; +import { z } from 'zod'; import { RemoteServer, RemoteTable, diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/SubMenuTopBarContainer.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/SubMenuTopBarContainer.tsx index 9b26f71bc1..78577b08bb 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/components/SubMenuTopBarContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/components/SubMenuTopBarContainer.tsx @@ -1,11 +1,7 @@ +import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper'; import styled from '@emotion/styled'; import { JSX, ReactNode } from 'react'; - -import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper'; -import { - Breadcrumb, - BreadcrumbProps, -} from '@/ui/navigation/bread-crumb/components/Breadcrumb'; +import { Breadcrumb, BreadcrumbProps } from 'twenty-ui'; import { PageBody } from './PageBody'; import { PageHeader } from './PageHeader'; diff --git a/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/__stories__/Breadcrumb.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/__stories__/Breadcrumb.stories.tsx index a655fc6b6f..d88b0ad3e8 100644 --- a/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/__stories__/Breadcrumb.stories.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/__stories__/Breadcrumb.stories.tsx @@ -1,10 +1,8 @@ import { Meta, StoryObj } from '@storybook/react'; -import { ComponentDecorator } from 'twenty-ui'; +import { ComponentDecorator, Breadcrumb } from 'twenty-ui'; import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; -import { Breadcrumb } from '../Breadcrumb'; - const meta: Meta = { title: 'UI/Navigation/Breadcrumb/Breadcrumb', component: Breadcrumb, diff --git a/packages/twenty-front/tsup.ui.index.tsx b/packages/twenty-front/tsup.ui.index.tsx index edb0ae481f..88c2ba996d 100644 --- a/packages/twenty-front/tsup.ui.index.tsx +++ b/packages/twenty-front/tsup.ui.index.tsx @@ -1,15 +1,9 @@ import { ThemeType } from 'twenty-ui'; export { ThemeProvider } from '@emotion/react'; - -declare module '@emotion/react' { - // eslint-disable-next-line @typescript-eslint/no-empty-interface - export interface Theme extends ThemeType {} -} - export * from 'twenty-ui'; -export * from './src/modules/ui/feedback/progress-bar/components/ProgressBar'; export * from './src/modules/ui/feedback/progress-bar/components/CircularProgressBar'; +export * from './src/modules/ui/feedback/progress-bar/components/ProgressBar'; export * from './src/modules/ui/input/button/components/Button'; export * from './src/modules/ui/input/button/components/ButtonGroup'; export * from './src/modules/ui/input/button/components/FloatingButton'; @@ -17,7 +11,6 @@ export * from './src/modules/ui/input/button/components/FloatingButtonGroup'; export * from './src/modules/ui/input/button/components/FloatingIconButton'; export * from './src/modules/ui/input/button/components/FloatingIconButtonGroup'; export * from './src/modules/ui/input/button/components/LightButton'; -export * from './src/modules/ui/navigation/link/components/ActionLink'; export * from './src/modules/ui/input/button/components/LightIconButton'; export * from './src/modules/ui/input/button/components/MainButton'; export * from './src/modules/ui/input/button/components/RoundedIconButton'; @@ -30,12 +23,12 @@ export * from './src/modules/ui/input/components/IconPicker'; export * from './src/modules/ui/input/components/ImageInput'; export * from './src/modules/ui/input/components/Radio'; export * from './src/modules/ui/input/components/RadioGroup'; -export * from './src/modules/ui/input/button/components/Button'; export * from './src/modules/ui/input/components/Select'; export * from './src/modules/ui/input/components/TextArea'; export * from './src/modules/ui/input/components/TextInput'; export * from './src/modules/ui/input/components/Toggle'; export * from './src/modules/ui/input/editor/components/BlockEditor'; +export * from './src/modules/ui/navigation/link/components/ActionLink'; export * from './src/modules/ui/navigation/link/components/ContactLink'; export * from './src/modules/ui/navigation/link/components/RawLink'; export * from './src/modules/ui/navigation/link/components/RoundedLink'; @@ -50,6 +43,12 @@ export * from './src/modules/ui/navigation/menu-item/components/MenuItemSelect'; export * from './src/modules/ui/navigation/menu-item/components/MenuItemSelectAvatar'; export * from './src/modules/ui/navigation/menu-item/components/MenuItemSelectColor'; export * from './src/modules/ui/navigation/menu-item/components/MenuItemToggle'; -export * from './src/modules/ui/navigation/bread-crumb/components/Breadcrumb'; export * from './src/modules/ui/navigation/navigation-bar/components/NavigationBar'; export * from './src/modules/ui/navigation/step-bar/components/StepBar'; + +declare module '@emotion/react' { + // eslint-disable-next-line @typescript-eslint/no-empty-interface + export interface Theme extends ThemeType {} +} + + diff --git a/packages/twenty-ui/src/index.ts b/packages/twenty-ui/src/index.ts index 7518172f6b..a5a04a16ff 100644 --- a/packages/twenty-ui/src/index.ts +++ b/packages/twenty-ui/src/index.ts @@ -2,6 +2,7 @@ export * from './accessibility'; export * from './components'; export * from './display'; export * from './layout'; +export * from './navigation'; export * from './testing'; export * from './theme'; export * from './utilities'; diff --git a/packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/Breadcrumb.tsx b/packages/twenty-ui/src/navigation/breadcrumb/components/Breadcrumb.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/navigation/bread-crumb/components/Breadcrumb.tsx rename to packages/twenty-ui/src/navigation/breadcrumb/components/Breadcrumb.tsx diff --git a/packages/twenty-ui/src/navigation/index.ts b/packages/twenty-ui/src/navigation/index.ts new file mode 100644 index 0000000000..94c4c0952d --- /dev/null +++ b/packages/twenty-ui/src/navigation/index.ts @@ -0,0 +1 @@ +export * from './breadcrumb/components/Breadcrumb'; From c6bc09c5a26ea0513089ad84073919a2eb7bb38c Mon Sep 17 00:00:00 2001 From: TheFaheem <35933338+FaheemOnHub@users.noreply.github.com> Date: Tue, 22 Oct 2024 13:43:02 +0530 Subject: [PATCH 073/123] [oss.gg]: hubspot to twenty contact data migration script (750 points) (migration-script) (#7937) HubSpot to Twenty CRM Contact Migration Script Script's github repo: https://github.com/FaheemOnHub/twenty-crm-hubspot-script This Node.js script facilitates migrating contacts from HubSpot CRM to Twenty CRM. The user has the option to either check for duplicates in the Twenty CRM before migrating or directly migrate all contacts without checking. Video Proof added in oss-gg's twenty-dev-challenges --- .../1-write-migration-script-other-crm-to-20.md | 1 + 1 file changed, 1 insertion(+) diff --git a/oss-gg/twenty-dev-challenges/1-write-migration-script-other-crm-to-20.md b/oss-gg/twenty-dev-challenges/1-write-migration-script-other-crm-to-20.md index 249d8e158c..f3dbdb16ef 100644 --- a/oss-gg/twenty-dev-challenges/1-write-migration-script-other-crm-to-20.md +++ b/oss-gg/twenty-dev-challenges/1-write-migration-script-other-crm-to-20.md @@ -18,4 +18,5 @@ Your turn 👇 » 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) video Link: [video](https://twenty.com/) +» 22-October-2024 by [FaheemOnHub](https://oss.gg/FaheemOnHub) video Link: [video](https://drive.google.com/file/d/1bR59Q5gqoqHjzgdrF6K68U2hloexkQYM/view) --- \ No newline at end of file From bf0a0597519380166c37e36b720d3a22bca174f6 Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2024 10:17:10 +0200 Subject: [PATCH 074/123] [Server Integration tests] Enrich integration GraphQL API tests #3 (#7931) This PR was created by [GitStart](https://gitstart.com/) to address the requirements from this ticket: [TWNTY-7526](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-7526). --- ### Description NoteTargets and MessageThreads are special cases because they do not have a notable property that we could use in "update" the test cases,\ for NoteTargets we are using the personId, testing the relation, but for MessageThreads we are using updatedAt. To test the relations for MessageThreads\ we would need to update another object (Message) because the relation is ONE_TO_MANY, updating another object in a test that would update the current tested object sounds incorrect.\ In the NoteTargets, we can update the NoteTarget object because the relation is MANY_TO_ONE\ for some tests we need an account ID, we are using Tim's account for all the tests (the token in jest-integration.config.ts), so we are using a constant to use the account ID ### Refs #7526 ### Demo ![](https://assets-service.gitstart.com/28455/7f1c520e-78e4-43c3-aa89-f6fc09e0a056.png) --------- Co-authored-by: gitstart-twenty Co-authored-by: gitstart-twenty <140154534+gitstart-twenty@users.noreply.github.com> --- .../graphql/integration.constants.ts | 1 + ...participants-resolvers.integration-spec.ts | 479 +++++++++++++++++ ...ted-accounts-resolvers.integration-spec.ts | 420 +++++++++++++++ ...ll-favorites-resolvers.integration-spec.ts | 409 +++++++++++++++ ...associations-resolvers.integration-spec.ts | 493 ++++++++++++++++++ ...age-channels-resolvers.integration-spec.ts | 455 ++++++++++++++++ ...participants-resolvers.integration-spec.ts | 466 +++++++++++++++++ ...sage-threads-resolvers.integration-spec.ts | 397 ++++++++++++++ ...note-targets-resolvers.integration-spec.ts | 444 ++++++++++++++++ .../all-notes-resolvers.integration-spec.ts | 403 ++++++++++++++ ...pportunities-resolvers.integration-spec.ts | 414 +++++++++++++++ 11 files changed, 4381 insertions(+) create mode 100644 packages/twenty-server/test/integration/graphql/integration.constants.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/all-calendar-event-participants-resolvers.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/all-connected-accounts-resolvers.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/all-favorites-resolvers.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/all-message-channel-message-associations-resolvers.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/all-message-channels-resolvers.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/all-message-participants-resolvers.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/all-message-threads-resolvers.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/all-note-targets-resolvers.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/all-notes-resolvers.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/all-opportunities-resolvers.integration-spec.ts diff --git a/packages/twenty-server/test/integration/graphql/integration.constants.ts b/packages/twenty-server/test/integration/graphql/integration.constants.ts new file mode 100644 index 0000000000..e5391d40b1 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/integration.constants.ts @@ -0,0 +1 @@ +export const TIM_ACCOUNT_ID = '20202020-0687-4c41-b707-ed1bfca972a7'; diff --git a/packages/twenty-server/test/integration/graphql/suites/all-calendar-event-participants-resolvers.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/all-calendar-event-participants-resolvers.integration-spec.ts new file mode 100644 index 0000000000..118533f2f3 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/all-calendar-event-participants-resolvers.integration-spec.ts @@ -0,0 +1,479 @@ +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 CALENDAR_EVENT_PARTICIPANT_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; +const CALENDAR_EVENT_PARTICIPANT_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; +const CALENDAR_EVENT_PARTICIPANT_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; +const CALENDAR_EVENT_ID = '777a8457-eb2d-40ac-a707-441b615b6989'; +const CALENDAR_EVENT_PARTICIPANT_GQL_FIELDS = ` + id + handle + displayName + isOrganizer + responseStatus + deletedAt +`; + +describe('calendarEventParticipants resolvers (integration)', () => { + beforeAll(async () => { + const calendarEventTitle = generateRecordName(CALENDAR_EVENT_ID); + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'calendarEvent', + gqlFields: `id`, + data: { + id: CALENDAR_EVENT_ID, + title: calendarEventTitle, + }, + }); + + await makeGraphqlAPIRequest(graphqlOperation); + }); + + afterAll(async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'calendarEvent', + gqlFields: `id`, + recordId: CALENDAR_EVENT_ID, + }); + + await makeGraphqlAPIRequest(graphqlOperation); + }); + + it('1. should create and return calendarEventParticipants', async () => { + const calendarEventParticipantDisplayName1 = generateRecordName( + CALENDAR_EVENT_PARTICIPANT_1_ID, + ); + const calendarEventParticipantDisplayName2 = generateRecordName( + CALENDAR_EVENT_PARTICIPANT_2_ID, + ); + + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'calendarEventParticipant', + objectMetadataPluralName: 'calendarEventParticipants', + gqlFields: CALENDAR_EVENT_PARTICIPANT_GQL_FIELDS, + data: [ + { + id: CALENDAR_EVENT_PARTICIPANT_1_ID, + displayName: calendarEventParticipantDisplayName1, + calendarEventId: CALENDAR_EVENT_ID, + }, + { + id: CALENDAR_EVENT_PARTICIPANT_2_ID, + displayName: calendarEventParticipantDisplayName2, + calendarEventId: CALENDAR_EVENT_ID, + }, + ], + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.createCalendarEventParticipants).toHaveLength(2); + + response.body.data.createCalendarEventParticipants.forEach( + (calendarEventParticipant) => { + expect(calendarEventParticipant).toHaveProperty('displayName'); + expect([ + calendarEventParticipantDisplayName1, + calendarEventParticipantDisplayName2, + ]).toContain(calendarEventParticipant.displayName); + + expect(calendarEventParticipant).toHaveProperty('id'); + expect(calendarEventParticipant).toHaveProperty('handle'); + expect(calendarEventParticipant).toHaveProperty('isOrganizer'); + expect(calendarEventParticipant).toHaveProperty('responseStatus'); + expect(calendarEventParticipant).toHaveProperty('deletedAt'); + }, + ); + }); + + it('1b. should create and return one calendarEventParticipant', async () => { + const calendarEventParticipantDisplayName = generateRecordName( + CALENDAR_EVENT_PARTICIPANT_3_ID, + ); + + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'calendarEventParticipant', + gqlFields: CALENDAR_EVENT_PARTICIPANT_GQL_FIELDS, + data: { + id: CALENDAR_EVENT_PARTICIPANT_3_ID, + displayName: calendarEventParticipantDisplayName, + calendarEventId: CALENDAR_EVENT_ID, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdCalendarEventParticipant = + response.body.data.createCalendarEventParticipant; + + expect(createdCalendarEventParticipant).toHaveProperty('displayName'); + expect(createdCalendarEventParticipant.displayName).toEqual( + calendarEventParticipantDisplayName, + ); + + expect(createdCalendarEventParticipant).toHaveProperty('id'); + expect(createdCalendarEventParticipant).toHaveProperty('handle'); + expect(createdCalendarEventParticipant).toHaveProperty('isOrganizer'); + expect(createdCalendarEventParticipant).toHaveProperty('responseStatus'); + expect(createdCalendarEventParticipant).toHaveProperty('deletedAt'); + }); + + it('2. should find many calendarEventParticipants', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'calendarEventParticipant', + objectMetadataPluralName: 'calendarEventParticipants', + gqlFields: CALENDAR_EVENT_PARTICIPANT_GQL_FIELDS, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const data = response.body.data.calendarEventParticipants; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const calendarEventParticipants = edges[0].node; + + expect(calendarEventParticipants).toHaveProperty('displayName'); + expect(calendarEventParticipants).toHaveProperty('id'); + expect(calendarEventParticipants).toHaveProperty('handle'); + expect(calendarEventParticipants).toHaveProperty('isOrganizer'); + expect(calendarEventParticipants).toHaveProperty('responseStatus'); + expect(calendarEventParticipants).toHaveProperty('deletedAt'); + } + }); + + it('2b. should find one calendarEventParticipant', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'calendarEventParticipant', + gqlFields: CALENDAR_EVENT_PARTICIPANT_GQL_FIELDS, + filter: { + id: { + eq: CALENDAR_EVENT_PARTICIPANT_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const calendarEventParticipant = + response.body.data.calendarEventParticipant; + + expect(calendarEventParticipant).toHaveProperty('displayName'); + + expect(calendarEventParticipant).toHaveProperty('id'); + expect(calendarEventParticipant).toHaveProperty('handle'); + expect(calendarEventParticipant).toHaveProperty('isOrganizer'); + expect(calendarEventParticipant).toHaveProperty('responseStatus'); + expect(calendarEventParticipant).toHaveProperty('deletedAt'); + }); + + it('3. should update many calendarEventParticipants', async () => { + const graphqlOperation = updateManyOperationFactory({ + objectMetadataSingularName: 'calendarEventParticipant', + objectMetadataPluralName: 'calendarEventParticipants', + gqlFields: CALENDAR_EVENT_PARTICIPANT_GQL_FIELDS, + data: { + displayName: 'New DisplayName', + }, + filter: { + id: { + in: [ + CALENDAR_EVENT_PARTICIPANT_1_ID, + CALENDAR_EVENT_PARTICIPANT_2_ID, + ], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedcalendarEventParticipants = + response.body.data.updateCalendarEventParticipants; + + expect(updatedcalendarEventParticipants).toHaveLength(2); + + updatedcalendarEventParticipants.forEach((calendarEventParticipant) => { + expect(calendarEventParticipant.displayName).toEqual('New DisplayName'); + }); + }); + + it('3b. should update one calendarEventParticipant', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'calendarEventParticipant', + gqlFields: CALENDAR_EVENT_PARTICIPANT_GQL_FIELDS, + data: { + displayName: 'Updated DisplayName', + }, + recordId: CALENDAR_EVENT_PARTICIPANT_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedcalendarEventParticipant = + response.body.data.updateCalendarEventParticipant; + + expect(updatedcalendarEventParticipant.displayName).toEqual( + 'Updated DisplayName', + ); + }); + + it('4. should find many calendarEventParticipants with updated displayName', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'calendarEventParticipant', + objectMetadataPluralName: 'calendarEventParticipants', + gqlFields: CALENDAR_EVENT_PARTICIPANT_GQL_FIELDS, + filter: { + displayName: { + eq: 'New DisplayName', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.calendarEventParticipants.edges).toHaveLength(2); + }); + + it('4b. should find one calendarEventParticipant with updated displayName', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'calendarEventParticipant', + gqlFields: CALENDAR_EVENT_PARTICIPANT_GQL_FIELDS, + filter: { + displayName: { + eq: 'Updated DisplayName', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.calendarEventParticipant.displayName).toEqual( + 'Updated DisplayName', + ); + }); + + it('5. should delete many calendarEventParticipants', async () => { + const graphqlOperation = deleteManyOperationFactory({ + objectMetadataSingularName: 'calendarEventParticipant', + objectMetadataPluralName: 'calendarEventParticipants', + gqlFields: CALENDAR_EVENT_PARTICIPANT_GQL_FIELDS, + filter: { + id: { + in: [ + CALENDAR_EVENT_PARTICIPANT_1_ID, + CALENDAR_EVENT_PARTICIPANT_2_ID, + ], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const deleteCalendarEventParticipants = + response.body.data.deleteCalendarEventParticipants; + + expect(deleteCalendarEventParticipants).toHaveLength(2); + + deleteCalendarEventParticipants.forEach((calendarEventParticipant) => { + expect(calendarEventParticipant.deletedAt).toBeTruthy(); + }); + }); + + it('5b. should delete one calendarEventParticipant', async () => { + const graphqlOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'calendarEventParticipant', + gqlFields: CALENDAR_EVENT_PARTICIPANT_GQL_FIELDS, + recordId: CALENDAR_EVENT_PARTICIPANT_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect( + response.body.data.deleteCalendarEventParticipant.deletedAt, + ).toBeTruthy(); + }); + + it('6. should not find many calendarEventParticipants anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'calendarEventParticipant', + objectMetadataPluralName: 'calendarEventParticipants', + gqlFields: CALENDAR_EVENT_PARTICIPANT_GQL_FIELDS, + filter: { + id: { + in: [ + CALENDAR_EVENT_PARTICIPANT_1_ID, + CALENDAR_EVENT_PARTICIPANT_2_ID, + ], + }, + }, + }); + + const findCalendarEventParticipantsResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + findCalendarEventParticipantsResponse.body.data.calendarEventParticipants + .edges, + ).toHaveLength(0); + }); + + it('6b. should not find one calendarEventParticipant anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'calendarEventParticipant', + gqlFields: CALENDAR_EVENT_PARTICIPANT_GQL_FIELDS, + filter: { + id: { + eq: CALENDAR_EVENT_PARTICIPANT_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.calendarEventParticipant).toBeNull(); + }); + + it('7. should find many deleted calendarEventParticipants with deletedAt filter', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'calendarEventParticipant', + objectMetadataPluralName: 'calendarEventParticipants', + gqlFields: CALENDAR_EVENT_PARTICIPANT_GQL_FIELDS, + filter: { + id: { + in: [ + CALENDAR_EVENT_PARTICIPANT_1_ID, + CALENDAR_EVENT_PARTICIPANT_2_ID, + ], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.calendarEventParticipants.edges).toHaveLength(2); + }); + + it('7b. should find one deleted calendarEventParticipant with deletedAt filter', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'calendarEventParticipant', + gqlFields: CALENDAR_EVENT_PARTICIPANT_GQL_FIELDS, + filter: { + id: { + eq: CALENDAR_EVENT_PARTICIPANT_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.calendarEventParticipant.id).toEqual( + CALENDAR_EVENT_PARTICIPANT_3_ID, + ); + }); + + it('8. should destroy many calendarEventParticipants', async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'calendarEventParticipant', + objectMetadataPluralName: 'calendarEventParticipants', + gqlFields: CALENDAR_EVENT_PARTICIPANT_GQL_FIELDS, + filter: { + id: { + in: [ + CALENDAR_EVENT_PARTICIPANT_1_ID, + CALENDAR_EVENT_PARTICIPANT_2_ID, + ], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.destroyCalendarEventParticipants).toHaveLength(2); + }); + + it('8b. should destroy one calendarEventParticipant', async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'calendarEventParticipant', + gqlFields: CALENDAR_EVENT_PARTICIPANT_GQL_FIELDS, + recordId: CALENDAR_EVENT_PARTICIPANT_3_ID, + }); + + const destroyCalendarEventParticipantResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + destroyCalendarEventParticipantResponse.body.data + .destroyCalendarEventParticipant, + ).toBeTruthy(); + }); + + it('9. should not find many calendarEventParticipants anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'calendarEventParticipant', + objectMetadataPluralName: 'calendarEventParticipants', + gqlFields: CALENDAR_EVENT_PARTICIPANT_GQL_FIELDS, + filter: { + id: { + in: [ + CALENDAR_EVENT_PARTICIPANT_1_ID, + CALENDAR_EVENT_PARTICIPANT_2_ID, + ], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.calendarEventParticipants.edges).toHaveLength(0); + }); + + it('9b. should not find one calendarEventParticipant anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'calendarEventParticipant', + gqlFields: CALENDAR_EVENT_PARTICIPANT_GQL_FIELDS, + filter: { + id: { + eq: CALENDAR_EVENT_PARTICIPANT_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.calendarEventParticipant).toBeNull(); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/all-connected-accounts-resolvers.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/all-connected-accounts-resolvers.integration-spec.ts new file mode 100644 index 0000000000..d64e559006 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/all-connected-accounts-resolvers.integration-spec.ts @@ -0,0 +1,420 @@ +import { TIM_ACCOUNT_ID } from 'test/integration/graphql/integration.constants'; +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 CONNECTED_ACCOUNT_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; +const CONNECTED_ACCOUNT_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; +const CONNECTED_ACCOUNT_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; +const CONNECTED_ACCOUNT_GQL_FIELDS = ` + id + handle + deletedAt + createdAt + provider + accessToken + scopes +`; + +describe('connectedAccounts resolvers (integration)', () => { + it('1. should create and return connectedAccounts', async () => { + const connectedAccountHandle1 = generateRecordName(CONNECTED_ACCOUNT_1_ID); + const connectedAccountHandle2 = generateRecordName(CONNECTED_ACCOUNT_2_ID); + + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + objectMetadataPluralName: 'connectedAccounts', + gqlFields: CONNECTED_ACCOUNT_GQL_FIELDS, + data: [ + { + id: CONNECTED_ACCOUNT_1_ID, + handle: connectedAccountHandle1, + accountOwnerId: TIM_ACCOUNT_ID, + }, + { + id: CONNECTED_ACCOUNT_2_ID, + handle: connectedAccountHandle2, + accountOwnerId: TIM_ACCOUNT_ID, + }, + ], + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.createConnectedAccounts).toHaveLength(2); + + response.body.data.createConnectedAccounts.forEach((connectedAccount) => { + expect(connectedAccount).toHaveProperty('handle'); + expect([connectedAccountHandle1, connectedAccountHandle2]).toContain( + connectedAccount.handle, + ); + + expect(connectedAccount).toHaveProperty('id'); + expect(connectedAccount).toHaveProperty('deletedAt'); + expect(connectedAccount).toHaveProperty('createdAt'); + expect(connectedAccount).toHaveProperty('provider'); + expect(connectedAccount).toHaveProperty('accessToken'); + expect(connectedAccount).toHaveProperty('scopes'); + }); + }); + + it('1b. should create and return one connectedAccount', async () => { + const connectedAccountHandle = generateRecordName(CONNECTED_ACCOUNT_3_ID); + + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + gqlFields: CONNECTED_ACCOUNT_GQL_FIELDS, + data: { + id: CONNECTED_ACCOUNT_3_ID, + handle: connectedAccountHandle, + accountOwnerId: TIM_ACCOUNT_ID, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdConnectedAccount = response.body.data.createConnectedAccount; + + expect(createdConnectedAccount).toHaveProperty('handle'); + expect(createdConnectedAccount.handle).toEqual(connectedAccountHandle); + + expect(createdConnectedAccount).toHaveProperty('id'); + expect(createdConnectedAccount).toHaveProperty('deletedAt'); + expect(createdConnectedAccount).toHaveProperty('createdAt'); + expect(createdConnectedAccount).toHaveProperty('provider'); + expect(createdConnectedAccount).toHaveProperty('accessToken'); + expect(createdConnectedAccount).toHaveProperty('scopes'); + }); + + it('2. should find many connectedAccounts', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + objectMetadataPluralName: 'connectedAccounts', + gqlFields: CONNECTED_ACCOUNT_GQL_FIELDS, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const data = response.body.data.connectedAccounts; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const connectedAccounts = edges[0].node; + + expect(connectedAccounts).toHaveProperty('handle'); + expect(connectedAccounts).toHaveProperty('id'); + expect(connectedAccounts).toHaveProperty('deletedAt'); + expect(connectedAccounts).toHaveProperty('createdAt'); + expect(connectedAccounts).toHaveProperty('provider'); + expect(connectedAccounts).toHaveProperty('accessToken'); + expect(connectedAccounts).toHaveProperty('scopes'); + } + }); + + it('2b. should find one connectedAccount', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + gqlFields: CONNECTED_ACCOUNT_GQL_FIELDS, + filter: { + id: { + eq: CONNECTED_ACCOUNT_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const connectedAccount = response.body.data.connectedAccount; + + expect(connectedAccount).toHaveProperty('handle'); + + expect(connectedAccount).toHaveProperty('id'); + expect(connectedAccount).toHaveProperty('deletedAt'); + expect(connectedAccount).toHaveProperty('createdAt'); + expect(connectedAccount).toHaveProperty('provider'); + expect(connectedAccount).toHaveProperty('accessToken'); + expect(connectedAccount).toHaveProperty('scopes'); + }); + + it('3. should update many connectedAccounts', async () => { + const graphqlOperation = updateManyOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + objectMetadataPluralName: 'connectedAccounts', + gqlFields: CONNECTED_ACCOUNT_GQL_FIELDS, + data: { + handle: 'New Handle', + }, + filter: { + id: { + in: [CONNECTED_ACCOUNT_1_ID, CONNECTED_ACCOUNT_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedconnectedAccounts = response.body.data.updateConnectedAccounts; + + expect(updatedconnectedAccounts).toHaveLength(2); + + updatedconnectedAccounts.forEach((connectedAccount) => { + expect(connectedAccount.handle).toEqual('New Handle'); + }); + }); + + it('3b. should update one connectedAccount', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + gqlFields: CONNECTED_ACCOUNT_GQL_FIELDS, + data: { + handle: 'Updated Handle', + }, + recordId: CONNECTED_ACCOUNT_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedconnectedAccount = response.body.data.updateConnectedAccount; + + expect(updatedconnectedAccount.handle).toEqual('Updated Handle'); + }); + + it('4. should find many connectedAccounts with updated handle', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + objectMetadataPluralName: 'connectedAccounts', + gqlFields: CONNECTED_ACCOUNT_GQL_FIELDS, + filter: { + handle: { + eq: 'New Handle', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.connectedAccounts.edges).toHaveLength(2); + }); + + it('4b. should find one connectedAccount with updated handle', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + gqlFields: CONNECTED_ACCOUNT_GQL_FIELDS, + filter: { + handle: { + eq: 'Updated Handle', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.connectedAccount.handle).toEqual( + 'Updated Handle', + ); + }); + + it('5. should delete many connectedAccounts', async () => { + const graphqlOperation = deleteManyOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + objectMetadataPluralName: 'connectedAccounts', + gqlFields: CONNECTED_ACCOUNT_GQL_FIELDS, + filter: { + id: { + in: [CONNECTED_ACCOUNT_1_ID, CONNECTED_ACCOUNT_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const deleteConnectedAccounts = response.body.data.deleteConnectedAccounts; + + expect(deleteConnectedAccounts).toHaveLength(2); + + deleteConnectedAccounts.forEach((connectedAccount) => { + expect(connectedAccount.deletedAt).toBeTruthy(); + }); + }); + + it('5b. should delete one connectedAccount', async () => { + const graphqlOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + gqlFields: CONNECTED_ACCOUNT_GQL_FIELDS, + recordId: CONNECTED_ACCOUNT_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.deleteConnectedAccount.deletedAt).toBeTruthy(); + }); + + it('6. should not find many connectedAccounts anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + objectMetadataPluralName: 'connectedAccounts', + gqlFields: CONNECTED_ACCOUNT_GQL_FIELDS, + filter: { + id: { + in: [CONNECTED_ACCOUNT_1_ID, CONNECTED_ACCOUNT_2_ID], + }, + }, + }); + + const findConnectedAccountsResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + findConnectedAccountsResponse.body.data.connectedAccounts.edges, + ).toHaveLength(0); + }); + + it('6b. should not find one connectedAccount anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + gqlFields: CONNECTED_ACCOUNT_GQL_FIELDS, + filter: { + id: { + eq: CONNECTED_ACCOUNT_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.connectedAccount).toBeNull(); + }); + + it('7. should find many deleted connectedAccounts with deletedAt filter', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + objectMetadataPluralName: 'connectedAccounts', + gqlFields: CONNECTED_ACCOUNT_GQL_FIELDS, + filter: { + id: { + in: [CONNECTED_ACCOUNT_1_ID, CONNECTED_ACCOUNT_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.connectedAccounts.edges).toHaveLength(2); + }); + + it('7b. should find one deleted connectedAccount with deletedAt filter', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + gqlFields: CONNECTED_ACCOUNT_GQL_FIELDS, + filter: { + id: { + eq: CONNECTED_ACCOUNT_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.connectedAccount.id).toEqual( + CONNECTED_ACCOUNT_3_ID, + ); + }); + + it('8. should destroy many connectedAccounts', async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + objectMetadataPluralName: 'connectedAccounts', + gqlFields: CONNECTED_ACCOUNT_GQL_FIELDS, + filter: { + id: { + in: [CONNECTED_ACCOUNT_1_ID, CONNECTED_ACCOUNT_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.destroyConnectedAccounts).toHaveLength(2); + }); + + it('8b. should destroy one connectedAccount', async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + gqlFields: CONNECTED_ACCOUNT_GQL_FIELDS, + recordId: CONNECTED_ACCOUNT_3_ID, + }); + + const destroyConnectedAccountResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + destroyConnectedAccountResponse.body.data.destroyConnectedAccount, + ).toBeTruthy(); + }); + + it('9. should not find many connectedAccounts anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + objectMetadataPluralName: 'connectedAccounts', + gqlFields: CONNECTED_ACCOUNT_GQL_FIELDS, + filter: { + id: { + in: [CONNECTED_ACCOUNT_1_ID, CONNECTED_ACCOUNT_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.connectedAccounts.edges).toHaveLength(0); + }); + + it('9b. should not find one connectedAccount anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + gqlFields: CONNECTED_ACCOUNT_GQL_FIELDS, + filter: { + id: { + eq: CONNECTED_ACCOUNT_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.connectedAccount).toBeNull(); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/all-favorites-resolvers.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/all-favorites-resolvers.integration-spec.ts new file mode 100644 index 0000000000..5ad4852588 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/all-favorites-resolvers.integration-spec.ts @@ -0,0 +1,409 @@ +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'; + +const FAVORITE_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; +const FAVORITE_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; +const FAVORITE_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; +const INITIAL_FAVORITE_POSITION_1 = 1111111; +const INITIAL_FAVORITE_POSITION_2 = 2222222; +const INITIAL_FAVORITE_POSITION_3 = 3333333; +const NEW_FAVORITE_POSITION_1 = 4444444; +const NEW_FAVORITE_POSITION_2 = 5555555; +const FAVORITE_GQL_FIELDS = ` + id + position + createdAt + updatedAt + deletedAt + companyId + personId +`; + +describe('favorites resolvers (integration)', () => { + it('1. should create and return favorites', async () => { + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'favorite', + objectMetadataPluralName: 'favorites', + gqlFields: FAVORITE_GQL_FIELDS, + data: [ + { + id: FAVORITE_1_ID, + position: INITIAL_FAVORITE_POSITION_1, + }, + { + id: FAVORITE_2_ID, + position: INITIAL_FAVORITE_POSITION_2, + }, + ], + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.createFavorites).toHaveLength(2); + + response.body.data.createFavorites.forEach((favorite) => { + expect(favorite).toHaveProperty('position'); + expect([ + INITIAL_FAVORITE_POSITION_1, + INITIAL_FAVORITE_POSITION_2, + ]).toContain(favorite.position); + + expect(favorite).toHaveProperty('id'); + expect(favorite).toHaveProperty('createdAt'); + expect(favorite).toHaveProperty('updatedAt'); + expect(favorite).toHaveProperty('deletedAt'); + expect(favorite).toHaveProperty('companyId'); + expect(favorite).toHaveProperty('personId'); + }); + }); + + it('1b. should create and return one favorite', async () => { + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'favorite', + gqlFields: FAVORITE_GQL_FIELDS, + data: { + id: FAVORITE_3_ID, + position: INITIAL_FAVORITE_POSITION_3, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdFavorite = response.body.data.createFavorite; + + expect(createdFavorite).toHaveProperty('position'); + expect(createdFavorite.position).toEqual(INITIAL_FAVORITE_POSITION_3); + + expect(createdFavorite).toHaveProperty('id'); + expect(createdFavorite).toHaveProperty('createdAt'); + expect(createdFavorite).toHaveProperty('updatedAt'); + expect(createdFavorite).toHaveProperty('deletedAt'); + expect(createdFavorite).toHaveProperty('companyId'); + expect(createdFavorite).toHaveProperty('personId'); + }); + + it('2. should find many favorites', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'favorite', + objectMetadataPluralName: 'favorites', + gqlFields: FAVORITE_GQL_FIELDS, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const data = response.body.data.favorites; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const favorite = edges[0].node; + + expect(favorite).toHaveProperty('position'); + expect(favorite).toHaveProperty('id'); + expect(favorite).toHaveProperty('createdAt'); + expect(favorite).toHaveProperty('updatedAt'); + expect(favorite).toHaveProperty('deletedAt'); + expect(favorite).toHaveProperty('companyId'); + expect(favorite).toHaveProperty('personId'); + } + }); + + it('2b. should find one favorite', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'favorite', + gqlFields: FAVORITE_GQL_FIELDS, + filter: { + id: { + eq: FAVORITE_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const favorite = response.body.data.favorite; + + expect(favorite).toHaveProperty('position'); + + expect(favorite).toHaveProperty('id'); + expect(favorite).toHaveProperty('createdAt'); + expect(favorite).toHaveProperty('updatedAt'); + expect(favorite).toHaveProperty('deletedAt'); + expect(favorite).toHaveProperty('companyId'); + expect(favorite).toHaveProperty('personId'); + }); + + it('3. should update many favorites', async () => { + const graphqlOperation = updateManyOperationFactory({ + objectMetadataSingularName: 'favorite', + objectMetadataPluralName: 'favorites', + gqlFields: FAVORITE_GQL_FIELDS, + data: { + position: NEW_FAVORITE_POSITION_1, + }, + filter: { + id: { + in: [FAVORITE_1_ID, FAVORITE_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedFavorites = response.body.data.updateFavorites; + + expect(updatedFavorites).toHaveLength(2); + + updatedFavorites.forEach((favorite) => { + expect(favorite.position).toEqual(NEW_FAVORITE_POSITION_1); + }); + }); + + it('3b. should update one favorite', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'favorite', + gqlFields: FAVORITE_GQL_FIELDS, + data: { + position: NEW_FAVORITE_POSITION_2, + }, + recordId: FAVORITE_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedFavorite = response.body.data.updateFavorite; + + expect(updatedFavorite.position).toEqual(NEW_FAVORITE_POSITION_2); + }); + + it('4. should find many favorites with updated position', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'favorite', + objectMetadataPluralName: 'favorites', + gqlFields: FAVORITE_GQL_FIELDS, + filter: { + position: { + eq: NEW_FAVORITE_POSITION_1, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.favorites.edges).toHaveLength(2); + }); + + it('4b. should find one favorite with updated position', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'favorite', + gqlFields: FAVORITE_GQL_FIELDS, + filter: { + position: { + eq: NEW_FAVORITE_POSITION_2, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.favorite.position).toEqual( + NEW_FAVORITE_POSITION_2, + ); + }); + + it('5. should delete many favorites', async () => { + const graphqlOperation = deleteManyOperationFactory({ + objectMetadataSingularName: 'favorite', + objectMetadataPluralName: 'favorites', + gqlFields: FAVORITE_GQL_FIELDS, + filter: { + id: { + in: [FAVORITE_1_ID, FAVORITE_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const deleteFavorites = response.body.data.deleteFavorites; + + expect(deleteFavorites).toHaveLength(2); + + deleteFavorites.forEach((favorite) => { + expect(favorite.deletedAt).toBeTruthy(); + }); + }); + + it('5b. should delete one favorite', async () => { + const graphqlOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'favorite', + gqlFields: FAVORITE_GQL_FIELDS, + recordId: FAVORITE_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.deleteFavorite.deletedAt).toBeTruthy(); + }); + + it('6. should not find many favorites anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'favorite', + objectMetadataPluralName: 'favorites', + gqlFields: FAVORITE_GQL_FIELDS, + filter: { + id: { + in: [FAVORITE_1_ID, FAVORITE_2_ID], + }, + }, + }); + + const findFavoritesResponse = await makeGraphqlAPIRequest(graphqlOperation); + + expect(findFavoritesResponse.body.data.favorites.edges).toHaveLength(0); + }); + + it('6b. should not find one favorite anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'favorite', + gqlFields: FAVORITE_GQL_FIELDS, + filter: { + id: { + eq: FAVORITE_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.favorite).toBeNull(); + }); + + it('7. should find many deleted favorites with deletedAt filter', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'favorite', + objectMetadataPluralName: 'favorites', + gqlFields: FAVORITE_GQL_FIELDS, + filter: { + id: { + in: [FAVORITE_1_ID, FAVORITE_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.favorites.edges).toHaveLength(2); + }); + + it('7b. should find one deleted favorite with deletedAt filter', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'favorite', + gqlFields: FAVORITE_GQL_FIELDS, + filter: { + id: { + eq: FAVORITE_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.favorite.id).toEqual(FAVORITE_3_ID); + }); + + it('8. should destroy many favorites', async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'favorite', + objectMetadataPluralName: 'favorites', + gqlFields: FAVORITE_GQL_FIELDS, + filter: { + id: { + in: [FAVORITE_1_ID, FAVORITE_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.destroyFavorites).toHaveLength(2); + }); + + it('8b. should destroy one favorite', async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'favorite', + gqlFields: FAVORITE_GQL_FIELDS, + recordId: FAVORITE_3_ID, + }); + + const destroyFavoriteResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect(destroyFavoriteResponse.body.data.destroyFavorite).toBeTruthy(); + }); + + it('9. should not find many favorites anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'favorite', + objectMetadataPluralName: 'favorites', + gqlFields: FAVORITE_GQL_FIELDS, + filter: { + id: { + in: [FAVORITE_1_ID, FAVORITE_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.favorites.edges).toHaveLength(0); + }); + + it('9b. should not find one favorite anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'favorite', + gqlFields: FAVORITE_GQL_FIELDS, + filter: { + id: { + eq: FAVORITE_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.favorite).toBeNull(); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/all-message-channel-message-associations-resolvers.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/all-message-channel-message-associations-resolvers.integration-spec.ts new file mode 100644 index 0000000000..2613311067 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/all-message-channel-message-associations-resolvers.integration-spec.ts @@ -0,0 +1,493 @@ +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 MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_1_ID = + '777a8457-eb2d-40ac-a707-551b615b6987'; +const MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_2_ID = + '777a8457-eb2d-40ac-a707-551b615b6988'; +const MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_3_ID = + '777a8457-eb2d-40ac-a707-551b615b6989'; + +const MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_GQL_FIELDS = ` + id + messageExternalId + createdAt + updatedAt + deletedAt + messageChannelId + messageId + direction +`; + +describe('messageChannelMessageAssociations resolvers (integration)', () => { + it('1. should create and return messageChannelMessageAssociations', async () => { + const messageExternalId1 = generateRecordName( + MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_1_ID, + ); + const messageExternalId2 = generateRecordName( + MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_2_ID, + ); + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'messageChannelMessageAssociation', + objectMetadataPluralName: 'messageChannelMessageAssociations', + gqlFields: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_GQL_FIELDS, + data: [ + { + id: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_1_ID, + messageExternalId: messageExternalId1, + }, + { + id: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_2_ID, + messageExternalId: messageExternalId2, + }, + ], + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect( + response.body.data.createMessageChannelMessageAssociations, + ).toHaveLength(2); + + response.body.data.createMessageChannelMessageAssociations.forEach( + (messageChannelMessageAssociation) => { + expect(messageChannelMessageAssociation).toHaveProperty( + 'messageExternalId', + ); + expect([messageExternalId1, messageExternalId2]).toContain( + messageChannelMessageAssociation.messageExternalId, + ); + + expect(messageChannelMessageAssociation).toHaveProperty('id'); + expect(messageChannelMessageAssociation).toHaveProperty('createdAt'); + expect(messageChannelMessageAssociation).toHaveProperty('updatedAt'); + expect(messageChannelMessageAssociation).toHaveProperty('deletedAt'); + expect(messageChannelMessageAssociation).toHaveProperty( + 'messageChannelId', + ); + expect(messageChannelMessageAssociation).toHaveProperty('messageId'); + expect(messageChannelMessageAssociation).toHaveProperty('direction'); + }, + ); + }); + + it('1b. should create and return one messageChannelMessageAssociation', async () => { + const messageExternalId3 = generateRecordName( + MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_3_ID, + ); + + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'messageChannelMessageAssociation', + gqlFields: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_GQL_FIELDS, + data: { + id: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_3_ID, + messageExternalId: messageExternalId3, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdMessageChannelMessageAssociation = + response.body.data.createMessageChannelMessageAssociation; + + expect(createdMessageChannelMessageAssociation).toHaveProperty( + 'messageExternalId', + ); + expect(createdMessageChannelMessageAssociation.messageExternalId).toEqual( + messageExternalId3, + ); + + expect(createdMessageChannelMessageAssociation).toHaveProperty('id'); + expect(createdMessageChannelMessageAssociation).toHaveProperty('createdAt'); + expect(createdMessageChannelMessageAssociation).toHaveProperty('updatedAt'); + expect(createdMessageChannelMessageAssociation).toHaveProperty('deletedAt'); + expect(createdMessageChannelMessageAssociation).toHaveProperty( + 'messageChannelId', + ); + expect(createdMessageChannelMessageAssociation).toHaveProperty('messageId'); + expect(createdMessageChannelMessageAssociation).toHaveProperty('direction'); + }); + + it('2. should find many messageChannelMessageAssociations', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageChannelMessageAssociation', + objectMetadataPluralName: 'messageChannelMessageAssociations', + gqlFields: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_GQL_FIELDS, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const data = response.body.data.messageChannelMessageAssociations; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const messageChannelMessageAssociation = edges[0].node; + + expect(messageChannelMessageAssociation).toHaveProperty( + 'messageExternalId', + ); + expect(messageChannelMessageAssociation).toHaveProperty('id'); + expect(messageChannelMessageAssociation).toHaveProperty('createdAt'); + expect(messageChannelMessageAssociation).toHaveProperty('updatedAt'); + expect(messageChannelMessageAssociation).toHaveProperty('deletedAt'); + expect(messageChannelMessageAssociation).toHaveProperty( + 'messageChannelId', + ); + expect(messageChannelMessageAssociation).toHaveProperty('messageId'); + expect(messageChannelMessageAssociation).toHaveProperty('direction'); + } + }); + + it('2b. should find one messageChannelMessageAssociation', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageChannelMessageAssociation', + gqlFields: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_GQL_FIELDS, + filter: { + id: { + eq: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const messageChannelMessageAssociation = + response.body.data.messageChannelMessageAssociation; + + expect(messageChannelMessageAssociation).toHaveProperty( + 'messageExternalId', + ); + + expect(messageChannelMessageAssociation).toHaveProperty('id'); + expect(messageChannelMessageAssociation).toHaveProperty('createdAt'); + expect(messageChannelMessageAssociation).toHaveProperty('updatedAt'); + expect(messageChannelMessageAssociation).toHaveProperty('deletedAt'); + expect(messageChannelMessageAssociation).toHaveProperty('messageChannelId'); + expect(messageChannelMessageAssociation).toHaveProperty('messageId'); + expect(messageChannelMessageAssociation).toHaveProperty('direction'); + }); + + it('3. should update many messageChannelMessageAssociations', async () => { + const graphqlOperation = updateManyOperationFactory({ + objectMetadataSingularName: 'messageChannelMessageAssociation', + objectMetadataPluralName: 'messageChannelMessageAssociations', + gqlFields: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_GQL_FIELDS, + data: { + messageExternalId: 'updated-message-external-id', + }, + filter: { + id: { + in: [ + MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_1_ID, + MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_2_ID, + ], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedmessageChannelMessageAssociations = + response.body.data.updateMessageChannelMessageAssociations; + + expect(updatedmessageChannelMessageAssociations).toHaveLength(2); + + updatedmessageChannelMessageAssociations.forEach( + (messageChannelMessageAssociation) => { + expect(messageChannelMessageAssociation.messageExternalId).toEqual( + 'updated-message-external-id', + ); + }, + ); + }); + + it('3b. should update one messageChannelMessageAssociation', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'messageChannelMessageAssociation', + gqlFields: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_GQL_FIELDS, + data: { + messageExternalId: 'new-message-external-id', + }, + recordId: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedmessageChannelMessageAssociation = + response.body.data.updateMessageChannelMessageAssociation; + + expect(updatedmessageChannelMessageAssociation.messageExternalId).toEqual( + 'new-message-external-id', + ); + }); + + it('4. should find many messageChannelMessageAssociations with updated messageExternalId', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageChannelMessageAssociation', + objectMetadataPluralName: 'messageChannelMessageAssociations', + gqlFields: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_GQL_FIELDS, + filter: { + messageExternalId: { + eq: 'updated-message-external-id', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect( + response.body.data.messageChannelMessageAssociations.edges, + ).toHaveLength(2); + }); + + it('4b. should find one messageChannelMessageAssociation with updated messageExternalId', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageChannelMessageAssociation', + gqlFields: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_GQL_FIELDS, + filter: { + messageExternalId: { + eq: 'new-message-external-id', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect( + response.body.data.messageChannelMessageAssociation.messageExternalId, + ).toEqual('new-message-external-id'); + }); + + it('5. should delete many messageChannelMessageAssociations', async () => { + const graphqlOperation = deleteManyOperationFactory({ + objectMetadataSingularName: 'messageChannelMessageAssociation', + objectMetadataPluralName: 'messageChannelMessageAssociations', + gqlFields: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_GQL_FIELDS, + filter: { + id: { + in: [ + MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_1_ID, + MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_2_ID, + ], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const deleteMessageChannelMessageAssociations = + response.body.data.deleteMessageChannelMessageAssociations; + + expect(deleteMessageChannelMessageAssociations).toHaveLength(2); + + deleteMessageChannelMessageAssociations.forEach( + (messageChannelMessageAssociation) => { + expect(messageChannelMessageAssociation.deletedAt).toBeTruthy(); + }, + ); + }); + + it('5b. should delete one messageChannelMessageAssociation', async () => { + const graphqlOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'messageChannelMessageAssociation', + gqlFields: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_GQL_FIELDS, + recordId: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect( + response.body.data.deleteMessageChannelMessageAssociation.deletedAt, + ).toBeTruthy(); + }); + + it('6. should not find many messageChannelMessageAssociations anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageChannelMessageAssociation', + objectMetadataPluralName: 'messageChannelMessageAssociations', + gqlFields: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_GQL_FIELDS, + filter: { + id: { + in: [ + MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_1_ID, + MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_2_ID, + ], + }, + }, + }); + + const findMessageChannelMessageAssociationsResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + findMessageChannelMessageAssociationsResponse.body.data + .messageChannelMessageAssociations.edges, + ).toHaveLength(0); + }); + + it('6b. should not find one messageChannelMessageAssociation anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageChannelMessageAssociation', + gqlFields: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_GQL_FIELDS, + filter: { + id: { + eq: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageChannelMessageAssociation).toBeNull(); + }); + + it('7. should find many deleted messageChannelMessageAssociations with deletedAt filter', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageChannelMessageAssociation', + objectMetadataPluralName: 'messageChannelMessageAssociations', + gqlFields: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_GQL_FIELDS, + filter: { + id: { + in: [ + MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_1_ID, + MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_2_ID, + ], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect( + response.body.data.messageChannelMessageAssociations.edges, + ).toHaveLength(2); + }); + + it('7b. should find one deleted messageChannelMessageAssociation with deletedAt filter', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageChannelMessageAssociation', + gqlFields: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_GQL_FIELDS, + filter: { + id: { + eq: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageChannelMessageAssociation.id).toEqual( + MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_3_ID, + ); + }); + + it('8. should destroy many messageChannelMessageAssociations', async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'messageChannelMessageAssociation', + objectMetadataPluralName: 'messageChannelMessageAssociations', + gqlFields: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_GQL_FIELDS, + filter: { + id: { + in: [ + MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_1_ID, + MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_2_ID, + ], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect( + response.body.data.destroyMessageChannelMessageAssociations, + ).toHaveLength(2); + }); + + it('8b. should destroy one messageChannelMessageAssociation', async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'messageChannelMessageAssociation', + gqlFields: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_GQL_FIELDS, + recordId: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_3_ID, + }); + + const destroyMessageChannelMessageAssociationResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + destroyMessageChannelMessageAssociationResponse.body.data + .destroyMessageChannelMessageAssociation, + ).toBeTruthy(); + }); + + it('9. should not find many messageChannelMessageAssociations anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageChannelMessageAssociation', + objectMetadataPluralName: 'messageChannelMessageAssociations', + gqlFields: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_GQL_FIELDS, + filter: { + id: { + in: [ + MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_1_ID, + MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_2_ID, + ], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect( + response.body.data.messageChannelMessageAssociations.edges, + ).toHaveLength(0); + }); + + it('9b. should not find one messageChannelMessageAssociation anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageChannelMessageAssociation', + gqlFields: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_GQL_FIELDS, + filter: { + id: { + eq: MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageChannelMessageAssociation).toBeNull(); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/all-message-channels-resolvers.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/all-message-channels-resolvers.integration-spec.ts new file mode 100644 index 0000000000..473efced95 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/all-message-channels-resolvers.integration-spec.ts @@ -0,0 +1,455 @@ +import { TIM_ACCOUNT_ID } from 'test/integration/graphql/integration.constants'; +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 MESSAGE_CHANNEL_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; +const MESSAGE_CHANNEL_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; +const MESSAGE_CHANNEL_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; +const CONNECTED_ACCOUNT_ID = '777a8457-eb2d-40ac-a707-441b615b6989'; + +const MESSAGE_CHANNEL_GQL_FIELDS = ` + id + handle + deletedAt + createdAt + contactAutoCreationPolicy + isContactAutoCreationEnabled + isSyncEnabled + syncCursor + type +`; + +describe('messageChannels resolvers (integration)', () => { + beforeAll(async () => { + const connectedAccountHandle = generateRecordName(CONNECTED_ACCOUNT_ID); + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + gqlFields: `id`, + data: { + id: CONNECTED_ACCOUNT_ID, + accountOwnerId: TIM_ACCOUNT_ID, + handle: connectedAccountHandle, + }, + }); + + await makeGraphqlAPIRequest(graphqlOperation); + }); + + afterAll(async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + gqlFields: `id`, + recordId: CONNECTED_ACCOUNT_ID, + }); + + await makeGraphqlAPIRequest(graphqlOperation); + }); + + it('1. should create and return messageChannels', async () => { + const messageChannelHandle1 = generateRecordName(MESSAGE_CHANNEL_1_ID); + const messageChannelHandle2 = generateRecordName(MESSAGE_CHANNEL_2_ID); + + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'messageChannel', + objectMetadataPluralName: 'messageChannels', + gqlFields: MESSAGE_CHANNEL_GQL_FIELDS, + data: [ + { + id: MESSAGE_CHANNEL_1_ID, + handle: messageChannelHandle1, + connectedAccountId: CONNECTED_ACCOUNT_ID, + }, + { + id: MESSAGE_CHANNEL_2_ID, + handle: messageChannelHandle2, + connectedAccountId: CONNECTED_ACCOUNT_ID, + }, + ], + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.createMessageChannels).toHaveLength(2); + + response.body.data.createMessageChannels.forEach((messageChannel) => { + expect(messageChannel).toHaveProperty('handle'); + expect([messageChannelHandle1, messageChannelHandle2]).toContain( + messageChannel.handle, + ); + + expect(messageChannel).toHaveProperty('id'); + expect(messageChannel).toHaveProperty('deletedAt'); + expect(messageChannel).toHaveProperty('createdAt'); + expect(messageChannel).toHaveProperty('contactAutoCreationPolicy'); + expect(messageChannel).toHaveProperty('isContactAutoCreationEnabled'); + expect(messageChannel).toHaveProperty('isSyncEnabled'); + expect(messageChannel).toHaveProperty('syncCursor'); + expect(messageChannel).toHaveProperty('type'); + }); + }); + + it('1b. should create and return one messageChannel', async () => { + const messageChannelHandle = generateRecordName(MESSAGE_CHANNEL_3_ID); + + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'messageChannel', + gqlFields: MESSAGE_CHANNEL_GQL_FIELDS, + data: { + id: MESSAGE_CHANNEL_3_ID, + handle: messageChannelHandle, + connectedAccountId: CONNECTED_ACCOUNT_ID, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdMessageChannel = response.body.data.createMessageChannel; + + expect(createdMessageChannel).toHaveProperty('handle'); + expect(createdMessageChannel.handle).toEqual(messageChannelHandle); + + expect(createdMessageChannel).toHaveProperty('id'); + expect(createdMessageChannel).toHaveProperty('deletedAt'); + expect(createdMessageChannel).toHaveProperty('createdAt'); + expect(createdMessageChannel).toHaveProperty('contactAutoCreationPolicy'); + expect(createdMessageChannel).toHaveProperty( + 'isContactAutoCreationEnabled', + ); + expect(createdMessageChannel).toHaveProperty('isSyncEnabled'); + expect(createdMessageChannel).toHaveProperty('syncCursor'); + expect(createdMessageChannel).toHaveProperty('type'); + }); + + it('2. should find many messageChannels', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageChannel', + objectMetadataPluralName: 'messageChannels', + gqlFields: MESSAGE_CHANNEL_GQL_FIELDS, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const data = response.body.data.messageChannels; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const messageChannel = edges[0].node; + + expect(messageChannel).toHaveProperty('handle'); + expect(messageChannel).toHaveProperty('id'); + expect(messageChannel).toHaveProperty('deletedAt'); + expect(messageChannel).toHaveProperty('createdAt'); + expect(messageChannel).toHaveProperty('contactAutoCreationPolicy'); + expect(messageChannel).toHaveProperty('isContactAutoCreationEnabled'); + expect(messageChannel).toHaveProperty('isSyncEnabled'); + expect(messageChannel).toHaveProperty('syncCursor'); + expect(messageChannel).toHaveProperty('type'); + } + }); + + it('2b. should find one messageChannel', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageChannel', + gqlFields: MESSAGE_CHANNEL_GQL_FIELDS, + filter: { + id: { + eq: MESSAGE_CHANNEL_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const messageChannel = response.body.data.messageChannel; + + expect(messageChannel).toHaveProperty('handle'); + + expect(messageChannel).toHaveProperty('id'); + expect(messageChannel).toHaveProperty('deletedAt'); + expect(messageChannel).toHaveProperty('createdAt'); + expect(messageChannel).toHaveProperty('contactAutoCreationPolicy'); + expect(messageChannel).toHaveProperty('isContactAutoCreationEnabled'); + expect(messageChannel).toHaveProperty('isSyncEnabled'); + expect(messageChannel).toHaveProperty('syncCursor'); + expect(messageChannel).toHaveProperty('type'); + }); + + it('3. should update many messageChannels', async () => { + const graphqlOperation = updateManyOperationFactory({ + objectMetadataSingularName: 'messageChannel', + objectMetadataPluralName: 'messageChannels', + gqlFields: MESSAGE_CHANNEL_GQL_FIELDS, + data: { + handle: 'New Handle', + }, + filter: { + id: { + in: [MESSAGE_CHANNEL_1_ID, MESSAGE_CHANNEL_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedMessageChannels = response.body.data.updateMessageChannels; + + expect(updatedMessageChannels).toHaveLength(2); + + updatedMessageChannels.forEach((messageChannel) => { + expect(messageChannel.handle).toEqual('New Handle'); + }); + }); + + it('3b. should update one messageChannel', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'messageChannel', + gqlFields: MESSAGE_CHANNEL_GQL_FIELDS, + data: { + handle: 'Updated Handle', + }, + recordId: MESSAGE_CHANNEL_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedMessageChannel = response.body.data.updateMessageChannel; + + expect(updatedMessageChannel.handle).toEqual('Updated Handle'); + }); + + it('4. should find many messageChannels with updated handle', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageChannel', + objectMetadataPluralName: 'messageChannels', + gqlFields: MESSAGE_CHANNEL_GQL_FIELDS, + filter: { + handle: { + eq: 'New Handle', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageChannels.edges).toHaveLength(2); + }); + + it('4b. should find one messageChannel with updated handle', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageChannel', + gqlFields: MESSAGE_CHANNEL_GQL_FIELDS, + filter: { + handle: { + eq: 'Updated Handle', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageChannel.handle).toEqual('Updated Handle'); + }); + + it('5. should delete many messageChannels', async () => { + const graphqlOperation = deleteManyOperationFactory({ + objectMetadataSingularName: 'messageChannel', + objectMetadataPluralName: 'messageChannels', + gqlFields: MESSAGE_CHANNEL_GQL_FIELDS, + filter: { + id: { + in: [MESSAGE_CHANNEL_1_ID, MESSAGE_CHANNEL_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const deleteMessageChannels = response.body.data.deleteMessageChannels; + + expect(deleteMessageChannels).toHaveLength(2); + + deleteMessageChannels.forEach((messageChannel) => { + expect(messageChannel.deletedAt).toBeTruthy(); + }); + }); + + it('5b. should delete one messageChannel', async () => { + const graphqlOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'messageChannel', + gqlFields: MESSAGE_CHANNEL_GQL_FIELDS, + recordId: MESSAGE_CHANNEL_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.deleteMessageChannel.deletedAt).toBeTruthy(); + }); + + it('6. should not find many messageChannels anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageChannel', + objectMetadataPluralName: 'messageChannels', + gqlFields: MESSAGE_CHANNEL_GQL_FIELDS, + filter: { + id: { + in: [MESSAGE_CHANNEL_1_ID, MESSAGE_CHANNEL_2_ID], + }, + }, + }); + + const findMessageChannelsResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + findMessageChannelsResponse.body.data.messageChannels.edges, + ).toHaveLength(0); + }); + + it('6b. should not find one messageChannel anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageChannel', + gqlFields: MESSAGE_CHANNEL_GQL_FIELDS, + filter: { + id: { + eq: MESSAGE_CHANNEL_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageChannel).toBeNull(); + }); + + it('7. should find many deleted messageChannels with deletedAt filter', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageChannel', + objectMetadataPluralName: 'messageChannels', + gqlFields: MESSAGE_CHANNEL_GQL_FIELDS, + filter: { + id: { + in: [MESSAGE_CHANNEL_1_ID, MESSAGE_CHANNEL_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageChannels.edges).toHaveLength(2); + }); + + it('7b. should find one deleted messageChannel with deletedAt filter', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageChannel', + gqlFields: MESSAGE_CHANNEL_GQL_FIELDS, + filter: { + id: { + eq: MESSAGE_CHANNEL_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageChannel.id).toEqual(MESSAGE_CHANNEL_3_ID); + }); + + it('8. should destroy many messageChannels', async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'messageChannel', + objectMetadataPluralName: 'messageChannels', + gqlFields: MESSAGE_CHANNEL_GQL_FIELDS, + filter: { + id: { + in: [MESSAGE_CHANNEL_1_ID, MESSAGE_CHANNEL_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.destroyMessageChannels).toHaveLength(2); + }); + + it('8b. should destroy one messageChannel', async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'messageChannel', + gqlFields: MESSAGE_CHANNEL_GQL_FIELDS, + recordId: MESSAGE_CHANNEL_3_ID, + }); + + const destroyMessageChannelResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + destroyMessageChannelResponse.body.data.destroyMessageChannel, + ).toBeTruthy(); + }); + + it('9. should not find many messageChannels anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageChannel', + objectMetadataPluralName: 'messageChannels', + gqlFields: MESSAGE_CHANNEL_GQL_FIELDS, + filter: { + id: { + in: [MESSAGE_CHANNEL_1_ID, MESSAGE_CHANNEL_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageChannels.edges).toHaveLength(0); + }); + + it('9b. should not find one messageChannel anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageChannel', + gqlFields: MESSAGE_CHANNEL_GQL_FIELDS, + filter: { + id: { + eq: MESSAGE_CHANNEL_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageChannel).toBeNull(); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/all-message-participants-resolvers.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/all-message-participants-resolvers.integration-spec.ts new file mode 100644 index 0000000000..3c064564b5 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/all-message-participants-resolvers.integration-spec.ts @@ -0,0 +1,466 @@ +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 MESSAGE_PARTICIPANT_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; +const MESSAGE_PARTICIPANT_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; +const MESSAGE_PARTICIPANT_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; +const MESSAGE_ID = '777a8457-eb2d-40ac-a707-441b615b6989'; +const MESSAGE_PARTICIPANT_GQL_FIELDS = ` + id + displayName + handle + role + messageId + workspaceMemberId + createdAt + deletedAt +`; + +describe('messageParticipants resolvers (integration)', () => { + beforeAll(async () => { + const messageSubject = generateRecordName(MESSAGE_ID); + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'message', + gqlFields: `id`, + data: { + id: MESSAGE_ID, + subject: messageSubject, + }, + }); + + await makeGraphqlAPIRequest(graphqlOperation); + }); + + afterAll(async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'message', + gqlFields: `id`, + recordId: MESSAGE_ID, + }); + + await makeGraphqlAPIRequest(graphqlOperation); + }); + + it('1. should create and return messageParticipants', async () => { + const messageParticipantDisplayName1 = generateRecordName( + MESSAGE_PARTICIPANT_1_ID, + ); + const messageParticipantDisplayName2 = generateRecordName( + MESSAGE_PARTICIPANT_2_ID, + ); + + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'messageParticipant', + objectMetadataPluralName: 'messageParticipants', + gqlFields: MESSAGE_PARTICIPANT_GQL_FIELDS, + data: [ + { + id: MESSAGE_PARTICIPANT_1_ID, + displayName: messageParticipantDisplayName1, + messageId: MESSAGE_ID, + }, + { + id: MESSAGE_PARTICIPANT_2_ID, + displayName: messageParticipantDisplayName2, + messageId: MESSAGE_ID, + }, + ], + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.createMessageParticipants).toHaveLength(2); + + response.body.data.createMessageParticipants.forEach( + (messageParticipant) => { + expect(messageParticipant).toHaveProperty('displayName'); + expect([ + messageParticipantDisplayName1, + messageParticipantDisplayName2, + ]).toContain(messageParticipant.displayName); + + expect(messageParticipant).toHaveProperty('id'); + expect(messageParticipant).toHaveProperty('handle'); + expect(messageParticipant).toHaveProperty('role'); + expect(messageParticipant).toHaveProperty('messageId'); + expect(messageParticipant).toHaveProperty('workspaceMemberId'); + expect(messageParticipant).toHaveProperty('createdAt'); + expect(messageParticipant).toHaveProperty('deletedAt'); + }, + ); + }); + + it('1b. should create and return one messageParticipant', async () => { + const messageParticipantDisplayName = generateRecordName( + MESSAGE_PARTICIPANT_3_ID, + ); + + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'messageParticipant', + gqlFields: MESSAGE_PARTICIPANT_GQL_FIELDS, + data: { + id: MESSAGE_PARTICIPANT_3_ID, + displayName: messageParticipantDisplayName, + messageId: MESSAGE_ID, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdMessageParticipant = + response.body.data.createMessageParticipant; + + expect(createdMessageParticipant).toHaveProperty('displayName'); + expect(createdMessageParticipant.displayName).toEqual( + messageParticipantDisplayName, + ); + + expect(createdMessageParticipant).toHaveProperty('id'); + expect(createdMessageParticipant).toHaveProperty('handle'); + expect(createdMessageParticipant).toHaveProperty('role'); + expect(createdMessageParticipant).toHaveProperty('messageId'); + expect(createdMessageParticipant).toHaveProperty('workspaceMemberId'); + expect(createdMessageParticipant).toHaveProperty('createdAt'); + expect(createdMessageParticipant).toHaveProperty('deletedAt'); + }); + + it('2. should find many messageParticipants', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageParticipant', + objectMetadataPluralName: 'messageParticipants', + gqlFields: MESSAGE_PARTICIPANT_GQL_FIELDS, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const data = response.body.data.messageParticipants; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const messageParticipant = edges[0].node; + + expect(messageParticipant).toHaveProperty('displayName'); + expect(messageParticipant).toHaveProperty('id'); + expect(messageParticipant).toHaveProperty('handle'); + expect(messageParticipant).toHaveProperty('role'); + expect(messageParticipant).toHaveProperty('messageId'); + expect(messageParticipant).toHaveProperty('workspaceMemberId'); + expect(messageParticipant).toHaveProperty('createdAt'); + expect(messageParticipant).toHaveProperty('deletedAt'); + } + }); + + it('2b. should find one messageParticipant', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageParticipant', + gqlFields: MESSAGE_PARTICIPANT_GQL_FIELDS, + filter: { + id: { + eq: MESSAGE_PARTICIPANT_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const messageParticipant = response.body.data.messageParticipant; + + expect(messageParticipant).toHaveProperty('displayName'); + + expect(messageParticipant).toHaveProperty('id'); + expect(messageParticipant).toHaveProperty('handle'); + expect(messageParticipant).toHaveProperty('role'); + expect(messageParticipant).toHaveProperty('messageId'); + expect(messageParticipant).toHaveProperty('workspaceMemberId'); + expect(messageParticipant).toHaveProperty('createdAt'); + expect(messageParticipant).toHaveProperty('deletedAt'); + }); + + it('3. should update many messageParticipants', async () => { + const graphqlOperation = updateManyOperationFactory({ + objectMetadataSingularName: 'messageParticipant', + objectMetadataPluralName: 'messageParticipants', + gqlFields: MESSAGE_PARTICIPANT_GQL_FIELDS, + data: { + displayName: 'New DisplayName', + }, + filter: { + id: { + in: [MESSAGE_PARTICIPANT_1_ID, MESSAGE_PARTICIPANT_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedmessageParticipants = + response.body.data.updateMessageParticipants; + + expect(updatedmessageParticipants).toHaveLength(2); + + updatedmessageParticipants.forEach((messageParticipant) => { + expect(messageParticipant.displayName).toEqual('New DisplayName'); + }); + }); + + it('3b. should update one messageParticipant', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'messageParticipant', + gqlFields: MESSAGE_PARTICIPANT_GQL_FIELDS, + data: { + displayName: 'Updated DisplayName', + }, + recordId: MESSAGE_PARTICIPANT_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedmessageParticipant = + response.body.data.updateMessageParticipant; + + expect(updatedmessageParticipant.displayName).toEqual( + 'Updated DisplayName', + ); + }); + + it('4. should find many messageParticipants with updated displayName', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageParticipant', + objectMetadataPluralName: 'messageParticipants', + gqlFields: MESSAGE_PARTICIPANT_GQL_FIELDS, + filter: { + displayName: { + eq: 'New DisplayName', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageParticipants.edges).toHaveLength(2); + }); + + it('4b. should find one messageParticipant with updated displayName', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageParticipant', + gqlFields: MESSAGE_PARTICIPANT_GQL_FIELDS, + filter: { + displayName: { + eq: 'Updated DisplayName', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageParticipant.displayName).toEqual( + 'Updated DisplayName', + ); + }); + + it('5. should delete many messageParticipants', async () => { + const graphqlOperation = deleteManyOperationFactory({ + objectMetadataSingularName: 'messageParticipant', + objectMetadataPluralName: 'messageParticipants', + gqlFields: MESSAGE_PARTICIPANT_GQL_FIELDS, + filter: { + id: { + in: [MESSAGE_PARTICIPANT_1_ID, MESSAGE_PARTICIPANT_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const deleteMessageParticipants = + response.body.data.deleteMessageParticipants; + + expect(deleteMessageParticipants).toHaveLength(2); + + deleteMessageParticipants.forEach((messageParticipant) => { + expect(messageParticipant.deletedAt).toBeTruthy(); + }); + }); + + it('5b. should delete one messageParticipant', async () => { + const graphqlOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'messageParticipant', + gqlFields: MESSAGE_PARTICIPANT_GQL_FIELDS, + recordId: MESSAGE_PARTICIPANT_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.deleteMessageParticipant.deletedAt).toBeTruthy(); + }); + + it('6. should not find many messageParticipants anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageParticipant', + objectMetadataPluralName: 'messageParticipants', + gqlFields: MESSAGE_PARTICIPANT_GQL_FIELDS, + filter: { + id: { + in: [MESSAGE_PARTICIPANT_1_ID, MESSAGE_PARTICIPANT_2_ID], + }, + }, + }); + + const findMessageParticipantsResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + findMessageParticipantsResponse.body.data.messageParticipants.edges, + ).toHaveLength(0); + }); + + it('6b. should not find one messageParticipant anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageParticipant', + gqlFields: MESSAGE_PARTICIPANT_GQL_FIELDS, + filter: { + id: { + eq: MESSAGE_PARTICIPANT_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageParticipant).toBeNull(); + }); + + it('7. should find many deleted messageParticipants with deletedAt filter', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageParticipant', + objectMetadataPluralName: 'messageParticipants', + gqlFields: MESSAGE_PARTICIPANT_GQL_FIELDS, + filter: { + id: { + in: [MESSAGE_PARTICIPANT_1_ID, MESSAGE_PARTICIPANT_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageParticipants.edges).toHaveLength(2); + }); + + it('7b. should find one deleted messageParticipant with deletedAt filter', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageParticipant', + gqlFields: MESSAGE_PARTICIPANT_GQL_FIELDS, + filter: { + id: { + eq: MESSAGE_PARTICIPANT_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageParticipant.id).toEqual( + MESSAGE_PARTICIPANT_3_ID, + ); + }); + + it('8. should destroy many messageParticipants', async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'messageParticipant', + objectMetadataPluralName: 'messageParticipants', + gqlFields: MESSAGE_PARTICIPANT_GQL_FIELDS, + filter: { + id: { + in: [MESSAGE_PARTICIPANT_1_ID, MESSAGE_PARTICIPANT_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.destroyMessageParticipants).toHaveLength(2); + }); + + it('8b. should destroy one messageParticipant', async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'messageParticipant', + gqlFields: MESSAGE_PARTICIPANT_GQL_FIELDS, + recordId: MESSAGE_PARTICIPANT_3_ID, + }); + + const destroyMessageParticipantResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + destroyMessageParticipantResponse.body.data.destroyMessageParticipant, + ).toBeTruthy(); + }); + + it('9. should not find many messageParticipants anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageParticipant', + objectMetadataPluralName: 'messageParticipants', + gqlFields: MESSAGE_PARTICIPANT_GQL_FIELDS, + filter: { + id: { + in: [MESSAGE_PARTICIPANT_1_ID, MESSAGE_PARTICIPANT_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageParticipants.edges).toHaveLength(0); + }); + + it('9b. should not find one messageParticipant anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageParticipant', + gqlFields: MESSAGE_PARTICIPANT_GQL_FIELDS, + filter: { + id: { + eq: MESSAGE_PARTICIPANT_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageParticipant).toBeNull(); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/all-message-threads-resolvers.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/all-message-threads-resolvers.integration-spec.ts new file mode 100644 index 0000000000..a2d905cd90 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/all-message-threads-resolvers.integration-spec.ts @@ -0,0 +1,397 @@ +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'; + +const MESSAGE_THREAD_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; +const MESSAGE_THREAD_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; +const MESSAGE_THREAD_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; +const UPDATED_AT_1 = new Date('10/10/2024'); +const UPDATED_AT_2 = new Date('10/20/2024'); + +const MESSAGE_THREAD_GQL_FIELDS = ` + id + updatedAt + createdAt + deletedAt + messages{ + edges{ + node{ + id + } + } + } +`; + +describe('messageThreads resolvers (integration)', () => { + it('1. should create and return messageThreads', async () => { + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'messageThread', + objectMetadataPluralName: 'messageThreads', + gqlFields: MESSAGE_THREAD_GQL_FIELDS, + data: [ + { + id: MESSAGE_THREAD_1_ID, + }, + { + id: MESSAGE_THREAD_2_ID, + }, + ], + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.createMessageThreads).toHaveLength(2); + + response.body.data.createMessageThreads.forEach((messageThread) => { + expect(messageThread).toHaveProperty('id'); + expect(messageThread).toHaveProperty('updatedAt'); + expect(messageThread).toHaveProperty('createdAt'); + expect(messageThread).toHaveProperty('deletedAt'); + expect(messageThread).toHaveProperty('messages'); + }); + }); + + it('1b. should create and return one messageThread', async () => { + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'messageThread', + gqlFields: MESSAGE_THREAD_GQL_FIELDS, + data: { + id: MESSAGE_THREAD_3_ID, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdMessageThread = response.body.data.createMessageThread; + + expect(createdMessageThread).toHaveProperty('id'); + expect(createdMessageThread).toHaveProperty('updatedAt'); + expect(createdMessageThread).toHaveProperty('createdAt'); + expect(createdMessageThread).toHaveProperty('deletedAt'); + expect(createdMessageThread).toHaveProperty('messages'); + }); + + it('2. should find many messageThreads', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageThread', + objectMetadataPluralName: 'messageThreads', + gqlFields: MESSAGE_THREAD_GQL_FIELDS, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const data = response.body.data.messageThreads; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const messageThread = edges[0].node; + + expect(messageThread).toHaveProperty('id'); + expect(messageThread).toHaveProperty('updatedAt'); + expect(messageThread).toHaveProperty('createdAt'); + expect(messageThread).toHaveProperty('deletedAt'); + expect(messageThread).toHaveProperty('messages'); + } + }); + + it('2b. should find one messageThread', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageThread', + gqlFields: MESSAGE_THREAD_GQL_FIELDS, + filter: { + id: { + eq: MESSAGE_THREAD_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const messageThread = response.body.data.messageThread; + + expect(messageThread).toHaveProperty('id'); + expect(messageThread).toHaveProperty('updatedAt'); + expect(messageThread).toHaveProperty('createdAt'); + expect(messageThread).toHaveProperty('deletedAt'); + expect(messageThread).toHaveProperty('messages'); + }); + + it('3. should update many messageThreads', async () => { + const graphqlOperation = updateManyOperationFactory({ + objectMetadataSingularName: 'messageThread', + objectMetadataPluralName: 'messageThreads', + gqlFields: MESSAGE_THREAD_GQL_FIELDS, + data: { + updatedAt: UPDATED_AT_1, + }, + filter: { + id: { + in: [MESSAGE_THREAD_1_ID, MESSAGE_THREAD_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedMessageThreads = response.body.data.updateMessageThreads; + + expect(updatedMessageThreads).toHaveLength(2); + + updatedMessageThreads.forEach((messageThread) => { + expect(messageThread.updatedAt).toEqual(UPDATED_AT_1.toISOString()); + }); + }); + + it('3b. should update one messageThread', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'messageThread', + gqlFields: MESSAGE_THREAD_GQL_FIELDS, + data: { + updatedAt: UPDATED_AT_2, + }, + recordId: MESSAGE_THREAD_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedMessageThread = response.body.data.updateMessageThread; + + expect(updatedMessageThread.updatedAt).toEqual(UPDATED_AT_2.toISOString()); + }); + + it('4. should find many messageThreads with updated updatedAt', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageThread', + objectMetadataPluralName: 'messageThreads', + gqlFields: MESSAGE_THREAD_GQL_FIELDS, + filter: { + updatedAt: { + eq: UPDATED_AT_1, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageThreads.edges).toHaveLength(2); + }); + + it('4b. should find one messageThread with updated updatedAt', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageThread', + gqlFields: MESSAGE_THREAD_GQL_FIELDS, + filter: { + updatedAt: { + eq: UPDATED_AT_2, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageThread.updatedAt).toEqual( + UPDATED_AT_2.toISOString(), + ); + }); + + it('5. should delete many messageThreads', async () => { + const graphqlOperation = deleteManyOperationFactory({ + objectMetadataSingularName: 'messageThread', + objectMetadataPluralName: 'messageThreads', + gqlFields: MESSAGE_THREAD_GQL_FIELDS, + filter: { + id: { + in: [MESSAGE_THREAD_1_ID, MESSAGE_THREAD_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const deleteMessageThreads = response.body.data.deleteMessageThreads; + + expect(deleteMessageThreads).toHaveLength(2); + + deleteMessageThreads.forEach((messageThread) => { + expect(messageThread.deletedAt).toBeTruthy(); + }); + }); + + it('5b. should delete one messageThread', async () => { + const graphqlOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'messageThread', + gqlFields: MESSAGE_THREAD_GQL_FIELDS, + recordId: MESSAGE_THREAD_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.deleteMessageThread.deletedAt).toBeTruthy(); + }); + + it('6. should not find many messageThreads anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageThread', + objectMetadataPluralName: 'messageThreads', + gqlFields: MESSAGE_THREAD_GQL_FIELDS, + filter: { + id: { + in: [MESSAGE_THREAD_1_ID, MESSAGE_THREAD_2_ID], + }, + }, + }); + + const findMessageThreadsResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + findMessageThreadsResponse.body.data.messageThreads.edges, + ).toHaveLength(0); + }); + + it('6b. should not find one messageThread anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageThread', + gqlFields: MESSAGE_THREAD_GQL_FIELDS, + filter: { + id: { + eq: MESSAGE_THREAD_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageThread).toBeNull(); + }); + + it('7. should find many deleted messageThreads with deletedAt filter', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageThread', + objectMetadataPluralName: 'messageThreads', + gqlFields: MESSAGE_THREAD_GQL_FIELDS, + filter: { + id: { + in: [MESSAGE_THREAD_1_ID, MESSAGE_THREAD_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageThreads.edges).toHaveLength(2); + }); + + it('7b. should find one deleted messageThread with deletedAt filter', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageThread', + gqlFields: MESSAGE_THREAD_GQL_FIELDS, + filter: { + id: { + eq: MESSAGE_THREAD_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageThread.id).toEqual(MESSAGE_THREAD_3_ID); + }); + + it('8. should destroy many messageThreads', async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'messageThread', + objectMetadataPluralName: 'messageThreads', + gqlFields: MESSAGE_THREAD_GQL_FIELDS, + filter: { + id: { + in: [MESSAGE_THREAD_1_ID, MESSAGE_THREAD_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.destroyMessageThreads).toHaveLength(2); + }); + + it('8b. should destroy one messageThread', async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'messageThread', + gqlFields: MESSAGE_THREAD_GQL_FIELDS, + recordId: MESSAGE_THREAD_3_ID, + }); + + const destroyMessageThreadsResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + destroyMessageThreadsResponse.body.data.destroyMessageThread, + ).toBeTruthy(); + }); + + it('9. should not find many messageThreads anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'messageThread', + objectMetadataPluralName: 'messageThreads', + gqlFields: MESSAGE_THREAD_GQL_FIELDS, + filter: { + id: { + in: [MESSAGE_THREAD_1_ID, MESSAGE_THREAD_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageThreads.edges).toHaveLength(0); + }); + + it('9b. should not find one messageThread anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'messageThread', + gqlFields: MESSAGE_THREAD_GQL_FIELDS, + filter: { + id: { + eq: MESSAGE_THREAD_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.messageThread).toBeNull(); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/all-note-targets-resolvers.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/all-note-targets-resolvers.integration-spec.ts new file mode 100644 index 0000000000..9b4d9fa0aa --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/all-note-targets-resolvers.integration-spec.ts @@ -0,0 +1,444 @@ +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 NOTE_TARGET_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; +const NOTE_TARGET_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; +const NOTE_TARGET_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; +const PERSON_1_ID = '777a8457-eb2d-40ac-a707-441b615b6989'; +const PERSON_2_ID = '777a8457-eb2d-40ac-a707-331b615b6989'; +const NOTE_TARGET_GQL_FIELDS = ` + id + createdAt + deletedAt + noteId + personId + companyId + opportunityId + person{ + id + } +`; + +describe('noteTargets resolvers (integration)', () => { + beforeAll(async () => { + const personName1 = generateRecordName(PERSON_1_ID); + const personName2 = generateRecordName(PERSON_2_ID); + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'person', + objectMetadataPluralName: 'people', + gqlFields: `id`, + data: [ + { + id: PERSON_1_ID, + name: { + firstName: personName1, + lastName: personName1, + }, + }, + { + id: PERSON_2_ID, + name: { + firstName: personName2, + lastName: personName2, + }, + }, + ], + }); + + await makeGraphqlAPIRequest(graphqlOperation); + }); + + afterAll(async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'person', + objectMetadataPluralName: 'people', + gqlFields: `id`, + filter: { + id: { + in: [PERSON_1_ID, PERSON_2_ID], + }, + }, + }); + + await makeGraphqlAPIRequest(graphqlOperation); + }); + it('1. should create and return noteTargets', async () => { + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'noteTarget', + objectMetadataPluralName: 'noteTargets', + gqlFields: NOTE_TARGET_GQL_FIELDS, + data: [ + { + id: NOTE_TARGET_1_ID, + }, + { + id: NOTE_TARGET_2_ID, + }, + ], + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.createNoteTargets).toHaveLength(2); + + response.body.data.createNoteTargets.forEach((noteTarget) => { + expect(noteTarget).toHaveProperty('id'); + expect(noteTarget).toHaveProperty('createdAt'); + expect(noteTarget).toHaveProperty('deletedAt'); + expect(noteTarget).toHaveProperty('noteId'); + expect(noteTarget).toHaveProperty('personId'); + expect(noteTarget).toHaveProperty('companyId'); + expect(noteTarget).toHaveProperty('opportunityId'); + expect(noteTarget).toHaveProperty('person'); + }); + }); + + it('1b. should create and return one noteTarget', async () => { + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'noteTarget', + gqlFields: NOTE_TARGET_GQL_FIELDS, + data: { + id: NOTE_TARGET_3_ID, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdNoteTarget = response.body.data.createNoteTarget; + + expect(createdNoteTarget).toHaveProperty('id'); + expect(createdNoteTarget).toHaveProperty('createdAt'); + expect(createdNoteTarget).toHaveProperty('deletedAt'); + expect(createdNoteTarget).toHaveProperty('noteId'); + expect(createdNoteTarget).toHaveProperty('personId'); + expect(createdNoteTarget).toHaveProperty('companyId'); + expect(createdNoteTarget).toHaveProperty('opportunityId'); + expect(createdNoteTarget).toHaveProperty('person'); + }); + + it('2. should find many noteTargets', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'noteTarget', + objectMetadataPluralName: 'noteTargets', + gqlFields: NOTE_TARGET_GQL_FIELDS, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const data = response.body.data.noteTargets; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const noteTarget = edges[0].node; + + expect(noteTarget).toHaveProperty('id'); + expect(noteTarget).toHaveProperty('createdAt'); + expect(noteTarget).toHaveProperty('deletedAt'); + expect(noteTarget).toHaveProperty('noteId'); + expect(noteTarget).toHaveProperty('personId'); + expect(noteTarget).toHaveProperty('companyId'); + expect(noteTarget).toHaveProperty('opportunityId'); + expect(noteTarget).toHaveProperty('person'); + } + }); + + it('2b. should find one noteTarget', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'noteTarget', + gqlFields: NOTE_TARGET_GQL_FIELDS, + filter: { + id: { + eq: NOTE_TARGET_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const noteTarget = response.body.data.noteTarget; + + expect(noteTarget).toHaveProperty('id'); + expect(noteTarget).toHaveProperty('createdAt'); + expect(noteTarget).toHaveProperty('deletedAt'); + expect(noteTarget).toHaveProperty('noteId'); + expect(noteTarget).toHaveProperty('personId'); + expect(noteTarget).toHaveProperty('companyId'); + expect(noteTarget).toHaveProperty('opportunityId'); + expect(noteTarget).toHaveProperty('person'); + }); + + it('3. should update many noteTargets', async () => { + const graphqlOperation = updateManyOperationFactory({ + objectMetadataSingularName: 'noteTarget', + objectMetadataPluralName: 'noteTargets', + gqlFields: NOTE_TARGET_GQL_FIELDS, + data: { + personId: PERSON_1_ID, + }, + filter: { + id: { + in: [NOTE_TARGET_1_ID, NOTE_TARGET_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedNoteTargets = response.body.data.updateNoteTargets; + + expect(updatedNoteTargets).toHaveLength(2); + + updatedNoteTargets.forEach((noteTarget) => { + expect(noteTarget.person.id).toEqual(PERSON_1_ID); + }); + }); + + it('3b. should update one noteTarget', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'noteTarget', + gqlFields: NOTE_TARGET_GQL_FIELDS, + data: { + personId: PERSON_2_ID, + }, + recordId: NOTE_TARGET_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedNoteTarget = response.body.data.updateNoteTarget; + + expect(updatedNoteTarget.person.id).toEqual(PERSON_2_ID); + }); + + it('4. should find many noteTargets with updated personId', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'noteTarget', + objectMetadataPluralName: 'noteTargets', + gqlFields: NOTE_TARGET_GQL_FIELDS, + filter: { + personId: { + eq: PERSON_1_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.noteTargets.edges).toHaveLength(2); + }); + + it('4b. should find one noteTarget with updated personId', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'noteTarget', + gqlFields: NOTE_TARGET_GQL_FIELDS, + filter: { + personId: { + eq: PERSON_2_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.noteTarget.person.id).toEqual(PERSON_2_ID); + }); + + it('5. should delete many noteTargets', async () => { + const graphqlOperation = deleteManyOperationFactory({ + objectMetadataSingularName: 'noteTarget', + objectMetadataPluralName: 'noteTargets', + gqlFields: NOTE_TARGET_GQL_FIELDS, + filter: { + id: { + in: [NOTE_TARGET_1_ID, NOTE_TARGET_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const deleteNoteTargets = response.body.data.deleteNoteTargets; + + expect(deleteNoteTargets).toHaveLength(2); + + deleteNoteTargets.forEach((noteTarget) => { + expect(noteTarget.deletedAt).toBeTruthy(); + }); + }); + + it('5b. should delete one noteTarget', async () => { + const graphqlOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'noteTarget', + gqlFields: NOTE_TARGET_GQL_FIELDS, + recordId: NOTE_TARGET_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.deleteNoteTarget.deletedAt).toBeTruthy(); + }); + + it('6. should not find many noteTargets anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'noteTarget', + objectMetadataPluralName: 'noteTargets', + gqlFields: NOTE_TARGET_GQL_FIELDS, + filter: { + id: { + in: [NOTE_TARGET_1_ID, NOTE_TARGET_2_ID], + }, + }, + }); + + const findNoteTargetsResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect(findNoteTargetsResponse.body.data.noteTargets.edges).toHaveLength(0); + }); + + it('6b. should not find one noteTarget anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'noteTarget', + gqlFields: NOTE_TARGET_GQL_FIELDS, + filter: { + id: { + eq: NOTE_TARGET_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.noteTarget).toBeNull(); + }); + + it('7. should find many deleted noteTargets with deletedAt filter', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'noteTarget', + objectMetadataPluralName: 'noteTargets', + gqlFields: NOTE_TARGET_GQL_FIELDS, + filter: { + id: { + in: [NOTE_TARGET_1_ID, NOTE_TARGET_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.noteTargets.edges).toHaveLength(2); + }); + + it('7b. should find one deleted noteTarget with deletedAt filter', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'noteTarget', + gqlFields: NOTE_TARGET_GQL_FIELDS, + filter: { + id: { + eq: NOTE_TARGET_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.noteTarget.id).toEqual(NOTE_TARGET_3_ID); + }); + + it('8. should destroy many noteTargets', async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'noteTarget', + objectMetadataPluralName: 'noteTargets', + gqlFields: NOTE_TARGET_GQL_FIELDS, + filter: { + id: { + in: [NOTE_TARGET_1_ID, NOTE_TARGET_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.destroyNoteTargets).toHaveLength(2); + }); + + it('8b. should destroy one noteTarget', async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'noteTarget', + gqlFields: NOTE_TARGET_GQL_FIELDS, + recordId: NOTE_TARGET_3_ID, + }); + + const destroyNoteTargetsResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect(destroyNoteTargetsResponse.body.data.destroyNoteTarget).toBeTruthy(); + }); + + it('9. should not find many noteTargets anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'noteTarget', + objectMetadataPluralName: 'noteTargets', + gqlFields: NOTE_TARGET_GQL_FIELDS, + filter: { + id: { + in: [NOTE_TARGET_1_ID, NOTE_TARGET_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.noteTargets.edges).toHaveLength(0); + }); + + it('9b. should not find one noteTarget anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'noteTarget', + gqlFields: NOTE_TARGET_GQL_FIELDS, + filter: { + id: { + eq: NOTE_TARGET_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.noteTarget).toBeNull(); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/all-notes-resolvers.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/all-notes-resolvers.integration-spec.ts new file mode 100644 index 0000000000..29fbe7602d --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/all-notes-resolvers.integration-spec.ts @@ -0,0 +1,403 @@ +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 NOTE_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; +const NOTE_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; +const NOTE_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; + +const NOTE_GQL_FIELDS = ` + id + title + createdAt + updatedAt + deletedAt + body + position +`; + +describe('notes resolvers (integration)', () => { + it('1. should create and return notes', async () => { + const noteTitle1 = generateRecordName(NOTE_1_ID); + const noteTitle2 = generateRecordName(NOTE_2_ID); + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'note', + objectMetadataPluralName: 'notes', + gqlFields: NOTE_GQL_FIELDS, + data: [ + { + id: NOTE_1_ID, + title: noteTitle1, + }, + { + id: NOTE_2_ID, + title: noteTitle2, + }, + ], + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.createNotes).toHaveLength(2); + + response.body.data.createNotes.forEach((note) => { + expect(note).toHaveProperty('title'); + expect([noteTitle1, noteTitle2]).toContain(note.title); + + expect(note).toHaveProperty('id'); + expect(note).toHaveProperty('createdAt'); + expect(note).toHaveProperty('updatedAt'); + expect(note).toHaveProperty('deletedAt'); + expect(note).toHaveProperty('body'); + expect(note).toHaveProperty('position'); + }); + }); + + it('1b. should create and return one note', async () => { + const noteTitle3 = generateRecordName(NOTE_3_ID); + + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'note', + gqlFields: NOTE_GQL_FIELDS, + data: { + id: NOTE_3_ID, + title: noteTitle3, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdNote = response.body.data.createNote; + + expect(createdNote).toHaveProperty('title'); + expect(createdNote.title).toEqual(noteTitle3); + + expect(createdNote).toHaveProperty('id'); + expect(createdNote).toHaveProperty('createdAt'); + expect(createdNote).toHaveProperty('updatedAt'); + expect(createdNote).toHaveProperty('deletedAt'); + expect(createdNote).toHaveProperty('body'); + expect(createdNote).toHaveProperty('position'); + }); + + it('2. should find many notes', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'note', + objectMetadataPluralName: 'notes', + gqlFields: NOTE_GQL_FIELDS, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const data = response.body.data.notes; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const note = edges[0].node; + + expect(note).toHaveProperty('id'); + expect(note).toHaveProperty('createdAt'); + expect(note).toHaveProperty('updatedAt'); + expect(note).toHaveProperty('deletedAt'); + expect(note).toHaveProperty('body'); + expect(note).toHaveProperty('position'); + } + }); + + it('2b. should find one note', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'note', + gqlFields: NOTE_GQL_FIELDS, + filter: { + id: { + eq: NOTE_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const note = response.body.data.note; + + expect(note).toHaveProperty('title'); + + expect(note).toHaveProperty('id'); + expect(note).toHaveProperty('createdAt'); + expect(note).toHaveProperty('updatedAt'); + expect(note).toHaveProperty('deletedAt'); + expect(note).toHaveProperty('body'); + expect(note).toHaveProperty('position'); + }); + + it('3. should update many notes', async () => { + const graphqlOperation = updateManyOperationFactory({ + objectMetadataSingularName: 'note', + objectMetadataPluralName: 'notes', + gqlFields: NOTE_GQL_FIELDS, + data: { + title: 'Updated Title', + }, + filter: { + id: { + in: [NOTE_1_ID, NOTE_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedNotes = response.body.data.updateNotes; + + expect(updatedNotes).toHaveLength(2); + + updatedNotes.forEach((note) => { + expect(note.title).toEqual('Updated Title'); + }); + }); + + it('3b. should update one note', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'note', + gqlFields: NOTE_GQL_FIELDS, + data: { + title: 'New Title', + }, + recordId: NOTE_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedNote = response.body.data.updateNote; + + expect(updatedNote.title).toEqual('New Title'); + }); + + it('4. should find many notes with updated title', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'note', + objectMetadataPluralName: 'notes', + gqlFields: NOTE_GQL_FIELDS, + filter: { + title: { + eq: 'Updated Title', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.notes.edges).toHaveLength(2); + }); + + it('4b. should find one note with updated title', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'note', + gqlFields: NOTE_GQL_FIELDS, + filter: { + title: { + eq: 'New Title', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.note.title).toEqual('New Title'); + }); + + it('5. should delete many notes', async () => { + const graphqlOperation = deleteManyOperationFactory({ + objectMetadataSingularName: 'note', + objectMetadataPluralName: 'notes', + gqlFields: NOTE_GQL_FIELDS, + filter: { + id: { + in: [NOTE_1_ID, NOTE_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const deleteNotes = response.body.data.deleteNotes; + + expect(deleteNotes).toHaveLength(2); + + deleteNotes.forEach((note) => { + expect(note.deletedAt).toBeTruthy(); + }); + }); + + it('5b. should delete one note', async () => { + const graphqlOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'note', + gqlFields: NOTE_GQL_FIELDS, + recordId: NOTE_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.deleteNote.deletedAt).toBeTruthy(); + }); + + it('6. should not find many notes anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'note', + objectMetadataPluralName: 'notes', + gqlFields: NOTE_GQL_FIELDS, + filter: { + id: { + in: [NOTE_1_ID, NOTE_2_ID], + }, + }, + }); + + const findNotesResponse = await makeGraphqlAPIRequest(graphqlOperation); + + expect(findNotesResponse.body.data.notes.edges).toHaveLength(0); + }); + + it('6b. should not find one note anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'note', + gqlFields: NOTE_GQL_FIELDS, + filter: { + id: { + eq: NOTE_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.note).toBeNull(); + }); + + it('7. should find many deleted notes with deletedAt filter', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'note', + objectMetadataPluralName: 'notes', + gqlFields: NOTE_GQL_FIELDS, + filter: { + id: { + in: [NOTE_1_ID, NOTE_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.notes.edges).toHaveLength(2); + }); + + it('7b. should find one deleted note with deletedAt filter', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'note', + gqlFields: NOTE_GQL_FIELDS, + filter: { + id: { + eq: NOTE_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.note.id).toEqual(NOTE_3_ID); + }); + + it('8. should destroy many notes', async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'note', + objectMetadataPluralName: 'notes', + gqlFields: NOTE_GQL_FIELDS, + filter: { + id: { + in: [NOTE_1_ID, NOTE_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.destroyNotes).toHaveLength(2); + }); + + it('8b. should destroy one note', async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'note', + gqlFields: NOTE_GQL_FIELDS, + recordId: NOTE_3_ID, + }); + + const destroyNotesResponse = await makeGraphqlAPIRequest(graphqlOperation); + + expect(destroyNotesResponse.body.data.destroyNote).toBeTruthy(); + }); + + it('9. should not find many notes anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'note', + objectMetadataPluralName: 'notes', + gqlFields: NOTE_GQL_FIELDS, + filter: { + id: { + in: [NOTE_1_ID, NOTE_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.notes.edges).toHaveLength(0); + }); + + it('9b. should not find one note anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'note', + gqlFields: NOTE_GQL_FIELDS, + filter: { + id: { + eq: NOTE_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.note).toBeNull(); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/all-opportunities-resolvers.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/all-opportunities-resolvers.integration-spec.ts new file mode 100644 index 0000000000..6c1a279be4 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/all-opportunities-resolvers.integration-spec.ts @@ -0,0 +1,414 @@ +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 OPPORTUNITY_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; +const OPPORTUNITY_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; +const OPPORTUNITY_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; + +const OPPORTUNITY_GQL_FIELDS = ` + id + name + createdAt + updatedAt + deletedAt + searchVector + stage + position +`; + +describe('opportunities resolvers (integration)', () => { + it('1. should create and return opportunities', async () => { + const opportunityName1 = generateRecordName(OPPORTUNITY_1_ID); + const opportunityName2 = generateRecordName(OPPORTUNITY_2_ID); + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'opportunity', + objectMetadataPluralName: 'opportunities', + gqlFields: OPPORTUNITY_GQL_FIELDS, + data: [ + { + id: OPPORTUNITY_1_ID, + name: opportunityName1, + }, + { + id: OPPORTUNITY_2_ID, + name: opportunityName2, + }, + ], + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.createOpportunities).toHaveLength(2); + + response.body.data.createOpportunities.forEach((opportunity) => { + expect(opportunity).toHaveProperty('name'); + expect([opportunityName1, opportunityName2]).toContain(opportunity.name); + + expect(opportunity).toHaveProperty('id'); + expect(opportunity).toHaveProperty('createdAt'); + expect(opportunity).toHaveProperty('updatedAt'); + expect(opportunity).toHaveProperty('deletedAt'); + expect(opportunity).toHaveProperty('searchVector'); + expect(opportunity).toHaveProperty('stage'); + expect(opportunity).toHaveProperty('position'); + }); + }); + + it('1b. should create and return one opportunity', async () => { + const opportunityName3 = generateRecordName(OPPORTUNITY_3_ID); + + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'opportunity', + gqlFields: OPPORTUNITY_GQL_FIELDS, + data: { + id: OPPORTUNITY_3_ID, + name: opportunityName3, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdOpportunity = response.body.data.createOpportunity; + + expect(createdOpportunity).toHaveProperty('name'); + expect(createdOpportunity.name).toEqual(opportunityName3); + + expect(createdOpportunity).toHaveProperty('id'); + expect(createdOpportunity).toHaveProperty('createdAt'); + expect(createdOpportunity).toHaveProperty('updatedAt'); + expect(createdOpportunity).toHaveProperty('deletedAt'); + expect(createdOpportunity).toHaveProperty('searchVector'); + expect(createdOpportunity).toHaveProperty('stage'); + expect(createdOpportunity).toHaveProperty('position'); + }); + + it('2. should find many opportunities', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'opportunity', + objectMetadataPluralName: 'opportunities', + gqlFields: OPPORTUNITY_GQL_FIELDS, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const data = response.body.data.opportunities; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const opportunity = edges[0].node; + + expect(opportunity).toHaveProperty('id'); + expect(opportunity).toHaveProperty('createdAt'); + expect(opportunity).toHaveProperty('updatedAt'); + expect(opportunity).toHaveProperty('deletedAt'); + expect(opportunity).toHaveProperty('searchVector'); + expect(opportunity).toHaveProperty('stage'); + expect(opportunity).toHaveProperty('position'); + } + }); + + it('2b. should find one opportunity', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'opportunity', + gqlFields: OPPORTUNITY_GQL_FIELDS, + filter: { + id: { + eq: OPPORTUNITY_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const opportunity = response.body.data.opportunity; + + expect(opportunity).toHaveProperty('name'); + + expect(opportunity).toHaveProperty('id'); + expect(opportunity).toHaveProperty('createdAt'); + expect(opportunity).toHaveProperty('updatedAt'); + expect(opportunity).toHaveProperty('deletedAt'); + expect(opportunity).toHaveProperty('searchVector'); + expect(opportunity).toHaveProperty('stage'); + expect(opportunity).toHaveProperty('position'); + }); + + it('3. should update many opportunities', async () => { + const graphqlOperation = updateManyOperationFactory({ + objectMetadataSingularName: 'opportunity', + objectMetadataPluralName: 'opportunities', + gqlFields: OPPORTUNITY_GQL_FIELDS, + data: { + name: 'Updated Name', + }, + filter: { + id: { + in: [OPPORTUNITY_1_ID, OPPORTUNITY_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedOpportunities = response.body.data.updateOpportunities; + + expect(updatedOpportunities).toHaveLength(2); + + updatedOpportunities.forEach((opportunity) => { + expect(opportunity.name).toEqual('Updated Name'); + }); + }); + + it('3b. should update one opportunity', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'opportunity', + gqlFields: OPPORTUNITY_GQL_FIELDS, + data: { + name: 'New Name', + }, + recordId: OPPORTUNITY_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedOpportunity = response.body.data.updateOpportunity; + + expect(updatedOpportunity.name).toEqual('New Name'); + }); + + it('4. should find many opportunities with updated name', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'opportunity', + objectMetadataPluralName: 'opportunities', + gqlFields: OPPORTUNITY_GQL_FIELDS, + filter: { + name: { + eq: 'Updated Name', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.opportunities.edges).toHaveLength(2); + }); + + it('4b. should find one opportunity with updated name', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'opportunity', + gqlFields: OPPORTUNITY_GQL_FIELDS, + filter: { + name: { + eq: 'New Name', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.opportunity.name).toEqual('New Name'); + }); + + it('5. should delete many opportunities', async () => { + const graphqlOperation = deleteManyOperationFactory({ + objectMetadataSingularName: 'opportunity', + objectMetadataPluralName: 'opportunities', + gqlFields: OPPORTUNITY_GQL_FIELDS, + filter: { + id: { + in: [OPPORTUNITY_1_ID, OPPORTUNITY_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const deleteOpportunities = response.body.data.deleteOpportunities; + + expect(deleteOpportunities).toHaveLength(2); + + deleteOpportunities.forEach((opportunity) => { + expect(opportunity.deletedAt).toBeTruthy(); + }); + }); + + it('5b. should delete one opportunity', async () => { + const graphqlOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'opportunity', + gqlFields: OPPORTUNITY_GQL_FIELDS, + recordId: OPPORTUNITY_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.deleteOpportunity.deletedAt).toBeTruthy(); + }); + + it('6. should not find many opportunities anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'opportunity', + objectMetadataPluralName: 'opportunities', + gqlFields: OPPORTUNITY_GQL_FIELDS, + filter: { + id: { + in: [OPPORTUNITY_1_ID, OPPORTUNITY_2_ID], + }, + }, + }); + + const findOpportunitiesResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + findOpportunitiesResponse.body.data.opportunities.edges, + ).toHaveLength(0); + }); + + it('6b. should not find one opportunity anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'opportunity', + gqlFields: OPPORTUNITY_GQL_FIELDS, + filter: { + id: { + eq: OPPORTUNITY_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.opportunity).toBeNull(); + }); + + it('7. should find many deleted opportunities with deletedAt filter', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'opportunity', + objectMetadataPluralName: 'opportunities', + gqlFields: OPPORTUNITY_GQL_FIELDS, + filter: { + id: { + in: [OPPORTUNITY_1_ID, OPPORTUNITY_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.opportunities.edges).toHaveLength(2); + }); + + it('7b. should find one deleted opportunity with deletedAt filter', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'opportunity', + gqlFields: OPPORTUNITY_GQL_FIELDS, + filter: { + id: { + eq: OPPORTUNITY_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.opportunity.id).toEqual(OPPORTUNITY_3_ID); + }); + + it('8. should destroy many opportunities', async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'opportunity', + objectMetadataPluralName: 'opportunities', + gqlFields: OPPORTUNITY_GQL_FIELDS, + filter: { + id: { + in: [OPPORTUNITY_1_ID, OPPORTUNITY_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.destroyOpportunities).toHaveLength(2); + }); + + it('8b. should destroy one opportunity', async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'opportunity', + gqlFields: OPPORTUNITY_GQL_FIELDS, + recordId: OPPORTUNITY_3_ID, + }); + + const destroyOpportunitiesResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + destroyOpportunitiesResponse.body.data.destroyOpportunity, + ).toBeTruthy(); + }); + + it('9. should not find many opportunities anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'opportunity', + objectMetadataPluralName: 'opportunities', + gqlFields: OPPORTUNITY_GQL_FIELDS, + filter: { + id: { + in: [OPPORTUNITY_1_ID, OPPORTUNITY_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.opportunities.edges).toHaveLength(0); + }); + + it('9b. should not find one opportunity anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'opportunity', + gqlFields: OPPORTUNITY_GQL_FIELDS, + filter: { + id: { + eq: OPPORTUNITY_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.opportunity).toBeNull(); + }); +}); From dfcf3ef8796f31cef0afaa4fd23b965582e6b3a6 Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2024 10:28:09 +0200 Subject: [PATCH 075/123] Migrate to twenty-ui - layout/animated-placeholder (#7794) This PR was created by [GitStart](https://gitstart.com/) to address the requirements from this ticket: [TWNTY-7531](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-7531). --- ### Description - Migrate the `animated-placeholder` to `twenty-ui` and update all imports.\ \ Fixes twentyhq/private-issues#87 Co-authored-by: gitstart-twenty Co-authored-by: Charles Bochet --- .../calendar/components/Calendar.tsx | 18 ++++++++--------- .../emails/components/EmailLoader.tsx | 4 ++-- .../emails/components/EmailThreads.tsx | 19 +++++++++--------- .../files/components/Attachments.tsx | 18 ++++++++--------- .../activities/notes/components/Notes.tsx | 18 ++++++++--------- .../tasks/components/TaskGroups.tsx | 18 ++++++++--------- .../components/TimelineActivities.tsx | 6 +++--- .../components/GenericErrorFallback.tsx | 15 +++++++------- .../RecordTableEmptyStateDisplay.tsx | 20 +++++++++---------- .../SettingsServerlessFunctionsTableEmpty.tsx | 14 ++++++------- .../src/pages/not-found/NotFound.tsx | 10 +++++----- .../components/AnimatedPlaceholder.tsx | 13 +++++------- .../components/EmptyPlaceholderStyled.tsx | 0 .../components/ErrorPlaceholderStyled.tsx | 0 .../constants/Background.ts | 0 .../constants/DarkBackground.ts | 0 .../constants/DarkMovingImage.ts | 0 .../constants/MovingImage.ts | 0 .../src/layout/animated-placeholder/index.ts | 7 +++++++ packages/twenty-ui/src/layout/index.ts | 7 +++++++ 20 files changed, 99 insertions(+), 88 deletions(-) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/layout/animated-placeholder/components/AnimatedPlaceholder.tsx (86%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/layout/animated-placeholder/components/EmptyPlaceholderStyled.tsx (100%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/layout/animated-placeholder/components/ErrorPlaceholderStyled.tsx (100%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/layout/animated-placeholder/constants/Background.ts (100%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/layout/animated-placeholder/constants/DarkBackground.ts (100%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/layout/animated-placeholder/constants/DarkMovingImage.ts (100%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/layout/animated-placeholder/constants/MovingImage.ts (100%) create mode 100644 packages/twenty-ui/src/layout/animated-placeholder/index.ts 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 83f06303b7..b8da221c6a 100644 --- a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx +++ b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx @@ -1,6 +1,14 @@ import styled from '@emotion/styled'; import { format, getYear } from 'date-fns'; -import { H3Title } from 'twenty-ui'; +import { + AnimatedPlaceholder, + AnimatedPlaceholderEmptyContainer, + AnimatedPlaceholderEmptySubTitle, + AnimatedPlaceholderEmptyTextContainer, + AnimatedPlaceholderEmptyTitle, + EMPTY_PLACEHOLDER_TRANSITION_PROPS, + H3Title, +} from 'twenty-ui'; import { CalendarMonthCard } from '@/activities/calendar/components/CalendarMonthCard'; import { TIMELINE_CALENDAR_EVENTS_DEFAULT_PAGE_SIZE } from '@/activities/calendar/constants/Calendar'; @@ -13,14 +21,6 @@ import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { useCustomResolver } from '@/activities/hooks/useCustomResolver'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; -import { - AnimatedPlaceholderEmptyContainer, - AnimatedPlaceholderEmptySubTitle, - AnimatedPlaceholderEmptyTextContainer, - AnimatedPlaceholderEmptyTitle, - EMPTY_PLACEHOLDER_TRANSITION_PROPS, -} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; import { Section } from '@/ui/layout/section/components/Section'; import { TimelineCalendarEventsWithTotal } from '~/generated/graphql'; diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailLoader.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailLoader.tsx index f497344606..f301828ff4 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailLoader.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailLoader.tsx @@ -1,10 +1,10 @@ import { Loader } from '@/ui/feedback/loader/components/Loader'; -import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; import { + AnimatedPlaceholder, AnimatedPlaceholderEmptyContainer, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, -} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; +} from 'twenty-ui'; export const EmailLoader = ({ loadingText }: { loadingText?: string }) => ( 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 a46df19b7f..f8c9b317f6 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx @@ -1,5 +1,14 @@ import styled from '@emotion/styled'; -import { H1Title, H1TitleFontColor } from 'twenty-ui'; +import { + AnimatedPlaceholder, + AnimatedPlaceholderEmptyContainer, + AnimatedPlaceholderEmptySubTitle, + AnimatedPlaceholderEmptyTextContainer, + AnimatedPlaceholderEmptyTitle, + EMPTY_PLACEHOLDER_TRANSITION_PROPS, + H1Title, + H1TitleFontColor, +} from 'twenty-ui'; import { ActivityList } from '@/activities/components/ActivityList'; import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader'; @@ -11,14 +20,6 @@ import { getTimelineThreadsFromPersonId } from '@/activities/emails/graphql/quer import { useCustomResolver } from '@/activities/hooks/useCustomResolver'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; -import { - AnimatedPlaceholderEmptyContainer, - AnimatedPlaceholderEmptySubTitle, - AnimatedPlaceholderEmptyTextContainer, - AnimatedPlaceholderEmptyTitle, - EMPTY_PLACEHOLDER_TRANSITION_PROPS, -} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; import { Section } from '@/ui/layout/section/components/Section'; import { TimelineThread, TimelineThreadsWithTotal } from '~/generated/graphql'; diff --git a/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx b/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx index c0bf6908e2..0ab90a7e0a 100644 --- a/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx @@ -1,6 +1,14 @@ import styled from '@emotion/styled'; import { ChangeEvent, useRef, useState } from 'react'; -import { IconPlus } from 'twenty-ui'; +import { + AnimatedPlaceholder, + AnimatedPlaceholderEmptyContainer, + AnimatedPlaceholderEmptySubTitle, + AnimatedPlaceholderEmptyTextContainer, + AnimatedPlaceholderEmptyTitle, + EMPTY_PLACEHOLDER_TRANSITION_PROPS, + IconPlus, +} from 'twenty-ui'; import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { AttachmentList } from '@/activities/files/components/AttachmentList'; @@ -9,14 +17,6 @@ import { useAttachments } from '@/activities/files/hooks/useAttachments'; import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { Button } from '@/ui/input/button/components/Button'; -import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; -import { - AnimatedPlaceholderEmptyContainer, - AnimatedPlaceholderEmptySubTitle, - AnimatedPlaceholderEmptyTextContainer, - AnimatedPlaceholderEmptyTitle, - EMPTY_PLACEHOLDER_TRANSITION_PROPS, -} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; import { isDefined } from '~/utils/isDefined'; const StyledAttachmentsContainer = styled.div` diff --git a/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx b/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx index b26624e1a9..de4e6cccca 100644 --- a/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx +++ b/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx @@ -1,5 +1,13 @@ import styled from '@emotion/styled'; -import { IconPlus } from 'twenty-ui'; +import { + AnimatedPlaceholder, + AnimatedPlaceholderEmptyContainer, + AnimatedPlaceholderEmptySubTitle, + AnimatedPlaceholderEmptyTextContainer, + AnimatedPlaceholderEmptyTitle, + EMPTY_PLACEHOLDER_TRANSITION_PROPS, + IconPlus, +} from 'twenty-ui'; import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; @@ -8,14 +16,6 @@ import { useNotes } from '@/activities/notes/hooks/useNotes'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { Button } from '@/ui/input/button/components/Button'; -import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; -import { - AnimatedPlaceholderEmptyContainer, - AnimatedPlaceholderEmptySubTitle, - AnimatedPlaceholderEmptyTextContainer, - AnimatedPlaceholderEmptyTitle, - EMPTY_PLACEHOLDER_TRANSITION_PROPS, -} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; const StyledNotesContainer = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx index 16ebbec0f3..03393d5762 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx @@ -1,6 +1,14 @@ import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; -import { IconPlus } from 'twenty-ui'; +import { + AnimatedPlaceholder, + AnimatedPlaceholderEmptyContainer, + AnimatedPlaceholderEmptySubTitle, + AnimatedPlaceholderEmptyTextContainer, + AnimatedPlaceholderEmptyTitle, + EMPTY_PLACEHOLDER_TRANSITION_PROPS, + IconPlus, +} from 'twenty-ui'; import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; @@ -8,14 +16,6 @@ import { TASKS_TAB_LIST_COMPONENT_ID } from '@/activities/tasks/constants/TasksT import { useTasks } from '@/activities/tasks/hooks/useTasks'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { Button } from '@/ui/input/button/components/Button'; -import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; -import { - AnimatedPlaceholderEmptyContainer, - AnimatedPlaceholderEmptySubTitle, - AnimatedPlaceholderEmptyTextContainer, - AnimatedPlaceholderEmptyTitle, - EMPTY_PLACEHOLDER_TRANSITION_PROPS, -} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; import { Task } from '@/activities/types/Task'; diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineActivities.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineActivities.tsx index a938ae8aa1..a0f8bf327f 100644 --- a/packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineActivities.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineActivities.tsx @@ -6,15 +6,15 @@ 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 { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { + AnimatedPlaceholder, AnimatedPlaceholderEmptyContainer, AnimatedPlaceholderEmptySubTitle, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, EMPTY_PLACEHOLDER_TRANSITION_PROPS, -} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; -import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; +} from 'twenty-ui'; const StyledMainContainer = styled.div` align-items: flex-start; diff --git a/packages/twenty-front/src/modules/error-handler/components/GenericErrorFallback.tsx b/packages/twenty-front/src/modules/error-handler/components/GenericErrorFallback.tsx index 2109891aca..9481fbcc4c 100644 --- a/packages/twenty-front/src/modules/error-handler/components/GenericErrorFallback.tsx +++ b/packages/twenty-front/src/modules/error-handler/components/GenericErrorFallback.tsx @@ -1,18 +1,19 @@ +import { ThemeProvider, useTheme } from '@emotion/react'; +import isEmpty from 'lodash.isempty'; import { useEffect, useState } from 'react'; import { FallbackProps } from 'react-error-boundary'; import { useLocation } from 'react-router-dom'; -import { ThemeProvider, useTheme } from '@emotion/react'; -import isEmpty from 'lodash.isempty'; -import { IconRefresh, THEME_LIGHT } from 'twenty-ui'; - -import { Button } from '@/ui/input/button/components/Button'; -import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; import { + AnimatedPlaceholder, AnimatedPlaceholderEmptyContainer, AnimatedPlaceholderEmptySubTitle, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, -} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; + IconRefresh, + THEME_LIGHT, +} from 'twenty-ui'; + +import { Button } from '@/ui/input/button/components/Button'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; type GenericErrorFallbackProps = FallbackProps; diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx index c5f120a6de..66affe2a28 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx @@ -1,18 +1,16 @@ -import AnimatedPlaceholder, { - AnimatedPlaceholderType, -} from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; -import { - AnimatedPlaceholderEmptyContainer, - AnimatedPlaceholderEmptySubTitle, - AnimatedPlaceholderEmptyTextContainer, - AnimatedPlaceholderEmptyTitle, -} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; - import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly'; import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; import { Button } from '@/ui/input/button/components/Button'; import { useContext } from 'react'; -import { IconComponent } from 'twenty-ui'; +import { + AnimatedPlaceholder, + AnimatedPlaceholderEmptyContainer, + AnimatedPlaceholderEmptySubTitle, + AnimatedPlaceholderEmptyTextContainer, + AnimatedPlaceholderEmptyTitle, + AnimatedPlaceholderType, + IconComponent, +} from 'twenty-ui'; type RecordTableEmptyStateDisplayProps = { animatedPlaceholderType: AnimatedPlaceholderType; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTableEmpty.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTableEmpty.tsx index 4f5744019f..77d854984b 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTableEmpty.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTableEmpty.tsx @@ -1,16 +1,16 @@ +import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; +import { SettingsPath } from '@/types/SettingsPath'; +import { Button } from '@/ui/input/button/components/Button'; +import styled from '@emotion/styled'; import { + AnimatedPlaceholder, AnimatedPlaceholderEmptyContainer, AnimatedPlaceholderEmptySubTitle, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, EMPTY_PLACEHOLDER_TRANSITION_PROPS, -} from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; -import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; -import { IconPlus } from 'twenty-ui'; -import { Button } from '@/ui/input/button/components/Button'; -import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; -import { SettingsPath } from '@/types/SettingsPath'; -import styled from '@emotion/styled'; + IconPlus, +} from 'twenty-ui'; const StyledEmptyFunctionsContainer = styled.div` height: 60vh; diff --git a/packages/twenty-front/src/pages/not-found/NotFound.tsx b/packages/twenty-front/src/pages/not-found/NotFound.tsx index 4270c5718b..3283d402b2 100644 --- a/packages/twenty-front/src/pages/not-found/NotFound.tsx +++ b/packages/twenty-front/src/pages/not-found/NotFound.tsx @@ -3,15 +3,15 @@ import styled from '@emotion/styled'; import { SignInBackgroundMockPage } from '@/sign-in-background-mock/components/SignInBackgroundMockPage'; import { AppPath } from '@/types/AppPath'; import { MainButton } from '@/ui/input/button/components/MainButton'; -import AnimatedPlaceholder from '@/ui/layout/animated-placeholder/components/AnimatedPlaceholder'; -import { AnimatedPlaceholderEmptyTextContainer } from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; +import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; +import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle'; import { + AnimatedPlaceholder, + AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderErrorContainer, AnimatedPlaceholderErrorSubTitle, AnimatedPlaceholderErrorTitle, -} from '@/ui/layout/animated-placeholder/components/ErrorPlaceholderStyled'; -import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; -import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle'; +} from 'twenty-ui'; const StyledBackDrop = styled.div` align-items: center; diff --git a/packages/twenty-front/src/modules/ui/layout/animated-placeholder/components/AnimatedPlaceholder.tsx b/packages/twenty-ui/src/layout/animated-placeholder/components/AnimatedPlaceholder.tsx similarity index 86% rename from packages/twenty-front/src/modules/ui/layout/animated-placeholder/components/AnimatedPlaceholder.tsx rename to packages/twenty-ui/src/layout/animated-placeholder/components/AnimatedPlaceholder.tsx index 49c6977885..10b7e366dc 100644 --- a/packages/twenty-front/src/modules/ui/layout/animated-placeholder/components/AnimatedPlaceholder.tsx +++ b/packages/twenty-ui/src/layout/animated-placeholder/components/AnimatedPlaceholder.tsx @@ -1,13 +1,12 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { BACKGROUND } from '@ui/layout/animated-placeholder/constants/Background'; +import { DARK_BACKGROUND } from '@ui/layout/animated-placeholder/constants/DarkBackground'; +import { DARK_MOVING_IMAGE } from '@ui/layout/animated-placeholder/constants/DarkMovingImage'; +import { MOVING_IMAGE } from '@ui/layout/animated-placeholder/constants/MovingImage'; import { animate, motion, useMotionValue, useTransform } from 'framer-motion'; import { useEffect } from 'react'; -import { BACKGROUND } from '@/ui/layout/animated-placeholder/constants/Background'; -import { DARK_BACKGROUND } from '@/ui/layout/animated-placeholder/constants/DarkBackground'; -import { DARK_MOVING_IMAGE } from '@/ui/layout/animated-placeholder/constants/DarkMovingImage'; -import { MOVING_IMAGE } from '@/ui/layout/animated-placeholder/constants/MovingImage'; - const StyledContainer = styled.div` display: flex; align-items: center; @@ -43,7 +42,7 @@ interface AnimatedPlaceholderProps { type: AnimatedPlaceholderType; } -const AnimatedPlaceholder = ({ type }: AnimatedPlaceholderProps) => { +export const AnimatedPlaceholder = ({ type }: AnimatedPlaceholderProps) => { const theme = useTheme(); const x = useMotionValue(window.innerWidth / 2); @@ -106,5 +105,3 @@ const AnimatedPlaceholder = ({ type }: AnimatedPlaceholderProps) => { ); }; - -export default AnimatedPlaceholder; diff --git a/packages/twenty-front/src/modules/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled.tsx b/packages/twenty-ui/src/layout/animated-placeholder/components/EmptyPlaceholderStyled.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled.tsx rename to packages/twenty-ui/src/layout/animated-placeholder/components/EmptyPlaceholderStyled.tsx diff --git a/packages/twenty-front/src/modules/ui/layout/animated-placeholder/components/ErrorPlaceholderStyled.tsx b/packages/twenty-ui/src/layout/animated-placeholder/components/ErrorPlaceholderStyled.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/layout/animated-placeholder/components/ErrorPlaceholderStyled.tsx rename to packages/twenty-ui/src/layout/animated-placeholder/components/ErrorPlaceholderStyled.tsx diff --git a/packages/twenty-front/src/modules/ui/layout/animated-placeholder/constants/Background.ts b/packages/twenty-ui/src/layout/animated-placeholder/constants/Background.ts similarity index 100% rename from packages/twenty-front/src/modules/ui/layout/animated-placeholder/constants/Background.ts rename to packages/twenty-ui/src/layout/animated-placeholder/constants/Background.ts diff --git a/packages/twenty-front/src/modules/ui/layout/animated-placeholder/constants/DarkBackground.ts b/packages/twenty-ui/src/layout/animated-placeholder/constants/DarkBackground.ts similarity index 100% rename from packages/twenty-front/src/modules/ui/layout/animated-placeholder/constants/DarkBackground.ts rename to packages/twenty-ui/src/layout/animated-placeholder/constants/DarkBackground.ts diff --git a/packages/twenty-front/src/modules/ui/layout/animated-placeholder/constants/DarkMovingImage.ts b/packages/twenty-ui/src/layout/animated-placeholder/constants/DarkMovingImage.ts similarity index 100% rename from packages/twenty-front/src/modules/ui/layout/animated-placeholder/constants/DarkMovingImage.ts rename to packages/twenty-ui/src/layout/animated-placeholder/constants/DarkMovingImage.ts diff --git a/packages/twenty-front/src/modules/ui/layout/animated-placeholder/constants/MovingImage.ts b/packages/twenty-ui/src/layout/animated-placeholder/constants/MovingImage.ts similarity index 100% rename from packages/twenty-front/src/modules/ui/layout/animated-placeholder/constants/MovingImage.ts rename to packages/twenty-ui/src/layout/animated-placeholder/constants/MovingImage.ts diff --git a/packages/twenty-ui/src/layout/animated-placeholder/index.ts b/packages/twenty-ui/src/layout/animated-placeholder/index.ts new file mode 100644 index 0000000000..74da7bd40c --- /dev/null +++ b/packages/twenty-ui/src/layout/animated-placeholder/index.ts @@ -0,0 +1,7 @@ +export * from './components/AnimatedPlaceholder'; +export * from './components/EmptyPlaceholderStyled'; +export * from './components/ErrorPlaceholderStyled'; +export * from './constants/Background'; +export * from './constants/DarkBackground'; +export * from './constants/DarkMovingImage'; +export * from './constants/MovingImage'; diff --git a/packages/twenty-ui/src/layout/index.ts b/packages/twenty-ui/src/layout/index.ts index 8b8c345268..8bd6c13dc5 100644 --- a/packages/twenty-ui/src/layout/index.ts +++ b/packages/twenty-ui/src/layout/index.ts @@ -1 +1,8 @@ +export * from './animated-placeholder/components/AnimatedPlaceholder'; +export * from './animated-placeholder/components/EmptyPlaceholderStyled'; +export * from './animated-placeholder/components/ErrorPlaceholderStyled'; +export * from './animated-placeholder/constants/Background'; +export * from './animated-placeholder/constants/DarkBackground'; +export * from './animated-placeholder/constants/DarkMovingImage'; +export * from './animated-placeholder/constants/MovingImage'; export * from './expandableContainer/components/ExpandableContainer'; From 6133a72cf673acc8af2a8a4f2befec7755ddda07 Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2024 10:34:42 +0200 Subject: [PATCH 076/123] Migrate to twenty-ui utilities/screen-size (#7836) This PR was created by [GitStart](https://gitstart.com/) to address the requirements from this ticket: [TWNTY-7540](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-7540). --- ### Description Move `utilities/screen-size` to the `twenty-ui` package ### Demo The `useScreenSize` was used to render the mobile nav for example on the landing page. It still renders properly ![](https://assets-service.gitstart.com/4814/018fa684-c192-455d-a38b-3b212fdb3c1a.png) ###### Fixes [#7540](https://github.com/twentyhq/twenty/issues/7540) ###### Dev QA - [x] `utilities/screen-size` should be moved to the `twenty-ui` folder - [x] The mobile nav should still show on the landing page Co-authored-by: gitstart-twenty Co-authored-by: Charles Bochet --- .../src/modules/ui/layout/page/components/DefaultLayout.tsx | 2 +- packages/twenty-ui/src/utilities/index.ts | 1 + .../src}/utilities/screen-size/hooks/useScreenSize.ts | 0 3 files changed, 2 insertions(+), 1 deletion(-) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/utilities/screen-size/hooks/useScreenSize.ts (100%) diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx index 0f11f9d50e..2a7fcf97a7 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/components/DefaultLayout.tsx @@ -10,7 +10,7 @@ import { SignInBackgroundMockPage } from '@/sign-in-background-mock/components/S import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal'; import { NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/NavDrawerWidths'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; -import { useScreenSize } from '@/ui/utilities/screen-size/hooks/useScreenSize'; +import { useScreenSize } from 'twenty-ui'; import { css, Global, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { AnimatePresence, LayoutGroup, motion } from 'framer-motion'; diff --git a/packages/twenty-ui/src/utilities/index.ts b/packages/twenty-ui/src/utilities/index.ts index 38cd4a1b60..f6f4d3659c 100644 --- a/packages/twenty-ui/src/utilities/index.ts +++ b/packages/twenty-ui/src/utilities/index.ts @@ -3,3 +3,4 @@ export * from './image/getImageAbsoluteURI'; export * from './isDefined'; export * from './state/utils/createState'; export * from './types/Nullable'; +export * from './screen-size/hooks/useScreenSize'; diff --git a/packages/twenty-front/src/modules/ui/utilities/screen-size/hooks/useScreenSize.ts b/packages/twenty-ui/src/utilities/screen-size/hooks/useScreenSize.ts similarity index 100% rename from packages/twenty-front/src/modules/ui/utilities/screen-size/hooks/useScreenSize.ts rename to packages/twenty-ui/src/utilities/screen-size/hooks/useScreenSize.ts From afb246558e91c421ea9912d0ec99d733f4693d6d Mon Sep 17 00:00:00 2001 From: Rajeev Dewangan <63413883+rajeevDewangan@users.noreply.github.com> Date: Tue, 22 Oct 2024 14:25:07 +0530 Subject: [PATCH 077/123] Update 2-write-blog-post-about-20.md (#7941) Update oss-gg block post list for https://github.com/twentyhq/twenty/pull/7938 --- oss-gg/twenty-content-challenges/2-write-blog-post-about-20.md | 2 ++ 1 file changed, 2 insertions(+) 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 c53fe4babe..0ff978abec 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 @@ -22,4 +22,6 @@ Your turn 👇 » 21-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) blog Link: [blog](https://dev.to/sateshcharan/twenty-crm-a-fresh-start-for-modern-businesses-46kf) +» 22-October-2024 by [rajeevDewangan](https://oss.gg/rajeevDewangan) blog Link: [blog](https://open.substack.com/pub/rajeevdewangan/p/comprehensive-guide-to-self-hosting?r=4lly3x&utm_campaign=post&utm_medium=web&showWelcomeOnShare=true) + --- From 07cdeeb59501bf8607a700d7353b8d922fd3c2a6 Mon Sep 17 00:00:00 2001 From: Ritansh Singh <90311141+RitanshRajput@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:50:11 +0530 Subject: [PATCH 078/123] Like & Re-Tweet oss.gg Launch Tweet #7947 (#7950) Point: 50 Points Task: Like & Re-Tweet oss.gg Launch Tweet Attachement: ![retweet oss gg](https://github.com/user-attachments/assets/6c7619be-707b-49ff-991d-15225e503b4b) --------- Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- 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 7121fb7da6..42ccacee9e 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 @@ -49,3 +49,6 @@ Your turn 👇 » 20-October-2024 by Naprila » Link to Tweet: https://x.com/mkprasad_821/status/1847886807314120762 + +» 22-October-2024 by Ritansh Rajput +» Link to Tweet: https://x.com/Ritansh_Dev/status/1848641904511975838 From 006104c5484b5ff7b678e22edef890f6a16aed5a Mon Sep 17 00:00:00 2001 From: ZiaCodes <72739794+Khaan25@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:22:06 +0500 Subject: [PATCH 079/123] feat: Side quest Tweet compeleted (#7943) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ![image](https://github.com/user-attachments/assets/9903f9b2-4a45-4a88-a50b-ca398aa556dd) --------- Co-authored-by: Félix Malfait --- 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 42ccacee9e..1f8edef409 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 @@ -50,5 +50,8 @@ Your turn 👇 » 20-October-2024 by Naprila » Link to Tweet: https://x.com/mkprasad_821/status/1847886807314120762 +» 22-October-2024 by Zia Ur Rehman Khan +» Link to Tweet: https://x.com/zia_webdev/status/1848659210243871165x + » 22-October-2024 by Ritansh Rajput » Link to Tweet: https://x.com/Ritansh_Dev/status/1848641904511975838 From 6e9774dac207f3750860d4da0fae7955526684c4 Mon Sep 17 00:00:00 2001 From: ZiaCodes <72739794+Khaan25@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:23:31 +0500 Subject: [PATCH 080/123] feat: Side quest meme magic completed (#7945) ![image](https://github.com/user-attachments/assets/615094b3-3d6b-40dc-a986-5e725201c47c) --- oss-gg/twenty-side-quest/4-meme-magic.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/oss-gg/twenty-side-quest/4-meme-magic.md b/oss-gg/twenty-side-quest/4-meme-magic.md index eb631acce0..f9133210af 100644 --- a/oss-gg/twenty-side-quest/4-meme-magic.md +++ b/oss-gg/twenty-side-quest/4-meme-magic.md @@ -44,4 +44,5 @@ Your turn 👇 » 20-October-2024 by Naprila » Link to Tweet: https://x.com/mkprasad_821/status/1847900277510123706 ---- +» 22-October-2024 by Zia Ur Rehman Khan +» Link to Tweet: https://x.com/zia_webdev/status/1846954638953926675 \ No newline at end of file From 2aa870d6ac045dd4dd494f6a1608c5cffe7131ea Mon Sep 17 00:00:00 2001 From: ZiaCodes <72739794+Khaan25@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:23:52 +0500 Subject: [PATCH 081/123] feat: Side quest Gif Magic completed (#7946) ![supermeme_14h47_52](https://github.com/user-attachments/assets/e7788a89-722c-4046-ae5d-c2a738daa433) --- oss-gg/twenty-side-quest/5-gif-magic.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/oss-gg/twenty-side-quest/5-gif-magic.md b/oss-gg/twenty-side-quest/5-gif-magic.md index eab5d57843..193aa8ec70 100644 --- a/oss-gg/twenty-side-quest/5-gif-magic.md +++ b/oss-gg/twenty-side-quest/5-gif-magic.md @@ -40,4 +40,6 @@ Your turn 👇 » 20-October-2024 by Naprila » Link to gif: https://giphy.com/gifs/uiTAwFJ0BWQsQb7jbM ---- + +» 22-October-2024 by Zia Ur Rehman Khan +» Link to gif: https://giphy.com/gifs/MG5FQSrG1mxf1N5qnA From 7fc844ea8f2974470acba56e4b4df1740b00623d Mon Sep 17 00:00:00 2001 From: ZiaCodes <72739794+Khaan25@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:24:16 +0500 Subject: [PATCH 082/123] feat: Side quest tweet fav completed (#7944) ![image](https://github.com/user-attachments/assets/26e7b781-afbe-467f-864f-bf04058a5b2d) --- oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 21840593b1..cb09b99991 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 @@ -31,4 +31,6 @@ Your turn 👇 » 20-October-2024 by Naprila » Link to Tweet: https://x.com/mkprasad_821/status/1847895747707953205 ---- + +» 22-October-2024 by Zia Ur Rehman Khan +» Link to Tweet: https://x.com/zia_webdev/status/1848660000190697633 From e767f16dbef42ac045ab576acce538678b85f78f Mon Sep 17 00:00:00 2001 From: martmull Date: Tue, 22 Oct 2024 14:51:03 +0200 Subject: [PATCH 083/123] 7415 serverless functions update environment variables in a dedicated tab in settings functions not a env file (#7939) ![image](https://github.com/user-attachments/assets/0ef9551d-d867-479e-9a76-faee6930bc0a) ![image](https://github.com/user-attachments/assets/a7aac417-4dd8-401f-8d5b-5b72f31710f6) ![image](https://github.com/user-attachments/assets/16c98e52-a2db-4ed3-b5d2-77745b4d2918) ![image](https://github.com/user-attachments/assets/847d23d6-8a58-4d8f-aff1-4f8a81862964) --- .../src/generated-metadata/gql.ts | 16 +- .../src/generated-metadata/graphql.ts | 94 ++++------- .../twenty-front/src/generated/graphql.tsx | 38 ++--- .../SettingsServerlessFunctionCodeEditor.tsx | 32 ++-- ...ettingsServerlessFunctionCodeEditorTab.tsx | 8 +- .../SettingsServerlessFunctionSettingsTab.tsx | 7 + ...FunctionTabEnvironmentVariableTableRow.tsx | 142 ++++++++++++++++ ...FunctionTabEnvironmentVariablesSection.tsx | 151 ++++++++++++++++++ .../fragments/serverlessFunctionFragment.ts | 1 + .../mutations/deleteOneServerlessFunction.ts | 2 +- .../queries/findManyServerlessFunctions.ts | 8 +- .../queries/findOneServerlessFunction.ts | 4 +- .../hooks/useDeleteOneServerlessFunction.ts | 4 +- .../hooks/useGetManyServerlessFunctions.ts | 3 +- .../hooks/useGetOneServerlessFunction.ts | 9 +- .../useServerlessFunctionUpdateFormState.ts | 5 +- .../SettingsServerlessFunctionDetail.tsx | 1 + ...2426186-updateServerlessFunctionColumns.ts | 19 +++ .../session-storage.module-factory.ts | 1 - ...put.ts => serverless-function-id.input.ts} | 2 +- .../dtos/serverless-function.dto.ts | 5 + .../serverless-function.entity.ts | 3 + .../serverless-function.module.ts | 42 +---- .../serverless-function.resolver.ts | 37 ++++- .../serverless-function.service.ts | 101 +++++++----- .../serverless-functions.integration-spec.ts | 54 +++---- 26 files changed, 547 insertions(+), 242 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariableTableRow.tsx create mode 100644 packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection.tsx create mode 100644 packages/twenty-server/src/database/typeorm/metadata/migrations/1729162426186-updateServerlessFunctionColumns.ts rename packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/{delete-serverless-function.input.ts => serverless-function-id.input.ts} (81%) diff --git a/packages/twenty-front/src/generated-metadata/gql.ts b/packages/twenty-front/src/generated-metadata/gql.ts index 4154825056..cf451ad555 100644 --- a/packages/twenty-front/src/generated-metadata/gql.ts +++ b/packages/twenty-front/src/generated-metadata/gql.ts @@ -33,15 +33,15 @@ const documents = { "\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n": types.DeleteOneFieldMetadataItemDocument, "\n mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) {\n deleteOneRelation(input: { id: $idToDelete }) {\n id\n }\n }\n": types.DeleteOneRelationMetadataItemDocument, "\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument, - "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc, + "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc, "\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument, - "\n \n mutation DeleteOneServerlessFunction($input: DeleteServerlessFunctionInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument, + "\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument, "\n mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument, "\n \n mutation PublishOneServerlessFunction(\n $input: PublishServerlessFunctionInput!\n ) {\n publishServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.PublishOneServerlessFunctionDocument, "\n \n mutation UpdateOneServerlessFunction($input: UpdateServerlessFunctionInput!) {\n updateOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.UpdateOneServerlessFunctionDocument, "\n query FindManyAvailablePackages {\n getAvailablePackages\n }\n": types.FindManyAvailablePackagesDocument, - "\n \n query GetManyServerlessFunctions {\n serverlessFunctions(paging: { first: 100 }) {\n edges {\n node {\n ...ServerlessFunctionFields\n }\n }\n }\n }\n": types.GetManyServerlessFunctionsDocument, - "\n \n query GetOneServerlessFunction($id: UUID!) {\n serverlessFunction(id: $id) {\n ...ServerlessFunctionFields\n }\n }\n": types.GetOneServerlessFunctionDocument, + "\n \n query GetManyServerlessFunctions {\n findManyServerlessFunctions {\n ...ServerlessFunctionFields\n }\n }\n": types.GetManyServerlessFunctionsDocument, + "\n \n query GetOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n findOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.GetOneServerlessFunctionDocument, "\n query FindOneServerlessFunctionSourceCode(\n $input: GetServerlessFunctionSourceCodeInput!\n ) {\n getServerlessFunctionSourceCode(input: $input)\n }\n": types.FindOneServerlessFunctionSourceCodeDocument, }; @@ -142,7 +142,7 @@ export function graphql(source: "\n query ObjectMetadataItems(\n $objectFilt /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n createdAt\n updatedAt\n }\n"]; +export function graphql(source: "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n publishedVersions\n createdAt\n updatedAt\n }\n"): (typeof documents)["\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n publishedVersions\n createdAt\n updatedAt\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -150,7 +150,7 @@ export function graphql(source: "\n \n mutation CreateOneServerlessFunctionIte /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n \n mutation DeleteOneServerlessFunction($input: DeleteServerlessFunctionInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n"): (typeof documents)["\n \n mutation DeleteOneServerlessFunction($input: DeleteServerlessFunctionInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n"]; +export function graphql(source: "\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n"): (typeof documents)["\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -170,11 +170,11 @@ export function graphql(source: "\n query FindManyAvailablePackages {\n getA /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n \n query GetManyServerlessFunctions {\n serverlessFunctions(paging: { first: 100 }) {\n edges {\n node {\n ...ServerlessFunctionFields\n }\n }\n }\n }\n"): (typeof documents)["\n \n query GetManyServerlessFunctions {\n serverlessFunctions(paging: { first: 100 }) {\n edges {\n node {\n ...ServerlessFunctionFields\n }\n }\n }\n }\n"]; +export function graphql(source: "\n \n query GetManyServerlessFunctions {\n findManyServerlessFunctions {\n ...ServerlessFunctionFields\n }\n }\n"): (typeof documents)["\n \n query GetManyServerlessFunctions {\n findManyServerlessFunctions {\n ...ServerlessFunctionFields\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n \n query GetOneServerlessFunction($id: UUID!) {\n serverlessFunction(id: $id) {\n ...ServerlessFunctionFields\n }\n }\n"): (typeof documents)["\n \n query GetOneServerlessFunction($id: UUID!) {\n serverlessFunction(id: $id) {\n ...ServerlessFunctionFields\n }\n }\n"]; +export function graphql(source: "\n \n query GetOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n findOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n"): (typeof documents)["\n \n query GetOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n findOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 620863e117..49c95cb374 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -272,11 +272,6 @@ export type DeleteOneRelationInput = { id: Scalars['UUID']['input']; }; -export type DeleteServerlessFunctionInput = { - /** The id of the function. */ - id: Scalars['ID']['input']; -}; - export type DeleteSsoInput = { identityProviderId: Scalars['String']['input']; }; @@ -694,7 +689,7 @@ export type MutationDeleteOneRemoteServerArgs = { export type MutationDeleteOneServerlessFunctionArgs = { - input: DeleteServerlessFunctionInput; + input: ServerlessFunctionIdInput; }; @@ -959,7 +954,9 @@ export type Query = { fields: FieldConnection; findDistantTablesWithStatus: Array; findManyRemoteServersByType: Array; + findManyServerlessFunctions: Array; findOneRemoteServerById: RemoteServer; + findOneServerlessFunction: ServerlessFunction; findWorkspaceFromInviteHash: Workspace; findWorkspaceInvitations: Array; getAvailablePackages: Scalars['JSON']['output']; @@ -977,8 +974,6 @@ export type Query = { objects: ObjectConnection; relation: Relation; relations: RelationConnection; - serverlessFunction: ServerlessFunction; - serverlessFunctions: ServerlessFunctionConnection; validatePasswordResetToken: ValidatePasswordResetToken; }; @@ -1025,6 +1020,11 @@ export type QueryFindOneRemoteServerByIdArgs = { }; +export type QueryFindOneServerlessFunctionArgs = { + input: ServerlessFunctionIdInput; +}; + + export type QueryFindWorkspaceFromInviteHashArgs = { inviteHash: Scalars['String']['input']; }; @@ -1100,18 +1100,6 @@ export type QueryRelationsArgs = { }; -export type QueryServerlessFunctionArgs = { - id: Scalars['UUID']['input']; -}; - - -export type QueryServerlessFunctionsArgs = { - filter?: ServerlessFunctionFilter; - paging?: CursorPaging; - sorting?: Array; -}; - - export type QueryValidatePasswordResetTokenArgs = { passwordResetToken: Scalars['String']['input']; }; @@ -1227,27 +1215,12 @@ export type ServerlessFunction = { id: Scalars['UUID']['output']; latestVersion?: Maybe; name: Scalars['String']['output']; + publishedVersions: Array; runtime: Scalars['String']['output']; syncStatus: ServerlessFunctionSyncStatus; updatedAt: Scalars['DateTime']['output']; }; -export type ServerlessFunctionConnection = { - __typename?: 'ServerlessFunctionConnection'; - /** Array of edges. */ - edges: Array; - /** Paging information */ - pageInfo: PageInfo; -}; - -export type ServerlessFunctionEdge = { - __typename?: 'ServerlessFunctionEdge'; - /** Cursor for this node. */ - cursor: Scalars['ConnectionCursor']['output']; - /** The node containing the ServerlessFunction */ - node: ServerlessFunction; -}; - export type ServerlessFunctionExecutionResult = { __typename?: 'ServerlessFunctionExecutionResult'; /** Execution result in JSON format */ @@ -1266,22 +1239,11 @@ export enum ServerlessFunctionExecutionStatus { Success = 'SUCCESS' } -export type ServerlessFunctionFilter = { - and?: InputMaybe>; - id?: InputMaybe; - or?: InputMaybe>; +export type ServerlessFunctionIdInput = { + /** The id of the function. */ + id: Scalars['ID']['input']; }; -export type ServerlessFunctionSort = { - direction: SortDirection; - field: ServerlessFunctionSortFields; - nulls?: InputMaybe; -}; - -export enum ServerlessFunctionSortFields { - Id = 'id' -} - /** SyncStatus of the serverlessFunction */ export enum ServerlessFunctionSyncStatus { NotReady = 'NOT_READY', @@ -2000,21 +1962,21 @@ export type ObjectMetadataItemsQueryVariables = Exact<{ export type ObjectMetadataItemsQuery = { __typename?: 'Query', objects: { __typename?: 'ObjectConnection', edges: Array<{ __typename?: 'objectEdge', node: { __typename?: 'object', id: any, dataSourceId: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, createdAt: any, updatedAt: any, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, indexMetadatas: { __typename?: 'ObjectIndexMetadatasConnection', edges: Array<{ __typename?: 'indexEdge', node: { __typename?: 'index', id: any, createdAt: any, updatedAt: any, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, indexFieldMetadatas: { __typename?: 'IndexIndexFieldMetadatasConnection', edges: Array<{ __typename?: 'indexFieldEdge', node: { __typename?: 'indexField', id: any, createdAt: any, updatedAt: any, order: number, fieldMetadataId: any } }> } } }> }, fields: { __typename?: 'ObjectFieldsConnection', edges: Array<{ __typename?: 'fieldEdge', node: { __typename?: 'field', id: any, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: any, updatedAt: any, defaultValue?: any | null, options?: any | null, settings?: any | null, relationDefinition?: { __typename?: 'RelationDefinition', relationId: any, direction: RelationDefinitionType, sourceObjectMetadata: { __typename?: 'object', id: any, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'field', id: any, name: string }, targetObjectMetadata: { __typename?: 'object', id: any, nameSingular: string, namePlural: string }, targetFieldMetadata: { __typename?: 'field', id: any, name: string } } | null } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } }; -export type ServerlessFunctionFieldsFragment = { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, createdAt: any, updatedAt: any }; +export type ServerlessFunctionFieldsFragment = { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, publishedVersions: Array, createdAt: any, updatedAt: any }; export type CreateOneServerlessFunctionItemMutationVariables = Exact<{ input: CreateServerlessFunctionInput; }>; -export type CreateOneServerlessFunctionItemMutation = { __typename?: 'Mutation', createOneServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, createdAt: any, updatedAt: any } }; +export type CreateOneServerlessFunctionItemMutation = { __typename?: 'Mutation', createOneServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, publishedVersions: Array, createdAt: any, updatedAt: any } }; export type DeleteOneServerlessFunctionMutationVariables = Exact<{ - input: DeleteServerlessFunctionInput; + input: ServerlessFunctionIdInput; }>; -export type DeleteOneServerlessFunctionMutation = { __typename?: 'Mutation', deleteOneServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, createdAt: any, updatedAt: any } }; +export type DeleteOneServerlessFunctionMutation = { __typename?: 'Mutation', deleteOneServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, publishedVersions: Array, createdAt: any, updatedAt: any } }; export type ExecuteOneServerlessFunctionMutationVariables = Exact<{ input: ExecuteServerlessFunctionInput; @@ -2028,14 +1990,14 @@ export type PublishOneServerlessFunctionMutationVariables = Exact<{ }>; -export type PublishOneServerlessFunctionMutation = { __typename?: 'Mutation', publishServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, createdAt: any, updatedAt: any } }; +export type PublishOneServerlessFunctionMutation = { __typename?: 'Mutation', publishServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, publishedVersions: Array, createdAt: any, updatedAt: any } }; export type UpdateOneServerlessFunctionMutationVariables = Exact<{ input: UpdateServerlessFunctionInput; }>; -export type UpdateOneServerlessFunctionMutation = { __typename?: 'Mutation', updateOneServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, createdAt: any, updatedAt: any } }; +export type UpdateOneServerlessFunctionMutation = { __typename?: 'Mutation', updateOneServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, publishedVersions: Array, createdAt: any, updatedAt: any } }; export type FindManyAvailablePackagesQueryVariables = Exact<{ [key: string]: never; }>; @@ -2045,14 +2007,14 @@ export type FindManyAvailablePackagesQuery = { __typename?: 'Query', getAvailabl export type GetManyServerlessFunctionsQueryVariables = Exact<{ [key: string]: never; }>; -export type GetManyServerlessFunctionsQuery = { __typename?: 'Query', serverlessFunctions: { __typename?: 'ServerlessFunctionConnection', edges: Array<{ __typename?: 'ServerlessFunctionEdge', node: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, createdAt: any, updatedAt: any } }> } }; +export type GetManyServerlessFunctionsQuery = { __typename?: 'Query', findManyServerlessFunctions: Array<{ __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, publishedVersions: Array, createdAt: any, updatedAt: any }> }; export type GetOneServerlessFunctionQueryVariables = Exact<{ - id: Scalars['UUID']['input']; + input: ServerlessFunctionIdInput; }>; -export type GetOneServerlessFunctionQuery = { __typename?: 'Query', serverlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, createdAt: any, updatedAt: any } }; +export type GetOneServerlessFunctionQuery = { __typename?: 'Query', findOneServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, publishedVersions: Array, createdAt: any, updatedAt: any } }; export type FindOneServerlessFunctionSourceCodeQueryVariables = Exact<{ input: GetServerlessFunctionSourceCodeInput; @@ -2063,7 +2025,7 @@ export type FindOneServerlessFunctionSourceCodeQuery = { __typename?: 'Query', g export const RemoteServerFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteServerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperId"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperOptions"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperType"}},{"kind":"Field","name":{"kind":"Name","value":"userMappingOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}},{"kind":"Field","name":{"kind":"Name","value":"label"}}]}}]} as unknown as DocumentNode; export const RemoteTableFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteTableFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteTable"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"schemaPendingUpdates"}}]}}]} as unknown as DocumentNode; -export const ServerlessFunctionFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; +export const ServerlessFunctionFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; export const CreateServerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"createServer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateRemoteServerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneRemoteServer"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"RemoteServerFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteServerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperId"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperOptions"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperType"}},{"kind":"Field","name":{"kind":"Name","value":"userMappingOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}},{"kind":"Field","name":{"kind":"Name","value":"label"}}]}}]} as unknown as DocumentNode; export const DeleteServerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"deleteServer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServerIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneRemoteServer"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const SyncRemoteTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"syncRemoteTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteTableInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"syncRemoteTable"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"RemoteTableFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteTableFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteTable"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"schemaPendingUpdates"}}]}}]} as unknown as DocumentNode; @@ -2082,12 +2044,12 @@ export const DeleteOneObjectMetadataItemDocument = {"kind":"Document","definitio export const DeleteOneFieldMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneFieldMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneField"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"settings"}}]}}]}}]} as unknown as DocumentNode; export const DeleteOneRelationMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneRelationMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneRelation"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; export const ObjectMetadataItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ObjectMetadataItems"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"objectFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fieldFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"fieldFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isRemote"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"indexMetadatas"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"indexWhereClause"}},{"kind":"Field","name":{"kind":"Name","value":"indexType"}},{"kind":"Field","name":{"kind":"Name","value":"isUnique"}},{"kind":"Field","name":{"kind":"Name","value":"indexFieldMetadatas"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"fieldMetadataId"}}]}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fieldFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"isUnique"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"defaultValue"}},{"kind":"Field","name":{"kind":"Name","value":"options"}},{"kind":"Field","name":{"kind":"Name","value":"settings"}},{"kind":"Field","name":{"kind":"Name","value":"relationDefinition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"relationId"}},{"kind":"Field","name":{"kind":"Name","value":"direction"}},{"kind":"Field","name":{"kind":"Name","value":"sourceObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}}]}},{"kind":"Field","name":{"kind":"Name","value":"sourceFieldMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"targetObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}}]}},{"kind":"Field","name":{"kind":"Name","value":"targetFieldMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode; -export const CreateOneServerlessFunctionItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneServerlessFunctionItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; -export const DeleteOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; +export const CreateOneServerlessFunctionItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneServerlessFunctionItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; +export const DeleteOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; export const ExecuteOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ExecuteOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ExecuteServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"executeOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode; -export const PublishOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"PublishOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PublishServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"publishServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; -export const UpdateOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; +export const PublishOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"PublishOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PublishServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"publishServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; +export const UpdateOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; export const FindManyAvailablePackagesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyAvailablePackages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAvailablePackages"}}]}}]} as unknown as DocumentNode; -export const GetManyServerlessFunctionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetManyServerlessFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverlessFunctions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; -export const GetOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; +export const GetManyServerlessFunctionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetManyServerlessFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findManyServerlessFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; +export const GetOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; export const FindOneServerlessFunctionSourceCodeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneServerlessFunctionSourceCode"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GetServerlessFunctionSourceCodeInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getServerlessFunctionSourceCode"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode; \ No newline at end of file diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 3006c0487f..145f90b385 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -176,11 +176,6 @@ export type DeleteOneObjectInput = { id: Scalars['UUID']; }; -export type DeleteServerlessFunctionInput = { - /** The id of the function. */ - id: Scalars['ID']; -}; - export type DeleteSsoInput = { identityProviderId: Scalars['String']; }; @@ -540,7 +535,7 @@ export type MutationDeleteOneObjectArgs = { export type MutationDeleteOneServerlessFunctionArgs = { - input: DeleteServerlessFunctionInput; + input: ServerlessFunctionIdInput; }; @@ -776,6 +771,8 @@ export type Query = { clientConfig: ClientConfig; currentUser: User; currentWorkspace: Workspace; + findManyServerlessFunctions: Array; + findOneServerlessFunction: ServerlessFunction; findWorkspaceFromInviteHash: Workspace; findWorkspaceInvitations: Array; getAvailablePackages: Scalars['JSON']; @@ -791,8 +788,6 @@ export type Query = { listSSOIdentityProvidersByWorkspaceId: Array; object: Object; objects: ObjectConnection; - serverlessFunction: ServerlessFunction; - serverlessFunctions: ServerlessFunctionConnection; validatePasswordResetToken: ValidatePasswordResetToken; }; @@ -813,6 +808,11 @@ export type QueryCheckWorkspaceInviteHashIsValidArgs = { }; +export type QueryFindOneServerlessFunctionArgs = { + input: ServerlessFunctionIdInput; +}; + + export type QueryFindWorkspaceFromInviteHashArgs = { inviteHash: Scalars['String']; }; @@ -957,27 +957,12 @@ export type ServerlessFunction = { id: Scalars['UUID']; latestVersion?: Maybe; name: Scalars['String']; + publishedVersions: Array; runtime: Scalars['String']; syncStatus: ServerlessFunctionSyncStatus; updatedAt: Scalars['DateTime']; }; -export type ServerlessFunctionConnection = { - __typename?: 'ServerlessFunctionConnection'; - /** Array of edges. */ - edges: Array; - /** Paging information */ - pageInfo: PageInfo; -}; - -export type ServerlessFunctionEdge = { - __typename?: 'ServerlessFunctionEdge'; - /** Cursor for this node. */ - cursor: Scalars['ConnectionCursor']; - /** The node containing the ServerlessFunction */ - node: ServerlessFunction; -}; - export type ServerlessFunctionExecutionResult = { __typename?: 'ServerlessFunctionExecutionResult'; /** Execution result in JSON format */ @@ -996,6 +981,11 @@ export enum ServerlessFunctionExecutionStatus { Success = 'SUCCESS' } +export type ServerlessFunctionIdInput = { + /** The id of the function. */ + id: Scalars['ID']; +}; + /** SyncStatus of the serverlessFunction */ export enum ServerlessFunctionSyncStatus { NotReady = 'NOT_READY', 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 index 7641d8f46d..44425baa4c 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditor.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditor.tsx @@ -72,23 +72,25 @@ export const SettingsServerlessFunctionCodeEditor = ({ ); const environmentDefinition = ` - declare namespace NodeJS { - interface ProcessEnv { - ${Object.keys(environmentVariables) - .map((key) => `${key}: string;`) - .join('\n')} + declare namespace NodeJS { + interface ProcessEnv { + ${Object.keys(environmentVariables) + .map((key) => `${key}: string;`) + .join('\n')} + } } - } + + declare const process: { + env: NodeJS.ProcessEnv; + }; + `; - declare const process: { - env: NodeJS.ProcessEnv; - }; - `; - - monaco.languages.typescript.typescriptDefaults.addExtraLib( - environmentDefinition, - 'ts:process-env.d.ts', - ); + monaco.languages.typescript.typescriptDefaults.setExtraLibs([ + { + content: environmentDefinition, + filePath: 'ts:process-env.d.ts', + }, + ]); } await AutoTypings.create(editor, { 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 c1131c1b66..22958b5ae4 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 @@ -80,9 +80,11 @@ export const SettingsServerlessFunctionCodeEditorTab = ({ const HeaderTabList = ( { - return { id: file.path, title: file.path.split('/').at(-1) || '' }; - })} + tabs={files + .filter((file) => file.path !== '.env') + .map((file) => { + return { id: file.path, title: file.path.split('/').at(-1) || '' }; + })} /> ); diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab.tsx index a59a563848..5f14f4453f 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab.tsx @@ -13,15 +13,18 @@ import { useNavigate } from 'react-router-dom'; import { Key } from 'ts-key-enum'; import { H2Title } from 'twenty-ui'; import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount'; +import { SettingsServerlessFunctionTabEnvironmentVariablesSection } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection'; export const SettingsServerlessFunctionSettingsTab = ({ formValues, serverlessFunctionId, onChange, + onCodeChange, }: { formValues: ServerlessFunctionFormValues; serverlessFunctionId: string; onChange: (key: string) => (value: string) => void; + onCodeChange: (filePath: string, value: string) => void; }) => { const navigate = useNavigate(); const [isDeleteFunctionModalOpen, setIsDeleteFunctionModalOpen] = @@ -58,6 +61,10 @@ export const SettingsServerlessFunctionSettingsTab = ({ formValues={formValues} onChange={onChange} /> +
+ ); +}; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment.ts b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment.ts index bbcad2f1c3..e575dcc9ca 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/fragments/serverlessFunctionFragment.ts @@ -8,6 +8,7 @@ export const SERVERLESS_FUNCTION_FRAGMENT = gql` runtime syncStatus latestVersion + publishedVersions createdAt updatedAt } diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/mutations/deleteOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/mutations/deleteOneServerlessFunction.ts index 5b2b0325b0..b5b568f7ea 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/mutations/deleteOneServerlessFunction.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/mutations/deleteOneServerlessFunction.ts @@ -3,7 +3,7 @@ import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/gr export const DELETE_ONE_SERVERLESS_FUNCTION = gql` ${SERVERLESS_FUNCTION_FRAGMENT} - mutation DeleteOneServerlessFunction($input: DeleteServerlessFunctionInput!) { + mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) { deleteOneServerlessFunction(input: $input) { ...ServerlessFunctionFields } diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/queries/findManyServerlessFunctions.ts b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/queries/findManyServerlessFunctions.ts index dfa0ca1558..37aeccc6db 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/queries/findManyServerlessFunctions.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/queries/findManyServerlessFunctions.ts @@ -4,12 +4,8 @@ import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/gr export const FIND_MANY_SERVERLESS_FUNCTIONS = gql` ${SERVERLESS_FUNCTION_FRAGMENT} query GetManyServerlessFunctions { - serverlessFunctions(paging: { first: 100 }) { - edges { - node { - ...ServerlessFunctionFields - } - } + findManyServerlessFunctions { + ...ServerlessFunctionFields } } `; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/queries/findOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/queries/findOneServerlessFunction.ts index f5ed4d3f27..142c827631 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/graphql/queries/findOneServerlessFunction.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/graphql/queries/findOneServerlessFunction.ts @@ -3,8 +3,8 @@ import { SERVERLESS_FUNCTION_FRAGMENT } from '@/settings/serverless-functions/gr export const FIND_ONE_SERVERLESS_FUNCTION = gql` ${SERVERLESS_FUNCTION_FRAGMENT} - query GetOneServerlessFunction($id: UUID!) { - serverlessFunction(id: $id) { + query GetOneServerlessFunction($input: ServerlessFunctionIdInput!) { + findOneServerlessFunction(input: $input) { ...ServerlessFunctionFields } } diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useDeleteOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useDeleteOneServerlessFunction.ts index db1c3f5a68..4654487b58 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useDeleteOneServerlessFunction.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useDeleteOneServerlessFunction.ts @@ -3,7 +3,7 @@ import { ApolloClient, useMutation } from '@apollo/client'; import { getOperationName } from '@apollo/client/utilities'; import { DELETE_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/mutations/deleteOneServerlessFunction'; import { - DeleteServerlessFunctionInput, + ServerlessFunctionIdInput, DeleteOneServerlessFunctionMutation, DeleteOneServerlessFunctionMutationVariables, } from '~/generated-metadata/graphql'; @@ -19,7 +19,7 @@ export const useDeleteOneServerlessFunction = () => { }); const deleteOneServerlessFunction = async ( - input: DeleteServerlessFunctionInput, + input: ServerlessFunctionIdInput, ) => { return await mutate({ variables: { diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetManyServerlessFunctions.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetManyServerlessFunctions.ts index eff46a628e..4023745907 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetManyServerlessFunctions.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetManyServerlessFunctions.ts @@ -15,7 +15,6 @@ export const useGetManyServerlessFunctions = () => { client: apolloMetadataClient ?? undefined, }); return { - serverlessFunctions: - data?.serverlessFunctions?.edges.map(({ node }) => node) || [], + serverlessFunctions: data?.findManyServerlessFunctions || [], }; }; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetOneServerlessFunction.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetOneServerlessFunction.ts index 8d22a21745..1aeda466d5 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetOneServerlessFunction.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useGetOneServerlessFunction.ts @@ -2,11 +2,14 @@ import { useQuery } from '@apollo/client'; import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient'; import { FIND_ONE_SERVERLESS_FUNCTION } from '@/settings/serverless-functions/graphql/queries/findOneServerlessFunction'; import { + ServerlessFunctionIdInput, GetOneServerlessFunctionQuery, GetOneServerlessFunctionQueryVariables, } from '~/generated-metadata/graphql'; -export const useGetOneServerlessFunction = (id: string) => { +export const useGetOneServerlessFunction = ( + input: ServerlessFunctionIdInput, +) => { const apolloMetadataClient = useApolloMetadataClient(); const { data } = useQuery< GetOneServerlessFunctionQuery, @@ -14,10 +17,10 @@ export const useGetOneServerlessFunction = (id: string) => { >(FIND_ONE_SERVERLESS_FUNCTION, { client: apolloMetadataClient ?? undefined, variables: { - id, + input, }, }); return { - serverlessFunction: data?.serverlessFunction || null, + serverlessFunction: data?.findOneServerlessFunction || null, }; }; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts index 9e8a134838..2d377ac78d 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState.ts @@ -29,8 +29,9 @@ export const useServerlessFunctionUpdateFormState = ( code: undefined, }); - const { serverlessFunction } = - useGetOneServerlessFunction(serverlessFunctionId); + const { serverlessFunction } = useGetOneServerlessFunction({ + id: serverlessFunctionId, + }); const { loading } = useGetOneServerlessFunctionSourceCode({ id: serverlessFunctionId, 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 b6237754fd..9c2ee994d1 100644 --- a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx +++ b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx @@ -217,6 +217,7 @@ export const SettingsServerlessFunctionDetail = () => { formValues={formValues} serverlessFunctionId={serverlessFunctionId} onChange={onChange} + onCodeChange={onCodeChange} /> ); default: diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1729162426186-updateServerlessFunctionColumns.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1729162426186-updateServerlessFunctionColumns.ts new file mode 100644 index 0000000000..1d765ba0e0 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/metadata/migrations/1729162426186-updateServerlessFunctionColumns.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class UpdateServerlessFunctionColumns1729162426186 + implements MigrationInterface +{ + name = 'UpdateServerlessFunctionColumns1729162426186'; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "metadata"."serverlessFunction" ADD "publishedVersions" jsonb NOT NULL DEFAULT '[]'`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `ALTER TABLE "metadata"."serverlessFunction" DROP COLUMN "publishedVersions"`, + ); + } +} 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 index 8f5099ba5b..e3022b8a42 100644 --- 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 @@ -6,7 +6,6 @@ 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, diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/delete-serverless-function.input.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function-id.input.ts similarity index 81% rename from packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/delete-serverless-function.input.ts rename to packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function-id.input.ts index 27cfe36fea..7cee1c799f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/delete-serverless-function.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function-id.input.ts @@ -3,7 +3,7 @@ import { ID, InputType } from '@nestjs/graphql'; import { IDField } from '@ptc-org/nestjs-query-graphql'; @InputType() -export class DeleteServerlessFunctionInput { +export class ServerlessFunctionIdInput { @IDField(() => ID, { description: 'The id of the function.' }) id!: string; } diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto.ts index e24687cec8..bca0230180 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto.ts @@ -11,6 +11,7 @@ import { QueryOptions, } from '@ptc-org/nestjs-query-graphql'; import { + IsArray, IsDateString, IsEnum, IsNotEmpty, @@ -59,6 +60,10 @@ export class ServerlessFunctionDTO { @Field({ nullable: true }) latestVersion: string; + @IsArray() + @Field(() => [String], { nullable: false }) + publishedVersions: string[]; + @IsEnum(ServerlessFunctionSyncStatus) @IsNotEmpty() @Field(() => ServerlessFunctionSyncStatus) diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts index 58406efbda..b06c626a73 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.entity.ts @@ -29,6 +29,9 @@ export class ServerlessFunctionEntity { @Column({ nullable: true }) latestVersion: string; + @Column({ nullable: false, type: 'jsonb', default: [] }) + publishedVersions: string[]; + @Column({ nullable: false, default: ServerlessFunctionRuntime.NODE18 }) runtime: ServerlessFunctionRuntime; diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts index 076343f4d2..df5ef68aaa 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts @@ -1,55 +1,23 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; -import { SortDirection } from '@ptc-org/nestjs-query-core'; -import { - NestjsQueryGraphQLModule, - PagingStrategies, -} from '@ptc-org/nestjs-query-graphql'; import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; -import { ServerlessModule } from 'src/engine/core-modules/serverless/serverless.module'; import { ThrottlerModule } from 'src/engine/core-modules/throttler/throttler.module'; -import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; -import { ServerlessFunctionDTO } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function.dto'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; import { ServerlessFunctionResolver } from 'src/engine/metadata-modules/serverless-function/serverless-function.resolver'; import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; @Module({ imports: [ - NestjsQueryGraphQLModule.forFeature({ - imports: [ - FileUploadModule, - NestjsQueryTypeOrmModule.forFeature( - [ServerlessFunctionEntity], - 'metadata', - ), - TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), - FileModule, - ThrottlerModule, - ], - services: [ServerlessFunctionService], - resolvers: [ - { - EntityClass: ServerlessFunctionEntity, - DTOClass: ServerlessFunctionDTO, - ServiceClass: ServerlessFunctionService, - pagingStrategy: PagingStrategies.CURSOR, - read: { - defaultSort: [{ field: 'id', direction: SortDirection.DESC }], - }, - create: { disabled: true }, - update: { disabled: true }, - delete: { disabled: true }, - guards: [WorkspaceAuthGuard], - }, - ], - }), - ServerlessModule, + FileUploadModule, + NestjsQueryTypeOrmModule.forFeature([ServerlessFunctionEntity], 'metadata'), + TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), + FileModule, + ThrottlerModule, ], providers: [ServerlessFunctionService, ServerlessFunctionResolver], exports: [ServerlessFunctionService], diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.resolver.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.resolver.ts index 05440c6c7a..2d1dd0152c 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.resolver.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.resolver.ts @@ -11,7 +11,7 @@ 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'; import { CreateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input'; -import { DeleteServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/delete-serverless-function.input'; +import { ServerlessFunctionIdInput } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-id.input'; import { ExecuteServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/execute-serverless-function.input'; import { GetServerlessFunctionSourceCodeInput } from 'src/engine/metadata-modules/serverless-function/dtos/get-serverless-function-source-code.input'; import { PublishServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/publish-serverless-function.input'; @@ -50,6 +50,39 @@ export class ServerlessFunctionResolver { } } + @Query(() => ServerlessFunctionDTO) + async findOneServerlessFunction( + @Args('input') { id }: ServerlessFunctionIdInput, + @AuthWorkspace() { id: workspaceId }: Workspace, + ) { + try { + await this.checkFeatureFlag(workspaceId); + + return ( + await this.serverlessFunctionService.findManyServerlessFunctions({ + id, + }) + )?.[0]; + } catch (error) { + serverlessFunctionGraphQLApiExceptionHandler(error); + } + } + + @Query(() => [ServerlessFunctionDTO]) + async findManyServerlessFunctions( + @AuthWorkspace() { id: workspaceId }: Workspace, + ) { + try { + await this.checkFeatureFlag(workspaceId); + + return await this.serverlessFunctionService.findManyServerlessFunctions({ + workspaceId, + }); + } catch (error) { + serverlessFunctionGraphQLApiExceptionHandler(error); + } + } + @Query(() => graphqlTypeJson) async getAvailablePackages(@AuthWorkspace() { id: workspaceId }: Workspace) { try { @@ -81,7 +114,7 @@ export class ServerlessFunctionResolver { @Mutation(() => ServerlessFunctionDTO) async deleteOneServerlessFunction( - @Args('input') input: DeleteServerlessFunctionInput, + @Args('input') input: ServerlessFunctionIdInput, @AuthWorkspace() { id: workspaceId }: Workspace, ) { try { 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 7e0fcbf9de..2e69781ac0 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 @@ -3,7 +3,6 @@ import { InjectRepository } from '@nestjs/typeorm'; import { basename, dirname, join } from 'path'; -import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import deepEqual from 'deep-equal'; import { Repository } from 'typeorm'; @@ -34,7 +33,7 @@ import { import { isDefined } from 'src/utils/is-defined'; @Injectable() -export class ServerlessFunctionService extends TypeOrmQueryService { +export class ServerlessFunctionService { constructor( private readonly fileStorageService: FileStorageService, private readonly serverlessService: ServerlessService, @@ -42,8 +41,10 @@ export class ServerlessFunctionService extends TypeOrmQueryService, private readonly throttlerService: ThrottlerService, private readonly environmentService: EnvironmentService, - ) { - super(serverlessFunctionRepository); + ) {} + + async findManyServerlessFunctions(where) { + return this.serverlessFunctionRepository.findBy(where); } async getServerlessFunctionSourceCode( @@ -51,12 +52,11 @@ export class ServerlessFunctionService extends TypeOrmQueryService { - const serverlessFunction = await this.serverlessFunctionRepository.findOne({ - where: { + const serverlessFunction = + await this.serverlessFunctionRepository.findOneBy({ id, workspaceId, - }, - }); + }); if (!serverlessFunction) { throw new ServerlessFunctionException( @@ -101,12 +101,12 @@ export class ServerlessFunctionService extends TypeOrmQueryService { await this.throttleExecution(workspaceId); - const functionToExecute = await this.serverlessFunctionRepository.findOne({ - where: { + const functionToExecute = await this.serverlessFunctionRepository.findOneBy( + { id, workspaceId, }, - }); + ); if (!functionToExecute) { throw new ServerlessFunctionException( @@ -120,9 +120,7 @@ export class ServerlessFunctionService extends TypeOrmQueryService { it('should find many serverlessFunctions', () => { const queryData = { query: ` - query serverlessFunctions { - serverlessFunctions { - edges { - node { - id - name - description - runtime - latestVersion - syncStatus - createdAt - updatedAt - } - } + query GetManyServerlessFunctions { + findManyServerlessFunctions { + id + name + description + runtime + syncStatus + latestVersion + publishedVersions + createdAt + updatedAt } } `, @@ -35,24 +32,23 @@ describe('serverlessFunctionsResolver (e2e)', () => { expect(res.body.errors).toBeUndefined(); }) .expect((res) => { - const data = res.body.data.serverlessFunctions; + const serverlessFunctions = res.body.data.findManyServerlessFunctions; - expect(data).toBeDefined(); - expect(Array.isArray(data.edges)).toBe(true); + expect(serverlessFunctions).toBeDefined(); + expect(Array.isArray(serverlessFunctions)).toBe(true); - const edges = data.edges; + if (serverlessFunctions.length > 0) { + const serverlessFunction = serverlessFunctions[0]; - 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'); + expect(serverlessFunction).toHaveProperty('id'); + expect(serverlessFunction).toHaveProperty('name'); + expect(serverlessFunction).toHaveProperty('description'); + expect(serverlessFunction).toHaveProperty('runtime'); + expect(serverlessFunction).toHaveProperty('syncStatus'); + expect(serverlessFunction).toHaveProperty('latestVersion'); + expect(serverlessFunction).toHaveProperty('publishedVersions'); + expect(serverlessFunction).toHaveProperty('createdAt'); + expect(serverlessFunction).toHaveProperty('updatedAt'); } }); }); From f0a2d38471c9e52c23ff336d80253d8eb157a7c2 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Tue, 22 Oct 2024 15:47:16 +0200 Subject: [PATCH 084/123] Improve search algorithm (#7955) We were previously checking for matching with each search term independently. Ex searching for "felix malfait" we were searching for correspondances with "felix" and "malfait". As a result record A with name "Marie-Claude Mala" and email "ma.lala@email.com" had a biggest search score than record B "Felix Malfait" with email felix@email.com for search "felix ma": for record A we had 0 match with felix and 3 matches with "ma" ("marie", "mala", "ma") for record B we had 1 match with felix and 1 match with "ma" (with "malfait"). So we want to give more weight to a row that would combine matches with both terms, considering "felix malfait" altogether. --- .../graphql-query-search-resolver.service.ts | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) 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 378dfff97f..ac77cb9d78 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 @@ -13,7 +13,6 @@ import { SearchResolverArgs } from 'src/engine/api/graphql/workspace-resolver-bu 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'; @@ -24,7 +23,6 @@ export class GraphqlQuerySearchResolverService { constructor( private readonly twentyORMGlobalManager: TwentyORMGlobalManager, - private readonly featureFlagService: FeatureFlagService, ) {} async resolve< @@ -61,7 +59,8 @@ export class GraphqlQuerySearchResolverService hasPreviousPage: false, }); } - const searchTerms = this.formatSearchTerms(args.searchInput); + const searchTerms = this.formatSearchTerms(args.searchInput, 'and'); + const searchTermsOr = this.formatSearchTerms(args.searchInput, 'or'); const limit = args?.limit ?? QUERY_MAX_RECORDS; @@ -86,11 +85,22 @@ export class GraphqlQuerySearchResolverService : `"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery(:searchTerms)`, searchTerms === '' ? {} : { searchTerms }, ) + .orWhere( + searchTermsOr === '' + ? `"${SEARCH_VECTOR_FIELD.name}" IS NOT NULL` + : `"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery(:searchTermsOr)`, + searchTermsOr === '' ? {} : { searchTermsOr }, + ) .orderBy( - `ts_rank("${SEARCH_VECTOR_FIELD.name}", to_tsquery(:searchTerms))`, + `ts_rank_cd("${SEARCH_VECTOR_FIELD.name}", to_tsquery(:searchTerms))`, + 'DESC', + ) + .addOrderBy( + `ts_rank("${SEARCH_VECTOR_FIELD.name}", to_tsquery(:searchTermsOr))`, 'DESC', ) .setParameter('searchTerms', searchTerms) + .setParameter('searchTermsOr', searchTermsOr) .take(limit) .getMany()) as ObjectRecord[]; @@ -110,7 +120,10 @@ export class GraphqlQuerySearchResolverService }); } - private formatSearchTerms(searchTerm: string) { + private formatSearchTerms( + searchTerm: string, + operator: 'and' | 'or' = 'and', + ) { if (searchTerm === '') { return ''; } @@ -121,7 +134,7 @@ export class GraphqlQuerySearchResolverService return `${escapedWord}:*`; }); - return formattedWords.join(' | '); + return formattedWords.join(` ${operator === 'and' ? '&' : '|'} `); } async validate( From 18cfe79b802370c73988d842440699b0561b1326 Mon Sep 17 00:00:00 2001 From: Ana Sofia Marin Alexandre <61988046+anamarn@users.noreply.github.com> Date: Tue, 22 Oct 2024 10:48:11 -0300 Subject: [PATCH 085/123] bug fix webhook response not sending data to tinybird (#7952) Solves https://github.com/twentyhq/private-issues/issues/118 **TLDR** Fix webhook response not sending data to tinybird when the url is not a link. **Changes in Tinybird:** - Add column Success to webhook payload (boolean) - Changed the parameter WebhookIdRequest to WebhookId in the getWebhooksResponse api point. - Those changes can be seen in the tinybird workspace twenty_analytics_playground **In order to test** 1. Set ANALYTICS_ENABLED to true 2. Set TINYBIRD_INGEST_TOKEN to your token from the workspace twenty_analytics_playground 3. Set TINYBIRD_GENERATE_JWT_TOKEN to the admin kwt token from the workspace twenty_analytics_playground 4. Set TINYBIRD_WORKSPACE_UUID to the UUID of twenty_analytics_playground 5. Create a Webhook in twenty and set wich events it needs to track 6. Run twenty-worker in order to make the webhooks work. 7. Do your tasks in order to populate the data 8. Look at your webhooks in settings>api and webhooks> your webhook and the statistics should be displayed --- .../__tests__/fetchGraphDataOrThrow.test.js | 6 +++++- .../webhook/utils/fetchGraphDataOrThrow.ts | 2 +- .../jobs/call-webhook.job.ts | 19 ++++++++++++------- 3 files changed, 18 insertions(+), 9 deletions(-) diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/utils/__tests__/fetchGraphDataOrThrow.test.js b/packages/twenty-front/src/modules/settings/developers/webhook/utils/__tests__/fetchGraphDataOrThrow.test.js index 365c964d2c..89e493e65a 100644 --- a/packages/twenty-front/src/modules/settings/developers/webhook/utils/__tests__/fetchGraphDataOrThrow.test.js +++ b/packages/twenty-front/src/modules/settings/developers/webhook/utils/__tests__/fetchGraphDataOrThrow.test.js @@ -7,6 +7,7 @@ global.fetch = jest.fn(); describe('fetchGraphDataOrThrow', () => { const mockWebhookId = 'test-webhook-id'; const mockWindowLength = '7D'; + const mockTinybirdJwt = 'test-jwt'; beforeEach(() => { jest.resetAllMocks(); @@ -27,6 +28,7 @@ describe('fetchGraphDataOrThrow', () => { const result = await fetchGraphDataOrThrow({ webhookId: mockWebhookId, windowLength: mockWindowLength, + tinybirdJwt: mockTinybirdJwt, }); expect(global.fetch).toHaveBeenCalledWith( @@ -71,6 +73,7 @@ describe('fetchGraphDataOrThrow', () => { fetchGraphDataOrThrow({ webhookId: mockWebhookId, windowLength: mockWindowLength, + tinybirdJwt: mockTinybirdJwt, }), ).rejects.toThrow('Something went wrong while fetching webhook usage'); }); @@ -85,13 +88,14 @@ describe('fetchGraphDataOrThrow', () => { await fetchGraphDataOrThrow({ webhookId: mockWebhookId, windowLength: '1D', + tinybirdJwt: mockTinybirdJwt, }); expect(global.fetch).toHaveBeenCalledWith( expect.stringContaining( new URLSearchParams({ ...WEBHOOK_GRAPH_API_OPTIONS_MAP['1D'], - webhookIdRequest: mockWebhookId, + webhookId: mockWebhookId, }).toString(), ), expect.any(Object), diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts b/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts index 4b66c4b9b0..083b061da8 100644 --- a/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts +++ b/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts @@ -14,7 +14,7 @@ export const fetchGraphDataOrThrow = async ({ }: fetchGraphDataOrThrowProps) => { const queryString = new URLSearchParams({ ...WEBHOOK_GRAPH_API_OPTIONS_MAP[windowLength], - webhookIdRequest: webhookId, + webhookId, }).toString(); const response = await fetch( diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook.job.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook.job.ts index e03c83204e..f0ec8317a6 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook.job.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook.job.ts @@ -26,18 +26,24 @@ export class CallWebhookJob { @Process(CallWebhookJob.name) async handle(data: CallWebhookJobData): Promise { + const commonPayload = { + url: data.targetUrl, + webhookId: data.webhookId, + eventName: data.eventName, + }; + try { const response = await this.httpService.axiosRef.post( data.targetUrl, data, ); + const success = response.status >= 200 && response.status < 300; const eventInput = { action: 'webhook.response', payload: { status: response.status, - url: data.targetUrl, - webhookId: data.webhookId, - eventName: data.eventName, + success, + ...commonPayload, }, }; @@ -46,10 +52,9 @@ export class CallWebhookJob { const eventInput = { action: 'webhook.response', payload: { - status: err.response.status, - url: data.targetUrl, - webhookId: data.webhookId, - eventName: data.eventName, + success: false, + ...commonPayload, + ...(err.response && { status: err.response.status }), }, }; From 02c34d547f9fed2168458eb073793ec870511917 Mon Sep 17 00:00:00 2001 From: Weiko Date: Tue, 22 Oct 2024 16:40:18 +0200 Subject: [PATCH 086/123] Fix redis connection (#7956) ## Context bull-mq connection was not working as intended, the connection parameter was ignored and was falling back to localhost. This PR should fix the issue by instantiating a IORedis client following bullmq documentation https://docs.bullmq.io/guide/connections I also changed cache-storage module to use IORedis client as well to be more consistent even though it was not necessary there. We could move that instantiation to a factory class in the future. ## Test start server + worker with correct port and wrong port with cache-storage-type memory/redis and queue-type sync/bull-mq --- .../cache-storage.module-factory.ts | 12 ++----- .../cache-storage/cache-storage.module.ts | 3 +- .../engine/core-modules/core-engine.module.ts | 7 ++-- .../message-queue.module-factory.ts | 14 ++------ .../redis-client/redis-client.module.ts | 12 +++++++ .../redis-client/redis-client.service.ts | 33 +++++++++++++++++++ 6 files changed, 58 insertions(+), 23 deletions(-) create mode 100644 packages/twenty-server/src/engine/core-modules/redis-client/redis-client.module.ts create mode 100644 packages/twenty-server/src/engine/core-modules/redis-client/redis-client.service.ts diff --git a/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module-factory.ts b/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module-factory.ts index 5a197a98e9..3a7928e48a 100644 --- a/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module-factory.ts +++ b/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module-factory.ts @@ -4,9 +4,11 @@ import { redisStore } from 'cache-manager-redis-yet'; import { CacheStorageType } from 'src/engine/core-modules/cache-storage/types/cache-storage-type.enum'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { RedisClientService } from 'src/engine/core-modules/redis-client/redis-client.service'; export const cacheStorageModuleFactory = ( environmentService: EnvironmentService, + redisClientService: RedisClientService, ): CacheModuleOptions => { const cacheStorageType = environmentService.get('CACHE_STORAGE_TYPE'); const cacheStorageTtl = environmentService.get('CACHE_STORAGE_TTL'); @@ -20,18 +22,10 @@ export const cacheStorageModuleFactory = ( return cacheModuleOptions; } case CacheStorageType.Redis: { - const connectionString = environmentService.get('REDIS_URL'); - - if (!connectionString) { - throw new Error( - `${cacheStorageType} cache storage requires REDIS_URL to be defined, check your .env file`, - ); - } - return { ...cacheModuleOptions, store: redisStore, - url: connectionString, + client: redisClientService.getClient(), }; } default: diff --git a/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module.ts b/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module.ts index 3f12d09e6d..dbefb42845 100644 --- a/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module.ts +++ b/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module.ts @@ -7,6 +7,7 @@ import { FlushCacheCommand } from 'src/engine/core-modules/cache-storage/command 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 { RedisClientService } from 'src/engine/core-modules/redis-client/redis-client.service'; @Global() @Module({ @@ -15,7 +16,7 @@ import { EnvironmentService } from 'src/engine/core-modules/environment/environm isGlobal: true, imports: [ConfigModule], useFactory: cacheStorageModuleFactory, - inject: [EnvironmentService], + inject: [EnvironmentService, RedisClientService], }), ], providers: [ 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 00cb30716f..48d1819e13 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 @@ -32,15 +32,17 @@ import { messageQueueModuleFactory } from 'src/engine/core-modules/message-queue import { TimelineMessagingModule } from 'src/engine/core-modules/messaging/timeline-messaging.module'; import { OpenApiModule } from 'src/engine/core-modules/open-api/open-api.module'; import { PostgresCredentialsModule } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.module'; +import { RedisClientModule } from 'src/engine/core-modules/redis-client/redis-client.module'; +import { RedisClientService } from 'src/engine/core-modules/redis-client/redis-client.service'; import { serverlessModuleFactory } from 'src/engine/core-modules/serverless/serverless-module.factory'; import { ServerlessModule } from 'src/engine/core-modules/serverless/serverless.module'; +import { WorkspaceSSOModule } from 'src/engine/core-modules/sso/sso.module'; import { TelemetryModule } from 'src/engine/core-modules/telemetry/telemetry.module'; import { UserModule } from 'src/engine/core-modules/user/user.module'; import { WorkflowTriggerApiModule } from 'src/engine/core-modules/workflow/workflow-trigger-api.module'; 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'; @@ -69,6 +71,7 @@ import { FileModule } from './file/file.module'; ActorModule, TelemetryModule, EnvironmentModule.forRoot({}), + RedisClientModule, FileStorageModule.forRootAsync({ useFactory: fileStorageModuleFactory, inject: [EnvironmentService], @@ -79,7 +82,7 @@ import { FileModule } from './file/file.module'; }), MessageQueueModule.registerAsync({ useFactory: messageQueueModuleFactory, - inject: [EnvironmentService], + inject: [EnvironmentService, RedisClientService], }), ExceptionHandlerModule.forRootAsync({ useFactory: exceptionHandlerModuleFactory, diff --git a/packages/twenty-server/src/engine/core-modules/message-queue/message-queue.module-factory.ts b/packages/twenty-server/src/engine/core-modules/message-queue/message-queue.module-factory.ts index 2948b2ad90..d19b23797e 100644 --- a/packages/twenty-server/src/engine/core-modules/message-queue/message-queue.module-factory.ts +++ b/packages/twenty-server/src/engine/core-modules/message-queue/message-queue.module-factory.ts @@ -1,5 +1,3 @@ -import { ConnectionOptions } from 'tls'; - import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { BullMQDriverFactoryOptions, @@ -8,6 +6,7 @@ import { PgBossDriverFactoryOptions, SyncDriverFactoryOptions, } from 'src/engine/core-modules/message-queue/interfaces'; +import { RedisClientService } from 'src/engine/core-modules/redis-client/redis-client.service'; /** * MessageQueue Module factory @@ -16,6 +15,7 @@ import { */ export const messageQueueModuleFactory = async ( environmentService: EnvironmentService, + redisClientService: RedisClientService, ): Promise => { const driverType = environmentService.get('MESSAGE_QUEUE_TYPE'); @@ -37,18 +37,10 @@ export const messageQueueModuleFactory = async ( } satisfies PgBossDriverFactoryOptions; } case MessageQueueDriverType.BullMQ: { - const connectionString = environmentService.get('REDIS_URL'); - - if (!connectionString) { - throw new Error( - `${MessageQueueDriverType.BullMQ} message queue requires REDIS_URL to be defined, check your .env file`, - ); - } - return { type: MessageQueueDriverType.BullMQ, options: { - connection: connectionString as ConnectionOptions, + connection: redisClientService.getClient(), }, } satisfies BullMQDriverFactoryOptions; } diff --git a/packages/twenty-server/src/engine/core-modules/redis-client/redis-client.module.ts b/packages/twenty-server/src/engine/core-modules/redis-client/redis-client.module.ts new file mode 100644 index 0000000000..8e62a0db73 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/redis-client/redis-client.module.ts @@ -0,0 +1,12 @@ +import { Global, Module } from '@nestjs/common'; + +import { EnvironmentModule } from 'src/engine/core-modules/environment/environment.module'; +import { RedisClientService } from 'src/engine/core-modules/redis-client/redis-client.service'; + +@Global() +@Module({ + imports: [EnvironmentModule], + providers: [RedisClientService], + exports: [RedisClientService], +}) +export class RedisClientModule {} diff --git a/packages/twenty-server/src/engine/core-modules/redis-client/redis-client.service.ts b/packages/twenty-server/src/engine/core-modules/redis-client/redis-client.service.ts new file mode 100644 index 0000000000..f721371b88 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/redis-client/redis-client.service.ts @@ -0,0 +1,33 @@ +import { Injectable, OnModuleDestroy } from '@nestjs/common'; + +import IORedis from 'ioredis'; + +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; + +@Injectable() +export class RedisClientService implements OnModuleDestroy { + private redisClient: IORedis | null = null; + + constructor(private readonly environmentService: EnvironmentService) {} + + getClient() { + if (!this.redisClient) { + const redisUrl = this.environmentService.get('REDIS_URL'); + + if (!redisUrl) { + throw new Error('REDIS_URL must be defined'); + } + + this.redisClient = new IORedis(redisUrl); + } + + return this.redisClient; + } + + async onModuleDestroy() { + if (this.redisClient) { + await this.redisClient.quit(); + this.redisClient = null; + } + } +} From 430644448a3de18dd3126d549d82bc064314d3d2 Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:36:26 +0200 Subject: [PATCH 087/123] Migrate to twenty-ui - navigation/link (#7837) This PR was created by [GitStart](https://gitstart.com/) to address the requirements from this ticket: [TWNTY-7535](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-7535). --- ### Description. Migrate link components to `twenty-ui` \ \ Fixes #7535 --------- Co-authored-by: gitstart-twenty Co-authored-by: gitstart-twenty <140154534+gitstart-twenty@users.noreply.github.com> Co-authored-by: Charles Bochet --- .../sign-in-up/components/SignInUpForm.tsx | 5 +- .../components/AppNavigationDrawer.tsx | 16 +++- .../object-record/components/RecordChip.tsx | 3 +- .../RecordIndexOptionsDropdownContent.tsx | 2 +- .../RecordTableHeaderPlusButtonContent.tsx | 3 +- ...SettingsAccountsCalendarChannelDetails.tsx | 7 +- .../SettingsAccountsMessageChannelDetails.tsx | 7 +- .../SettingsAccountsSettingsSection.tsx | 2 +- .../SettingsDataModelFieldToggle.tsx | 2 +- .../SettingsObjectNewFieldSelector.tsx | 3 +- ...IntegrationRemoteTableSyncStatusToggle.tsx | 2 +- ...tegrationDatabaseConnectionSummaryCard.tsx | 8 +- .../SettingsSecurityOptionsList.tsx | 11 ++- .../components/ToggleImpersonate.tsx | 2 +- .../ValidationStep/ValidationStep.tsx | 3 +- .../ValidationStep/components/columns.tsx | 3 +- .../field/display/components/EmailDisplay.tsx | 2 +- .../display/components/EmailsDisplay.tsx | 3 +- .../field/display/components/LinkDisplay.tsx | 7 +- .../field/display/components/LinksDisplay.tsx | 9 +- .../field/display/components/PhoneDisplay.tsx | 2 +- .../display/components/PhonesDisplay.tsx | 3 +- .../field/display/components/URLDisplay.tsx | 8 +- .../modules/ui/input/components/Select.tsx | 4 +- .../components/__stories__/Select.stories.tsx | 5 +- .../RightDrawerTopBarExpandButton.tsx | 3 +- .../menu-item/components/MenuItemToggle.tsx | 4 +- .../components/NavigationDrawerBackButton.tsx | 3 +- .../__stories__/NavigationDrawer.stories.tsx | 6 +- .../twenty-front/src/pages/auth/Authorize.tsx | 2 +- .../src/pages/not-found/NotFound.tsx | 3 +- .../src/pages/onboarding/ChooseYourPlan.tsx | 5 +- .../src/pages/onboarding/PaymentSuccess.tsx | 4 +- .../src/pages/onboarding/SyncEmails.tsx | 5 +- .../src/pages/settings/SettingsWorkspace.tsx | 7 +- .../SettingsObjectDetailPageContent.tsx | 3 +- .../settings/data-model/SettingsObjects.tsx | 9 +- .../SettingsServerlessFunctions.tsx | 3 +- .../getDisplayValueByUrlType.test.ts | 2 +- .../twenty-front/src/utils/checkUrlType.ts | 3 +- .../src/utils/getDisplayValueByUrlType.ts | 3 +- packages/twenty-front/tsup.ui.index.tsx | 8 -- packages/twenty-ui/src/index.ts | 1 + .../src}/input/components/Toggle.tsx | 2 +- packages/twenty-ui/src/input/index.ts | 1 + packages/twenty-ui/src/navigation/index.ts | 10 +++ .../navigation/link/components/ActionLink.tsx | 0 .../components/AdvancedSettingsToggle.tsx | 21 +++-- .../link/components/ContactLink.tsx | 0 .../link/components/GithubVersionLink.tsx | 15 ++-- .../navigation/link/components/RawLink.tsx | 0 .../link/components/RoundedLink.tsx | 2 +- .../navigation/link/components/SocialLink.tsx | 3 +- .../link/components/UndecoratedLink.tsx | 0 .../__stories__/ActionLink.stories.tsx | 5 +- .../__stories__/ContactLink.stories.tsx | 3 +- .../__stories__/GithubVersionLink.stories.tsx | 3 +- .../__stories__/RawLink.stories.tsx | 3 +- .../__stories__/RoundedLink.stories.tsx | 3 +- .../__stories__/SocialLink.stories.tsx | 3 +- .../__stories__/UndecoratedLink.stories.tsx | 5 +- .../src/navigation/link/components/index.ts | 8 ++ .../src}/navigation/link/constants/Cal.ts | 0 .../navigation/link/constants/GithubLink.ts | 0 .../twenty-ui/src/navigation/link/index.ts | 3 + .../ComponentWithRouterDecorator.tsx | 82 +++++++++++++++++++ packages/twenty-ui/src/testing/index.ts | 1 + .../src/utilities/getDisplayValueByUrlType.ts | 34 ++++++++ packages/twenty-ui/src/utilities/index.ts | 3 +- .../src/content/twenty-ui/input/toggle.mdx | 2 +- .../content/twenty-ui/navigation/links.mdx | 8 +- 71 files changed, 262 insertions(+), 154 deletions(-) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/components/Toggle.tsx (97%) create mode 100644 packages/twenty-ui/src/input/index.ts rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/navigation/link/components/ActionLink.tsx (100%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/navigation/link/components/AdvancedSettingsToggle.tsx (77%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/navigation/link/components/ContactLink.tsx (100%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/navigation/link/components/GithubVersionLink.tsx (51%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/navigation/link/components/RawLink.tsx (100%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/navigation/link/components/RoundedLink.tsx (96%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/navigation/link/components/SocialLink.tsx (87%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/navigation/link/components/UndecoratedLink.tsx (100%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/navigation/link/components/__stories__/ActionLink.stories.tsx (82%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/navigation/link/components/__stories__/ContactLink.stories.tsx (87%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/navigation/link/components/__stories__/GithubVersionLink.stories.tsx (79%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/navigation/link/components/__stories__/RawLink.stories.tsx (89%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/navigation/link/components/__stories__/RoundedLink.stories.tsx (89%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/navigation/link/components/__stories__/SocialLink.stories.tsx (91%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/navigation/link/components/__stories__/UndecoratedLink.stories.tsx (79%) create mode 100644 packages/twenty-ui/src/navigation/link/components/index.ts rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/navigation/link/constants/Cal.ts (100%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/navigation/link/constants/GithubLink.ts (100%) create mode 100644 packages/twenty-ui/src/navigation/link/index.ts create mode 100644 packages/twenty-ui/src/testing/decorators/ComponentWithRouterDecorator.tsx create mode 100644 packages/twenty-ui/src/utilities/getDisplayValueByUrlType.ts diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx index 120eacb5fd..ba4add2976 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx @@ -5,7 +5,7 @@ import { useMemo, useState } from 'react'; import { Controller } from 'react-hook-form'; import { useRecoilState, useRecoilValue } from 'recoil'; import { Key } from 'ts-key-enum'; -import { IconGoogle, IconMicrosoft, IconKey } from 'twenty-ui'; +import { ActionLink, IconGoogle, IconKey, IconMicrosoft } from 'twenty-ui'; import { FooterNote } from '@/auth/sign-in-up/components/FooterNote'; import { HorizontalSeparator } from '@/auth/sign-in-up/components/HorizontalSeparator'; @@ -14,15 +14,14 @@ import { SignInUpMode, useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp'; import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm'; import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle'; import { useSignInWithMicrosoft } from '@/auth/sign-in-up/hooks/useSignInWithMicrosoft'; +import { SignInUpStep } from '@/auth/states/signInUpStepState'; import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCaptchaTokenState'; import { authProvidersState } from '@/client-config/states/authProvidersState'; import { captchaProviderState } from '@/client-config/states/captchaProviderState'; import { Loader } from '@/ui/feedback/loader/components/Loader'; import { MainButton } from '@/ui/input/button/components/MainButton'; import { TextInput } from '@/ui/input/components/TextInput'; -import { ActionLink } from '@/ui/navigation/link/components/ActionLink'; import { isDefined } from '~/utils/isDefined'; -import { SignInUpStep } from '@/auth/states/signInUpStepState'; const StyledContentContainer = styled.div` margin-bottom: ${({ theme }) => theme.spacing(8)}; diff --git a/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx b/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx index b286b4e91d..2b4038b7d7 100644 --- a/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx +++ b/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx @@ -1,4 +1,4 @@ -import { useRecoilValue } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { SettingsNavigationDrawerItems } from '@/settings/components/SettingsNavigationDrawerItems'; @@ -7,12 +7,12 @@ import { NavigationDrawer, NavigationDrawerProps, } from '@/ui/navigation/navigation-drawer/components/NavigationDrawer'; - +import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState'; import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI'; import { useIsSettingsDrawer } from '@/navigation/hooks/useIsSettingsDrawer'; -import { AdvancedSettingsToggle } from '@/ui/navigation/link/components/AdvancedSettingsToggle'; +import { AdvancedSettingsToggle } from 'twenty-ui'; import { MainNavigationDrawerItems } from './MainNavigationDrawerItems'; export type AppNavigationDrawerProps = { @@ -25,12 +25,20 @@ export const AppNavigationDrawer = ({ const isSettingsDrawer = useIsSettingsDrawer(); const currentWorkspace = useRecoilValue(currentWorkspaceState); + const [isAdvancedModeEnabled, setIsAdvancedModeEnabled] = useRecoilState( + isAdvancedModeEnabledState, + ); const drawerProps: NavigationDrawerProps = isSettingsDrawer ? { title: 'Exit Settings', children: , - footer: , + footer: ( + + ), } : { logo: diff --git a/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx index 16b207dbe1..5e073040f4 100644 --- a/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx +++ b/packages/twenty-front/src/modules/object-record/components/RecordChip.tsx @@ -1,9 +1,8 @@ -import { AvatarChip, AvatarChipVariant } from 'twenty-ui'; +import { AvatarChip, AvatarChipVariant, UndecoratedLink } from 'twenty-ui'; import { getLinkToShowPage } from '@/object-metadata/utils/getLinkToShowPage'; import { useRecordChipData } from '@/object-record/hooks/useRecordChipData'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; import { MouseEvent } from 'react'; export type RecordChipProps = { 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 9396d30da0..5f036cc546 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 @@ -9,6 +9,7 @@ import { IconRotate2, IconSettings, IconTag, + UndecoratedLink, } from 'twenty-ui'; import { useObjectNamePluralFromSingular } from '@/object-metadata/hooks/useObjectNamePluralFromSingular'; @@ -30,7 +31,6 @@ import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenu import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; -import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItemNavigate } from '@/ui/navigation/menu-item/components/MenuItemNavigate'; import { MenuItemToggle } from '@/ui/navigation/menu-item/components/MenuItemToggle'; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx index f258a92138..f584354333 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderPlusButtonContent.tsx @@ -1,7 +1,7 @@ import { useCallback, useContext } from 'react'; import { useLocation } from 'react-router-dom'; import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { IconSettings, useIcons } from 'twenty-ui'; +import { IconSettings, UndecoratedLink, useIcons } from 'twenty-ui'; import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; @@ -12,7 +12,6 @@ import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefin import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; -import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelDetails.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelDetails.tsx index f6b2e90393..3152e98ba0 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelDetails.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelDetails.tsx @@ -2,13 +2,12 @@ import { CalendarChannel } from '@/accounts/types/CalendarChannel'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { SettingsAccountsEventVisibilitySettingsCard } from '@/settings/accounts/components/SettingsAccountsCalendarVisibilitySettingsCard'; +import { SettingsOptionCardContent } from '@/settings/components/SettingsOptionCardContent'; +import { Card } from '@/ui/layout/card/components/Card'; import styled from '@emotion/styled'; import { Section } from '@react-email/components'; -import { H2Title } from 'twenty-ui'; +import { H2Title, Toggle } from 'twenty-ui'; import { CalendarChannelVisibility } from '~/generated-metadata/graphql'; -import { Card } from '@/ui/layout/card/components/Card'; -import { SettingsOptionCardContent } from '@/settings/components/SettingsOptionCardContent'; -import { Toggle } from '@/ui/input/components/Toggle'; const StyledDetailsContainer = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelDetails.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelDetails.tsx index c28fa10561..cfe203a15a 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelDetails.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelDetails.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { H2Title } from 'twenty-ui'; +import { H2Title, Toggle } from 'twenty-ui'; import { MessageChannel, @@ -9,11 +9,10 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { SettingsAccountsMessageAutoCreationCard } from '@/settings/accounts/components/SettingsAccountsMessageAutoCreationCard'; import { SettingsAccountsMessageVisibilityCard } from '@/settings/accounts/components/SettingsAccountsMessageVisibilityCard'; +import { SettingsOptionCardContent } from '@/settings/components/SettingsOptionCardContent'; +import { Card } from '@/ui/layout/card/components/Card'; import { Section } from '@/ui/layout/section/components/Section'; import { MessageChannelVisibility } from '~/generated-metadata/graphql'; -import { Card } from '@/ui/layout/card/components/Card'; -import { SettingsOptionCardContent } from '@/settings/components/SettingsOptionCardContent'; -import { Toggle } from '@/ui/input/components/Toggle'; type SettingsAccountsMessageChannelDetailsProps = { messageChannel: Pick< diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsSettingsSection.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsSettingsSection.tsx index 5da069e0b2..5c10785d10 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsSettingsSection.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsSettingsSection.tsx @@ -4,13 +4,13 @@ import { IconCalendarEvent, IconMailCog, MOBILE_VIEWPORT, + UndecoratedLink, } from 'twenty-ui'; import { SettingsCard } from '@/settings/components/SettingsCard'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { Section } from '@/ui/layout/section/components/Section'; -import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; import { useTheme } from '@emotion/react'; const StyledCardsContainer = styled.div` diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldToggle.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldToggle.tsx index e859d652da..5a99420730 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldToggle.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldToggle.tsx @@ -1,4 +1,3 @@ -import { Toggle } from '@/ui/input/components/Toggle'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { createPortal } from 'react-dom'; @@ -6,6 +5,7 @@ import { AppTooltip, IconComponent, IconInfoCircle, + Toggle, TooltipDelay, } from 'twenty-ui'; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsObjectNewFieldSelector.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsObjectNewFieldSelector.tsx index 962fb7aaba..69644d6ab8 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsObjectNewFieldSelector.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsObjectNewFieldSelector.tsx @@ -9,13 +9,12 @@ import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fiel import { useSelectSettingsFormInitialValues } from '@/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues'; import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; import { TextInput } from '@/ui/input/components/TextInput'; -import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { Section } from '@react-email/components'; import { useState } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; -import { H2Title, IconSearch } from 'twenty-ui'; +import { H2Title, IconSearch, UndecoratedLink } from 'twenty-ui'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { SettingsDataModelFieldTypeFormValues } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldSelect'; diff --git a/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationRemoteTableSyncStatusToggle.tsx b/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationRemoteTableSyncStatusToggle.tsx index f2d92a4ea1..88c9084cf8 100644 --- a/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationRemoteTableSyncStatusToggle.tsx +++ b/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationRemoteTableSyncStatusToggle.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; +import { Toggle } from 'twenty-ui'; -import { Toggle } from '@/ui/input/components/Toggle'; import { RemoteTableStatus } from '~/generated-metadata/graphql'; export const SettingsIntegrationRemoteTableSyncStatusToggle = ({ diff --git a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionSummaryCard.tsx b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionSummaryCard.tsx index a29353d223..ccc7dc0abb 100644 --- a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionSummaryCard.tsx +++ b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionSummaryCard.tsx @@ -1,5 +1,10 @@ import styled from '@emotion/styled'; -import { IconDotsVertical, IconPencil, IconTrash } from 'twenty-ui'; +import { + IconDotsVertical, + IconPencil, + IconTrash, + UndecoratedLink, +} from 'twenty-ui'; import { SettingsSummaryCard } from '@/settings/components/SettingsSummaryCard'; import { SettingsIntegrationDatabaseConnectionSyncStatus } from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionSyncStatus'; @@ -7,7 +12,6 @@ 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 { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; type SettingsIntegrationDatabaseConnectionSummaryCardProps = { diff --git a/packages/twenty-front/src/modules/settings/security/components/SettingsSecurityOptionsList.tsx b/packages/twenty-front/src/modules/settings/security/components/SettingsSecurityOptionsList.tsx index 7e4b3b22d6..b2855d93b6 100644 --- a/packages/twenty-front/src/modules/settings/security/components/SettingsSecurityOptionsList.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/SettingsSecurityOptionsList.tsx @@ -1,13 +1,12 @@ -import { IconLink } from 'twenty-ui'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { SettingsOptionCardContent } from '@/settings/components/SettingsOptionCardContent'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; 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'; +import { IconLink, Toggle } from 'twenty-ui'; +import { useUpdateWorkspaceMutation } from '~/generated/graphql'; const StyledToggle = styled(Toggle)` margin-left: auto; diff --git a/packages/twenty-front/src/modules/settings/workspace/components/ToggleImpersonate.tsx b/packages/twenty-front/src/modules/settings/workspace/components/ToggleImpersonate.tsx index 2b700540dd..f38d318b37 100644 --- a/packages/twenty-front/src/modules/settings/workspace/components/ToggleImpersonate.tsx +++ b/packages/twenty-front/src/modules/settings/workspace/components/ToggleImpersonate.tsx @@ -3,7 +3,7 @@ import { useRecoilState } from 'recoil'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { Toggle } from '@/ui/input/components/Toggle'; +import { Toggle } from 'twenty-ui'; import { useUpdateWorkspaceMutation } from '~/generated/graphql'; export const ToggleImpersonate = () => { diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx index 21b6d03492..f74e6dcc74 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx @@ -8,7 +8,7 @@ import { } from 'react'; // @ts-expect-error Todo: remove usage of react-data-grid` import { RowsChangeData } from 'react-data-grid'; -import { IconTrash } from 'twenty-ui'; +import { IconTrash, Toggle } from 'twenty-ui'; import { Heading } from '@/spreadsheet-import/components/Heading'; import { SpreadsheetImportTable } from '@/spreadsheet-import/components/SpreadsheetImportTable'; @@ -25,7 +25,6 @@ import { import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations'; import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager'; import { Button } from '@/ui/input/button/components/Button'; -import { Toggle } from '@/ui/input/components/Toggle'; import { isDefined } from '~/utils/isDefined'; import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep'; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx index f2aa7983f4..de907c23f1 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/components/columns.tsx @@ -2,13 +2,12 @@ import styled from '@emotion/styled'; // @ts-expect-error // Todo: remove usage of react-data-grid import { Column, useRowSelection } from 'react-data-grid'; import { createPortal } from 'react-dom'; -import { AppTooltip } from 'twenty-ui'; +import { AppTooltip, Toggle } from 'twenty-ui'; import { MatchColumnSelect } from '@/spreadsheet-import/components/MatchColumnSelect'; import { Fields, ImportedStructuredRow } from '@/spreadsheet-import/types'; import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox'; import { TextInput } from '@/ui/input/components/TextInput'; -import { Toggle } from '@/ui/input/components/Toggle'; import { isDefined } from '~/utils/isDefined'; import { ImportedStructuredRowMetadata } from '../types'; diff --git a/packages/twenty-front/src/modules/ui/field/display/components/EmailDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/EmailDisplay.tsx index 2fe096c176..75089eaf6f 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/EmailDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/EmailDisplay.tsx @@ -1,8 +1,8 @@ import { MouseEvent } from 'react'; -import { ContactLink } from '@/ui/navigation/link/components/ContactLink'; import { isDefined } from '~/utils/isDefined'; +import { ContactLink } from 'twenty-ui'; import { EllipsisDisplay } from './EllipsisDisplay'; const validateEmail = (email: string) => { diff --git a/packages/twenty-front/src/modules/ui/field/display/components/EmailsDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/EmailsDisplay.tsx index 8b7e67780a..e2737dadce 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/EmailsDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/EmailsDisplay.tsx @@ -1,9 +1,8 @@ import { useMemo } from 'react'; -import { THEME_COMMON } from 'twenty-ui'; +import { RoundedLink, THEME_COMMON } from 'twenty-ui'; import { FieldEmailsValue } from '@/object-record/record-field/types/FieldMetadata'; import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList'; -import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink'; import styled from '@emotion/styled'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/modules/ui/field/display/components/LinkDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/LinkDisplay.tsx index 963887f11e..f5cef6d9d1 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/LinkDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/LinkDisplay.tsx @@ -1,10 +1,5 @@ import { isNonEmptyString } from '@sniptt/guards'; - -import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink'; -import { - LinkType, - SocialLink, -} from '@/ui/navigation/link/components/SocialLink'; +import { LinkType, RoundedLink, SocialLink } from 'twenty-ui'; type LinkDisplayProps = { value?: { url: string; label?: string }; diff --git a/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx index 339859dfe6..8b4b52a63b 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/LinksDisplay.tsx @@ -1,14 +1,9 @@ -import { useMemo } from 'react'; import { styled } from '@linaria/react'; -import { THEME_COMMON } from 'twenty-ui'; +import { useMemo } from 'react'; +import { LinkType, RoundedLink, SocialLink, THEME_COMMON } from 'twenty-ui'; import { FieldLinksValue } from '@/object-record/record-field/types/FieldMetadata'; import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList'; -import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink'; -import { - LinkType, - SocialLink, -} from '@/ui/navigation/link/components/SocialLink'; import { checkUrlType } from '~/utils/checkUrlType'; import { isDefined } from '~/utils/isDefined'; import { getAbsoluteUrl } from '~/utils/url/getAbsoluteUrl'; diff --git a/packages/twenty-front/src/modules/ui/field/display/components/PhoneDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/PhoneDisplay.tsx index 7c79ac85e7..02c518278f 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/PhoneDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/PhoneDisplay.tsx @@ -1,7 +1,7 @@ import { parsePhoneNumber, PhoneNumber } from 'libphonenumber-js'; import { MouseEvent } from 'react'; +import { ContactLink } from 'twenty-ui'; -import { ContactLink } from '@/ui/navigation/link/components/ContactLink'; import { isDefined } from '~/utils/isDefined'; type PhoneDisplayProps = { diff --git a/packages/twenty-front/src/modules/ui/field/display/components/PhonesDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/PhonesDisplay.tsx index deee867fc1..6dbca8829d 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/PhonesDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/PhonesDisplay.tsx @@ -1,10 +1,9 @@ import styled from '@emotion/styled'; import { useMemo } from 'react'; -import { THEME_COMMON } from 'twenty-ui'; +import { RoundedLink, THEME_COMMON } from 'twenty-ui'; import { FieldPhonesValue } from '@/object-record/record-field/types/FieldMetadata'; import { ExpandableList } from '@/ui/layout/expandable-list/components/ExpandableList'; -import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink'; import { parsePhoneNumber } from 'libphonenumber-js'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/modules/ui/field/display/components/URLDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/URLDisplay.tsx index 561b1d0dd3..976ba1c080 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/URLDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/URLDisplay.tsx @@ -1,13 +1,9 @@ -import { MouseEvent } from 'react'; import styled from '@emotion/styled'; +import { MouseEvent } from 'react'; -import { RoundedLink } from '@/ui/navigation/link/components/RoundedLink'; -import { - LinkType, - SocialLink, -} from '@/ui/navigation/link/components/SocialLink'; import { checkUrlType } from '~/utils/checkUrlType'; +import { LinkType, RoundedLink, SocialLink } from 'twenty-ui'; import { EllipsisDisplay } from './EllipsisDisplay'; const StyledRawLink = styled(RoundedLink)` diff --git a/packages/twenty-front/src/modules/ui/input/components/Select.tsx b/packages/twenty-front/src/modules/ui/input/components/Select.tsx index ba29909b50..ec574e083d 100644 --- a/packages/twenty-front/src/modules/ui/input/components/Select.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/Select.tsx @@ -1,6 +1,6 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import React, { MouseEvent, useMemo, useRef, useState } from 'react'; +import { MouseEvent, useMemo, useRef, useState } from 'react'; import { IconChevronDown, IconComponent } from 'twenty-ui'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; @@ -10,8 +10,8 @@ import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownM import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; -import { SelectHotkeyScope } from '../types/SelectHotkeyScope'; import { isDefined } from '~/utils/isDefined'; +import { SelectHotkeyScope } from '../types/SelectHotkeyScope'; export type SelectOption = { value: Value; diff --git a/packages/twenty-front/src/modules/ui/input/components/__stories__/Select.stories.tsx b/packages/twenty-front/src/modules/ui/input/components/__stories__/Select.stories.tsx index f24c7a1c94..5b46bc8cb1 100644 --- a/packages/twenty-front/src/modules/ui/input/components/__stories__/Select.stories.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/__stories__/Select.stories.tsx @@ -1,10 +1,9 @@ -import { useState } from 'react'; import { Meta, StoryObj } from '@storybook/react'; import { userEvent, within } from '@storybook/test'; -import { ComponentDecorator } from 'twenty-ui'; +import { useState } from 'react'; +import { ComponentDecorator, IconPlus } from 'twenty-ui'; import { Select, SelectProps } from '../Select'; -import { IconPlus } from 'packages/twenty-ui'; type RenderProps = SelectProps; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarExpandButton.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarExpandButton.tsx index f3d187a940..a0b96b0501 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarExpandButton.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarExpandButton.tsx @@ -1,7 +1,6 @@ import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; -import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; -import { IconExternalLink } from 'twenty-ui'; +import { IconExternalLink, UndecoratedLink } from 'twenty-ui'; export const RightDrawerTopBarExpandButton = ({ to }: { to: string }) => { const { closeRightDrawer } = useRightDrawer(); diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemToggle.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemToggle.tsx index 2692c65773..256c038e8a 100644 --- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemToggle.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemToggle.tsx @@ -1,6 +1,4 @@ -import { IconComponent } from 'twenty-ui'; - -import { Toggle, ToggleSize } from '@/ui/input/components/Toggle'; +import { IconComponent, Toggle, ToggleSize } from 'twenty-ui'; import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent'; import { diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx index 4860279661..ec0c5bcfe0 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx @@ -1,9 +1,8 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { IconX } from 'twenty-ui'; +import { IconX, UndecoratedLink } from 'twenty-ui'; -import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; import { navigationDrawerExpandedMemorizedState } from '@/ui/navigation/states/navigationDrawerExpandedMemorizedState'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawer.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawer.stories.tsx index 1fe45b7822..93db01f858 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawer.stories.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/__stories__/NavigationDrawer.stories.tsx @@ -1,5 +1,6 @@ import { Meta, StoryObj } from '@storybook/react'; import { + GithubVersionLink, IconAt, IconBell, IconBuildingSkyscraper, @@ -18,18 +19,17 @@ import { import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { GithubVersionLink } from '@/ui/navigation/link/components/GithubVersionLink'; import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { CurrentWorkspaceMemberFavorites } from '@/favorites/components/CurrentWorkspaceMemberFavorites'; import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem'; +import jsonPage from '../../../../../../../package.json'; import { NavigationDrawer } from '../NavigationDrawer'; import { NavigationDrawerItem } from '../NavigationDrawerItem'; import { NavigationDrawerItemGroup } from '../NavigationDrawerItemGroup'; import { NavigationDrawerSection } from '../NavigationDrawerSection'; import { NavigationDrawerSectionTitle } from '../NavigationDrawerSectionTitle'; - const meta: Meta = { title: 'UI/Navigation/NavigationDrawer/NavigationDrawer', component: NavigationDrawer, @@ -148,6 +148,6 @@ export const Settings: Story = { ), - footer: , + footer: , }, }; diff --git a/packages/twenty-front/src/pages/auth/Authorize.tsx b/packages/twenty-front/src/pages/auth/Authorize.tsx index 20be8c5017..b97c31815d 100644 --- a/packages/twenty-front/src/pages/auth/Authorize.tsx +++ b/packages/twenty-front/src/pages/auth/Authorize.tsx @@ -4,7 +4,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom'; import { AppPath } from '@/types/AppPath'; import { MainButton } from '@/ui/input/button/components/MainButton'; -import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; +import { UndecoratedLink } from 'twenty-ui'; import { useAuthorizeAppMutation } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/pages/not-found/NotFound.tsx b/packages/twenty-front/src/pages/not-found/NotFound.tsx index 3283d402b2..3843244ae9 100644 --- a/packages/twenty-front/src/pages/not-found/NotFound.tsx +++ b/packages/twenty-front/src/pages/not-found/NotFound.tsx @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; import { SignInBackgroundMockPage } from '@/sign-in-background-mock/components/SignInBackgroundMockPage'; import { AppPath } from '@/types/AppPath'; import { MainButton } from '@/ui/input/button/components/MainButton'; -import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; + import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle'; import { AnimatedPlaceholder, @@ -11,6 +11,7 @@ import { AnimatedPlaceholderErrorContainer, AnimatedPlaceholderErrorSubTitle, AnimatedPlaceholderErrorTitle, + UndecoratedLink, } from 'twenty-ui'; const StyledBackDrop = styled.div` diff --git a/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx b/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx index 02fc25fbb9..d9e934e57c 100644 --- a/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx +++ b/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx @@ -1,6 +1,6 @@ -import React, { useState } from 'react'; import styled from '@emotion/styled'; import { isNonEmptyString, isNumber } from '@sniptt/guards'; +import { useState } from 'react'; import { useRecoilValue } from 'recoil'; import { SubTitle } from '@/auth/components/SubTitle'; @@ -15,8 +15,7 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { MainButton } from '@/ui/input/button/components/MainButton'; import { CardPicker } from '@/ui/input/components/CardPicker'; -import { ActionLink } from '@/ui/navigation/link/components/ActionLink'; -import { CAL_LINK } from '@/ui/navigation/link/constants/Cal'; +import { ActionLink, CAL_LINK } from 'twenty-ui'; import { ProductPriceEntity, SubscriptionInterval, diff --git a/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx b/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx index ce57c50795..f0e3c02d8f 100644 --- a/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx +++ b/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx @@ -1,15 +1,13 @@ -import React from 'react'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; -import { IconCheck, RGBA } from 'twenty-ui'; +import { IconCheck, RGBA, UndecoratedLink } from 'twenty-ui'; import { SubTitle } from '@/auth/components/SubTitle'; import { Title } from '@/auth/components/Title'; import { currentUserState } from '@/auth/states/currentUserState'; import { AppPath } from '@/types/AppPath'; import { MainButton } from '@/ui/input/button/components/MainButton'; -import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn'; import { OnboardingStatus } from '~/generated/graphql'; diff --git a/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx b/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx index c80eed892c..259f519e4e 100644 --- a/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx +++ b/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx @@ -1,9 +1,9 @@ -import React, { useState } from 'react'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { useState } from 'react'; import { useRecoilValue } from 'recoil'; import { Key } from 'ts-key-enum'; -import { IconGoogle } from 'twenty-ui'; +import { ActionLink, IconGoogle } from 'twenty-ui'; import { SubTitle } from '@/auth/components/SubTitle'; import { Title } from '@/auth/components/Title'; @@ -14,7 +14,6 @@ import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerG import { AppPath } from '@/types/AppPath'; import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { MainButton } from '@/ui/input/button/components/MainButton'; -import { ActionLink } from '@/ui/navigation/link/components/ActionLink'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { CalendarChannelVisibility, diff --git a/packages/twenty-front/src/pages/settings/SettingsWorkspace.tsx b/packages/twenty-front/src/pages/settings/SettingsWorkspace.tsx index f61342a4c5..2dcad9d36b 100644 --- a/packages/twenty-front/src/pages/settings/SettingsWorkspace.tsx +++ b/packages/twenty-front/src/pages/settings/SettingsWorkspace.tsx @@ -1,4 +1,4 @@ -import { H2Title } from 'twenty-ui'; +import { GithubVersionLink, H2Title } from 'twenty-ui'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { DeleteWorkspace } from '@/settings/profile/components/DeleteWorkspace'; @@ -9,8 +9,7 @@ import { WorkspaceLogoUploader } from '@/settings/workspace/components/Workspace import { SettingsPath } from '@/types/SettingsPath'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; -import { GithubVersionLink } from '@/ui/navigation/link/components/GithubVersionLink'; - +import packageJson from '../../../package.json'; export const SettingsWorkspace = () => ( (
- +
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 d973ab8f3c..c40a6baea2 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPageContent.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPageContent.tsx @@ -7,13 +7,12 @@ import { SettingsPath } from '@/types/SettingsPath'; import { Button } from '@/ui/input/button/components/Button'; 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'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import styled from '@emotion/styled'; import { useNavigate } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; -import { H2Title, IconPlus } from 'twenty-ui'; +import { H2Title, IconPlus, UndecoratedLink } from 'twenty-ui'; import { SettingsObjectFieldTable } from '~/pages/settings/data-model/SettingsObjectFieldTable'; import { SettingsObjectIndexTable } from '~/pages/settings/data-model/SettingsObjectIndexTable'; 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 ba5fd0fd60..03b3a60c30 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjects.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjects.tsx @@ -1,6 +1,12 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { H2Title, IconChevronRight, IconPlus, IconSearch } from 'twenty-ui'; +import { + H2Title, + IconChevronRight, + IconPlus, + IconSearch, + UndecoratedLink, +} from 'twenty-ui'; import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; @@ -26,7 +32,6 @@ import { Table } from '@/ui/layout/table/components/Table'; import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableSection } from '@/ui/layout/table/components/TableSection'; import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray'; -import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; import { isNonEmptyArray } from '@sniptt/guards'; import { useMemo, useState } from 'react'; import { SETTINGS_OBJECT_TABLE_METADATA } from '~/pages/settings/data-model/constants/SettingsObjectTableMetadata'; 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 1d240653c9..c694ad6942 100644 --- a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctions.tsx +++ b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctions.tsx @@ -4,8 +4,7 @@ import { SettingsPath } from '@/types/SettingsPath'; import { Button } from '@/ui/input/button/components/Button'; 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'; +import { IconPlus, UndecoratedLink } from 'twenty-ui'; export const SettingsServerlessFunctions = () => { return ( diff --git a/packages/twenty-front/src/utils/__tests__/getDisplayValueByUrlType.test.ts b/packages/twenty-front/src/utils/__tests__/getDisplayValueByUrlType.test.ts index 58f447bec1..aff9792f56 100644 --- a/packages/twenty-front/src/utils/__tests__/getDisplayValueByUrlType.test.ts +++ b/packages/twenty-front/src/utils/__tests__/getDisplayValueByUrlType.test.ts @@ -1,4 +1,4 @@ -import { LinkType } from '@/ui/navigation/link/components/SocialLink'; +import { LinkType } from 'twenty-ui'; import { getDisplayValueByUrlType } from '~/utils/getDisplayValueByUrlType'; describe('getDisplayValueByUrlType', () => { diff --git a/packages/twenty-front/src/utils/checkUrlType.ts b/packages/twenty-front/src/utils/checkUrlType.ts index 13f1499c20..a806478b9b 100644 --- a/packages/twenty-front/src/utils/checkUrlType.ts +++ b/packages/twenty-front/src/utils/checkUrlType.ts @@ -1,5 +1,4 @@ -import { LinkType } from '@/ui/navigation/link/components/SocialLink'; - +import { LinkType } from 'twenty-ui'; import { isDefined } from './isDefined'; export const checkUrlType = (url: string) => { diff --git a/packages/twenty-front/src/utils/getDisplayValueByUrlType.ts b/packages/twenty-front/src/utils/getDisplayValueByUrlType.ts index 5e5087d5c0..cc1accb568 100644 --- a/packages/twenty-front/src/utils/getDisplayValueByUrlType.ts +++ b/packages/twenty-front/src/utils/getDisplayValueByUrlType.ts @@ -1,5 +1,4 @@ -import { LinkType } from '@/ui/navigation/link/components/SocialLink'; - +import { LinkType } from 'twenty-ui'; import { isDefined } from './isDefined'; type getUrlDisplayValueByUrlTypeProps = { diff --git a/packages/twenty-front/tsup.ui.index.tsx b/packages/twenty-front/tsup.ui.index.tsx index 88c2ba996d..01b5aea3d2 100644 --- a/packages/twenty-front/tsup.ui.index.tsx +++ b/packages/twenty-front/tsup.ui.index.tsx @@ -26,13 +26,7 @@ export * from './src/modules/ui/input/components/RadioGroup'; export * from './src/modules/ui/input/components/Select'; export * from './src/modules/ui/input/components/TextArea'; export * from './src/modules/ui/input/components/TextInput'; -export * from './src/modules/ui/input/components/Toggle'; export * from './src/modules/ui/input/editor/components/BlockEditor'; -export * from './src/modules/ui/navigation/link/components/ActionLink'; -export * from './src/modules/ui/navigation/link/components/ContactLink'; -export * from './src/modules/ui/navigation/link/components/RawLink'; -export * from './src/modules/ui/navigation/link/components/RoundedLink'; -export * from './src/modules/ui/navigation/link/components/SocialLink'; export * from './src/modules/ui/navigation/menu-item/components/MenuItem'; export * from './src/modules/ui/navigation/menu-item/components/MenuItemCommand'; export * from './src/modules/ui/navigation/menu-item/components/MenuItemDraggable'; @@ -50,5 +44,3 @@ declare module '@emotion/react' { // eslint-disable-next-line @typescript-eslint/no-empty-interface export interface Theme extends ThemeType {} } - - diff --git a/packages/twenty-ui/src/index.ts b/packages/twenty-ui/src/index.ts index a5a04a16ff..a05da58616 100644 --- a/packages/twenty-ui/src/index.ts +++ b/packages/twenty-ui/src/index.ts @@ -1,6 +1,7 @@ export * from './accessibility'; export * from './components'; export * from './display'; +export * from './input'; export * from './layout'; export * from './navigation'; export * from './testing'; diff --git a/packages/twenty-front/src/modules/ui/input/components/Toggle.tsx b/packages/twenty-ui/src/input/components/Toggle.tsx similarity index 97% rename from packages/twenty-front/src/modules/ui/input/components/Toggle.tsx rename to packages/twenty-ui/src/input/components/Toggle.tsx index f6c28cc2f5..86b9975101 100644 --- a/packages/twenty-front/src/modules/ui/input/components/Toggle.tsx +++ b/packages/twenty-ui/src/input/components/Toggle.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; +import { VisibilityHiddenInput } from '@ui/accessibility'; import { motion } from 'framer-motion'; -import { VisibilityHiddenInput } from 'twenty-ui'; export type ToggleSize = 'small' | 'medium'; diff --git a/packages/twenty-ui/src/input/index.ts b/packages/twenty-ui/src/input/index.ts new file mode 100644 index 0000000000..133f827052 --- /dev/null +++ b/packages/twenty-ui/src/input/index.ts @@ -0,0 +1 @@ +export * from './components/Toggle'; diff --git a/packages/twenty-ui/src/navigation/index.ts b/packages/twenty-ui/src/navigation/index.ts index 94c4c0952d..89cbeb8eb0 100644 --- a/packages/twenty-ui/src/navigation/index.ts +++ b/packages/twenty-ui/src/navigation/index.ts @@ -1 +1,11 @@ export * from './breadcrumb/components/Breadcrumb'; +export * from './link/components/ActionLink'; +export * from './link/components/AdvancedSettingsToggle'; +export * from './link/components/ContactLink'; +export * from './link/components/GithubVersionLink'; +export * from './link/components/RawLink'; +export * from './link/components/RoundedLink'; +export * from './link/components/SocialLink'; +export * from './link/components/UndecoratedLink'; +export * from './link/constants/Cal'; +export * from './link/constants/GithubLink'; diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/ActionLink.tsx b/packages/twenty-ui/src/navigation/link/components/ActionLink.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/navigation/link/components/ActionLink.tsx rename to packages/twenty-ui/src/navigation/link/components/ActionLink.tsx diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/AdvancedSettingsToggle.tsx b/packages/twenty-ui/src/navigation/link/components/AdvancedSettingsToggle.tsx similarity index 77% rename from packages/twenty-front/src/modules/ui/navigation/link/components/AdvancedSettingsToggle.tsx rename to packages/twenty-ui/src/navigation/link/components/AdvancedSettingsToggle.tsx index 9a74cca814..0506455d0c 100644 --- a/packages/twenty-front/src/modules/ui/navigation/link/components/AdvancedSettingsToggle.tsx +++ b/packages/twenty-ui/src/navigation/link/components/AdvancedSettingsToggle.tsx @@ -1,9 +1,8 @@ -import { Toggle } from '@/ui/input/components/Toggle'; -import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState'; import styled from '@emotion/styled'; +import { IconTool } from '@ui/display'; +import { Toggle } from '@ui/input'; +import { MAIN_COLORS } from '@ui/theme'; import { useId } from 'react'; -import { useRecoilState } from 'recoil'; -import { IconTool, MAIN_COLORS } from 'twenty-ui'; const StyledContainer = styled.div` align-items: center; @@ -38,15 +37,19 @@ const StyledIconTool = styled(IconTool)` margin-right: ${({ theme }) => theme.spacing(0.5)}; `; -export const AdvancedSettingsToggle = () => { - const [isAdvancedModeEnabled, setIsAdvancedModeEnabled] = useRecoilState( - isAdvancedModeEnabledState, - ); - const inputId = useId(); +type AdvancedSettingsToggleProps = { + isAdvancedModeEnabled: boolean; + setIsAdvancedModeEnabled: (enabled: boolean) => void; +}; +export const AdvancedSettingsToggle = ({ + isAdvancedModeEnabled, + setIsAdvancedModeEnabled, +}: AdvancedSettingsToggleProps) => { const onChange = (newValue: boolean) => { setIsAdvancedModeEnabled(newValue); }; + const inputId = useId(); return ( diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/ContactLink.tsx b/packages/twenty-ui/src/navigation/link/components/ContactLink.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/navigation/link/components/ContactLink.tsx rename to packages/twenty-ui/src/navigation/link/components/ContactLink.tsx diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/GithubVersionLink.tsx b/packages/twenty-ui/src/navigation/link/components/GithubVersionLink.tsx similarity index 51% rename from packages/twenty-front/src/modules/ui/navigation/link/components/GithubVersionLink.tsx rename to packages/twenty-ui/src/navigation/link/components/GithubVersionLink.tsx index 0d2ed27331..10e4534d6e 100644 --- a/packages/twenty-front/src/modules/ui/navigation/link/components/GithubVersionLink.tsx +++ b/packages/twenty-ui/src/navigation/link/components/GithubVersionLink.tsx @@ -1,18 +1,19 @@ import { useTheme } from '@emotion/react'; -import { IconBrandGithub } from 'twenty-ui'; - -import { ActionLink } from '@/ui/navigation/link/components/ActionLink'; - -import packageJson from '../../../../../../package.json'; +import { IconBrandGithub } from '@ui/display'; +import { ActionLink } from '@ui/navigation/link/components/ActionLink'; import { GITHUB_LINK } from '../constants/GithubLink'; -export const GithubVersionLink = () => { +interface GithubVersionLinkProps { + version: string; +} + +export const GithubVersionLink = ({ version }: GithubVersionLinkProps) => { const theme = useTheme(); return ( - {packageJson.version} + {version} ); }; diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/RawLink.tsx b/packages/twenty-ui/src/navigation/link/components/RawLink.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/navigation/link/components/RawLink.tsx rename to packages/twenty-ui/src/navigation/link/components/RawLink.tsx diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/RoundedLink.tsx b/packages/twenty-ui/src/navigation/link/components/RoundedLink.tsx similarity index 96% rename from packages/twenty-front/src/modules/ui/navigation/link/components/RoundedLink.tsx rename to packages/twenty-ui/src/navigation/link/components/RoundedLink.tsx index 4db0e8dd2c..ef3b05953e 100644 --- a/packages/twenty-front/src/modules/ui/navigation/link/components/RoundedLink.tsx +++ b/packages/twenty-ui/src/navigation/link/components/RoundedLink.tsx @@ -1,7 +1,7 @@ import { styled } from '@linaria/react'; import { isNonEmptyString } from '@sniptt/guards'; +import { FONT_COMMON, THEME_COMMON, ThemeContext } from '@ui/theme'; import { MouseEvent, useContext } from 'react'; -import { FONT_COMMON, THEME_COMMON, ThemeContext } from 'twenty-ui'; type RoundedLinkProps = { href: string; diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/SocialLink.tsx b/packages/twenty-ui/src/navigation/link/components/SocialLink.tsx similarity index 87% rename from packages/twenty-front/src/modules/ui/navigation/link/components/SocialLink.tsx rename to packages/twenty-ui/src/navigation/link/components/SocialLink.tsx index aa98b45691..a5b756e30b 100644 --- a/packages/twenty-front/src/modules/ui/navigation/link/components/SocialLink.tsx +++ b/packages/twenty-ui/src/navigation/link/components/SocialLink.tsx @@ -1,7 +1,6 @@ import * as React from 'react'; -import { getDisplayValueByUrlType } from '~/utils/getDisplayValueByUrlType'; - +import { getDisplayValueByUrlType } from '@ui/utilities'; import { RoundedLink } from './RoundedLink'; export enum LinkType { diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/UndecoratedLink.tsx b/packages/twenty-ui/src/navigation/link/components/UndecoratedLink.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/navigation/link/components/UndecoratedLink.tsx rename to packages/twenty-ui/src/navigation/link/components/UndecoratedLink.tsx diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/ActionLink.stories.tsx b/packages/twenty-ui/src/navigation/link/components/__stories__/ActionLink.stories.tsx similarity index 82% rename from packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/ActionLink.stories.tsx rename to packages/twenty-ui/src/navigation/link/components/__stories__/ActionLink.stories.tsx index bb7dc0de6a..eaf20ade7f 100644 --- a/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/ActionLink.stories.tsx +++ b/packages/twenty-ui/src/navigation/link/components/__stories__/ActionLink.stories.tsx @@ -1,7 +1,6 @@ import { Meta, StoryObj } from '@storybook/react'; -import { ComponentDecorator } from 'twenty-ui'; - -import { ActionLink } from '@/ui/navigation/link/components/ActionLink'; +import { ActionLink } from '@ui/navigation/link/components/ActionLink'; +import { ComponentDecorator } from '@ui/testing'; const meta: Meta = { title: 'UI/navigation/link/ActionLink', diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/ContactLink.stories.tsx b/packages/twenty-ui/src/navigation/link/components/__stories__/ContactLink.stories.tsx similarity index 87% rename from packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/ContactLink.stories.tsx rename to packages/twenty-ui/src/navigation/link/components/__stories__/ContactLink.stories.tsx index f4bcf867b5..4051c93749 100644 --- a/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/ContactLink.stories.tsx +++ b/packages/twenty-ui/src/navigation/link/components/__stories__/ContactLink.stories.tsx @@ -1,8 +1,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { fn } from '@storybook/test'; -import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; - +import { ComponentWithRouterDecorator } from '@ui/testing'; import { ContactLink } from '../ContactLink'; const meta: Meta = { diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/GithubVersionLink.stories.tsx b/packages/twenty-ui/src/navigation/link/components/__stories__/GithubVersionLink.stories.tsx similarity index 79% rename from packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/GithubVersionLink.stories.tsx rename to packages/twenty-ui/src/navigation/link/components/__stories__/GithubVersionLink.stories.tsx index e21fe63249..2e06aea8a6 100644 --- a/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/GithubVersionLink.stories.tsx +++ b/packages/twenty-ui/src/navigation/link/components/__stories__/GithubVersionLink.stories.tsx @@ -1,7 +1,6 @@ import { Meta, StoryObj } from '@storybook/react'; -import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; - +import { ComponentWithRouterDecorator } from '@ui/testing'; import { GithubVersionLink } from '../GithubVersionLink'; const meta: Meta = { diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/RawLink.stories.tsx b/packages/twenty-ui/src/navigation/link/components/__stories__/RawLink.stories.tsx similarity index 89% rename from packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/RawLink.stories.tsx rename to packages/twenty-ui/src/navigation/link/components/__stories__/RawLink.stories.tsx index c2f83757f1..8be070812a 100644 --- a/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/RawLink.stories.tsx +++ b/packages/twenty-ui/src/navigation/link/components/__stories__/RawLink.stories.tsx @@ -1,8 +1,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { expect, fn, userEvent, within } from '@storybook/test'; -import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; - +import { ComponentWithRouterDecorator } from '@ui/testing'; import { RawLink } from '../RawLink'; const meta: Meta = { diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/RoundedLink.stories.tsx b/packages/twenty-ui/src/navigation/link/components/__stories__/RoundedLink.stories.tsx similarity index 89% rename from packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/RoundedLink.stories.tsx rename to packages/twenty-ui/src/navigation/link/components/__stories__/RoundedLink.stories.tsx index 898213169d..5b9107f1a7 100644 --- a/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/RoundedLink.stories.tsx +++ b/packages/twenty-ui/src/navigation/link/components/__stories__/RoundedLink.stories.tsx @@ -1,8 +1,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { expect, fn, userEvent, within } from '@storybook/test'; -import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; - +import { ComponentWithRouterDecorator } from '@ui/testing'; import { RoundedLink } from '../RoundedLink'; const meta: Meta = { diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/SocialLink.stories.tsx b/packages/twenty-ui/src/navigation/link/components/__stories__/SocialLink.stories.tsx similarity index 91% rename from packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/SocialLink.stories.tsx rename to packages/twenty-ui/src/navigation/link/components/__stories__/SocialLink.stories.tsx index 48fc83fd16..0b5880d2ce 100644 --- a/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/SocialLink.stories.tsx +++ b/packages/twenty-ui/src/navigation/link/components/__stories__/SocialLink.stories.tsx @@ -1,8 +1,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { expect, fn, userEvent, within } from '@storybook/test'; -import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; - +import { ComponentWithRouterDecorator } from '@ui/testing'; import { LinkType, SocialLink } from '../SocialLink'; const meta: Meta = { diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/UndecoratedLink.stories.tsx b/packages/twenty-ui/src/navigation/link/components/__stories__/UndecoratedLink.stories.tsx similarity index 79% rename from packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/UndecoratedLink.stories.tsx rename to packages/twenty-ui/src/navigation/link/components/__stories__/UndecoratedLink.stories.tsx index 1765a9f9e3..172cb91545 100644 --- a/packages/twenty-front/src/modules/ui/navigation/link/components/__stories__/UndecoratedLink.stories.tsx +++ b/packages/twenty-ui/src/navigation/link/components/__stories__/UndecoratedLink.stories.tsx @@ -1,9 +1,8 @@ import { expect } from '@storybook/jest'; import { Meta, StoryObj } from '@storybook/react'; import { userEvent, within } from '@storybook/test'; - -import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; -import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; +import { UndecoratedLink } from '@ui/navigation/link/components/UndecoratedLink'; +import { ComponentWithRouterDecorator } from '@ui/testing'; const meta: Meta = { title: 'UI/navigation/link/UndecoratedLink', diff --git a/packages/twenty-ui/src/navigation/link/components/index.ts b/packages/twenty-ui/src/navigation/link/components/index.ts new file mode 100644 index 0000000000..153a89d7af --- /dev/null +++ b/packages/twenty-ui/src/navigation/link/components/index.ts @@ -0,0 +1,8 @@ +export * from './ActionLink'; +export * from './AdvancedSettingsToggle'; +export * from './ContactLink'; +export * from './GithubVersionLink'; +export * from './RawLink'; +export * from './RoundedLink'; +export * from './SocialLink'; +export * from './UndecoratedLink'; diff --git a/packages/twenty-front/src/modules/ui/navigation/link/constants/Cal.ts b/packages/twenty-ui/src/navigation/link/constants/Cal.ts similarity index 100% rename from packages/twenty-front/src/modules/ui/navigation/link/constants/Cal.ts rename to packages/twenty-ui/src/navigation/link/constants/Cal.ts diff --git a/packages/twenty-front/src/modules/ui/navigation/link/constants/GithubLink.ts b/packages/twenty-ui/src/navigation/link/constants/GithubLink.ts similarity index 100% rename from packages/twenty-front/src/modules/ui/navigation/link/constants/GithubLink.ts rename to packages/twenty-ui/src/navigation/link/constants/GithubLink.ts diff --git a/packages/twenty-ui/src/navigation/link/index.ts b/packages/twenty-ui/src/navigation/link/index.ts new file mode 100644 index 0000000000..c46a592e01 --- /dev/null +++ b/packages/twenty-ui/src/navigation/link/index.ts @@ -0,0 +1,3 @@ +export * from './components'; +export * from './constants/Cal'; +export * from './constants/GithubLink'; diff --git a/packages/twenty-ui/src/testing/decorators/ComponentWithRouterDecorator.tsx b/packages/twenty-ui/src/testing/decorators/ComponentWithRouterDecorator.tsx new file mode 100644 index 0000000000..2ebe92443b --- /dev/null +++ b/packages/twenty-ui/src/testing/decorators/ComponentWithRouterDecorator.tsx @@ -0,0 +1,82 @@ +import { Decorator } from '@storybook/react'; +import { + createMemoryRouter, + createRoutesFromElements, + Outlet, + Route, + RouterProvider, +} from 'react-router-dom'; + +import { ComponentStorybookLayout } from '../ComponentStorybookLayout'; + +interface StrictArgs { + [name: string]: unknown; +} +export type RouteParams = { + [param: string]: string; +}; + +export const isRouteParams = (obj: any): obj is RouteParams => { + if (typeof obj !== 'object' || obj === null) { + return false; + } + + return Object.keys(obj).every((key) => typeof obj[key] === 'string'); +}; + +export const computeLocation = ( + routePath: string, + routeParams?: RouteParams, +) => { + return { + pathname: routePath.replace( + /:(\w+)/g, + (paramName) => routeParams?.[paramName] ?? '', + ), + }; +}; + +const Providers = () => ( + + + +); + +const createRouter = ({ + Story, + args, + initialEntries, + initialIndex, +}: { + Story: () => JSX.Element; + args: StrictArgs; + initialEntries?: { + pathname: string; + }[]; + initialIndex?: number; +}) => + createMemoryRouter( + createRoutesFromElements( + }> + } /> + , + ), + { initialEntries, initialIndex }, + ); + +export const ComponentWithRouterDecorator: Decorator = (Story, { args }) => { + return ( + + ); +}; diff --git a/packages/twenty-ui/src/testing/index.ts b/packages/twenty-ui/src/testing/index.ts index 95631bbd7d..dc67f4a8d5 100644 --- a/packages/twenty-ui/src/testing/index.ts +++ b/packages/twenty-ui/src/testing/index.ts @@ -1,6 +1,7 @@ export * from './ComponentStorybookLayout'; export * from './decorators/CatalogDecorator'; export * from './decorators/ComponentDecorator'; +export * from './decorators/ComponentWithRouterDecorator'; export * from './decorators/RouterDecorator'; export * from './mocks/avatarUrlMock'; export * from './types/CatalogStory'; diff --git a/packages/twenty-ui/src/utilities/getDisplayValueByUrlType.ts b/packages/twenty-ui/src/utilities/getDisplayValueByUrlType.ts new file mode 100644 index 0000000000..2325d96c38 --- /dev/null +++ b/packages/twenty-ui/src/utilities/getDisplayValueByUrlType.ts @@ -0,0 +1,34 @@ +import { LinkType } from '@ui/navigation/link'; +import { isDefined } from './isDefined'; + +type getUrlDisplayValueByUrlTypeProps = { + type: LinkType; + href: string; +}; + +export const getDisplayValueByUrlType = ({ + type, + href, +}: getUrlDisplayValueByUrlTypeProps) => { + if (type === 'linkedin') { + const matches = href.match( + /(?:https?:\/\/)?(?:www.)?linkedin.com\/(?:in|company|school)\/(.*)/, + ); + if (isDefined(matches?.[1])) { + return decodeURIComponent(matches?.[1]); + } else { + return 'LinkedIn'; + } + } + + if (type === 'twitter') { + const matches = href.match( + /(?:https?:\/\/)?(?:www.)?twitter.com\/([-a-zA-Z0-9@:%_+.~#?&//=]*)/, + ); + if (isDefined(matches?.[1])) { + return `@${matches?.[1]}`; + } else { + return '@twitter'; + } + } +}; diff --git a/packages/twenty-ui/src/utilities/index.ts b/packages/twenty-ui/src/utilities/index.ts index f6f4d3659c..1cd3b91094 100644 --- a/packages/twenty-ui/src/utilities/index.ts +++ b/packages/twenty-ui/src/utilities/index.ts @@ -1,6 +1,7 @@ export * from './color/utils/stringToHslColor'; +export * from './getDisplayValueByUrlType'; export * from './image/getImageAbsoluteURI'; export * from './isDefined'; +export * from './screen-size/hooks/useScreenSize'; export * from './state/utils/createState'; export * from './types/Nullable'; -export * from './screen-size/hooks/useScreenSize'; diff --git a/packages/twenty-website/src/content/twenty-ui/input/toggle.mdx b/packages/twenty-website/src/content/twenty-ui/input/toggle.mdx index cd78b7b58e..b866369439 100644 --- a/packages/twenty-website/src/content/twenty-ui/input/toggle.mdx +++ b/packages/twenty-website/src/content/twenty-ui/input/toggle.mdx @@ -7,7 +7,7 @@ image: /images/user-guide/table-views/table.png - { return ( diff --git a/packages/twenty-website/src/content/twenty-ui/navigation/links.mdx b/packages/twenty-website/src/content/twenty-ui/navigation/links.mdx index 2abf0babf8..dce088149e 100644 --- a/packages/twenty-website/src/content/twenty-ui/navigation/links.mdx +++ b/packages/twenty-website/src/content/twenty-ui/navigation/links.mdx @@ -13,7 +13,7 @@ A stylized link component for displaying contact information. { const handleLinkClick = (event) => { @@ -56,7 +56,7 @@ A stylized link component for displaying links. - { @@ -97,7 +97,7 @@ A rounded-styled link with a Chip component for links. - { @@ -134,7 +134,7 @@ Stylized social links, with support for various social link types, such as URLs, - { From 29bd4e5f2d890517f09c60d4e154aa9bafbcd833 Mon Sep 17 00:00:00 2001 From: Ana Sofia Marin Alexandre <61988046+anamarn@users.noreply.github.com> Date: Tue, 22 Oct 2024 12:51:51 -0300 Subject: [PATCH 088/123] track serverless functions executions (#7963) Solves: https://github.com/twentyhq/private-issues/issues/74 **TLDR** When a serverless function is executed, the result is send to tinybird event data source. **In order to test:** 1. Set ANALYTICS_ENABLED to true 2. Put your TINYBIRD_INGEST_TOKEN from twenty_event_playground in your .env file 3. Don't forget to run the worker 4. Create your serverless function and run it 5. The event should be logged on the event datasource in twenty_event_playground **What is the structure of the payload of a serverless function event?** Here are two examples of the payload: `{"duration":37,"status":"SUCCESS","functionId":"a9fd87c0-af86-4e17-be3a-a6d3d961678a","functionName":"testingFunction"}` ` {"duration":34,"status":"ERROR","errorType":"ReferenceError","functionId":"a9fd87c0-af86-4e17-be3a-a6d3d961678a","functionName":"testingFunction"}` **Possible improvments** - Change the status(str) to success(bool) - Enrich data in the payload --- .../serverless-function.module.ts | 2 ++ .../serverless-function.service.ts | 28 ++++++++++++++++++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts index df5ef68aaa..d289f9c086 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/serverless-function.module.ts @@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; +import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; @@ -18,6 +19,7 @@ import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverles TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), FileModule, ThrottlerModule, + AnalyticsModule, ], providers: [ServerlessFunctionService, ServerlessFunctionResolver], exports: [ServerlessFunctionService], 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 2e69781ac0..aa5ef6279c 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 @@ -9,6 +9,7 @@ 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 { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.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'; @@ -41,6 +42,7 @@ export class ServerlessFunctionService { private readonly serverlessFunctionRepository: Repository, private readonly throttlerService: ThrottlerService, private readonly environmentService: EnvironmentService, + private readonly analyticsService: AnalyticsService, ) {} async findManyServerlessFunctions(where) { @@ -115,7 +117,31 @@ export class ServerlessFunctionService { ); } - return this.serverlessService.execute(functionToExecute, payload, version); + const resultServerlessFunction = await this.serverlessService.execute( + functionToExecute, + payload, + version, + ); + const eventInput = { + action: 'serverlessFunction.executed', + payload: { + duration: resultServerlessFunction.duration, + status: resultServerlessFunction.status, + ...(resultServerlessFunction.error && { + errorType: resultServerlessFunction.error.errorType, + }), + functionId: functionToExecute.id, + functionName: functionToExecute.name, + }, + }; + + this.analyticsService.create( + eventInput, + 'serverless-function', + workspaceId, + ); + + return resultServerlessFunction; } async publishOneServerlessFunction(id: string, workspaceId: string) { From 6dd0ebe0874629a9fcabd656c51add285e749cbe Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Tue, 22 Oct 2024 17:52:15 +0200 Subject: [PATCH 089/123] Add skip option at invite team step (#7960) Closing [#5925](https://github.com/twentyhq/twenty/issues/5925) --- .../record-board/components/RecordBoardHeader.tsx | 2 +- .../src/pages/onboarding/InviteTeam.tsx | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx index 59e1acf479..7c591b3bf1 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx @@ -27,7 +27,7 @@ export const RecordBoardHeader = () => { return ( {columnIds.map((columnId) => ( - + ))} ); diff --git a/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx b/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx index 63cd583ec0..33c4825780 100644 --- a/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx +++ b/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx @@ -25,6 +25,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { LightButton } from '@/ui/input/button/components/LightButton'; import { MainButton } from '@/ui/input/button/components/MainButton'; import { TextInputV2 } from '@/ui/input/components/TextInputV2'; +import { ActionLink } from '@/ui/navigation/link/components/ActionLink'; import { AnimatedTranslation } from '@/ui/utilities/animation/components/AnimatedTranslation'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { OnboardingStatus } from '~/generated/graphql'; @@ -52,6 +53,10 @@ const StyledButtonContainer = styled.div` width: 200px; `; +const StyledActionSkipLinkContainer = styled.div` + margin: ${({ theme }) => theme.spacing(3)} 0 0; +`; + const validationSchema = z.object({ emails: z.array( z.object({ email: z.union([z.literal(''), z.string().email()]) }), @@ -150,6 +155,10 @@ export const InviteTeam = () => { [enqueueSnackBar, sendInvitation, setNextOnboardingStatus], ); + const handleSkip = async () => { + await onSubmit({ emails: [] }); + }; + useScopedHotkeys( [Key.Enter], () => { @@ -170,7 +179,7 @@ export const InviteTeam = () => { Get the most out of your workspace by inviting your team. - {fields.map((field, index) => ( + {fields.map((_field, index) => ( { fullWidth /> + + Skip + ); }; From 7a6926ea5a2c44454b66dc0752acfb9016e67d64 Mon Sep 17 00:00:00 2001 From: ZiaCodes <72739794+Khaan25@users.noreply.github.com> Date: Tue, 22 Oct 2024 20:55:04 +0500 Subject: [PATCH 090/123] feat: Design a new logo for Twenty (300 points) (#7965) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR fixes #7936 ![image](https://github.com/user-attachments/assets/083a8475-b2f4-4cb4-9c5b-509d3823c66b) Co-authored-by: Félix Malfait --- oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 ab02480c9a..1e50e3842f 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 @@ -1,5 +1,5 @@ **Side Quest**: Design/Create new Twenty logo, tweet your design, and mention @twentycrm. -**Points**: 300 Points +**Points**: 50 Points **Proof**: Create a logo upload it on any of the platform and add your oss handle and logo link to the list below. Please follow the following schema: @@ -20,6 +20,8 @@ Your turn 👇 » 11-October-2024 by [thefool76](https://oss.gg/thefool76) Logo Link: [logo](https://drive.google.com/file/d/1DxSwNY_i90kGgWzPQj5SxScBz_6r02l4/view?usp=sharing) » tweet Link: [tweet](https://x.com/thefool1135/status/1844693487067034008) +» 22-October-2024 by [Khaan25](https://oss.gg/Khaan25) Logo Link: [logo](https://drive.google.com/drive/folders/1yaegQ7Hr8YraMNs50AHZmDprvzLn6A90?usp=sharing) » tweet Link: [tweet](https://x.com/zia_webdev/status/1848754055717212388) + » 13-October-2024 by [Atharva_404](https://oss.gg/Atharva-3000) Logo Link: [logo](https://drive.google.com/drive/folders/1XB7ELR7kPA4x7Fx5RQr8wo5etdZAZgcs?usp=drive_link) » tweet Link: [tweet](https://x.com/0x_atharva/status/1845421218914095453) » 13-October-2024 by [Ionfinisher](https://oss.gg/Ionfinisher) Logo Link: [logo](https://drive.google.com/file/d/1l9vE8CIjW9KfdioI5WKzxrdmvO8LR4j7/view?usp=drive_link) » tweet Link: [tweet](https://x.com/ion_finisher/status/1845466470429442163) From 32cf88fa8dd4dbd3b2509a0e01608a9aaefd3f6a Mon Sep 17 00:00:00 2001 From: sateshcharan Date: Tue, 22 Oct 2024 21:28:16 +0530 Subject: [PATCH 091/123] fix: [oss.gg] solves #7920 (#7962) ![image](https://github.com/user-attachments/assets/bb813194-30e9-4f1c-8633-5b7a3dca87b1) #7920 --- .../3-create-custom-interfact-theme-20.md | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/oss-gg/twenty-design-challenges/3-create-custom-interfact-theme-20.md b/oss-gg/twenty-design-challenges/3-create-custom-interfact-theme-20.md index e51945ea99..126a1972aa 100644 --- a/oss-gg/twenty-design-challenges/3-create-custom-interfact-theme-20.md +++ b/oss-gg/twenty-design-challenges/3-create-custom-interfact-theme-20.md @@ -1,12 +1,13 @@ -**Side Quest**: Duplicate the Figma file from the main repo and customize the variables to create a unique interface theme for Twenty. -**Points**: 750 Points -**Proof**: Add your oss handle and Figma link to the list below. +**Side Quest**: Duplicate the Figma file from the main repo and customize the variables to create a unique interface theme for Twenty.
+**Points**: 750 Points
+**Proof**: Add your oss handle and Figma link to the list below.
+**Figma Link**: https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?t=YIFyswta6Xf6sSYK-0 Please follow the following schema: --- -» 05-April-2024 by YOUR oss.gg HANDLE » Figma Link: https://link.to/content +» 05-April-2024 by YOUR oss.gg HANDLE] Figma Link: https://www.figma.com/design/xt8O9mFeLl46C5InWwoMrN/Twenty?t=YIFyswta6Xf6sSYK-0 --- @@ -18,4 +19,4 @@ Your turn 👇 » 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) Figma Link: [Figma](https://twenty.com/) ---- \ No newline at end of file +--- From edc36c707d9aba9f9926084e391e0da16b6bcff7 Mon Sep 17 00:00:00 2001 From: NitinPSingh <71833171+NitinPSingh@users.noreply.github.com> Date: Tue, 22 Oct 2024 21:32:00 +0530 Subject: [PATCH 092/123] fix #7821 added gap in record tag and count (#7822) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit solves #7821 added 4px gap in tag and count --------- Co-authored-by: Félix Malfait --- .../record-board-column/components/RecordBoardColumnHeader.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx index 891b12c846..a36d73cd2f 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx @@ -52,6 +52,7 @@ const StyledHeaderContainer = styled.div` const StyledLeftContainer = styled.div` align-items: center; display: flex; + gap: ${({ theme }) => theme.spacing(1)}; `; const StyledRightContainer = styled.div` From 113e9fc8c731fffb23ca630fe14c3b6a6015676a Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Tue, 22 Oct 2024 18:08:54 +0200 Subject: [PATCH 093/123] Migrate to twenty-ui - utilities/animation (#7951) This PR was created by [GitStart](https://gitstart.com/) to address the requirements from this ticket: [TWNTY-7538](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-7538). --- ### Description - Move animation components to `twenty-ui` \ \ \ Fixes #7538 Co-authored-by: gitstart-twenty Co-authored-by: Charles Bochet --- .../emails/components/EmailThreadMessageBody.tsx | 4 +--- .../twenty-front/src/modules/auth/components/Title.tsx | 5 ++--- .../record-board-card/components/RecordBoardCard.tsx | 8 ++++++-- .../components/RecordInlineCellEditButton.tsx | 3 +-- .../components/RecordDetailRelationRecordsListItem.tsx | 2 +- .../components/RecordTableCellButton.tsx | 3 +-- .../layout/expandable-list/components/ExpandableList.tsx | 3 +-- packages/twenty-front/src/pages/auth/Invite.tsx | 6 +++--- packages/twenty-front/src/pages/auth/PasswordReset.tsx | 2 +- packages/twenty-front/src/pages/auth/SignInUp.tsx | 2 +- packages/twenty-front/src/pages/onboarding/InviteTeam.tsx | 4 +--- .../twenty-front/src/pages/onboarding/PaymentSuccess.tsx | 3 +-- .../utilities/animation/components/AnimatedContainer.tsx | 0 .../utilities/animation/components/AnimatedEaseIn.tsx | 2 +- .../utilities/animation/components/AnimatedEaseInOut.tsx | 2 +- .../utilities/animation/components/AnimatedFadeOut.tsx | 2 +- .../utilities/animation/components/AnimatedTextWord.tsx | 0 .../animation/components/AnimatedTranslation.tsx | 0 packages/twenty-ui/src/utilities/animation/index.ts | 6 ++++++ packages/twenty-ui/src/utilities/index.ts | 6 ++++++ 20 files changed, 35 insertions(+), 28 deletions(-) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/utilities/animation/components/AnimatedContainer.tsx (100%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/utilities/animation/components/AnimatedEaseIn.tsx (93%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/utilities/animation/components/AnimatedEaseInOut.tsx (95%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/utilities/animation/components/AnimatedFadeOut.tsx (95%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/utilities/animation/components/AnimatedTextWord.tsx (100%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/utilities/animation/components/AnimatedTranslation.tsx (100%) create mode 100644 packages/twenty-ui/src/utilities/animation/index.ts diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadMessageBody.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadMessageBody.tsx index 128197c609..b31fa54fe2 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadMessageBody.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadMessageBody.tsx @@ -1,8 +1,6 @@ -import React from 'react'; import styled from '@emotion/styled'; import { motion } from 'framer-motion'; - -import { AnimatedEaseInOut } from '@/ui/utilities/animation/components/AnimatedEaseInOut'; +import { AnimatedEaseInOut } from 'twenty-ui'; const StyledThreadMessageBody = styled(motion.div)` color: ${({ theme }) => theme.font.color.primary}; diff --git a/packages/twenty-front/src/modules/auth/components/Title.tsx b/packages/twenty-front/src/modules/auth/components/Title.tsx index d0a496b252..7f87362b1d 100644 --- a/packages/twenty-front/src/modules/auth/components/Title.tsx +++ b/packages/twenty-front/src/modules/auth/components/Title.tsx @@ -1,7 +1,6 @@ -import React from 'react'; import styled from '@emotion/styled'; - -import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn'; +import React from 'react'; +import { AnimatedEaseIn } from 'twenty-ui'; type TitleProps = React.PropsWithChildren & { animate?: boolean; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx index a2ea5b03de..2fdffd2598 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx @@ -20,7 +20,6 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox'; import { TextInput } from '@/ui/input/components/TextInput'; -import { AnimatedEaseInOut } from '@/ui/utilities/animation/components/AnimatedEaseInOut'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; import { RecordBoardScrollWrapperContext } from '@/ui/utilities/scroll/contexts/ScrollWrapperContexts'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; @@ -28,7 +27,12 @@ import styled from '@emotion/styled'; import { ReactNode, useContext, useState } from 'react'; import { useInView } from 'react-intersection-observer'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; -import { AvatarChipVariant, IconEye, IconEyeOff } from 'twenty-ui'; +import { + AnimatedEaseInOut, + AvatarChipVariant, + IconEye, + IconEyeOff, +} from 'twenty-ui'; import { useDebouncedCallback } from 'use-debounce'; import { useAddNewCard } from '../../record-board-column/hooks/useAddNewCard'; diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx index 2af1e91d7c..c14742e8e0 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx @@ -1,8 +1,7 @@ import styled from '@emotion/styled'; -import { IconComponent } from 'twenty-ui'; +import { AnimatedContainer, IconComponent } from 'twenty-ui'; import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; -import { AnimatedContainer } from '@/ui/utilities/animation/components/AnimatedContainer'; const StyledInlineCellButtonContainer = styled.div` align-items: center; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx index eefeb35408..1ed6c37fa4 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx @@ -3,6 +3,7 @@ import styled from '@emotion/styled'; import { motion } from 'framer-motion'; import { useCallback, useContext } from 'react'; import { + AnimatedEaseInOut, IconChevronDown, IconComponent, IconDotsVertical, @@ -38,7 +39,6 @@ import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/Drop import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; -import { AnimatedEaseInOut } from '@/ui/utilities/animation/components/AnimatedEaseInOut'; import { RelationDefinitionType } from '~/generated-metadata/graphql'; const StyledListItem = styled(RecordDetailRecordsListItem)<{ diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx index 25b0e10f63..957aa74843 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx @@ -1,8 +1,7 @@ import styled from '@emotion/styled'; -import { IconComponent } from 'twenty-ui'; +import { AnimatedContainer, IconComponent } from 'twenty-ui'; import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; -import { AnimatedContainer } from '@/ui/utilities/animation/components/AnimatedContainer'; const StyledButtonContainer = styled.div` margin: ${({ theme }) => theme.spacing(1)}; diff --git a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx index 9f669598b6..db901cab95 100644 --- a/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx +++ b/packages/twenty-front/src/modules/ui/layout/expandable-list/components/ExpandableList.tsx @@ -1,10 +1,9 @@ import styled from '@emotion/styled'; import { ReactElement, useCallback, useEffect, useRef, useState } from 'react'; -import { Chip, ChipVariant } from 'twenty-ui'; +import { AnimatedContainer, Chip, ChipVariant } from 'twenty-ui'; import { ExpandedListDropdown } from '@/ui/layout/expandable-list/components/ExpandedListDropdown'; import { isFirstOverflowingChildElement } from '@/ui/layout/expandable-list/utils/isFirstOverflowingChildElement'; -import { AnimatedContainer } from '@/ui/utilities/animation/components/AnimatedContainer'; import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useListenClickOutside'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/pages/auth/Invite.tsx b/packages/twenty-front/src/pages/auth/Invite.tsx index f14b7ea751..0afef7d320 100644 --- a/packages/twenty-front/src/pages/auth/Invite.tsx +++ b/packages/twenty-front/src/pages/auth/Invite.tsx @@ -12,13 +12,13 @@ import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { Loader } from '@/ui/feedback/loader/components/Loader'; import { MainButton } from '@/ui/input/button/components/MainButton'; import { useWorkspaceSwitching } from '@/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching'; -import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn'; +import { useSearchParams } from 'react-router-dom'; +import { AnimatedEaseIn } from 'twenty-ui'; import { - useAddUserToWorkspaceMutation, useAddUserToWorkspaceByInviteTokenMutation, + useAddUserToWorkspaceMutation, } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; -import { useSearchParams } from 'react-router-dom'; const StyledContentContainer = styled.div` margin-bottom: ${({ theme }) => theme.spacing(8)}; diff --git a/packages/twenty-front/src/pages/auth/PasswordReset.tsx b/packages/twenty-front/src/pages/auth/PasswordReset.tsx index 11cf5d016b..137810855e 100644 --- a/packages/twenty-front/src/pages/auth/PasswordReset.tsx +++ b/packages/twenty-front/src/pages/auth/PasswordReset.tsx @@ -23,7 +23,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { MainButton } from '@/ui/input/button/components/MainButton'; import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { isDefaultLayoutAuthModalVisibleState } from '@/ui/layout/states/isDefaultLayoutAuthModalVisibleState'; -import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn'; +import { AnimatedEaseIn } from 'twenty-ui'; import { useUpdatePasswordViaResetTokenMutation, useValidatePasswordResetTokenQuery, diff --git a/packages/twenty-front/src/pages/auth/SignInUp.tsx b/packages/twenty-front/src/pages/auth/SignInUp.tsx index d9eefac9ed..2608ceaaac 100644 --- a/packages/twenty-front/src/pages/auth/SignInUp.tsx +++ b/packages/twenty-front/src/pages/auth/SignInUp.tsx @@ -7,7 +7,7 @@ import { SignInUpForm } from '@/auth/sign-in-up/components/SignInUpForm'; 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 { AnimatedEaseIn } from 'twenty-ui'; import { isDefined } from '~/utils/isDefined'; import { SignInUpStep } from '@/auth/states/signInUpStepState'; import { IconLockCustom } from '@ui/display/icon/components/IconLock'; diff --git a/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx b/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx index 33c4825780..3b2a3ee94b 100644 --- a/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx +++ b/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx @@ -10,7 +10,7 @@ import { } from 'react-hook-form'; import { useRecoilValue } from 'recoil'; import { Key } from 'ts-key-enum'; -import { IconCopy } from 'twenty-ui'; +import { ActionLink, AnimatedTranslation, IconCopy } from 'twenty-ui'; import { z } from 'zod'; import { SubTitle } from '@/auth/components/SubTitle'; @@ -25,8 +25,6 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { LightButton } from '@/ui/input/button/components/LightButton'; import { MainButton } from '@/ui/input/button/components/MainButton'; import { TextInputV2 } from '@/ui/input/components/TextInputV2'; -import { ActionLink } from '@/ui/navigation/link/components/ActionLink'; -import { AnimatedTranslation } from '@/ui/utilities/animation/components/AnimatedTranslation'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { OnboardingStatus } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx b/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx index f0e3c02d8f..e2fb35efcd 100644 --- a/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx +++ b/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx @@ -1,14 +1,13 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useRecoilValue } from 'recoil'; -import { IconCheck, RGBA, UndecoratedLink } from 'twenty-ui'; +import { AnimatedEaseIn, IconCheck, RGBA, UndecoratedLink } from 'twenty-ui'; import { SubTitle } from '@/auth/components/SubTitle'; import { Title } from '@/auth/components/Title'; import { currentUserState } from '@/auth/states/currentUserState'; import { AppPath } from '@/types/AppPath'; import { MainButton } from '@/ui/input/button/components/MainButton'; -import { AnimatedEaseIn } from '@/ui/utilities/animation/components/AnimatedEaseIn'; import { OnboardingStatus } from '~/generated/graphql'; const StyledCheckContainer = styled.div` diff --git a/packages/twenty-front/src/modules/ui/utilities/animation/components/AnimatedContainer.tsx b/packages/twenty-ui/src/utilities/animation/components/AnimatedContainer.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/utilities/animation/components/AnimatedContainer.tsx rename to packages/twenty-ui/src/utilities/animation/components/AnimatedContainer.tsx diff --git a/packages/twenty-front/src/modules/ui/utilities/animation/components/AnimatedEaseIn.tsx b/packages/twenty-ui/src/utilities/animation/components/AnimatedEaseIn.tsx similarity index 93% rename from packages/twenty-front/src/modules/ui/utilities/animation/components/AnimatedEaseIn.tsx rename to packages/twenty-ui/src/utilities/animation/components/AnimatedEaseIn.tsx index e3a25bf605..1c58cdd6de 100644 --- a/packages/twenty-front/src/modules/ui/utilities/animation/components/AnimatedEaseIn.tsx +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedEaseIn.tsx @@ -1,6 +1,6 @@ import { useTheme } from '@emotion/react'; +import { AnimationDuration } from '@ui/theme'; import { motion } from 'framer-motion'; -import { AnimationDuration } from 'twenty-ui'; type AnimatedEaseInProps = Omit< React.ComponentProps, diff --git a/packages/twenty-front/src/modules/ui/utilities/animation/components/AnimatedEaseInOut.tsx b/packages/twenty-ui/src/utilities/animation/components/AnimatedEaseInOut.tsx similarity index 95% rename from packages/twenty-front/src/modules/ui/utilities/animation/components/AnimatedEaseInOut.tsx rename to packages/twenty-ui/src/utilities/animation/components/AnimatedEaseInOut.tsx index 8411a4fb31..0020e1cf1d 100644 --- a/packages/twenty-front/src/modules/ui/utilities/animation/components/AnimatedEaseInOut.tsx +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedEaseInOut.tsx @@ -1,6 +1,6 @@ import { useTheme } from '@emotion/react'; +import { AnimationDuration } from '@ui/theme'; import { AnimatePresence, motion } from 'framer-motion'; -import { AnimationDuration } from 'twenty-ui'; type AnimatedEaseInOutProps = { isOpen: boolean; diff --git a/packages/twenty-front/src/modules/ui/utilities/animation/components/AnimatedFadeOut.tsx b/packages/twenty-ui/src/utilities/animation/components/AnimatedFadeOut.tsx similarity index 95% rename from packages/twenty-front/src/modules/ui/utilities/animation/components/AnimatedFadeOut.tsx rename to packages/twenty-ui/src/utilities/animation/components/AnimatedFadeOut.tsx index 7dd5f4a4b1..0f2d2620ee 100644 --- a/packages/twenty-front/src/modules/ui/utilities/animation/components/AnimatedFadeOut.tsx +++ b/packages/twenty-ui/src/utilities/animation/components/AnimatedFadeOut.tsx @@ -1,6 +1,6 @@ import { useTheme } from '@emotion/react'; +import { AnimationDuration } from '@ui/theme'; import { AnimatePresence, motion } from 'framer-motion'; -import { AnimationDuration } from 'twenty-ui'; type AnimatedFadeOutProps = { isOpen: boolean; diff --git a/packages/twenty-front/src/modules/ui/utilities/animation/components/AnimatedTextWord.tsx b/packages/twenty-ui/src/utilities/animation/components/AnimatedTextWord.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/utilities/animation/components/AnimatedTextWord.tsx rename to packages/twenty-ui/src/utilities/animation/components/AnimatedTextWord.tsx diff --git a/packages/twenty-front/src/modules/ui/utilities/animation/components/AnimatedTranslation.tsx b/packages/twenty-ui/src/utilities/animation/components/AnimatedTranslation.tsx similarity index 100% rename from packages/twenty-front/src/modules/ui/utilities/animation/components/AnimatedTranslation.tsx rename to packages/twenty-ui/src/utilities/animation/components/AnimatedTranslation.tsx diff --git a/packages/twenty-ui/src/utilities/animation/index.ts b/packages/twenty-ui/src/utilities/animation/index.ts new file mode 100644 index 0000000000..fdde953664 --- /dev/null +++ b/packages/twenty-ui/src/utilities/animation/index.ts @@ -0,0 +1,6 @@ +export * from './components/AnimatedContainer'; +export * from './components/AnimatedEaseIn'; +export * from './components/AnimatedEaseInOut'; +export * from './components/AnimatedFadeOut'; +export * from './components/AnimatedTextWord'; +export * from './components/AnimatedTranslation'; diff --git a/packages/twenty-ui/src/utilities/index.ts b/packages/twenty-ui/src/utilities/index.ts index 1cd3b91094..0def2683ee 100644 --- a/packages/twenty-ui/src/utilities/index.ts +++ b/packages/twenty-ui/src/utilities/index.ts @@ -1,3 +1,9 @@ +export * from './animation/components/AnimatedContainer'; +export * from './animation/components/AnimatedEaseIn'; +export * from './animation/components/AnimatedEaseInOut'; +export * from './animation/components/AnimatedFadeOut'; +export * from './animation/components/AnimatedTextWord'; +export * from './animation/components/AnimatedTranslation'; export * from './color/utils/stringToHslColor'; export * from './getDisplayValueByUrlType'; export * from './image/getImageAbsoluteURI'; From 6c93587efb652fb4f8fc3a4234f98f7e289070f2 Mon Sep 17 00:00:00 2001 From: Weiko Date: Tue, 22 Oct 2024 18:21:00 +0200 Subject: [PATCH 094/123] Fix cache storage (#7966) --- .../cache-storage/cache-storage.module-factory.ts | 12 +++++++++--- .../cache-storage/cache-storage.module.ts | 3 +-- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module-factory.ts b/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module-factory.ts index 3a7928e48a..669b0b523b 100644 --- a/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module-factory.ts +++ b/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module-factory.ts @@ -4,11 +4,9 @@ import { redisStore } from 'cache-manager-redis-yet'; import { CacheStorageType } from 'src/engine/core-modules/cache-storage/types/cache-storage-type.enum'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; -import { RedisClientService } from 'src/engine/core-modules/redis-client/redis-client.service'; export const cacheStorageModuleFactory = ( environmentService: EnvironmentService, - redisClientService: RedisClientService, ): CacheModuleOptions => { const cacheStorageType = environmentService.get('CACHE_STORAGE_TYPE'); const cacheStorageTtl = environmentService.get('CACHE_STORAGE_TTL'); @@ -22,10 +20,18 @@ export const cacheStorageModuleFactory = ( return cacheModuleOptions; } case CacheStorageType.Redis: { + const redisUrl = environmentService.get('REDIS_URL'); + + if (!redisUrl) { + throw new Error( + `${cacheStorageType} cache storage requires REDIS_URL to be defined, check your .env file`, + ); + } + return { ...cacheModuleOptions, store: redisStore, - client: redisClientService.getClient(), + url: redisUrl, }; } default: diff --git a/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module.ts b/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module.ts index dbefb42845..3f12d09e6d 100644 --- a/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module.ts +++ b/packages/twenty-server/src/engine/core-modules/cache-storage/cache-storage.module.ts @@ -7,7 +7,6 @@ import { FlushCacheCommand } from 'src/engine/core-modules/cache-storage/command 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 { RedisClientService } from 'src/engine/core-modules/redis-client/redis-client.service'; @Global() @Module({ @@ -16,7 +15,7 @@ import { RedisClientService } from 'src/engine/core-modules/redis-client/redis-c isGlobal: true, imports: [ConfigModule], useFactory: cacheStorageModuleFactory, - inject: [EnvironmentService, RedisClientService], + inject: [EnvironmentService], }), ], providers: [ From 6843a642b595573a5dfc118a0e907bbcddc8ae9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rapha=C3=ABl=20Bosi?= <71827178+bosiraphael@users.noreply.github.com> Date: Tue, 22 Oct 2024 18:35:45 +0200 Subject: [PATCH 095/123] 7499 refactor right drawer to have contextual actions (#7954) Closes #7499 - Modifies context store states to be component states - Introduces the concept of `mainContextStore` which will dictate the available actions inside the command K - Adds contextual actions inside the right drawer - Creates a new type of modal variant --- .../components/DeleteRecordsActionEffect.tsx | 33 +++-- .../ManageFavoritesActionEffect.tsx | 7 +- ...MultipleRecordsActionMenuEntriesSetter.tsx | 4 + .../RecordActionMenuEntriesSetter.tsx | 23 +-- .../SingleRecordActionMenuEntriesSetter.tsx | 4 + .../action-menu/components/ActionMenu.tsx | 30 ---- .../components/RecordIndexActionMenu.tsx | 35 +++++ ...nuBar.tsx => RecordIndexActionMenuBar.tsx} | 13 +- ....tsx => RecordIndexActionMenuBarEntry.tsx} | 6 +- ....tsx => RecordIndexActionMenuDropdown.tsx} | 6 +- ...ct.tsx => RecordIndexActionMenuEffect.tsx} | 9 +- .../components/RecordShowActionMenu.tsx | 31 +++++ .../components/RecordShowActionMenuBar.tsx | 16 +++ .../RecordShowActionMenuBarEntry.tsx | 53 +++++++ .../__stories__/ActionMenuBar.stories.tsx | 116 ---------------- .../RecordIndexActionMenuBar.stories.tsx | 131 ++++++++++++++++++ ...RecordIndexActionMenuBarEntry.stories.tsx} | 10 +- ...RecordIndexActionMenuDropdown.stories.tsx} | 14 +- .../RecordShowActionMenuBar.stories.tsx | 131 ++++++++++++++++++ ...tionMenuDropdownPositionComponentState.ts} | 4 +- .../action-menu/types/ActionMenuType.ts | 1 + ...nContextStoreComponentInstanceIdEffect.tsx | 22 +++ ...reCurrentObjectMetadataIdComponentState.ts | 9 ++ ...ontextStoreCurrentObjectMetadataIdState.ts | 8 -- ...contextStoreCurrentViewIdComponentState.ts | 10 ++ .../states/contextStoreCurrentViewIdState.ts | 6 - ...reNumberOfSelectedRecordsComponentState.ts | 9 ++ ...ontextStoreNumberOfSelectedRecordsState.ts | 6 - ...StoreTargetedRecordsRuleComponentState.ts} | 10 +- .../ContextStoreComponentInstanceContext.tsx | 4 + .../mainContextStoreComponentInstanceId.ts | 8 ++ .../computeContextStoreFilters.test.ts | 2 +- .../utils/computeContextStoreFilters.ts | 2 +- .../components/RecordBoardCard.tsx | 4 +- .../RecordIndexBoardDataLoaderEffect.tsx | 7 +- .../components/RecordIndexContainer.tsx | 11 +- ...textStoreNumberOfSelectedRecordsEffect.tsx | 15 +- ...tainerContextStoreObjectMetadataEffect.tsx | 8 +- .../RecordIndexTableContainerEffect.tsx | 9 +- .../hooks/__tests__/useRecordData.test.tsx | 3 +- .../options/hooks/useRecordData.ts | 7 +- .../components/RecordShowContainer.tsx | 18 ++- .../RecordShowContainerContextStoreEffect.tsx | 56 ++++++++ .../hooks/useTriggerActionMenuDropdown.ts | 4 +- .../modal/components/ConfirmationModal.tsx | 5 +- .../ui/layout/modal/components/Modal.tsx | 6 +- .../page/components/RightDrawerContainer.tsx | 52 ------- .../components/ShowPageSubContainer.tsx | 24 +--- .../components/QueryParamsViewIdEffect.tsx | 8 +- .../pages/object-record/RecordIndexPage.tsx | 15 +- .../pages/object-record/RecordShowPage.tsx | 2 - .../RecordShowPageContextStoreEffect.tsx | 57 -------- .../testing/jest/JestContextStoreSetter.tsx | 17 +-- ...taAndApolloMocksAndContextStoreWrapper.tsx | 25 ++-- .../src/theme/constants/BackgroundDark.ts | 5 +- .../src/theme/constants/BackgroundLight.ts | 5 +- 56 files changed, 718 insertions(+), 418 deletions(-) delete mode 100644 packages/twenty-front/src/modules/action-menu/components/ActionMenu.tsx create mode 100644 packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenu.tsx rename packages/twenty-front/src/modules/action-menu/components/{ActionMenuBar.tsx => RecordIndexActionMenuBar.tsx} (76%) rename packages/twenty-front/src/modules/action-menu/components/{ActionMenuBarEntry.tsx => RecordIndexActionMenuBarEntry.tsx} (89%) rename packages/twenty-front/src/modules/action-menu/components/{ActionMenuDropdown.tsx => RecordIndexActionMenuDropdown.tsx} (91%) rename packages/twenty-front/src/modules/action-menu/components/{ActionMenuEffect.tsx => RecordIndexActionMenuEffect.tsx} (77%) create mode 100644 packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenu.tsx create mode 100644 packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenuBar.tsx create mode 100644 packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenuBarEntry.tsx delete mode 100644 packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBar.stories.tsx create mode 100644 packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBar.stories.tsx rename packages/twenty-front/src/modules/action-menu/components/__stories__/{ActionMenuBarEntry.stories.tsx => RecordIndexActionMenuBarEntry.stories.tsx} (78%) rename packages/twenty-front/src/modules/action-menu/components/__stories__/{ActionMenuDropdown.stories.tsx => RecordIndexActionMenuDropdown.stories.tsx} (85%) create mode 100644 packages/twenty-front/src/modules/action-menu/components/__stories__/RecordShowActionMenuBar.stories.tsx rename packages/twenty-front/src/modules/action-menu/states/{actionMenuDropdownPositionComponentState.ts => recordIndexActionMenuDropdownPositionComponentState.ts} (67%) create mode 100644 packages/twenty-front/src/modules/action-menu/types/ActionMenuType.ts create mode 100644 packages/twenty-front/src/modules/context-store/components/SetMainContextStoreComponentInstanceIdEffect.tsx create mode 100644 packages/twenty-front/src/modules/context-store/states/contextStoreCurrentObjectMetadataIdComponentState.ts delete mode 100644 packages/twenty-front/src/modules/context-store/states/contextStoreCurrentObjectMetadataIdState.ts create mode 100644 packages/twenty-front/src/modules/context-store/states/contextStoreCurrentViewIdComponentState.ts delete mode 100644 packages/twenty-front/src/modules/context-store/states/contextStoreCurrentViewIdState.ts create mode 100644 packages/twenty-front/src/modules/context-store/states/contextStoreNumberOfSelectedRecordsComponentState.ts delete mode 100644 packages/twenty-front/src/modules/context-store/states/contextStoreNumberOfSelectedRecordsState.ts rename packages/twenty-front/src/modules/context-store/states/{contextStoreTargetedRecordsRuleState.ts => contextStoreTargetedRecordsRuleComponentState.ts} (53%) create mode 100644 packages/twenty-front/src/modules/context-store/states/contexts/ContextStoreComponentInstanceContext.tsx create mode 100644 packages/twenty-front/src/modules/context-store/states/mainContextStoreComponentInstanceId.ts create mode 100644 packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainerContextStoreEffect.tsx delete mode 100644 packages/twenty-front/src/modules/ui/layout/page/components/RightDrawerContainer.tsx delete mode 100644 packages/twenty-front/src/pages/object-record/RecordShowPageContextStoreEffect.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 dfa609fc05..a1f1f34f1d 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,6 +1,7 @@ import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; -import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; -import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { ActionMenuType } from '@/action-menu/types/ActionMenuType'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; import { useFavorites } from '@/favorites/hooks/useFavorites'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; @@ -9,16 +10,19 @@ 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 { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useCallback, useEffect, useState } from 'react'; -import { useRecoilValue } from 'recoil'; import { IconTrash, isDefined } from 'twenty-ui'; export const DeleteRecordsActionEffect = ({ position, objectMetadataItem, + actionMenuType, }: { position: number; objectMetadataItem: ObjectMetadataItem; + actionMenuType: ActionMenuType; }) => { const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); @@ -35,12 +39,12 @@ export const DeleteRecordsActionEffect = ({ const { favorites, deleteFavorite } = useFavorites(); - const contextStoreNumberOfSelectedRecords = useRecoilValue( - contextStoreNumberOfSelectedRecordsState, + const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2( + contextStoreNumberOfSelectedRecordsComponentState, ); - const contextStoreTargetedRecordsRule = useRecoilValue( - contextStoreTargetedRecordsRuleState, + const contextStoreTargetedRecordsRule = useRecoilComponentValueV2( + contextStoreTargetedRecordsRuleComponentState, ); const graphqlFilter = computeContextStoreFilters( @@ -53,6 +57,8 @@ export const DeleteRecordsActionEffect = ({ filter: graphqlFilter, }); + const { closeRightDrawer } = useRightDrawer(); + const handleDeleteClick = useCallback(async () => { const recordIdsToDelete = await fetchAllRecordIds(); @@ -112,10 +118,19 @@ export const DeleteRecordsActionEffect = ({ }? ${ contextStoreNumberOfSelectedRecords === 1 ? 'It' : 'They' } can be recovered from the Options menu.`} - onConfirmClick={() => handleDeleteClick()} + onConfirmClick={() => { + handleDeleteClick(); + + if (actionMenuType === 'recordShow') { + closeRightDrawer(); + } + }} deleteButtonText={`Delete ${ contextStoreNumberOfSelectedRecords > 1 ? 'Records' : 'Record' }`} + modalVariant={ + actionMenuType === 'recordShow' ? 'tertiary' : 'primary' + } /> ), }); @@ -127,8 +142,10 @@ export const DeleteRecordsActionEffect = ({ removeActionMenuEntry('delete'); }; }, [ + actionMenuType, addActionMenuEntry, canDelete, + closeRightDrawer, contextStoreNumberOfSelectedRecords, handleDeleteClick, isDeleteRecordsModalOpen, 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 572bc23939..face480344 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,9 @@ import { useActionMenuEntries } from '@/action-menu/hooks/useActionMenuEntries'; -import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { useFavorites } from '@/favorites/hooks/useFavorites'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useEffect } from 'react'; import { useRecoilValue } from 'recoil'; import { IconHeart, IconHeartOff, isDefined } from 'twenty-ui'; @@ -16,8 +17,8 @@ export const ManageFavoritesActionEffect = ({ }) => { const { addActionMenuEntry, removeActionMenuEntry } = useActionMenuEntries(); - const contextStoreTargetedRecordsRule = useRecoilValue( - contextStoreTargetedRecordsRuleState, + const contextStoreTargetedRecordsRule = useRecoilComponentValueV2( + contextStoreTargetedRecordsRuleComponentState, ); const { favorites, createFavorite, deleteFavorite } = useFavorites(); 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 ad47a1ee17..f1fcd52c9b 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,16 @@ import { DeleteRecordsActionEffect } from '@/action-menu/actions/record-actions/components/DeleteRecordsActionEffect'; import { ExportRecordsActionEffect } from '@/action-menu/actions/record-actions/components/ExportRecordsActionEffect'; +import { ActionMenuType } from '@/action-menu/types/ActionMenuType'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; const actionEffects = [ExportRecordsActionEffect, DeleteRecordsActionEffect]; export const MultipleRecordsActionMenuEntriesSetter = ({ objectMetadataItem, + actionMenuType, }: { objectMetadataItem: ObjectMetadataItem; + actionMenuType: ActionMenuType; }) => { return ( <> @@ -16,6 +19,7 @@ export const MultipleRecordsActionMenuEntriesSetter = ({ key={index} position={index} objectMetadataItem={objectMetadataItem} + actionMenuType={actionMenuType} /> ))} 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 acf4a9bed7..5ed405684b 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,17 +1,22 @@ import { MultipleRecordsActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/MultipleRecordsActionMenuEntriesSetter'; import { SingleRecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter'; -import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; -import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; +import { ActionMenuType } from '@/action-menu/types/ActionMenuType'; +import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { useObjectMetadataItemById } from '@/object-metadata/hooks/useObjectMetadataItemById'; -import { useRecoilValue } from 'recoil'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; -export const RecordActionMenuEntriesSetter = () => { - const contextStoreNumberOfSelectedRecords = useRecoilValue( - contextStoreNumberOfSelectedRecordsState, +export const RecordActionMenuEntriesSetter = ({ + actionMenuType, +}: { + actionMenuType: ActionMenuType; +}) => { + const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2( + contextStoreNumberOfSelectedRecordsComponentState, ); - const contextStoreCurrentObjectMetadataId = useRecoilValue( - contextStoreCurrentObjectMetadataIdState, + const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2( + contextStoreCurrentObjectMetadataIdComponentState, ); const { objectMetadataItem } = useObjectMetadataItemById({ @@ -32,6 +37,7 @@ export const RecordActionMenuEntriesSetter = () => { return ( ); } @@ -39,6 +45,7 @@ export const RecordActionMenuEntriesSetter = () => { 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 9c4b1d528f..7bf10746fb 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,12 +1,15 @@ 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 { ActionMenuType } from '@/action-menu/types/ActionMenuType'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; export const SingleRecordActionMenuEntriesSetter = ({ objectMetadataItem, + actionMenuType, }: { objectMetadataItem: ObjectMetadataItem; + actionMenuType: ActionMenuType; }) => { const actionEffects = [ ManageFavoritesActionEffect, @@ -20,6 +23,7 @@ export const SingleRecordActionMenuEntriesSetter = ({ key={index} position={index} objectMetadataItem={objectMetadataItem} + actionMenuType={actionMenuType} /> ))} diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenu.tsx b/packages/twenty-front/src/modules/action-menu/components/ActionMenu.tsx deleted file mode 100644 index 92cda27cc9..0000000000 --- a/packages/twenty-front/src/modules/action-menu/components/ActionMenu.tsx +++ /dev/null @@ -1,30 +0,0 @@ -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/RecordIndexActionMenu.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenu.tsx new file mode 100644 index 0000000000..866303d3f5 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenu.tsx @@ -0,0 +1,35 @@ +import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter'; +import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals'; +import { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar'; +import { RecordIndexActionMenuDropdown } from '@/action-menu/components/RecordIndexActionMenuDropdown'; +import { RecordIndexActionMenuEffect } from '@/action-menu/components/RecordIndexActionMenuEffect'; + +import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; + +export const RecordIndexActionMenu = ({ + actionMenuId, +}: { + actionMenuId: string; +}) => { + const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2( + contextStoreCurrentObjectMetadataIdComponentState, + ); + + return ( + <> + {contextStoreCurrentObjectMetadataId && ( + + + + + + + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBar.tsx similarity index 76% rename from packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx rename to packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBar.tsx index 2fd2937408..ea0383727e 100644 --- a/packages/twenty-front/src/modules/action-menu/components/ActionMenuBar.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBar.tsx @@ -1,14 +1,13 @@ import styled from '@emotion/styled'; -import { ActionMenuBarEntry } from '@/action-menu/components/ActionMenuBarEntry'; +import { RecordIndexActionMenuBarEntry } from '@/action-menu/components/RecordIndexActionMenuBarEntry'; import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; import { ActionBarHotkeyScope } from '@/action-menu/types/ActionBarHotKeyScope'; -import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; 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'; -import { useRecoilValue } from 'recoil'; const StyledLabel = styled.div` color: ${({ theme }) => theme.font.color.tertiary}; @@ -18,9 +17,9 @@ const StyledLabel = styled.div` padding-right: ${({ theme }) => theme.spacing(2)}; `; -export const ActionMenuBar = () => { - const contextStoreNumberOfSelectedRecords = useRecoilValue( - contextStoreNumberOfSelectedRecordsState, +export const RecordIndexActionMenuBar = () => { + const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2( + contextStoreNumberOfSelectedRecordsComponentState, ); const actionMenuId = useAvailableComponentInstanceIdOrThrow( @@ -44,7 +43,7 @@ export const ActionMenuBar = () => { > {contextStoreNumberOfSelectedRecords} selected: {actionMenuEntries.map((entry, index) => ( - + ))} ); diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuBarEntry.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarEntry.tsx similarity index 89% rename from packages/twenty-front/src/modules/action-menu/components/ActionMenuBarEntry.tsx rename to packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarEntry.tsx index 02802ec4a6..8be4747983 100644 --- a/packages/twenty-front/src/modules/action-menu/components/ActionMenuBarEntry.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuBarEntry.tsx @@ -4,7 +4,7 @@ import styled from '@emotion/styled'; import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent'; -type ActionMenuBarEntryProps = { +type RecordIndexActionMenuBarEntryProps = { entry: ActionMenuEntry; }; @@ -35,7 +35,9 @@ const StyledButtonLabel = styled.div` margin-left: ${({ theme }) => theme.spacing(1)}; `; -export const ActionMenuBarEntry = ({ entry }: ActionMenuBarEntryProps) => { +export const RecordIndexActionMenuBarEntry = ({ + entry, +}: RecordIndexActionMenuBarEntryProps) => { const theme = useTheme(); return ( ` width: auto; `; -export const ActionMenuDropdown = () => { +export const RecordIndexActionMenuDropdown = () => { const actionMenuEntries = useRecoilComponentValueV2( actionMenuEntriesComponentSelector, ); @@ -45,7 +45,7 @@ export const ActionMenuDropdown = () => { const actionMenuDropdownPosition = useRecoilValue( extractComponentState( - actionMenuDropdownPositionComponentState, + recordIndexActionMenuDropdownPositionComponentState, `action-menu-dropdown-${actionMenuId}`, ), ); diff --git a/packages/twenty-front/src/modules/action-menu/components/ActionMenuEffect.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuEffect.tsx similarity index 77% rename from packages/twenty-front/src/modules/action-menu/components/ActionMenuEffect.tsx rename to packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuEffect.tsx index 89ba0f003e..97c785af7f 100644 --- a/packages/twenty-front/src/modules/action-menu/components/ActionMenuEffect.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuEffect.tsx @@ -1,15 +1,16 @@ import { useActionMenu } from '@/action-menu/hooks/useActionMenu'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; -import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; import { useEffect } from 'react'; import { useRecoilValue } from 'recoil'; -export const ActionMenuEffect = () => { - const contextStoreNumberOfSelectedRecords = useRecoilValue( - contextStoreNumberOfSelectedRecordsState, +export const RecordIndexActionMenuEffect = () => { + const contextStoreNumberOfSelectedRecords = useRecoilComponentValueV2( + contextStoreNumberOfSelectedRecordsComponentState, ); const actionMenuId = useAvailableComponentInstanceIdOrThrow( diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenu.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenu.tsx new file mode 100644 index 0000000000..80bbfe4e14 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenu.tsx @@ -0,0 +1,31 @@ +import { RecordActionMenuEntriesSetter } from '@/action-menu/actions/record-actions/components/RecordActionMenuEntriesSetter'; +import { ActionMenuConfirmationModals } from '@/action-menu/components/ActionMenuConfirmationModals'; +import { RecordShowActionMenuBar } from '@/action-menu/components/RecordShowActionMenuBar'; + +import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; + +export const RecordShowActionMenu = ({ + actionMenuId, +}: { + actionMenuId: string; +}) => { + const contextStoreCurrentObjectMetadataId = useRecoilComponentValueV2( + contextStoreCurrentObjectMetadataIdComponentState, + ); + + return ( + <> + {contextStoreCurrentObjectMetadataId && ( + + + + + + )} + + ); +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenuBar.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenuBar.tsx new file mode 100644 index 0000000000..c4fefcef4a --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenuBar.tsx @@ -0,0 +1,16 @@ +import { RecordShowActionMenuBarEntry } from '@/action-menu/components/RecordShowActionMenuBarEntry'; +import { actionMenuEntriesComponentSelector } from '@/action-menu/states/actionMenuEntriesComponentSelector'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; + +export const RecordShowActionMenuBar = () => { + const actionMenuEntries = useRecoilComponentValueV2( + actionMenuEntriesComponentSelector, + ); + return ( + <> + {actionMenuEntries.map((actionMenuEntry) => ( + + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenuBarEntry.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenuBarEntry.tsx new file mode 100644 index 0000000000..debc175b7c --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/RecordShowActionMenuBarEntry.tsx @@ -0,0 +1,53 @@ +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; + +import { ActionMenuEntry } from '@/action-menu/types/ActionMenuEntry'; +import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent'; + +type RecordShowActionMenuBarEntryProps = { + entry: ActionMenuEntry; +}; + +const StyledButton = styled.div<{ accent: MenuItemAccent }>` + border-radius: ${({ theme }) => theme.border.radius.sm}; + color: ${(props) => + props.accent === 'danger' + ? props.theme.color.red + : props.theme.font.color.secondary}; + cursor: pointer; + display: flex; + justify-content: center; + + padding: ${({ theme }) => theme.spacing(2)}; + transition: background 0.1s ease; + user-select: none; + + &:hover { + background: ${({ theme, accent }) => + accent === 'danger' + ? theme.background.danger + : theme.background.tertiary}; + } +`; + +const StyledButtonLabel = styled.div` + font-weight: ${({ theme }) => theme.font.weight.medium}; + margin-left: ${({ theme }) => theme.spacing(1)}; +`; + +// For now, this component is the same as RecordIndexActionMenuBarEntry but they +// will probably diverge in the future +export const RecordShowActionMenuBarEntry = ({ + entry, +}: RecordShowActionMenuBarEntryProps) => { + const theme = useTheme(); + return ( + entry.onClick?.()} + > + {entry.Icon && } + {entry.label} + + ); +}; 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 deleted file mode 100644 index b34462d8fb..0000000000 --- a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBar.stories.tsx +++ /dev/null @@ -1,116 +0,0 @@ -import { expect, jest } from '@storybook/jest'; -import { Meta, StoryObj } from '@storybook/react'; -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 { 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'; - -const deleteMock = jest.fn(); -const markAsDoneMock = jest.fn(); - -const meta: Meta = { - title: 'Modules/ActionMenu/ActionMenuBar', - component: ActionMenuBar, - decorators: [ - (Story) => ( - { - set(contextStoreTargetedRecordsRuleState, { - mode: 'selection', - selectedRecordIds: ['1', '2', '3'], - }); - set(contextStoreNumberOfSelectedRecordsState, 3); - set( - actionMenuEntriesComponentState.atomFamily({ - instanceId: 'story-action-menu', - }), - new Map([ - [ - 'delete', - { - key: 'delete', - label: 'Delete', - position: 0, - Icon: IconTrash, - onClick: deleteMock, - }, - ], - [ - 'markAsDone', - { - key: 'markAsDone', - label: 'Mark as done', - position: 1, - Icon: IconCheckbox, - onClick: markAsDoneMock, - }, - ], - ]), - ); - set( - isBottomBarOpenedComponentState.atomFamily({ - instanceId: 'action-bar-story-action-menu', - }), - true, - ); - }} - > - - - - - ), - ], - args: { - actionMenuId: 'story-action-menu', - }, -}; - -export default meta; - -type Story = StoryObj; - -export const Default: Story = { - args: { - actionMenuId: 'story-action-menu', - }, -}; - -export const WithCustomSelection: Story = { - args: { - actionMenuId: 'story-action-menu', - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - const selectionText = await canvas.findByText('3 selected:'); - expect(selectionText).toBeInTheDocument(); - }, -}; - -export const WithButtonClicks: Story = { - args: { - actionMenuId: 'story-action-menu', - }, - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - - const deleteButton = await canvas.findByText('Delete'); - await userEvent.click(deleteButton); - - const markAsDoneButton = await canvas.findByText('Mark as done'); - await userEvent.click(markAsDoneButton); - - await waitFor(() => { - expect(deleteMock).toHaveBeenCalled(); - expect(markAsDoneMock).toHaveBeenCalled(); - }); - }, -}; diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBar.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBar.stories.tsx new file mode 100644 index 0000000000..f3a3eab2fd --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBar.stories.tsx @@ -0,0 +1,131 @@ +import { expect, jest } from '@storybook/jest'; +import { Meta, StoryObj } from '@storybook/react'; +import { RecoilRoot } from 'recoil'; + +import { RecordIndexActionMenuBar } from '@/action-menu/components/RecordIndexActionMenuBar'; +import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; +import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState'; +import { userEvent, waitFor, within } from '@storybook/test'; +import { IconCheckbox, IconTrash } from 'twenty-ui'; + +const deleteMock = jest.fn(); +const markAsDoneMock = jest.fn(); + +const meta: Meta = { + title: 'Modules/ActionMenu/RecordIndexActionMenuBar', + component: RecordIndexActionMenuBar, + decorators: [ + (Story) => ( + + { + set( + contextStoreTargetedRecordsRuleComponentState.atomFamily({ + instanceId: 'story-action-menu', + }), + { + mode: 'selection', + selectedRecordIds: ['1', '2', '3'], + }, + ); + set( + contextStoreNumberOfSelectedRecordsComponentState.atomFamily({ + instanceId: 'story-action-menu', + }), + 3, + ); + set( + actionMenuEntriesComponentState.atomFamily({ + instanceId: 'story-action-menu', + }), + new Map([ + [ + 'delete', + { + key: 'delete', + label: 'Delete', + position: 0, + Icon: IconTrash, + onClick: deleteMock, + }, + ], + [ + 'markAsDone', + { + key: 'markAsDone', + label: 'Mark as done', + position: 1, + Icon: IconCheckbox, + onClick: markAsDoneMock, + }, + ], + ]), + ); + set( + isBottomBarOpenedComponentState.atomFamily({ + instanceId: 'action-bar-story-action-menu', + }), + true, + ); + }} + > + + + + + + ), + ], + args: { + actionMenuId: 'story-action-menu', + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + actionMenuId: 'story-action-menu', + }, +}; + +export const WithCustomSelection: Story = { + args: { + actionMenuId: 'story-action-menu', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + const selectionText = await canvas.findByText('3 selected:'); + expect(selectionText).toBeInTheDocument(); + }, +}; + +export const WithButtonClicks: Story = { + args: { + actionMenuId: 'story-action-menu', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const deleteButton = await canvas.findByText('Delete'); + await userEvent.click(deleteButton); + + const markAsDoneButton = await canvas.findByText('Mark as done'); + await userEvent.click(markAsDoneButton); + + await waitFor(() => { + expect(deleteMock).toHaveBeenCalled(); + expect(markAsDoneMock).toHaveBeenCalled(); + }); + }, +}; diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBarEntry.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBarEntry.stories.tsx similarity index 78% rename from packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBarEntry.stories.tsx rename to packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBarEntry.stories.tsx index a9c7b26b84..96267fed3e 100644 --- a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuBarEntry.stories.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuBarEntry.stories.tsx @@ -1,19 +1,19 @@ +import { RecordIndexActionMenuBarEntry } from '@/action-menu/components/RecordIndexActionMenuBarEntry'; import { expect, jest } from '@storybook/jest'; import { Meta, StoryObj } from '@storybook/react'; import { userEvent, within } from '@storybook/testing-library'; import { ComponentDecorator, IconCheckbox, IconTrash } from 'twenty-ui'; -import { ActionMenuBarEntry } from '../ActionMenuBarEntry'; -const meta: Meta = { - title: 'Modules/ActionMenu/ActionMenuBarEntry', - component: ActionMenuBarEntry, +const meta: Meta = { + title: 'Modules/ActionMenu/RecordIndexActionMenuBarEntry', + component: RecordIndexActionMenuBarEntry, decorators: [ComponentDecorator], }; export default meta; -type Story = StoryObj; +type Story = StoryObj; const deleteMock = jest.fn(); const markAsDoneMock = jest.fn(); diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuDropdown.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuDropdown.stories.tsx similarity index 85% rename from packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuDropdown.stories.tsx rename to packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuDropdown.stories.tsx index 53a0714cee..bb9260755a 100644 --- a/packages/twenty-front/src/modules/action-menu/components/__stories__/ActionMenuDropdown.stories.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordIndexActionMenuDropdown.stories.tsx @@ -3,10 +3,10 @@ import { Meta, StoryObj } from '@storybook/react'; import { userEvent, within } from '@storybook/testing-library'; import { RecoilRoot } from 'recoil'; -import { ActionMenuDropdown } from '@/action-menu/components/ActionMenuDropdown'; -import { actionMenuDropdownPositionComponentState } from '@/action-menu/states/actionMenuDropdownPositionComponentState'; +import { RecordIndexActionMenuDropdown } from '@/action-menu/components/RecordIndexActionMenuDropdown'; import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; import { IconCheckbox, IconHeart, IconTrash } from 'twenty-ui'; @@ -15,16 +15,16 @@ const deleteMock = jest.fn(); const markAsDoneMock = jest.fn(); const addToFavoritesMock = jest.fn(); -const meta: Meta = { - title: 'Modules/ActionMenu/ActionMenuDropdown', - component: ActionMenuDropdown, +const meta: Meta = { + title: 'Modules/ActionMenu/RecordIndexActionMenuDropdown', + component: RecordIndexActionMenuDropdown, decorators: [ (Story) => ( { set( extractComponentState( - actionMenuDropdownPositionComponentState, + recordIndexActionMenuDropdownPositionComponentState, 'action-menu-dropdown-story', ), { x: 10, y: 10 }, @@ -87,7 +87,7 @@ const meta: Meta = { export default meta; -type Story = StoryObj; +type Story = StoryObj; export const Default: Story = { args: { diff --git a/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordShowActionMenuBar.stories.tsx b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordShowActionMenuBar.stories.tsx new file mode 100644 index 0000000000..22f1bab670 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/components/__stories__/RecordShowActionMenuBar.stories.tsx @@ -0,0 +1,131 @@ +import { expect, jest } from '@storybook/jest'; +import { Meta, StoryObj } from '@storybook/react'; +import { RecoilRoot } from 'recoil'; + +import { RecordShowActionMenuBar } from '@/action-menu/components/RecordShowActionMenuBar'; +import { actionMenuEntriesComponentState } from '@/action-menu/states/actionMenuEntriesComponentState'; +import { ActionMenuComponentInstanceContext } from '@/action-menu/states/contexts/ActionMenuComponentInstanceContext'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { MenuItemAccent } from '@/ui/navigation/menu-item/types/MenuItemAccent'; +import { userEvent, waitFor, within } from '@storybook/test'; +import { + ComponentDecorator, + IconFileExport, + IconHeart, + IconTrash, +} from 'twenty-ui'; + +const deleteMock = jest.fn(); +const addToFavoritesMock = jest.fn(); +const exportMock = jest.fn(); + +const meta: Meta = { + title: 'Modules/ActionMenu/RecordShowActionMenuBar', + component: RecordShowActionMenuBar, + decorators: [ + (Story) => ( + { + set( + contextStoreTargetedRecordsRuleComponentState.atomFamily({ + instanceId: 'story-action-menu', + }), + { + mode: 'selection', + selectedRecordIds: ['1'], + }, + ); + set( + contextStoreNumberOfSelectedRecordsComponentState.atomFamily({ + instanceId: 'story-action-menu', + }), + 1, + ); + set( + actionMenuEntriesComponentState.atomFamily({ + instanceId: 'story-action-menu', + }), + new Map([ + [ + 'addToFavorites', + { + key: 'addToFavorites', + label: 'Add to favorites', + position: 0, + Icon: IconHeart, + onClick: addToFavoritesMock, + }, + ], + [ + 'export', + { + key: 'export', + label: 'Export', + position: 1, + Icon: IconFileExport, + onClick: exportMock, + }, + ], + [ + 'delete', + { + key: 'delete', + label: 'Delete', + position: 2, + Icon: IconTrash, + onClick: deleteMock, + accent: 'danger' as MenuItemAccent, + }, + ], + ]), + ); + }} + > + + + + + ), + ComponentDecorator, + ], + args: { + actionMenuId: 'story-action-menu', + }, +}; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = { + args: { + actionMenuId: 'story-action-menu', + }, +}; + +export const WithButtonClicks: Story = { + args: { + actionMenuId: 'story-action-menu', + }, + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + + const deleteButton = await canvas.findByText('Delete'); + await userEvent.click(deleteButton); + + const addToFavoritesButton = await canvas.findByText('Add to favorites'); + await userEvent.click(addToFavoritesButton); + + const exportButton = await canvas.findByText('Export'); + await userEvent.click(exportButton); + + await waitFor(() => { + expect(deleteMock).toHaveBeenCalled(); + expect(addToFavoritesMock).toHaveBeenCalled(); + expect(exportMock).toHaveBeenCalled(); + }); + }, +}; diff --git a/packages/twenty-front/src/modules/action-menu/states/actionMenuDropdownPositionComponentState.ts b/packages/twenty-front/src/modules/action-menu/states/recordIndexActionMenuDropdownPositionComponentState.ts similarity index 67% rename from packages/twenty-front/src/modules/action-menu/states/actionMenuDropdownPositionComponentState.ts rename to packages/twenty-front/src/modules/action-menu/states/recordIndexActionMenuDropdownPositionComponentState.ts index f2f8f06b13..4be2f83c5b 100644 --- a/packages/twenty-front/src/modules/action-menu/states/actionMenuDropdownPositionComponentState.ts +++ b/packages/twenty-front/src/modules/action-menu/states/recordIndexActionMenuDropdownPositionComponentState.ts @@ -1,9 +1,9 @@ import { PositionType } from '@/action-menu/types/PositionType'; import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; -export const actionMenuDropdownPositionComponentState = +export const recordIndexActionMenuDropdownPositionComponentState = createComponentState({ - key: 'actionMenuDropdownPositionComponentState', + key: 'recordIndexActionMenuDropdownPositionComponentState', defaultValue: { x: null, y: null, diff --git a/packages/twenty-front/src/modules/action-menu/types/ActionMenuType.ts b/packages/twenty-front/src/modules/action-menu/types/ActionMenuType.ts new file mode 100644 index 0000000000..a0e7d0e4b5 --- /dev/null +++ b/packages/twenty-front/src/modules/action-menu/types/ActionMenuType.ts @@ -0,0 +1 @@ +export type ActionMenuType = 'recordIndex' | 'recordShow'; diff --git a/packages/twenty-front/src/modules/context-store/components/SetMainContextStoreComponentInstanceIdEffect.tsx b/packages/twenty-front/src/modules/context-store/components/SetMainContextStoreComponentInstanceIdEffect.tsx new file mode 100644 index 0000000000..b1acc25d84 --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/components/SetMainContextStoreComponentInstanceIdEffect.tsx @@ -0,0 +1,22 @@ +import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; +import { mainContextStoreComponentInstanceIdState } from '@/context-store/states/mainContextStoreComponentInstanceId'; +import { useContext, useEffect } from 'react'; +import { useSetRecoilState } from 'recoil'; + +export const SetMainContextStoreComponentInstanceIdEffect = () => { + const setMainContextStoreComponentInstanceId = useSetRecoilState( + mainContextStoreComponentInstanceIdState, + ); + + const context = useContext(ContextStoreComponentInstanceContext); + + useEffect(() => { + setMainContextStoreComponentInstanceId(context?.instanceId ?? null); + + return () => { + setMainContextStoreComponentInstanceId(null); + }; + }, [context, setMainContextStoreComponentInstanceId]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentObjectMetadataIdComponentState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentObjectMetadataIdComponentState.ts new file mode 100644 index 0000000000..9898e6c6f5 --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentObjectMetadataIdComponentState.ts @@ -0,0 +1,9 @@ +import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const contextStoreCurrentObjectMetadataIdComponentState = + createComponentStateV2({ + key: 'contextStoreCurrentObjectMetadataIdComponentState', + defaultValue: null, + componentInstanceContext: ContextStoreComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentObjectMetadataIdState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentObjectMetadataIdState.ts deleted file mode 100644 index 3227e53807..0000000000 --- a/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentObjectMetadataIdState.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { createState } from 'twenty-ui'; - -export const contextStoreCurrentObjectMetadataIdState = createState< - string | null ->({ - key: 'contextStoreCurrentObjectMetadataIdState', - defaultValue: null, -}); diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentViewIdComponentState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentViewIdComponentState.ts new file mode 100644 index 0000000000..10136c28d0 --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentViewIdComponentState.ts @@ -0,0 +1,10 @@ +import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const contextStoreCurrentViewIdComponentState = createComponentStateV2< + string | null +>({ + key: 'contextStoreCurrentViewIdComponentState', + defaultValue: null, + componentInstanceContext: ContextStoreComponentInstanceContext, +}); diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentViewIdState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentViewIdState.ts deleted file mode 100644 index 41af1cc135..0000000000 --- a/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentViewIdState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createState } from 'twenty-ui'; - -export const contextStoreCurrentViewIdState = createState({ - key: 'contextStoreCurrentViewIdState', - defaultValue: null, -}); diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreNumberOfSelectedRecordsComponentState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreNumberOfSelectedRecordsComponentState.ts new file mode 100644 index 0000000000..54a6cb1adb --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/states/contextStoreNumberOfSelectedRecordsComponentState.ts @@ -0,0 +1,9 @@ +import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; + +export const contextStoreNumberOfSelectedRecordsComponentState = + createComponentStateV2({ + key: 'contextStoreNumberOfSelectedRecordsComponentState', + defaultValue: 0, + componentInstanceContext: ContextStoreComponentInstanceContext, + }); diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreNumberOfSelectedRecordsState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreNumberOfSelectedRecordsState.ts deleted file mode 100644 index fb1b3544d3..0000000000 --- a/packages/twenty-front/src/modules/context-store/states/contextStoreNumberOfSelectedRecordsState.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { createState } from 'twenty-ui'; - -export const contextStoreNumberOfSelectedRecordsState = createState({ - key: 'contextStoreNumberOfSelectedRecordsState', - defaultValue: 0, -}); diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsRuleState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsRuleComponentState.ts similarity index 53% rename from packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsRuleState.ts rename to packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsRuleComponentState.ts index 7f71377c31..1540c05f3f 100644 --- a/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsRuleState.ts +++ b/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordsRuleComponentState.ts @@ -1,5 +1,6 @@ +import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; -import { createState } from 'twenty-ui'; +import { createComponentStateV2 } from '@/ui/utilities/state/component-state/utils/createComponentStateV2'; type ContextStoreTargetedRecordsRuleSelectionMode = { mode: 'selection'; @@ -16,11 +17,12 @@ export type ContextStoreTargetedRecordsRule = | ContextStoreTargetedRecordsRuleSelectionMode | ContextStoreTargetedRecordsRuleExclusionMode; -export const contextStoreTargetedRecordsRuleState = - createState({ - key: 'contextStoreTargetedRecordsRuleState', +export const contextStoreTargetedRecordsRuleComponentState = + createComponentStateV2({ + key: 'contextStoreTargetedRecordsRuleComponentState', defaultValue: { mode: 'selection', selectedRecordIds: [], }, + componentInstanceContext: ContextStoreComponentInstanceContext, }); diff --git a/packages/twenty-front/src/modules/context-store/states/contexts/ContextStoreComponentInstanceContext.tsx b/packages/twenty-front/src/modules/context-store/states/contexts/ContextStoreComponentInstanceContext.tsx new file mode 100644 index 0000000000..f82a5d53b1 --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/states/contexts/ContextStoreComponentInstanceContext.tsx @@ -0,0 +1,4 @@ +import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext'; + +export const ContextStoreComponentInstanceContext = + createComponentInstanceContext(); diff --git a/packages/twenty-front/src/modules/context-store/states/mainContextStoreComponentInstanceId.ts b/packages/twenty-front/src/modules/context-store/states/mainContextStoreComponentInstanceId.ts new file mode 100644 index 0000000000..2e73436727 --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/states/mainContextStoreComponentInstanceId.ts @@ -0,0 +1,8 @@ +import { createState } from 'twenty-ui'; + +export const mainContextStoreComponentInstanceIdState = createState< + string | null +>({ + key: 'mainContextStoreComponentInstanceIdState', + defaultValue: null, +}); 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 index 689d7287d4..1c65848ede 100644 --- 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 @@ -1,4 +1,4 @@ -import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; diff --git a/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts index 26727fbc26..edc4e17336 100644 --- a/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts +++ b/packages/twenty-front/src/modules/context-store/utils/computeContextStoreFilters.ts @@ -1,4 +1,4 @@ -import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; import { turnFiltersIntoQueryFilter } from '@/object-record/record-filter/utils/turnFiltersIntoQueryFilter'; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx index 2fdffd2598..8e3c002055 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx @@ -1,5 +1,5 @@ import { useActionMenu } from '@/action-menu/hooks/useActionMenu'; -import { actionMenuDropdownPositionComponentState } from '@/action-menu/states/actionMenuDropdownPositionComponentState'; +import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { RecordBoardCardContext } from '@/object-record/record-board/record-board-card/contexts/RecordBoardCardContext'; @@ -188,7 +188,7 @@ export const RecordBoardCard = ({ const setActionMenuDropdownPosition = useSetRecoilState( extractComponentState( - actionMenuDropdownPositionComponentState, + recordIndexActionMenuDropdownPositionComponentState, `action-menu-dropdown-${recordBoardId}`, ), ); 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 9e8358f9e0..e034730304 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,7 +2,7 @@ import { useCallback, useEffect } from 'react'; import { useLocation, useNavigate } from 'react-router-dom'; import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; @@ -11,6 +11,7 @@ import { recordIndexIsCompactModeActiveState } from '@/object-record/record-inde import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; import { computeRecordBoardColumnDefinitionsFromObjectMetadata } from '@/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { isDefined } from '~/utils/isDefined'; @@ -120,8 +121,8 @@ export const RecordIndexBoardDataLoaderEffect = ({ const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector()); - const setContextStoreTargetedRecords = useSetRecoilState( - contextStoreTargetedRecordsRuleState, + const setContextStoreTargetedRecords = useSetRecoilComponentStateV2( + contextStoreTargetedRecordsRuleComponentState, ); useEffect(() => { 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 a28d2f26ac..1bd29294bb 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,8 +22,9 @@ import { RecordFieldValueSelectorContextProvider } from '@/object-record/record- import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { SpreadsheetImportProvider } from '@/spreadsheet-import/provider/components/SpreadsheetImportProvider'; -import { ActionMenu } from '@/action-menu/components/ActionMenu'; -import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { RecordIndexActionMenu } from '@/action-menu/components/RecordIndexActionMenu'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { ViewBar } from '@/views/components/ViewBar'; import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewComponentInstanceContext'; import { ViewField } from '@/views/types/ViewField'; @@ -102,8 +103,8 @@ export const RecordIndexContainer = () => { [columnDefinitions, setTableColumns], ); - const setContextStoreTargetedRecordsRule = useSetRecoilState( - contextStoreTargetedRecordsRuleState, + const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2( + contextStoreTargetedRecordsRuleComponentState, ); return ( @@ -186,7 +187,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 index 2a538c542a..602ed22723 100644 --- 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 @@ -1,22 +1,23 @@ -import { contextStoreNumberOfSelectedRecordsState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsState'; -import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; 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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useContext, useEffect } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; export const RecordIndexContainerContextStoreNumberOfSelectedRecordsEffect = () => { - const setContextStoreNumberOfSelectedRecords = useSetRecoilState( - contextStoreNumberOfSelectedRecordsState, + const setContextStoreNumberOfSelectedRecords = useSetRecoilComponentStateV2( + contextStoreNumberOfSelectedRecordsComponentState, ); - const contextStoreTargetedRecordsRule = useRecoilValue( - contextStoreTargetedRecordsRuleState, + const contextStoreTargetedRecordsRule = useRecoilComponentValueV2( + contextStoreTargetedRecordsRuleComponentState, ); const { objectNamePlural } = useContext(RecordIndexRootPropsContext); 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 index c94611836a..03a02ee36f 100644 --- 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 @@ -1,13 +1,13 @@ -import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; +import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useContext, useEffect } from 'react'; -import { useSetRecoilState } from 'recoil'; export const RecordIndexContainerContextStoreObjectMetadataEffect = () => { - const setContextStoreCurrentObjectMetadataItem = useSetRecoilState( - contextStoreCurrentObjectMetadataIdState, + const setContextStoreCurrentObjectMetadataItem = useSetRecoilComponentStateV2( + contextStoreCurrentObjectMetadataIdComponentState, ); const { objectNamePlural } = useContext(RecordIndexRootPropsContext); 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 ba541ca1a6..a30c797f22 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,13 +1,14 @@ import { useContext, useEffect } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue } from 'recoil'; -import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; import { useHandleToggleColumnFilter } from '@/object-record/record-index/hooks/useHandleToggleColumnFilter'; import { useHandleToggleColumnSort } from '@/object-record/record-index/hooks/useHandleToggleColumnSort'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView'; export const RecordIndexTableContainerEffect = () => { @@ -73,8 +74,8 @@ export const RecordIndexTableContainerEffect = () => { ); }, [setRecordCountInCurrentView, setOnEntityCountChange]); - const setContextStoreTargetedRecords = useSetRecoilState( - contextStoreTargetedRecordsRuleState, + const setContextStoreTargetedRecords = useSetRecoilComponentStateV2( + contextStoreTargetedRecordsRuleComponentState, ); const hasUserSelectedAllRows = useRecoilValue(hasUserSelectedAllRowsState); const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useRecordData.test.tsx b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useRecordData.test.tsx index 9747c2c4e9..d7dc9df12c 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useRecordData.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/__tests__/useRecordData.test.tsx @@ -130,6 +130,7 @@ const mocks: MockedResponse[] = [ const WrapperWithResponse = getJestMetadataAndApolloMocksAndContextStoreWrapper( { apolloMocks: mocks, + componentInstanceId: 'recordIndexId', contextStoreTargetedRecordsRule: { mode: 'selection', selectedRecordIds: [], @@ -155,6 +156,7 @@ const graphqlEmptyResponse = [ const WrapperWithEmptyResponse = getJestMetadataAndApolloMocksAndContextStoreWrapper({ apolloMocks: graphqlEmptyResponse, + componentInstanceId: 'recordIndexId', contextStoreTargetedRecordsRule: { mode: 'selection', selectedRecordIds: [], @@ -207,7 +209,6 @@ describe('useRecordData', () => { objectMetadataItem, callback, pageSize: 30, - delayMs: 0, }), { wrapper: WrapperWithResponse }, diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordData.ts index 7c65a53105..fe08785490 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordData.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordData.ts @@ -7,7 +7,7 @@ import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefin import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { isDefined } from '~/utils/isDefined'; -import { contextStoreTargetedRecordsRuleState } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { computeContextStoreFilters } from '@/context-store/utils/computeContextStoreFilters'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { useLazyFindManyRecords } from '@/object-record/hooks/useLazyFindManyRecords'; @@ -15,6 +15,7 @@ import { useRecordBoardStates } from '@/object-record/record-board/hooks/interna 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 { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { ViewType } from '@/views/types/ViewType'; export const sleep = (ms: number) => @@ -75,8 +76,8 @@ export const useRecordData = ({ ); const columns = useRecoilValue(visibleTableColumnsSelector()); - const contextStoreTargetedRecordsRule = useRecoilValue( - contextStoreTargetedRecordsRuleState, + const contextStoreTargetedRecordsRule = useRecoilComponentValueV2( + contextStoreTargetedRecordsRuleComponentState, ); const queryFilter = computeContextStoreFilters( 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 01e5f42d65..075b83cd35 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,7 +1,10 @@ -import { InformationBannerDeletedRecord } from '@/information-banner/components/deleted-record/InformationBannerDeletedRecord'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { ShowPageContainer } from '@/ui/layout/page/components/ShowPageContainer'; +import { SetMainContextStoreComponentInstanceIdEffect } from '@/context-store/components/SetMainContextStoreComponentInstanceIdEffect'; +import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; +import { InformationBannerDeletedRecord } from '@/information-banner/components/deleted-record/InformationBannerDeletedRecord'; +import { RecordShowContainerContextStoreEffect } from '@/object-record/record-show/components/RecordShowContainerContextStoreEffect'; 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'; @@ -38,7 +41,16 @@ export const RecordShowContainer = ({ ); return ( - <> + + + {!isInRightDrawer && } {recordFromStore && recordFromStore.deletedAt && ( - + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainerContextStoreEffect.tsx b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainerContextStoreEffect.tsx new file mode 100644 index 0000000000..fe1c65796b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-show/components/RecordShowContainerContextStoreEffect.tsx @@ -0,0 +1,56 @@ +import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; +import { contextStoreNumberOfSelectedRecordsComponentState } from '@/context-store/states/contextStoreNumberOfSelectedRecordsComponentState'; +import { contextStoreTargetedRecordsRuleComponentState } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; +import { useEffect } from 'react'; + +export const RecordShowContainerContextStoreEffect = ({ + recordId, + objectNameSingular, +}: { + recordId: string; + objectNameSingular: string; +}) => { + const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2( + contextStoreTargetedRecordsRuleComponentState, + ); + + const setContextStoreCurrentObjectMetadataId = useSetRecoilComponentStateV2( + contextStoreCurrentObjectMetadataIdComponentState, + ); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: objectNameSingular, + }); + + const setContextStoreNumberOfSelectedRecords = useSetRecoilComponentStateV2( + contextStoreNumberOfSelectedRecordsComponentState, + ); + + useEffect(() => { + setContextStoreTargetedRecordsRule({ + mode: 'selection', + selectedRecordIds: [recordId], + }); + setContextStoreCurrentObjectMetadataId(objectMetadataItem?.id); + setContextStoreNumberOfSelectedRecords(1); + + return () => { + setContextStoreTargetedRecordsRule({ + mode: 'selection', + selectedRecordIds: [], + }); + setContextStoreCurrentObjectMetadataId(null); + setContextStoreNumberOfSelectedRecords(0); + }; + }, [ + recordId, + setContextStoreTargetedRecordsRule, + setContextStoreCurrentObjectMetadataId, + objectMetadataItem?.id, + setContextStoreNumberOfSelectedRecords, + ]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown.ts index 0870b7bb4d..a651c22cf4 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useTriggerActionMenuDropdown.ts @@ -1,6 +1,6 @@ import { useRecoilCallback } from 'recoil'; -import { actionMenuDropdownPositionComponentState } from '@/action-menu/states/actionMenuDropdownPositionComponentState'; +import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState'; import { isRowSelectedComponentFamilyState } from '@/object-record/record-table/record-table-row/states/isRowSelectedComponentFamilyState'; import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState'; import { isDropdownOpenComponentState } from '@/ui/layout/dropdown/states/isDropdownOpenComponentState'; @@ -23,7 +23,7 @@ export const useTriggerActionMenuDropdown = ({ set( extractComponentState( - actionMenuDropdownPositionComponentState, + recordIndexActionMenuDropdownPositionComponentState, `action-menu-dropdown-${recordTableId}`, ), { 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 7b75faec18..3165ebce78 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 @@ -7,7 +7,7 @@ import { useDebouncedCallback } from 'use-debounce'; import { Button, ButtonAccent } from '@/ui/input/button/components/Button'; import { TextInput } from '@/ui/input/components/TextInput'; -import { Modal } from '@/ui/layout/modal/components/Modal'; +import { Modal, ModalVariants } from '@/ui/layout/modal/components/Modal'; import { Section, SectionAlignment, @@ -26,6 +26,7 @@ export type ConfirmationModalProps = { confirmationValue?: string; confirmButtonAccent?: ButtonAccent; AdditionalButtons?: React.ReactNode; + modalVariant?: ModalVariants; }; const StyledConfirmationModal = styled(Modal)` @@ -71,6 +72,7 @@ export const ConfirmationModal = ({ confirmationPlaceholder, confirmButtonAccent = 'danger', AdditionalButtons, + modalVariant = 'primary', }: ConfirmationModalProps) => { const [inputConfirmationValue, setInputConfirmationValue] = useState(''); @@ -113,6 +115,7 @@ export const ConfirmationModal = ({ onEnter={handleEnter} isClosable={true} padding="large" + modalVariant={modalVariant} > diff --git a/packages/twenty-front/src/modules/ui/layout/modal/components/Modal.tsx b/packages/twenty-front/src/modules/ui/layout/modal/components/Modal.tsx index fe7983082e..5916849250 100644 --- a/packages/twenty-front/src/modules/ui/layout/modal/components/Modal.tsx +++ b/packages/twenty-front/src/modules/ui/layout/modal/components/Modal.tsx @@ -94,7 +94,9 @@ const StyledBackDrop = styled(motion.div)<{ background: ${({ theme, modalVariant }) => modalVariant === 'primary' ? theme.background.overlayPrimary - : theme.background.overlaySecondary}; + : modalVariant === 'secondary' + ? theme.background.overlaySecondary + : theme.background.overlayTertiary}; display: flex; height: 100%; justify-content: center; @@ -132,7 +134,7 @@ const ModalFooter = ({ children, className }: ModalFooterProps) => ( export type ModalSize = 'small' | 'medium' | 'large'; export type ModalPadding = 'none' | 'small' | 'medium' | 'large'; -export type ModalVariants = 'primary' | 'secondary'; +export type ModalVariants = 'primary' | 'secondary' | 'tertiary'; export type ModalProps = React.PropsWithChildren & { size?: ModalSize; diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/RightDrawerContainer.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/RightDrawerContainer.tsx deleted file mode 100644 index 5d4c0efb7e..0000000000 --- a/packages/twenty-front/src/modules/ui/layout/page/components/RightDrawerContainer.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import { ReactNode } from 'react'; -import styled from '@emotion/styled'; -import { MOBILE_VIEWPORT } from 'twenty-ui'; - -import { RightDrawer } from '@/ui/layout/right-drawer/components/RightDrawer'; - -import { PagePanel } from './PagePanel'; - -type RightDrawerContainerProps = { - children: ReactNode; -}; - -const StyledMainContainer = styled.div` - background: ${({ theme }) => theme.background.noisy}; - box-sizing: border-box; - display: flex; - flex: 1 1 auto; - flex-direction: row; - gap: ${({ theme }) => theme.spacing(2)}; - min-height: 0; - padding-bottom: ${({ theme }) => theme.spacing(3)}; - padding-right: ${({ theme }) => theme.spacing(3)}; - padding-left: 0; - width: 100%; - - @media (max-width: ${MOBILE_VIEWPORT}px) { - padding-left: ${({ theme }) => theme.spacing(3)}; - padding-bottom: 0; - } -`; - -type LeftContainerProps = { - isRightDrawerOpen?: boolean; -}; - -const StyledLeftContainer = styled.div` - display: flex; - flex-direction: column; - position: relative; - width: 100%; -`; - -export const RightDrawerContainer = ({ - children, -}: RightDrawerContainerProps) => ( - - - {children} - - - -); 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 29a53033ed..1c6777148f 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 @@ -1,3 +1,4 @@ +import { RecordShowActionMenu } from '@/action-menu/components/RecordShowActionMenu'; import { Calendar } from '@/activities/calendar/components/Calendar'; import { EmailThreads } from '@/activities/emails/components/EmailThreads'; import { Attachments } from '@/activities/files/components/Attachments'; @@ -6,13 +7,11 @@ import { ObjectTasks } from '@/activities/tasks/components/ObjectTasks'; 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'; 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 { ShowPageLeftContainer } from '@/ui/layout/show-page/components/ShowPageLeftContainer'; import { SingleTabProps, TabList } from '@/ui/layout/tab/components/TabList'; @@ -25,9 +24,7 @@ import { WorkflowVersionVisualizerEffect } from '@/workflow/components/WorkflowV import { WorkflowVisualizer } from '@/workflow/components/WorkflowVisualizer'; import { WorkflowVisualizerEffect } from '@/workflow/components/WorkflowVisualizerEffect'; import styled from '@emotion/styled'; -import { useState } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; -import { IconTrash } from 'twenty-ui'; const StyledShowPageRightContainer = styled.div<{ isMobile: boolean }>` display: flex; @@ -198,18 +195,6 @@ export const ShowPageSubContainer = ({ } }; - const [isDeleting, setIsDeleting] = useState(false); - - const { deleteOneRecord } = useDeleteOneRecord({ - objectNameSingular: targetableObject.targetObjectNameSingular, - }); - - const handleDelete = async () => { - setIsDeleting(true); - await deleteOneRecord(targetableObject.id); - setIsDeleting(false); - }; - const [recordFromStore] = useRecoilState( recordStoreFamilyState(targetableObject.id), ); @@ -236,12 +221,7 @@ export const ShowPageSubContainer = ({ {isInRightDrawer && recordFromStore && !recordFromStore.deletedAt && ( - + )} diff --git a/packages/twenty-front/src/modules/views/components/QueryParamsViewIdEffect.tsx b/packages/twenty-front/src/modules/views/components/QueryParamsViewIdEffect.tsx index f306f29bd7..ced095e33a 100644 --- a/packages/twenty-front/src/modules/views/components/QueryParamsViewIdEffect.tsx +++ b/packages/twenty-front/src/modules/views/components/QueryParamsViewIdEffect.tsx @@ -1,14 +1,14 @@ -import { contextStoreCurrentViewIdState } from '@/context-store/states/contextStoreCurrentViewIdState'; +import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; import { useLastVisitedObjectMetadataItem } from '@/navigation/hooks/useLastVisitedObjectMetadataItem'; import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentStateV2'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState'; import { isUndefined } from '@sniptt/guards'; import { useEffect } from 'react'; -import { useSetRecoilState } from 'recoil'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isDefined } from '~/utils/isDefined'; @@ -39,8 +39,8 @@ export const QueryParamsViewIdEffect = () => { objectMetadataItemId?.id, lastVisitedObjectMetadataItemId, ); - const setContextStoreCurrentViewId = useSetRecoilState( - contextStoreCurrentViewIdState, + const setContextStoreCurrentViewId = useSetRecoilComponentStateV2( + contextStoreCurrentViewIdComponentState, ); // // TODO: scope view bar per view id if possible diff --git a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx index 854f89e060..c9352d52f0 100644 --- a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx @@ -1,6 +1,8 @@ import styled from '@emotion/styled'; import { useParams } from 'react-router-dom'; +import { SetMainContextStoreComponentInstanceIdEffect } from '@/context-store/components/SetMainContextStoreComponentInstanceIdEffect'; +import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId'; @@ -73,9 +75,16 @@ export const RecordIndexPage = () => { - - - + + + + + + diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx index 6c83840037..8f2d489bf7 100644 --- a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx @@ -12,7 +12,6 @@ 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'; -import { RecordShowPageContextStoreEffect } from '~/pages/object-record/RecordShowPageContextStoreEffect'; import { RecordShowPageHeader } from '~/pages/object-record/RecordShowPageHeader'; export const RecordShowPage = () => { @@ -40,7 +39,6 @@ export const RecordShowPage = () => { return ( - { - const setContextStoreTargetedRecordsRule = useSetRecoilState( - contextStoreTargetedRecordsRuleState, - ); - - const setContextStoreCurrentObjectMetadataId = useSetRecoilState( - contextStoreCurrentObjectMetadataIdState, - ); - - const { objectNameSingular } = useParams(); - - const { objectMetadataItem } = useObjectMetadataItem({ - objectNameSingular: objectNameSingular ?? '', - }); - - const setContextStoreNumberOfSelectedRecords = useSetRecoilState( - contextStoreNumberOfSelectedRecordsState, - ); - - useEffect(() => { - setContextStoreTargetedRecordsRule({ - mode: 'selection', - selectedRecordIds: [recordId], - }); - setContextStoreCurrentObjectMetadataId(objectMetadataItem?.id); - setContextStoreNumberOfSelectedRecords(1); - - return () => { - setContextStoreTargetedRecordsRule({ - mode: 'selection', - selectedRecordIds: [], - }); - setContextStoreCurrentObjectMetadataId(null); - setContextStoreNumberOfSelectedRecords(0); - }; - }, [ - recordId, - setContextStoreTargetedRecordsRule, - setContextStoreCurrentObjectMetadataId, - objectMetadataItem?.id, - setContextStoreNumberOfSelectedRecords, - ]); - - return null; -}; diff --git a/packages/twenty-front/src/testing/jest/JestContextStoreSetter.tsx b/packages/twenty-front/src/testing/jest/JestContextStoreSetter.tsx index 866ebe143e..47ac02bcc8 100644 --- a/packages/twenty-front/src/testing/jest/JestContextStoreSetter.tsx +++ b/packages/twenty-front/src/testing/jest/JestContextStoreSetter.tsx @@ -1,12 +1,12 @@ import { ReactNode, useEffect, useState } from 'react'; -import { useSetRecoilState } from 'recoil'; -import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; +import { contextStoreCurrentObjectMetadataIdComponentState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdComponentState'; import { ContextStoreTargetedRecordsRule, - contextStoreTargetedRecordsRuleState, -} from '@/context-store/states/contextStoreTargetedRecordsRuleState'; + contextStoreTargetedRecordsRuleComponentState, +} from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useSetRecoilComponentStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentStateV2'; export const JestContextStoreSetter = ({ contextStoreTargetedRecordsRule = { @@ -20,11 +20,12 @@ export const JestContextStoreSetter = ({ contextStoreCurrentObjectMetadataNameSingular?: string; children: ReactNode; }) => { - const setContextStoreTargetedRecordsRule = useSetRecoilState( - contextStoreTargetedRecordsRuleState, + const setContextStoreTargetedRecordsRule = useSetRecoilComponentStateV2( + contextStoreTargetedRecordsRuleComponentState, ); - const setContextStoreCurrentObjectMetadataId = useSetRecoilState( - contextStoreCurrentObjectMetadataIdState, + + const setContextStoreCurrentObjectMetadataId = useSetRecoilComponentStateV2( + contextStoreCurrentObjectMetadataIdComponentState, ); const { objectMetadataItem } = useObjectMetadataItem({ diff --git a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper.tsx b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper.tsx index e674d42821..fba19c23ba 100644 --- a/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper.tsx +++ b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksAndContextStoreWrapper.tsx @@ -1,4 +1,5 @@ -import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleState'; +import { ContextStoreComponentInstanceContext } from '@/context-store/states/contexts/ContextStoreComponentInstanceContext'; +import { ContextStoreTargetedRecordsRule } from '@/context-store/states/contextStoreTargetedRecordsRuleComponentState'; import { MockedResponse } from '@apollo/client/testing'; import { ReactNode } from 'react'; import { MutableSnapshot } from 'recoil'; @@ -10,6 +11,7 @@ export const getJestMetadataAndApolloMocksAndContextStoreWrapper = ({ onInitializeRecoilSnapshot, contextStoreTargetedRecordsRule, contextStoreCurrentObjectMetadataNameSingular, + componentInstanceId, }: { apolloMocks: | readonly MockedResponse, Record>[] @@ -17,6 +19,7 @@ export const getJestMetadataAndApolloMocksAndContextStoreWrapper = ({ onInitializeRecoilSnapshot?: (snapshot: MutableSnapshot) => void; contextStoreTargetedRecordsRule?: ContextStoreTargetedRecordsRule; contextStoreCurrentObjectMetadataNameSingular?: string; + componentInstanceId: string; }) => { const Wrapper = getJestMetadataAndApolloMocksWrapper({ apolloMocks, @@ -24,14 +27,20 @@ export const getJestMetadataAndApolloMocksAndContextStoreWrapper = ({ }); return ({ children }: { children: ReactNode }) => ( - - {children} - + + {children} + + ); }; diff --git a/packages/twenty-ui/src/theme/constants/BackgroundDark.ts b/packages/twenty-ui/src/theme/constants/BackgroundDark.ts index b594cb842b..446a6d2aa4 100644 --- a/packages/twenty-ui/src/theme/constants/BackgroundDark.ts +++ b/packages/twenty-ui/src/theme/constants/BackgroundDark.ts @@ -23,8 +23,9 @@ export const BACKGROUND_DARK = { lighter: RGBA(GRAY_SCALE.gray0, 0.03), danger: RGBA(COLOR.red, 0.08), }, - overlayPrimary: RGBA(GRAY_SCALE.gray80, 0.8), - overlaySecondary: RGBA(GRAY_SCALE.gray80, 0.4), + overlayPrimary: RGBA(GRAY_SCALE.gray90, 0.8), + overlaySecondary: RGBA(GRAY_SCALE.gray90, 0.4), + overlayTertiary: RGBA(GRAY_SCALE.gray90, 0.4), radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`, radialGradientHover: `radial-gradient(76.32% 95.59% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`, primaryInverted: GRAY_SCALE.gray20, diff --git a/packages/twenty-ui/src/theme/constants/BackgroundLight.ts b/packages/twenty-ui/src/theme/constants/BackgroundLight.ts index 187ecf7de8..dfd68500ae 100644 --- a/packages/twenty-ui/src/theme/constants/BackgroundLight.ts +++ b/packages/twenty-ui/src/theme/constants/BackgroundLight.ts @@ -23,8 +23,9 @@ export const BACKGROUND_LIGHT = { lighter: RGBA(GRAY_SCALE.gray100, 0.02), danger: RGBA(COLOR.red, 0.08), }, - overlayPrimary: RGBA(GRAY_SCALE.gray80, 0.8), - overlaySecondary: RGBA(GRAY_SCALE.gray80, 0.4), + overlayPrimary: RGBA(GRAY_SCALE.gray90, 0.8), + overlaySecondary: RGBA(GRAY_SCALE.gray90, 0.4), + overlayTertiary: RGBA(GRAY_SCALE.gray90, 0.08), radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`, radialGradientHover: `radial-gradient(76.32% 95.59% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`, primaryInverted: GRAY_SCALE.gray60, From ec0250616ed01fee34ff9700eec6d36949f46b88 Mon Sep 17 00:00:00 2001 From: shubham yadav <126192924+yadavshubham01@users.noreply.github.com> Date: Wed, 23 Oct 2024 01:49:46 +0530 Subject: [PATCH 096/123] Update install.sh (#7973) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR updates the install.sh script to fetch the docker-compose.yml file from the GitHub branch or tag that matches the version specified by the user, instead of defaulting to the main branch. --------- Co-authored-by: Félix Malfait --- install.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/install.sh b/install.sh index 39eb096b8e..af3f40a46e 100755 --- a/install.sh +++ b/install.sh @@ -45,7 +45,7 @@ trap on_exit EXIT # Use environment variables VERSION and BRANCH, with defaults if not set version=${VERSION:-$(curl -s https://api.github.com/repos/twentyhq/twenty/releases/latest | grep '"tag_name":' | cut -d '"' -f 4)} -branch=${BRANCH:-main} +branch=${BRANCH:-$version} echo "🚀 Using version $version and branch $branch" @@ -72,7 +72,7 @@ done echo "📁 Creating directory '$dir_name'" mkdir -p "$dir_name" && cd "$dir_name" || { echo "❌ Failed to create/access directory '$dir_name'"; exit 1; } -# Copy the twenty/packages/twenty-docker/docker-compose.yml file in it +# Copy twenty/packages/twenty-docker/docker-compose.yml in it echo -e "\t• Copying docker-compose.yml" curl -sLo docker-compose.yml https://raw.githubusercontent.com/twentyhq/twenty/$branch/packages/twenty-docker/docker-compose.yml From 74ecacb791559e3283644ea0b32bc1d1635b8925 Mon Sep 17 00:00:00 2001 From: Sanskar Jain <52746480+sans-byte@users.noreply.github.com> Date: Wed, 23 Oct 2024 17:27:04 +0530 Subject: [PATCH 097/123] fix x axis scroll bar issue on developer page (#7975) This PR Fixes Issue : #7932 I have added the CSS to remove the scroll bar from x axis of the sidebar on the developers page. --- packages/twenty-website/src/app/_components/docs/DocsSideBar.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/twenty-website/src/app/_components/docs/DocsSideBar.tsx b/packages/twenty-website/src/app/_components/docs/DocsSideBar.tsx index dfed8f237f..e118f796a5 100644 --- a/packages/twenty-website/src/app/_components/docs/DocsSideBar.tsx +++ b/packages/twenty-website/src/app/_components/docs/DocsSideBar.tsx @@ -25,6 +25,7 @@ const StyledContainer = styled.div` width: 300px; min-width: 300px; overflow: scroll; + overflow-x: hidden; height: calc(100vh - 60px); position: sticky; top: 64px; From 412877e49a478487bc7c663fd7a9e204d380ac70 Mon Sep 17 00:00:00 2001 From: ZiaCodes <72739794+Khaan25@users.noreply.github.com> Date: Wed, 23 Oct 2024 17:28:36 +0500 Subject: [PATCH 098/123] feat: design-promotional-poster (300 points) (#7970) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Link to tweet: https://x.com/zia_webdev/status/1848764487081619470 ![image](https://github.com/user-attachments/assets/033c06e6-e18b-47f4-83d0-94c09519a8ac) --------- Co-authored-by: Félix Malfait --- .../1-design-promotional-poster-20-share.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 481fa9572d..005b98ce08 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 @@ -1,5 +1,5 @@ **Side Quest**: Design a promotional poster of Twenty and share it on social media. -**Points**: 300 Points +**Points**: 50 Points **Proof**: Add your oss handle and poster link to the list below. Please follow the following schema: @@ -32,4 +32,4 @@ Your turn 👇 » 21-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) poster Link: [poster](https://x.com/sateshcharans/status/1848358958970396727) ---- +» 22-October-2024 by [Khaan25](https://oss.gg/Khaan25) poster Link: [poster](https://drive.google.com/file/d/1IFtzwzKa0C_hT9cL4o3ChsKwVNRP33G_/view?usp=sharing) - [Tweet Link](https://x.com/zia_webdev/status/1848764487081619470) From 25a8638e4e24b7d87f3decbbe745baa4c677ed41 Mon Sep 17 00:00:00 2001 From: ZiaCodes <72739794+Khaan25@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:00:23 +0500 Subject: [PATCH 099/123] =?UTF-8?q?[=F0=9F=95=B9=EF=B8=8F]=20Quest=20Wizar?= =?UTF-8?q?d=20(300=20points)=20(#7971)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ![image](https://github.com/user-attachments/assets/b7907de9-80df-4801-9db0-deaaf196e663) ![image](https://github.com/user-attachments/assets/26687eb0-2a82-4256-97e6-f41a118513fc) ![image](https://github.com/user-attachments/assets/c9ab8c1a-0d42-4afa-ae17-9933819dbfd9) ![image](https://github.com/user-attachments/assets/76cea0cd-41cc-4508-a8fd-23d2338903c3) ![image](https://github.com/user-attachments/assets/61478b84-bced-4cb5-b66c-3d38c5610c7e) --- oss-gg/twenty-side-quest/6-quest-wizard.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/oss-gg/twenty-side-quest/6-quest-wizard.md b/oss-gg/twenty-side-quest/6-quest-wizard.md index 1b7f1c8c5f..53d5fc0659 100644 --- a/oss-gg/twenty-side-quest/6-quest-wizard.md +++ b/oss-gg/twenty-side-quest/6-quest-wizard.md @@ -18,4 +18,4 @@ Your turn 👇 » 21-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) ---- +» 22-October-2024 by [Khaan25](https://oss.gg/Khaan25) \ No newline at end of file From a55423642ae0fcb09a5e534ae15f1fdf18680ac7 Mon Sep 17 00:00:00 2001 From: Vardhaman Bhandari <97441447+Vardhaman619@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:43:14 +0530 Subject: [PATCH 100/123] fix: context menu padding (#7918) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This pull request addresses the issue #7915 regarding the lack of padding in the right-click record menu. This PR add padding to context menu and reuse the existing component used for the filter/sort feature. ![image](https://github.com/user-attachments/assets/4534eba5-a7de-4142-9d5f-ff0aee86a526) ![image](https://github.com/user-attachments/assets/1690890e-cb18-4879-9a9b-3153cd6aeb26) --------- Co-authored-by: Félix Malfait --- .../RecordIndexActionMenuDropdown.tsx | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuDropdown.tsx b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuDropdown.tsx index 156219f00a..5a431d2af3 100644 --- a/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuDropdown.tsx +++ b/packages/twenty-front/src/modules/action-menu/components/RecordIndexActionMenuDropdown.tsx @@ -8,6 +8,7 @@ import { ActionMenuComponentInstanceContext } from '@/action-menu/states/context import { recordIndexActionMenuDropdownPositionComponentState } from '@/action-menu/states/recordIndexActionMenuDropdownPositionComponentState'; import { ActionMenuDropdownHotkeyScope } from '@/action-menu/types/ActionMenuDropdownHotKeyScope'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { useAvailableComponentInstanceIdOrThrow } from '@/ui/utilities/state/component-state/hooks/useAvailableComponentInstanceIdOrThrow'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; @@ -73,15 +74,19 @@ export const RecordIndexActionMenuDropdown = () => { }} data-select-disable dropdownMenuWidth={width} - dropdownComponents={actionMenuEntries.map((item, index) => ( - - ))} + dropdownComponents={ + + {actionMenuEntries.map((item, index) => ( + + ))} + + } /> ); From 5b6487979c2fb1f218bf0457d51ff40e021d7b74 Mon Sep 17 00:00:00 2001 From: ZiaCodes <72739794+Khaan25@users.noreply.github.com> Date: Wed, 23 Oct 2024 18:14:52 +0500 Subject: [PATCH 101/123] feat: Write Blog Post About 20 (750 points) (#7972) ![image](https://github.com/user-attachments/assets/0aaac3a2-7862-469f-ba69-ab17352a883e) --- oss-gg/twenty-content-challenges/2-write-blog-post-about-20.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 0ff978abec..8899872748 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 @@ -24,4 +24,4 @@ Your turn 👇 » 22-October-2024 by [rajeevDewangan](https://oss.gg/rajeevDewangan) blog Link: [blog](https://open.substack.com/pub/rajeevdewangan/p/comprehensive-guide-to-self-hosting?r=4lly3x&utm_campaign=post&utm_medium=web&showWelcomeOnShare=true) ---- +» 22-October-2024 by [Khaan25](https://oss.gg/Khaan25) blog Link: [blog](https://medium.com/@ziaurzai/twenty-crm-modern-solution-for-modern-problems-a0b65fec9d6c) From 45b3992784af43a606962a92cd5268eb356897d6 Mon Sep 17 00:00:00 2001 From: Ngan Phan Date: Wed, 23 Oct 2024 06:44:17 -0700 Subject: [PATCH 102/123] fix: Default View Font Color and Reordering (#7940) This PR fixes issue #6114 I removed the separate check for default item and add it into the draggable list. --- .../components/ViewPickerListContent.tsx | 59 ++++++++----------- 1 file changed, 26 insertions(+), 33 deletions(-) diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx index 80d3f329a8..5463bf5846 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerListContent.tsx @@ -60,8 +60,6 @@ export const ViewPickerListContent = () => { const { getIcon } = useIcons(); - const indexView = viewsOnCurrentObject.find((view) => view.key === 'INDEX'); - const handleDragEnd = useCallback( (result: DropResult) => { if (!result.destination) return; @@ -81,33 +79,29 @@ export const ViewPickerListContent = () => { return ( <> - {indexView && ( - handleViewSelect(indexView.id)} - LeftIcon={getIcon(indexView.icon)} - text={indexView.name} - accent="placeholder" - isDragDisabled - /> - )} indexView?.id !== view.id) - .map((view, index) => ( - ( + handleViewSelect(view.id)} + LeftIcon={getIcon(view.icon)} + text={view.name} + /> + ) : ( { handleEditViewButtonClick(event, view.id), }, ].filter(isDefined)} - isIconDisplayedOnHoverOnly={ - indexView?.id === view.id ? false : true - } + isIconDisplayedOnHoverOnly={true} onClick={() => handleViewSelect(view.id)} LeftIcon={getIcon(view.icon)} text={view.name} /> - } - /> - ))} + ) + } + /> + ))} /> From 849d7c2423db7524f9fd82ccc354945668eaa342 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Wed, 23 Oct 2024 15:49:10 +0200 Subject: [PATCH 103/123] Implement search for rich text fields and use it for notes (#7953) Co-authored-by: Weiko --- .../command-menu/components/CommandMenu.tsx | 11 +--- .../workspace-migration-field.factory.ts | 61 ++++++++++++++----- .../constants/standard-field-ids.ts | 1 + .../get-ts-vector-column-expression.util.ts | 11 ++-- .../utils/is-searchable-field.util.ts | 1 + .../standard-objects/note.workspace-entity.ts | 35 ++++++++++- 6 files changed, 89 insertions(+), 31 deletions(-) diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx index 07ca2a2562..2ad134409c 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx @@ -13,9 +13,7 @@ import { Company } from '@/companies/types/Company'; import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeyboardShortcutMenu'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useSearchRecords } from '@/object-record/hooks/useSearchRecords'; -import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables'; import { Opportunity } from '@/opportunities/types/Opportunity'; import { Person } from '@/people/types/Person'; import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; @@ -181,16 +179,11 @@ export const CommandMenu = () => { searchInput: deferredCommandMenuSearch ?? undefined, }); - const { loading: isNotesLoading, records: notes } = useFindManyRecords({ + const { loading: isNotesLoading, records: notes } = useSearchRecords({ skip: !isCommandMenuOpened, objectNameSingular: CoreObjectNameSingular.Note, - filter: deferredCommandMenuSearch - ? makeOrFilterVariables([ - { title: { ilike: `%${deferredCommandMenuSearch}%` } }, - { body: { ilike: `%${deferredCommandMenuSearch}%` } }, - ]) - : undefined, limit: 3, + searchInput: deferredCommandMenuSearch ?? undefined, }); const { loading: isOpportunitiesLoading, records: opportunities } = diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts index a79838dfbf..c4852a5841 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-field.factory.ts @@ -11,6 +11,7 @@ import { import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; import { + WorkspaceMigrationColumnAction, WorkspaceMigrationColumnActionType, WorkspaceMigrationEntity, WorkspaceMigrationTableAction, @@ -87,29 +88,57 @@ export class WorkspaceMigrationFieldFactory { ): Promise[]> { const workspaceMigrations: Partial[] = []; - for (const fieldMetadata of fieldMetadataCollection) { - if (fieldMetadata.type === FieldMetadataType.RELATION) { - continue; - } + const fieldMetadataCollectionGroupByObjectMetadataId = + fieldMetadataCollection.reduce( + (result, currentFieldMetadata) => { + result[currentFieldMetadata.objectMetadataId] = [ + ...(result[currentFieldMetadata.objectMetadataId] || []), + currentFieldMetadata, + ]; - const migrations: WorkspaceMigrationTableAction[] = [ - { - name: computeObjectTargetTable( - originalObjectMetadataMap[fieldMetadata.objectMetadataId], - ), - action: WorkspaceMigrationTableActionType.ALTER, - columns: this.workspaceMigrationFactory.createColumnActions( + return result; + }, + {} as Record, + ); + + for (const objectMetadataId in fieldMetadataCollectionGroupByObjectMetadataId) { + const fieldMetadataCollection = + fieldMetadataCollectionGroupByObjectMetadataId[objectMetadataId]; + + const columns: WorkspaceMigrationColumnAction[] = []; + + const objectMetadata = + originalObjectMetadataMap[fieldMetadataCollection[0]?.objectMetadataId]; + + for (const fieldMetadata of fieldMetadataCollection) { + // Relations are handled in workspace-migration-relation.factory.ts + if (fieldMetadata.type === FieldMetadataType.RELATION) { + continue; + } + + columns.push( + ...this.workspaceMigrationFactory.createColumnActions( WorkspaceMigrationColumnActionType.CREATE, fieldMetadata, ), - }, - ]; + ); + } workspaceMigrations.push({ - workspaceId: fieldMetadata.workspaceId, - name: generateMigrationName(`create-${fieldMetadata.name}`), + workspaceId: objectMetadata.workspaceId, + name: generateMigrationName( + `create-${objectMetadata.nameSingular}-fields`, + ), isCustom: false, - migrations, + migrations: [ + { + name: computeObjectTargetTable( + originalObjectMetadataMap[objectMetadataId], + ), + action: WorkspaceMigrationTableActionType.ALTER, + columns, + }, + ], }); } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index 764c48237f..25949019e3 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -282,6 +282,7 @@ export const NOTE_STANDARD_FIELD_IDS = { attachments: '20202020-4986-4c92-bf19-39934b149b16', timelineActivities: '20202020-7030-42f8-929c-1a57b25d6bce', favorites: '20202020-4d1d-41ac-b13b-621631298d67', + searchVector: '20202020-7ea8-44d4-9d4c-51dd2a757950', }; export const NOTE_TARGET_STANDARD_FIELD_IDS = { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts index f4f197ff52..9b177555d5 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts @@ -75,8 +75,9 @@ const getColumnExpression = ( ): string => { const quotedColumnName = `"${columnName}"`; - if (fieldType === FieldMetadataType.EMAILS) { - return ` + switch (fieldType) { + case FieldMetadataType.EMAILS: + return ` COALESCE( replace( ${quotedColumnName}, @@ -86,7 +87,9 @@ const getColumnExpression = ( '' ) `; - } else { - return `COALESCE(${quotedColumnName}, '')`; + case FieldMetadataType.RICH_TEXT: + return `COALESCE(jsonb_path_query_array(${quotedColumnName}::jsonb, '$[*].content[*]."text"'::jsonpath)::text, '')`; + default: + return `COALESCE(${quotedColumnName}, '')`; } }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts index bb482ae4a8..78b9c62e75 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/is-searchable-field.util.ts @@ -6,6 +6,7 @@ const SEARCHABLE_FIELD_TYPES = [ FieldMetadataType.EMAILS, FieldMetadataType.ADDRESS, FieldMetadataType.LINKS, + FieldMetadataType.RICH_TEXT, ] as const; export type SearchableFieldType = (typeof SEARCHABLE_FIELD_TYPES)[number]; diff --git a/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts b/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts index 997e8aca19..16e3fb82c2 100644 --- a/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts +++ b/packages/twenty-server/src/modules/note/standard-objects/note.workspace-entity.ts @@ -1,27 +1,42 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; +import { SEARCH_VECTOR_FIELD } from 'src/engine/metadata-modules/constants/search-vector-field.constants'; import { ActorMetadata, FieldActorSource, } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { RelationMetadataType, RelationOnDeleteAction, } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; +import { WorkspaceFieldIndex } from 'src/engine/twenty-orm/decorators/workspace-field-index.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace-is-nullable.decorator'; import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; import { NOTE_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; +import { + FieldTypeAndNameMetadata, + getTsVectorColumnExpressionFromFields, +} from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; import { NoteTargetWorkspaceEntity } from 'src/modules/note/standard-objects/note-target.workspace-entity'; import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; +const TITLE_FIELD_NAME = 'title'; +const BODY_FIELD_NAME = 'body'; + +export const SEARCH_FIELDS_FOR_NOTES: FieldTypeAndNameMetadata[] = [ + { name: TITLE_FIELD_NAME, type: FieldMetadataType.TEXT }, + { name: BODY_FIELD_NAME, type: FieldMetadataType.RICH_TEXT }, +]; + @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.note, namePlural: 'notes', @@ -50,7 +65,7 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity { description: 'Note title', icon: 'IconNotes', }) - title: string; + [TITLE_FIELD_NAME]: string; @WorkspaceField({ standardId: NOTE_STANDARD_FIELD_IDS.body, @@ -60,7 +75,7 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconFilePencil', }) @WorkspaceIsNullable() - body: string | null; + [BODY_FIELD_NAME]: string | null; @WorkspaceField({ standardId: NOTE_STANDARD_FIELD_IDS.createdBy, @@ -122,4 +137,20 @@ export class NoteWorkspaceEntity extends BaseWorkspaceEntity { }) @WorkspaceIsSystem() favorites: Relation; + + @WorkspaceField({ + standardId: NOTE_STANDARD_FIELD_IDS.searchVector, + type: FieldMetadataType.TS_VECTOR, + label: SEARCH_VECTOR_FIELD.label, + description: SEARCH_VECTOR_FIELD.description, + icon: 'IconUser', + generatedType: 'STORED', + asExpression: getTsVectorColumnExpressionFromFields( + SEARCH_FIELDS_FOR_NOTES, + ), + }) + @WorkspaceIsNullable() + @WorkspaceIsSystem() + @WorkspaceFieldIndex({ indexType: IndexType.GIN }) + [SEARCH_VECTOR_FIELD.name]: any; } From dcf92ae7f1a20411ed630b5ab9daf3cfa329fe4f Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Wed, 23 Oct 2024 17:09:32 +0200 Subject: [PATCH 104/123] Migrate to twenty-ui - utilities/dimensions (#7949) This PR was created by [GitStart](https://gitstart.com/) to address the requirements from this ticket: [TWNTY-7539](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-7539). --- ### Description - Move the utilities/dimensions from twenty-front to twenty-ui and update imports\ Fixes twentyhq/private-issues#79 --------- Co-authored-by: gitstart-twenty Co-authored-by: Charles Bochet --- .github/workflows/ci-front.yaml | 2 +- nx.json | 12 +-------- packages/twenty-front/.storybook/main.ts | 6 ++++- packages/twenty-front/project.json | 26 +++++++++---------- .../components/EntityTitleDoubleTextInput.tsx | 4 +-- .../components/ProfilingReporter.tsx | 2 +- .../components/ComputeNodeDimensions.tsx | 5 ++-- packages/twenty-ui/src/utilities/index.ts | 3 ++- .../{ => utils}/getDisplayValueByUrlType.ts | 2 +- 9 files changed, 28 insertions(+), 34 deletions(-) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/utilities/dimensions/components/ComputeNodeDimensions.tsx (96%) rename packages/twenty-ui/src/utilities/{ => utils}/getDisplayValueByUrlType.ts (94%) diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml index 0d9a5b4dc6..c13fca662b 100644 --- a/.github/workflows/ci-front.yaml +++ b/.github/workflows/ci-front.yaml @@ -133,7 +133,7 @@ jobs: 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 + run: npx nx run twenty-front:storybook:serve-and-test:static:performance front-chromatic-deployment: if: contains(github.event.pull_request.labels.*.name, 'run-chromatic') || github.event_name == 'push' needs: front-sb-build diff --git a/nx.json b/nx.json index 0030428ee9..19c6f5462c 100644 --- a/nx.json +++ b/nx.json @@ -108,7 +108,6 @@ "storybook:build": { "executor": "nx:run-commands", "cache": true, - "dependsOn": ["^build"], "inputs": ["^default", "excludeTests"], "outputs": ["{projectRoot}/{options.output-dir}"], "options": { @@ -192,16 +191,7 @@ "executor": "nx:run-commands", "options": { "commands": [ - "npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:serve:static {projectName} --port={args.port}' 'npx wait-on tcp:{args.port} && nx storybook:test {projectName} --port={args.port}'" - ], - "port": 6006 - } - }, - "storybook:serve-and-test:static:performance": { - "executor": "nx:run-commands", - "options": { - "commands": [ - "npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:serve:dev {projectName} --configuration=performance --port={args.port}' 'npx wait-on tcp:{args.port} && nx storybook:test:no-coverage {projectName} --port={args.port} --configuration=performance'" + "npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:serve:static {projectName} --port={args.port} --configuration={args.performance}' 'npx wait-on tcp:{args.port} && nx storybook:test {projectName} --port={args.port} --configuration={args.scope}'" ], "port": 6006 } diff --git a/packages/twenty-front/.storybook/main.ts b/packages/twenty-front/.storybook/main.ts index 8b65348c4b..04dd231aaa 100644 --- a/packages/twenty-front/.storybook/main.ts +++ b/packages/twenty-front/.storybook/main.ts @@ -50,7 +50,11 @@ const config: StorybookConfig = { const { mergeConfig } = await import('vite'); return mergeConfig(config, { - // Add dependencies to pre-optimization + resolve: { + alias: { + 'react-dom/client': 'react-dom/profiling', + }, + }, }); }, }; diff --git a/packages/twenty-front/project.json b/packages/twenty-front/project.json index 245fde1fac..ad72457839 100644 --- a/packages/twenty-front/project.json +++ b/packages/twenty-front/project.json @@ -70,6 +70,12 @@ "storybook:build": { "options": { "env": { "NODE_OPTIONS": "--max_old_space_size=6500" } + }, + "configurations": { + "docs": { "env": { "STORYBOOK_SCOPE": "ui-docs" } }, + "modules": { "env": { "STORYBOOK_SCOPE": "modules" } }, + "pages": { "env": { "STORYBOOK_SCOPE": "pages" } }, + "performance": { "env": { "STORYBOOK_SCOPE": "performance" } } } }, "storybook:serve:dev": { @@ -82,7 +88,13 @@ } }, "storybook:serve:static": { - "options": { "port": 6006 } + "options": { "port": 6006 }, + "configurations": { + "docs": { "env": { "STORYBOOK_SCOPE": "ui-docs" } }, + "modules": { "env": { "STORYBOOK_SCOPE": "modules" } }, + "pages": { "env": { "STORYBOOK_SCOPE": "pages" } }, + "performance": { "env": { "STORYBOOK_SCOPE": "performance" } } + } }, "storybook:coverage": { "configurations": { @@ -104,9 +116,6 @@ }, "storybook:serve-and-test:static": { "options": { - "commands": [ - "npx concurrently --kill-others --success=first -n SB,TEST 'nx storybook:serve:static {projectName} --port={args.port}' 'npx wait-on tcp:{args.port} && nx storybook:test {projectName} --port={args.port} --configuration={args.scope}'" - ], "port": 6006 }, "configurations": { @@ -116,15 +125,6 @@ "performance": { "scope": "performance" } } }, - "storybook:serve-and-test:static:performance": {}, - "storybook:test:no-coverage": { - "configurations": { - "docs": { "env": { "STORYBOOK_SCOPE": "ui-docs" } }, - "modules": { "env": { "STORYBOOK_SCOPE": "modules" } }, - "pages": { "env": { "STORYBOOK_SCOPE": "pages" } }, - "performance": { "env": { "STORYBOOK_SCOPE": "performance" } } - } - }, "graphql:generate": { "executor": "nx:run-commands", "defaultConfiguration": "data", diff --git a/packages/twenty-front/src/modules/ui/input/components/EntityTitleDoubleTextInput.tsx b/packages/twenty-front/src/modules/ui/input/components/EntityTitleDoubleTextInput.tsx index fc16739743..9bffbf8c1f 100644 --- a/packages/twenty-front/src/modules/ui/input/components/EntityTitleDoubleTextInput.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/EntityTitleDoubleTextInput.tsx @@ -1,10 +1,10 @@ -import { ChangeEvent } from 'react'; import styled from '@emotion/styled'; +import { ChangeEvent } from 'react'; import { StyledTextInput as UIStyledTextInput } from '@/ui/field/input/components/TextInput'; -import { ComputeNodeDimensions } from '@/ui/utilities/dimensions/components/ComputeNodeDimensions'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; +import { ComputeNodeDimensions } from 'twenty-ui'; import { InputHotkeyScope } from '../types/InputHotkeyScope'; export type EntityTitleDoubleTextInputProps = { diff --git a/packages/twenty-front/src/testing/profiling/components/ProfilingReporter.tsx b/packages/twenty-front/src/testing/profiling/components/ProfilingReporter.tsx index 91c6e26e3d..6d4ecb0149 100644 --- a/packages/twenty-front/src/testing/profiling/components/ProfilingReporter.tsx +++ b/packages/twenty-front/src/testing/profiling/components/ProfilingReporter.tsx @@ -1,5 +1,5 @@ -import { useMemo } from 'react'; import styled from '@emotion/styled'; +import { useMemo } from 'react'; import { useRecoilState } from 'recoil'; import { PROFILING_REPORTER_DIV_ID } from '~/testing/profiling/constants/ProfilingReporterDivId'; diff --git a/packages/twenty-front/src/modules/ui/utilities/dimensions/components/ComputeNodeDimensions.tsx b/packages/twenty-ui/src/utilities/dimensions/components/ComputeNodeDimensions.tsx similarity index 96% rename from packages/twenty-front/src/modules/ui/utilities/dimensions/components/ComputeNodeDimensions.tsx rename to packages/twenty-ui/src/utilities/dimensions/components/ComputeNodeDimensions.tsx index 2d3acb3f0f..49e84b79fb 100644 --- a/packages/twenty-front/src/modules/ui/utilities/dimensions/components/ComputeNodeDimensions.tsx +++ b/packages/twenty-ui/src/utilities/dimensions/components/ComputeNodeDimensions.tsx @@ -1,7 +1,6 @@ -import { ReactNode, useLayoutEffect, useRef, useState } from 'react'; import styled from '@emotion/styled'; - -import { isDefined } from '~/utils/isDefined'; +import { isDefined } from '@ui/utilities'; +import { ReactNode, useLayoutEffect, useRef, useState } from 'react'; type ComputeNodeDimensionsProps = { children: ( diff --git a/packages/twenty-ui/src/utilities/index.ts b/packages/twenty-ui/src/utilities/index.ts index 0def2683ee..377997ec33 100644 --- a/packages/twenty-ui/src/utilities/index.ts +++ b/packages/twenty-ui/src/utilities/index.ts @@ -5,9 +5,10 @@ export * from './animation/components/AnimatedFadeOut'; export * from './animation/components/AnimatedTextWord'; export * from './animation/components/AnimatedTranslation'; export * from './color/utils/stringToHslColor'; -export * from './getDisplayValueByUrlType'; +export * from './dimensions/components/ComputeNodeDimensions'; export * from './image/getImageAbsoluteURI'; export * from './isDefined'; export * from './screen-size/hooks/useScreenSize'; export * from './state/utils/createState'; export * from './types/Nullable'; +export * from './utils/getDisplayValueByUrlType'; diff --git a/packages/twenty-ui/src/utilities/getDisplayValueByUrlType.ts b/packages/twenty-ui/src/utilities/utils/getDisplayValueByUrlType.ts similarity index 94% rename from packages/twenty-ui/src/utilities/getDisplayValueByUrlType.ts rename to packages/twenty-ui/src/utilities/utils/getDisplayValueByUrlType.ts index 2325d96c38..577906adaf 100644 --- a/packages/twenty-ui/src/utilities/getDisplayValueByUrlType.ts +++ b/packages/twenty-ui/src/utilities/utils/getDisplayValueByUrlType.ts @@ -1,5 +1,5 @@ import { LinkType } from '@ui/navigation/link'; -import { isDefined } from './isDefined'; +import { isDefined } from '../isDefined'; type getUrlDisplayValueByUrlTypeProps = { type: LinkType; From c38a6c6af92a8a5de8555bc84dc85966da19d7ed Mon Sep 17 00:00:00 2001 From: Rajeev Dewangan <63413883+rajeevDewangan@users.noreply.github.com> Date: Wed, 23 Oct 2024 21:09:50 +0530 Subject: [PATCH 105/123] Update 2-tweet-about-fav-twenty-feature.md (#8009) What side quest are you doing : Share a tweet about your favorite feature in Twenty Points : 50 Proof : ![Screenshot 2024-10-23 205918](https://github.com/user-attachments/assets/25d4ded1-1a93-4c14-a193-420d5b2a11a0) --- 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 cb09b99991..598dff8362 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 @@ -34,3 +34,6 @@ Your turn 👇 » 22-October-2024 by Zia Ur Rehman Khan » Link to Tweet: https://x.com/zia_webdev/status/1848660000190697633 + +» 23-October-2024 by Rajeev Dewangan +» Link to Tweet: https://x.com/rajeevdew/status/1849110473272442991 From 165dd872645e23fab8aebdd1ce3d7d9e9da95ba5 Mon Sep 17 00:00:00 2001 From: Rajeev Dewangan <63413883+rajeevDewangan@users.noreply.github.com> Date: Wed, 23 Oct 2024 21:10:12 +0530 Subject: [PATCH 106/123] Update 1-quote-tweet-20-oss-gg-launch.md (#8008) What side quest you solving : Like & Re-Tweet oss.gg Launch Tweet Points : 50 Proof : ![Screenshot 2024-10-23 205849](https://github.com/user-attachments/assets/ad30fc02-6325-41b8-a8c1-a38d0528d984) --- oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md | 5 +++++ 1 file changed, 5 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 1f8edef409..b9805af703 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 @@ -55,3 +55,8 @@ Your turn 👇 » 22-October-2024 by Ritansh Rajput » Link to Tweet: https://x.com/Ritansh_Dev/status/1848641904511975838 + +» 23-October-2024 by Rajeev Dewangan +» Link to Tweet: https://x.com/rajeevdew/status/1849109074685907374 + + From 18778c55ac0241c3216d3149e371c06caab6a217 Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Wed, 23 Oct 2024 21:27:46 +0530 Subject: [PATCH 107/123] Multiple operations on webhooks (#7807) fixes #7792 WIP :) https://github.com/user-attachments/assets/91f16744-c002-4f24-9cdd-cff79743cab1 --------- Co-authored-by: martmull --- .../developers/types/webhook/Webhook.ts | 1 + .../modules/ui/input/components/Select.tsx | 4 +- .../SettingsDevelopersWebhookDetail.tsx | 202 +++++++++++++----- .../SettingsDevelopersWebhooksNew.tsx | 2 + .../constants/WebhookEmptyOperation.ts | 6 + .../webhooks/types/WebhookOperationsType.ts | 4 + ...bhook-operation-into-operations-command.ts | 56 +++++ .../0-32/0-32-upgrade-version.module.ts | 2 + .../jobs/call-webhook-jobs.job.ts | 17 +- .../dtos/default-value.input.ts | 6 + .../field-metadata-default-value.interface.ts | 2 + .../constants/standard-field-ids.ts | 1 + .../webhook.workspace-entity.ts | 12 ++ .../search-webhooks.integration-spec.ts | 4 +- .../webhooks.integration-spec.ts | 4 +- .../display/icon/components/TablerIcons.ts | 2 + 16 files changed, 257 insertions(+), 68 deletions(-) create mode 100644 packages/twenty-front/src/pages/settings/developers/webhooks/constants/WebhookEmptyOperation.ts create mode 100644 packages/twenty-front/src/pages/settings/developers/webhooks/types/WebhookOperationsType.ts create mode 100644 packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-copy-webhook-operation-into-operations-command.ts diff --git a/packages/twenty-front/src/modules/settings/developers/types/webhook/Webhook.ts b/packages/twenty-front/src/modules/settings/developers/types/webhook/Webhook.ts index fea0808b1c..6e202c9a79 100644 --- a/packages/twenty-front/src/modules/settings/developers/types/webhook/Webhook.ts +++ b/packages/twenty-front/src/modules/settings/developers/types/webhook/Webhook.ts @@ -3,5 +3,6 @@ export type Webhook = { targetUrl: string; description?: string; operation: string; + operations: string[]; __typename: 'Webhook'; }; diff --git a/packages/twenty-front/src/modules/ui/input/components/Select.tsx b/packages/twenty-front/src/modules/ui/input/components/Select.tsx index ec574e083d..905fa1a9ef 100644 --- a/packages/twenty-front/src/modules/ui/input/components/Select.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/Select.tsx @@ -11,6 +11,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { isDefined } from '~/utils/isDefined'; +import { EllipsisDisplay } from '@/ui/field/display/components/EllipsisDisplay'; import { SelectHotkeyScope } from '../types/SelectHotkeyScope'; export type SelectOption = { @@ -73,6 +74,7 @@ const StyledLabel = styled.span` const StyledControlLabel = styled.div` align-items: center; display: flex; + overflow: hidden; gap: ${({ theme }) => theme.spacing(1)}; `; @@ -136,7 +138,7 @@ export const Select = ({ stroke={theme.icon.stroke.sm} /> )} - {selectedOption?.label} + {selectedOption?.label} 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 1a2316a0a3..7063102961 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 @@ -1,7 +1,16 @@ import styled from '@emotion/styled'; -import { useState } from 'react'; +import { useMemo, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import { H2Title, IconTrash } from 'twenty-ui'; +import { + H2Title, + IconBox, + IconNorthStar, + IconPlus, + IconRefresh, + IconTrash, + isDefined, + useIcons, +} from 'twenty-ui'; import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; @@ -17,7 +26,8 @@ import { SettingsDevelopersWebhookUsageGraphEffect } from '@/settings/developers import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { Button } from '@/ui/input/button/components/Button'; -import { Select } from '@/ui/input/components/Select'; +import { IconButton } from '@/ui/input/button/components/IconButton'; +import { Select, SelectOption } 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'; @@ -25,11 +35,23 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBa import { Section } from '@/ui/layout/section/components/Section'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import { useRecoilValue } from 'recoil'; +import { WEBHOOK_EMPTY_OPERATION } from '~/pages/settings/developers/webhooks/constants/WebhookEmptyOperation'; +import { WebhookOperationType } from '~/pages/settings/developers/webhooks/types/WebhookOperationsType'; + +const OBJECT_DROPDOWN_WIDTH = 340; +const ACTION_DROPDOWN_WIDTH = 140; const StyledFilterRow = styled.div` - display: flex; - flex-direction: row; + display: grid; + grid-template-columns: ${OBJECT_DROPDOWN_WIDTH}px ${ACTION_DROPDOWN_WIDTH}px auto; gap: ${({ theme }) => theme.spacing(2)}; + margin-bottom: ${({ theme }) => theme.spacing(2)}; + align-items: center; +`; + +const StyledPlaceholder = styled.div` + height: ${({ theme }) => theme.spacing(8)}; + width: ${({ theme }) => theme.spacing(8)}; `; export const SettingsDevelopersWebhooksDetail = () => { @@ -41,20 +63,33 @@ export const SettingsDevelopersWebhooksDetail = () => { const [isDeleteWebhookModalOpen, setIsDeleteWebhookModalOpen] = useState(false); - const [description, setDescription] = useState(''); - const [operationObjectSingularName, setOperationObjectSingularName] = - useState(''); - const [operationAction, setOperationAction] = useState(''); + const [operations, setOperations] = useState([ + WEBHOOK_EMPTY_OPERATION, + ]); const [isDirty, setIsDirty] = useState(false); + const { getIcon } = useIcons(); const { record: webhookData } = useFindOneRecord({ objectNameSingular: CoreObjectNameSingular.Webhook, objectRecordId: webhookId, onCompleted: (data) => { setDescription(data?.description ?? ''); - setOperationObjectSingularName(data?.operation.split('.')[0] ?? ''); - setOperationAction(data?.operation.split('.')[1] ?? ''); + const baseOperations = data?.operations + ? data.operations.map((op: string) => { + const [object, action] = op.split('.'); + return { object, action }; + }) + : data?.operation + ? [ + { + object: data.operation.split('.')[0], + action: data.operation.split('.')[1], + }, + ] + : []; + + setOperations(addEmptyOperationIfNecessary(baseOperations)); setIsDirty(false); }, }); @@ -72,30 +107,87 @@ export const SettingsDevelopersWebhooksDetail = () => { const isAnalyticsV2Enabled = useIsFeatureEnabled('IS_ANALYTICS_V2_ENABLED'); - const fieldTypeOptions = [ - { value: '*', label: 'All Objects' }, - ...objectMetadataItems.map((item) => ({ - value: item.nameSingular, - label: item.labelSingular, - })), + const fieldTypeOptions: SelectOption[] = useMemo( + () => [ + { value: '*', label: 'All Objects', Icon: IconNorthStar }, + ...objectMetadataItems.map((item) => ({ + value: item.nameSingular, + label: item.labelPlural, + Icon: getIcon(item.icon), + })), + ], + [objectMetadataItems, getIcon], + ); + + const actionOptions: SelectOption[] = [ + { value: '*', label: 'All Actions', Icon: IconNorthStar }, + { value: 'create', label: 'Created', Icon: IconPlus }, + { value: 'update', label: 'Updated', Icon: IconRefresh }, + { value: 'delete', label: 'Deleted', Icon: IconTrash }, ]; const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular: CoreObjectNameSingular.Webhook, }); + const cleanAndFormatOperations = (operations: WebhookOperationType[]) => { + return Array.from( + new Set( + operations + .filter((op) => isDefined(op.object) && isDefined(op.action)) + .map((op) => `${op.object}.${op.action}`), + ), + ); + }; + const handleSave = async () => { + const cleanedOperations = cleanAndFormatOperations(operations); setIsDirty(false); await updateOneRecord({ idToUpdate: webhookId, updateOneRecordInput: { - operation: `${operationObjectSingularName}.${operationAction}`, + operation: cleanedOperations?.[0], + operations: cleanedOperations, description: description, }, }); navigate(developerPath); }; + const addEmptyOperationIfNecessary = ( + newOperations: WebhookOperationType[], + ) => { + if ( + !newOperations.some((op) => op.object === '*' && op.action === '*') && + !newOperations.some((op) => op.object === null) + ) { + return [...newOperations, WEBHOOK_EMPTY_OPERATION]; + } + return newOperations; + }; + + const updateOperation = ( + index: number, + field: 'object' | 'action', + value: string | null, + ) => { + const newOperations = [...operations]; + + newOperations[index] = { + ...newOperations[index], + [field]: value, + }; + + setOperations(addEmptyOperationIfNecessary(newOperations)); + setIsDirty(true); + }; + + const removeOperation = (index: number) => { + const newOperations = operations.filter((_, i) => i !== index); + setOperations(addEmptyOperationIfNecessary(newOperations)); + setIsDirty(true); + }; + if (!webhookData?.targetUrl) { return <>; } @@ -108,10 +200,7 @@ export const SettingsDevelopersWebhooksDetail = () => { children: 'Workspace', href: getSettingsPagePath(SettingsPath.Workspace), }, - { - children: 'Developers', - href: developerPath, - }, + { children: 'Developers', href: developerPath }, { children: 'Webhook' }, ]} actionButton={ @@ -152,43 +241,50 @@ export const SettingsDevelopersWebhooksDetail = () => {
- - { - setIsDirty(true); - setOperationAction(operationAction); - }} - options={[ - { value: '*', label: 'All Actions' }, - { value: 'create', label: 'Create' }, - { value: 'update', label: 'Update' }, - { value: 'delete', label: 'Delete' }, - ]} - /> - + {operations.map((operation, index) => ( + + updateOperation(index, 'action', action)} + options={actionOptions} + /> + + {index < operations.length - 1 ? ( + removeOperation(index)} + variant="tertiary" + size="medium" + Icon={IconTrash} + /> + ) : ( + + )} + + ))}
- {isAnalyticsEnabled && isAnalyticsV2Enabled ? ( + {isAnalyticsEnabled && isAnalyticsV2Enabled && ( <> - ) : ( - <> )}
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 4d0e7bb47a..92dcb49d98 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 @@ -19,9 +19,11 @@ export const SettingsDevelopersWebhooksNew = () => { const [formValues, setFormValues] = useState<{ targetUrl: string; operation: string; + operations: string[]; }>({ targetUrl: '', operation: '*.*', + operations: ['*.*'], }); const [isTargetUrlValid, setIsTargetUrlValid] = useState(true); diff --git a/packages/twenty-front/src/pages/settings/developers/webhooks/constants/WebhookEmptyOperation.ts b/packages/twenty-front/src/pages/settings/developers/webhooks/constants/WebhookEmptyOperation.ts new file mode 100644 index 0000000000..218645a0b4 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/developers/webhooks/constants/WebhookEmptyOperation.ts @@ -0,0 +1,6 @@ +import { WebhookOperationType } from '~/pages/settings/developers/webhooks/types/WebhookOperationsType'; + +export const WEBHOOK_EMPTY_OPERATION: WebhookOperationType = { + object: null, + action: '*', +}; diff --git a/packages/twenty-front/src/pages/settings/developers/webhooks/types/WebhookOperationsType.ts b/packages/twenty-front/src/pages/settings/developers/webhooks/types/WebhookOperationsType.ts new file mode 100644 index 0000000000..a3b81332fa --- /dev/null +++ b/packages/twenty-front/src/pages/settings/developers/webhooks/types/WebhookOperationsType.ts @@ -0,0 +1,4 @@ +export type WebhookOperationType = { + object: string | null; + action: string; +}; diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-copy-webhook-operation-into-operations-command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-copy-webhook-operation-into-operations-command.ts new file mode 100644 index 0000000000..44d044fded --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-copy-webhook-operation-into-operations-command.ts @@ -0,0 +1,56 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import { Command } from 'nest-commander'; +import { Repository } from 'typeorm'; +import chalk from 'chalk'; + +import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { BaseCommandOptions } from 'src/database/commands/base.command'; + +@Command({ + name: 'upgrade-0.32:copy-webhook-operation-into-operations', + description: + 'Read, transform and copy webhook from deprecated column operation into newly created column operations', +}) +export class CopyWebhookOperationIntoOperationsCommand extends ActiveWorkspacesCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) { + super(workspaceRepository); + } + + async executeActiveWorkspacesCommand( + passedParams: string[], + options: BaseCommandOptions, + activeWorkspaceIds: string[], + ): Promise { + this.logger.log('Running command to copy operation to operations'); + + for (const workspaceId of activeWorkspaceIds) { + this.logger.log(`Running command for workspace ${workspaceId}`); + + const webhookRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + workspaceId, + 'webhook', + ); + + const webhooks = await webhookRepository.find(); + + for (const webhook of webhooks) { + if ('operation' in webhook) { + await webhookRepository.update(webhook.id, { + operations: [webhook.operation], + }); + this.logger.log( + chalk.yellow(`Copied webhook operation to operations`), + ); + } + } + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.module.ts index 2018c4563a..0d0f59548f 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-32/0-32-upgrade-version.module.ts @@ -10,6 +10,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat import { SearchModule } from 'src/engine/metadata-modules/search/search.module'; import { WorkspaceMigrationRunnerModule } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.module'; import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/workspace-sync-metadata-commands.module'; +import { CopyWebhookOperationIntoOperationsCommand } from 'src/database/commands/upgrade-version/0-32/0-32-copy-webhook-operation-into-operations-command'; @Module({ imports: [ @@ -25,6 +26,7 @@ import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manage providers: [ UpgradeTo0_32Command, EnforceUniqueConstraintsCommand, + CopyWebhookOperationIntoOperationsCommand, SimplifySearchVectorExpressionCommand, ], }) diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job.ts index a1b43eb220..8705d35547 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job.ts @@ -1,6 +1,6 @@ import { Logger } from '@nestjs/common'; -import { Like } from 'typeorm'; +import { ArrayContains } from 'typeorm'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; @@ -54,10 +54,10 @@ export class CallWebhookJobsJob { const webhooks = await webhookRepository.find({ where: [ - { operation: Like(`%${eventName}%`) }, - { operation: Like(`%*.${operation}%`) }, - { operation: Like(`%${nameSingular}.*%`) }, - { operation: Like('%*.*%') }, + { operations: ArrayContains([eventName]) }, + { operations: ArrayContains([`*.${operation}`]) }, + { operations: ArrayContains([`${nameSingular}.*`]) }, + { operations: ArrayContains(['*.*']) }, ], }); @@ -80,12 +80,9 @@ export class CallWebhookJobsJob { ); }); - if (webhooks.length) { + webhooks.length > 0 && this.logger.log( - `CallWebhookJobsJob on eventName '${eventName}' called on webhooks ids [\n"${webhooks - .map((webhook) => webhook.id) - .join('",\n"')}"\n]`, + `CallWebhookJobsJob on eventName '${eventName}' triggered webhooks with ids [\n"${webhooks.map((webhook) => webhook.id).join('",\n"')}"\n]`, ); - } } } diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts index 42d98b4d6e..679a68f713 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/dtos/default-value.input.ts @@ -189,3 +189,9 @@ export class FieldMetadataDefaultValuePhones { @IsObject() additionalPhones: object | null; } + +export class FieldMetadataDefaultArray { + @ValidateIf((_object, value) => value !== null) + @IsArray() + value: string[] | null; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface.ts index 262cd9f227..8acf362373 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-default-value.interface.ts @@ -1,5 +1,6 @@ import { FieldMetadataDefaultActor, + FieldMetadataDefaultArray, FieldMetadataDefaultValueAddress, FieldMetadataDefaultValueBoolean, FieldMetadataDefaultValueCurrency, @@ -48,6 +49,7 @@ type FieldMetadataDefaultValueMapping = { [FieldMetadataType.RAW_JSON]: FieldMetadataDefaultValueRawJson; [FieldMetadataType.RICH_TEXT]: FieldMetadataDefaultValueRichText; [FieldMetadataType.ACTOR]: FieldMetadataDefaultActor; + [FieldMetadataType.ARRAY]: FieldMetadataDefaultArray; }; export type FieldMetadataClassValidation = diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts index 25949019e3..ac9d921310 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids.ts @@ -400,6 +400,7 @@ export const VIEW_STANDARD_FIELD_IDS = { export const WEBHOOK_STANDARD_FIELD_IDS = { targetUrl: '20202020-1229-45a8-8cf4-85c9172aae12', operation: '20202020-15b7-458e-bf30-74770a54410c', + operations: '20202020-15b7-458e-bf30-74770a54411c', description: '20202020-15b7-458e-bf30-74770a54410d', }; diff --git a/packages/twenty-server/src/modules/webhook/standard-objects/webhook.workspace-entity.ts b/packages/twenty-server/src/modules/webhook/standard-objects/webhook.workspace-entity.ts index 70507cddc6..a52f0e8fe7 100644 --- a/packages/twenty-server/src/modules/webhook/standard-objects/webhook.workspace-entity.ts +++ b/packages/twenty-server/src/modules/webhook/standard-objects/webhook.workspace-entity.ts @@ -7,6 +7,7 @@ import { WorkspaceIsNullable } from 'src/engine/twenty-orm/decorators/workspace- import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; import { WEBHOOK_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; +import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.decorator'; @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.webhook, @@ -36,8 +37,19 @@ export class WebhookWorkspaceEntity extends BaseWorkspaceEntity { description: 'Webhook operation', icon: 'IconCheckbox', }) + @WorkspaceIsDeprecated() operation: string; + @WorkspaceField({ + standardId: WEBHOOK_STANDARD_FIELD_IDS.operations, + type: FieldMetadataType.ARRAY, + label: 'Operations', + description: 'Webhook operations', + icon: 'IconCheckbox', + defaultValue: ['*.*'], + }) + operations: string[]; + @WorkspaceField({ standardId: WEBHOOK_STANDARD_FIELD_IDS.description, type: FieldMetadataType.TEXT, 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 index d5a93db25e..89d7bce89e 100644 --- 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 @@ -12,7 +12,7 @@ describe('searchWebhooksResolver (e2e)', () => { node { id targetUrl - operation + operations description createdAt updatedAt @@ -46,7 +46,7 @@ describe('searchWebhooksResolver (e2e)', () => { expect(searchWebhooks).toHaveProperty('id'); expect(searchWebhooks).toHaveProperty('targetUrl'); - expect(searchWebhooks).toHaveProperty('operation'); + expect(searchWebhooks).toHaveProperty('operations'); expect(searchWebhooks).toHaveProperty('description'); expect(searchWebhooks).toHaveProperty('createdAt'); expect(searchWebhooks).toHaveProperty('updatedAt'); diff --git a/packages/twenty-server/test/integration/graphql/suites/object-generated/webhooks.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/object-generated/webhooks.integration-spec.ts index aaf181bf38..f1ddfd1a48 100644 --- a/packages/twenty-server/test/integration/graphql/suites/object-generated/webhooks.integration-spec.ts +++ b/packages/twenty-server/test/integration/graphql/suites/object-generated/webhooks.integration-spec.ts @@ -12,7 +12,7 @@ describe('webhooksResolver (e2e)', () => { node { id targetUrl - operation + operations description createdAt updatedAt @@ -46,7 +46,7 @@ describe('webhooksResolver (e2e)', () => { expect(webhooks).toHaveProperty('id'); expect(webhooks).toHaveProperty('targetUrl'); - expect(webhooks).toHaveProperty('operation'); + expect(webhooks).toHaveProperty('operations'); expect(webhooks).toHaveProperty('description'); expect(webhooks).toHaveProperty('createdAt'); expect(webhooks).toHaveProperty('updatedAt'); diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index 5244849f69..f85046affd 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -134,6 +134,7 @@ export { IconH1, IconH2, IconH3, + IconHandClick, IconHeadphones, IconHeart, IconHeartOff, @@ -166,6 +167,7 @@ export { IconMoneybag, IconMoodSmile, IconMouse2, + IconNorthStar, IconNotes, IconNumbers, IconPaperclip, From 2e8b8452c1b0d685d7f908aab99ff409fd6cff8f Mon Sep 17 00:00:00 2001 From: Thomas Trompette Date: Wed, 23 Oct 2024 18:32:10 +0200 Subject: [PATCH 108/123] Add available variables dropdown (#7964) - Add variable dropdown - Insert variables on click - Save variable as `{{stepName.object.myVar}}` and display only `myVar` https://github.com/user-attachments/assets/9b49e32c-15e6-4b64-9901-0e63664bc3e8 --- packages/twenty-front/package.json | 6 + .../PhoneCountryPickerDropdownButton.tsx | 4 +- .../WorkflowEditActionFormSendEmail.tsx | 8 +- .../components/SearchVariablesDropdown.tsx | 94 +++++++ .../SearchVariablesDropdownStepItem.tsx | 28 ++ .../SearchVariablesDropdownStepSubItem.tsx | 68 +++++ .../components/VariableTagInput.tsx | 154 +++++++++++ .../constants/AvailableVariablesMock.ts | 30 +++ .../constants/SearchVariablesDropdownId.ts | 1 + .../types/WorkflowStepMock.ts | 5 + .../__tests__/initializeEditorContent.test.ts | 153 +++++++++++ .../__tests__/parseEditorContent.test.ts | 239 ++++++++++++++++++ .../utils/initializeEditorContent.ts | 26 ++ .../utils/parseEditorContent.ts | 25 ++ .../search-variables/utils/variableTag.ts | 64 +++++ .../display/icon/components/TablerIcons.ts | 1 + yarn.lock | 96 ++++++- 17 files changed, 997 insertions(+), 5 deletions(-) create mode 100644 packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdown.tsx create mode 100644 packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownStepItem.tsx create mode 100644 packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownStepSubItem.tsx create mode 100644 packages/twenty-front/src/modules/workflow/search-variables/components/VariableTagInput.tsx create mode 100644 packages/twenty-front/src/modules/workflow/search-variables/constants/AvailableVariablesMock.ts create mode 100644 packages/twenty-front/src/modules/workflow/search-variables/constants/SearchVariablesDropdownId.ts create mode 100644 packages/twenty-front/src/modules/workflow/search-variables/types/WorkflowStepMock.ts create mode 100644 packages/twenty-front/src/modules/workflow/search-variables/utils/__tests__/initializeEditorContent.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/search-variables/utils/__tests__/parseEditorContent.test.ts create mode 100644 packages/twenty-front/src/modules/workflow/search-variables/utils/initializeEditorContent.ts create mode 100644 packages/twenty-front/src/modules/workflow/search-variables/utils/parseEditorContent.ts create mode 100644 packages/twenty-front/src/modules/workflow/search-variables/utils/variableTag.ts diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json index 80eb86a7cf..e95bbba5e4 100644 --- a/packages/twenty-front/package.json +++ b/packages/twenty-front/package.json @@ -33,6 +33,12 @@ "@nivo/calendar": "^0.87.0", "@nivo/core": "^0.87.0", "@nivo/line": "^0.87.0", + "@tiptap/extension-document": "^2.9.0", + "@tiptap/extension-paragraph": "^2.9.0", + "@tiptap/extension-placeholder": "^2.9.0", + "@tiptap/extension-text": "^2.9.0", + "@tiptap/extension-text-style": "^2.8.0", + "@tiptap/react": "^2.8.0", "@xyflow/react": "^12.0.4", "transliteration": "^2.3.5" } diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton.tsx index 55d36556cb..0618202504 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/phone/components/PhoneCountryPickerDropdownButton.tsx @@ -69,7 +69,9 @@ export const PhoneCountryPickerDropdownButton = ({ const [selectedCountry, setSelectedCountry] = useState(); - const { isDropdownOpen, closeDropdown } = useDropdown('country-picker'); + const { isDropdownOpen, closeDropdown } = useDropdown( + CountryPickerHotkeyScope.CountryPicker, + ); const handleChange = (countryCode: string) => { onChange(countryCode); diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx index f7d3255c12..764da25a81 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx @@ -5,8 +5,8 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth'; import { Select, SelectOption } from '@/ui/input/components/Select'; import { TextArea } from '@/ui/input/components/TextArea'; -import { TextInput } from '@/ui/input/components/TextInput'; import { WorkflowEditActionFormBase } from '@/workflow/components/WorkflowEditActionFormBase'; +import { VariableTagInput } from '@/workflow/search-variables/components/VariableTagInput'; import { workflowIdState } from '@/workflow/states/workflowIdState'; import { WorkflowSendEmailStep } from '@/workflow/types/Workflow'; import { useTheme } from '@emotion/react'; @@ -208,7 +208,8 @@ export const WorkflowEditActionFormSendEmail = ( name="email" control={form.control} render={({ field }) => ( - ( - theme.background.transparent.lighter}; + color: ${({ theme }) => theme.font.color.tertiary}; + padding: ${({ theme }) => theme.spacing(0)}; + margin: ${({ theme }) => theme.spacing(2)}; +`; + +const SearchVariablesDropdown = ({ + inputId, + editor, +}: { + inputId: string; + editor: Editor; +}) => { + const theme = useTheme(); + + const dropdownId = `${SEARCH_VARIABLES_DROPDOWN_ID}-${inputId}`; + const { isDropdownOpen } = useDropdown(dropdownId); + const [selectedStep, setSelectedStep] = useState< + WorkflowStepMock | undefined + >(undefined); + + const insertVariableTag = (variable: string) => { + editor.commands.insertVariableTag(variable); + }; + + const handleStepSelect = (stepId: string) => { + setSelectedStep( + AVAILABLE_VARIABLES_MOCK.find((step) => step.id === stepId), + ); + }; + + const handleSubItemSelect = (subItem: string) => { + insertVariableTag(subItem); + }; + + const handleBack = () => { + setSelectedStep(undefined); + }; + + return ( + + + + } + dropdownComponents={ + + + {selectedStep ? ( + + ) : ( + + )} + + + } + dropdownPlacement="bottom-end" + dropdownOffset={{ x: 0, y: 4 }} + /> + ); +}; + +export default SearchVariablesDropdown; diff --git a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownStepItem.tsx b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownStepItem.tsx new file mode 100644 index 0000000000..ea0d7cbe5a --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownStepItem.tsx @@ -0,0 +1,28 @@ +import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect'; +import { WorkflowStepMock } from '@/workflow/search-variables/types/WorkflowStepMock'; + +type SearchVariablesDropdownStepItemProps = { + steps: WorkflowStepMock[]; + onSelect: (value: string) => void; +}; + +export const SearchVariablesDropdownStepItem = ({ + steps, + onSelect, +}: SearchVariablesDropdownStepItemProps) => { + return ( + <> + {steps.map((item, _index) => ( + onSelect(item.id)} + text={item.name} + LeftIcon={undefined} + hasSubMenu + /> + ))} + + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownStepSubItem.tsx b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownStepSubItem.tsx new file mode 100644 index 0000000000..2055912892 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/search-variables/components/SearchVariablesDropdownStepSubItem.tsx @@ -0,0 +1,68 @@ +import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; +import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect'; +import { WorkflowStepMock } from '@/workflow/search-variables/types/WorkflowStepMock'; +import { isObject } from '@sniptt/guards'; +import { useState } from 'react'; +import { IconChevronLeft } from 'twenty-ui'; + +type SearchVariablesDropdownStepSubItemProps = { + step: WorkflowStepMock; + onSelect: (value: string) => void; + onBack: () => void; +}; + +const SearchVariablesDropdownStepSubItem = ({ + step, + onSelect, + onBack, +}: SearchVariablesDropdownStepSubItemProps) => { + const [currentPath, setCurrentPath] = useState([]); + + const getSelectedObject = () => { + let selected = step.output; + for (const key of currentPath) { + selected = selected[key]; + } + return selected; + }; + + const handleSelect = (key: string) => { + const selectedObject = getSelectedObject(); + if (isObject(selectedObject[key])) { + setCurrentPath([...currentPath, key]); + } else { + onSelect(`{{${step.id}.${[...currentPath, key].join('.')}}}`); + } + }; + + const goBack = () => { + if (currentPath.length === 0) { + onBack(); + } else { + setCurrentPath(currentPath.slice(0, -1)); + } + }; + + const headerLabel = currentPath.length === 0 ? step.name : currentPath.at(-1); + + return ( + <> + + {headerLabel} + + {Object.entries(getSelectedObject()).map(([key, value]) => ( + handleSelect(key)} + text={key} + hasSubMenu={isObject(value)} + LeftIcon={undefined} + /> + ))} + + ); +}; + +export default SearchVariablesDropdownStepSubItem; diff --git a/packages/twenty-front/src/modules/workflow/search-variables/components/VariableTagInput.tsx b/packages/twenty-front/src/modules/workflow/search-variables/components/VariableTagInput.tsx new file mode 100644 index 0000000000..8b347ecfb6 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/search-variables/components/VariableTagInput.tsx @@ -0,0 +1,154 @@ +import SearchVariablesDropdown from '@/workflow/search-variables/components/SearchVariablesDropdown'; +import { initializeEditorContent } from '@/workflow/search-variables/utils/initializeEditorContent'; +import { parseEditorContent } from '@/workflow/search-variables/utils/parseEditorContent'; +import { VariableTag } from '@/workflow/search-variables/utils/variableTag'; +import styled from '@emotion/styled'; +import Document from '@tiptap/extension-document'; +import Paragraph from '@tiptap/extension-paragraph'; +import Placeholder from '@tiptap/extension-placeholder'; +import Text from '@tiptap/extension-text'; +import { EditorContent, useEditor } from '@tiptap/react'; +import { isDefined } from 'twenty-ui'; +import { useDebouncedCallback } from 'use-debounce'; + +const StyledContainer = styled.div` + display: inline-flex; + flex-direction: column; +`; + +const StyledLabel = styled.div` + color: ${({ theme }) => theme.font.color.light}; + font-size: ${({ theme }) => theme.font.size.xs}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + margin-bottom: ${({ theme }) => theme.spacing(1)}; +`; + +const StyledInputContainer = styled.div` + display: flex; + flex-direction: row; +`; + +const StyledSearchVariablesDropdownContainer = styled.div` + display: flex; + justify-content: center; + align-items: center; + background-color: ${({ theme }) => theme.background.transparent.lighter}; + border-top-right-radius: ${({ theme }) => theme.border.radius.sm}; + border-bottom-right-radius: ${({ theme }) => theme.border.radius.sm}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; +`; + +const StyledEditor = styled.div` + display: flex; + height: 32px; + width: 100%; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + border-bottom-left-radius: ${({ theme }) => theme.border.radius.sm}; + border-top-left-radius: ${({ theme }) => theme.border.radius.sm}; + border-right: none; + box-sizing: border-box; + background-color: ${({ theme }) => theme.background.transparent.lighter}; + overflow: hidden; + padding: ${({ theme }) => theme.spacing(2)}; + + .editor-content { + width: 100%; + } + + .tiptap { + display: flex; + align-items: center; + height: 100%; + color: ${({ theme }) => theme.font.color.primary}; + font-family: ${({ theme }) => theme.font.family}; + font-weight: ${({ theme }) => theme.font.weight.regular}; + border: none !important; + white-space: nowrap; + + p.is-editor-empty:first-of-type::before { + content: attr(data-placeholder); + color: ${({ theme }) => theme.font.color.light}; + float: left; + height: 0; + pointer-events: none; + } + + p { + margin: 0; + } + + .variable-tag { + color: ${({ theme }) => theme.color.blue}; + background-color: ${({ theme }) => theme.color.blue10}; + padding: ${({ theme }) => theme.spacing(1)}; + border-radius: ${({ theme }) => theme.border.radius.sm}; + } + } + + .ProseMirror-focused { + outline: none; + } +`; + +interface VariableTagInputProps { + inputId: string; + label?: string; + value?: string; + onChange?: (content: string) => void; + placeholder?: string; +} + +export const VariableTagInput = ({ + inputId, + label, + value, + placeholder, + onChange, +}: VariableTagInputProps) => { + const deboucedOnUpdate = useDebouncedCallback((editor) => { + const jsonContent = editor.getJSON(); + const parsedContent = parseEditorContent(jsonContent); + onChange?.(parsedContent); + }, 500); + + const editor = useEditor({ + extensions: [ + Document, + Paragraph, + Text, + Placeholder.configure({ + placeholder, + }), + VariableTag, + ], + editable: true, + onCreate: ({ editor }) => { + if (isDefined(value)) { + initializeEditorContent(editor, value); + } + }, + onUpdate: ({ editor }) => { + deboucedOnUpdate(editor); + }, + }); + + if (!editor) { + return null; + } + + return ( + + {label && {label}} + + + + + + + + + + ); +}; + +export default VariableTagInput; diff --git a/packages/twenty-front/src/modules/workflow/search-variables/constants/AvailableVariablesMock.ts b/packages/twenty-front/src/modules/workflow/search-variables/constants/AvailableVariablesMock.ts new file mode 100644 index 0000000000..bb98363608 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/search-variables/constants/AvailableVariablesMock.ts @@ -0,0 +1,30 @@ +import { WorkflowStepMock } from '@/workflow/search-variables/types/WorkflowStepMock'; + +export const AVAILABLE_VARIABLES_MOCK: WorkflowStepMock[] = [ + { + id: '1', + name: 'Person is Created', + output: { + userId: '1', + recordId: '123', + objectMetadataItem: { + id: '1234', + nameSingular: 'person', + namePlural: 'people', + }, + properties: { + after: { + name: 'John Doe', + email: 'john.doe@email.com', + }, + }, + }, + }, + { + id: '2', + name: 'Send Email', + output: { + success: true, + }, + }, +]; diff --git a/packages/twenty-front/src/modules/workflow/search-variables/constants/SearchVariablesDropdownId.ts b/packages/twenty-front/src/modules/workflow/search-variables/constants/SearchVariablesDropdownId.ts new file mode 100644 index 0000000000..72ba97f811 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/search-variables/constants/SearchVariablesDropdownId.ts @@ -0,0 +1 @@ +export const SEARCH_VARIABLES_DROPDOWN_ID = 'search-variables'; diff --git a/packages/twenty-front/src/modules/workflow/search-variables/types/WorkflowStepMock.ts b/packages/twenty-front/src/modules/workflow/search-variables/types/WorkflowStepMock.ts new file mode 100644 index 0000000000..7f6d1813a3 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/search-variables/types/WorkflowStepMock.ts @@ -0,0 +1,5 @@ +export type WorkflowStepMock = { + id: string; + name: string; + output: Record; +}; diff --git a/packages/twenty-front/src/modules/workflow/search-variables/utils/__tests__/initializeEditorContent.test.ts b/packages/twenty-front/src/modules/workflow/search-variables/utils/__tests__/initializeEditorContent.test.ts new file mode 100644 index 0000000000..58220e3374 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/search-variables/utils/__tests__/initializeEditorContent.test.ts @@ -0,0 +1,153 @@ +import { Editor } from '@tiptap/react'; +import { initializeEditorContent } from '../initializeEditorContent'; + +describe('initializeEditorContent', () => { + const mockEditor = { + commands: { + insertContent: jest.fn(), + }, + } as unknown as Editor; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should handle empty string', () => { + initializeEditorContent(mockEditor, ''); + expect(mockEditor.commands.insertContent).not.toHaveBeenCalled(); + }); + + it('should insert plain text correctly', () => { + initializeEditorContent(mockEditor, 'Hello world'); + + expect(mockEditor.commands.insertContent).toHaveBeenCalledTimes(1); + expect(mockEditor.commands.insertContent).toHaveBeenCalledWith( + 'Hello world', + ); + }); + + it('should insert single variable correctly', () => { + initializeEditorContent(mockEditor, '{{user.name}}'); + + expect(mockEditor.commands.insertContent).toHaveBeenCalledTimes(1); + expect(mockEditor.commands.insertContent).toHaveBeenCalledWith({ + type: 'variableTag', + attrs: { variable: '{{user.name}}' }, + }); + }); + + it('should handle text with variable in the middle', () => { + initializeEditorContent(mockEditor, 'Hello {{user.name}} world'); + + expect(mockEditor.commands.insertContent).toHaveBeenCalledTimes(3); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( + 1, + 'Hello ', + ); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(2, { + type: 'variableTag', + attrs: { variable: '{{user.name}}' }, + }); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( + 3, + ' world', + ); + }); + + it('should handle multiple variables', () => { + initializeEditorContent( + mockEditor, + 'Hello {{user.firstName}} {{user.lastName}}, welcome to {{app.name}}', + ); + + expect(mockEditor.commands.insertContent).toHaveBeenCalledTimes(6); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( + 1, + 'Hello ', + ); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(2, { + type: 'variableTag', + attrs: { variable: '{{user.firstName}}' }, + }); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(3, ' '); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(4, { + type: 'variableTag', + attrs: { variable: '{{user.lastName}}' }, + }); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( + 5, + ', welcome to ', + ); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(6, { + type: 'variableTag', + attrs: { variable: '{{app.name}}' }, + }); + }); + + it('should handle variables at the start and end', () => { + initializeEditorContent(mockEditor, '{{start.var}} middle {{end.var}}'); + + expect(mockEditor.commands.insertContent).toHaveBeenCalledTimes(3); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(1, { + type: 'variableTag', + attrs: { variable: '{{start.var}}' }, + }); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( + 2, + ' middle ', + ); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(3, { + type: 'variableTag', + attrs: { variable: '{{end.var}}' }, + }); + }); + + it('should handle consecutive variables', () => { + initializeEditorContent(mockEditor, '{{var1}}{{var2}}{{var3}}'); + + expect(mockEditor.commands.insertContent).toHaveBeenCalledTimes(3); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(1, { + type: 'variableTag', + attrs: { variable: '{{var1}}' }, + }); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(2, { + type: 'variableTag', + attrs: { variable: '{{var2}}' }, + }); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(3, { + type: 'variableTag', + attrs: { variable: '{{var3}}' }, + }); + }); + + it('should handle whitespace between variables', () => { + initializeEditorContent(mockEditor, '{{var1}} {{var2}} '); + + expect(mockEditor.commands.insertContent).toHaveBeenCalledTimes(4); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(1, { + type: 'variableTag', + attrs: { variable: '{{var1}}' }, + }); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(2, ' '); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(3, { + type: 'variableTag', + attrs: { variable: '{{var2}}' }, + }); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(4, ' '); + }); + + it('should handle nested variable syntax', () => { + initializeEditorContent(mockEditor, 'Hello {{user.address.city}}!'); + + expect(mockEditor.commands.insertContent).toHaveBeenCalledTimes(3); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith( + 1, + 'Hello ', + ); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(2, { + type: 'variableTag', + attrs: { variable: '{{user.address.city}}' }, + }); + expect(mockEditor.commands.insertContent).toHaveBeenNthCalledWith(3, '!'); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/search-variables/utils/__tests__/parseEditorContent.test.ts b/packages/twenty-front/src/modules/workflow/search-variables/utils/__tests__/parseEditorContent.test.ts new file mode 100644 index 0000000000..b3ea1e5c34 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/search-variables/utils/__tests__/parseEditorContent.test.ts @@ -0,0 +1,239 @@ +import { JSONContent } from '@tiptap/react'; +import { parseEditorContent } from '../parseEditorContent'; + +describe('parseEditorContent', () => { + it('should parse empty doc', () => { + const input: JSONContent = { + type: 'doc', + content: [], + }; + + expect(parseEditorContent(input)).toBe(''); + }); + + it('should parse simple text node', () => { + const input: JSONContent = { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Hello world', + }, + ], + }, + ], + }; + + expect(parseEditorContent(input)).toBe('Hello world'); + }); + + it('should parse variable tag node', () => { + const input: JSONContent = { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'variableTag', + attrs: { + variable: '{{user.name}}', + }, + }, + ], + }, + ], + }; + + expect(parseEditorContent(input)).toBe('{{user.name}}'); + }); + + it('should parse mixed content with text and variables', () => { + const input: JSONContent = { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Hello ', + }, + { + type: 'variableTag', + attrs: { + variable: '{{user.name}}', + }, + }, + { + type: 'text', + text: ', welcome to ', + }, + { + type: 'variableTag', + attrs: { + variable: '{{app.name}}', + }, + }, + ], + }, + ], + }; + + expect(parseEditorContent(input)).toBe( + 'Hello {{user.name}}, welcome to {{app.name}}', + ); + }); + + it('should parse multiple paragraphs', () => { + const input: JSONContent = { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'First line', + }, + ], + }, + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Second line', + }, + ], + }, + ], + }; + + expect(parseEditorContent(input)).toBe('First lineSecond line'); + }); + + it('should handle missing content array', () => { + const input: JSONContent = { + type: 'doc', + }; + + expect(parseEditorContent(input)).toBe(''); + }); + + it('should handle missing text in text node', () => { + const input: JSONContent = { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + }, + ], + }, + ], + }; + + expect(parseEditorContent(input)).toBe(''); + }); + + it('should handle missing variable in variableTag node', () => { + const input: JSONContent = { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'variableTag', + attrs: {}, + }, + ], + }, + ], + }; + + expect(parseEditorContent(input)).toBe(''); + }); + + it('should handle unknown node types', () => { + const input: JSONContent = { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'unknownType', + content: [ + { + type: 'text', + text: 'This should be ignored', + }, + ], + }, + ], + }, + ], + }; + + expect(parseEditorContent(input)).toBe(''); + }); + + it('should parse complex nested structure', () => { + const input: JSONContent = { + type: 'doc', + content: [ + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Hello ', + }, + { + type: 'variableTag', + attrs: { + variable: '{{user.firstName}}', + }, + }, + { + type: 'text', + text: ' ', + }, + { + type: 'variableTag', + attrs: { + variable: '{{user.lastName}}', + }, + }, + ], + }, + { + type: 'paragraph', + content: [ + { + type: 'text', + text: 'Your ID is: ', + }, + { + type: 'variableTag', + attrs: { + variable: '{{user.id}}', + }, + }, + ], + }, + ], + }; + + expect(parseEditorContent(input)).toBe( + 'Hello {{user.firstName}} {{user.lastName}}Your ID is: {{user.id}}', + ); + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/search-variables/utils/initializeEditorContent.ts b/packages/twenty-front/src/modules/workflow/search-variables/utils/initializeEditorContent.ts new file mode 100644 index 0000000000..5128bc06ec --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/search-variables/utils/initializeEditorContent.ts @@ -0,0 +1,26 @@ +import { isNonEmptyString } from '@sniptt/guards'; +import { Editor } from '@tiptap/react'; + +const REGEX_VARIABLE_TAG = /(\{\{[^}]+\}\})/; + +export const initializeEditorContent = (editor: Editor, content: string) => { + const parts = content.split(REGEX_VARIABLE_TAG); + + parts.forEach((part) => { + if (part.length === 0) { + return; + } + + if (part.startsWith('{{') && part.endsWith('}}')) { + editor.commands.insertContent({ + type: 'variableTag', + attrs: { variable: part }, + }); + return; + } + + if (isNonEmptyString(part)) { + editor.commands.insertContent(part); + } + }); +}; diff --git a/packages/twenty-front/src/modules/workflow/search-variables/utils/parseEditorContent.ts b/packages/twenty-front/src/modules/workflow/search-variables/utils/parseEditorContent.ts new file mode 100644 index 0000000000..e1d77f3108 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/search-variables/utils/parseEditorContent.ts @@ -0,0 +1,25 @@ +import { JSONContent } from '@tiptap/react'; +import { isDefined } from 'twenty-ui'; + +export const parseEditorContent = (json: JSONContent): string => { + const parseNode = (node: JSONContent): string => { + if ( + (node.type === 'paragraph' || node.type === 'doc') && + isDefined(node.content) + ) { + return node.content.map(parseNode).join(''); + } + + if (node.type === 'text') { + return node.text || ''; + } + + if (node.type === 'variableTag') { + return node.attrs?.variable || ''; + } + + return ''; + }; + + return parseNode(json); +}; diff --git a/packages/twenty-front/src/modules/workflow/search-variables/utils/variableTag.ts b/packages/twenty-front/src/modules/workflow/search-variables/utils/variableTag.ts new file mode 100644 index 0000000000..ec2361ee22 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/search-variables/utils/variableTag.ts @@ -0,0 +1,64 @@ +import { Node } from '@tiptap/core'; +import { mergeAttributes } from '@tiptap/react'; + +declare module '@tiptap/core' { + interface Commands { + variableTag: { + insertVariableTag: (variable: string) => ReturnType; + }; + } +} + +export const VariableTag = Node.create({ + name: 'variableTag', + group: 'inline', + inline: true, + atom: true, + + addAttributes: () => ({ + variable: { + default: null, + parseHTML: (element) => element.getAttribute('data-variable'), + renderHTML: (attributes) => { + return { + 'data-variable': attributes.variable, + }; + }, + }, + }), + + renderHTML: ({ node, HTMLAttributes }) => { + const variable = node.attrs.variable as string; + const variableWithoutBrackets = variable.replace( + /\{\{([^}]+)\}\}/g, + (_, variable) => { + return variable; + }, + ); + + const parts = variableWithoutBrackets.split('.'); + const displayText = parts[parts.length - 1]; + + return [ + 'span', + mergeAttributes(HTMLAttributes, { + 'data-type': 'variableTag', + class: 'variable-tag', + }), + displayText, + ]; + }, + + addCommands: () => ({ + insertVariableTag: + (variable: string) => + ({ commands }) => { + commands.insertContent?.({ + type: 'variableTag', + attrs: { variable }, + }); + + return true; + }, + }), +}); diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index f85046affd..76bd4bc2a4 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -223,6 +223,7 @@ export { IconUser, IconUserCircle, IconUsers, + IconVariable, IconVideo, IconWand, IconWorld, diff --git a/yarn.lock b/yarn.lock index 615c20e4f3..962484b69c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14861,6 +14861,18 @@ __metadata: languageName: node linkType: hard +"@tiptap/extension-bubble-menu@npm:^2.8.0": + version: 2.8.0 + resolution: "@tiptap/extension-bubble-menu@npm:2.8.0" + dependencies: + tippy.js: "npm:^6.3.7" + peerDependencies: + "@tiptap/core": ^2.7.0 + "@tiptap/pm": ^2.7.0 + checksum: 10c0/8c05bf1a1ea3a72c290e69f64b5e165e1af740a5b1434d8da2ab457def27793ece75680f5ab7c6c5f264d69be75a2f42c104acb07f4338fd55a70028cd8a4ad1 + languageName: node + linkType: hard + "@tiptap/extension-code@npm:^2.5.0": version: 2.5.9 resolution: "@tiptap/extension-code@npm:2.5.9" @@ -14891,6 +14903,15 @@ __metadata: languageName: node linkType: hard +"@tiptap/extension-document@npm:^2.9.0": + version: 2.9.0 + resolution: "@tiptap/extension-document@npm:2.9.0" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10c0/2cc551050f0d4507b0c8be93c2d17a11cb9649d9b667e9d0923d197ed686e16b7dedd9582538dd7e4d04c33a3ba91145809623fcda63cfdbc3ddf7f5066dca6e + languageName: node + linkType: hard + "@tiptap/extension-dropcursor@npm:^2.5.0": version: 2.5.9 resolution: "@tiptap/extension-dropcursor@npm:2.5.9" @@ -14913,6 +14934,18 @@ __metadata: languageName: node linkType: hard +"@tiptap/extension-floating-menu@npm:^2.8.0": + version: 2.8.0 + resolution: "@tiptap/extension-floating-menu@npm:2.8.0" + dependencies: + tippy.js: "npm:^6.3.7" + peerDependencies: + "@tiptap/core": ^2.7.0 + "@tiptap/pm": ^2.7.0 + checksum: 10c0/d9895b0c78d40dca295fe17bf2d3c1a181a2aeb1e9fec958ef7df8bac1fe59345f4f22a1bc3a5f7cfe54ff472c6ebea725c71b8db8f5082ec3e350e5da7f4a7d + languageName: node + linkType: hard + "@tiptap/extension-gapcursor@npm:^2.5.0": version: 2.5.9 resolution: "@tiptap/extension-gapcursor@npm:2.5.9" @@ -14982,6 +15015,25 @@ __metadata: languageName: node linkType: hard +"@tiptap/extension-paragraph@npm:^2.9.0": + version: 2.9.0 + resolution: "@tiptap/extension-paragraph@npm:2.9.0" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10c0/23c36c28d76356a139fd113119d17df11dacda03e9f5b926d623bb2c0267e14505a4ba9eaa674094d38a766535abefa14cd2542797ad44f313a53587bd8893e6 + languageName: node + linkType: hard + +"@tiptap/extension-placeholder@npm:^2.9.0": + version: 2.9.0 + resolution: "@tiptap/extension-placeholder@npm:2.9.0" + peerDependencies: + "@tiptap/core": ^2.7.0 + "@tiptap/pm": ^2.7.0 + checksum: 10c0/e8e978a50af1d89e302e3086990f48a1d2fd8754a178faa42444788a4208d72e6f09ccd529eaa37705c1e3dfd15ffd54d063f5cc023a3533dadb34e9babf1cec + languageName: node + linkType: hard + "@tiptap/extension-strike@npm:^2.5.0": version: 2.5.9 resolution: "@tiptap/extension-strike@npm:2.5.9" @@ -15018,6 +15070,15 @@ __metadata: languageName: node linkType: hard +"@tiptap/extension-text-style@npm:^2.8.0": + version: 2.8.0 + resolution: "@tiptap/extension-text-style@npm:2.8.0" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10c0/92abcb01139331aee8ed41170450ae6327017fe654b7e057394bbac2624a38351114de811f996b65a362fca6835015b160a32ea2a80efd175384b76f951ac181 + languageName: node + linkType: hard + "@tiptap/extension-text@npm:^2.5.0": version: 2.5.9 resolution: "@tiptap/extension-text@npm:2.5.9" @@ -15027,6 +15088,15 @@ __metadata: languageName: node linkType: hard +"@tiptap/extension-text@npm:^2.9.0": + version: 2.9.0 + resolution: "@tiptap/extension-text@npm:2.9.0" + peerDependencies: + "@tiptap/core": ^2.7.0 + checksum: 10c0/049a1ce42df566de647632461344414c59a52930cf6a530b987f51857df4373d41f83d8feea304f95a077617fd605b62503adc4cbcd28e688c564e24d4139391 + languageName: node + linkType: hard + "@tiptap/extension-underline@npm:^2.5.0": version: 2.5.9 resolution: "@tiptap/extension-underline@npm:2.5.9" @@ -15079,6 +15149,24 @@ __metadata: languageName: node linkType: hard +"@tiptap/react@npm:^2.8.0": + version: 2.8.0 + resolution: "@tiptap/react@npm:2.8.0" + dependencies: + "@tiptap/extension-bubble-menu": "npm:^2.8.0" + "@tiptap/extension-floating-menu": "npm:^2.8.0" + "@types/use-sync-external-store": "npm:^0.0.6" + fast-deep-equal: "npm:^3" + use-sync-external-store: "npm:^1.2.2" + peerDependencies: + "@tiptap/core": ^2.7.0 + "@tiptap/pm": ^2.7.0 + react: ^17.0.0 || ^18.0.0 + react-dom: ^17.0.0 || ^18.0.0 + checksum: 10c0/a925761dd9fa778fc7a3f32a502ee9874fa785c167ad6d37e2744d0c5b7d1e72bc0c7fafbf1c7f50f04a65d01d00435361a9aa2a44110d67836fbc43e8cd0f9e + languageName: node + linkType: hard + "@tokenizer/token@npm:^0.3.0": version: 0.3.0 resolution: "@tokenizer/token@npm:0.3.0" @@ -26521,7 +26609,7 @@ __metadata: languageName: node linkType: hard -"fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": +"fast-deep-equal@npm:^3, fast-deep-equal@npm:^3.1.1, fast-deep-equal@npm:^3.1.3": version: 3.1.3 resolution: "fast-deep-equal@npm:3.1.3" checksum: 10c0/40dedc862eb8992c54579c66d914635afbec43350afbbe991235fdcb4e3a8d5af1b23ae7e79bef7d4882d0ecee06c3197488026998fb19f72dc95acff1d1b1d0 @@ -44086,6 +44174,12 @@ __metadata: "@nivo/calendar": "npm:^0.87.0" "@nivo/core": "npm:^0.87.0" "@nivo/line": "npm:^0.87.0" + "@tiptap/extension-document": "npm:^2.9.0" + "@tiptap/extension-paragraph": "npm:^2.9.0" + "@tiptap/extension-placeholder": "npm:^2.9.0" + "@tiptap/extension-text": "npm:^2.9.0" + "@tiptap/extension-text-style": "npm:^2.8.0" + "@tiptap/react": "npm:^2.8.0" "@xyflow/react": "npm:^12.0.4" transliteration: "npm:^2.3.5" languageName: unknown From dd6b8be0d37be58f457da25b0f19b8e6ce25952c Mon Sep 17 00:00:00 2001 From: Harsh Singh Date: Wed, 23 Oct 2024 22:44:28 +0530 Subject: [PATCH 109/123] fix: date-picker flips in records cell even when there is enough space (#7905) Fixes: #7897 This PR fixes the flipping of the date-picker in the record's cell even if there is enough space below the table. I attached a screencast to show that it's working fine now. With this, it only flips when there is less space to accommodate the date-picker comp. Also, I tested this after adding lots of records to see if the scrolling behaviour is intact or not. And I found no issues, it's working as expected. [Screencast from 2024-10-21 13-39-42.webm](https://github.com/user-attachments/assets/615fac80-ae2e-4d26-8f94-55d7ee3f91c2) --- .../record-table-body/components/RecordTableBodyDroppable.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx index bd062d3abb..f9e7f5a9f8 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyDroppable.tsx @@ -8,8 +8,6 @@ import { v4 } from 'uuid'; const StyledTbody = styled.tbody<{ theme: Theme; }>` - overflow: hidden; - &.first-columns-sticky { td:nth-of-type(1) { position: sticky; From 6b3cd4dc14ceec34a2dd5323b0d1d8b631d21242 Mon Sep 17 00:00:00 2001 From: Bhavesh Mishra <69065938+thefool76@users.noreply.github.com> Date: Wed, 23 Oct 2024 23:56:06 +0530 Subject: [PATCH 110/123] Oss.gg A detailed Guide to self-host Twenty CRM on you local server (#8012) This Pr consist of a Blog which I wrote on hashnode in which I have explained in-dept how to setup/host twenty on your local server or cloud with troubleshooting guide. I have added output images as refers as well. check it out [Click here](https://k5lo7h.hashnode.dev/a-detailed-guide-to-self-host-twenty-crm-on-you-local-server) **Points 750** --- .../3-write-selfthost-guide-blog-post-20.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md b/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md index 57308fb4d3..e6dcaf145c 100644 --- a/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md +++ b/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md @@ -20,4 +20,6 @@ Your turn 👇 » 21-October-2024 by [sateshcharan](https://oss.gg/sateshcharan) blog Link: [blog](https://dev.to/sateshcharan/streamlined-self-hosting-with-twenty-crm-1-click-docker-compose-setup-188o) +» 23-October-2024 by [Thefool76](https://oss.gg/thefool76) blog Link: [blog](https://k5lo7h.hashnode.dev/a-detailed-guide-to-self-host-twenty-crm-on-you-local-server) + --- From d9429b1a831a15b764cb00f8d8064dfb3682757c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Wed, 23 Oct 2024 20:43:13 +0200 Subject: [PATCH 111/123] Delete unused file (#8014) Oops, this shouldn't have been merged, it was in the wrong directory --- Write-a-blog-post-about-Twenty | 2 -- 1 file changed, 2 deletions(-) delete mode 100644 Write-a-blog-post-about-Twenty diff --git a/Write-a-blog-post-about-Twenty b/Write-a-blog-post-about-Twenty deleted file mode 100644 index 86325c0af7..0000000000 --- a/Write-a-blog-post-about-Twenty +++ /dev/null @@ -1,2 +0,0 @@ -Description: -Shared my experience using Twenty in a detailed blog post From a35d888c12ebc916edf919e258491b6492ecde49 Mon Sep 17 00:00:00 2001 From: nitin <142569587+ehconitin@users.noreply.github.com> Date: Thu, 24 Oct 2024 01:03:35 +0530 Subject: [PATCH 112/123] Chip right height according to view (#7976) --- .../record-board-card/components/RecordBoardCard.tsx | 2 ++ .../meta-types/display/components/ChipFieldDisplay.tsx | 2 ++ .../record-index/components/RecordIndexRecordChip.tsx | 5 ++++- .../twenty-ui/src/display/chip/components/AvatarChip.tsx | 5 ++++- packages/twenty-ui/src/display/chip/components/Chip.tsx | 8 +++----- 5 files changed, 15 insertions(+), 7 deletions(-) diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx index 8e3c002055..dd746b8a74 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx @@ -30,6 +30,7 @@ import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { AnimatedEaseInOut, AvatarChipVariant, + ChipSize, IconEye, IconEyeOff, } from 'twenty-ui'; @@ -295,6 +296,7 @@ export const RecordBoardCard = ({ objectNameSingular={objectMetadataItem.nameSingular} record={record as ObjectRecord} variant={AvatarChipVariant.Transparent} + size={ChipSize.Large} /> )} diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ChipFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ChipFieldDisplay.tsx index 94360aa0f2..851460f5d2 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ChipFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/ChipFieldDisplay.tsx @@ -1,6 +1,7 @@ import { RecordChip } from '@/object-record/components/RecordChip'; import { useChipFieldDisplay } from '@/object-record/record-field/meta-types/hooks/useChipFieldDisplay'; import { RecordIdentifierChip } from '@/object-record/record-index/components/RecordIndexRecordChip'; +import { ChipSize } from 'twenty-ui'; export const ChipFieldDisplay = () => { const { recordValue, objectNameSingular, isLabelIdentifier } = @@ -14,6 +15,7 @@ export const ChipFieldDisplay = () => { ) : ( diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRecordChip.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRecordChip.tsx index 8e84fa8332..d883ce55f5 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRecordChip.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexRecordChip.tsx @@ -3,18 +3,20 @@ import { useRecordChipData } from '@/object-record/hooks/useRecordChipData'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { useContext } from 'react'; -import { AvatarChip, AvatarChipVariant } from 'twenty-ui'; +import { AvatarChip, AvatarChipVariant, ChipSize } from 'twenty-ui'; export type RecordIdentifierChipProps = { objectNameSingular: string; record: ObjectRecord; variant?: AvatarChipVariant; + size?: ChipSize; }; export const RecordIdentifierChip = ({ objectNameSingular, record, variant, + size, }: RecordIdentifierChipProps) => { const { onIndexIdentifierClick } = useContext(RecordIndexRootPropsContext); const { recordChipData } = useRecordChipData({ @@ -38,6 +40,7 @@ export const RecordIdentifierChip = ({ variant={variant} LeftIcon={LeftIcon} LeftIconColor={LeftIconColor} + size={size} /> ); }; diff --git a/packages/twenty-ui/src/display/chip/components/AvatarChip.tsx b/packages/twenty-ui/src/display/chip/components/AvatarChip.tsx index 72b0253eed..d71afa2f60 100644 --- a/packages/twenty-ui/src/display/chip/components/AvatarChip.tsx +++ b/packages/twenty-ui/src/display/chip/components/AvatarChip.tsx @@ -1,7 +1,7 @@ import { styled } from '@linaria/react'; import { Avatar } from '@ui/display/avatar/components/Avatar'; import { AvatarType } from '@ui/display/avatar/types/AvatarType'; -import { Chip, ChipVariant } from '@ui/display/chip/components/Chip'; +import { Chip, ChipSize, ChipVariant } from '@ui/display/chip/components/Chip'; import { IconComponent } from '@ui/display/icon/types/IconComponent'; import { ThemeContext } from '@ui/theme'; import { isDefined } from '@ui/utilities/isDefined'; @@ -13,6 +13,7 @@ export type AvatarChipProps = { avatarUrl?: string; avatarType?: Nullable; variant?: AvatarChipVariant; + size?: ChipSize; LeftIcon?: IconComponent; LeftIconColor?: string; isIconInverted?: boolean; @@ -47,6 +48,7 @@ export const AvatarChip = ({ className, placeholderColorSeed, onClick, + size = ChipSize.Small, }: AvatarChipProps) => { const { theme } = useContext(ThemeContext); @@ -60,6 +62,7 @@ export const AvatarChip = ({ : ChipVariant.Regular : ChipVariant.Transparent } + size={size} leftComponent={ isDefined(LeftIcon) ? ( isIconInverted === true ? ( diff --git a/packages/twenty-ui/src/display/chip/components/Chip.tsx b/packages/twenty-ui/src/display/chip/components/Chip.tsx index 3bf0cd9bb9..42afc99b51 100644 --- a/packages/twenty-ui/src/display/chip/components/Chip.tsx +++ b/packages/twenty-ui/src/display/chip/components/Chip.tsx @@ -66,7 +66,8 @@ const StyledContainer = withTheme(styled.div< display: inline-flex; justify-content: center; gap: ${({ theme }) => theme.spacing(1)}; - height: ${({ theme }) => theme.spacing(4)}; + height: ${({ theme, size }) => + size === ChipSize.Large ? theme.spacing(4) : theme.spacing(3)}; max-width: ${({ maxWidth }) => maxWidth ? `calc(${maxWidth}px - 2 * var(--chip-horizontal-padding))` @@ -141,10 +142,7 @@ export const Chip = ({ className={className} > {leftComponent} - + {rightComponent} ); From 1f84e61da8df0c40ede4481883f8426368330c82 Mon Sep 17 00:00:00 2001 From: Anis Hamal <73131641+AndrewHamal@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:39:22 +0545 Subject: [PATCH 113/123] Added "Select an option" as default none selector on Workflow Visualizer (#7867) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What does this PR do? Shows "Select an option" as a default selector on the select component for the trigger step in the workflow visualizer Fixes #7432 Screenshot 2024-10-20 at 12 48 39 AM Screenshot 2024-10-20 at 12 48 50 AM --------- Co-authored-by: Devessier --- .../src/modules/workflow/components/WorkflowEditTriggerForm.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerForm.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerForm.tsx index 0a2d8eeb87..6ff8d1a844 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerForm.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerForm.tsx @@ -114,6 +114,7 @@ export const WorkflowEditTriggerForm = ({ fullWidth disabled={readonly} value={triggerEvent?.objectType} + emptyOption={{ label: 'Select an option', value: '' }} options={availableMetadata} onChange={(updatedRecordType) => { if (readonly === true) { @@ -143,6 +144,7 @@ export const WorkflowEditTriggerForm = ({ label="Event type" fullWidth value={triggerEvent?.event} + emptyOption={{ label: 'Select an option', value: '' }} options={OBJECT_EVENT_TRIGGERS} disabled={readonly} onChange={(updatedEvent) => { From 60e44ccf73d60e7365ce0b6f7bddfb2693253d08 Mon Sep 17 00:00:00 2001 From: ZiaCodes <72739794+Khaan25@users.noreply.github.com> Date: Thu, 24 Oct 2024 14:48:02 +0500 Subject: [PATCH 114/123] feat: Self-hosting guide (750 points) (#8019) ![image](https://github.com/user-attachments/assets/73d3c515-2d12-4013-bd44-9ac759cf777f) --- .../3-write-selfthost-guide-blog-post-20.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md b/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md index e6dcaf145c..fb1682663c 100644 --- a/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md +++ b/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md @@ -22,4 +22,6 @@ Your turn 👇 » 23-October-2024 by [Thefool76](https://oss.gg/thefool76) blog Link: [blog](https://k5lo7h.hashnode.dev/a-detailed-guide-to-self-host-twenty-crm-on-you-local-server) +» 24-October-2024 by [Khaan25](https://oss.gg/Khaan25) blog Link: [blog](https://medium.com/@ziaurzai/detailed-guide-on-self-hosting-twenty-crm-on-your-server-troubleshooting-and-best-practices-1f2ca15cd6eb) + --- From 0a28c15747d58f7fcc2dfc860b4e216c8b604e8e Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:20:02 +0200 Subject: [PATCH 115/123] Migrate to twenty-ui - input/button (#7994) This PR was created by [GitStart](https://gitstart.com/) to address the requirements from this ticket: [TWNTY-7529](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-7529). --- ### Description - Migrated all button components to `twenty-ui` \ \ `Button`\ `ButtonGroup`\ `ColorPickerButton`\ `FloatingButton`\ `FloatingButtonGroup`\ `FloatingIconButton`\ `FloatingIconButtonGroup`\ `IconButton`\ `IconButtonGroup`\ `LightButton`\ `LightIconButton`\ `LightIconButtonGroup`\ `MainButton`\ \ Fixes twentyhq/private-issues#89 Co-authored-by: gitstart-twenty Co-authored-by: Charles Bochet --- .../blocks/components/FileBlock.tsx | 2 +- .../components/EmailThreadBottomBar.tsx | 4 +- .../components/IntermediaryMessages.tsx | 3 +- .../components/RightDrawerEmailThread.tsx | 3 +- .../files/components/AttachmentDropdown.tsx | 2 +- .../files/components/Attachments.tsx | 2 +- .../activities/notes/components/Notes.tsx | 15 ++++--- .../tasks/components/AddTaskButton.tsx | 3 +- .../tasks/components/TaskGroups.tsx | 5 +-- .../components/TimelineCreateButtonGroup.tsx | 13 ++++--- .../rows/components/EventCardToggleButton.tsx | 4 +- .../sign-in-up/components/SignInUpForm.tsx | 24 +++++++----- .../command-menu/components/CommandMenu.tsx | 10 ++++- .../components/GenericErrorFallback.tsx | 3 +- .../components/InformationBanner.tsx | 3 +- .../components/KeyboardShortcutMenuDialog.tsx | 4 +- .../AddObjectFilterFromDetailsButton.tsx | 3 +- .../components/RecordBoardCard.tsx | 2 +- .../components/RecordBoardColumnHeader.tsx | 3 +- .../components/LightCopyIconButton.tsx | 3 +- .../input/components/MultiItemFieldInput.tsx | 3 +- .../RecordIndexPageKanbanAddButton.tsx | 3 +- .../components/RecordInlineCellEditButton.tsx | 8 ++-- .../RecordDetailRelationRecordsListItem.tsx | 2 +- .../RecordDetailRelationSection.tsx | 3 +- .../RecordTableEmptyStateDisplay.tsx | 2 +- .../components/RecordTableCellButton.tsx | 8 ++-- .../components/RecordTableHeaderCell.tsx | 3 +- .../SettingsAccountsBlocklistInput.tsx | 6 +-- .../SettingsAccountsBlocklistTableRow.tsx | 4 +- .../SettingsAccountsListEmptyStateCard.tsx | 3 +- .../SettingsAccountsRowDropdownMenu.tsx | 2 +- .../SaveAndCancelButtons/CancelButton.tsx | 2 +- .../SaveAndCancelButtons/SaveButton.tsx | 4 +- ...ngsDataModelNewFieldBreadcrumbDropDown.tsx | 3 +- ...ttingsDataModelFieldNumberDecimalInput.tsx | 3 +- .../SettingsDataModelFieldSelectForm.tsx | 3 +- ...tingsDataModelFieldSelectFormOptionRow.tsx | 2 +- .../components/SettingsDataModelOverview.tsx | 15 +++---- ...ettingsObjectFieldActiveActionDropdown.tsx | 2 +- ...tingsObjectFieldDisabledActionDropdown.tsx | 2 +- .../SettingsObjectFieldItemTableRow.tsx | 32 ++++++++------- .../components/SettingsObjectSummaryCard.tsx | 9 ++++- .../components/SettingsObjectCoverImage.tsx | 3 +- .../SettingsObjectInactiveMenuDropDown.tsx | 8 +++- .../developers/components/ApiKeyInput.tsx | 3 +- .../SettingsReadDocumentationButton.tsx | 4 +- .../SettingsIntegrationComponent.tsx | 5 +-- ...ingsIntegrationRemoteTableSchemaUpdate.tsx | 3 +- ...tegrationDatabaseConnectionSummaryCard.tsx | 15 ++++--- ...IntegrationDatabaseConnectionsListCard.tsx | 5 +-- .../profile/components/ChangePassword.tsx | 3 +- .../profile/components/DeleteAccount.tsx | 3 +- .../profile/components/DeleteWorkspace.tsx | 3 +- ...OIdentitiesProvidersListEmptyStateCard.tsx | 6 +-- .../components/SettingsSSOOIDCForm.tsx | 3 +- .../components/SettingsSSOSAMLForm.tsx | 2 +- .../SettingsSecuritySSORowDropdownMenu.tsx | 8 +++- .../SettingsServerlessFunctionsTableEmpty.tsx | 2 +- ...ettingsServerlessFunctionCodeEditorTab.tsx | 9 ++++- .../SettingsServerlessFunctionSettingsTab.tsx | 3 +- ...FunctionTabEnvironmentVariableTableRow.tsx | 22 +++++------ ...FunctionTabEnvironmentVariablesSection.tsx | 29 ++++++++------ .../SettingsServerlessFunctionTestTab.tsx | 3 +- .../components/ModalCloseButton.tsx | 3 +- .../components/StepNavigationButton.tsx | 2 +- .../UploadStep/components/DropZone.tsx | 2 +- .../ValidationStep/ValidationStep.tsx | 39 +++++++++---------- .../support/components/SupportButton.tsx | 3 +- .../ui/display/info/components/Info.tsx | 7 ++-- .../dialog-manager/components/Dialog.tsx | 2 +- .../snack-bar-manager/components/SnackBar.tsx | 4 +- .../ui/input/components/AutosizeTextInput.tsx | 6 +-- .../ui/input/components/IconPicker.tsx | 11 ++++-- .../ui/input/components/ImageInput.tsx | 3 +- .../components/AbsoluteDatePickerHeader.tsx | 3 +- .../__stories__/BottomBar.stories.tsx | 6 +-- .../components/DropdownMenuHeader.tsx | 6 +-- .../__stories__/DropdownMenu.stories.tsx | 3 +- .../modal/components/ConfirmationModal.tsx | 3 +- .../layout/page/components/PageAddButton.tsx | 4 +- .../page/components/PageFavoriteButton.tsx | 4 +- .../ui/layout/page/components/PageHeader.tsx | 2 +- .../RightDrawerTopBarCloseButton.tsx | 4 +- .../RightDrawerTopBarExpandButton.tsx | 3 +- .../RightDrawerTopBarMinimizeButton.tsx | 3 +- .../components/ShowPageAddButton.tsx | 3 +- .../components/ShowPageMoreButton.tsx | 8 +++- .../menu-item/components/MenuItem.tsx | 10 +++-- .../menu-item/components/MenuItemAvatar.tsx | 7 ++-- .../components/MenuItemDraggable.tsx | 4 +- .../NavigationDrawerCollapseButton.tsx | 2 +- .../components/UpdateViewButtonGroup.tsx | 4 +- .../components/ViewPickerCreateButton.tsx | 2 +- .../components/ViewPickerEditButton.tsx | 2 +- .../RecordShowPageWorkflowHeader.tsx | 2 +- .../RecordShowPageWorkflowVersionHeader.tsx | 9 ++++- .../WorkflowDiagramCreateStepNode.tsx | 3 +- .../WorkflowDiagramStepNodeEditable.tsx | 3 +- .../components/WorkspaceInviteLink.tsx | 3 +- .../components/WorkspaceInviteTeam.tsx | 3 +- .../twenty-front/src/pages/auth/Authorize.tsx | 6 +-- .../twenty-front/src/pages/auth/Invite.tsx | 10 ++--- .../src/pages/auth/PasswordReset.tsx | 26 ++++++------- .../src/pages/auth/SSOWorkspaceSelection.tsx | 2 +- .../src/pages/not-found/NotFound.tsx | 6 +-- .../src/pages/onboarding/ChooseYourPlan.tsx | 12 +++--- .../src/pages/onboarding/CreateProfile.tsx | 7 ++-- .../src/pages/onboarding/CreateWorkspace.tsx | 7 ++-- .../src/pages/onboarding/InviteTeam.tsx | 33 +++++++++------- .../src/pages/onboarding/PaymentSuccess.tsx | 16 +++++--- .../src/pages/onboarding/SyncEmails.tsx | 15 ++++--- .../src/pages/settings/SettingsBilling.tsx | 2 +- .../settings/SettingsWorkspaceMembers.tsx | 31 +++++++-------- .../SettingsObjectDetailPageContent.tsx | 3 +- .../data-model/SettingsObjectEdit.tsx | 16 ++++---- .../data-model/SettingsObjectFieldEdit.tsx | 20 +++++----- .../settings/data-model/SettingsObjects.tsx | 21 +++++----- .../developers/SettingsDevelopers.tsx | 8 ++-- .../SettingsDevelopersApiKeyDetail.tsx | 3 +- .../SettingsDevelopersWebhookDetail.tsx | 4 +- .../SettingsServerlessFunctions.tsx | 3 +- packages/twenty-front/tsup.ui.index.tsx | 10 ----- .../src}/input/button/components/Button.tsx | 3 +- .../input/button/components/ButtonGroup.tsx | 4 +- .../button/components/ColorPickerButton.tsx | 5 +-- .../button/components/FloatingButton.tsx | 2 +- .../button/components/FloatingButtonGroup.tsx | 4 +- .../button/components/FloatingIconButton.tsx | 2 +- .../components/FloatingIconButtonGroup.tsx | 4 +- .../input/button/components/IconButton.tsx | 2 +- .../button/components/IconButtonGroup.tsx | 4 +- .../input/button/components/LightButton.tsx | 4 +- .../button/components/LightIconButton.tsx | 4 +- .../components/LightIconButtonGroup.tsx | 4 +- .../input/button/components/MainButton.tsx | 2 +- .../button/components/RoundedIconButton.tsx | 2 +- .../components/__stories__/Button.docs.mdx | 0 .../components/__stories__/Button.stories.tsx | 5 +-- .../__stories__/ButtonGroup.stories.tsx | 7 +--- .../__stories__/ColorPickerButton.stories.tsx | 3 +- .../__stories__/FloatingButton.stories.tsx | 5 +-- .../FloatingButtonGroup.stories.tsx | 7 +--- .../FloatingIconButton.stories.tsx | 5 +-- .../FloatingIconButtonGroup.stories.tsx | 7 +--- .../__stories__/IconButton.stories.tsx | 5 +-- .../__stories__/IconButtonGroup.stories.tsx | 7 +--- .../__stories__/LightButton.stories.tsx | 5 +-- .../__stories__/LightIconButton.stories.tsx | 5 +-- .../__stories__/MainButton.stories.tsx | 4 +- .../__stories__/RoundedIconButton.stories.tsx | 4 +- packages/twenty-ui/src/input/index.ts | 14 +++++++ 152 files changed, 450 insertions(+), 505 deletions(-) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/Button.tsx (99%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/ButtonGroup.tsx (96%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/ColorPickerButton.tsx (88%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/FloatingButton.tsx (98%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/FloatingButtonGroup.tsx (96%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/FloatingIconButton.tsx (98%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/FloatingIconButtonGroup.tsx (97%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/IconButton.tsx (99%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/IconButtonGroup.tsx (96%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/LightButton.tsx (98%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/LightIconButton.tsx (98%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/LightIconButtonGroup.tsx (96%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/MainButton.tsx (98%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/RoundedIconButton.tsx (95%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/__stories__/Button.docs.mdx (100%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/__stories__/Button.stories.tsx (99%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/__stories__/ButtonGroup.stories.tsx (95%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/__stories__/ColorPickerButton.stories.tsx (90%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/__stories__/FloatingButton.stories.tsx (96%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/__stories__/FloatingButtonGroup.stories.tsx (93%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/__stories__/FloatingIconButton.stories.tsx (96%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/__stories__/FloatingIconButtonGroup.stories.tsx (92%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/__stories__/IconButton.stories.tsx (98%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/__stories__/IconButtonGroup.stories.tsx (94%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/__stories__/LightButton.stories.tsx (97%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/__stories__/LightIconButton.stories.tsx (97%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/__stories__/MainButton.stories.tsx (93%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/button/components/__stories__/RoundedIconButton.stories.tsx (89%) diff --git a/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx b/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx index d1d308dcd4..5c8de03869 100644 --- a/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx +++ b/packages/twenty-front/src/modules/activities/blocks/components/FileBlock.tsx @@ -3,8 +3,8 @@ import styled from '@emotion/styled'; import { isNonEmptyString } from '@sniptt/guards'; import { ChangeEvent, useRef } from 'react'; -import { Button } from '@/ui/input/button/components/Button'; import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; +import { Button } from 'twenty-ui'; import { isDefined } from '~/utils/isDefined'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadBottomBar.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadBottomBar.tsx index c010da2cd1..902039f452 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadBottomBar.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadBottomBar.tsx @@ -1,7 +1,5 @@ import styled from '@emotion/styled'; -import { IconArrowBackUp, IconUserCircle } from 'twenty-ui'; - -import { Button } from '@/ui/input/button/components/Button'; +import { Button, IconArrowBackUp, IconUserCircle } from 'twenty-ui'; const StyledThreadBottomBar = styled.div` align-items: center; diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/components/IntermediaryMessages.tsx b/packages/twenty-front/src/modules/activities/emails/right-drawer/components/IntermediaryMessages.tsx index 3ef96d5731..aee31f1e1b 100644 --- a/packages/twenty-front/src/modules/activities/emails/right-drawer/components/IntermediaryMessages.tsx +++ b/packages/twenty-front/src/modules/activities/emails/right-drawer/components/IntermediaryMessages.tsx @@ -1,10 +1,9 @@ import styled from '@emotion/styled'; import { useState } from 'react'; -import { IconArrowsVertical } from 'twenty-ui'; +import { Button, IconArrowsVertical } from 'twenty-ui'; import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMessage'; import { EmailThreadMessageWithSender } from '@/activities/emails/types/EmailThreadMessageWithSender'; -import { Button } from '@/ui/input/button/components/Button'; const StyledButtonContainer = styled.div` border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx b/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx index 42367f2f9a..1ac7fb6519 100644 --- a/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx +++ b/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx @@ -9,12 +9,11 @@ import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMe import { IntermediaryMessages } from '@/activities/emails/right-drawer/components/IntermediaryMessages'; import { useRightDrawerEmailThread } from '@/activities/emails/right-drawer/hooks/useRightDrawerEmailThread'; import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/states/lastViewableEmailThreadIdState'; -import { Button } from '@/ui/input/button/components/Button'; import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener'; import { messageThreadState } from '@/ui/layout/right-drawer/states/messageThreadState'; import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; -import { IconArrowBackUp } from 'twenty-ui'; +import { Button, IconArrowBackUp } from 'twenty-ui'; const StyledWrapper = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/activities/files/components/AttachmentDropdown.tsx b/packages/twenty-front/src/modules/activities/files/components/AttachmentDropdown.tsx index 7dc80b1717..a52aa4c910 100644 --- a/packages/twenty-front/src/modules/activities/files/components/AttachmentDropdown.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/AttachmentDropdown.tsx @@ -3,9 +3,9 @@ import { IconDownload, IconPencil, IconTrash, + LightIconButton, } from 'twenty-ui'; -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'; diff --git a/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx b/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx index 0ab90a7e0a..1cc6de187a 100644 --- a/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/Attachments.tsx @@ -6,6 +6,7 @@ import { AnimatedPlaceholderEmptySubTitle, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, + Button, EMPTY_PLACEHOLDER_TRANSITION_PROPS, IconPlus, } from 'twenty-ui'; @@ -16,7 +17,6 @@ import { DropZone } from '@/activities/files/components/DropZone'; import { useAttachments } from '@/activities/files/hooks/useAttachments'; import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttachmentFile'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { Button } from '@/ui/input/button/components/Button'; import { isDefined } from '~/utils/isDefined'; const StyledAttachmentsContainer = styled.div` diff --git a/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx b/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx index de4e6cccca..c99a8e57c5 100644 --- a/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx +++ b/packages/twenty-front/src/modules/activities/notes/components/Notes.tsx @@ -1,3 +1,9 @@ +import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; +import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; +import { NoteList } from '@/activities/notes/components/NoteList'; +import { useNotes } from '@/activities/notes/hooks/useNotes'; +import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import styled from '@emotion/styled'; import { AnimatedPlaceholder, @@ -5,18 +11,11 @@ import { AnimatedPlaceholderEmptySubTitle, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, + Button, EMPTY_PLACEHOLDER_TRANSITION_PROPS, IconPlus, } from 'twenty-ui'; -import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; -import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; -import { NoteList } from '@/activities/notes/components/NoteList'; -import { useNotes } from '@/activities/notes/hooks/useNotes'; -import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { Button } from '@/ui/input/button/components/Button'; - const StyledNotesContainer = styled.div` display: flex; flex: 1; diff --git a/packages/twenty-front/src/modules/activities/tasks/components/AddTaskButton.tsx b/packages/twenty-front/src/modules/activities/tasks/components/AddTaskButton.tsx index 869e2f1ade..0004f351d6 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/AddTaskButton.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/AddTaskButton.tsx @@ -1,10 +1,9 @@ import { isNonEmptyArray } from '@sniptt/guards'; -import { IconPlus } from 'twenty-ui'; +import { Button, IconPlus } from 'twenty-ui'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { Button } from '@/ui/input/button/components/Button'; export const AddTaskButton = ({ activityTargetableObjects, diff --git a/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx index 03393d5762..b32d17ed6f 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx @@ -6,6 +6,7 @@ import { AnimatedPlaceholderEmptySubTitle, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, + Button, EMPTY_PLACEHOLDER_TRANSITION_PROPS, IconPlus, } from 'twenty-ui'; @@ -15,11 +16,9 @@ import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateAct import { TASKS_TAB_LIST_COMPONENT_ID } from '@/activities/tasks/constants/TasksTabListComponentId'; import { useTasks } from '@/activities/tasks/hooks/useTasks'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { Button } from '@/ui/input/button/components/Button'; -import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; - import { Task } from '@/activities/types/Task'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; import groupBy from 'lodash.groupby'; import { AddTaskButton } from './AddTaskButton'; import { TaskList } from './TaskList'; diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineCreateButtonGroup.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineCreateButtonGroup.tsx index 4e8ec1c647..e5bc090bc2 100644 --- a/packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineCreateButtonGroup.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/components/TimelineCreateButtonGroup.tsx @@ -1,10 +1,13 @@ -import { useSetRecoilState } from 'recoil'; -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/ShowPageSubContainer'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; +import { useSetRecoilState } from 'recoil'; +import { + Button, + ButtonGroup, + IconCheckbox, + IconNotes, + IconPaperclip, +} from 'twenty-ui'; export const TimelineCreateButtonGroup = ({ isInRightDrawer = false, diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventCardToggleButton.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventCardToggleButton.tsx index 44a2cb4066..2e0795845d 100644 --- a/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventCardToggleButton.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventCardToggleButton.tsx @@ -1,7 +1,5 @@ import styled from '@emotion/styled'; -import { IconChevronDown, IconChevronUp } from 'twenty-ui'; - -import { IconButton } from '@/ui/input/button/components/IconButton'; +import { IconButton, IconChevronDown, IconChevronUp } from 'twenty-ui'; type EventCardToggleButtonProps = { isOpen: boolean; diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx index ba4add2976..38856d33d5 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx +++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx @@ -1,12 +1,3 @@ -import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { motion } from 'framer-motion'; -import { useMemo, useState } from 'react'; -import { Controller } from 'react-hook-form'; -import { useRecoilState, useRecoilValue } from 'recoil'; -import { Key } from 'ts-key-enum'; -import { ActionLink, IconGoogle, IconKey, IconMicrosoft } from 'twenty-ui'; - import { FooterNote } from '@/auth/sign-in-up/components/FooterNote'; import { HorizontalSeparator } from '@/auth/sign-in-up/components/HorizontalSeparator'; import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword'; @@ -19,8 +10,21 @@ import { isRequestingCaptchaTokenState } from '@/captcha/states/isRequestingCapt import { authProvidersState } from '@/client-config/states/authProvidersState'; import { captchaProviderState } from '@/client-config/states/captchaProviderState'; import { Loader } from '@/ui/feedback/loader/components/Loader'; -import { MainButton } from '@/ui/input/button/components/MainButton'; import { TextInput } from '@/ui/input/components/TextInput'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { motion } from 'framer-motion'; +import { useMemo, useState } from 'react'; +import { Controller } from 'react-hook-form'; +import { useRecoilState, useRecoilValue } from 'recoil'; +import { Key } from 'ts-key-enum'; +import { + ActionLink, + IconGoogle, + IconKey, + IconMicrosoft, + MainButton, +} from 'twenty-ui'; import { isDefined } from '~/utils/isDefined'; const StyledContentContainer = styled.div` diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx index 2ad134409c..02f6365827 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx @@ -16,7 +16,6 @@ import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainNa import { useSearchRecords } from '@/object-record/hooks/useSearchRecords'; import { Opportunity } from '@/opportunities/types/Opportunity'; import { Person } from '@/people/types/Person'; -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; @@ -30,7 +29,14 @@ import { isNonEmptyString } from '@sniptt/guards'; import { useMemo, useRef } from 'react'; import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil'; import { Key } from 'ts-key-enum'; -import { Avatar, IconNotes, IconSparkles, IconX, isDefined } from 'twenty-ui'; +import { + Avatar, + IconNotes, + IconSparkles, + IconX, + LightIconButton, + isDefined, +} from 'twenty-ui'; import { useDebounce } from 'use-debounce'; import { getLogoUrlFromDomainName } from '~/utils'; diff --git a/packages/twenty-front/src/modules/error-handler/components/GenericErrorFallback.tsx b/packages/twenty-front/src/modules/error-handler/components/GenericErrorFallback.tsx index 9481fbcc4c..9fcb323368 100644 --- a/packages/twenty-front/src/modules/error-handler/components/GenericErrorFallback.tsx +++ b/packages/twenty-front/src/modules/error-handler/components/GenericErrorFallback.tsx @@ -9,11 +9,10 @@ import { AnimatedPlaceholderEmptySubTitle, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, + Button, IconRefresh, THEME_LIGHT, } from 'twenty-ui'; - -import { Button } from '@/ui/input/button/components/Button'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; type GenericErrorFallbackProps = FallbackProps; diff --git a/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx b/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx index 39d2437bf6..2c951b2e0b 100644 --- a/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx +++ b/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx @@ -1,6 +1,5 @@ -import { Button } from '@/ui/input/button/components/Button'; import styled from '@emotion/styled'; -import { Banner, BannerVariant, IconComponent } from 'twenty-ui'; +import { Banner, BannerVariant, Button, IconComponent } from 'twenty-ui'; const StyledBanner = styled(Banner)` position: absolute; diff --git a/packages/twenty-front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuDialog.tsx b/packages/twenty-front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuDialog.tsx index 80cc5c6b51..36c800f31e 100644 --- a/packages/twenty-front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuDialog.tsx +++ b/packages/twenty-front/src/modules/keyboard-shortcut-menu/components/KeyboardShortcutMenuDialog.tsx @@ -1,6 +1,4 @@ -import { IconX } from 'twenty-ui'; - -import { IconButton } from '@/ui/input/button/components/IconButton'; +import { IconButton, IconX } from 'twenty-ui'; import { StyledContainer, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton.tsx index 1fb257f79b..21d4074714 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/AddObjectFilterFromDetailsButton.tsx @@ -1,8 +1,7 @@ -import { IconPlus } from 'twenty-ui'; +import { IconPlus, LightButton } from 'twenty-ui'; import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; -import { LightButton } from '@/ui/input/button/components/LightButton'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; type AddObjectFilterFromDetailsButtonProps = { diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx index dd746b8a74..648fc221f6 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx @@ -17,7 +17,6 @@ import { InlineCellHotkeyScope } from '@/object-record/record-inline-cell/types/ import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect'; import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { Checkbox, CheckboxVariant } from '@/ui/input/components/Checkbox'; import { TextInput } from '@/ui/input/components/TextInput'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; @@ -33,6 +32,7 @@ import { ChipSize, IconEye, IconEyeOff, + LightIconButton, } from 'twenty-ui'; import { useDebouncedCallback } from 'use-debounce'; import { useAddNewCard } from '../../record-board-column/hooks/useAddNewCard'; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx index a36d73cd2f..1f25864dd6 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeader.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; import { useContext, useState } from 'react'; -import { IconDotsVertical, IconPlus, Tag } from 'twenty-ui'; +import { IconDotsVertical, IconPlus, LightIconButton, Tag } from 'twenty-ui'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; @@ -12,7 +12,6 @@ import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-b import { RecordBoardColumnHotkeyScope } from '@/object-record/record-board/types/BoardColumnHotkeyScope'; import { RecordBoardColumnDefinitionType } from '@/object-record/record-board/types/RecordBoardColumnDefinition'; import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect'; -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; const StyledHeader = styled.div` diff --git a/packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx b/packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx index 61b6b0fb69..79ee5f2222 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/components/LightCopyIconButton.tsx @@ -1,10 +1,9 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconCopy } from 'twenty-ui'; +import { IconCopy, LightIconButton } from 'twenty-ui'; 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'; const StyledButtonContainer = styled.div` padding: 0 ${({ theme }) => theme.spacing(1)}; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx index e416e50ce8..7aa56379fd 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx @@ -1,10 +1,9 @@ import styled from '@emotion/styled'; import React, { useRef, useState } from 'react'; import { Key } from 'ts-key-enum'; -import { IconCheck, IconPlus } from 'twenty-ui'; +import { IconCheck, IconPlus, LightIconButton } from 'twenty-ui'; import { PhoneRecord } from '@/object-record/record-field/types/FieldMetadata'; -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenuInput, diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx index 4c3826fa8b..3b80799091 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexPageKanbanAddButton.tsx @@ -7,7 +7,6 @@ import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/ import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; -import { IconButton } from '@/ui/input/button/components/IconButton'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; @@ -15,7 +14,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import styled from '@emotion/styled'; import { useCallback, useContext } from 'react'; import { useRecoilValue } from 'recoil'; -import { IconPlus } from 'twenty-ui'; +import { IconButton, IconPlus } from 'twenty-ui'; const StyledDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)` width: 100%; diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx index c14742e8e0..92bfdb84b3 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditButton.tsx @@ -1,7 +1,9 @@ import styled from '@emotion/styled'; -import { AnimatedContainer, IconComponent } from 'twenty-ui'; - -import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; +import { + AnimatedContainer, + FloatingIconButton, + IconComponent, +} from 'twenty-ui'; const StyledInlineCellButtonContainer = styled.div` align-items: center; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx index 1ed6c37fa4..b1f16b08cd 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationRecordsListItem.tsx @@ -9,6 +9,7 @@ import { IconDotsVertical, IconTrash, IconUnlink, + LightIconButton, } from 'twenty-ui'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; @@ -33,7 +34,6 @@ import { RecordDetailRecordsListItem } from '@/object-record/record-show/record- import { RecordValueSetterEffect } from '@/object-record/record-store/components/RecordValueSetterEffect'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { isFieldCellSupported } from '@/object-record/utils/isFieldCellSupported'; -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx index 432ff14fe5..0281cf306f 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/RecordDetailRelationSection.tsx @@ -2,7 +2,7 @@ import styled from '@emotion/styled'; import qs from 'qs'; import { useCallback, useContext } from 'react'; import { useRecoilValue } from 'recoil'; -import { IconForbid, IconPencil, IconPlus } from 'twenty-ui'; +import { IconForbid, IconPencil, IconPlus, LightIconButton } from 'twenty-ui'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; @@ -26,7 +26,6 @@ import { EntityForSelect } from '@/object-record/relation-picker/types/EntityFor import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { DropdownScope } from '@/ui/layout/dropdown/scopes/DropdownScope'; diff --git a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx index 66affe2a28..1d97ed9f46 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/empty-state/components/RecordTableEmptyStateDisplay.tsx @@ -1,6 +1,5 @@ import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly'; import { RecordTableContext } from '@/object-record/record-table/contexts/RecordTableContext'; -import { Button } from '@/ui/input/button/components/Button'; import { useContext } from 'react'; import { AnimatedPlaceholder, @@ -9,6 +8,7 @@ import { AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, AnimatedPlaceholderType, + Button, IconComponent, } from 'twenty-ui'; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx index 957aa74843..101c8cb658 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellButton.tsx @@ -1,7 +1,9 @@ import styled from '@emotion/styled'; -import { AnimatedContainer, IconComponent } from 'twenty-ui'; - -import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; +import { + AnimatedContainer, + FloatingIconButton, + IconComponent, +} from 'twenty-ui'; const StyledButtonContainer = styled.div` margin: ${({ theme }) => theme.spacing(1)}; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx index 66cbddd5ed..393dfd5f00 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import { useCallback, useMemo, useState } from 'react'; import { useRecoilCallback, useRecoilState, useRecoilValue } from 'recoil'; -import { IconPlus } from 'twenty-ui'; +import { IconPlus, LightIconButton } from 'twenty-ui'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { isObjectMetadataReadOnly } from '@/object-metadata/utils/isObjectMetadataReadOnly'; @@ -12,7 +12,6 @@ import { useTableColumns } from '@/object-record/record-table/hooks/useTableColu import { RecordTableColumnHeadWithDropdown } from '@/object-record/record-table/record-table-header/components/RecordTableColumnHeadWithDropdown'; import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { useTrackPointer } from '@/ui/utilities/pointer-event/hooks/useTrackPointer'; import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistInput.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistInput.tsx index 9bb4fdd98c..b082cfc08d 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistInput.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistInput.tsx @@ -1,12 +1,12 @@ -import { useEffect } from 'react'; -import { Controller, useForm } from 'react-hook-form'; import styled from '@emotion/styled'; import { zodResolver } from '@hookform/resolvers/zod'; +import { useEffect } from 'react'; +import { Controller, useForm } from 'react-hook-form'; import { Key } from 'ts-key-enum'; import { z } from 'zod'; -import { Button } from '@/ui/input/button/components/Button'; import { TextInput } from '@/ui/input/components/TextInput'; +import { Button } from 'twenty-ui'; import { isDomain } from '~/utils/is-domain'; const StyledContainer = styled.div` 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 30cf3a37a3..cb6966a1cc 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTableRow.tsx @@ -1,9 +1,7 @@ -import { IconX, OverflowingTextWithTooltip } from 'twenty-ui'; - import { BlocklistItem } from '@/accounts/types/BlocklistItem'; -import { IconButton } from '@/ui/input/button/components/IconButton'; import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableRow } from '@/ui/layout/table/components/TableRow'; +import { IconButton, IconX, OverflowingTextWithTooltip } from 'twenty-ui'; import { formatToHumanReadableDate } from '~/utils/date-utils'; type SettingsAccountsBlocklistTableRowProps = { diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx index 6178caf681..981bbcb6d5 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx @@ -1,8 +1,7 @@ import styled from '@emotion/styled'; -import { IconGoogle } from 'twenty-ui'; +import { Button, IconGoogle } from 'twenty-ui'; import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth'; -import { Button } from '@/ui/input/button/components/Button'; import { Card } from '@/ui/layout/card/components/Card'; import { CardContent } from '@/ui/layout/card/components/CardContent'; import { CardHeader } from '@/ui/layout/card/components/CardHeader'; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRowDropdownMenu.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRowDropdownMenu.tsx index beba37f8b4..743d191a6d 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRowDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsRowDropdownMenu.tsx @@ -5,13 +5,13 @@ import { IconMail, IconRefresh, IconTrash, + LightIconButton, } from 'twenty-ui'; import { ConnectedAccount } from '@/accounts/types/ConnectedAccount'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord'; import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth'; -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'; diff --git a/packages/twenty-front/src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx b/packages/twenty-front/src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx index e93d27a836..8226db8c65 100644 --- a/packages/twenty-front/src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx +++ b/packages/twenty-front/src/modules/settings/components/SaveAndCancelButtons/CancelButton.tsx @@ -1,4 +1,4 @@ -import { LightButton } from '@/ui/input/button/components/LightButton'; +import { LightButton } from 'twenty-ui'; type CancelButtonProps = { onCancel?: () => void; diff --git a/packages/twenty-front/src/modules/settings/components/SaveAndCancelButtons/SaveButton.tsx b/packages/twenty-front/src/modules/settings/components/SaveAndCancelButtons/SaveButton.tsx index 01913ba96a..a3553e5f01 100644 --- a/packages/twenty-front/src/modules/settings/components/SaveAndCancelButtons/SaveButton.tsx +++ b/packages/twenty-front/src/modules/settings/components/SaveAndCancelButtons/SaveButton.tsx @@ -1,6 +1,4 @@ -import { IconDeviceFloppy } from 'twenty-ui'; - -import { Button } from '@/ui/input/button/components/Button'; +import { Button, IconDeviceFloppy } from 'twenty-ui'; type SaveButtonProps = { onSave?: () => void; diff --git a/packages/twenty-front/src/modules/settings/data-model/components/SettingsDataModelNewFieldBreadcrumbDropDown.tsx b/packages/twenty-front/src/modules/settings/data-model/components/SettingsDataModelNewFieldBreadcrumbDropDown.tsx index ce41c5f563..4bdf2ee860 100644 --- a/packages/twenty-front/src/modules/settings/data-model/components/SettingsDataModelNewFieldBreadcrumbDropDown.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/components/SettingsDataModelNewFieldBreadcrumbDropDown.tsx @@ -1,5 +1,4 @@ import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; -import { Button } from '@/ui/input/button/components/Button'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; @@ -13,7 +12,7 @@ import { useParams, useSearchParams, } from 'react-router-dom'; -import { IconChevronDown, isDefined } from 'twenty-ui'; +import { Button, IconChevronDown, isDefined } from 'twenty-ui'; const StyledContainer = styled.div` align-items: center; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberDecimalInput.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberDecimalInput.tsx index 706bcea371..ce67a9bda9 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberDecimalInput.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberDecimalInput.tsx @@ -1,8 +1,7 @@ import styled from '@emotion/styled'; -import { Button } from '@/ui/input/button/components/Button'; import { TextInput } from '@/ui/input/components/TextInput'; -import { IconInfoCircle, IconMinus, IconPlus } from 'twenty-ui'; +import { Button, IconInfoCircle, IconMinus, IconPlus } from 'twenty-ui'; import { castAsNumberOrNull } from '~/utils/cast-as-number-or-null'; type SettingsDataModelFieldNumberDecimalsInputProps = { diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm.tsx index 038c2a1436..f78a23b45e 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectForm.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import { DropResult } from '@hello-pangea/dnd'; import { Controller, useFormContext } from 'react-hook-form'; -import { IconPlus, IconTool, MAIN_COLORS } from 'twenty-ui'; +import { IconPlus, IconTool, LightButton, MAIN_COLORS } from 'twenty-ui'; import { z } from 'zod'; import { @@ -14,7 +14,6 @@ import { selectFieldDefaultValueSchema } from '@/object-record/record-field/vali import { useSelectSettingsFormInitialValues } from '@/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues'; import { generateNewSelectOption } from '@/settings/data-model/fields/forms/select/utils/generateNewSelectOption'; import { isSelectOptionDefaultValue } from '@/settings/data-model/utils/isSelectOptionDefaultValue'; -import { LightButton } from '@/ui/input/button/components/LightButton'; import { CardContent } from '@/ui/layout/card/components/CardContent'; import { CardFooter } from '@/ui/layout/card/components/CardFooter'; import { DraggableItem } from '@/ui/layout/draggable-list/components/DraggableItem'; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectFormOptionRow.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectFormOptionRow.tsx index d0c74e805c..e41c012c1d 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectFormOptionRow.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/select/components/SettingsDataModelFieldSelectFormOptionRow.tsx @@ -8,6 +8,7 @@ import { IconGripVertical, IconTrash, IconX, + LightIconButton, MAIN_COLOR_NAMES, } from 'twenty-ui'; import { v4 } from 'uuid'; @@ -16,7 +17,6 @@ import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataIt import { EXPANDED_WIDTH_ANIMATION_VARIANTS } from '@/settings/constants/ExpandedWidthAnimationVariants'; import { OPTION_VALUE_MAXIMUM_LENGTH } from '@/settings/data-model/constants/OptionValueMaximumLength'; import { getOptionValueFromLabel } from '@/settings/data-model/fields/forms/select/utils/getOptionValueFromLabel'; -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { TextInput } from '@/ui/input/components/TextInput'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; 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 42f4fabd50..341fdb7d72 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,3 +1,7 @@ +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/utils/calculateHandlePosition'; import styled from '@emotion/styled'; import { useCallback, useState } from 'react'; import ReactFlow, { @@ -13,6 +17,8 @@ import ReactFlow, { useReactFlow, } from 'reactflow'; import { + Button, + IconButtonGroup, IconLock, IconLockOpen, IconMaximize, @@ -20,17 +26,8 @@ import { IconPlus, IconX, } from 'twenty-ui'; - -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/utils/calculateHandlePosition'; -import { Button } from '@/ui/input/button/components/Button'; -import { IconButtonGroup } from '@/ui/input/button/components/IconButtonGroup'; import { isDefined } from '~/utils/isDefined'; -import 'reactflow/dist/style.css'; - const NodeTypes = { object: SettingsDataModelOverviewObject, }; diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldActiveActionDropdown.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldActiveActionDropdown.tsx index 79e2cabb61..6851987691 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldActiveActionDropdown.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldActiveActionDropdown.tsx @@ -4,9 +4,9 @@ import { IconEye, IconPencil, IconTextSize, + LightIconButton, } from 'twenty-ui'; -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'; diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDisabledActionDropdown.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDisabledActionDropdown.tsx index a6b68c6b5a..0dcd5be86c 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDisabledActionDropdown.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDisabledActionDropdown.tsx @@ -4,9 +4,9 @@ import { IconEye, IconPencil, IconTrash, + LightIconButton, } from 'twenty-ui'; -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'; diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx index 6052b9f23e..0460643109 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldItemTableRow.tsx @@ -1,22 +1,11 @@ -import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { useMemo } from 'react'; -import { IconMinus, IconPlus, isDefined, useIcons } from 'twenty-ui'; - -import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; -import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; -import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings'; -import { TableCell } from '@/ui/layout/table/components/TableCell'; -import { TableRow } from '@/ui/layout/table/components/TableRow'; - -import { RELATION_TYPES } from '../../constants/RelationTypes'; - import { LABEL_IDENTIFIER_FIELD_METADATA_TYPES } from '@/object-metadata/constants/LabelIdentifierFieldMetadataTypes'; import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; +import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { getFieldSlug } from '@/object-metadata/utils/getFieldSlug'; +import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; import { isLabelIdentifierField } from '@/object-metadata/utils/isLabelIdentifierField'; import { useDeleteRecordFromCache } from '@/object-record/cache/hooks/useDeleteRecordFromCache'; import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; @@ -24,13 +13,26 @@ import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; import { SettingsObjectFieldActiveActionDropdown } from '@/settings/data-model/object-details/components/SettingsObjectFieldActiveActionDropdown'; import { SettingsObjectFieldInactiveActionDropdown } from '@/settings/data-model/object-details/components/SettingsObjectFieldDisabledActionDropdown'; import { settingsObjectFieldsFamilyState } from '@/settings/data-model/object-details/states/settingsObjectFieldsFamilyState'; -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; +import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings'; +import { TableCell } from '@/ui/layout/table/components/TableCell'; +import { TableRow } from '@/ui/layout/table/components/TableRow'; import { navigationMemorizedUrlState } from '@/ui/navigation/states/navigationMemorizedUrlState'; import { View } from '@/views/types/View'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { useMemo } from 'react'; import { useNavigate } from 'react-router-dom'; import { useRecoilState } from 'recoil'; +import { + IconMinus, + IconPlus, + LightIconButton, + isDefined, + useIcons, +} from 'twenty-ui'; import { RelationDefinitionType } from '~/generated-metadata/graphql'; import { SettingsObjectDetailTableItem } from '~/pages/settings/data-model/types/SettingsObjectDetailTableItem'; +import { RELATION_TYPES } from '../../constants/RelationTypes'; import { SettingsObjectFieldDataType } from './SettingsObjectFieldDataType'; type SettingsObjectFieldItemTableRowProps = { 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 22ae24fdb0..6a0eb48238 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 @@ -1,6 +1,12 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconArchive, IconDotsVertical, IconPencil, useIcons } from 'twenty-ui'; +import { + IconArchive, + IconDotsVertical, + IconPencil, + LightIconButton, + useIcons, +} from 'twenty-ui'; import { useLastVisitedObjectMetadataItem } from '@/navigation/hooks/useLastVisitedObjectMetadataItem'; import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView'; @@ -8,7 +14,6 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { SettingsSummaryCard } from '@/settings/components/SettingsSummaryCard'; 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'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsObjectCoverImage.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsObjectCoverImage.tsx index f2db34f27e..d260936f34 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsObjectCoverImage.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsObjectCoverImage.tsx @@ -1,7 +1,6 @@ import styled from '@emotion/styled'; -import { IconEye } from 'twenty-ui'; +import { FloatingButton, IconEye } from 'twenty-ui'; -import { FloatingButton } from '@/ui/input/button/components/FloatingButton'; import { Card } from '@/ui/layout/card/components/Card'; import { SettingsPath } from '@/types/SettingsPath'; diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsObjectInactiveMenuDropDown.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsObjectInactiveMenuDropDown.tsx index 77e39f8f11..7761b11b79 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsObjectInactiveMenuDropDown.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/components/SettingsObjectInactiveMenuDropDown.tsx @@ -1,6 +1,10 @@ -import { IconArchiveOff, IconDotsVertical, IconTrash } from 'twenty-ui'; +import { + IconArchiveOff, + IconDotsVertical, + IconTrash, + LightIconButton, +} from 'twenty-ui'; -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'; diff --git a/packages/twenty-front/src/modules/settings/developers/components/ApiKeyInput.tsx b/packages/twenty-front/src/modules/settings/developers/components/ApiKeyInput.tsx index 298d24d0f8..d7523ab94d 100644 --- a/packages/twenty-front/src/modules/settings/developers/components/ApiKeyInput.tsx +++ b/packages/twenty-front/src/modules/settings/developers/components/ApiKeyInput.tsx @@ -1,10 +1,9 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconCopy } from 'twenty-ui'; +import { Button, IconCopy } from 'twenty-ui'; 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 { TextInput } from '@/ui/input/components/TextInput'; const StyledContainer = styled.div` diff --git a/packages/twenty-front/src/modules/settings/developers/components/SettingsReadDocumentationButton.tsx b/packages/twenty-front/src/modules/settings/developers/components/SettingsReadDocumentationButton.tsx index c4cc6432d7..04d47f6225 100644 --- a/packages/twenty-front/src/modules/settings/developers/components/SettingsReadDocumentationButton.tsx +++ b/packages/twenty-front/src/modules/settings/developers/components/SettingsReadDocumentationButton.tsx @@ -1,6 +1,4 @@ -import { IconBook2 } from 'twenty-ui'; - -import { Button } from '@/ui/input/button/components/Button'; +import { Button, IconBook2 } from 'twenty-ui'; export const SettingsReadDocumentationButton = () => { return ( diff --git a/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationComponent.tsx b/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationComponent.tsx index 2aa04d320e..d6e6286b4c 100644 --- a/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationComponent.tsx +++ b/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationComponent.tsx @@ -1,11 +1,10 @@ -import { Link } from 'react-router-dom'; import { css } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconArrowUpRight, IconBolt, IconPlus, Pill } from 'twenty-ui'; +import { Link } from 'react-router-dom'; +import { Button, IconArrowUpRight, IconBolt, IconPlus, Pill } from 'twenty-ui'; import { SettingsIntegration } from '@/settings/integrations/types/SettingsIntegration'; import { Status } from '@/ui/display/status/components/Status'; -import { Button } from '@/ui/input/button/components/Button'; import { isDefined } from '~/utils/isDefined'; interface SettingsIntegrationComponentProps { diff --git a/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationRemoteTableSchemaUpdate.tsx b/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationRemoteTableSchemaUpdate.tsx index 6fc4d875fd..e86bd54366 100644 --- a/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationRemoteTableSchemaUpdate.tsx +++ b/packages/twenty-front/src/modules/settings/integrations/components/SettingsIntegrationRemoteTableSchemaUpdate.tsx @@ -1,8 +1,7 @@ import { FetchResult } from '@apollo/client'; import styled from '@emotion/styled'; -import { IconReload } from 'twenty-ui'; +import { Button, IconReload } from 'twenty-ui'; -import { Button } from '@/ui/input/button/components/Button'; import { SyncRemoteTableSchemaChangesMutation } from '~/generated-metadata/graphql'; const StyledText = styled.h3` diff --git a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionSummaryCard.tsx b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionSummaryCard.tsx index ccc7dc0abb..b5d2177904 100644 --- a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionSummaryCard.tsx +++ b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionSummaryCard.tsx @@ -1,19 +1,18 @@ +import { SettingsSummaryCard } from '@/settings/components/SettingsSummaryCard'; +import { SettingsIntegrationDatabaseConnectionSyncStatus } from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionSyncStatus'; +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 { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import styled from '@emotion/styled'; import { IconDotsVertical, IconPencil, IconTrash, + LightIconButton, UndecoratedLink, } from 'twenty-ui'; -import { SettingsSummaryCard } from '@/settings/components/SettingsSummaryCard'; -import { SettingsIntegrationDatabaseConnectionSyncStatus } from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionSyncStatus'; -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 { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; - type SettingsIntegrationDatabaseConnectionSummaryCardProps = { databaseLogoUrl: string; connectionId: string; diff --git a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionsListCard.tsx b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionsListCard.tsx index 04752ffdb4..1358615676 100644 --- a/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionsListCard.tsx +++ b/packages/twenty-front/src/modules/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionsListCard.tsx @@ -1,11 +1,10 @@ -import { useNavigate } from 'react-router-dom'; import styled from '@emotion/styled'; -import { IconChevronRight } from 'twenty-ui'; +import { useNavigate } from 'react-router-dom'; +import { IconChevronRight, LightIconButton } from 'twenty-ui'; import { SettingsListCard } from '@/settings/components/SettingsListCard'; import { SettingsIntegrationDatabaseConnectionSyncStatus } from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionSyncStatus'; import { SettingsIntegration } from '@/settings/integrations/types/SettingsIntegration'; -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { RemoteServer } from '~/generated-metadata/graphql'; type SettingsIntegrationDatabaseConnectionsListCardProps = { diff --git a/packages/twenty-front/src/modules/settings/profile/components/ChangePassword.tsx b/packages/twenty-front/src/modules/settings/profile/components/ChangePassword.tsx index 5c3344b406..9110cfb7b8 100644 --- a/packages/twenty-front/src/modules/settings/profile/components/ChangePassword.tsx +++ b/packages/twenty-front/src/modules/settings/profile/components/ChangePassword.tsx @@ -1,10 +1,9 @@ import { useRecoilValue } from 'recoil'; -import { H2Title } from 'twenty-ui'; +import { Button, H2Title } from 'twenty-ui'; import { currentUserState } from '@/auth/states/currentUserState'; 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 { useEmailPasswordResetLinkMutation } from '~/generated/graphql'; export const ChangePassword = () => { diff --git a/packages/twenty-front/src/modules/settings/profile/components/DeleteAccount.tsx b/packages/twenty-front/src/modules/settings/profile/components/DeleteAccount.tsx index 11c2461733..f7e25c658e 100644 --- a/packages/twenty-front/src/modules/settings/profile/components/DeleteAccount.tsx +++ b/packages/twenty-front/src/modules/settings/profile/components/DeleteAccount.tsx @@ -1,10 +1,9 @@ import { useState } from 'react'; import { useRecoilValue } from 'recoil'; -import { H2Title } from 'twenty-ui'; +import { Button, H2Title } from 'twenty-ui'; import { useAuth } from '@/auth/hooks/useAuth'; import { currentUserState } from '@/auth/states/currentUserState'; -import { Button } from '@/ui/input/button/components/Button'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { useDeleteUserAccountMutation } from '~/generated/graphql'; diff --git a/packages/twenty-front/src/modules/settings/profile/components/DeleteWorkspace.tsx b/packages/twenty-front/src/modules/settings/profile/components/DeleteWorkspace.tsx index 190bd60166..f94f1fed42 100644 --- a/packages/twenty-front/src/modules/settings/profile/components/DeleteWorkspace.tsx +++ b/packages/twenty-front/src/modules/settings/profile/components/DeleteWorkspace.tsx @@ -1,12 +1,11 @@ import { useState } from 'react'; import { useRecoilValue } from 'recoil'; -import { H2Title, IconTrash } from 'twenty-ui'; +import { Button, H2Title, IconTrash } from 'twenty-ui'; import { useAuth } from '@/auth/hooks/useAuth'; import { currentUserState } from '@/auth/states/currentUserState'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { useDeleteCurrentWorkspaceMutation } from '~/generated/graphql'; -import { Button } from '@/ui/input/button/components/Button'; export const DeleteWorkspace = () => { const [isDeleteWorkSpaceModalOpen, setIsDeleteWorkSpaceModalOpen] = useState(false); diff --git a/packages/twenty-front/src/modules/settings/security/components/SettingsSSOIdentitiesProvidersListEmptyStateCard.tsx b/packages/twenty-front/src/modules/settings/security/components/SettingsSSOIdentitiesProvidersListEmptyStateCard.tsx index 024fe70d95..f7bffb9188 100644 --- a/packages/twenty-front/src/modules/settings/security/components/SettingsSSOIdentitiesProvidersListEmptyStateCard.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/SettingsSSOIdentitiesProvidersListEmptyStateCard.tsx @@ -1,14 +1,12 @@ /* @license Enterprise */ -import styled from '@emotion/styled'; - import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { Button } from '@/ui/input/button/components/Button'; import { Card } from '@/ui/layout/card/components/Card'; import { CardContent } from '@/ui/layout/card/components/CardContent'; import { CardHeader } from '@/ui/layout/card/components/CardHeader'; -import { IconKey } from 'twenty-ui'; +import styled from '@emotion/styled'; +import { Button, IconKey } from 'twenty-ui'; const StyledHeader = styled(CardHeader)` align-items: center; diff --git a/packages/twenty-front/src/modules/settings/security/components/SettingsSSOOIDCForm.tsx b/packages/twenty-front/src/modules/settings/security/components/SettingsSSOOIDCForm.tsx index 55d6cde7ac..44dc1900bf 100644 --- a/packages/twenty-front/src/modules/settings/security/components/SettingsSSOOIDCForm.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/SettingsSSOOIDCForm.tsx @@ -2,13 +2,12 @@ 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 { TextInput } from '@/ui/input/components/TextInput'; import { Section } from '@/ui/layout/section/components/Section'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { Controller, useFormContext } from 'react-hook-form'; -import { H2Title, IconCopy } from 'twenty-ui'; +import { Button, H2Title, IconCopy } from 'twenty-ui'; const StyledInputsContainer = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/settings/security/components/SettingsSSOSAMLForm.tsx b/packages/twenty-front/src/modules/settings/security/components/SettingsSSOSAMLForm.tsx index 72c5bdb739..ef0bf05e61 100644 --- a/packages/twenty-front/src/modules/settings/security/components/SettingsSSOSAMLForm.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/SettingsSSOSAMLForm.tsx @@ -4,7 +4,6 @@ import { HorizontalSeparator } from '@/auth/sign-in-up/components/HorizontalSepa import { parseSAMLMetadataFromXMLFile } from '@/settings/security/utils/parseSAMLMetadataFromXMLFile'; 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 { TextInput } from '@/ui/input/components/TextInput'; import { Section } from '@/ui/layout/section/components/Section'; import { useTheme } from '@emotion/react'; @@ -12,6 +11,7 @@ import styled from '@emotion/styled'; import { ChangeEvent, useRef } from 'react'; import { useFormContext } from 'react-hook-form'; import { + Button, H2Title, IconCheck, IconCopy, diff --git a/packages/twenty-front/src/modules/settings/security/components/SettingsSecuritySSORowDropdownMenu.tsx b/packages/twenty-front/src/modules/settings/security/components/SettingsSecuritySSORowDropdownMenu.tsx index fa619ef2cd..3212c6dec0 100644 --- a/packages/twenty-front/src/modules/settings/security/components/SettingsSecuritySSORowDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/settings/security/components/SettingsSecuritySSORowDropdownMenu.tsx @@ -1,13 +1,17 @@ /* @license Enterprise */ -import { IconArchive, IconDotsVertical, IconTrash } from 'twenty-ui'; +import { + IconArchive, + IconDotsVertical, + IconTrash, + LightIconButton, +} 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'; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTableEmpty.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTableEmpty.tsx index 77d854984b..a67d4d3582 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTableEmpty.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTableEmpty.tsx @@ -1,6 +1,5 @@ import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { Button } from '@/ui/input/button/components/Button'; import styled from '@emotion/styled'; import { AnimatedPlaceholder, @@ -8,6 +7,7 @@ import { AnimatedPlaceholderEmptySubTitle, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderEmptyTitle, + Button, EMPTY_PLACEHOLDER_TRANSITION_PROPS, IconPlus, } from 'twenty-ui'; 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 22958b5ae4..12fbbe1ec0 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 @@ -6,7 +6,6 @@ import { SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID } from '@/settings/s 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 { CoreEditorHeader } from '@/ui/input/code-editor/components/CodeEditorHeader'; import { Section } from '@/ui/layout/section/components/Section'; import { TabList } from '@/ui/layout/tab/components/TabList'; @@ -16,7 +15,13 @@ 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 { + Button, + H2Title, + IconGitCommit, + IconPlayerPlay, + IconRestore, +} from 'twenty-ui'; import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount'; const StyledTabList = styled(TabList)` diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab.tsx index 5f14f4453f..121827d783 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionSettingsTab.tsx @@ -4,14 +4,13 @@ import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/ho 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 { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { Section } from '@/ui/layout/section/components/Section'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; import { Key } from 'ts-key-enum'; -import { H2Title } from 'twenty-ui'; +import { Button, H2Title } from 'twenty-ui'; import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount'; import { SettingsServerlessFunctionTabEnvironmentVariablesSection } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection'; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariableTableRow.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariableTableRow.tsx index da2dd2f796..eb8e380b7c 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariableTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariableTableRow.tsx @@ -1,23 +1,23 @@ -import { TableRow } from '@/ui/layout/table/components/TableRow'; -import { TableCell } from '@/ui/layout/table/components/TableCell'; +import { EnvironmentVariable } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection'; import { TextInputV2 } from '@/ui/input/components/TextInputV2'; -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 { TableCell } from '@/ui/layout/table/components/TableCell'; +import { TableRow } from '@/ui/layout/table/components/TableRow'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import styled from '@emotion/styled'; +import { useState } from 'react'; import { IconCheck, IconDotsVertical, IconPencil, IconTrash, IconX, + LightIconButton, OverflowingTextWithTooltip, } from 'twenty-ui'; -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 { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; -import React, { useState } from 'react'; -import styled from '@emotion/styled'; -import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; -import { EnvironmentVariable } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection'; const StyledEditModeTableRow = styled(TableRow)` grid-template-columns: 180px auto 56px; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection.tsx index 4ba2b0f7ad..00e8f6b376 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariablesSection.tsx @@ -1,16 +1,21 @@ -import dotenv from 'dotenv'; -import { H2Title, IconPlus, IconSearch, MOBILE_VIEWPORT } from 'twenty-ui'; -import { Table } from '@/ui/layout/table/components/Table'; -import { TableHeader } from '@/ui/layout/table/components/TableHeader'; -import { Section } from '@/ui/layout/section/components/Section'; -import { TextInput } from '@/ui/input/components/TextInput'; -import styled from '@emotion/styled'; -import React, { useMemo, useState } from 'react'; -import { TableBody } from '@/ui/layout/table/components/TableBody'; -import { Button } from '@/ui/input/button/components/Button'; -import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState'; -import { TableRow } from '@/ui/layout/table/components/TableRow'; import { SettingsServerlessFunctionTabEnvironmentVariableTableRow } from '@/settings/serverless-functions/components/tabs/SettingsServerlessFunctionTabEnvironmentVariableTableRow'; +import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState'; +import { TextInput } from '@/ui/input/components/TextInput'; +import { Section } from '@/ui/layout/section/components/Section'; +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'; +import dotenv from 'dotenv'; +import { useMemo, useState } from 'react'; +import { + Button, + H2Title, + IconPlus, + IconSearch, + MOBILE_VIEWPORT, +} from 'twenty-ui'; import { v4 } from 'uuid'; const StyledSearchInput = styled(TextInput)` 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 54a565215d..257d138136 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 @@ -1,5 +1,5 @@ import { Section } from '@/ui/layout/section/components/Section'; -import { H2Title, IconPlayerPlay } from 'twenty-ui'; +import { Button, H2Title, IconPlayerPlay } from 'twenty-ui'; import { LightCopyIconButton } from '@/object-record/record-field/components/LightCopyIconButton'; import { SettingsServerlessFunctionCodeEditorContainer } from '@/settings/serverless-functions/components/SettingsServerlessFunctionCodeEditorContainer'; @@ -10,7 +10,6 @@ import { settingsServerlessFunctionOutputState } from '@/settings/serverless-fun 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 } from '@/ui/input/code-editor/components/CodeEditor'; import { CoreEditorHeader } from '@/ui/input/code-editor/components/CodeEditorHeader'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx index 5c03dc5356..a04d344a9d 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/components/ModalCloseButton.tsx @@ -1,10 +1,9 @@ import styled from '@emotion/styled'; -import { IconX } from 'twenty-ui'; +import { IconButton, IconX } from 'twenty-ui'; import { useSpreadsheetImportInitialStep } from '@/spreadsheet-import/hooks/useSpreadsheetImportInitialStep'; import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager'; -import { IconButton } from '@/ui/input/button/components/IconButton'; import { useStepBar } from '@/ui/navigation/step-bar/hooks/useStepBar'; const StyledCloseButtonContainer = styled.div` diff --git a/packages/twenty-front/src/modules/spreadsheet-import/components/StepNavigationButton.tsx b/packages/twenty-front/src/modules/spreadsheet-import/components/StepNavigationButton.tsx index 6462fcd8c6..5e412dffcb 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/components/StepNavigationButton.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/components/StepNavigationButton.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import { CircularProgressBar } from '@/ui/feedback/progress-bar/components/CircularProgressBar'; -import { MainButton } from '@/ui/input/button/components/MainButton'; +import { MainButton } from 'twenty-ui'; import { Modal } from '@/ui/layout/modal/components/Modal'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx index 534deb1e0b..e0d7ac6a22 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/UploadStep/components/DropZone.tsx @@ -7,7 +7,7 @@ import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpre import { readFileAsync } from '@/spreadsheet-import/utils/readFilesAsync'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { MainButton } from '@/ui/input/button/components/MainButton'; +import { MainButton } from 'twenty-ui'; const StyledContainer = styled.div` align-items: center; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx index f74e6dcc74..12e89fd550 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/steps/components/ValidationStep/ValidationStep.tsx @@ -1,3 +1,20 @@ +import { Heading } from '@/spreadsheet-import/components/Heading'; +import { SpreadsheetImportTable } from '@/spreadsheet-import/components/SpreadsheetImportTable'; +import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton'; +import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; +import { + ColumnType, + Columns, +} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; +import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep'; +import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType'; +import { + ImportValidationResult, + ImportedStructuredRow, +} from '@/spreadsheet-import/types'; +import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations'; +import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager'; +import { Modal } from '@/ui/layout/modal/components/Modal'; import styled from '@emotion/styled'; import { Dispatch, @@ -8,28 +25,8 @@ import { } from 'react'; // @ts-expect-error Todo: remove usage of react-data-grid` import { RowsChangeData } from 'react-data-grid'; -import { IconTrash, Toggle } from 'twenty-ui'; - -import { Heading } from '@/spreadsheet-import/components/Heading'; -import { SpreadsheetImportTable } from '@/spreadsheet-import/components/SpreadsheetImportTable'; -import { StepNavigationButton } from '@/spreadsheet-import/components/StepNavigationButton'; -import { useSpreadsheetImportInternal } from '@/spreadsheet-import/hooks/useSpreadsheetImportInternal'; -import { - Columns, - ColumnType, -} from '@/spreadsheet-import/steps/components/MatchColumnsStep/MatchColumnsStep'; -import { - ImportedStructuredRow, - ImportValidationResult, -} from '@/spreadsheet-import/types'; -import { addErrorsAndRunHooks } from '@/spreadsheet-import/utils/dataMutations'; -import { useDialogManager } from '@/ui/feedback/dialog-manager/hooks/useDialogManager'; -import { Button } from '@/ui/input/button/components/Button'; +import { Button, IconTrash, Toggle } from 'twenty-ui'; import { isDefined } from '~/utils/isDefined'; - -import { SpreadsheetImportStep } from '@/spreadsheet-import/steps/types/SpreadsheetImportStep'; -import { SpreadsheetImportStepType } from '@/spreadsheet-import/steps/types/SpreadsheetImportStepType'; -import { Modal } from '@/ui/layout/modal/components/Modal'; import { generateColumns } from './components/columns'; import { ImportedStructuredRowMetadata } from './types'; diff --git a/packages/twenty-front/src/modules/support/components/SupportButton.tsx b/packages/twenty-front/src/modules/support/components/SupportButton.tsx index 9229232b6c..5634c842df 100644 --- a/packages/twenty-front/src/modules/support/components/SupportButton.tsx +++ b/packages/twenty-front/src/modules/support/components/SupportButton.tsx @@ -1,9 +1,8 @@ import styled from '@emotion/styled'; -import { IconHelpCircle } from 'twenty-ui'; +import { Button, IconHelpCircle } from 'twenty-ui'; import { SupportButtonSkeletonLoader } from '@/support/components/SupportButtonSkeletonLoader'; import { useSupportChat } from '@/support/hooks/useSupportChat'; -import { Button } from '@/ui/input/button/components/Button'; const StyledButtonContainer = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/ui/display/info/components/Info.tsx b/packages/twenty-front/src/modules/ui/display/info/components/Info.tsx index e97dbbb438..9761daf684 100644 --- a/packages/twenty-front/src/modules/ui/display/info/components/Info.tsx +++ b/packages/twenty-front/src/modules/ui/display/info/components/Info.tsx @@ -1,11 +1,10 @@ -import React from 'react'; -import { Link } from 'react-router-dom'; import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconInfoCircle } from 'twenty-ui'; +import React from 'react'; +import { Link } from 'react-router-dom'; +import { Button, IconInfoCircle } from 'twenty-ui'; import { AppPath } from '@/types/AppPath'; -import { Button } from '@/ui/input/button/components/Button'; export type InfoAccent = 'blue' | 'danger'; export type InfoProps = { diff --git a/packages/twenty-front/src/modules/ui/feedback/dialog-manager/components/Dialog.tsx b/packages/twenty-front/src/modules/ui/feedback/dialog-manager/components/Dialog.tsx index c8e5d05cbd..febaa3f080 100644 --- a/packages/twenty-front/src/modules/ui/feedback/dialog-manager/components/Dialog.tsx +++ b/packages/twenty-front/src/modules/ui/feedback/dialog-manager/components/Dialog.tsx @@ -3,8 +3,8 @@ import { motion } from 'framer-motion'; import { useCallback } from 'react'; import { Key } from 'ts-key-enum'; -import { Button } from '@/ui/input/button/components/Button'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { Button } from 'twenty-ui'; import { isDefined } from '~/utils/isDefined'; import { DialogHotkeyScope } from '../types/DialogHotkeyScope'; diff --git a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx index b1f6630650..2ee43e5f23 100644 --- a/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx +++ b/packages/twenty-front/src/modules/ui/feedback/snack-bar-manager/components/SnackBar.tsx @@ -7,13 +7,13 @@ import { IconInfoCircle, IconSquareRoundedCheck, IconX, + LightButton, + LightIconButton, MOBILE_VIEWPORT, } from 'twenty-ui'; import { ProgressBar } from '@/ui/feedback/progress-bar/components/ProgressBar'; import { useProgressAnimation } from '@/ui/feedback/progress-bar/hooks/useProgressAnimation'; -import { LightButton } from '@/ui/input/button/components/LightButton'; -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { isDefined } from '~/utils/isDefined'; export enum SnackBarVariant { diff --git a/packages/twenty-front/src/modules/ui/input/components/AutosizeTextInput.tsx b/packages/twenty-front/src/modules/ui/input/components/AutosizeTextInput.tsx index eb9afd68a3..06aa24c1bf 100644 --- a/packages/twenty-front/src/modules/ui/input/components/AutosizeTextInput.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/AutosizeTextInput.tsx @@ -1,12 +1,10 @@ +import styled from '@emotion/styled'; import { useRef, useState } from 'react'; import { HotkeysEvent } from 'react-hotkeys-hook/dist/types'; import TextareaAutosize from 'react-textarea-autosize'; -import styled from '@emotion/styled'; import { Key } from 'ts-key-enum'; -import { IconArrowRight } from 'twenty-ui'; +import { Button, IconArrowRight, RoundedIconButton } from 'twenty-ui'; -import { Button } from '@/ui/input/button/components/Button'; -import { RoundedIconButton } from '@/ui/input/button/components/RoundedIconButton'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; diff --git a/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx b/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx index b0e8278b89..dd9191f0ab 100644 --- a/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx @@ -1,7 +1,14 @@ import styled from '@emotion/styled'; import { useMemo, useState } from 'react'; import { useRecoilValue } from 'recoil'; -import { IconApps, IconComponent, useIcons } from 'twenty-ui'; +import { + IconApps, + IconComponent, + useIcons, + IconButton, + IconButtonVariant, + LightIconButton, +} from 'twenty-ui'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; @@ -14,8 +21,6 @@ import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectab import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { arrayToChunks } from '~/utils/array/arrayToChunks'; -import { IconButton, IconButtonVariant } from '../button/components/IconButton'; -import { LightIconButton } from '../button/components/LightIconButton'; import { IconPickerHotkeyScope } from '../types/IconPickerHotkeyScope'; export type IconPickerProps = { diff --git a/packages/twenty-front/src/modules/ui/input/components/ImageInput.tsx b/packages/twenty-front/src/modules/ui/input/components/ImageInput.tsx index 77779acc58..82f2695aa7 100644 --- a/packages/twenty-front/src/modules/ui/input/components/ImageInput.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/ImageInput.tsx @@ -1,8 +1,7 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import React, { useMemo } from 'react'; -import { IconPhotoUp, IconTrash, IconUpload, IconX } from 'twenty-ui'; -import { Button } from '@/ui/input/button/components/Button'; +import { Button, IconPhotoUp, IconTrash, IconUpload, IconX } from 'twenty-ui'; import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx index 1efc985d34..6ffeb3fc3d 100644 --- a/packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx @@ -1,8 +1,7 @@ import styled from '@emotion/styled'; import { DateTime } from 'luxon'; -import { IconChevronLeft, IconChevronRight } from 'twenty-ui'; +import { IconChevronLeft, IconChevronRight, LightIconButton } from 'twenty-ui'; -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { Select } from '@/ui/input/components/Select'; import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput'; diff --git a/packages/twenty-front/src/modules/ui/layout/bottom-bar/components/__stories__/BottomBar.stories.tsx b/packages/twenty-front/src/modules/ui/layout/bottom-bar/components/__stories__/BottomBar.stories.tsx index 8562f67cdc..96f43c1bc2 100644 --- a/packages/twenty-front/src/modules/ui/layout/bottom-bar/components/__stories__/BottomBar.stories.tsx +++ b/packages/twenty-front/src/modules/ui/layout/bottom-bar/components/__stories__/BottomBar.stories.tsx @@ -1,11 +1,9 @@ -import { Meta, StoryObj } from '@storybook/react'; -import { IconPlus } from 'twenty-ui'; - -import { Button } from '@/ui/input/button/components/Button'; import { BottomBar } from '@/ui/layout/bottom-bar/components/BottomBar'; import { isBottomBarOpenedComponentState } from '@/ui/layout/bottom-bar/states/isBottomBarOpenedComponentState'; import styled from '@emotion/styled'; +import { Meta, StoryObj } from '@storybook/react'; import { RecoilRoot } from 'recoil'; +import { Button, IconPlus } from 'twenty-ui'; const StyledContainer = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx index dd2d94dcae..6356bbdd48 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx @@ -1,9 +1,7 @@ -import { ComponentProps, MouseEvent } from 'react'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconComponent } from 'twenty-ui'; - -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; +import { ComponentProps, MouseEvent } from 'react'; +import { IconComponent, LightIconButton } from 'twenty-ui'; const StyledHeader = styled.li` align-items: center; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/DropdownMenu.stories.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/DropdownMenu.stories.tsx index 69c9c7b9cc..73b1c068ad 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/DropdownMenu.stories.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/DropdownMenu.stories.tsx @@ -3,9 +3,8 @@ import { Decorator, Meta, StoryObj } from '@storybook/react'; import { expect, userEvent, waitFor, within } from '@storybook/test'; import { PlayFunction } from '@storybook/types'; import { useState } from 'react'; -import { Avatar, ComponentDecorator } from 'twenty-ui'; +import { Avatar, Button, ComponentDecorator } from 'twenty-ui'; -import { Button } from '@/ui/input/button/components/Button'; import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar'; 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 3165ebce78..e49fcff277 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 @@ -1,10 +1,9 @@ import styled from '@emotion/styled'; import { AnimatePresence, LayoutGroup } from 'framer-motion'; import { ReactNode, useState } from 'react'; -import { H1Title, H1TitleFontColor } from 'twenty-ui'; +import { Button, ButtonAccent, H1Title, H1TitleFontColor } from 'twenty-ui'; import { useDebouncedCallback } from 'use-debounce'; -import { Button, ButtonAccent } from '@/ui/input/button/components/Button'; import { TextInput } from '@/ui/input/components/TextInput'; import { Modal, ModalVariants } from '@/ui/layout/modal/components/Modal'; diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/PageAddButton.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/PageAddButton.tsx index d6a977a18c..3c1478f862 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/components/PageAddButton.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/components/PageAddButton.tsx @@ -1,6 +1,4 @@ -import { IconPlus } from 'twenty-ui'; - -import { IconButton } from '@/ui/input/button/components/IconButton'; +import { IconButton, IconPlus } from 'twenty-ui'; type PageAddButtonProps = { onClick: () => void; diff --git a/packages/twenty-front/src/modules/ui/layout/page/components/PageFavoriteButton.tsx b/packages/twenty-front/src/modules/ui/layout/page/components/PageFavoriteButton.tsx index b66a73807d..9d49b6f066 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/components/PageFavoriteButton.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/components/PageFavoriteButton.tsx @@ -1,6 +1,4 @@ -import { IconHeart } from 'twenty-ui'; - -import { IconButton } from '@/ui/input/button/components/IconButton'; +import { IconButton, IconHeart } from 'twenty-ui'; type PageFavoriteButtonProps = { isFavorite: boolean; 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 8cdb9cfea7..db981feb4d 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 @@ -3,6 +3,7 @@ import styled from '@emotion/styled'; import { ReactNode } from 'react'; import { useRecoilValue } from 'recoil'; import { + IconButton, IconChevronDown, IconChevronUp, IconComponent, @@ -11,7 +12,6 @@ import { OverflowingTextWithTooltip, } from 'twenty-ui'; -import { IconButton } from '@/ui/input/button/components/IconButton'; import { NavigationDrawerCollapseButton } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton'; import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarCloseButton.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarCloseButton.tsx index 9a225f4571..68bc57c0af 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarCloseButton.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarCloseButton.tsx @@ -1,6 +1,4 @@ -import { IconX } from 'twenty-ui'; - -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; +import { IconX, LightIconButton } from 'twenty-ui'; import { useRightDrawer } from '../hooks/useRightDrawer'; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarExpandButton.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarExpandButton.tsx index a0b96b0501..d56a5f22f7 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarExpandButton.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarExpandButton.tsx @@ -1,6 +1,5 @@ -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; -import { IconExternalLink, UndecoratedLink } from 'twenty-ui'; +import { IconExternalLink, LightIconButton, UndecoratedLink } from 'twenty-ui'; export const RightDrawerTopBarExpandButton = ({ to }: { to: string }) => { const { closeRightDrawer } = useRightDrawer(); diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarMinimizeButton.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarMinimizeButton.tsx index b95be35787..8f5635ec02 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarMinimizeButton.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarMinimizeButton.tsx @@ -1,6 +1,5 @@ -import { IconMinus } from 'twenty-ui'; +import { IconMinus, LightIconButton } from 'twenty-ui'; -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; export const RightDrawerTopBarMinimizeButton = () => { diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageAddButton.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageAddButton.tsx index 9e5621d9d5..eb0373618a 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageAddButton.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageAddButton.tsx @@ -1,10 +1,9 @@ import styled from '@emotion/styled'; -import { IconCheckbox, IconNotes, IconPlus } from 'twenty-ui'; +import { IconButton, IconCheckbox, IconNotes, IconPlus } from 'twenty-ui'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { PageHotkeyScope } from '@/types/PageHotkeyScope'; -import { IconButton } from '@/ui/input/button/components/IconButton'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { SHOW_PAGE_ADD_BUTTON_DROPDOWN_ID } from '@/ui/layout/show-page/constants/ShowPageAddButtonDropdownId'; diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageMoreButton.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageMoreButton.tsx index 8b44e15639..8cf27704dc 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageMoreButton.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageMoreButton.tsx @@ -1,11 +1,15 @@ import styled from '@emotion/styled'; import { useNavigate } from 'react-router-dom'; import { useRecoilState, useRecoilValue } from 'recoil'; -import { IconDotsVertical, IconRestore, IconTrash } from 'twenty-ui'; +import { + IconButton, + IconDotsVertical, + IconRestore, + IconTrash, +} from 'twenty-ui'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; import { PageHotkeyScope } from '@/types/PageHotkeyScope'; -import { IconButton } from '@/ui/input/button/components/IconButton'; 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'; diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItem.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItem.tsx index 5ff957ea00..bb54ce02fa 100644 --- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItem.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItem.tsx @@ -1,9 +1,11 @@ import { useTheme } from '@emotion/react'; import { FunctionComponent, MouseEvent, ReactElement, ReactNode } from 'react'; -import { IconChevronRight, IconComponent } from 'twenty-ui'; - -import { LightIconButtonProps } from '@/ui/input/button/components/LightIconButton'; -import { LightIconButtonGroup } from '@/ui/input/button/components/LightIconButtonGroup'; +import { + IconChevronRight, + IconComponent, + LightIconButtonGroup, + LightIconButtonProps, +} from 'twenty-ui'; import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent'; import { diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemAvatar.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemAvatar.tsx index c3c384bf35..a5829298b4 100644 --- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemAvatar.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemAvatar.tsx @@ -5,13 +5,12 @@ import { AvatarProps, IconChevronRight, IconComponent, - isDefined, + LightIconButtonGroup, + LightIconButtonProps, OverflowingTextWithTooltip, + isDefined, } from 'twenty-ui'; -import { LightIconButtonProps } from '@/ui/input/button/components/LightIconButton'; -import { LightIconButtonGroup } from '@/ui/input/button/components/LightIconButtonGroup'; - import { StyledHoverableMenuItemBase, StyledMenuItemLeftContent, diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemDraggable.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemDraggable.tsx index dfe895fd82..c324e8ec48 100644 --- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemDraggable.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemDraggable.tsx @@ -1,6 +1,4 @@ -import { IconComponent } from 'twenty-ui'; - -import { LightIconButtonGroup } from '@/ui/input/button/components/LightIconButtonGroup'; +import { IconComponent, LightIconButtonGroup } from 'twenty-ui'; import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent'; import { StyledHoverableMenuItemBase } from '../internals/components/StyledMenuItemBase'; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton.tsx index 9be3547320..348a3d6c87 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerCollapseButton.tsx @@ -1,8 +1,8 @@ -import { IconButton } from '@/ui/input/button/components/IconButton'; import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; import styled from '@emotion/styled'; import { useSetRecoilState } from 'recoil'; import { + IconButton, IconLayoutSidebarLeftCollapse, IconLayoutSidebarRightCollapse, } from 'twenty-ui'; diff --git a/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx b/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx index ffe89f6b6b..958e29ff95 100644 --- a/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx +++ b/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx @@ -1,8 +1,6 @@ import styled from '@emotion/styled'; -import { IconChevronDown, IconPlus } from 'twenty-ui'; +import { Button, ButtonGroup, IconChevronDown, IconPlus } from 'twenty-ui'; -import { Button } from '@/ui/input/button/components/Button'; -import { ButtonGroup } from '@/ui/input/button/components/ButtonGroup'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateButton.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateButton.tsx index 2fb0cb4f42..b2bb66204f 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateButton.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerCreateButton.tsx @@ -1,4 +1,3 @@ -import { Button } from '@/ui/input/button/components/Button'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { ViewType } from '@/views/types/ViewType'; import { useCreateViewFromCurrentState } from '@/views/view-picker/hooks/useCreateViewFromCurrentState'; @@ -8,6 +7,7 @@ import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode'; import { viewPickerIsPersistingComponentState } from '@/views/view-picker/states/viewPickerIsPersistingComponentState'; import { viewPickerKanbanFieldMetadataIdComponentState } from '@/views/view-picker/states/viewPickerKanbanFieldMetadataIdComponentState'; import { viewPickerTypeComponentState } from '@/views/view-picker/states/viewPickerTypeComponentState'; +import { Button } from 'twenty-ui'; export const ViewPickerCreateButton = () => { const { availableFieldsForKanban, navigateToSelectSettings } = diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerEditButton.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerEditButton.tsx index a2f67dcabb..16df5ea4a3 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerEditButton.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerEditButton.tsx @@ -1,4 +1,3 @@ -import { Button } from '@/ui/input/button/components/Button'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { ViewType } from '@/views/types/ViewType'; import { useCreateViewFromCurrentState } from '@/views/view-picker/hooks/useCreateViewFromCurrentState'; @@ -8,6 +7,7 @@ import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode'; import { viewPickerIsPersistingComponentState } from '@/views/view-picker/states/viewPickerIsPersistingComponentState'; import { viewPickerKanbanFieldMetadataIdComponentState } from '@/views/view-picker/states/viewPickerKanbanFieldMetadataIdComponentState'; import { viewPickerTypeComponentState } from '@/views/view-picker/states/viewPickerTypeComponentState'; +import { Button } from 'twenty-ui'; export const ViewPickerEditButton = () => { const { availableFieldsForKanban, navigateToSelectSettings } = diff --git a/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowHeader.tsx b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowHeader.tsx index c2000974e5..2e741bcf37 100644 --- a/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowHeader.tsx +++ b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowHeader.tsx @@ -1,9 +1,9 @@ -import { Button } from '@/ui/input/button/components/Button'; import { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion'; import { useDeactivateWorkflowVersion } from '@/workflow/hooks/useDeactivateWorkflowVersion'; import { useDeleteOneWorkflowVersion } from '@/workflow/hooks/useDeleteOneWorkflowVersion'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; import { + Button, IconPlayerPlay, IconPlayerStop, IconPower, diff --git a/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx index e796c4a88c..03c9c5a520 100644 --- a/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx +++ b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx @@ -1,7 +1,6 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; -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'; @@ -10,7 +9,13 @@ 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'; +import { + Button, + IconPencil, + IconPlayerStop, + IconPower, + isDefined, +} from 'twenty-ui'; export const RecordShowPageWorkflowVersionHeader = ({ workflowVersionId, diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCreateStepNode.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCreateStepNode.tsx index 2e1b1328a0..ec06140378 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCreateStepNode.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCreateStepNode.tsx @@ -1,7 +1,6 @@ -import { IconButton } from '@/ui/input/button/components/IconButton'; import styled from '@emotion/styled'; import { Handle, Position } from '@xyflow/react'; -import { IconPlus } from 'twenty-ui'; +import { IconButton, IconPlus } from 'twenty-ui'; export const StyledTargetHandle = styled(Handle)` visibility: hidden; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeEditable.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeEditable.tsx index cb8290fd73..579e3c9234 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeEditable.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeEditable.tsx @@ -1,4 +1,3 @@ -import { FloatingIconButton } from '@/ui/input/button/components/FloatingIconButton'; import { WorkflowDiagramStepNodeBase } from '@/workflow/components/WorkflowDiagramStepNodeBase'; import { useDeleteOneStep } from '@/workflow/hooks/useDeleteOneStep'; import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; @@ -6,7 +5,7 @@ import { workflowIdState } from '@/workflow/states/workflowIdState'; import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram'; import { assertWorkflowWithCurrentVersionIsDefined } from '@/workflow/utils/assertWorkflowWithCurrentVersionIsDefined'; import { useRecoilValue } from 'recoil'; -import { IconTrash } from 'twenty-ui'; +import { FloatingIconButton, IconTrash } from 'twenty-ui'; export const WorkflowDiagramStepNodeEditable = ({ id, diff --git a/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteLink.tsx b/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteLink.tsx index 92bdf69afa..1e0a10aeae 100644 --- a/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteLink.tsx +++ b/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteLink.tsx @@ -1,10 +1,9 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconCopy, IconLink } from 'twenty-ui'; +import { Button, IconCopy, IconLink } from 'twenty-ui'; 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 { TextInput } from '@/ui/input/components/TextInput'; const StyledContainer = styled.div` diff --git a/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteTeam.tsx b/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteTeam.tsx index 4c007b5180..7bff502047 100644 --- a/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteTeam.tsx +++ b/packages/twenty-front/src/modules/workspace/components/WorkspaceInviteTeam.tsx @@ -3,12 +3,11 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useEffect } from 'react'; import { Controller, useForm } from 'react-hook-form'; import { Key } from 'ts-key-enum'; -import { IconSend } from 'twenty-ui'; +import { Button, IconSend } from 'twenty-ui'; import { z } from 'zod'; 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 { TextInput } from '@/ui/input/components/TextInput'; import { sanitizeEmailList } from '@/workspace/utils/sanitizeEmailList'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/pages/auth/Authorize.tsx b/packages/twenty-front/src/pages/auth/Authorize.tsx index b97c31815d..1019907d22 100644 --- a/packages/twenty-front/src/pages/auth/Authorize.tsx +++ b/packages/twenty-front/src/pages/auth/Authorize.tsx @@ -1,10 +1,8 @@ +import { AppPath } from '@/types/AppPath'; import styled from '@emotion/styled'; import { useEffect, useState } from 'react'; import { useNavigate, useSearchParams } from 'react-router-dom'; - -import { AppPath } from '@/types/AppPath'; -import { MainButton } from '@/ui/input/button/components/MainButton'; -import { UndecoratedLink } from 'twenty-ui'; +import { MainButton, UndecoratedLink } from 'twenty-ui'; import { useAuthorizeAppMutation } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; diff --git a/packages/twenty-front/src/pages/auth/Invite.tsx b/packages/twenty-front/src/pages/auth/Invite.tsx index 0afef7d320..737faa3442 100644 --- a/packages/twenty-front/src/pages/auth/Invite.tsx +++ b/packages/twenty-front/src/pages/auth/Invite.tsx @@ -1,7 +1,3 @@ -import styled from '@emotion/styled'; -import { useMemo } from 'react'; -import { useRecoilValue } from 'recoil'; - import { Logo } from '@/auth/components/Logo'; import { Title } from '@/auth/components/Title'; import { FooterNote } from '@/auth/sign-in-up/components/FooterNote'; @@ -10,10 +6,12 @@ import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm'; import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { Loader } from '@/ui/feedback/loader/components/Loader'; -import { MainButton } from '@/ui/input/button/components/MainButton'; import { useWorkspaceSwitching } from '@/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching'; +import styled from '@emotion/styled'; +import { useMemo } from 'react'; import { useSearchParams } from 'react-router-dom'; -import { AnimatedEaseIn } from 'twenty-ui'; +import { useRecoilValue } from 'recoil'; +import { AnimatedEaseIn, MainButton } from 'twenty-ui'; import { useAddUserToWorkspaceByInviteTokenMutation, useAddUserToWorkspaceMutation, diff --git a/packages/twenty-front/src/pages/auth/PasswordReset.tsx b/packages/twenty-front/src/pages/auth/PasswordReset.tsx index 137810855e..4be8e04446 100644 --- a/packages/twenty-front/src/pages/auth/PasswordReset.tsx +++ b/packages/twenty-front/src/pages/auth/PasswordReset.tsx @@ -1,15 +1,3 @@ -import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { zodResolver } from '@hookform/resolvers/zod'; -import { isNonEmptyString } from '@sniptt/guards'; -import { motion } from 'framer-motion'; -import { useState } from 'react'; -import { Controller, useForm } from 'react-hook-form'; -import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; -import { useNavigate, useParams } from 'react-router-dom'; -import { useSetRecoilState } from 'recoil'; -import { z } from 'zod'; - import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { Logo } from '@/auth/components/Logo'; import { Title } from '@/auth/components/Title'; @@ -20,10 +8,20 @@ import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken'; 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 { MainButton } from '@/ui/input/button/components/MainButton'; import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { isDefaultLayoutAuthModalVisibleState } from '@/ui/layout/states/isDefaultLayoutAuthModalVisibleState'; -import { AnimatedEaseIn } from 'twenty-ui'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { zodResolver } from '@hookform/resolvers/zod'; +import { isNonEmptyString } from '@sniptt/guards'; +import { motion } from 'framer-motion'; +import { useState } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; +import { useNavigate, useParams } from 'react-router-dom'; +import { useSetRecoilState } from 'recoil'; +import { AnimatedEaseIn, MainButton } from 'twenty-ui'; +import { z } from 'zod'; import { useUpdatePasswordViaResetTokenMutation, useValidatePasswordResetTokenQuery, diff --git a/packages/twenty-front/src/pages/auth/SSOWorkspaceSelection.tsx b/packages/twenty-front/src/pages/auth/SSOWorkspaceSelection.tsx index 6d3e604a58..1767b13426 100644 --- a/packages/twenty-front/src/pages/auth/SSOWorkspaceSelection.tsx +++ b/packages/twenty-front/src/pages/auth/SSOWorkspaceSelection.tsx @@ -5,10 +5,10 @@ import { HorizontalSeparator } from '@/auth/sign-in-up/components/HorizontalSepa 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'; +import { MainButton } from 'twenty-ui'; const StyledContentContainer = styled.div` margin-bottom: ${({ theme }) => theme.spacing(8)}; diff --git a/packages/twenty-front/src/pages/not-found/NotFound.tsx b/packages/twenty-front/src/pages/not-found/NotFound.tsx index 3843244ae9..dee24fc8c7 100644 --- a/packages/twenty-front/src/pages/not-found/NotFound.tsx +++ b/packages/twenty-front/src/pages/not-found/NotFound.tsx @@ -1,16 +1,14 @@ -import styled from '@emotion/styled'; - import { SignInBackgroundMockPage } from '@/sign-in-background-mock/components/SignInBackgroundMockPage'; import { AppPath } from '@/types/AppPath'; -import { MainButton } from '@/ui/input/button/components/MainButton'; - import { PageTitle } from '@/ui/utilities/page-title/components/PageTitle'; +import styled from '@emotion/styled'; import { AnimatedPlaceholder, AnimatedPlaceholderEmptyTextContainer, AnimatedPlaceholderErrorContainer, AnimatedPlaceholderErrorSubTitle, AnimatedPlaceholderErrorTitle, + MainButton, UndecoratedLink, } from 'twenty-ui'; diff --git a/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx b/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx index d9e934e57c..19f1521e14 100644 --- a/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx +++ b/packages/twenty-front/src/pages/onboarding/ChooseYourPlan.tsx @@ -1,8 +1,3 @@ -import styled from '@emotion/styled'; -import { isNonEmptyString, isNumber } from '@sniptt/guards'; -import { useState } from 'react'; -import { useRecoilValue } from 'recoil'; - import { SubTitle } from '@/auth/components/SubTitle'; import { Title } from '@/auth/components/Title'; import { useAuth } from '@/auth/hooks/useAuth'; @@ -13,9 +8,12 @@ import { AppPath } from '@/types/AppPath'; import { Loader } from '@/ui/feedback/loader/components/Loader'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { MainButton } from '@/ui/input/button/components/MainButton'; import { CardPicker } from '@/ui/input/components/CardPicker'; -import { ActionLink, CAL_LINK } from 'twenty-ui'; +import styled from '@emotion/styled'; +import { isNonEmptyString, isNumber } from '@sniptt/guards'; +import { useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { ActionLink, CAL_LINK, MainButton } from 'twenty-ui'; import { ProductPriceEntity, SubscriptionInterval, diff --git a/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx b/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx index f5faa4ff4b..ebb0fbc1bd 100644 --- a/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx +++ b/packages/twenty-front/src/pages/onboarding/CreateProfile.tsx @@ -1,10 +1,10 @@ -import { useCallback, useState } from 'react'; -import { Controller, SubmitHandler, useForm } from 'react-hook-form'; import styled from '@emotion/styled'; import { zodResolver } from '@hookform/resolvers/zod'; +import { useCallback, useState } from 'react'; +import { Controller, SubmitHandler, useForm } from 'react-hook-form'; import { useRecoilState } from 'recoil'; import { Key } from 'ts-key-enum'; -import { H2Title } from 'twenty-ui'; +import { H2Title, MainButton } from 'twenty-ui'; import { z } from 'zod'; import { SubTitle } from '@/auth/components/SubTitle'; @@ -18,7 +18,6 @@ import { ProfilePictureUploader } from '@/settings/profile/components/ProfilePic import { PageHotkeyScope } from '@/types/PageHotkeyScope'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { MainButton } from '@/ui/input/button/components/MainButton'; import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; diff --git a/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx b/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx index b703ef13b0..bb8f7d1d49 100644 --- a/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx +++ b/packages/twenty-front/src/pages/onboarding/CreateWorkspace.tsx @@ -1,10 +1,10 @@ -import { useCallback } from 'react'; -import { Controller, SubmitHandler, useForm } from 'react-hook-form'; import styled from '@emotion/styled'; import { zodResolver } from '@hookform/resolvers/zod'; +import { useCallback } from 'react'; +import { Controller, SubmitHandler, useForm } from 'react-hook-form'; import { useSetRecoilState } from 'recoil'; import { Key } from 'ts-key-enum'; -import { H2Title } from 'twenty-ui'; +import { H2Title, MainButton } from 'twenty-ui'; import { z } from 'zod'; import { SubTitle } from '@/auth/components/SubTitle'; @@ -17,7 +17,6 @@ import { WorkspaceLogoUploader } from '@/settings/workspace/components/Workspace import { Loader } from '@/ui/feedback/loader/components/Loader'; import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { MainButton } from '@/ui/input/button/components/MainButton'; import { TextInputV2 } from '@/ui/input/components/TextInputV2'; import { OnboardingStatus, diff --git a/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx b/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx index 3b2a3ee94b..42dac7188e 100644 --- a/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx +++ b/packages/twenty-front/src/pages/onboarding/InviteTeam.tsx @@ -1,3 +1,14 @@ +import { SubTitle } from '@/auth/components/SubTitle'; +import { Title } from '@/auth/components/Title'; +import { currentUserState } from '@/auth/states/currentUserState'; +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus'; +import { PageHotkeyScope } from '@/types/PageHotkeyScope'; +import { SeparatorLineText } from '@/ui/display/text/components/SeparatorLineText'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { TextInputV2 } from '@/ui/input/components/TextInputV2'; +import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { zodResolver } from '@hookform/resolvers/zod'; @@ -10,22 +21,14 @@ import { } from 'react-hook-form'; import { useRecoilValue } from 'recoil'; import { Key } from 'ts-key-enum'; -import { ActionLink, AnimatedTranslation, IconCopy } from 'twenty-ui'; +import { + ActionLink, + AnimatedTranslation, + IconCopy, + LightButton, + MainButton, +} from 'twenty-ui'; import { z } from 'zod'; - -import { SubTitle } from '@/auth/components/SubTitle'; -import { Title } from '@/auth/components/Title'; -import { currentUserState } from '@/auth/states/currentUserState'; -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; -import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboardingStatus'; -import { PageHotkeyScope } from '@/types/PageHotkeyScope'; -import { SeparatorLineText } from '@/ui/display/text/components/SeparatorLineText'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { LightButton } from '@/ui/input/button/components/LightButton'; -import { MainButton } from '@/ui/input/button/components/MainButton'; -import { TextInputV2 } from '@/ui/input/components/TextInputV2'; -import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { OnboardingStatus } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; import { useCreateWorkspaceInvitation } from '../../modules/workspace-invitation/hooks/useCreateWorkspaceInvitation'; diff --git a/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx b/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx index e2fb35efcd..0e2ed08f08 100644 --- a/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx +++ b/packages/twenty-front/src/pages/onboarding/PaymentSuccess.tsx @@ -1,13 +1,17 @@ -import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { useRecoilValue } from 'recoil'; -import { AnimatedEaseIn, IconCheck, RGBA, UndecoratedLink } from 'twenty-ui'; - import { SubTitle } from '@/auth/components/SubTitle'; import { Title } from '@/auth/components/Title'; import { currentUserState } from '@/auth/states/currentUserState'; import { AppPath } from '@/types/AppPath'; -import { MainButton } from '@/ui/input/button/components/MainButton'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; +import { + AnimatedEaseIn, + IconCheck, + MainButton, + RGBA, + UndecoratedLink, +} from 'twenty-ui'; import { OnboardingStatus } from '~/generated/graphql'; const StyledCheckContainer = styled.div` diff --git a/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx b/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx index 259f519e4e..607e9fe805 100644 --- a/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx +++ b/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx @@ -1,10 +1,3 @@ -import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { useState } from 'react'; -import { useRecoilValue } from 'recoil'; -import { Key } from 'ts-key-enum'; -import { ActionLink, IconGoogle } from 'twenty-ui'; - import { SubTitle } from '@/auth/components/SubTitle'; import { Title } from '@/auth/components/Title'; import { currentUserState } from '@/auth/states/currentUserState'; @@ -13,8 +6,14 @@ import { useSetNextOnboardingStatus } from '@/onboarding/hooks/useSetNextOnboard import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth'; import { AppPath } from '@/types/AppPath'; import { PageHotkeyScope } from '@/types/PageHotkeyScope'; -import { MainButton } from '@/ui/input/button/components/MainButton'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { Key } from 'ts-key-enum'; +import { ActionLink, IconGoogle, MainButton } from 'twenty-ui'; + import { CalendarChannelVisibility, MessageChannelVisibility, diff --git a/packages/twenty-front/src/pages/settings/SettingsBilling.tsx b/packages/twenty-front/src/pages/settings/SettingsBilling.tsx index c1f3f4aa51..e30f3e88d4 100644 --- a/packages/twenty-front/src/pages/settings/SettingsBilling.tsx +++ b/packages/twenty-front/src/pages/settings/SettingsBilling.tsx @@ -1,6 +1,7 @@ import { useState } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { + Button, H2Title, IconCalendarEvent, IconCircleX, @@ -17,7 +18,6 @@ import { SettingsPath } from '@/types/SettingsPath'; import { Info } from '@/ui/display/info/components/Info'; 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 { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; diff --git a/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx b/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx index 6d60faf1bb..0986ea87ee 100644 --- a/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx +++ b/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx @@ -1,18 +1,3 @@ -import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { isNonEmptyArray } from '@sniptt/guards'; -import { useState } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { - AppTooltip, - Avatar, - H2Title, - IconMail, - IconReload, - IconTrash, - TooltipDelay, -} from 'twenty-ui'; - import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; @@ -23,7 +8,6 @@ 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 { IconButton } from '@/ui/input/button/components/IconButton'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; @@ -32,7 +16,22 @@ import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; import { WorkspaceInviteLink } from '@/workspace/components/WorkspaceInviteLink'; import { WorkspaceInviteTeam } from '@/workspace/components/WorkspaceInviteTeam'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { isNonEmptyArray } from '@sniptt/guards'; import { formatDistanceToNow } from 'date-fns'; +import { useState } from 'react'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { + AppTooltip, + Avatar, + H2Title, + IconButton, + IconMail, + IconReload, + IconTrash, + TooltipDelay, +} from 'twenty-ui'; import { useGetWorkspaceInvitationsQuery } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; import { Status } from '../../modules/ui/display/status/components/Status'; 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 c40a6baea2..3668087f85 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPageContent.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPageContent.tsx @@ -4,7 +4,6 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain import { SettingsObjectSummaryCard } from '@/settings/data-model/object-details/components/SettingsObjectSummaryCard'; 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/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState'; @@ -12,7 +11,7 @@ import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; import styled from '@emotion/styled'; import { useNavigate } from 'react-router-dom'; import { useRecoilValue } from 'recoil'; -import { H2Title, IconPlus, UndecoratedLink } from 'twenty-ui'; +import { Button, H2Title, IconPlus, UndecoratedLink } from 'twenty-ui'; import { SettingsObjectFieldTable } from '~/pages/settings/data-model/SettingsObjectFieldTable'; import { SettingsObjectIndexTable } from '~/pages/settings/data-model/SettingsObjectIndexTable'; 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 06cc4999fe..12e5cb4d69 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectEdit.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectEdit.tsx @@ -1,13 +1,5 @@ /* eslint-disable react/jsx-props-no-spreading */ -import { zodResolver } from '@hookform/resolvers/zod'; -import pick from 'lodash.pick'; -import { useEffect } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; -import { useNavigate, useParams } from 'react-router-dom'; -import { H2Title, IconArchive } from 'twenty-ui'; -import { z } from 'zod'; - import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; @@ -26,9 +18,15 @@ 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 { Button } from '@/ui/input/button/components/Button'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; +import { zodResolver } from '@hookform/resolvers/zod'; +import pick from 'lodash.pick'; +import { useEffect } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Button, H2Title, IconArchive } from 'twenty-ui'; +import { z } from 'zod'; const objectEditFormSchema = z .object({}) 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 a08d6658cf..d9be092b99 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx @@ -1,13 +1,3 @@ -import { useApolloClient } from '@apollo/client'; -import { zodResolver } from '@hookform/resolvers/zod'; -import omit from 'lodash.omit'; -import pick from 'lodash.pick'; -import { useEffect } from 'react'; -import { FormProvider, useForm } from 'react-hook-form'; -import { useNavigate, useParams } from 'react-router-dom'; -import { H2Title, IconArchive, IconArchiveOff } from 'twenty-ui'; -import { z } from 'zod'; - import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; @@ -31,9 +21,17 @@ 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 { Button } from '@/ui/input/button/components/Button'; import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; +import { useApolloClient } from '@apollo/client'; +import { zodResolver } from '@hookform/resolvers/zod'; +import omit from 'lodash.omit'; +import pick from 'lodash.pick'; +import { useEffect } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { useNavigate, useParams } from 'react-router-dom'; +import { Button, H2Title, IconArchive, IconArchiveOff } from 'twenty-ui'; +import { z } from 'zod'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { isDefined } from '~/utils/isDefined'; 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 03b3a60c30..fda857733c 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjects.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjects.tsx @@ -1,13 +1,3 @@ -import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { - H2Title, - IconChevronRight, - IconPlus, - IconSearch, - UndecoratedLink, -} from 'twenty-ui'; - import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; @@ -23,7 +13,6 @@ import { SettingsObjectInactiveMenuDropDown } from '@/settings/data-model/object 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/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; @@ -32,8 +21,18 @@ import { Table } from '@/ui/layout/table/components/Table'; import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableSection } from '@/ui/layout/table/components/TableSection'; import { useSortedArray } from '@/ui/layout/table/hooks/useSortedArray'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; import { isNonEmptyArray } from '@sniptt/guards'; import { useMemo, useState } from 'react'; +import { + Button, + H2Title, + IconChevronRight, + IconPlus, + IconSearch, + UndecoratedLink, +} from 'twenty-ui'; import { SETTINGS_OBJECT_TABLE_METADATA } from '~/pages/settings/data-model/constants/SettingsObjectTableMetadata'; import { SettingsObjectTableItem } from '~/pages/settings/data-model/types/SettingsObjectTableItem'; diff --git a/packages/twenty-front/src/pages/settings/developers/SettingsDevelopers.tsx b/packages/twenty-front/src/pages/settings/developers/SettingsDevelopers.tsx index a200a84455..8cf5a0db5b 100644 --- a/packages/twenty-front/src/pages/settings/developers/SettingsDevelopers.tsx +++ b/packages/twenty-front/src/pages/settings/developers/SettingsDevelopers.tsx @@ -1,16 +1,14 @@ -import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; -import styled from '@emotion/styled'; -import { H2Title, IconPlus, MOBILE_VIEWPORT } from 'twenty-ui'; - import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsApiKeysTable } from '@/settings/developers/components/SettingsApiKeysTable'; import { SettingsReadDocumentationButton } from '@/settings/developers/components/SettingsReadDocumentationButton'; import { SettingsWebhooksTable } from '@/settings/developers/components/SettingsWebhooksTable'; 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/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; +import styled from '@emotion/styled'; +import { Button, H2Title, IconPlus, MOBILE_VIEWPORT } from 'twenty-ui'; const StyledButtonContainer = styled.div` display: flex; 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 8d45c760cb..51ae6756ca 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 @@ -4,7 +4,7 @@ import { DateTime } from 'luxon'; import { useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { useRecoilState } from 'recoil'; -import { H2Title, IconRepeat, IconTrash } from 'twenty-ui'; +import { Button, H2Title, IconRepeat, IconTrash } from 'twenty-ui'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; @@ -21,7 +21,6 @@ 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 { 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/components/SubMenuTopBarContainer'; 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 7063102961..83b87c390a 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 @@ -2,8 +2,10 @@ import styled from '@emotion/styled'; import { useMemo, useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; import { + Button, H2Title, IconBox, + IconButton, IconNorthStar, IconPlus, IconRefresh, @@ -25,8 +27,6 @@ import { SettingsDevelopersWebhookUsageGraph } from '@/settings/developers/webho import { SettingsDevelopersWebhookUsageGraphEffect } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { Button } from '@/ui/input/button/components/Button'; -import { IconButton } from '@/ui/input/button/components/IconButton'; import { Select, SelectOption } from '@/ui/input/components/Select'; import { TextArea } from '@/ui/input/components/TextArea'; import { TextInput } from '@/ui/input/components/TextInput'; 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 c694ad6942..81aeb5663d 100644 --- a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctions.tsx +++ b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctions.tsx @@ -1,10 +1,9 @@ import { SettingsServerlessFunctionsTable } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsTable'; 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/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; -import { IconPlus, UndecoratedLink } from 'twenty-ui'; +import { Button, IconPlus, UndecoratedLink } from 'twenty-ui'; export const SettingsServerlessFunctions = () => { return ( diff --git a/packages/twenty-front/tsup.ui.index.tsx b/packages/twenty-front/tsup.ui.index.tsx index 01b5aea3d2..597f9d2cd8 100644 --- a/packages/twenty-front/tsup.ui.index.tsx +++ b/packages/twenty-front/tsup.ui.index.tsx @@ -4,16 +4,6 @@ export { ThemeProvider } from '@emotion/react'; export * from 'twenty-ui'; export * from './src/modules/ui/feedback/progress-bar/components/CircularProgressBar'; export * from './src/modules/ui/feedback/progress-bar/components/ProgressBar'; -export * from './src/modules/ui/input/button/components/Button'; -export * from './src/modules/ui/input/button/components/ButtonGroup'; -export * from './src/modules/ui/input/button/components/FloatingButton'; -export * from './src/modules/ui/input/button/components/FloatingButtonGroup'; -export * from './src/modules/ui/input/button/components/FloatingIconButton'; -export * from './src/modules/ui/input/button/components/FloatingIconButtonGroup'; -export * from './src/modules/ui/input/button/components/LightButton'; -export * from './src/modules/ui/input/button/components/LightIconButton'; -export * from './src/modules/ui/input/button/components/MainButton'; -export * from './src/modules/ui/input/button/components/RoundedIconButton'; export * from './src/modules/ui/input/color-scheme/components/ColorSchemeCard'; export * from './src/modules/ui/input/color-scheme/components/ColorSchemePicker'; export * from './src/modules/ui/input/components/AutosizeTextInput'; diff --git a/packages/twenty-front/src/modules/ui/input/button/components/Button.tsx b/packages/twenty-ui/src/input/button/components/Button.tsx similarity index 99% rename from packages/twenty-front/src/modules/ui/input/button/components/Button.tsx rename to packages/twenty-ui/src/input/button/components/Button.tsx index de07f83039..6a6b49b039 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/Button.tsx +++ b/packages/twenty-ui/src/input/button/components/Button.tsx @@ -1,9 +1,10 @@ import isPropValid from '@emotion/is-prop-valid'; import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { Pill } from '@ui/components'; +import { IconComponent } from '@ui/display'; import React from 'react'; import { Link } from 'react-router-dom'; -import { IconComponent, Pill } from 'twenty-ui'; export type ButtonSize = 'medium' | 'small'; export type ButtonPosition = 'standalone' | 'left' | 'middle' | 'right'; diff --git a/packages/twenty-front/src/modules/ui/input/button/components/ButtonGroup.tsx b/packages/twenty-ui/src/input/button/components/ButtonGroup.tsx similarity index 96% rename from packages/twenty-front/src/modules/ui/input/button/components/ButtonGroup.tsx rename to packages/twenty-ui/src/input/button/components/ButtonGroup.tsx index 9bac9d6ddd..e0a8cff1d0 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/ButtonGroup.tsx +++ b/packages/twenty-ui/src/input/button/components/ButtonGroup.tsx @@ -1,7 +1,7 @@ -import React, { ReactNode } from 'react'; import styled from '@emotion/styled'; +import React, { ReactNode } from 'react'; -import { isDefined } from '~/utils/isDefined'; +import { isDefined } from '@ui/utilities'; import { ButtonPosition, ButtonProps } from './Button'; diff --git a/packages/twenty-front/src/modules/ui/input/button/components/ColorPickerButton.tsx b/packages/twenty-ui/src/input/button/components/ColorPickerButton.tsx similarity index 88% rename from packages/twenty-front/src/modules/ui/input/button/components/ColorPickerButton.tsx rename to packages/twenty-ui/src/input/button/components/ColorPickerButton.tsx index 9a7f9fdb15..8b95062096 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/ColorPickerButton.tsx +++ b/packages/twenty-ui/src/input/button/components/ColorPickerButton.tsx @@ -1,11 +1,10 @@ import { css } from '@emotion/react'; import styled from '@emotion/styled'; -import { ColorSample, ColorSampleProps } from 'twenty-ui'; - +import { ColorSample, ColorSampleProps } from '@ui/display'; import { LightIconButton, LightIconButtonProps, -} from '@/ui/input/button/components/LightIconButton'; +} from '@ui/input/button/components/LightIconButton'; type ColorPickerButtonProps = Pick & Pick & { diff --git a/packages/twenty-front/src/modules/ui/input/button/components/FloatingButton.tsx b/packages/twenty-ui/src/input/button/components/FloatingButton.tsx similarity index 98% rename from packages/twenty-front/src/modules/ui/input/button/components/FloatingButton.tsx rename to packages/twenty-ui/src/input/button/components/FloatingButton.tsx index 820da15de5..9b3aacc210 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/FloatingButton.tsx +++ b/packages/twenty-ui/src/input/button/components/FloatingButton.tsx @@ -1,8 +1,8 @@ import isPropValid from '@emotion/is-prop-valid'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { IconComponent } from '@ui/display'; import { Link } from 'react-router-dom'; -import { IconComponent } from 'twenty-ui'; export type FloatingButtonSize = 'small' | 'medium'; export type FloatingButtonPosition = 'standalone' | 'left' | 'middle' | 'right'; diff --git a/packages/twenty-front/src/modules/ui/input/button/components/FloatingButtonGroup.tsx b/packages/twenty-ui/src/input/button/components/FloatingButtonGroup.tsx similarity index 96% rename from packages/twenty-front/src/modules/ui/input/button/components/FloatingButtonGroup.tsx rename to packages/twenty-ui/src/input/button/components/FloatingButtonGroup.tsx index db9485c175..29edea1254 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/FloatingButtonGroup.tsx +++ b/packages/twenty-ui/src/input/button/components/FloatingButtonGroup.tsx @@ -1,7 +1,7 @@ -import React from 'react'; import styled from '@emotion/styled'; +import React from 'react'; -import { isDefined } from '~/utils/isDefined'; +import { isDefined } from '@ui/utilities'; import { FloatingButtonPosition, FloatingButtonProps } from './FloatingButton'; diff --git a/packages/twenty-front/src/modules/ui/input/button/components/FloatingIconButton.tsx b/packages/twenty-ui/src/input/button/components/FloatingIconButton.tsx similarity index 98% rename from packages/twenty-front/src/modules/ui/input/button/components/FloatingIconButton.tsx rename to packages/twenty-ui/src/input/button/components/FloatingIconButton.tsx index 40d0e86978..328e1b75b7 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/FloatingIconButton.tsx +++ b/packages/twenty-ui/src/input/button/components/FloatingIconButton.tsx @@ -1,7 +1,7 @@ import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { IconComponent } from '@ui/display'; import React from 'react'; -import { IconComponent } from 'twenty-ui'; export type FloatingIconButtonSize = 'small' | 'medium'; export type FloatingIconButtonPosition = diff --git a/packages/twenty-front/src/modules/ui/input/button/components/FloatingIconButtonGroup.tsx b/packages/twenty-ui/src/input/button/components/FloatingIconButtonGroup.tsx similarity index 97% rename from packages/twenty-front/src/modules/ui/input/button/components/FloatingIconButtonGroup.tsx rename to packages/twenty-ui/src/input/button/components/FloatingIconButtonGroup.tsx index 6d9a3ad601..cc7b6d96e2 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/FloatingIconButtonGroup.tsx +++ b/packages/twenty-ui/src/input/button/components/FloatingIconButtonGroup.tsx @@ -1,6 +1,6 @@ -import { MouseEvent } from 'react'; import styled from '@emotion/styled'; -import { IconComponent } from 'twenty-ui'; +import { IconComponent } from '@ui/display'; +import { MouseEvent } from 'react'; import { FloatingIconButton, diff --git a/packages/twenty-front/src/modules/ui/input/button/components/IconButton.tsx b/packages/twenty-ui/src/input/button/components/IconButton.tsx similarity index 99% rename from packages/twenty-front/src/modules/ui/input/button/components/IconButton.tsx rename to packages/twenty-ui/src/input/button/components/IconButton.tsx index dae5a8f1bc..759b1b7dd9 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/IconButton.tsx +++ b/packages/twenty-ui/src/input/button/components/IconButton.tsx @@ -1,7 +1,7 @@ import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { IconComponent } from '@ui/display'; import React from 'react'; -import { IconComponent } from 'twenty-ui'; export type IconButtonSize = 'medium' | 'small'; export type IconButtonPosition = 'standalone' | 'left' | 'middle' | 'right'; diff --git a/packages/twenty-front/src/modules/ui/input/button/components/IconButtonGroup.tsx b/packages/twenty-ui/src/input/button/components/IconButtonGroup.tsx similarity index 96% rename from packages/twenty-front/src/modules/ui/input/button/components/IconButtonGroup.tsx rename to packages/twenty-ui/src/input/button/components/IconButtonGroup.tsx index 32c989c689..9509debfd0 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/IconButtonGroup.tsx +++ b/packages/twenty-ui/src/input/button/components/IconButtonGroup.tsx @@ -1,6 +1,6 @@ -import { MouseEvent } from 'react'; import styled from '@emotion/styled'; -import { IconComponent } from 'twenty-ui'; +import { IconComponent } from '@ui/display'; +import { MouseEvent } from 'react'; import { IconButton, IconButtonPosition, IconButtonProps } from './IconButton'; diff --git a/packages/twenty-front/src/modules/ui/input/button/components/LightButton.tsx b/packages/twenty-ui/src/input/button/components/LightButton.tsx similarity index 98% rename from packages/twenty-front/src/modules/ui/input/button/components/LightButton.tsx rename to packages/twenty-ui/src/input/button/components/LightButton.tsx index 982c77a5d3..9fc383be88 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/LightButton.tsx +++ b/packages/twenty-ui/src/input/button/components/LightButton.tsx @@ -1,7 +1,7 @@ -import { MouseEvent } from 'react'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconComponent } from 'twenty-ui'; +import { IconComponent } from '@ui/display'; +import { MouseEvent } from 'react'; export type LightButtonAccent = 'secondary' | 'tertiary'; diff --git a/packages/twenty-front/src/modules/ui/input/button/components/LightIconButton.tsx b/packages/twenty-ui/src/input/button/components/LightIconButton.tsx similarity index 98% rename from packages/twenty-front/src/modules/ui/input/button/components/LightIconButton.tsx rename to packages/twenty-ui/src/input/button/components/LightIconButton.tsx index 868ed0c534..b81ee5b848 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/LightIconButton.tsx +++ b/packages/twenty-ui/src/input/button/components/LightIconButton.tsx @@ -1,7 +1,7 @@ -import { ComponentProps, MouseEvent } from 'react'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconComponent } from 'twenty-ui'; +import { IconComponent } from '@ui/display'; +import { ComponentProps, MouseEvent } from 'react'; export type LightIconButtonAccent = 'secondary' | 'tertiary'; export type LightIconButtonSize = 'small' | 'medium'; diff --git a/packages/twenty-front/src/modules/ui/input/button/components/LightIconButtonGroup.tsx b/packages/twenty-ui/src/input/button/components/LightIconButtonGroup.tsx similarity index 96% rename from packages/twenty-front/src/modules/ui/input/button/components/LightIconButtonGroup.tsx rename to packages/twenty-ui/src/input/button/components/LightIconButtonGroup.tsx index 7c0459252d..6f6d530a4e 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/LightIconButtonGroup.tsx +++ b/packages/twenty-ui/src/input/button/components/LightIconButtonGroup.tsx @@ -1,6 +1,6 @@ -import { FunctionComponent, MouseEvent, ReactElement } from 'react'; import styled from '@emotion/styled'; -import { IconComponent } from 'twenty-ui'; +import { IconComponent } from '@ui/display'; +import { FunctionComponent, MouseEvent, ReactElement } from 'react'; import { LightIconButton, LightIconButtonProps } from './LightIconButton'; diff --git a/packages/twenty-front/src/modules/ui/input/button/components/MainButton.tsx b/packages/twenty-ui/src/input/button/components/MainButton.tsx similarity index 98% rename from packages/twenty-front/src/modules/ui/input/button/components/MainButton.tsx rename to packages/twenty-ui/src/input/button/components/MainButton.tsx index cb9dbd2719..ddfba85f25 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/MainButton.tsx +++ b/packages/twenty-ui/src/input/button/components/MainButton.tsx @@ -1,7 +1,7 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { IconComponent } from '@ui/display'; import React from 'react'; -import { IconComponent } from 'twenty-ui'; export type MainButtonVariant = 'primary' | 'secondary'; diff --git a/packages/twenty-front/src/modules/ui/input/button/components/RoundedIconButton.tsx b/packages/twenty-ui/src/input/button/components/RoundedIconButton.tsx similarity index 95% rename from packages/twenty-front/src/modules/ui/input/button/components/RoundedIconButton.tsx rename to packages/twenty-ui/src/input/button/components/RoundedIconButton.tsx index 3d9e89a264..1ede7c2393 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/RoundedIconButton.tsx +++ b/packages/twenty-ui/src/input/button/components/RoundedIconButton.tsx @@ -1,6 +1,6 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconComponent } from 'twenty-ui'; +import { IconComponent } from '@ui/display'; const StyledIconButton = styled.button` align-items: center; diff --git a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/Button.docs.mdx b/packages/twenty-ui/src/input/button/components/__stories__/Button.docs.mdx similarity index 100% rename from packages/twenty-front/src/modules/ui/input/button/components/__stories__/Button.docs.mdx rename to packages/twenty-ui/src/input/button/components/__stories__/Button.docs.mdx diff --git a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/Button.stories.tsx b/packages/twenty-ui/src/input/button/components/__stories__/Button.stories.tsx similarity index 99% rename from packages/twenty-front/src/modules/ui/input/button/components/__stories__/Button.stories.tsx rename to packages/twenty-ui/src/input/button/components/__stories__/Button.stories.tsx index 3165cc5ee8..d67f0d399f 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/Button.stories.tsx +++ b/packages/twenty-ui/src/input/button/components/__stories__/Button.stories.tsx @@ -1,11 +1,10 @@ import { Meta, StoryObj } from '@storybook/react'; +import { IconSearch } from '@ui/display'; import { CatalogDecorator, CatalogStory, ComponentDecorator, - IconSearch, -} from 'twenty-ui'; - +} from '@ui/testing'; import { Button, ButtonAccent, diff --git a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/ButtonGroup.stories.tsx b/packages/twenty-ui/src/input/button/components/__stories__/ButtonGroup.stories.tsx similarity index 95% rename from packages/twenty-front/src/modules/ui/input/button/components/__stories__/ButtonGroup.stories.tsx rename to packages/twenty-ui/src/input/button/components/__stories__/ButtonGroup.stories.tsx index 803bbbb7dd..ec4e492c7f 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/ButtonGroup.stories.tsx +++ b/packages/twenty-ui/src/input/button/components/__stories__/ButtonGroup.stories.tsx @@ -1,13 +1,10 @@ import { Meta, StoryObj } from '@storybook/react'; +import { IconCheckbox, IconNotes, IconTimelineEvent } from '@ui/display'; import { CatalogDecorator, CatalogStory, ComponentDecorator, - IconCheckbox, - IconNotes, - IconTimelineEvent, -} from 'twenty-ui'; - +} from '@ui/testing'; import { Button, ButtonAccent, ButtonSize, ButtonVariant } from '../Button'; import { ButtonGroup } from '../ButtonGroup'; diff --git a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/ColorPickerButton.stories.tsx b/packages/twenty-ui/src/input/button/components/__stories__/ColorPickerButton.stories.tsx similarity index 90% rename from packages/twenty-front/src/modules/ui/input/button/components/__stories__/ColorPickerButton.stories.tsx rename to packages/twenty-ui/src/input/button/components/__stories__/ColorPickerButton.stories.tsx index e12c5d0631..3d28791416 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/ColorPickerButton.stories.tsx +++ b/packages/twenty-ui/src/input/button/components/__stories__/ColorPickerButton.stories.tsx @@ -1,6 +1,5 @@ import { Meta, StoryObj } from '@storybook/react'; -import { ComponentDecorator } from 'twenty-ui'; - +import { ComponentDecorator } from '@ui/testing'; import { ColorPickerButton } from '../ColorPickerButton'; const meta: Meta = { diff --git a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/FloatingButton.stories.tsx b/packages/twenty-ui/src/input/button/components/__stories__/FloatingButton.stories.tsx similarity index 96% rename from packages/twenty-front/src/modules/ui/input/button/components/__stories__/FloatingButton.stories.tsx rename to packages/twenty-ui/src/input/button/components/__stories__/FloatingButton.stories.tsx index 5e9272318c..b0ce9fc138 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/FloatingButton.stories.tsx +++ b/packages/twenty-ui/src/input/button/components/__stories__/FloatingButton.stories.tsx @@ -1,11 +1,10 @@ import { Meta, StoryObj } from '@storybook/react'; +import { IconSearch } from '@ui/display'; import { CatalogDecorator, CatalogStory, ComponentDecorator, - IconSearch, -} from 'twenty-ui'; - +} from '@ui/testing'; import { FloatingButton, FloatingButtonSize } from '../FloatingButton'; const meta: Meta = { diff --git a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/FloatingButtonGroup.stories.tsx b/packages/twenty-ui/src/input/button/components/__stories__/FloatingButtonGroup.stories.tsx similarity index 93% rename from packages/twenty-front/src/modules/ui/input/button/components/__stories__/FloatingButtonGroup.stories.tsx rename to packages/twenty-ui/src/input/button/components/__stories__/FloatingButtonGroup.stories.tsx index e3b8f0e28d..248ed0c5ea 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/FloatingButtonGroup.stories.tsx +++ b/packages/twenty-ui/src/input/button/components/__stories__/FloatingButtonGroup.stories.tsx @@ -1,13 +1,10 @@ import { Meta, StoryObj } from '@storybook/react'; +import { IconCheckbox, IconNotes, IconTimelineEvent } from '@ui/display'; import { CatalogDecorator, CatalogStory, ComponentDecorator, - IconCheckbox, - IconNotes, - IconTimelineEvent, -} from 'twenty-ui'; - +} from '@ui/testing'; import { FloatingButton, FloatingButtonSize } from '../FloatingButton'; import { FloatingButtonGroup } from '../FloatingButtonGroup'; diff --git a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/FloatingIconButton.stories.tsx b/packages/twenty-ui/src/input/button/components/__stories__/FloatingIconButton.stories.tsx similarity index 96% rename from packages/twenty-front/src/modules/ui/input/button/components/__stories__/FloatingIconButton.stories.tsx rename to packages/twenty-ui/src/input/button/components/__stories__/FloatingIconButton.stories.tsx index 6a0e973b70..71c4e0c56d 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/FloatingIconButton.stories.tsx +++ b/packages/twenty-ui/src/input/button/components/__stories__/FloatingIconButton.stories.tsx @@ -1,11 +1,10 @@ import { Meta, StoryObj } from '@storybook/react'; +import { IconSearch } from '@ui/display'; import { CatalogDecorator, CatalogStory, ComponentDecorator, - IconSearch, -} from 'twenty-ui'; - +} from '@ui/testing'; import { FloatingIconButton, FloatingIconButtonSize, diff --git a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/FloatingIconButtonGroup.stories.tsx b/packages/twenty-ui/src/input/button/components/__stories__/FloatingIconButtonGroup.stories.tsx similarity index 92% rename from packages/twenty-front/src/modules/ui/input/button/components/__stories__/FloatingIconButtonGroup.stories.tsx rename to packages/twenty-ui/src/input/button/components/__stories__/FloatingIconButtonGroup.stories.tsx index 3317f6a064..494a644cbc 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/FloatingIconButtonGroup.stories.tsx +++ b/packages/twenty-ui/src/input/button/components/__stories__/FloatingIconButtonGroup.stories.tsx @@ -1,13 +1,10 @@ import { Meta, StoryObj } from '@storybook/react'; +import { IconCheckbox, IconNotes, IconTimelineEvent } from '@ui/display'; import { CatalogDecorator, CatalogStory, ComponentDecorator, - IconCheckbox, - IconNotes, - IconTimelineEvent, -} from 'twenty-ui'; - +} from '@ui/testing'; import { FloatingIconButtonSize } from '../FloatingIconButton'; import { FloatingIconButtonGroup } from '../FloatingIconButtonGroup'; diff --git a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/IconButton.stories.tsx b/packages/twenty-ui/src/input/button/components/__stories__/IconButton.stories.tsx similarity index 98% rename from packages/twenty-front/src/modules/ui/input/button/components/__stories__/IconButton.stories.tsx rename to packages/twenty-ui/src/input/button/components/__stories__/IconButton.stories.tsx index 7063356417..538a987539 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/IconButton.stories.tsx +++ b/packages/twenty-ui/src/input/button/components/__stories__/IconButton.stories.tsx @@ -1,11 +1,10 @@ import { Meta, StoryObj } from '@storybook/react'; +import { IconSearch } from '@ui/display'; import { CatalogDecorator, CatalogStory, ComponentDecorator, - IconSearch, -} from 'twenty-ui'; - +} from '@ui/testing'; import { IconButton, IconButtonAccent, diff --git a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/IconButtonGroup.stories.tsx b/packages/twenty-ui/src/input/button/components/__stories__/IconButtonGroup.stories.tsx similarity index 94% rename from packages/twenty-front/src/modules/ui/input/button/components/__stories__/IconButtonGroup.stories.tsx rename to packages/twenty-ui/src/input/button/components/__stories__/IconButtonGroup.stories.tsx index f4bc972e8a..a9a431b4dc 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/IconButtonGroup.stories.tsx +++ b/packages/twenty-ui/src/input/button/components/__stories__/IconButtonGroup.stories.tsx @@ -1,13 +1,10 @@ import { Meta, StoryObj } from '@storybook/react'; +import { IconCheckbox, IconNotes, IconTimelineEvent } from '@ui/display'; import { CatalogDecorator, CatalogStory, ComponentDecorator, - IconCheckbox, - IconNotes, - IconTimelineEvent, -} from 'twenty-ui'; - +} from '@ui/testing'; import { IconButtonAccent, IconButtonSize, diff --git a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/LightButton.stories.tsx b/packages/twenty-ui/src/input/button/components/__stories__/LightButton.stories.tsx similarity index 97% rename from packages/twenty-front/src/modules/ui/input/button/components/__stories__/LightButton.stories.tsx rename to packages/twenty-ui/src/input/button/components/__stories__/LightButton.stories.tsx index ddf9de2a27..d5a3ceacb7 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/LightButton.stories.tsx +++ b/packages/twenty-ui/src/input/button/components/__stories__/LightButton.stories.tsx @@ -1,11 +1,10 @@ import { Meta, StoryObj } from '@storybook/react'; +import { IconSearch } from '@ui/display'; import { CatalogDecorator, CatalogStory, ComponentDecorator, - IconSearch, -} from 'twenty-ui'; - +} from '@ui/testing'; import { LightButton, LightButtonAccent } from '../LightButton'; const meta: Meta = { diff --git a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/LightIconButton.stories.tsx b/packages/twenty-ui/src/input/button/components/__stories__/LightIconButton.stories.tsx similarity index 97% rename from packages/twenty-front/src/modules/ui/input/button/components/__stories__/LightIconButton.stories.tsx rename to packages/twenty-ui/src/input/button/components/__stories__/LightIconButton.stories.tsx index 58069b57e5..a2d40a79db 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/LightIconButton.stories.tsx +++ b/packages/twenty-ui/src/input/button/components/__stories__/LightIconButton.stories.tsx @@ -1,11 +1,10 @@ import { Meta, StoryObj } from '@storybook/react'; +import { IconSearch } from '@ui/display'; import { CatalogDecorator, CatalogStory, ComponentDecorator, - IconSearch, -} from 'twenty-ui'; - +} from '@ui/testing'; import { LightIconButton, LightIconButtonAccent, diff --git a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/MainButton.stories.tsx b/packages/twenty-ui/src/input/button/components/__stories__/MainButton.stories.tsx similarity index 93% rename from packages/twenty-front/src/modules/ui/input/button/components/__stories__/MainButton.stories.tsx rename to packages/twenty-ui/src/input/button/components/__stories__/MainButton.stories.tsx index 631d8258fa..c1f715bd03 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/MainButton.stories.tsx +++ b/packages/twenty-ui/src/input/button/components/__stories__/MainButton.stories.tsx @@ -1,7 +1,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { expect, fn, userEvent, within } from '@storybook/test'; -import { ComponentDecorator, IconBrandGoogle } from 'twenty-ui'; - +import { IconBrandGoogle } from '@ui/display'; +import { ComponentDecorator } from '@ui/testing'; import { MainButton } from '../MainButton'; const clickJestFn = fn(); diff --git a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/RoundedIconButton.stories.tsx b/packages/twenty-ui/src/input/button/components/__stories__/RoundedIconButton.stories.tsx similarity index 89% rename from packages/twenty-front/src/modules/ui/input/button/components/__stories__/RoundedIconButton.stories.tsx rename to packages/twenty-ui/src/input/button/components/__stories__/RoundedIconButton.stories.tsx index da2d19b824..e36e3eadd1 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/__stories__/RoundedIconButton.stories.tsx +++ b/packages/twenty-ui/src/input/button/components/__stories__/RoundedIconButton.stories.tsx @@ -1,7 +1,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { expect, fn, userEvent, within } from '@storybook/test'; -import { ComponentDecorator, IconArrowRight } from 'twenty-ui'; - +import { IconArrowRight } from '@ui/display'; +import { ComponentDecorator } from '@ui/testing'; import { RoundedIconButton } from '../RoundedIconButton'; const clickJestFn = fn(); diff --git a/packages/twenty-ui/src/input/index.ts b/packages/twenty-ui/src/input/index.ts index 133f827052..0733094165 100644 --- a/packages/twenty-ui/src/input/index.ts +++ b/packages/twenty-ui/src/input/index.ts @@ -1 +1,15 @@ +export * from './button/components/Button'; +export * from './button/components/ButtonGroup'; +export * from './button/components/ColorPickerButton'; +export * from './button/components/FloatingButton'; +export * from './button/components/FloatingButtonGroup'; +export * from './button/components/FloatingIconButton'; +export * from './button/components/FloatingIconButtonGroup'; +export * from './button/components/IconButton'; +export * from './button/components/IconButtonGroup'; +export * from './button/components/LightButton'; +export * from './button/components/LightIconButton'; +export * from './button/components/LightIconButtonGroup'; +export * from './button/components/MainButton'; +export * from './button/components/RoundedIconButton'; export * from './components/Toggle'; From 5ad8ff81f923ad2f6856498308b3c2676a7a85f3 Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:25:21 +0200 Subject: [PATCH 116/123] [Server Integration tests] Enrich integration GraphQL API tests #2 (#7978) This PR was created by [GitStart](https://gitstart.com/) to address the requirements from this ticket: [TWNTY-7526](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-7526). --- ### Description For workspace members, the deletion of multiple members is a special case that is not permitted by the method for regular users. As a result, we ensure that multiple deletions are not processed. For certain tests, both an account ID and a user ID are required. We are utilizing Tim's account for all testing purposes, as specified by the token in `jest-integration.config.ts`. To streamline this process, we have defined a constant to store and reference the account ID and user ID during testing.Refs #7526 ### Dem ![](https://assets-service.gitstart.com/16336/4df04650-70ff-4eb6-b43f-25edecc8e66f.png) Co-authored-by: gitstart-twenty --- .../graphql/integration.constants.ts | 2 + ...all-api-keys-resolvers.integration-spec.ts | 404 +++++++++++++ ...-attachments-resolvers.integration-spec.ts | 440 ++++++++++++++ ...l-audit-logs-resolvers.integration-spec.ts | 421 ++++++++++++++ ...l-blocklists-resolvers.integration-spec.ts | 406 +++++++++++++ ...associations-resolvers.integration-spec.ts | 535 ++++++++++++++++++ .../all-calendar-channels.integration-spec.ts | 482 ++++++++++++++++ ...pace-members-resolvers.integration-spec.ts | 439 ++++++++++++++ 8 files changed, 3129 insertions(+) create mode 100644 packages/twenty-server/test/integration/graphql/suites/all-api-keys-resolvers.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/all-attachments-resolvers.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/all-audit-logs-resolvers.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/all-blocklists-resolvers.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/all-calendar-channel-event-associations-resolvers.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/all-calendar-channels.integration-spec.ts create mode 100644 packages/twenty-server/test/integration/graphql/suites/all-workspace-members-resolvers.integration-spec.ts diff --git a/packages/twenty-server/test/integration/graphql/integration.constants.ts b/packages/twenty-server/test/integration/graphql/integration.constants.ts index e5391d40b1..e86bb95de3 100644 --- a/packages/twenty-server/test/integration/graphql/integration.constants.ts +++ b/packages/twenty-server/test/integration/graphql/integration.constants.ts @@ -1 +1,3 @@ export const TIM_ACCOUNT_ID = '20202020-0687-4c41-b707-ed1bfca972a7'; + +export const TIM_USER_ID = '20202020-9e3b-46d4-a556-88b9ddc2b034'; diff --git a/packages/twenty-server/test/integration/graphql/suites/all-api-keys-resolvers.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/all-api-keys-resolvers.integration-spec.ts new file mode 100644 index 0000000000..369ea84309 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/all-api-keys-resolvers.integration-spec.ts @@ -0,0 +1,404 @@ +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 API_KEY_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; +const API_KEY_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; +const API_KEY_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; +const API_KEY_GQL_FIELDS = ` + id + name + expiresAt + revokedAt + createdAt + updatedAt + deletedAt +`; + +describe('apiKeys resolvers (integration)', () => { + it('1. should create and return API keys', async () => { + const apiKeyName1 = generateRecordName(API_KEY_1_ID); + const apiKeyName2 = generateRecordName(API_KEY_2_ID); + + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'apiKey', + objectMetadataPluralName: 'apiKeys', + gqlFields: API_KEY_GQL_FIELDS, + data: [ + { + id: API_KEY_1_ID, + name: apiKeyName1, + expiresAt: new Date(), + }, + { + id: API_KEY_2_ID, + name: apiKeyName2, + expiresAt: new Date(), + }, + ], + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.createApiKeys).toHaveLength(2); + + response.body.data.createApiKeys.forEach((apiKey) => { + expect(apiKey).toHaveProperty('name'); + expect([apiKeyName1, apiKeyName2]).toContain(apiKey.name); + expect(apiKey).toHaveProperty('expiresAt'); + expect(apiKey).toHaveProperty('revokedAt'); + expect(apiKey).toHaveProperty('id'); + expect(apiKey).toHaveProperty('createdAt'); + expect(apiKey).toHaveProperty('updatedAt'); + expect(apiKey).toHaveProperty('deletedAt'); + }); + }); + + it('1b. should create and return one API key', async () => { + const apiKeyName = generateRecordName(API_KEY_3_ID); + + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'apiKey', + gqlFields: API_KEY_GQL_FIELDS, + data: { + id: API_KEY_3_ID, + name: apiKeyName, + expiresAt: new Date(), + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdApiKey = response.body.data.createApiKey; + + expect(createdApiKey).toHaveProperty('name'); + expect(createdApiKey.name).toEqual(apiKeyName); + expect(createdApiKey).toHaveProperty('expiresAt'); + expect(createdApiKey).toHaveProperty('revokedAt'); + expect(createdApiKey).toHaveProperty('id'); + expect(createdApiKey).toHaveProperty('createdAt'); + expect(createdApiKey).toHaveProperty('updatedAt'); + expect(createdApiKey).toHaveProperty('deletedAt'); + }); + + it('2. should find many API keys', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'apiKey', + objectMetadataPluralName: 'apiKeys', + gqlFields: API_KEY_GQL_FIELDS, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const data = response.body.data.apiKeys; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + const edges = data.edges; + + if (edges.length > 0) { + const apiKeys = edges[0].node; + + expect(apiKeys).toHaveProperty('name'); + expect(apiKeys).toHaveProperty('expiresAt'); + expect(apiKeys).toHaveProperty('revokedAt'); + expect(apiKeys).toHaveProperty('id'); + expect(apiKeys).toHaveProperty('createdAt'); + expect(apiKeys).toHaveProperty('updatedAt'); + expect(apiKeys).toHaveProperty('deletedAt'); + } + }); + + it('2b. should find one API key', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'apiKey', + gqlFields: API_KEY_GQL_FIELDS, + filter: { + id: { + eq: API_KEY_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const apiKey = response.body.data.apiKey; + + expect(apiKey).toHaveProperty('name'); + expect(apiKey).toHaveProperty('expiresAt'); + expect(apiKey).toHaveProperty('revokedAt'); + expect(apiKey).toHaveProperty('id'); + expect(apiKey).toHaveProperty('createdAt'); + expect(apiKey).toHaveProperty('updatedAt'); + expect(apiKey).toHaveProperty('deletedAt'); + }); + + it('3. should update many API keys', async () => { + const graphqlOperation = updateManyOperationFactory({ + objectMetadataSingularName: 'apiKey', + objectMetadataPluralName: 'apiKeys', + gqlFields: API_KEY_GQL_FIELDS, + data: { + name: 'Updated Name', + }, + filter: { + id: { + in: [API_KEY_1_ID, API_KEY_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedApiKeys = response.body.data.updateApiKeys; + + expect(updatedApiKeys).toHaveLength(2); + + updatedApiKeys.forEach((apiKey) => { + expect(apiKey.name).toEqual('Updated Name'); + }); + }); + + it('3b. should update one API key', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'apiKey', + gqlFields: API_KEY_GQL_FIELDS, + data: { + name: 'New Name', + }, + recordId: API_KEY_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedApiKey = response.body.data.updateApiKey; + + expect(updatedApiKey.name).toEqual('New Name'); + }); + + it('4. should find many API keys with updated name', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'apiKey', + objectMetadataPluralName: 'apiKeys', + gqlFields: API_KEY_GQL_FIELDS, + filter: { + name: { + eq: 'Updated Name', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.apiKeys.edges).toHaveLength(2); + }); + + it('4b. should find one API key with updated name', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'apiKey', + gqlFields: API_KEY_GQL_FIELDS, + filter: { + name: { + eq: 'New Name', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.apiKey.name).toEqual('New Name'); + }); + + it('5. should delete many API keys', async () => { + const graphqlOperation = deleteManyOperationFactory({ + objectMetadataSingularName: 'apiKey', + objectMetadataPluralName: 'apiKeys', + gqlFields: API_KEY_GQL_FIELDS, + filter: { + id: { + in: [API_KEY_1_ID, API_KEY_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const deletedApiKeys = response.body.data.deleteApiKeys; + + expect(deletedApiKeys).toHaveLength(2); + + deletedApiKeys.forEach((apiKey) => { + expect(apiKey.deletedAt).toBeTruthy(); + }); + }); + + it('5b. should delete one API key', async () => { + const graphqlOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'apiKey', + gqlFields: API_KEY_GQL_FIELDS, + recordId: API_KEY_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.deleteApiKey.deletedAt).toBeTruthy(); + }); + + it('6. should not find many API keys anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'apiKey', + objectMetadataPluralName: 'apiKeys', + gqlFields: API_KEY_GQL_FIELDS, + filter: { + id: { + in: [API_KEY_1_ID, API_KEY_2_ID], + }, + }, + }); + + const findApiKeysResponse = await makeGraphqlAPIRequest(graphqlOperation); + + expect(findApiKeysResponse.body.data.apiKeys.edges).toHaveLength(0); + }); + + it('6b. should not find one API key anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'apiKey', + gqlFields: API_KEY_GQL_FIELDS, + filter: { + id: { + eq: API_KEY_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.apiKey).toBeNull(); + }); + + it('7. should find many deleted API keys with deletedAt filter', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'apiKey', + objectMetadataPluralName: 'apiKeys', + gqlFields: API_KEY_GQL_FIELDS, + filter: { + id: { + in: [API_KEY_1_ID, API_KEY_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.apiKeys.edges).toHaveLength(2); + }); + + it('7b. should find one deleted API key with deletedAt filter', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'apiKey', + gqlFields: API_KEY_GQL_FIELDS, + filter: { + id: { + eq: API_KEY_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.apiKey.id).toEqual(API_KEY_3_ID); + }); + + it('8. should destroy many API keys', async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'apiKey', + objectMetadataPluralName: 'apiKeys', + gqlFields: API_KEY_GQL_FIELDS, + filter: { + id: { + in: [API_KEY_1_ID, API_KEY_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.destroyApiKeys).toHaveLength(2); + }); + + it('8b. should destroy one API key', async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'apiKey', + gqlFields: API_KEY_GQL_FIELDS, + recordId: API_KEY_3_ID, + }); + + const destroyApiKeyResponse = await makeGraphqlAPIRequest(graphqlOperation); + + expect(destroyApiKeyResponse.body.data.destroyApiKey).toBeTruthy(); + }); + + it('9. should not find many API keys anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'apiKey', + objectMetadataPluralName: 'apiKeys', + gqlFields: API_KEY_GQL_FIELDS, + filter: { + id: { + in: [API_KEY_1_ID, API_KEY_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.apiKeys.edges).toHaveLength(0); + }); + + it('9b. should not find one API key anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'apiKey', + gqlFields: API_KEY_GQL_FIELDS, + filter: { + id: { + eq: API_KEY_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.apiKey).toBeNull(); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/all-attachments-resolvers.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/all-attachments-resolvers.integration-spec.ts new file mode 100644 index 0000000000..c8113b112f --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/all-attachments-resolvers.integration-spec.ts @@ -0,0 +1,440 @@ +import { TIM_ACCOUNT_ID } from 'test/integration/graphql/integration.constants'; +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 ATTACHMENT_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; +const ATTACHMENT_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; +const ATTACHMENT_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; + +const ATTACHMENT_GQL_FIELDS = ` + id + name + fullPath + type + createdAt + updatedAt + deletedAt + authorId + activityId + taskId + noteId + personId + companyId + opportunityId +`; + +describe('attachments resolvers (integration)', () => { + it('1. should create and return multiple attachments', async () => { + const attachmentName1 = generateRecordName(ATTACHMENT_1_ID); + const attachmentName2 = generateRecordName(ATTACHMENT_2_ID); + + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'attachment', + objectMetadataPluralName: 'attachments', + gqlFields: ATTACHMENT_GQL_FIELDS, + data: [ + { + id: ATTACHMENT_1_ID, + name: attachmentName1, + authorId: TIM_ACCOUNT_ID, + }, + { + id: ATTACHMENT_2_ID, + name: attachmentName2, + authorId: TIM_ACCOUNT_ID, + }, + ], + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.createAttachments).toHaveLength(2); + + response.body.data.createAttachments.forEach((attachment) => { + expect(attachment).toHaveProperty('name'); + expect([attachmentName1, attachmentName2]).toContain(attachment.name); + expect(attachment).toHaveProperty('fullPath'); + expect(attachment).toHaveProperty('type'); + expect(attachment).toHaveProperty('id'); + expect(attachment).toHaveProperty('createdAt'); + expect(attachment).toHaveProperty('updatedAt'); + expect(attachment).toHaveProperty('deletedAt'); + expect(attachment).toHaveProperty('authorId'); + expect(attachment).toHaveProperty('activityId'); + expect(attachment).toHaveProperty('taskId'); + expect(attachment).toHaveProperty('noteId'); + expect(attachment).toHaveProperty('personId'); + expect(attachment).toHaveProperty('companyId'); + expect(attachment).toHaveProperty('opportunityId'); + }); + }); + + it('2. should create and return one attachment', async () => { + const attachmentName = generateRecordName(ATTACHMENT_3_ID); + + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'attachment', + gqlFields: ATTACHMENT_GQL_FIELDS, + data: { + id: ATTACHMENT_3_ID, + name: attachmentName, + authorId: TIM_ACCOUNT_ID, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdAttachment = response.body.data.createAttachment; + + expect(createdAttachment).toHaveProperty('name', attachmentName); + expect(createdAttachment).toHaveProperty('fullPath'); + expect(createdAttachment).toHaveProperty('type'); + expect(createdAttachment).toHaveProperty('id'); + expect(createdAttachment).toHaveProperty('createdAt'); + expect(createdAttachment).toHaveProperty('updatedAt'); + expect(createdAttachment).toHaveProperty('deletedAt'); + expect(createdAttachment).toHaveProperty('authorId'); + expect(createdAttachment).toHaveProperty('activityId'); + expect(createdAttachment).toHaveProperty('taskId'); + expect(createdAttachment).toHaveProperty('noteId'); + expect(createdAttachment).toHaveProperty('personId'); + expect(createdAttachment).toHaveProperty('companyId'); + expect(createdAttachment).toHaveProperty('opportunityId'); + }); + + it('2. should find many attachments', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'attachment', + objectMetadataPluralName: 'attachments', + gqlFields: ATTACHMENT_GQL_FIELDS, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const data = response.body.data.attachments; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + if (data.edges.length > 0) { + const attachments = data.edges[0].node; + + expect(attachments).toHaveProperty('name'); + expect(attachments).toHaveProperty('fullPath'); + expect(attachments).toHaveProperty('type'); + expect(attachments).toHaveProperty('id'); + expect(attachments).toHaveProperty('createdAt'); + expect(attachments).toHaveProperty('updatedAt'); + expect(attachments).toHaveProperty('deletedAt'); + expect(attachments).toHaveProperty('authorId'); + expect(attachments).toHaveProperty('activityId'); + expect(attachments).toHaveProperty('taskId'); + expect(attachments).toHaveProperty('noteId'); + expect(attachments).toHaveProperty('personId'); + expect(attachments).toHaveProperty('companyId'); + expect(attachments).toHaveProperty('opportunityId'); + } + }); + + it('2b. should find one attachment', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'attachment', + gqlFields: ATTACHMENT_GQL_FIELDS, + filter: { + id: { + eq: ATTACHMENT_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const attachment = response.body.data.attachment; + + expect(attachment).toHaveProperty('name'); + expect(attachment).toHaveProperty('fullPath'); + expect(attachment).toHaveProperty('type'); + expect(attachment).toHaveProperty('id'); + expect(attachment).toHaveProperty('createdAt'); + expect(attachment).toHaveProperty('updatedAt'); + expect(attachment).toHaveProperty('deletedAt'); + expect(attachment).toHaveProperty('authorId'); + expect(attachment).toHaveProperty('activityId'); + expect(attachment).toHaveProperty('taskId'); + expect(attachment).toHaveProperty('noteId'); + expect(attachment).toHaveProperty('personId'); + expect(attachment).toHaveProperty('companyId'); + expect(attachment).toHaveProperty('opportunityId'); + }); + + it('3. should update many attachments', async () => { + const graphqlOperation = updateManyOperationFactory({ + objectMetadataSingularName: 'attachment', + objectMetadataPluralName: 'attachments', + gqlFields: ATTACHMENT_GQL_FIELDS, + data: { + name: 'Updated Name', + }, + filter: { + id: { + in: [ATTACHMENT_1_ID, ATTACHMENT_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedAttachments = response.body.data.updateAttachments; + + expect(updatedAttachments).toHaveLength(2); + + updatedAttachments.forEach((attachment) => { + expect(attachment.name).toEqual('Updated Name'); + }); + }); + + it('3b. should update one attachment', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'attachment', + gqlFields: ATTACHMENT_GQL_FIELDS, + data: { + name: 'New Name', + }, + recordId: ATTACHMENT_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedAttachment = response.body.data.updateAttachment; + + expect(updatedAttachment.name).toEqual('New Name'); + }); + + it('4. should find many attachments with updated name', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'attachment', + objectMetadataPluralName: 'attachments', + gqlFields: ATTACHMENT_GQL_FIELDS, + filter: { + name: { + eq: 'Updated Name', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.attachments.edges).toHaveLength(2); + }); + + it('4b. should find one attachment with updated name', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'attachment', + gqlFields: ATTACHMENT_GQL_FIELDS, + filter: { + name: { + eq: 'New Name', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.attachment.name).toEqual('New Name'); + }); + + it('5. should delete many attachments', async () => { + const graphqlOperation = deleteManyOperationFactory({ + objectMetadataSingularName: 'attachment', + objectMetadataPluralName: 'attachments', + gqlFields: ATTACHMENT_GQL_FIELDS, + filter: { + id: { + in: [ATTACHMENT_1_ID, ATTACHMENT_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const deletedAttachments = response.body.data.deleteAttachments; + + expect(deletedAttachments).toHaveLength(2); + + deletedAttachments.forEach((attachment) => { + expect(attachment.deletedAt).toBeTruthy(); + }); + }); + + it('5b. should delete one attachment', async () => { + const graphqlOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'attachment', + gqlFields: ATTACHMENT_GQL_FIELDS, + recordId: ATTACHMENT_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.deleteAttachment.deletedAt).toBeTruthy(); + }); + + it('6. should not find many attachments anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'attachment', + objectMetadataPluralName: 'attachments', + gqlFields: ATTACHMENT_GQL_FIELDS, + filter: { + id: { + in: [ATTACHMENT_1_ID, ATTACHMENT_2_ID], + }, + }, + }); + + const findAttachmentsResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect(findAttachmentsResponse.body.data.attachments.edges).toHaveLength(0); + }); + + it('6b. should not find one attachment anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'attachment', + gqlFields: ATTACHMENT_GQL_FIELDS, + filter: { + id: { + eq: ATTACHMENT_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.attachment).toBeNull(); + }); + + it('7. should find many deleted attachments with deletedAt filter', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'attachment', + objectMetadataPluralName: 'attachments', + gqlFields: ATTACHMENT_GQL_FIELDS, + filter: { + id: { + in: [ATTACHMENT_1_ID, ATTACHMENT_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.attachments.edges).toHaveLength(2); + }); + + it('7b. should find one deleted attachment with deletedAt filter', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'attachment', + gqlFields: ATTACHMENT_GQL_FIELDS, + filter: { + id: { + eq: ATTACHMENT_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.attachment.id).toEqual(ATTACHMENT_3_ID); + }); + + it('8. should destroy many attachments', async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'attachment', + objectMetadataPluralName: 'attachments', + gqlFields: ATTACHMENT_GQL_FIELDS, + filter: { + id: { + in: [ATTACHMENT_1_ID, ATTACHMENT_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.destroyAttachments).toHaveLength(2); + }); + + it('8b. should destroy one attachment', async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'attachment', + gqlFields: ATTACHMENT_GQL_FIELDS, + recordId: ATTACHMENT_3_ID, + }); + + const destroyAttachmentResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect(destroyAttachmentResponse.body.data.destroyAttachment).toBeTruthy(); + }); + + it('9. should not find many attachments anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'attachment', + objectMetadataPluralName: 'attachments', + gqlFields: ATTACHMENT_GQL_FIELDS, + filter: { + id: { + in: [ATTACHMENT_1_ID, ATTACHMENT_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.attachments.edges).toHaveLength(0); + }); + + it('9b. should not find one attachment anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'attachment', + gqlFields: ATTACHMENT_GQL_FIELDS, + filter: { + id: { + eq: ATTACHMENT_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.attachment).toBeNull(); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/all-audit-logs-resolvers.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/all-audit-logs-resolvers.integration-spec.ts new file mode 100644 index 0000000000..4e5c74c220 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/all-audit-logs-resolvers.integration-spec.ts @@ -0,0 +1,421 @@ +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 AUDIT_LOG_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; +const AUDIT_LOG_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; +const AUDIT_LOG_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; + +const AUDIT_LOG_GQL_FIELDS = ` + id + name + properties + context + objectName + objectMetadataId + recordId + createdAt + updatedAt + deletedAt + workspaceMemberId +`; + +describe('auditLogs resolvers (integration)', () => { + it('1. should create and return auditLogs', async () => { + const auditLogName1 = generateRecordName(AUDIT_LOG_1_ID); + const auditLogName2 = generateRecordName(AUDIT_LOG_2_ID); + + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'auditLog', + objectMetadataPluralName: 'auditLogs', + gqlFields: AUDIT_LOG_GQL_FIELDS, + data: [ + { + id: AUDIT_LOG_1_ID, + name: auditLogName1, + }, + { + id: AUDIT_LOG_2_ID, + name: auditLogName2, + }, + ], + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.createAuditLogs).toHaveLength(2); + + response.body.data.createAuditLogs.forEach((auditLog) => { + expect(auditLog).toHaveProperty('name'); + expect([auditLogName1, auditLogName2]).toContain(auditLog.name); + expect(auditLog).toHaveProperty('properties'); + expect(auditLog).toHaveProperty('context'); + expect(auditLog).toHaveProperty('objectName'); + expect(auditLog).toHaveProperty('objectMetadataId'); + expect(auditLog).toHaveProperty('recordId'); + expect(auditLog).toHaveProperty('id'); + expect(auditLog).toHaveProperty('createdAt'); + expect(auditLog).toHaveProperty('updatedAt'); + expect(auditLog).toHaveProperty('deletedAt'); + expect(auditLog).toHaveProperty('workspaceMemberId'); + }); + }); + + it('1b. should create and return one auditLog', async () => { + const auditLogName = generateRecordName(AUDIT_LOG_3_ID); + + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'auditLog', + gqlFields: AUDIT_LOG_GQL_FIELDS, + data: { + id: AUDIT_LOG_3_ID, + name: auditLogName, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdAuditLog = response.body.data.createAuditLog; + + expect(createdAuditLog).toHaveProperty('name'); + expect(createdAuditLog.name).toEqual(auditLogName); + expect(createdAuditLog).toHaveProperty('properties'); + expect(createdAuditLog).toHaveProperty('context'); + expect(createdAuditLog).toHaveProperty('objectName'); + expect(createdAuditLog).toHaveProperty('objectMetadataId'); + expect(createdAuditLog).toHaveProperty('recordId'); + expect(createdAuditLog).toHaveProperty('id'); + expect(createdAuditLog).toHaveProperty('createdAt'); + expect(createdAuditLog).toHaveProperty('updatedAt'); + expect(createdAuditLog).toHaveProperty('deletedAt'); + expect(createdAuditLog).toHaveProperty('workspaceMemberId'); + }); + + it('2. should find many auditLogs', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'auditLog', + objectMetadataPluralName: 'auditLogs', + gqlFields: AUDIT_LOG_GQL_FIELDS, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const data = response.body.data.auditLogs; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + if (data.edges.length > 0) { + const auditLogs = data.edges[0].node; + + expect(auditLogs).toHaveProperty('name'); + expect(auditLogs).toHaveProperty('properties'); + expect(auditLogs).toHaveProperty('context'); + expect(auditLogs).toHaveProperty('objectName'); + expect(auditLogs).toHaveProperty('objectMetadataId'); + expect(auditLogs).toHaveProperty('recordId'); + expect(auditLogs).toHaveProperty('id'); + expect(auditLogs).toHaveProperty('createdAt'); + expect(auditLogs).toHaveProperty('updatedAt'); + expect(auditLogs).toHaveProperty('deletedAt'); + expect(auditLogs).toHaveProperty('workspaceMemberId'); + } + }); + + it('2b. should find one auditLog', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'auditLog', + gqlFields: AUDIT_LOG_GQL_FIELDS, + filter: { + id: { + eq: AUDIT_LOG_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const auditLog = response.body.data.auditLog; + + expect(auditLog).toHaveProperty('name'); + expect(auditLog).toHaveProperty('properties'); + expect(auditLog).toHaveProperty('context'); + expect(auditLog).toHaveProperty('objectName'); + expect(auditLog).toHaveProperty('objectMetadataId'); + expect(auditLog).toHaveProperty('recordId'); + expect(auditLog).toHaveProperty('id'); + expect(auditLog).toHaveProperty('createdAt'); + expect(auditLog).toHaveProperty('updatedAt'); + expect(auditLog).toHaveProperty('deletedAt'); + expect(auditLog).toHaveProperty('workspaceMemberId'); + }); + + it('3. should update many auditLogs', async () => { + const graphqlOperation = updateManyOperationFactory({ + objectMetadataSingularName: 'auditLog', + objectMetadataPluralName: 'auditLogs', + gqlFields: AUDIT_LOG_GQL_FIELDS, + data: { + name: 'Updated Name', + }, + filter: { + id: { + in: [AUDIT_LOG_1_ID, AUDIT_LOG_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedAuditLogs = response.body.data.updateAuditLogs; + + expect(updatedAuditLogs).toHaveLength(2); + + updatedAuditLogs.forEach((auditLog) => { + expect(auditLog.name).toEqual('Updated Name'); + }); + }); + + it('3b. should update one auditLog', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'auditLog', + gqlFields: AUDIT_LOG_GQL_FIELDS, + data: { + name: 'New Name', + }, + recordId: AUDIT_LOG_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedAuditLog = response.body.data.updateAuditLog; + + expect(updatedAuditLog.name).toEqual('New Name'); + }); + + it('4. should find many auditLogs with updated name', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'auditLog', + objectMetadataPluralName: 'auditLogs', + gqlFields: AUDIT_LOG_GQL_FIELDS, + filter: { + name: { + eq: 'Updated Name', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.auditLogs.edges).toHaveLength(2); + }); + + it('4b. should find one auditLog with updated name', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'auditLog', + gqlFields: AUDIT_LOG_GQL_FIELDS, + filter: { + name: { + eq: 'New Name', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.auditLog.name).toEqual('New Name'); + }); + + it('5. should delete many auditLogs', async () => { + const graphqlOperation = deleteManyOperationFactory({ + objectMetadataSingularName: 'auditLog', + objectMetadataPluralName: 'auditLogs', + gqlFields: AUDIT_LOG_GQL_FIELDS, + filter: { + id: { + in: [AUDIT_LOG_1_ID, AUDIT_LOG_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const deletedAuditLogs = response.body.data.deleteAuditLogs; + + expect(deletedAuditLogs).toHaveLength(2); + + deletedAuditLogs.forEach((auditLog) => { + expect(auditLog.deletedAt).toBeTruthy(); + }); + }); + + it('5b. should delete one auditLog', async () => { + const graphqlOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'auditLog', + gqlFields: AUDIT_LOG_GQL_FIELDS, + recordId: AUDIT_LOG_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.deleteAuditLog.deletedAt).toBeTruthy(); + }); + + it('6. should not find many auditLogs anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'auditLog', + objectMetadataPluralName: 'auditLogs', + gqlFields: AUDIT_LOG_GQL_FIELDS, + filter: { + id: { + in: [AUDIT_LOG_1_ID, AUDIT_LOG_2_ID], + }, + }, + }); + + const findAuditLogsResponse = await makeGraphqlAPIRequest(graphqlOperation); + + expect(findAuditLogsResponse.body.data.auditLogs.edges).toHaveLength(0); + }); + + it('6b. should not find one auditLog anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'auditLog', + gqlFields: AUDIT_LOG_GQL_FIELDS, + filter: { + id: { + eq: AUDIT_LOG_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.auditLog).toBeNull(); + }); + + it('7. should find many deleted auditLogs with deletedAt filter', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'auditLog', + objectMetadataPluralName: 'auditLogs', + gqlFields: AUDIT_LOG_GQL_FIELDS, + filter: { + id: { + in: [AUDIT_LOG_1_ID, AUDIT_LOG_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.auditLogs.edges).toHaveLength(2); + }); + + it('7b. should find one deleted auditLog with deletedAt filter', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'auditLog', + gqlFields: AUDIT_LOG_GQL_FIELDS, + filter: { + id: { + eq: AUDIT_LOG_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.auditLog.id).toEqual(AUDIT_LOG_3_ID); + }); + + it('8. should destroy many auditLogs', async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'auditLog', + objectMetadataPluralName: 'auditLogs', + gqlFields: AUDIT_LOG_GQL_FIELDS, + filter: { + id: { + in: [AUDIT_LOG_1_ID, AUDIT_LOG_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.destroyAuditLogs).toHaveLength(2); + }); + + it('8b. should destroy one auditLog', async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'auditLog', + gqlFields: AUDIT_LOG_GQL_FIELDS, + recordId: AUDIT_LOG_3_ID, + }); + + const destroyAuditLogResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect(destroyAuditLogResponse.body.data.destroyAuditLog).toBeTruthy(); + }); + + it('9. should not find many auditLogs anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'auditLog', + objectMetadataPluralName: 'auditLogs', + gqlFields: AUDIT_LOG_GQL_FIELDS, + filter: { + id: { + in: [AUDIT_LOG_1_ID, AUDIT_LOG_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.auditLogs.edges).toHaveLength(0); + }); + + it('9b. should not find one auditLog anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'auditLog', + gqlFields: AUDIT_LOG_GQL_FIELDS, + filter: { + id: { + eq: AUDIT_LOG_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.auditLog).toBeNull(); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/all-blocklists-resolvers.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/all-blocklists-resolvers.integration-spec.ts new file mode 100644 index 0000000000..a7afc4577a --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/all-blocklists-resolvers.integration-spec.ts @@ -0,0 +1,406 @@ +import { TIM_ACCOUNT_ID } from 'test/integration/graphql/integration.constants'; +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'; + +const BLOCKLIST_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; +const BLOCKLIST_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; +const BLOCKLIST_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; +const BLOCKLIST_HANDLE_1 = 'email@email.com'; +const BLOCKLIST_HANDLE_2 = '@domain.com'; +const BLOCKLIST_HANDLE_3 = '@domain.org'; +const UPDATED_BLOCKLIST_HANDLE_1 = 'updated@email.com'; +const UPDATED_BLOCKLIST_HANDLE_2 = '@updated-domain.com'; + +const BLOCKLIST_GQL_FIELDS = ` + id + handle + createdAt + updatedAt + deletedAt + workspaceMemberId +`; + +describe('blocklists resolvers (integration)', () => { + it('1. should create and return blocklists', async () => { + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'blocklist', + objectMetadataPluralName: 'blocklists', + gqlFields: BLOCKLIST_GQL_FIELDS, + data: [ + { + id: BLOCKLIST_1_ID, + handle: BLOCKLIST_HANDLE_1, + workspaceMemberId: TIM_ACCOUNT_ID, + }, + { + id: BLOCKLIST_2_ID, + handle: BLOCKLIST_HANDLE_2, + workspaceMemberId: TIM_ACCOUNT_ID, + }, + ], + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.createBlocklists).toHaveLength(2); + + response.body.data.createBlocklists.forEach((blocklist) => { + expect(blocklist).toHaveProperty('handle'); + expect([BLOCKLIST_HANDLE_1, BLOCKLIST_HANDLE_2]).toContain( + blocklist.handle, + ); + expect(blocklist).toHaveProperty('id'); + expect(blocklist).toHaveProperty('createdAt'); + expect(blocklist).toHaveProperty('updatedAt'); + expect(blocklist).toHaveProperty('deletedAt'); + expect(blocklist).toHaveProperty('workspaceMemberId'); + }); + }); + + it('1b. should create and return one blocklist', async () => { + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'blocklist', + gqlFields: BLOCKLIST_GQL_FIELDS, + data: { + id: BLOCKLIST_3_ID, + handle: BLOCKLIST_HANDLE_3, + workspaceMemberId: TIM_ACCOUNT_ID, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdBlocklist = response.body.data.createBlocklist; + + expect(createdBlocklist).toHaveProperty('handle'); + expect(createdBlocklist.handle).toEqual(BLOCKLIST_HANDLE_3); + expect(createdBlocklist).toHaveProperty('id'); + expect(createdBlocklist).toHaveProperty('createdAt'); + expect(createdBlocklist).toHaveProperty('updatedAt'); + expect(createdBlocklist).toHaveProperty('deletedAt'); + expect(createdBlocklist).toHaveProperty('workspaceMemberId'); + }); + + it('2. should find many blocklists', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'blocklist', + objectMetadataPluralName: 'blocklists', + gqlFields: BLOCKLIST_GQL_FIELDS, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const data = response.body.data.blocklists; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + if (data.edges.length > 0) { + const blocklists = data.edges[0].node; + + expect(blocklists).toHaveProperty('handle'); + expect(blocklists).toHaveProperty('id'); + expect(blocklists).toHaveProperty('createdAt'); + expect(blocklists).toHaveProperty('updatedAt'); + expect(blocklists).toHaveProperty('deletedAt'); + expect(blocklists).toHaveProperty('workspaceMemberId'); + } + }); + + it('2b. should find one blocklist', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'blocklist', + gqlFields: BLOCKLIST_GQL_FIELDS, + filter: { + id: { + eq: BLOCKLIST_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const blocklist = response.body.data.blocklist; + + expect(blocklist).toHaveProperty('handle'); + expect(blocklist).toHaveProperty('id'); + expect(blocklist).toHaveProperty('createdAt'); + expect(blocklist).toHaveProperty('updatedAt'); + expect(blocklist).toHaveProperty('deletedAt'); + expect(blocklist).toHaveProperty('workspaceMemberId'); + }); + + it('3. should not update many blocklists', async () => { + const graphqlOperation = updateManyOperationFactory({ + objectMetadataSingularName: 'blocklist', + objectMetadataPluralName: 'blocklists', + gqlFields: BLOCKLIST_GQL_FIELDS, + data: { + handle: UPDATED_BLOCKLIST_HANDLE_1, + workspaceMemberId: TIM_ACCOUNT_ID, + }, + filter: { + id: { + in: [BLOCKLIST_1_ID, BLOCKLIST_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.updateBlocklists).toBeNull(); + expect(response.body.errors).toStrictEqual([ + { + extensions: { code: 'INTERNAL_SERVER_ERROR' }, + message: 'Method not allowed.', + }, + ]); + }); + + it('3b. should update one blocklist', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'blocklist', + gqlFields: BLOCKLIST_GQL_FIELDS, + data: { + handle: UPDATED_BLOCKLIST_HANDLE_2, + workspaceMemberId: TIM_ACCOUNT_ID, + }, + recordId: BLOCKLIST_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedBlocklist = response.body.data.updateBlocklist; + + expect(updatedBlocklist.handle).toEqual(UPDATED_BLOCKLIST_HANDLE_2); + }); + + it('4. should not find many blocklists with updated name', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'blocklist', + objectMetadataPluralName: 'blocklists', + gqlFields: BLOCKLIST_GQL_FIELDS, + filter: { + handle: { + eq: UPDATED_BLOCKLIST_HANDLE_1, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.blocklists.edges).toHaveLength(0); + }); + + it('4b. should find one blocklist with updated name', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'blocklist', + gqlFields: BLOCKLIST_GQL_FIELDS, + filter: { + handle: { + eq: UPDATED_BLOCKLIST_HANDLE_2, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.blocklist.handle).toEqual( + UPDATED_BLOCKLIST_HANDLE_2, + ); + }); + + it('5. should delete many blocklists', async () => { + const graphqlOperation = deleteManyOperationFactory({ + objectMetadataSingularName: 'blocklist', + objectMetadataPluralName: 'blocklists', + gqlFields: BLOCKLIST_GQL_FIELDS, + filter: { + id: { + in: [BLOCKLIST_1_ID, BLOCKLIST_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const deletedBlocklists = response.body.data.deleteBlocklists; + + expect(deletedBlocklists).toHaveLength(2); + + deletedBlocklists.forEach((blocklist) => { + expect(blocklist.deletedAt).toBeTruthy(); + }); + }); + + it('5b. should delete one blocklist', async () => { + const graphqlOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'blocklist', + gqlFields: BLOCKLIST_GQL_FIELDS, + recordId: BLOCKLIST_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.deleteBlocklist.deletedAt).toBeTruthy(); + }); + + it('6. should not find many blocklists anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'blocklist', + objectMetadataPluralName: 'blocklists', + gqlFields: BLOCKLIST_GQL_FIELDS, + filter: { + id: { + in: [BLOCKLIST_1_ID, BLOCKLIST_2_ID], + }, + }, + }); + + const findBlocklistsResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect(findBlocklistsResponse.body.data.blocklists.edges).toHaveLength(0); + }); + + it('6b. should not find one blocklist anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'blocklist', + gqlFields: BLOCKLIST_GQL_FIELDS, + filter: { + id: { + eq: BLOCKLIST_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.blocklist).toBeNull(); + }); + + it('7. should find many deleted blocklists with deletedAt filter', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'blocklist', + objectMetadataPluralName: 'blocklists', + gqlFields: BLOCKLIST_GQL_FIELDS, + filter: { + id: { + in: [BLOCKLIST_1_ID, BLOCKLIST_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.blocklists.edges).toHaveLength(2); + }); + + it('7b. should find one deleted blocklist with deletedAt filter', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'blocklist', + gqlFields: BLOCKLIST_GQL_FIELDS, + filter: { + id: { + eq: BLOCKLIST_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.blocklist.id).toEqual(BLOCKLIST_3_ID); + }); + + it('8. should destroy many blocklists', async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'blocklist', + objectMetadataPluralName: 'blocklists', + gqlFields: BLOCKLIST_GQL_FIELDS, + filter: { + id: { + in: [BLOCKLIST_1_ID, BLOCKLIST_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.destroyBlocklists).toHaveLength(2); + }); + + it('8b. should destroy one blocklist', async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'blocklist', + gqlFields: BLOCKLIST_GQL_FIELDS, + recordId: BLOCKLIST_3_ID, + }); + + const destroyBlocklistResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect(destroyBlocklistResponse.body.data.destroyBlocklist).toBeTruthy(); + }); + + it('9. should not find many blocklists anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'blocklist', + objectMetadataPluralName: 'blocklists', + gqlFields: BLOCKLIST_GQL_FIELDS, + filter: { + id: { + in: [BLOCKLIST_1_ID, BLOCKLIST_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.blocklists.edges).toHaveLength(0); + }); + + it('9b. should not find one blocklist anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'blocklist', + gqlFields: BLOCKLIST_GQL_FIELDS, + filter: { + id: { + eq: BLOCKLIST_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.blocklist).toBeNull(); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/all-calendar-channel-event-associations-resolvers.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/all-calendar-channel-event-associations-resolvers.integration-spec.ts new file mode 100644 index 0000000000..e24c7af992 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/all-calendar-channel-event-associations-resolvers.integration-spec.ts @@ -0,0 +1,535 @@ +import { TIM_ACCOUNT_ID } from 'test/integration/graphql/integration.constants'; +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 CALENDAR_CHANNEL_EVENT_ASSOCIATION_1_ID = + '777a8457-eb2d-40ac-a707-551b615b6987'; +const CALENDAR_CHANNEL_EVENT_ASSOCIATION_2_ID = + '777a8457-eb2d-40ac-a707-551b615b6988'; +const CALENDAR_CHANNEL_EVENT_ASSOCIATION_3_ID = + '777a8457-eb2d-40ac-a707-551b615b6989'; +const CALENDAR_EVENT_ID = '777a8457-eb2d-40ac-a707-221b615b6989'; +const CALENDAR_CHANNEL_ID = '777a8457-eb2d-40ac-a707-331b615b6989'; +const CONNECTED_ACCOUNT_ID = '777a8457-eb2d-40ac-a707-441b615b6989'; + +const CALENDAR_CHANNEL_EVENT_ASSOCIATION_GQL_FIELDS = ` + id + eventExternalId + createdAt + updatedAt + deletedAt + calendarChannelId + calendarEventId +`; + +describe('calendarChannelEventAssociations resolvers (integration)', () => { + beforeAll(async () => { + const connectedAccountHandle = generateRecordName(CONNECTED_ACCOUNT_ID); + const createConnectedAccountgraphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + gqlFields: `id`, + data: { + id: CONNECTED_ACCOUNT_ID, + accountOwnerId: TIM_ACCOUNT_ID, + handle: connectedAccountHandle, + }, + }); + + const calendarChannelHandle = generateRecordName(CALENDAR_CHANNEL_ID); + const createCalendarChannelgraphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + gqlFields: `id`, + data: { + id: CALENDAR_CHANNEL_ID, + handle: calendarChannelHandle, + connectedAccountId: CONNECTED_ACCOUNT_ID, + }, + }); + + const calendarEventTitle = generateRecordName(CALENDAR_EVENT_ID); + const createCalendarEventgraphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'calendarEvent', + gqlFields: `id`, + data: { + id: CALENDAR_EVENT_ID, + title: calendarEventTitle, + }, + }); + + await makeGraphqlAPIRequest(createConnectedAccountgraphqlOperation); + + await makeGraphqlAPIRequest(createCalendarChannelgraphqlOperation); + await makeGraphqlAPIRequest(createCalendarEventgraphqlOperation); + }); + + afterAll(async () => { + const destroyConnectedAccountGraphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + gqlFields: `id`, + recordId: CONNECTED_ACCOUNT_ID, + }); + + const destroyCalendarChannelGraphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + gqlFields: `id`, + recordId: CALENDAR_CHANNEL_ID, + }); + + const destroyCalendarEventGraphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'calendarEvent', + gqlFields: `id`, + recordId: CALENDAR_EVENT_ID, + }); + + await makeGraphqlAPIRequest(destroyConnectedAccountGraphqlOperation); + await makeGraphqlAPIRequest(destroyCalendarChannelGraphqlOperation); + await makeGraphqlAPIRequest(destroyCalendarEventGraphqlOperation); + }); + + it('1. should create and return calendarChannelEventAssociations', async () => { + const eventExternalId1 = generateRecordName( + CALENDAR_CHANNEL_EVENT_ASSOCIATION_1_ID, + ); + const eventExternalId2 = generateRecordName( + CALENDAR_CHANNEL_EVENT_ASSOCIATION_2_ID, + ); + + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'calendarChannelEventAssociation', + objectMetadataPluralName: 'calendarChannelEventAssociations', + gqlFields: CALENDAR_CHANNEL_EVENT_ASSOCIATION_GQL_FIELDS, + data: [ + { + id: CALENDAR_CHANNEL_EVENT_ASSOCIATION_1_ID, + eventExternalId: eventExternalId1, + calendarChannelId: CALENDAR_CHANNEL_ID, + calendarEventId: CALENDAR_EVENT_ID, + }, + { + id: CALENDAR_CHANNEL_EVENT_ASSOCIATION_2_ID, + eventExternalId: eventExternalId2, + calendarChannelId: CALENDAR_CHANNEL_ID, + calendarEventId: CALENDAR_EVENT_ID, + }, + ], + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect( + response.body.data.createCalendarChannelEventAssociations, + ).toHaveLength(2); + + response.body.data.createCalendarChannelEventAssociations.forEach( + (association) => { + expect(association).toHaveProperty('eventExternalId'); + expect([eventExternalId1, eventExternalId2]).toContain( + association.eventExternalId, + ); + expect(association).toHaveProperty('id'); + expect(association).toHaveProperty('createdAt'); + expect(association).toHaveProperty('updatedAt'); + expect(association).toHaveProperty('deletedAt'); + expect(association).toHaveProperty('calendarChannelId'); + expect(association).toHaveProperty('calendarEventId'); + }, + ); + }); + + it('1b. should create and return one calendarChannelEventAssociation', async () => { + const eventExternalId = generateRecordName( + CALENDAR_CHANNEL_EVENT_ASSOCIATION_3_ID, + ); + + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'calendarChannelEventAssociation', + gqlFields: CALENDAR_CHANNEL_EVENT_ASSOCIATION_GQL_FIELDS, + data: { + id: CALENDAR_CHANNEL_EVENT_ASSOCIATION_3_ID, + eventExternalId: eventExternalId, + calendarChannelId: CALENDAR_CHANNEL_ID, + calendarEventId: CALENDAR_EVENT_ID, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdAssociation = + response.body.data.createCalendarChannelEventAssociation; + + expect(createdAssociation).toHaveProperty('eventExternalId'); + expect(createdAssociation.eventExternalId).toEqual(eventExternalId); + expect(createdAssociation).toHaveProperty('id'); + expect(createdAssociation).toHaveProperty('createdAt'); + expect(createdAssociation).toHaveProperty('updatedAt'); + expect(createdAssociation).toHaveProperty('deletedAt'); + expect(createdAssociation).toHaveProperty('calendarChannelId'); + expect(createdAssociation).toHaveProperty('calendarEventId'); + }); + + it('2. should find many calendarChannelEventAssociations', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'calendarChannelEventAssociation', + objectMetadataPluralName: 'calendarChannelEventAssociations', + gqlFields: CALENDAR_CHANNEL_EVENT_ASSOCIATION_GQL_FIELDS, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const data = response.body.data.calendarChannelEventAssociations; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + if (data.edges.length > 0) { + const associations = data.edges[0].node; + + expect(associations).toHaveProperty('eventExternalId'); + expect(associations).toHaveProperty('id'); + expect(associations).toHaveProperty('createdAt'); + expect(associations).toHaveProperty('updatedAt'); + expect(associations).toHaveProperty('deletedAt'); + expect(associations).toHaveProperty('calendarChannelId'); + expect(associations).toHaveProperty('calendarEventId'); + } + }); + + it('2b. should find one calendarChannelEventAssociation', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'calendarChannelEventAssociation', + gqlFields: CALENDAR_CHANNEL_EVENT_ASSOCIATION_GQL_FIELDS, + filter: { + id: { + eq: CALENDAR_CHANNEL_EVENT_ASSOCIATION_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const association = response.body.data.calendarChannelEventAssociation; + + expect(association).toHaveProperty('eventExternalId'); + expect(association).toHaveProperty('id'); + expect(association).toHaveProperty('createdAt'); + expect(association).toHaveProperty('updatedAt'); + expect(association).toHaveProperty('deletedAt'); + expect(association).toHaveProperty('calendarChannelId'); + expect(association).toHaveProperty('calendarEventId'); + }); + + it('3. should update many calendarChannelEventAssociations', async () => { + const graphqlOperation = updateManyOperationFactory({ + objectMetadataSingularName: 'calendarChannelEventAssociation', + objectMetadataPluralName: 'calendarChannelEventAssociations', + gqlFields: CALENDAR_CHANNEL_EVENT_ASSOCIATION_GQL_FIELDS, + data: { + eventExternalId: 'updated-message-external-id', + }, + filter: { + id: { + in: [ + CALENDAR_CHANNEL_EVENT_ASSOCIATION_1_ID, + CALENDAR_CHANNEL_EVENT_ASSOCIATION_2_ID, + ], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedAssociations = + response.body.data.updateCalendarChannelEventAssociations; + + expect(updatedAssociations).toHaveLength(2); + + updatedAssociations.forEach((association) => { + expect(association.eventExternalId).toEqual( + 'updated-message-external-id', + ); + }); + }); + + it('3b. should update one calendarChannelEventAssociation', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'calendarChannelEventAssociation', + gqlFields: CALENDAR_CHANNEL_EVENT_ASSOCIATION_GQL_FIELDS, + data: { + eventExternalId: 'new-message-external-id', + }, + recordId: CALENDAR_CHANNEL_EVENT_ASSOCIATION_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedAssociation = + response.body.data.updateCalendarChannelEventAssociation; + + expect(updatedAssociation.eventExternalId).toEqual( + 'new-message-external-id', + ); + }); + + it('4. should find many calendarChannelEventAssociations with updated eventExternalId', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'calendarChannelEventAssociation', + objectMetadataPluralName: 'calendarChannelEventAssociations', + gqlFields: CALENDAR_CHANNEL_EVENT_ASSOCIATION_GQL_FIELDS, + filter: { + eventExternalId: { + eq: 'updated-message-external-id', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect( + response.body.data.calendarChannelEventAssociations.edges, + ).toHaveLength(2); + }); + + it('4b. should find one calendarChannelEventAssociation with updated eventExternalId', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'calendarChannelEventAssociation', + gqlFields: CALENDAR_CHANNEL_EVENT_ASSOCIATION_GQL_FIELDS, + filter: { + eventExternalId: { + eq: 'new-message-external-id', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect( + response.body.data.calendarChannelEventAssociation.eventExternalId, + ).toEqual('new-message-external-id'); + }); + + it('5. should delete many calendarChannelEventAssociations', async () => { + const graphqlOperation = deleteManyOperationFactory({ + objectMetadataSingularName: 'calendarChannelEventAssociation', + objectMetadataPluralName: 'calendarChannelEventAssociations', + gqlFields: CALENDAR_CHANNEL_EVENT_ASSOCIATION_GQL_FIELDS, + filter: { + id: { + in: [ + CALENDAR_CHANNEL_EVENT_ASSOCIATION_1_ID, + CALENDAR_CHANNEL_EVENT_ASSOCIATION_2_ID, + ], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const deletedAssociations = + response.body.data.deleteCalendarChannelEventAssociations; + + expect(deletedAssociations).toHaveLength(2); + + deletedAssociations.forEach((association) => { + expect(association.deletedAt).toBeTruthy(); + }); + }); + + it('5b. should delete one calendarChannelEventAssociation', async () => { + const graphqlOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'calendarChannelEventAssociation', + gqlFields: CALENDAR_CHANNEL_EVENT_ASSOCIATION_GQL_FIELDS, + recordId: CALENDAR_CHANNEL_EVENT_ASSOCIATION_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect( + response.body.data.deleteCalendarChannelEventAssociation.deletedAt, + ).toBeTruthy(); + }); + + it('6. should not find many calendarChannelEventAssociations anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'calendarChannelEventAssociation', + objectMetadataPluralName: 'calendarChannelEventAssociations', + gqlFields: CALENDAR_CHANNEL_EVENT_ASSOCIATION_GQL_FIELDS, + filter: { + id: { + in: [ + CALENDAR_CHANNEL_EVENT_ASSOCIATION_1_ID, + CALENDAR_CHANNEL_EVENT_ASSOCIATION_2_ID, + ], + }, + }, + }); + + const findAssociationsResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + findAssociationsResponse.body.data.calendarChannelEventAssociations.edges, + ).toHaveLength(0); + }); + + it('6b. should not find one calendarChannelEventAssociation anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'calendarChannelEventAssociation', + gqlFields: CALENDAR_CHANNEL_EVENT_ASSOCIATION_GQL_FIELDS, + filter: { + id: { + eq: CALENDAR_CHANNEL_EVENT_ASSOCIATION_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.calendarChannelEventAssociation).toBeNull(); + }); + + it('7. should find many deleted calendarChannelEventAssociations with deletedAt filter', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'calendarChannelEventAssociation', + objectMetadataPluralName: 'calendarChannelEventAssociations', + gqlFields: CALENDAR_CHANNEL_EVENT_ASSOCIATION_GQL_FIELDS, + filter: { + id: { + in: [ + CALENDAR_CHANNEL_EVENT_ASSOCIATION_1_ID, + CALENDAR_CHANNEL_EVENT_ASSOCIATION_2_ID, + ], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect( + response.body.data.calendarChannelEventAssociations.edges, + ).toHaveLength(2); + }); + + it('7b. should find one deleted calendarChannelEventAssociation with deletedAt filter', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'calendarChannelEventAssociation', + gqlFields: CALENDAR_CHANNEL_EVENT_ASSOCIATION_GQL_FIELDS, + filter: { + id: { + eq: CALENDAR_CHANNEL_EVENT_ASSOCIATION_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.calendarChannelEventAssociation.id).toEqual( + CALENDAR_CHANNEL_EVENT_ASSOCIATION_3_ID, + ); + }); + + it('8. should destroy many calendarChannelEventAssociations', async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'calendarChannelEventAssociation', + objectMetadataPluralName: 'calendarChannelEventAssociations', + gqlFields: CALENDAR_CHANNEL_EVENT_ASSOCIATION_GQL_FIELDS, + filter: { + id: { + in: [ + CALENDAR_CHANNEL_EVENT_ASSOCIATION_1_ID, + CALENDAR_CHANNEL_EVENT_ASSOCIATION_2_ID, + ], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect( + response.body.data.destroyCalendarChannelEventAssociations, + ).toHaveLength(2); + }); + + it('8b. should destroy one calendarChannelEventAssociation', async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'calendarChannelEventAssociation', + gqlFields: CALENDAR_CHANNEL_EVENT_ASSOCIATION_GQL_FIELDS, + recordId: CALENDAR_CHANNEL_EVENT_ASSOCIATION_3_ID, + }); + + const destroyAssociationResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + destroyAssociationResponse.body.data + .destroyCalendarChannelEventAssociation, + ).toBeTruthy(); + }); + + it('9. should not find many calendarChannelEventAssociations anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'calendarChannelEventAssociation', + objectMetadataPluralName: 'calendarChannelEventAssociations', + gqlFields: CALENDAR_CHANNEL_EVENT_ASSOCIATION_GQL_FIELDS, + filter: { + id: { + in: [ + CALENDAR_CHANNEL_EVENT_ASSOCIATION_1_ID, + CALENDAR_CHANNEL_EVENT_ASSOCIATION_2_ID, + ], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect( + response.body.data.calendarChannelEventAssociations.edges, + ).toHaveLength(0); + }); + + it('9b. should not find one calendarChannelEventAssociation anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'calendarChannelEventAssociation', + gqlFields: CALENDAR_CHANNEL_EVENT_ASSOCIATION_GQL_FIELDS, + filter: { + id: { + eq: CALENDAR_CHANNEL_EVENT_ASSOCIATION_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.calendarChannelEventAssociation).toBeNull(); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/all-calendar-channels.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/all-calendar-channels.integration-spec.ts new file mode 100644 index 0000000000..704dfbfb5f --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/all-calendar-channels.integration-spec.ts @@ -0,0 +1,482 @@ +import { TIM_ACCOUNT_ID } from 'test/integration/graphql/integration.constants'; +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 CALENDAR_CHANNEL_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; +const CALENDAR_CHANNEL_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; +const CALENDAR_CHANNEL_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; +const CONNECTED_ACCOUNT_ID = '777a8457-eb2d-40ac-a707-441b615b6989'; + +const CALENDAR_CHANNEL_GQL_FIELDS = ` + id + handle + syncStatus + syncStage + visibility + isContactAutoCreationEnabled + contactAutoCreationPolicy + isSyncEnabled + syncCursor + syncStageStartedAt + throttleFailureCount + createdAt + updatedAt + deletedAt + connectedAccountId +`; + +describe('calendarChannels resolvers (integration)', () => { + beforeAll(async () => { + const connectedAccountHandle = generateRecordName(CONNECTED_ACCOUNT_ID); + const createConnectedAccountgraphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + gqlFields: `id`, + data: { + id: CONNECTED_ACCOUNT_ID, + accountOwnerId: TIM_ACCOUNT_ID, + handle: connectedAccountHandle, + }, + }); + + await makeGraphqlAPIRequest(createConnectedAccountgraphqlOperation); + }); + + afterAll(async () => { + const destroyConnectedAccountGraphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'connectedAccount', + gqlFields: `id`, + recordId: CONNECTED_ACCOUNT_ID, + }); + + await makeGraphqlAPIRequest(destroyConnectedAccountGraphqlOperation); + }); + + it('1. should create and return calendarChannels', async () => { + const calendarChannelHandle1 = generateRecordName(CALENDAR_CHANNEL_1_ID); + const calendarChannelHandle2 = generateRecordName(CALENDAR_CHANNEL_2_ID); + + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + objectMetadataPluralName: 'calendarChannels', + gqlFields: CALENDAR_CHANNEL_GQL_FIELDS, + data: [ + { + id: CALENDAR_CHANNEL_1_ID, + handle: calendarChannelHandle1, + connectedAccountId: CONNECTED_ACCOUNT_ID, + }, + { + id: CALENDAR_CHANNEL_2_ID, + handle: calendarChannelHandle2, + connectedAccountId: CONNECTED_ACCOUNT_ID, + }, + ], + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.createCalendarChannels).toHaveLength(2); + + response.body.data.createCalendarChannels.forEach((calendarChannel) => { + expect(calendarChannel).toHaveProperty('handle'); + expect([calendarChannelHandle1, calendarChannelHandle2]).toContain( + calendarChannel.handle, + ); + expect(calendarChannel).toHaveProperty('id'); + expect(calendarChannel).toHaveProperty('syncStatus'); + expect(calendarChannel).toHaveProperty('syncStage'); + expect(calendarChannel).toHaveProperty('visibility'); + expect(calendarChannel).toHaveProperty('isContactAutoCreationEnabled'); + expect(calendarChannel).toHaveProperty('contactAutoCreationPolicy'); + expect(calendarChannel).toHaveProperty('isSyncEnabled'); + expect(calendarChannel).toHaveProperty('syncCursor'); + expect(calendarChannel).toHaveProperty('syncStageStartedAt'); + expect(calendarChannel).toHaveProperty('throttleFailureCount'); + expect(calendarChannel).toHaveProperty('createdAt'); + expect(calendarChannel).toHaveProperty('updatedAt'); + expect(calendarChannel).toHaveProperty('deletedAt'); + expect(calendarChannel).toHaveProperty('connectedAccountId'); + }); + }); + + it('1b. should create and return one calendarChannel', async () => { + const calendarChannelHandle = generateRecordName(CALENDAR_CHANNEL_3_ID); + + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + gqlFields: CALENDAR_CHANNEL_GQL_FIELDS, + data: { + id: CALENDAR_CHANNEL_3_ID, + handle: calendarChannelHandle, + connectedAccountId: CONNECTED_ACCOUNT_ID, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdCalendarChannel = response.body.data.createCalendarChannel; + + expect(createdCalendarChannel).toHaveProperty('handle'); + expect(createdCalendarChannel.handle).toEqual(calendarChannelHandle); + expect(createdCalendarChannel).toHaveProperty('id'); + expect(createdCalendarChannel).toHaveProperty('syncStatus'); + expect(createdCalendarChannel).toHaveProperty('syncStage'); + expect(createdCalendarChannel).toHaveProperty('visibility'); + expect(createdCalendarChannel).toHaveProperty( + 'isContactAutoCreationEnabled', + ); + expect(createdCalendarChannel).toHaveProperty('contactAutoCreationPolicy'); + expect(createdCalendarChannel).toHaveProperty('isSyncEnabled'); + expect(createdCalendarChannel).toHaveProperty('syncCursor'); + expect(createdCalendarChannel).toHaveProperty('syncStageStartedAt'); + expect(createdCalendarChannel).toHaveProperty('throttleFailureCount'); + expect(createdCalendarChannel).toHaveProperty('createdAt'); + expect(createdCalendarChannel).toHaveProperty('updatedAt'); + expect(createdCalendarChannel).toHaveProperty('deletedAt'); + expect(createdCalendarChannel).toHaveProperty('connectedAccountId'); + }); + + it('2. should find many calendarChannels', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + objectMetadataPluralName: 'calendarChannels', + gqlFields: CALENDAR_CHANNEL_GQL_FIELDS, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const data = response.body.data.calendarChannels; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + if (data.edges.length > 0) { + const calendarChannels = data.edges[0].node; + + expect(calendarChannels).toHaveProperty('handle'); + expect(calendarChannels).toHaveProperty('syncStatus'); + expect(calendarChannels).toHaveProperty('syncStage'); + expect(calendarChannels).toHaveProperty('visibility'); + expect(calendarChannels).toHaveProperty('isContactAutoCreationEnabled'); + expect(calendarChannels).toHaveProperty('contactAutoCreationPolicy'); + expect(calendarChannels).toHaveProperty('isSyncEnabled'); + expect(calendarChannels).toHaveProperty('syncCursor'); + expect(calendarChannels).toHaveProperty('syncStageStartedAt'); + expect(calendarChannels).toHaveProperty('throttleFailureCount'); + expect(calendarChannels).toHaveProperty('id'); + expect(calendarChannels).toHaveProperty('createdAt'); + expect(calendarChannels).toHaveProperty('updatedAt'); + expect(calendarChannels).toHaveProperty('deletedAt'); + expect(calendarChannels).toHaveProperty('connectedAccountId'); + } + }); + + it('2b. should find one calendarChannel', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + gqlFields: CALENDAR_CHANNEL_GQL_FIELDS, + filter: { + id: { + eq: CALENDAR_CHANNEL_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const calendarChannel = response.body.data.calendarChannel; + + expect(calendarChannel).toHaveProperty('handle'); + expect(calendarChannel).toHaveProperty('syncStatus'); + expect(calendarChannel).toHaveProperty('syncStage'); + expect(calendarChannel).toHaveProperty('visibility'); + expect(calendarChannel).toHaveProperty('isContactAutoCreationEnabled'); + expect(calendarChannel).toHaveProperty('contactAutoCreationPolicy'); + expect(calendarChannel).toHaveProperty('isSyncEnabled'); + expect(calendarChannel).toHaveProperty('syncCursor'); + expect(calendarChannel).toHaveProperty('syncStageStartedAt'); + expect(calendarChannel).toHaveProperty('throttleFailureCount'); + expect(calendarChannel).toHaveProperty('id'); + expect(calendarChannel).toHaveProperty('createdAt'); + expect(calendarChannel).toHaveProperty('updatedAt'); + expect(calendarChannel).toHaveProperty('deletedAt'); + expect(calendarChannel).toHaveProperty('connectedAccountId'); + }); + + it('3. should update many calendarChannels', async () => { + const graphqlOperation = updateManyOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + objectMetadataPluralName: 'calendarChannels', + gqlFields: CALENDAR_CHANNEL_GQL_FIELDS, + data: { + handle: 'Updated Handle', + }, + filter: { + id: { + in: [CALENDAR_CHANNEL_1_ID, CALENDAR_CHANNEL_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedCalendarChannels = response.body.data.updateCalendarChannels; + + expect(updatedCalendarChannels).toHaveLength(2); + + updatedCalendarChannels.forEach((calendarChannel) => { + expect(calendarChannel.handle).toEqual('Updated Handle'); + }); + }); + + it('3b. should update one calendarChannel', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + gqlFields: CALENDAR_CHANNEL_GQL_FIELDS, + data: { + handle: 'New Handle', + }, + recordId: CALENDAR_CHANNEL_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedCalendarChannel = response.body.data.updateCalendarChannel; + + expect(updatedCalendarChannel.handle).toEqual('New Handle'); + }); + + it('4. should find many calendarChannels with updated handle', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + objectMetadataPluralName: 'calendarChannels', + gqlFields: CALENDAR_CHANNEL_GQL_FIELDS, + filter: { + handle: { + eq: 'Updated Handle', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.calendarChannels.edges).toHaveLength(2); + }); + + it('4b. should find one calendarChannel with updated handle', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + gqlFields: CALENDAR_CHANNEL_GQL_FIELDS, + filter: { + handle: { + eq: 'New Handle', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.calendarChannel.handle).toEqual('New Handle'); + }); + + it('5. should delete many calendarChannels', async () => { + const graphqlOperation = deleteManyOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + objectMetadataPluralName: 'calendarChannels', + gqlFields: CALENDAR_CHANNEL_GQL_FIELDS, + filter: { + id: { + in: [CALENDAR_CHANNEL_1_ID, CALENDAR_CHANNEL_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const deletedCalendarChannels = response.body.data.deleteCalendarChannels; + + expect(deletedCalendarChannels).toHaveLength(2); + + deletedCalendarChannels.forEach((calendarChannel) => { + expect(calendarChannel.deletedAt).toBeTruthy(); + }); + }); + + it('5b. should delete one calendarChannel', async () => { + const graphqlOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + gqlFields: CALENDAR_CHANNEL_GQL_FIELDS, + recordId: CALENDAR_CHANNEL_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.deleteCalendarChannel.deletedAt).toBeTruthy(); + }); + + it('6. should not find many calendarChannels anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + objectMetadataPluralName: 'calendarChannels', + gqlFields: CALENDAR_CHANNEL_GQL_FIELDS, + filter: { + id: { + in: [CALENDAR_CHANNEL_1_ID, CALENDAR_CHANNEL_2_ID], + }, + }, + }); + + const findCalendarChannelsResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + findCalendarChannelsResponse.body.data.calendarChannels.edges, + ).toHaveLength(0); + }); + + it('6b. should not find one calendarChannel anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + gqlFields: CALENDAR_CHANNEL_GQL_FIELDS, + filter: { + id: { + eq: CALENDAR_CHANNEL_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.calendarChannel).toBeNull(); + }); + + it('7. should find many deleted calendarChannels with deletedAt filter', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + objectMetadataPluralName: 'calendarChannels', + gqlFields: CALENDAR_CHANNEL_GQL_FIELDS, + filter: { + id: { + in: [CALENDAR_CHANNEL_1_ID, CALENDAR_CHANNEL_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.calendarChannels.edges).toHaveLength(2); + }); + + it('7b. should find one deleted calendarChannel with deletedAt filter', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + gqlFields: CALENDAR_CHANNEL_GQL_FIELDS, + filter: { + id: { + eq: CALENDAR_CHANNEL_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.calendarChannel.id).toEqual( + CALENDAR_CHANNEL_3_ID, + ); + }); + + it('8. should destroy many calendarChannels', async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + objectMetadataPluralName: 'calendarChannels', + gqlFields: CALENDAR_CHANNEL_GQL_FIELDS, + filter: { + id: { + in: [CALENDAR_CHANNEL_1_ID, CALENDAR_CHANNEL_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.destroyCalendarChannels).toHaveLength(2); + }); + + it('8b. should destroy one calendarChannel', async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + gqlFields: CALENDAR_CHANNEL_GQL_FIELDS, + recordId: CALENDAR_CHANNEL_3_ID, + }); + + const destroyCalendarChannelResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + destroyCalendarChannelResponse.body.data.destroyCalendarChannel, + ).toBeTruthy(); + }); + + it('9. should not find many calendarChannels anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + objectMetadataPluralName: 'calendarChannels', + gqlFields: CALENDAR_CHANNEL_GQL_FIELDS, + filter: { + id: { + in: [CALENDAR_CHANNEL_1_ID, CALENDAR_CHANNEL_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.calendarChannels.edges).toHaveLength(0); + }); + + it('9b. should not find one calendarChannel anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'calendarChannel', + gqlFields: CALENDAR_CHANNEL_GQL_FIELDS, + filter: { + id: { + eq: CALENDAR_CHANNEL_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.calendarChannel).toBeNull(); + }); +}); diff --git a/packages/twenty-server/test/integration/graphql/suites/all-workspace-members-resolvers.integration-spec.ts b/packages/twenty-server/test/integration/graphql/suites/all-workspace-members-resolvers.integration-spec.ts new file mode 100644 index 0000000000..050069c761 --- /dev/null +++ b/packages/twenty-server/test/integration/graphql/suites/all-workspace-members-resolvers.integration-spec.ts @@ -0,0 +1,439 @@ +import { TIM_USER_ID } from 'test/integration/graphql/integration.constants'; +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 WORKSPACE_MEMBER_1_ID = '777a8457-eb2d-40ac-a707-551b615b6987'; +const WORKSPACE_MEMBER_2_ID = '777a8457-eb2d-40ac-a707-551b615b6988'; +const WORKSPACE_MEMBER_3_ID = '777a8457-eb2d-40ac-a707-551b615b6989'; + +const WORKSPACE_MEMBER_GQL_FIELDS = ` + id + colorScheme + avatarUrl + locale + timeZone + dateFormat + timeFormat + userEmail + userId + createdAt + updatedAt + deletedAt +`; + +describe('workspaceMembers resolvers (integration)', () => { + it('1. should create and return workspaceMembers', async () => { + const workspaceMemberEmail1 = generateRecordName(WORKSPACE_MEMBER_1_ID); + const workspaceMemberEmail2 = generateRecordName(WORKSPACE_MEMBER_2_ID); + + const graphqlOperation = createManyOperationFactory({ + objectMetadataSingularName: 'workspaceMember', + objectMetadataPluralName: 'workspaceMembers', + gqlFields: WORKSPACE_MEMBER_GQL_FIELDS, + data: [ + { + id: WORKSPACE_MEMBER_1_ID, + userEmail: workspaceMemberEmail1, + userId: TIM_USER_ID, + }, + { + id: WORKSPACE_MEMBER_2_ID, + userEmail: workspaceMemberEmail2, + userId: TIM_USER_ID, + }, + ], + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.createWorkspaceMembers).toHaveLength(2); + + response.body.data.createWorkspaceMembers.forEach((workspaceMember) => { + expect(workspaceMember).toHaveProperty('userEmail'); + expect([workspaceMemberEmail1, workspaceMemberEmail2]).toContain( + workspaceMember.userEmail, + ); + expect(workspaceMember).toHaveProperty('id'); + expect(workspaceMember).toHaveProperty('colorScheme'); + expect(workspaceMember).toHaveProperty('avatarUrl'); + expect(workspaceMember).toHaveProperty('locale'); + expect(workspaceMember).toHaveProperty('timeZone'); + expect(workspaceMember).toHaveProperty('dateFormat'); + expect(workspaceMember).toHaveProperty('timeFormat'); + expect(workspaceMember).toHaveProperty('userId'); + expect(workspaceMember).toHaveProperty('createdAt'); + expect(workspaceMember).toHaveProperty('updatedAt'); + expect(workspaceMember).toHaveProperty('deletedAt'); + }); + }); + + it('1b. should create and return one workspaceMember', async () => { + const workspaceMemberEmail = generateRecordName(WORKSPACE_MEMBER_3_ID); + + const graphqlOperation = createOneOperationFactory({ + objectMetadataSingularName: 'workspaceMember', + gqlFields: WORKSPACE_MEMBER_GQL_FIELDS, + data: { + id: WORKSPACE_MEMBER_3_ID, + userEmail: workspaceMemberEmail, + userId: TIM_USER_ID, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const createdWorkspaceMember = response.body.data.createWorkspaceMember; + + expect(createdWorkspaceMember).toHaveProperty('userEmail'); + expect(createdWorkspaceMember.userEmail).toEqual(workspaceMemberEmail); + expect(createdWorkspaceMember).toHaveProperty('id'); + expect(createdWorkspaceMember).toHaveProperty('colorScheme'); + expect(createdWorkspaceMember).toHaveProperty('avatarUrl'); + expect(createdWorkspaceMember).toHaveProperty('locale'); + expect(createdWorkspaceMember).toHaveProperty('timeZone'); + expect(createdWorkspaceMember).toHaveProperty('dateFormat'); + expect(createdWorkspaceMember).toHaveProperty('timeFormat'); + expect(createdWorkspaceMember).toHaveProperty('userId'); + expect(createdWorkspaceMember).toHaveProperty('createdAt'); + expect(createdWorkspaceMember).toHaveProperty('updatedAt'); + expect(createdWorkspaceMember).toHaveProperty('deletedAt'); + }); + + it('2. should find many workspaceMembers', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'workspaceMember', + objectMetadataPluralName: 'workspaceMembers', + gqlFields: WORKSPACE_MEMBER_GQL_FIELDS, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const data = response.body.data.workspaceMembers; + + expect(data).toBeDefined(); + expect(Array.isArray(data.edges)).toBe(true); + + if (data.edges.length > 0) { + const workspaceMembers = data.edges[0].node; + + expect(workspaceMembers).toHaveProperty('id'); + expect(workspaceMembers).toHaveProperty('colorScheme'); + expect(workspaceMembers).toHaveProperty('avatarUrl'); + expect(workspaceMembers).toHaveProperty('locale'); + expect(workspaceMembers).toHaveProperty('timeZone'); + expect(workspaceMembers).toHaveProperty('dateFormat'); + expect(workspaceMembers).toHaveProperty('timeFormat'); + expect(workspaceMembers).toHaveProperty('userEmail'); + expect(workspaceMembers).toHaveProperty('userId'); + expect(workspaceMembers).toHaveProperty('createdAt'); + expect(workspaceMembers).toHaveProperty('updatedAt'); + expect(workspaceMembers).toHaveProperty('deletedAt'); + } + }); + + it('2b. should find one workspaceMember', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'workspaceMember', + gqlFields: WORKSPACE_MEMBER_GQL_FIELDS, + filter: { + id: { + eq: WORKSPACE_MEMBER_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const workspaceMember = response.body.data.workspaceMember; + + expect(workspaceMember).toHaveProperty('id'); + expect(workspaceMember).toHaveProperty('colorScheme'); + expect(workspaceMember).toHaveProperty('avatarUrl'); + expect(workspaceMember).toHaveProperty('locale'); + expect(workspaceMember).toHaveProperty('timeZone'); + expect(workspaceMember).toHaveProperty('dateFormat'); + expect(workspaceMember).toHaveProperty('timeFormat'); + expect(workspaceMember).toHaveProperty('userEmail'); + expect(workspaceMember).toHaveProperty('userId'); + expect(workspaceMember).toHaveProperty('createdAt'); + expect(workspaceMember).toHaveProperty('updatedAt'); + expect(workspaceMember).toHaveProperty('deletedAt'); + }); + + it('3. should update many workspaceMembers', async () => { + const graphqlOperation = updateManyOperationFactory({ + objectMetadataSingularName: 'workspaceMember', + objectMetadataPluralName: 'workspaceMembers', + gqlFields: WORKSPACE_MEMBER_GQL_FIELDS, + data: { + locale: 'en-US', + }, + filter: { + id: { + in: [WORKSPACE_MEMBER_1_ID, WORKSPACE_MEMBER_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedWorkspaceMembers = response.body.data.updateWorkspaceMembers; + + expect(updatedWorkspaceMembers).toHaveLength(2); + + updatedWorkspaceMembers.forEach((workspaceMember) => { + expect(workspaceMember.locale).toEqual('en-US'); + }); + }); + + it('3b. should update one workspaceMember', async () => { + const graphqlOperation = updateOneOperationFactory({ + objectMetadataSingularName: 'workspaceMember', + gqlFields: WORKSPACE_MEMBER_GQL_FIELDS, + data: { + locale: 'fr-CA', + }, + recordId: WORKSPACE_MEMBER_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + const updatedWorkspaceMember = response.body.data.updateWorkspaceMember; + + expect(updatedWorkspaceMember.locale).toEqual('fr-CA'); + }); + + it('4. should find many workspaceMembers with updated locale', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'workspaceMember', + objectMetadataPluralName: 'workspaceMembers', + gqlFields: WORKSPACE_MEMBER_GQL_FIELDS, + filter: { + locale: { + eq: 'en-US', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.workspaceMembers.edges).toHaveLength(2); + }); + + it('4b. should find one workspaceMember with updated locale', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'workspaceMember', + gqlFields: WORKSPACE_MEMBER_GQL_FIELDS, + filter: { + locale: { + eq: 'fr-CA', + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.workspaceMember.locale).toEqual('fr-CA'); + }); + + it('5. should not delete many workspaceMembers', async () => { + const graphqlOperation = deleteManyOperationFactory({ + objectMetadataSingularName: 'workspaceMember', + objectMetadataPluralName: 'workspaceMembers', + gqlFields: WORKSPACE_MEMBER_GQL_FIELDS, + filter: { + id: { + in: [WORKSPACE_MEMBER_1_ID, WORKSPACE_MEMBER_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.deleteWorkspaceMembers).toBeNull(); + expect(response.body.errors).toStrictEqual([ + { + extensions: { code: 'INTERNAL_SERVER_ERROR' }, + message: 'Method not allowed.', + }, + ]); + }); + + it('5b. should delete one workspaceMember', async () => { + const graphqlOperation = deleteOneOperationFactory({ + objectMetadataSingularName: 'workspaceMember', + gqlFields: WORKSPACE_MEMBER_GQL_FIELDS, + recordId: WORKSPACE_MEMBER_3_ID, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.deleteWorkspaceMember.deletedAt).toBeTruthy(); + }); + + it('6. should still find many workspaceMembers that were not deleted', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'workspaceMember', + objectMetadataPluralName: 'workspaceMembers', + gqlFields: WORKSPACE_MEMBER_GQL_FIELDS, + filter: { + id: { + in: [WORKSPACE_MEMBER_1_ID, WORKSPACE_MEMBER_2_ID], + }, + }, + }); + + const findWorkspaceMembersResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + findWorkspaceMembersResponse.body.data.workspaceMembers.edges, + ).toHaveLength(2); + }); + + it('6b. should not find one workspaceMember anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'workspaceMember', + gqlFields: WORKSPACE_MEMBER_GQL_FIELDS, + filter: { + id: { + eq: WORKSPACE_MEMBER_3_ID, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.workspaceMember).toBeNull(); + }); + + it('7. should not find many deleted workspaceMembers with deletedAt filter', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'workspaceMember', + objectMetadataPluralName: 'workspaceMembers', + gqlFields: WORKSPACE_MEMBER_GQL_FIELDS, + filter: { + id: { + in: [WORKSPACE_MEMBER_1_ID, WORKSPACE_MEMBER_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.workspaceMembers.edges).toHaveLength(0); + }); + + it('7b. should find one deleted workspaceMember with deletedAt filter', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'workspaceMember', + gqlFields: WORKSPACE_MEMBER_GQL_FIELDS, + filter: { + id: { + eq: WORKSPACE_MEMBER_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.workspaceMember.id).toEqual( + WORKSPACE_MEMBER_3_ID, + ); + }); + + it('8. should destroy many workspaceMembers', async () => { + const graphqlOperation = destroyManyOperationFactory({ + objectMetadataSingularName: 'workspaceMember', + objectMetadataPluralName: 'workspaceMembers', + gqlFields: WORKSPACE_MEMBER_GQL_FIELDS, + filter: { + id: { + in: [WORKSPACE_MEMBER_1_ID, WORKSPACE_MEMBER_2_ID], + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.destroyWorkspaceMembers).toHaveLength(2); + }); + + it('8b. should destroy one workspaceMember', async () => { + const graphqlOperation = destroyOneOperationFactory({ + objectMetadataSingularName: 'workspaceMember', + gqlFields: WORKSPACE_MEMBER_GQL_FIELDS, + recordId: WORKSPACE_MEMBER_3_ID, + }); + + const destroyWorkspaceMemberResponse = + await makeGraphqlAPIRequest(graphqlOperation); + + expect( + destroyWorkspaceMemberResponse.body.data.destroyWorkspaceMember, + ).toBeTruthy(); + }); + + it('9. should not find many workspaceMembers anymore', async () => { + const graphqlOperation = findManyOperationFactory({ + objectMetadataSingularName: 'workspaceMember', + objectMetadataPluralName: 'workspaceMembers', + gqlFields: WORKSPACE_MEMBER_GQL_FIELDS, + filter: { + id: { + in: [WORKSPACE_MEMBER_1_ID, WORKSPACE_MEMBER_2_ID], + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.workspaceMembers.edges).toHaveLength(0); + }); + + it('9b. should not find one workspaceMember anymore', async () => { + const graphqlOperation = findOneOperationFactory({ + objectMetadataSingularName: 'workspaceMember', + gqlFields: WORKSPACE_MEMBER_GQL_FIELDS, + filter: { + id: { + eq: WORKSPACE_MEMBER_3_ID, + }, + not: { + deletedAt: { + is: 'NULL', + }, + }, + }, + }); + + const response = await makeGraphqlAPIRequest(graphqlOperation); + + expect(response.body.data.workspaceMember).toBeNull(); + }); +}); From 67fb750ef6f85aa48c6a860badae804a2027e61e Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:39:25 +0200 Subject: [PATCH 117/123] Migrate to twenty-ui - input/color-scheme (#7995) This PR was created by [GitStart](https://gitstart.com/) to address the requirements from this ticket: [TWNTY-7063](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-7063). --- ### Description - Move color-scheme components to `twenty-ui` Fixes twentyhq/private-issues#93 Co-authored-by: gitstart-twenty Co-authored-by: Charles Bochet --- .../profile/appearance/components/SettingsAppearance.tsx | 3 +-- packages/twenty-front/tsup.ui.index.tsx | 2 -- .../src}/input/color-scheme/components/ColorSchemeCard.tsx | 7 +++---- .../input/color-scheme/components/ColorSchemePicker.tsx | 6 ++---- .../components/__stories__/ColorSchemeCard.stories.tsx | 2 +- packages/twenty-ui/src/input/index.ts | 3 +++ packages/twenty-ui/src/input/types/ColorScheme.ts | 1 + .../src/content/twenty-ui/input/color-scheme.mdx | 4 ++-- 8 files changed, 13 insertions(+), 15 deletions(-) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/color-scheme/components/ColorSchemeCard.tsx (97%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/color-scheme/components/ColorSchemePicker.tsx (91%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/input/color-scheme/components/__stories__/ColorSchemeCard.stories.tsx (95%) create mode 100644 packages/twenty-ui/src/input/types/ColorScheme.ts 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 891d69d957..c049f8d895 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 @@ -1,9 +1,8 @@ -import { H2Title } from 'twenty-ui'; +import { ColorSchemePicker, H2Title } from 'twenty-ui'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; 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/components/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { useColorScheme } from '@/ui/theme/hooks/useColorScheme'; diff --git a/packages/twenty-front/tsup.ui.index.tsx b/packages/twenty-front/tsup.ui.index.tsx index 597f9d2cd8..53c921e4f5 100644 --- a/packages/twenty-front/tsup.ui.index.tsx +++ b/packages/twenty-front/tsup.ui.index.tsx @@ -4,8 +4,6 @@ export { ThemeProvider } from '@emotion/react'; export * from 'twenty-ui'; export * from './src/modules/ui/feedback/progress-bar/components/CircularProgressBar'; export * from './src/modules/ui/feedback/progress-bar/components/ProgressBar'; -export * from './src/modules/ui/input/color-scheme/components/ColorSchemeCard'; -export * from './src/modules/ui/input/color-scheme/components/ColorSchemePicker'; export * from './src/modules/ui/input/components/AutosizeTextInput'; export * from './src/modules/ui/input/components/Checkbox'; export * from './src/modules/ui/input/components/EntityTitleDoubleTextInput'; diff --git a/packages/twenty-front/src/modules/ui/input/color-scheme/components/ColorSchemeCard.tsx b/packages/twenty-ui/src/input/color-scheme/components/ColorSchemeCard.tsx similarity index 97% rename from packages/twenty-front/src/modules/ui/input/color-scheme/components/ColorSchemeCard.tsx rename to packages/twenty-ui/src/input/color-scheme/components/ColorSchemeCard.tsx index 85bb45a675..10614c5e23 100644 --- a/packages/twenty-front/src/modules/ui/input/color-scheme/components/ColorSchemeCard.tsx +++ b/packages/twenty-ui/src/input/color-scheme/components/ColorSchemeCard.tsx @@ -1,14 +1,13 @@ -import React from 'react'; import styled from '@emotion/styled'; +import { Checkmark } from '@ui/display/checkmark/components/Checkmark'; +import { ColorScheme } from '@ui/input/types/ColorScheme'; import { AnimatePresence, AnimationControls, motion, useAnimation, } from 'framer-motion'; -import { Checkmark } from 'twenty-ui'; - -import { ColorScheme } from '@/workspace-member/types/WorkspaceMember'; +import React from 'react'; const StyledColorSchemeBackground = styled.div< Pick diff --git a/packages/twenty-front/src/modules/ui/input/color-scheme/components/ColorSchemePicker.tsx b/packages/twenty-ui/src/input/color-scheme/components/ColorSchemePicker.tsx similarity index 91% rename from packages/twenty-front/src/modules/ui/input/color-scheme/components/ColorSchemePicker.tsx rename to packages/twenty-ui/src/input/color-scheme/components/ColorSchemePicker.tsx index 4835fab247..d6e29dc55f 100644 --- a/packages/twenty-front/src/modules/ui/input/color-scheme/components/ColorSchemePicker.tsx +++ b/packages/twenty-ui/src/input/color-scheme/components/ColorSchemePicker.tsx @@ -1,10 +1,8 @@ -import React from 'react'; import styled from '@emotion/styled'; -import { ColorScheme } from '@/workspace-member/types/WorkspaceMember'; - +import { ColorScheme } from '@ui/input/types/ColorScheme'; +import { MOBILE_VIEWPORT } from '@ui/theme'; import { ColorSchemeCard } from './ColorSchemeCard'; -import { MOBILE_VIEWPORT } from 'twenty-ui'; const StyledContainer = styled.div` display: flex; diff --git a/packages/twenty-front/src/modules/ui/input/color-scheme/components/__stories__/ColorSchemeCard.stories.tsx b/packages/twenty-ui/src/input/color-scheme/components/__stories__/ColorSchemeCard.stories.tsx similarity index 95% rename from packages/twenty-front/src/modules/ui/input/color-scheme/components/__stories__/ColorSchemeCard.stories.tsx rename to packages/twenty-ui/src/input/color-scheme/components/__stories__/ColorSchemeCard.stories.tsx index 700845eb03..81a36fbb34 100644 --- a/packages/twenty-front/src/modules/ui/input/color-scheme/components/__stories__/ColorSchemeCard.stories.tsx +++ b/packages/twenty-ui/src/input/color-scheme/components/__stories__/ColorSchemeCard.stories.tsx @@ -1,6 +1,6 @@ import styled from '@emotion/styled'; import { Meta, StoryObj } from '@storybook/react'; -import { ComponentDecorator } from 'twenty-ui'; +import { ComponentDecorator } from '@ui/testing'; import { ColorSchemeCard } from '../ColorSchemeCard'; diff --git a/packages/twenty-ui/src/input/index.ts b/packages/twenty-ui/src/input/index.ts index 0733094165..8fa2e16019 100644 --- a/packages/twenty-ui/src/input/index.ts +++ b/packages/twenty-ui/src/input/index.ts @@ -12,4 +12,7 @@ export * from './button/components/LightIconButton'; export * from './button/components/LightIconButtonGroup'; export * from './button/components/MainButton'; export * from './button/components/RoundedIconButton'; +export * from './color-scheme/components/ColorSchemeCard'; +export * from './color-scheme/components/ColorSchemePicker'; export * from './components/Toggle'; +export * from './types/ColorScheme'; diff --git a/packages/twenty-ui/src/input/types/ColorScheme.ts b/packages/twenty-ui/src/input/types/ColorScheme.ts new file mode 100644 index 0000000000..0a5f3723d7 --- /dev/null +++ b/packages/twenty-ui/src/input/types/ColorScheme.ts @@ -0,0 +1 @@ +export type ColorScheme = 'Dark' | 'Light' | 'System'; diff --git a/packages/twenty-website/src/content/twenty-ui/input/color-scheme.mdx b/packages/twenty-website/src/content/twenty-ui/input/color-scheme.mdx index 8edd3c5501..2a8f27a405 100644 --- a/packages/twenty-website/src/content/twenty-ui/input/color-scheme.mdx +++ b/packages/twenty-website/src/content/twenty-ui/input/color-scheme.mdx @@ -11,7 +11,7 @@ Represents different color schemes and is specially tailored for light and dark - { return ( @@ -43,7 +43,7 @@ Allows users to choose between different color schemes. - { return Date: Thu, 24 Oct 2024 13:43:57 +0200 Subject: [PATCH 118/123] Use search in multi object pickers (#7909) Fixes https://github.com/twentyhq/twenty/issues/3298. We still have some existing glitches in the picker yet to fix. --------- Co-authored-by: Weiko --- .../useGenerateCombinedSearchRecordsQuery.ts | 96 +++++++++++++++++++ ...atchesSearchFilterAndSelectedItemsQuery.ts | 72 +++++++------- ...archMatchesSearchFilterAndToSelectQuery.ts | 36 +++---- .../hooks/useSearchFilterPerMetadataItem.ts | 67 ------------- .../utils/isObjectMetadataItemSearchable.ts | 17 ++++ ...MetadataItemSearchableInCombinedRequest.ts | 17 ++++ 6 files changed, 177 insertions(+), 128 deletions(-) create mode 100644 packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedSearchRecordsQuery.ts delete mode 100644 packages/twenty-front/src/modules/object-record/relation-picker/hooks/useSearchFilterPerMetadataItem.ts create mode 100644 packages/twenty-front/src/modules/object-record/utils/isObjectMetadataItemSearchable.ts create mode 100644 packages/twenty-front/src/modules/object-record/utils/isObjectMetadataItemSearchableInCombinedRequest.ts diff --git a/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedSearchRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedSearchRecordsQuery.ts new file mode 100644 index 0000000000..fc7725c3ae --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/multiple-objects/hooks/useGenerateCombinedSearchRecordsQuery.ts @@ -0,0 +1,96 @@ +import { gql } from '@apollo/client'; +import { isUndefined } from '@sniptt/guards'; +import { useRecoilValue } from 'recoil'; + +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; +import { RecordGqlOperationSignature } from '@/object-record/graphql/types/RecordGqlOperationSignature'; +import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; +import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField'; +import { isObjectMetadataItemSearchable } from '@/object-record/utils/isObjectMetadataItemSearchable'; +import { isNonEmptyArray } from '~/utils/isNonEmptyArray'; +import { capitalize } from '~/utils/string/capitalize'; + +export const useGenerateCombinedSearchRecordsQuery = ({ + operationSignatures, +}: { + operationSignatures: RecordGqlOperationSignature[]; +}) => { + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + + if (!isNonEmptyArray(operationSignatures)) { + return null; + } + + const filterPerMetadataItemArray = operationSignatures + .map( + ({ objectNameSingular }) => + `$filter${capitalize(objectNameSingular)}: ${capitalize( + objectNameSingular, + )}FilterInput`, + ) + .join(', '); + + const limitPerMetadataItemArray = operationSignatures + .map( + ({ objectNameSingular }) => + `$limit${capitalize(objectNameSingular)}: Int`, + ) + .join(', '); + + const queryKeyWithObjectMetadataItemArray = operationSignatures.map( + (queryKey) => { + const objectMetadataItem = objectMetadataItems.find( + (objectMetadataItem) => + objectMetadataItem.nameSingular === queryKey.objectNameSingular, + ); + + if (isUndefined(objectMetadataItem)) { + throw new Error( + `Object metadata item not found for object name singular: ${queryKey.objectNameSingular}`, + ); + } + + return { ...queryKey, objectMetadataItem }; + }, + ); + + const filteredQueryKeyWithObjectMetadataItemArray = + queryKeyWithObjectMetadataItemArray.filter(({ objectMetadataItem }) => + isObjectMetadataItemSearchable(objectMetadataItem), + ); + + return gql` + query CombinedSearchRecords( + ${filterPerMetadataItemArray}, + ${limitPerMetadataItemArray}, + $search: String, + ) { + ${filteredQueryKeyWithObjectMetadataItemArray + .map( + ({ objectMetadataItem, fields }) => + `${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(filter: $filter${capitalize( + objectMetadataItem.nameSingular, + )}, + limit: $limit${capitalize(objectMetadataItem.nameSingular)}, + searchInput: $search + ){ + edges { + node ${mapObjectMetadataToGraphQLQuery({ + objectMetadataItems: objectMetadataItems, + objectMetadataItem, + recordGqlFields: + fields ?? + generateDepthOneRecordGqlFields({ + objectMetadataItem, + }), + })} + cursor + } + totalCount + }`, + ) + .join('\n')} + } + `; +}; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery.ts index d42b2338b3..b69ef1f40c 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery.ts @@ -4,18 +4,32 @@ import { useRecoilValue } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery'; -import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery'; +import { useGenerateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedSearchRecordsQuery'; import { useLimitPerMetadataItem } from '@/object-record/relation-picker/hooks/useLimitPerMetadataItem'; import { MultiObjectRecordQueryResult, useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray, } from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray'; import { SelectedObjectRecordId } from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; -import { useOrderByFieldPerMetadataItem } from '@/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem'; -import { useSearchFilterPerMetadataItem } from '@/object-record/relation-picker/hooks/useSearchFilterPerMetadataItem'; +import { useMemo } from 'react'; import { isDefined } from '~/utils/isDefined'; import { capitalize } from '~/utils/string/capitalize'; +export const formatSearchResults = ( + searchResults: MultiObjectRecordQueryResult | undefined, +): MultiObjectRecordQueryResult => { + if (!searchResults) { + return {}; + } + + return Object.entries(searchResults).reduce((acc, [key, value]) => { + let newKey = key.replace(/^search/, ''); + newKey = newKey.charAt(0).toLowerCase() + newKey.slice(1); + acc[newKey] = value; + return acc; + }, {} as MultiObjectRecordQueryResult); +}; + export const useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery = ({ selectedObjectRecordIds, searchFilterValue, @@ -27,18 +41,14 @@ export const useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery = ({ }) => { const objectMetadataItems = useRecoilValue(objectMetadataItemsState); - const { searchFilterPerMetadataItemNameSingular } = - useSearchFilterPerMetadataItem({ - objectMetadataItems, - searchFilterValue, - }); - - const objectMetadataItemsUsedInSelectedIdsQuery = objectMetadataItems.filter( - ({ nameSingular }) => { - return selectedObjectRecordIds.some(({ objectNameSingular }) => { - return objectNameSingular === nameSingular; - }); - }, + const objectMetadataItemsUsedInSelectedIdsQuery = useMemo( + () => + objectMetadataItems.filter(({ nameSingular }) => { + return selectedObjectRecordIds.some(({ objectNameSingular }) => { + return objectNameSingular === nameSingular; + }); + }), + [objectMetadataItems, selectedObjectRecordIds], ); const selectedAndMatchesSearchFilterTextFilterPerMetadataItem = @@ -53,38 +63,25 @@ export const useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery = ({ if (!isNonEmptyArray(selectedIds)) return null; - const searchFilter = - searchFilterPerMetadataItemNameSingular[nameSingular] ?? {}; return [ `filter${capitalize(nameSingular)}`, { - and: [ - { - ...searchFilter, - }, - { - id: { - in: selectedIds, - }, - }, - ], + id: { + in: selectedIds, + }, }, ]; }) .filter(isDefined), ); - const { orderByFieldPerMetadataItem } = useOrderByFieldPerMetadataItem({ - objectMetadataItems: objectMetadataItemsUsedInSelectedIdsQuery, - }); - const { limitPerMetadataItem } = useLimitPerMetadataItem({ objectMetadataItems: objectMetadataItemsUsedInSelectedIdsQuery, limit, }); - const multiSelectQueryForSelectedIds = - useGenerateCombinedFindManyRecordsQuery({ + const multiSelectSearchQueryForSelectedIds = + useGenerateCombinedSearchRecordsQuery({ operationSignatures: objectMetadataItemsUsedInSelectedIdsQuery.map( (objectMetadataItem) => ({ objectNameSingular: objectMetadataItem.nameSingular, @@ -97,22 +94,23 @@ export const useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery = ({ loading: selectedAndMatchesSearchFilterObjectRecordsLoading, data: selectedAndMatchesSearchFilterObjectRecordsQueryResult, } = useQuery( - multiSelectQueryForSelectedIds ?? EMPTY_QUERY, + multiSelectSearchQueryForSelectedIds ?? EMPTY_QUERY, { variables: { + search: searchFilterValue, ...selectedAndMatchesSearchFilterTextFilterPerMetadataItem, - ...orderByFieldPerMetadataItem, ...limitPerMetadataItem, }, - skip: !isDefined(multiSelectQueryForSelectedIds), + skip: !isDefined(multiSelectSearchQueryForSelectedIds), }, ); const { objectRecordForSelectArray: selectedAndMatchesSearchFilterObjectRecords, } = useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({ - multiObjectRecordsQueryResult: + multiObjectRecordsQueryResult: formatSearchResults( selectedAndMatchesSearchFilterObjectRecordsQueryResult, + ), }); return { diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery.ts index 607eef1806..c3150cd44c 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery.ts +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery.ts @@ -4,15 +4,15 @@ import { useRecoilValue } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { EMPTY_QUERY } from '@/object-record/constants/EmptyQuery'; -import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery'; +import { useGenerateCombinedSearchRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedSearchRecordsQuery'; import { useLimitPerMetadataItem } from '@/object-record/relation-picker/hooks/useLimitPerMetadataItem'; import { MultiObjectRecordQueryResult, useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray, } from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray'; import { SelectedObjectRecordId } from '@/object-record/relation-picker/hooks/useMultiObjectSearch'; -import { useOrderByFieldPerMetadataItem } from '@/object-record/relation-picker/hooks/useOrderByFieldPerMetadataItem'; -import { useSearchFilterPerMetadataItem } from '@/object-record/relation-picker/hooks/useSearchFilterPerMetadataItem'; +import { formatSearchResults } from '@/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery'; +import { isObjectMetadataItemSearchableInCombinedRequest } from '@/object-record/utils/isObjectMetadataItemSearchableInCombinedRequest'; import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; import { isDefined } from '~/utils/isDefined'; import { capitalize } from '~/utils/string/capitalize'; @@ -36,13 +36,10 @@ export const useMultiObjectSearchMatchesSearchFilterAndToSelectQuery = ({ .filter(({ isSystem, isRemote }) => !isSystem && !isRemote) .filter(({ nameSingular }) => { return !excludedObjects?.includes(nameSingular as CoreObjectNameSingular); - }); - - const { searchFilterPerMetadataItemNameSingular } = - useSearchFilterPerMetadataItem({ - objectMetadataItems: selectableObjectMetadataItems, - searchFilterValue, - }); + }) + .filter((object) => + isObjectMetadataItemSearchableInCombinedRequest(object), + ); const objectRecordsToSelectAndMatchesSearchFilterTextFilterPerMetadataItem = Object.fromEntries( @@ -65,29 +62,19 @@ export const useMultiObjectSearchMatchesSearchFilterAndToSelectQuery = ({ ? { not: { id: { in: excludedIdsUnion } } } : undefined; - const searchFilters = [ - searchFilterPerMetadataItemNameSingular[nameSingular], - excludedIdsFilter, - ]; - return [ `filter${capitalize(nameSingular)}`, - makeAndFilterVariables(searchFilters), + makeAndFilterVariables([excludedIdsFilter]), ]; }) .filter(isDefined), ); - - const { orderByFieldPerMetadataItem } = useOrderByFieldPerMetadataItem({ - objectMetadataItems: selectableObjectMetadataItems, - }); - const { limitPerMetadataItem } = useLimitPerMetadataItem({ objectMetadataItems: selectableObjectMetadataItems, limit, }); - const multiSelectQuery = useGenerateCombinedFindManyRecordsQuery({ + const multiSelectQuery = useGenerateCombinedSearchRecordsQuery({ operationSignatures: selectableObjectMetadataItems.map( (objectMetadataItem) => ({ objectNameSingular: objectMetadataItem.nameSingular, @@ -101,8 +88,8 @@ export const useMultiObjectSearchMatchesSearchFilterAndToSelectQuery = ({ data: toSelectAndMatchesSearchFilterObjectRecordsQueryResult, } = useQuery(multiSelectQuery ?? EMPTY_QUERY, { variables: { + search: searchFilterValue, ...objectRecordsToSelectAndMatchesSearchFilterTextFilterPerMetadataItem, - ...orderByFieldPerMetadataItem, ...limitPerMetadataItem, }, skip: !isDefined(multiSelectQuery), @@ -111,8 +98,9 @@ export const useMultiObjectSearchMatchesSearchFilterAndToSelectQuery = ({ const { objectRecordForSelectArray: toSelectAndMatchesSearchFilterObjectRecords, } = useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray({ - multiObjectRecordsQueryResult: + multiObjectRecordsQueryResult: formatSearchResults( toSelectAndMatchesSearchFilterObjectRecordsQueryResult, + ), }); return { diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useSearchFilterPerMetadataItem.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useSearchFilterPerMetadataItem.ts deleted file mode 100644 index a4822dea14..0000000000 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useSearchFilterPerMetadataItem.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { isNonEmptyString } from '@sniptt/guards'; - -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { getLabelIdentifierFieldMetadataItem } from '@/object-metadata/utils/getLabelIdentifierFieldMetadataItem'; -import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; -import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables'; -import { FieldMetadataType } from '~/generated/graphql'; -import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields'; -import { isDefined } from '~/utils/isDefined'; - -export const useSearchFilterPerMetadataItem = ({ - objectMetadataItems, - searchFilterValue, -}: { - objectMetadataItems: ObjectMetadataItem[]; - searchFilterValue: string; -}) => { - const searchFilterPerMetadataItemNameSingular = - Object.fromEntries( - objectMetadataItems - .map((objectMetadataItem) => { - if (searchFilterValue === '') return null; - - const labelIdentifierFieldMetadataItem = - getLabelIdentifierFieldMetadataItem(objectMetadataItem); - - let searchFilter: RecordGqlOperationFilter = {}; - - if (isDefined(labelIdentifierFieldMetadataItem)) { - switch (labelIdentifierFieldMetadataItem.type) { - case FieldMetadataType.FullName: { - if (isNonEmptyString(searchFilterValue)) { - const compositeFilter = makeOrFilterVariables( - generateILikeFiltersForCompositeFields( - searchFilterValue, - labelIdentifierFieldMetadataItem.name, - ['firstName', 'lastName'], - ), - ); - - if (isDefined(compositeFilter)) { - searchFilter = compositeFilter; - } - } - break; - } - default: { - if (isNonEmptyString(searchFilterValue)) { - searchFilter = { - [labelIdentifierFieldMetadataItem.name]: { - ilike: `%${searchFilterValue}%`, - }, - }; - } - } - } - } - - return [objectMetadataItem.nameSingular, searchFilter] as const; - }) - .filter(isDefined), - ); - - return { - searchFilterPerMetadataItemNameSingular, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/utils/isObjectMetadataItemSearchable.ts b/packages/twenty-front/src/modules/object-record/utils/isObjectMetadataItemSearchable.ts new file mode 100644 index 0000000000..21bb1b2510 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/isObjectMetadataItemSearchable.ts @@ -0,0 +1,17 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + +const SEARCHABLE_STANDARD_OBJECTS_NAMES_PLURAL = [ + 'companies', + 'people', + 'opportunities', +]; +export const isObjectMetadataItemSearchable = ( + objectMetadataItem: ObjectMetadataItem, +) => { + return ( + objectMetadataItem.isCustom || + SEARCHABLE_STANDARD_OBJECTS_NAMES_PLURAL.includes( + objectMetadataItem.namePlural, + ) + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/utils/isObjectMetadataItemSearchableInCombinedRequest.ts b/packages/twenty-front/src/modules/object-record/utils/isObjectMetadataItemSearchableInCombinedRequest.ts new file mode 100644 index 0000000000..7b16a87ecb --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/isObjectMetadataItemSearchableInCombinedRequest.ts @@ -0,0 +1,17 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + +const SEARCHABLE_STANDARD_OBJECTS_IN_COMBINED_REQUEST_NAMES_PLURAL = [ + 'companies', + 'people', + 'opportunities', +]; +export const isObjectMetadataItemSearchableInCombinedRequest = ( + objectMetadataItem: ObjectMetadataItem, +) => { + return ( + objectMetadataItem.isCustom || + SEARCHABLE_STANDARD_OBJECTS_IN_COMBINED_REQUEST_NAMES_PLURAL.includes( + objectMetadataItem.namePlural, + ) + ); +}; From c6ef14acc4b8f861134365ae47b56fd5aee1eadf Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 13:45:52 +0200 Subject: [PATCH 119/123] Migrate to twenty-ui - navigation/navigation-bar (#7996) This PR was created by [GitStart](https://gitstart.com/) to address the requirements from this ticket: [TWNTY-7537](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-7537). --- ### Description - Move navigation-bar components to `twenty-ui` Fixes twentyhq/private-issues#81 Co-authored-by: gitstart-twenty Co-authored-by: Charles Bochet --- .../navigation/components/MobileNavigationBar.tsx | 9 +++++++-- .../components/__stories__/NavigationBar.stories.tsx | 3 +-- packages/twenty-front/tsup.ui.index.tsx | 1 - packages/twenty-ui/src/navigation/index.ts | 2 ++ .../navigation-bar/components/NavigationBar.tsx | 2 +- .../navigation-bar/components/NavigationBarItem.tsx | 2 +- 6 files changed, 12 insertions(+), 7 deletions(-) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/navigation/navigation-bar/components/NavigationBar.tsx (91%) rename packages/{twenty-front/src/modules/ui => twenty-ui/src}/navigation/navigation-bar/components/NavigationBarItem.tsx (93%) diff --git a/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx b/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx index e64a8deda1..9c615ee08e 100644 --- a/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx +++ b/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx @@ -1,9 +1,14 @@ import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { isCommandMenuOpenedState } from '@/command-menu/states/isCommandMenuOpenedState'; -import { NavigationBar } from '@/ui/navigation/navigation-bar/components/NavigationBar'; import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded'; import { useRecoilState } from 'recoil'; -import { IconComponent, IconList, IconSearch, IconSettings } from 'twenty-ui'; +import { + IconComponent, + IconList, + IconSearch, + IconSettings, + NavigationBar, +} from 'twenty-ui'; import { useIsSettingsPage } from '../hooks/useIsSettingsPage'; import { currentMobileNavigationDrawerState } from '../states/currentMobileNavigationDrawerState'; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-bar/components/__stories__/NavigationBar.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-bar/components/__stories__/NavigationBar.stories.tsx index 2dba91b685..ee7c612641 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-bar/components/__stories__/NavigationBar.stories.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-bar/components/__stories__/NavigationBar.stories.tsx @@ -5,12 +5,11 @@ import { IconList, IconSearch, IconSettings, + NavigationBar, } from 'twenty-ui'; import { ComponentWithRouterDecorator } from '~/testing/decorators/ComponentWithRouterDecorator'; -import { NavigationBar } from '../NavigationBar'; - const meta: Meta = { title: 'UI/Navigation/NavigationBar/NavigationBar', component: NavigationBar, diff --git a/packages/twenty-front/tsup.ui.index.tsx b/packages/twenty-front/tsup.ui.index.tsx index 53c921e4f5..cc6e700dd7 100644 --- a/packages/twenty-front/tsup.ui.index.tsx +++ b/packages/twenty-front/tsup.ui.index.tsx @@ -25,7 +25,6 @@ export * from './src/modules/ui/navigation/menu-item/components/MenuItemSelect'; export * from './src/modules/ui/navigation/menu-item/components/MenuItemSelectAvatar'; export * from './src/modules/ui/navigation/menu-item/components/MenuItemSelectColor'; export * from './src/modules/ui/navigation/menu-item/components/MenuItemToggle'; -export * from './src/modules/ui/navigation/navigation-bar/components/NavigationBar'; export * from './src/modules/ui/navigation/step-bar/components/StepBar'; declare module '@emotion/react' { diff --git a/packages/twenty-ui/src/navigation/index.ts b/packages/twenty-ui/src/navigation/index.ts index 89cbeb8eb0..8a6b0bb0e6 100644 --- a/packages/twenty-ui/src/navigation/index.ts +++ b/packages/twenty-ui/src/navigation/index.ts @@ -9,3 +9,5 @@ export * from './link/components/SocialLink'; export * from './link/components/UndecoratedLink'; export * from './link/constants/Cal'; export * from './link/constants/GithubLink'; +export * from './navigation-bar/components/NavigationBar'; +export * from './navigation-bar/components/NavigationBarItem'; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-bar/components/NavigationBar.tsx b/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBar.tsx similarity index 91% rename from packages/twenty-front/src/modules/ui/navigation/navigation-bar/components/NavigationBar.tsx rename to packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBar.tsx index 5474ff1cc6..7f438c9da8 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-bar/components/NavigationBar.tsx +++ b/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBar.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { IconComponent } from 'twenty-ui'; +import { IconComponent } from '@ui/display/icon/types/IconComponent'; import { NavigationBarItem } from './NavigationBarItem'; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-bar/components/NavigationBarItem.tsx b/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBarItem.tsx similarity index 93% rename from packages/twenty-front/src/modules/ui/navigation/navigation-bar/components/NavigationBarItem.tsx rename to packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBarItem.tsx index e7259d9053..f8be843165 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-bar/components/NavigationBarItem.tsx +++ b/packages/twenty-ui/src/navigation/navigation-bar/components/NavigationBarItem.tsx @@ -1,6 +1,6 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconComponent } from 'twenty-ui'; +import { IconComponent } from '@ui/display/icon/types/IconComponent'; const StyledIconButton = styled.div<{ isActive?: boolean }>` align-items: center; From 414f2ac4989e1279cd90b6a7ae51dc094c7eef34 Mon Sep 17 00:00:00 2001 From: "gitstart-app[bot]" <57568882+gitstart-app[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 11:52:30 +0000 Subject: [PATCH 120/123] Support custom object renaming (#7504) This PR was created by [GitStart](https://gitstart.com/) to address the requirements from this ticket: [TWNTY-5491](https://clients.gitstart.com/twenty/5449/tickets/TWNTY-5491). This ticket was imported from: [TWNTY-5491](https://github.com/twentyhq/twenty/issues/5491) --- ### Description **How To Test:**\ 1. Reset db using `npx nx database:reset twenty-server` on this PR 1. Run both backend and frontend 2. Navigate to `settings/data-model/objects/ `page 3. Select a `Custom `object from the list or create a new `Custom `object 4. Navigate to custom object details page and click on edit button 5. Finally edit the object details. **Issues and bugs** The Typecheck is failing but we could not see this error locally There is a bug after updating the label of a custom object. View title is not updated till refreshing the page. We could not find a consistent way to update this, should we reload the page after editing an object? ![](https://assets-service.gitstart.com/45430/03cd560f-a4f6-4ce2-9d78-6d3a9f56d197.png)### Demo ### Refs #5491 --------- Co-authored-by: gitstart-twenty Co-authored-by: gitstart-twenty <140154534+gitstart-twenty@users.noreply.github.com> Co-authored-by: Marie Stoppa Co-authored-by: Charles Bochet Co-authored-by: Weiko --- .../src/generated-metadata/gql.ts | 4 +- .../src/generated-metadata/graphql.ts | 7 +- .../twenty-front/src/generated/graphql.tsx | 2 + .../components/EventRowDynamicComponent.tsx | 14 +- .../hooks/__tests__/useCommandMenu.test.tsx | 1 + .../object-metadata/graphql/queries.ts | 1 + .../objectMetadataItemSchema.test.ts | 1 + .../objectMetadataItemSchema.ts | 1 + .../__tests__/turnSortsIntoOrderBy.test.ts | 1 + .../useLimitPerMetadataItem.test.tsx | 1 + .../__tests__/useMultiObjectSearch.test.tsx | 1 + .../SettingsDataModelObjectAboutForm.tsx | 347 ++++++++++++++---- .../SyncObjectLabelAndNameToggle.tsx | 72 ++++ .../settingsCreateObjectInputSchema.test.ts | 8 +- .../settingsUpdateObjectInputSchema.test.ts | 6 +- .../settingsCreateObjectInputSchema.ts | 27 +- .../settingsUpdateObjectInputSchema.ts | 33 +- .../data-model/SettingsObjectEdit.tsx | 74 +++- .../compute-metadata-name-from-label.utils.ts | 3 + .../generated/mock-metadata-query-result.ts | 37 ++ ...1728579416430-addShouldSyncLabelAndName.ts | 19 + .../dtos/create-object.input.ts | 7 +- .../dtos/object-metadata.dto.ts | 3 + .../dtos/update-object.input.ts | 5 + .../hooks/before-update-one-object.hook.ts | 42 --- .../object-metadata/object-metadata.entity.ts | 3 + .../object-metadata.service.ts | 318 ++++++++++++++-- .../validate-object-metadata-input.util.ts | 35 ++ ...te-object-metadata-sync-label-name.util.ts | 19 + 29 files changed, 900 insertions(+), 192 deletions(-) create mode 100644 packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SyncObjectLabelAndNameToggle.tsx create mode 100644 packages/twenty-server/src/database/typeorm/metadata/migrations/1728579416430-addShouldSyncLabelAndName.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/validate-object-metadata-sync-label-name.util.ts diff --git a/packages/twenty-front/src/generated-metadata/gql.ts b/packages/twenty-front/src/generated-metadata/gql.ts index cf451ad555..68c28acf6b 100644 --- a/packages/twenty-front/src/generated-metadata/gql.ts +++ b/packages/twenty-front/src/generated-metadata/gql.ts @@ -32,7 +32,7 @@ const documents = { "\n mutation DeleteOneObjectMetadataItem($idToDelete: UUID!) {\n deleteOneObject(input: { id: $idToDelete }) {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isActive\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n }\n }\n": types.DeleteOneObjectMetadataItemDocument, "\n mutation DeleteOneFieldMetadataItem($idToDelete: UUID!) {\n deleteOneField(input: { id: $idToDelete }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n settings\n }\n }\n": types.DeleteOneFieldMetadataItemDocument, "\n mutation DeleteOneRelationMetadataItem($idToDelete: UUID!) {\n deleteOneRelation(input: { id: $idToDelete }) {\n id\n }\n }\n": types.DeleteOneRelationMetadataItemDocument, - "\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument, + "\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shouldSyncLabelAndName\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n": types.ObjectMetadataItemsDocument, "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n runtime\n syncStatus\n latestVersion\n publishedVersions\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc, "\n \n mutation CreateOneServerlessFunctionItem(\n $input: CreateServerlessFunctionInput!\n ) {\n createOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.CreateOneServerlessFunctionItemDocument, "\n \n mutation DeleteOneServerlessFunction($input: ServerlessFunctionIdInput!) {\n deleteOneServerlessFunction(input: $input) {\n ...ServerlessFunctionFields\n }\n }\n": types.DeleteOneServerlessFunctionDocument, @@ -138,7 +138,7 @@ export function graphql(source: "\n mutation DeleteOneRelationMetadataItem($idT /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"]; +export function graphql(source: "\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shouldSyncLabelAndName\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"): (typeof documents)["\n query ObjectMetadataItems(\n $objectFilter: objectFilter\n $fieldFilter: fieldFilter\n ) {\n objects(paging: { first: 1000 }, filter: $objectFilter) {\n edges {\n node {\n id\n dataSourceId\n nameSingular\n namePlural\n labelSingular\n labelPlural\n description\n icon\n isCustom\n isRemote\n isActive\n isSystem\n createdAt\n updatedAt\n labelIdentifierFieldMetadataId\n imageIdentifierFieldMetadataId\n shouldSyncLabelAndName\n indexMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n name\n indexWhereClause\n indexType\n isUnique\n indexFieldMetadatas(paging: { first: 100 }) {\n edges {\n node {\n id\n createdAt\n updatedAt\n order\n fieldMetadataId\n }\n }\n }\n }\n }\n }\n fields(paging: { first: 1000 }, filter: $fieldFilter) {\n edges {\n node {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isSystem\n isNullable\n isUnique\n createdAt\n updatedAt\n defaultValue\n options\n settings\n relationDefinition {\n relationId\n direction\n sourceObjectMetadata {\n id\n nameSingular\n namePlural\n }\n sourceFieldMetadata {\n id\n name\n }\n targetObjectMetadata {\n id\n nameSingular\n namePlural\n }\n targetFieldMetadata {\n id\n name\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n }\n pageInfo {\n hasNextPage\n hasPreviousPage\n startCursor\n endCursor\n }\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 49c95cb374..e390776e93 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -196,6 +196,7 @@ export type CreateObjectInput = { nameSingular: Scalars['String']['input']; primaryKeyColumnType?: InputMaybe; primaryKeyFieldMetadataSettings?: InputMaybe; + shouldSyncLabelAndName?: InputMaybe; }; export type CreateOneAppTokenInput = { @@ -1433,6 +1434,7 @@ export type UpdateObjectPayload = { labelSingular?: InputMaybe; namePlural?: InputMaybe; nameSingular?: InputMaybe; + shouldSyncLabelAndName?: InputMaybe; }; export type UpdateOneFieldMetadataInput = { @@ -1774,6 +1776,7 @@ export type Object = { labelSingular: Scalars['String']['output']; namePlural: Scalars['String']['output']; nameSingular: Scalars['String']['output']; + shouldSyncLabelAndName: Scalars['Boolean']['output']; updatedAt: Scalars['DateTime']['output']; }; @@ -1960,7 +1963,7 @@ export type ObjectMetadataItemsQueryVariables = Exact<{ }>; -export type ObjectMetadataItemsQuery = { __typename?: 'Query', objects: { __typename?: 'ObjectConnection', edges: Array<{ __typename?: 'objectEdge', node: { __typename?: 'object', id: any, dataSourceId: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, createdAt: any, updatedAt: any, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, indexMetadatas: { __typename?: 'ObjectIndexMetadatasConnection', edges: Array<{ __typename?: 'indexEdge', node: { __typename?: 'index', id: any, createdAt: any, updatedAt: any, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, indexFieldMetadatas: { __typename?: 'IndexIndexFieldMetadatasConnection', edges: Array<{ __typename?: 'indexFieldEdge', node: { __typename?: 'indexField', id: any, createdAt: any, updatedAt: any, order: number, fieldMetadataId: any } }> } } }> }, fields: { __typename?: 'ObjectFieldsConnection', edges: Array<{ __typename?: 'fieldEdge', node: { __typename?: 'field', id: any, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: any, updatedAt: any, defaultValue?: any | null, options?: any | null, settings?: any | null, relationDefinition?: { __typename?: 'RelationDefinition', relationId: any, direction: RelationDefinitionType, sourceObjectMetadata: { __typename?: 'object', id: any, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'field', id: any, name: string }, targetObjectMetadata: { __typename?: 'object', id: any, nameSingular: string, namePlural: string }, targetFieldMetadata: { __typename?: 'field', id: any, name: string } } | null } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } }; +export type ObjectMetadataItemsQuery = { __typename?: 'Query', objects: { __typename?: 'ObjectConnection', edges: Array<{ __typename?: 'objectEdge', node: { __typename?: 'object', id: any, dataSourceId: string, nameSingular: string, namePlural: string, labelSingular: string, labelPlural: string, description?: string | null, icon?: string | null, isCustom: boolean, isRemote: boolean, isActive: boolean, isSystem: boolean, createdAt: any, updatedAt: any, labelIdentifierFieldMetadataId?: string | null, imageIdentifierFieldMetadataId?: string | null, shouldSyncLabelAndName: boolean, indexMetadatas: { __typename?: 'ObjectIndexMetadatasConnection', edges: Array<{ __typename?: 'indexEdge', node: { __typename?: 'index', id: any, createdAt: any, updatedAt: any, name: string, indexWhereClause?: string | null, indexType: IndexType, isUnique: boolean, indexFieldMetadatas: { __typename?: 'IndexIndexFieldMetadatasConnection', edges: Array<{ __typename?: 'indexFieldEdge', node: { __typename?: 'indexField', id: any, createdAt: any, updatedAt: any, order: number, fieldMetadataId: any } }> } } }> }, fields: { __typename?: 'ObjectFieldsConnection', edges: Array<{ __typename?: 'fieldEdge', node: { __typename?: 'field', id: any, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isSystem?: boolean | null, isNullable?: boolean | null, isUnique?: boolean | null, createdAt: any, updatedAt: any, defaultValue?: any | null, options?: any | null, settings?: any | null, relationDefinition?: { __typename?: 'RelationDefinition', relationId: any, direction: RelationDefinitionType, sourceObjectMetadata: { __typename?: 'object', id: any, nameSingular: string, namePlural: string }, sourceFieldMetadata: { __typename?: 'field', id: any, name: string }, targetObjectMetadata: { __typename?: 'object', id: any, nameSingular: string, namePlural: string }, targetFieldMetadata: { __typename?: 'field', id: any, name: string } } | null } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } } }>, pageInfo: { __typename?: 'PageInfo', hasNextPage?: boolean | null, hasPreviousPage?: boolean | null, startCursor?: any | null, endCursor?: any | null } } }; export type ServerlessFunctionFieldsFragment = { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, publishedVersions: Array, createdAt: any, updatedAt: any }; @@ -2043,7 +2046,7 @@ export const UpdateOneObjectMetadataItemDocument = {"kind":"Document","definitio export const DeleteOneObjectMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneObjectMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneObject"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}}]}}]}}]} as unknown as DocumentNode; export const DeleteOneFieldMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneFieldMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneField"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"settings"}}]}}]}}]} as unknown as DocumentNode; export const DeleteOneRelationMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneRelationMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneRelation"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode; -export const ObjectMetadataItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ObjectMetadataItems"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"objectFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fieldFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"fieldFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isRemote"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"indexMetadatas"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"indexWhereClause"}},{"kind":"Field","name":{"kind":"Name","value":"indexType"}},{"kind":"Field","name":{"kind":"Name","value":"isUnique"}},{"kind":"Field","name":{"kind":"Name","value":"indexFieldMetadatas"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"fieldMetadataId"}}]}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fieldFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"isUnique"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"defaultValue"}},{"kind":"Field","name":{"kind":"Name","value":"options"}},{"kind":"Field","name":{"kind":"Name","value":"settings"}},{"kind":"Field","name":{"kind":"Name","value":"relationDefinition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"relationId"}},{"kind":"Field","name":{"kind":"Name","value":"direction"}},{"kind":"Field","name":{"kind":"Name","value":"sourceObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}}]}},{"kind":"Field","name":{"kind":"Name","value":"sourceFieldMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"targetObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}}]}},{"kind":"Field","name":{"kind":"Name","value":"targetFieldMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode; +export const ObjectMetadataItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ObjectMetadataItems"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"objectFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fieldFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"fieldFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isRemote"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"shouldSyncLabelAndName"}},{"kind":"Field","name":{"kind":"Name","value":"indexMetadatas"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"indexWhereClause"}},{"kind":"Field","name":{"kind":"Name","value":"indexType"}},{"kind":"Field","name":{"kind":"Name","value":"isUnique"}},{"kind":"Field","name":{"kind":"Name","value":"indexFieldMetadatas"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"order"}},{"kind":"Field","name":{"kind":"Name","value":"fieldMetadataId"}}]}}]}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"fields"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"fieldFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"isUnique"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"defaultValue"}},{"kind":"Field","name":{"kind":"Name","value":"options"}},{"kind":"Field","name":{"kind":"Name","value":"settings"}},{"kind":"Field","name":{"kind":"Name","value":"relationDefinition"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"relationId"}},{"kind":"Field","name":{"kind":"Name","value":"direction"}},{"kind":"Field","name":{"kind":"Name","value":"sourceObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}}]}},{"kind":"Field","name":{"kind":"Name","value":"sourceFieldMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}},{"kind":"Field","name":{"kind":"Name","value":"targetObjectMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}}]}},{"kind":"Field","name":{"kind":"Name","value":"targetFieldMetadata"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"pageInfo"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"hasNextPage"}},{"kind":"Field","name":{"kind":"Name","value":"hasPreviousPage"}},{"kind":"Field","name":{"kind":"Name","value":"startCursor"}},{"kind":"Field","name":{"kind":"Name","value":"endCursor"}}]}}]}}]}}]} as unknown as DocumentNode; export const CreateOneServerlessFunctionItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneServerlessFunctionItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; export const DeleteOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunctionIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"publishedVersions"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode; export const ExecuteOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ExecuteOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ExecuteServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"executeOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode; diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx index 145f90b385..386ab99f23 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -1160,6 +1160,7 @@ export type UpdateObjectPayload = { labelSingular?: InputMaybe; namePlural?: InputMaybe; nameSingular?: InputMaybe; + shouldSyncLabelAndName?: InputMaybe; }; export type UpdateOneObjectInput = { @@ -1476,6 +1477,7 @@ export type Object = { labelSingular: Scalars['String']; namePlural: Scalars['String']; nameSingular: Scalars['String']; + shouldSyncLabelAndName: Scalars['Boolean']; updatedAt: Scalars['DateTime']; }; diff --git a/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventRowDynamicComponent.tsx b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventRowDynamicComponent.tsx index 441d6598a8..e51556decd 100644 --- a/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventRowDynamicComponent.tsx +++ b/packages/twenty-front/src/modules/activities/timeline-activities/rows/components/EventRowDynamicComponent.tsx @@ -35,9 +35,7 @@ export const EventRowDynamicComponent = ({ linkedObjectMetadataItem, authorFullName, }: EventRowDynamicComponentProps) => { - const [eventName] = event.name.split('.'); - - switch (eventName) { + switch (linkedObjectMetadataItem?.nameSingular) { case 'calendarEvent': return ( ); - case 'linked-task': + case 'task': return ( ); - case 'linked-note': + case 'note': return ( ); - case mainObjectMetadataItem?.nameSingular: + default: return ( ); - default: - throw new Error( - `Cannot find event component for event name ${eventName}`, - ); } }; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx index 9d7e103398..c2449c755e 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/__tests__/useCommandMenu.test.tsx @@ -124,6 +124,7 @@ describe('useCommandMenu', () => { namePlural: 'tasks', labelSingular: 'Task', labelPlural: 'Tasks', + shouldSyncLabelAndName: true, description: 'A task', icon: 'IconCheckbox', isCustom: false, diff --git a/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts b/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts index 35beb162d3..7a6a79265b 100644 --- a/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts +++ b/packages/twenty-front/src/modules/object-metadata/graphql/queries.ts @@ -24,6 +24,7 @@ export const FIND_MANY_OBJECT_METADATA_ITEMS = gql` updatedAt labelIdentifierFieldMetadataId imageIdentifierFieldMetadataId + shouldSyncLabelAndName indexMetadatas(paging: { first: 100 }) { edges { node { diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/__tests__/objectMetadataItemSchema.test.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/__tests__/objectMetadataItemSchema.test.ts index cbb1b2c46b..7fc130833b 100644 --- a/packages/twenty-front/src/modules/object-metadata/validation-schemas/__tests__/objectMetadataItemSchema.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/__tests__/objectMetadataItemSchema.test.ts @@ -30,6 +30,7 @@ describe('objectMetadataItemSchema', () => { namePlural: 'notCamelCase', nameSingular: 'notCamelCase', updatedAt: 'invalid date', + shouldSyncLabelAndName: 'not a boolean', }; // When diff --git a/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts b/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts index a12b072ebc..38ce3cbd1e 100644 --- a/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts +++ b/packages/twenty-front/src/modules/object-metadata/validation-schemas/objectMetadataItemSchema.ts @@ -26,4 +26,5 @@ export const objectMetadataItemSchema = z.object({ namePlural: camelCaseStringSchema, nameSingular: camelCaseStringSchema, updatedAt: z.string().datetime(), + shouldSyncLabelAndName: z.boolean(), }) satisfies z.ZodType; diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.ts b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.ts index 12fcaff755..ec874811ff 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.ts +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.ts @@ -25,6 +25,7 @@ const objectMetadataItem: ObjectMetadataItem = { isRemote: false, labelPlural: 'object1s', labelSingular: 'object1', + shouldSyncLabelAndName: true, }; describe('turnSortsIntoOrderBy', () => { diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useLimitPerMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useLimitPerMetadataItem.test.tsx index ead53f498f..f0df37214e 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useLimitPerMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useLimitPerMetadataItem.test.tsx @@ -26,6 +26,7 @@ describe('useLimitPerMetadataItem', () => { namePlural: 'namePlural', nameSingular: 'nameSingular', updatedAt: 'updatedAt', + shouldSyncLabelAndName: false, fields: [], indexMetadatas: [], }, diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearch.test.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearch.test.tsx index 73d1715f55..f6373efd3e 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearch.test.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearch.test.tsx @@ -34,6 +34,7 @@ const objectData: ObjectMetadataItem[] = [ labelSingular: 'labelSingular', namePlural: 'namePlural', nameSingular: 'nameSingular', + shouldSyncLabelAndName: false, updatedAt: 'updatedAt', fields: [ { diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx index 7a40b61e7a..b61f984367 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/SettingsDataModelObjectAboutForm.tsx @@ -1,21 +1,45 @@ -import styled from '@emotion/styled'; -import { Controller, useFormContext } from 'react-hook-form'; -import { z } from 'zod'; - import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { objectMetadataItemSchema } from '@/object-metadata/validation-schemas/objectMetadataItemSchema'; import { OBJECT_NAME_MAXIMUM_LENGTH } from '@/settings/data-model/constants/ObjectNameMaximumLength'; +import { SyncObjectLabelAndNameToggle } from '@/settings/data-model/objects/forms/components/SyncObjectLabelAndNameToggle'; +import { useExpandedHeightAnimation } from '@/settings/hooks/useExpandedHeightAnimation'; import { IconPicker } from '@/ui/input/components/IconPicker'; import { TextArea } from '@/ui/input/components/TextArea'; import { TextInput } from '@/ui/input/components/TextInput'; +import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { AnimatePresence, motion } from 'framer-motion'; +import { plural } from 'pluralize'; +import { Controller, useFormContext } from 'react-hook-form'; +import { useRecoilValue } from 'recoil'; +import { + AppTooltip, + IconInfoCircle, + IconTool, + MAIN_COLORS, + TooltipDelay, +} from 'twenty-ui'; +import { z } from 'zod'; +import { computeMetadataNameFromLabelOrThrow } from '~/pages/settings/data-model/utils/compute-metadata-name-from-label.utils'; +import { isDefined } from '~/utils/isDefined'; -export const settingsDataModelObjectAboutFormSchema = - objectMetadataItemSchema.pick({ +export const settingsDataModelObjectAboutFormSchema = objectMetadataItemSchema + .pick({ description: true, icon: true, labelPlural: true, labelSingular: true, - }); + }) + .merge( + objectMetadataItemSchema + .pick({ + nameSingular: true, + namePlural: true, + shouldSyncLabelAndName: true, + }) + .partial(), + ); type SettingsDataModelObjectAboutFormValues = z.infer< typeof settingsDataModelObjectAboutFormSchema @@ -34,6 +58,41 @@ const StyledInputsContainer = styled.div` width: 100%; `; +const StyledInputContainer = styled.div` + display: flex; + flex-direction: column; +`; + +const StyledSectionWrapper = styled.div` + margin-bottom: ${({ theme }) => theme.spacing(4)}; +`; + +const StyledAdvancedSettingsSectionInputWrapper = styled.div` + display: flex; + flex-direction: column; + gap: ${({ theme }) => theme.spacing(4)}; + width: 100%; +`; + +const StyledAdvancedSettingsContainer = styled.div` + display: flex; + width: 100%; + gap: ${({ theme }) => theme.spacing(2)}; + position: relative; +`; + +const StyledIconToolContainer = styled.div` + border-right: 1px solid ${MAIN_COLORS.yellow}; + display: flex; + left: ${({ theme }) => theme.spacing(-5)}; + position: absolute; + height: 100%; +`; + +const StyledIconTool = styled(IconTool)` + margin-right: ${({ theme }) => theme.spacing(0.5)}; +`; + const StyledLabel = styled.span` color: ${({ theme }) => theme.font.color.light}; font-size: ${({ theme }) => theme.font.size.xs}; @@ -41,83 +100,247 @@ const StyledLabel = styled.span` margin-bottom: ${({ theme }) => theme.spacing(1)}; `; -const StyledInputContainer = styled.div` - display: flex; - flex-direction: column; -`; +const infoCircleElementId = 'info-circle-id'; export const SettingsDataModelObjectAboutForm = ({ disabled, disableNameEdit, objectMetadataItem, }: SettingsDataModelObjectAboutFormProps) => { - const { control } = useFormContext(); + const { control, watch, setValue } = + useFormContext(); + const theme = useTheme(); + const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState); + const { contentRef, motionAnimationVariants } = useExpandedHeightAnimation( + isAdvancedModeEnabled, + ); + + const shouldSyncLabelAndName = watch('shouldSyncLabelAndName'); + const labelSingular = watch('labelSingular'); + const labelPlural = watch('labelPlural'); + const apiNameTooltipText = shouldSyncLabelAndName + ? 'Deactivate "Synchronize Objects Labels and API Names" to set a custom API name' + : 'Input must be in camel case and cannot start with a number'; + + const fillLabelPlural = (labelSingular: string) => { + const newLabelPluralValue = isDefined(labelSingular) + ? plural(labelSingular) + : ''; + setValue('labelPlural', newLabelPluralValue, { + shouldDirty: isDefined(labelSingular) ? true : false, + }); + if (shouldSyncLabelAndName === true) { + fillNamePluralFromLabelPlural(newLabelPluralValue); + } + }; + + const fillNameSingularFromLabelSingular = (labelSingular: string) => { + isDefined(labelSingular) && + setValue( + 'nameSingular', + computeMetadataNameFromLabelOrThrow(labelSingular), + { shouldDirty: false }, + ); + }; + + const fillNamePluralFromLabelPlural = (labelPlural: string) => { + isDefined(labelPlural) && + setValue('namePlural', computeMetadataNameFromLabelOrThrow(labelPlural), { + shouldDirty: false, + }); + }; return ( <> - - - Icon + + + + Icon + ( + onChange(iconKey)} + /> + )} + /> + ( - onChange(iconKey)} - /> - )} - /> - - {[ - { - label: 'Singular', - fieldName: 'labelSingular' as const, - placeholder: 'Listing', - defaultValue: objectMetadataItem?.labelSingular, - }, - { - label: 'Plural', - fieldName: 'labelPlural' as const, - placeholder: 'Listings', - defaultValue: objectMetadataItem?.labelPlural, - }, - ].map(({ defaultValue, fieldName, label, placeholder }) => ( - ( { + onChange(value); + fillLabelPlural(value); + if (shouldSyncLabelAndName === true) { + fillNameSingularFromLabelSingular(value); + } + }} disabled={disabled || disableNameEdit} fullWidth maxLength={OBJECT_NAME_MAXIMUM_LENGTH} /> )} /> - ))} - - ( -