diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml index aa6955723796..b61506709f93 100644 --- a/.github/workflows/ci-front.yaml +++ b/.github/workflows/ci-front.yaml @@ -31,6 +31,8 @@ jobs: uses: actions/checkout@v4 - name: Install dependencies uses: ./.github/workflows/actions/yarn-install + - name: Diagnostic disk space issue + run: df -h - name: Front / Restore Storybook Task Cache uses: ./.github/workflows/actions/task-cache with: diff --git a/.github/workflows/ci-server.yaml b/.github/workflows/ci-server.yaml index 857dec2fc863..074d63fdda40 100644 --- a/.github/workflows/ci-server.yaml +++ b/.github/workflows/ci-server.yaml @@ -54,7 +54,7 @@ jobs: - name: Server / Write .env run: npx nx reset:env twenty-server - name: Worker / Run - run: MESSAGE_QUEUE_TYPE=sync npx nx worker twenty-server + run: npx nx run twenty-server:worker:ci server-test: runs-on: ubuntu-latest diff --git a/README.md b/README.md index f0c96b0a32ac..86ed0fe51a6f 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ +

+ + + + + Hacktoberfest + + +


@@ -34,7 +43,7 @@ We felt the need for a CRM platform that empowers rather than constrains. We bel Go to demo.twenty.com and login with the following credentials: ``` -email: noah@demo.dev +email: tim@apple.dev password: Applecar2025 ``` 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 new file mode 100644 index 000000000000..455b5e35bae3 --- /dev/null +++ b/oss-gg/twenty-content-challenges/1-create-youtube-video-about-20.md @@ -0,0 +1,21 @@ +**Side Quest**: Create a YouTube Video about Twenty showcasing a specific way to use Twenty effectively. +**Points**: 750 Points +**Proof**: Add your oss handle and YouTube video link to the list below. + +Please follow the following schema: + +--- + +» 05-April-2024 by YOUR oss.gg HANDLE » YouTube Link: https://link.to/content + +--- + +//////////////////////////// + +Your turn 👇 + +//////////////////////////// + +» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) YouTube Link: [YouTube](https://twenty.com/) + +--- \ No newline at end of file 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 new file mode 100644 index 000000000000..a4c4e6bee944 --- /dev/null +++ b/oss-gg/twenty-content-challenges/2-write-blog-post-about-20.md @@ -0,0 +1,21 @@ +**Side Quest**: Write a blog post about sharing your experience using Twenty in a detailed format on any platform. +**Points**: 750 Points +**Proof**: Add your oss handle and blog link to the list below. + +Please follow the following schema: + +--- + +» 05-April-2024 by YOUR oss.gg HANDLE » blog Link: https://link.to/content + +--- + +//////////////////////////// + +Your turn 👇 + +//////////////////////////// + +» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) blog Link: [blog](https://twenty.com/) + +--- \ No newline at end of file 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 new file mode 100644 index 000000000000..c7352ec430fc --- /dev/null +++ b/oss-gg/twenty-content-challenges/3-write-selfthost-guide-blog-post-20.md @@ -0,0 +1,21 @@ +**Side Quest**: Write a blog post about self-hosting Twenty in a detailed format on any platform. +**Points**: 750 Points +**Proof**: Add your oss handle and blog link to the list below. + +Please follow the following schema: + +--- + +» 05-April-2024 by YOUR oss.gg HANDLE » blog Link: https://link.to/content + +--- + +//////////////////////////// + +Your turn 👇 + +//////////////////////////// + +» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) blog Link: [blog](https://twenty.com/) + +--- \ No newline at end of file diff --git a/oss-gg/twenty-content-challenges/4-create-promotional-video-20-share.md b/oss-gg/twenty-content-challenges/4-create-promotional-video-20-share.md new file mode 100644 index 000000000000..e52cb43a4247 --- /dev/null +++ b/oss-gg/twenty-content-challenges/4-create-promotional-video-20-share.md @@ -0,0 +1,21 @@ +**Side Quest**: Create a promotional video for Twenty and share it on social media. +**Points**: 750 Points +**Proof**: Add your oss handle and video link to the list below. + +Please follow the following schema: + +--- + +» 05-April-2024 by YOUR oss.gg HANDLE » video Link: https://link.to/content + +--- + +//////////////////////////// + +Your turn 👇 + +//////////////////////////// + +» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) video Link: [video](https://twenty.com/) + +--- \ No newline at end of file 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 new file mode 100644 index 000000000000..b995788bc3f1 --- /dev/null +++ b/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md @@ -0,0 +1,21 @@ +**Side Quest**: Design a promotional poster of Twenty and share it on social media. +**Points**: 300 Points +**Proof**: Add your oss handle and poster link to the list below. + +Please follow the following schema: + +--- + +» 05-April-2024 by YOUR oss.gg HANDLE » poster Link: https://link.to/content + +--- + +//////////////////////////// + +Your turn 👇 + +//////////////////////////// + +» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) poster Link: [poster](https://twenty.com/) + +--- \ No newline at end of file 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 new file mode 100644 index 000000000000..d211055f0361 --- /dev/null +++ b/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md @@ -0,0 +1,21 @@ +**Side Quest**: Design/Create new Twenty logo, tweet your design, and mention @twentycrm. +**Points**: 300 Points +**Proof**: Create a logo uploade it on any of the platform and add your oss handle and logo link to the list below. + +Please follow the following schema: + +--- + +» 05-April-2024 by YOUR oss.gg HANDLE » Logo Link: https://link.to/content » tweet Link: https://link.to/content + +--- + +//////////////////////////// + +Your turn 👇 + +//////////////////////////// + +» 08-October-2024 by [adityadeshlahre](https://oss.gg/adityadeshlahre) Logo Link: [logo](https://drive.google.com/drive/folders/13k22xMnX2fhnWK94vas_hO1t-ImqXcHZ?usp=drive_link) » tweet Link: [tweet](https://x.com/adityadeshlahre/status/1843354963176718374) + +--- \ No newline at end of file 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 new file mode 100644 index 000000000000..e51945ea9988 --- /dev/null +++ b/oss-gg/twenty-design-challenges/3-create-custom-interfact-theme-20.md @@ -0,0 +1,21 @@ +**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. + +Please follow the following schema: + +--- + +» 05-April-2024 by YOUR oss.gg HANDLE » Figma Link: https://link.to/content + +--- + +//////////////////////////// + +Your turn 👇 + +//////////////////////////// + +» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) Figma Link: [Figma](https://twenty.com/) + +--- \ No newline at end of file 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 new file mode 100644 index 000000000000..249d8e158cfa --- /dev/null +++ b/oss-gg/twenty-dev-challenges/1-write-migration-script-other-crm-to-20.md @@ -0,0 +1,21 @@ +**Side Quest**: Develop a script to facilitate the migration of data from another CRM to Twenty. +**Points**: 750 Points +**Proof**: Add your oss handle and record video and share link to the list below. In video show the working proof of your created script. + +Please follow the following schema: + +--- + +» 05-April-2024 by YOUR oss.gg HANDLE » video Link: https://link.to/content + +--- + +//////////////////////////// + +Your turn 👇 + +//////////////////////////// + +» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) video Link: [video](https://twenty.com/) + +--- \ No newline at end of file diff --git a/oss-gg/twenty-dev-challenges/2-create-raycast-integration-for-20.md b/oss-gg/twenty-dev-challenges/2-create-raycast-integration-for-20.md new file mode 100644 index 000000000000..e4793c40d66f --- /dev/null +++ b/oss-gg/twenty-dev-challenges/2-create-raycast-integration-for-20.md @@ -0,0 +1,21 @@ +**Side Quest**: Develop an integration for Raycast that enables users to create records on any object within Twenty directly from Raycast. +**Points**: 1500 Points +**Proof**: Add your oss handle and record video and share link to the list below. In video show the workflow of the your integration created and perform some task. + +Please follow the following schema: + +--- + +» 05-April-2024 by YOUR oss.gg HANDLE » video Link: https://link.to/content + +--- + +//////////////////////////// + +Your turn 👇 + +//////////////////////////// + +» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) video Link: [video](https://twenty.com/) + +--- \ No newline at end of file diff --git a/oss-gg/twenty-no-code-challenges/1-create-n8n-template-integrate-20-API.md b/oss-gg/twenty-no-code-challenges/1-create-n8n-template-integrate-20-API.md new file mode 100644 index 000000000000..6786e5a94553 --- /dev/null +++ b/oss-gg/twenty-no-code-challenges/1-create-n8n-template-integrate-20-API.md @@ -0,0 +1,21 @@ +**Side Quest**: Create an n8n workflow that empowers Twenty by connecting it to another tool. +**Points**: 750 Points +**Proof**: Add your oss handle and template link to the list below. + +Please follow the following schema: + +--- + +» 05-April-2024 by YOUR oss.gg HANDLE » template Link: https://link.to/content + +--- + +//////////////////////////// + +Your turn 👇 + +//////////////////////////// + +» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) template Link: [template](https://twenty.com/) + +--- \ No newline at end of file diff --git a/oss-gg/twenty-no-code-challenges/2-write-selfthost-guide-blog-post-20.md b/oss-gg/twenty-no-code-challenges/2-write-selfthost-guide-blog-post-20.md new file mode 100644 index 000000000000..58fa6de4d8d6 --- /dev/null +++ b/oss-gg/twenty-no-code-challenges/2-write-selfthost-guide-blog-post-20.md @@ -0,0 +1,21 @@ +**Side Quest**: Write a comprehensive guide on how to integrate Twenty with marketing automation tool (n8n, Zapier). Include a concrete use case and explain how to leverage AI to write API requests for non-developers and share it. +**Points**: 1500 Points +**Proof**: Add your oss handle and guide link to the list below. + +Please follow the following schema: + +--- + +» 05-April-2024 by YOUR oss.gg HANDLE » guide Link: https://link.to/content + +--- + +//////////////////////////// + +Your turn 👇 + +//////////////////////////// + +» 02-October-2024 by [yourhandle](https://oss.gg/yourhandle) guide Link: [guide](https://twenty.com/) + +--- \ No newline at end of file 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 new file mode 100644 index 000000000000..0a33e2287146 --- /dev/null +++ b/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md @@ -0,0 +1,23 @@ +**Side Quest**: Meme Magic - Craft a meme where a brick plays a role. Tweet it, and tag us @papermarkio to submit. +**Points**: 150 Points +**Proof**: Add a screenshot of meme to the PR description. Add a link to your tweet in the list below. + +Please follow the following schema: + +--- + +» 05-April-2024 by YOUR NAME +» Link to Tweet: https://x.com/... + +--- + +//////////////////////////// + +Your turn 👇 + +//////////////////////////// + +» 01-October-2024 by YOUR NAME +» Link to Tweet: https://x.com/... + +--- 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 new file mode 100644 index 000000000000..0a33e2287146 --- /dev/null +++ b/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md @@ -0,0 +1,23 @@ +**Side Quest**: Meme Magic - Craft a meme where a brick plays a role. Tweet it, and tag us @papermarkio to submit. +**Points**: 150 Points +**Proof**: Add a screenshot of meme to the PR description. Add a link to your tweet in the list below. + +Please follow the following schema: + +--- + +» 05-April-2024 by YOUR NAME +» Link to Tweet: https://x.com/... + +--- + +//////////////////////////// + +Your turn 👇 + +//////////////////////////// + +» 01-October-2024 by YOUR NAME +» Link to Tweet: https://x.com/... + +--- diff --git a/oss-gg/twenty-side-quest/3-meme-magic.md b/oss-gg/twenty-side-quest/3-meme-magic.md new file mode 100644 index 000000000000..0a33e2287146 --- /dev/null +++ b/oss-gg/twenty-side-quest/3-meme-magic.md @@ -0,0 +1,23 @@ +**Side Quest**: Meme Magic - Craft a meme where a brick plays a role. Tweet it, and tag us @papermarkio to submit. +**Points**: 150 Points +**Proof**: Add a screenshot of meme to the PR description. Add a link to your tweet in the list below. + +Please follow the following schema: + +--- + +» 05-April-2024 by YOUR NAME +» Link to Tweet: https://x.com/... + +--- + +//////////////////////////// + +Your turn 👇 + +//////////////////////////// + +» 01-October-2024 by YOUR NAME +» Link to Tweet: https://x.com/... + +--- diff --git a/oss-gg/twenty-side-quest/4-gif-magic.md b/oss-gg/twenty-side-quest/4-gif-magic.md new file mode 100644 index 000000000000..0e38ace584d6 --- /dev/null +++ b/oss-gg/twenty-side-quest/4-gif-magic.md @@ -0,0 +1,23 @@ +**Side Quest**: GIF Magic - Craft a GIF where a brick plays a role. Upload it to GIPHY with tags 'open source', 'foss', 'papermarkio'. +**Points**: 150 Points +**Proof**: Add a screenshot of GIF on Giphy to the PR description. Add a link to your GIPHY in the list below. + +Please follow the following schema: + +--- + +» 05-April-2024 by YOUR NAME +» Link to Tweet: https://giphy.com/... + +--- + +//////////////////////////// + +Your turn 👇 + +//////////////////////////// + +» 01-October-2024 by YOUR NAME +» Link to Tweet: https://x.com/... + +--- diff --git a/oss-gg/twenty-side-quest/5-quest-wizard.md b/oss-gg/twenty-side-quest/5-quest-wizard.md new file mode 100644 index 000000000000..2dfe4bd9c86b --- /dev/null +++ b/oss-gg/twenty-side-quest/5-quest-wizard.md @@ -0,0 +1,19 @@ +**Side Quest**: Complete all papermarkio side quests +**Points**: 300 Points +**Proof**: Add screenshots for each side quest to the PR description. Add your name to the list below. + +Please follow the following schema: + +--- + + » 05-April-2024 by YOUR NAME + +//////////////////////////// + +Your turn 👇 + +//////////////////////////// + +» 01-October-2024 by X + +--- diff --git a/package.json b/package.json index 640ba3bb5c96..a4dc90df92ac 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,6 @@ "@blocknote/react": "^0.15.3", "@codesandbox/sandpack-react": "^2.13.5", "@dagrejs/dagre": "^1.1.2", - "@docusaurus/core": "^3.1.0", - "@docusaurus/preset-classic": "^3.1.0", "@emotion/react": "^11.11.1", "@emotion/styled": "^11.11.0", "@envelop/on-resolve": "^4.1.0", @@ -38,8 +36,6 @@ "@nestjs/serve-static": "^4.0.1", "@nestjs/terminus": "^9.2.2", "@nestjs/typeorm": "^10.0.0", - "@nivo/calendar": "^0.84.0", - "@nivo/core": "^0.84.0", "@nx/eslint-plugin": "^17.2.8", "@octokit/graphql": "^7.0.2", "@ptc-org/nestjs-query-core": "^4.2.0", @@ -77,13 +73,13 @@ "class-transformer": "^0.5.1", "clsx": "^2.1.1", "cross-env": "^7.0.3", + "css-loader": "^7.1.2", "danger-plugin-todos": "^1.3.1", "dataloader": "^2.2.2", "date-fns": "^2.30.0", "date-fns-tz": "^2.0.0", "debounce": "^2.0.0", "deep-equal": "^2.2.2", - "docusaurus-node-polyfills": "^1.0.0", "dompurify": "^3.0.11", "dotenv-cli": "^7.2.1", "drizzle-orm": "^0.29.3", @@ -198,8 +194,6 @@ "@babel/preset-react": "^7.14.5", "@babel/preset-typescript": "^7.24.6", "@crxjs/vite-plugin": "^1.0.14", - "@docusaurus/module-type-aliases": "^3.1.0", - "@docusaurus/tsconfig": "3.1.0", "@graphql-codegen/cli": "^3.3.1", "@graphql-codegen/client-preset": "^4.1.0", "@graphql-codegen/typescript": "^3.0.4", @@ -273,6 +267,7 @@ "@types/node": "18.19.26", "@types/passport-google-oauth20": "^2.0.11", "@types/passport-jwt": "^3.0.8", + "@types/pluralize": "^0.0.33", "@types/react": "^18.2.39", "@types/react-datepicker": "^6.2.0", "@types/react-dom": "^18.2.15", @@ -343,7 +338,7 @@ }, "license": "AGPL-3.0", "name": "twenty", - "packageManager": "yarn@4.3.1", + "packageManager": "yarn@4.4.0", "resolutions": { "graphql": "16.8.0", "type-fest": "4.10.1", diff --git a/packages/twenty-docker/.env.example b/packages/twenty-docker/.env.example index c25482220fce..59d8d03f93a7 100644 --- a/packages/twenty-docker/.env.example +++ b/packages/twenty-docker/.env.example @@ -5,6 +5,8 @@ TAG=latest PG_DATABASE_HOST=db:5432 SERVER_URL=http://localhost:3000 +# REDIS_HOST=redis +# REDIS_PORT=6379 # Use openssl rand -base64 32 for each secret # ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access @@ -19,5 +21,3 @@ STORAGE_TYPE=local # STORAGE_S3_REGION=eu-west3 # STORAGE_S3_NAME=my-bucket # STORAGE_S3_ENDPOINT= - -MESSAGE_QUEUE_TYPE=pg-boss diff --git a/packages/twenty-docker/docker-compose.yml b/packages/twenty-docker/docker-compose.yml index 553d8ca6c9fa..b2efc1a168e4 100644 --- a/packages/twenty-docker/docker-compose.yml +++ b/packages/twenty-docker/docker-compose.yml @@ -25,7 +25,8 @@ services: PG_DATABASE_URL: postgres://twenty:twenty@${PG_DATABASE_HOST}/default SERVER_URL: ${SERVER_URL} FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL} - MESSAGE_QUEUE_TYPE: ${MESSAGE_QUEUE_TYPE} + REDIS_PORT: ${REDIS_PORT:-6379} + REDIS_HOST: ${REDIS_HOST:-redis} ENABLE_DB_MIGRATIONS: "true" @@ -34,6 +35,7 @@ services: STORAGE_S3_REGION: ${STORAGE_S3_REGION} STORAGE_S3_NAME: ${STORAGE_S3_NAME} STORAGE_S3_ENDPOINT: ${STORAGE_S3_ENDPOINT} + ACCESS_TOKEN_SECRET: ${ACCESS_TOKEN_SECRET} LOGIN_TOKEN_SECRET: ${LOGIN_TOKEN_SECRET} REFRESH_TOKEN_SECRET: ${REFRESH_TOKEN_SECRET} @@ -57,7 +59,8 @@ services: PG_DATABASE_URL: postgres://twenty:twenty@${PG_DATABASE_HOST}/default SERVER_URL: ${SERVER_URL} FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL} - MESSAGE_QUEUE_TYPE: ${MESSAGE_QUEUE_TYPE} + REDIS_PORT: ${REDIS_PORT:-6379} + REDIS_HOST: ${REDIS_HOST:-redis} ENABLE_DB_MIGRATIONS: "false" # it already runs on the server @@ -65,6 +68,7 @@ services: STORAGE_S3_REGION: ${STORAGE_S3_REGION} STORAGE_S3_NAME: ${STORAGE_S3_NAME} STORAGE_S3_ENDPOINT: ${STORAGE_S3_ENDPOINT} + ACCESS_TOKEN_SECRET: ${ACCESS_TOKEN_SECRET} LOGIN_TOKEN_SECRET: ${LOGIN_TOKEN_SECRET} REFRESH_TOKEN_SECRET: ${REFRESH_TOKEN_SECRET} @@ -89,6 +93,12 @@ services: retries: 10 restart: always + redis: + image: redis + ports: + - "6379:6379" + restart: always + volumes: docker-data: db-data: diff --git a/packages/twenty-docker/k8s/manifests/deployment-db.yaml b/packages/twenty-docker/k8s/manifests/deployment-db.yaml index 2e317376d53b..31a3361774e4 100644 --- a/packages/twenty-docker/k8s/manifests/deployment-db.yaml +++ b/packages/twenty-docker/k8s/manifests/deployment-db.yaml @@ -22,33 +22,33 @@ spec: app: twentycrm-db spec: volumes: - - name: twentycrm-db-data - persistentVolumeClaim: - claimName: twentycrm-db-pvc + - name: twentycrm-db-data + persistentVolumeClaim: + claimName: twentycrm-db-pvc containers: - - env: - - name: POSTGRES_PASSWORD - value: "twenty" - - name: BITNAMI_DEBUG - value: "true" - - image: twentycrm/twenty-postgres:latest - imagePullPolicy: Always - name: twentycrm - ports: - - containerPort: 5432 - name: tcp - protocol: TCP - resources: - requests: - memory: "256Mi" - cpu: "250m" - limits: - memory: "1024Mi" - cpu: "1000m" - stdin: true - tty: true - volumeMounts: - - mountPath: /bitnami/postgresql - name: twentycrm-db-data + - name: twentycrm + image: twentycrm/twenty-postgres:latest + imagePullPolicy: Always + env: + - name: POSTGRES_PASSWORD + value: "twenty" + - name: BITNAMI_DEBUG + value: "true" + ports: + - containerPort: 5432 + name: tcp + protocol: TCP + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "1024Mi" + cpu: "1000m" + stdin: true + tty: true + volumeMounts: + - mountPath: /bitnami/postgresql + name: twentycrm-db-data dnsPolicy: ClusterFirst restartPolicy: Always diff --git a/packages/twenty-docker/k8s/manifests/deployment-redis.yaml b/packages/twenty-docker/k8s/manifests/deployment-redis.yaml new file mode 100644 index 000000000000..e09874aac262 --- /dev/null +++ b/packages/twenty-docker/k8s/manifests/deployment-redis.yaml @@ -0,0 +1,44 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + labels: + app: twentycrm-redis + name: twentycrm-redis + namespace: twentycrm +spec: + progressDeadlineSeconds: 600 + replicas: 1 + strategy: + rollingUpdate: + maxSurge: 1 + maxUnavailable: 1 + type: RollingUpdate + selector: + matchLabels: + app: twentycrm-redis + template: + metadata: + labels: + app: twentycrm-redis + spec: + containers: + - name: redis + image: redis/redis-stack-server:latest + imagePullPolicy: Always + env: + - name: PORT + value: 6379 + ports: + - containerPort: 6379 + name: redis + protocol: TCP + resources: + requests: + memory: "1024Mi" + cpu: "250m" + limits: + memory: "2048Mi" + cpu: "500m" + + dnsPolicy: ClusterFirst + restartPolicy: Always diff --git a/packages/twenty-docker/k8s/manifests/deployment-server.yaml b/packages/twenty-docker/k8s/manifests/deployment-server.yaml index b4596e9fc87b..b1229d649bbb 100644 --- a/packages/twenty-docker/k8s/manifests/deployment-server.yaml +++ b/packages/twenty-docker/k8s/manifests/deployment-server.yaml @@ -22,67 +22,78 @@ spec: app: twentycrm-server spec: volumes: - - name: twentycrm-server-data - persistentVolumeClaim: - claimName: twentycrm-server-pvc + - name: twentycrm-server-data + persistentVolumeClaim: + claimName: twentycrm-server-pvc + - name: twentycrm-docker-data + persistentVolumeClaim: + claimName: twentycrm-docker-data-pvc containers: - - env: - - name: PORT - value: 3000 - - name: SERVER_URL - value: "https://crm.example.com:443" - - name: FRONT_BASE_URL - value: "https://crm.example.com:443" - - name: PG_DATABASE_URL - value: "postgres://twenty:twenty@twenty-db.twentycrm.svc.cluster.local/default" - - name: ENABLE_DB_MIGRATIONS - value: "true" - - name: SIGN_IN_PREFILLED - value: "true" - - name: STORAGE_TYPE - value: "local" - - name: "MESSAGE_QUEUE_TYPE" - value: "pg-boss" - - name: ACCESS_TOKEN_SECRET - valueFrom: - secretKeyRef: - name: tokens - key: accessToken - - name: LOGIN_TOKEN_SECRET - valueFrom: - secretKeyRef: - name: tokens - key: loginToken - - name: REFRESH_TOKEN_SECRET - valueFrom: - secretKeyRef: - name: tokens - key: refreshToken - - name: FILE_TOKEN_SECRET - valueFrom: - secretKeyRef: - name: tokens - key: fileToken - - image: twentycrm/twenty:latest - imagePullPolicy: Always - name: twentycrm - ports: - - containerPort: 3000 - name: http-tcp - protocol: TCP - resources: - requests: - memory: "256Mi" - cpu: "250m" - limits: - memory: "1024Mi" - cpu: "1000m" - stdin: true - tty: true - volumeMounts: - - mountPath: /app/docker-data - name: twentycrm-server-data - - mountPath: /app/.local-storage - name: twentycrm-server-data + - name: twentycrm + image: twentycrm/twenty:latest + imagePullPolicy: Always + env: + - name: PORT + value: 3000 + - name: SERVER_URL + value: "https://crm.example.com:443" + - name: FRONT_BASE_URL + value: "https://crm.example.com:443" + - name: "PG_DATABASE_URL" + value: "postgres://twenty:twenty@twenty-db.twentycrm.svc.cluster.local/default" + - name: "REDIS_HOST" + value: "twentycrm-redis.twentycrm.svc.cluster.local" + - name: "REDIS_PORT" + value: 6379 + - name: ENABLE_DB_MIGRATIONS + value: "true" + - name: SIGN_IN_PREFILLED + value: "true" + - name: STORAGE_TYPE + value: "local" + - name: "MESSAGE_QUEUE_TYPE" + value: "bull-mq" + - name: "ACCESS_TOKEN_EXPIRES_IN" + value: "7d" + - name: "LOGIN_TOKEN_EXPIRES_IN" + value: "1h" + - name: ACCESS_TOKEN_SECRET + valueFrom: + secretKeyRef: + name: tokens + key: accessToken + - name: LOGIN_TOKEN_SECRET + valueFrom: + secretKeyRef: + name: tokens + key: loginToken + - name: REFRESH_TOKEN_SECRET + valueFrom: + secretKeyRef: + name: tokens + key: refreshToken + - name: FILE_TOKEN_SECRET + valueFrom: + secretKeyRef: + name: tokens + key: fileToken + ports: + - containerPort: 3000 + name: http-tcp + protocol: TCP + resources: + requests: + memory: "256Mi" + cpu: "250m" + limits: + memory: "1024Mi" + cpu: "1000m" + stdin: true + tty: true + volumeMounts: + - mountPath: /app/docker-data + name: twentycrm-docker-data + - mountPath: /app/packages/twenty-server/.local-storage + name: twentycrm-server-data dnsPolicy: ClusterFirst restartPolicy: Always diff --git a/packages/twenty-docker/k8s/manifests/deployment-worker.yaml b/packages/twenty-docker/k8s/manifests/deployment-worker.yaml index b3834c46e515..b3a7e07a19aa 100644 --- a/packages/twenty-docker/k8s/manifests/deployment-worker.yaml +++ b/packages/twenty-docker/k8s/manifests/deployment-worker.yaml @@ -21,58 +21,60 @@ spec: labels: app: twentycrm-worker spec: - volumes: - - name: twentycrm-worker-data - persistentVolumeClaim: - claimName: twentycrm-worker-pvc containers: - - env: - - name: SERVER_URL - value: "https://crm.example.com:443" - - name: FRONT_BASE_URL - value: "https://crm.example.com:443" - - name: PG_DATABASE_URL - value: "postgres://twenty:twenty@twenty-db.twentycrm.svc.cluster.local/default" - - name: ENABLE_DB_MIGRATIONS - value: "false" # it already runs on the server - - name: STORAGE_TYPE - value: "local" - - name: "MESSAGE_QUEUE_TYPE" - value: "pg-boss" - - name: ACCESS_TOKEN_SECRET - valueFrom: - secretKeyRef: - name: tokens - key: accessToken - - name: LOGIN_TOKEN_SECRET - valueFrom: - secretKeyRef: - name: tokens - key: loginToken - - name: REFRESH_TOKEN_SECRET - valueFrom: - secretKeyRef: - name: tokens - key: refreshToken - - name: FILE_TOKEN_SECRET - valueFrom: - secretKeyRef: - name: tokens - key: fileToken - - image: twentycrm/twenty:latest - imagePullPolicy: Always - name: twentycrm - command: - - yarn - - worker:prod - resources: - requests: - memory: "256Mi" - cpu: "250m" - limits: - memory: "1024Mi" - cpu: "1000m" - stdin: true - tty: true + - name: twentycrm + image: twentycrm/twenty:latest + imagePullPolicy: Always + env: + - name: SERVER_URL + value: "https://crm.example.com:443" + - name: FRONT_BASE_URL + value: "https://crm.example.com:443" + - name: PG_DATABASE_URL + value: "postgres://twenty:twenty@twenty-db.twentycrm.svc.cluster.local/default" + - name: ENABLE_DB_MIGRATIONS + value: "false" # it already runs on the server + - name: STORAGE_TYPE + value: "local" + - name: "MESSAGE_QUEUE_TYPE" + value: "bull-mq" + - name: "CACHE_STORAGE_TYPE" + value: "redis" + - name: "REDIS_HOST" + value: "twentycrm-redis.twentycrm.svc.cluster.local" + - name: "REDIS_PORT" + value: 6379 + - name: ACCESS_TOKEN_SECRET + valueFrom: + secretKeyRef: + name: tokens + key: accessToken + - name: LOGIN_TOKEN_SECRET + valueFrom: + secretKeyRef: + name: tokens + key: loginToken + - name: REFRESH_TOKEN_SECRET + valueFrom: + secretKeyRef: + name: tokens + key: refreshToken + - name: FILE_TOKEN_SECRET + valueFrom: + secretKeyRef: + name: tokens + key: fileToken + command: + - yarn + - worker:prod + resources: + requests: + memory: "1024Mi" + cpu: "250m" + limits: + memory: "2048Mi" + cpu: "1000m" + stdin: true + tty: true dnsPolicy: ClusterFirst restartPolicy: Always diff --git a/packages/twenty-docker/k8s/manifests/ingress.yaml b/packages/twenty-docker/k8s/manifests/ingress.yaml index b334aac21916..0bbae11dd72b 100644 --- a/packages/twenty-docker/k8s/manifests/ingress.yaml +++ b/packages/twenty-docker/k8s/manifests/ingress.yaml @@ -4,21 +4,21 @@ metadata: name: twentycrm namespace: twentycrm annotations: - nginx.ingress.kubernetes.io/configuration-snippet: | + nginx.ingress.kubernetes.io/configuration-snippet: | more_set_headers "X-Forwarded-For $http_x_forwarded_for"; - nginx.ingress.kubernetes.io/force-ssl-redirect: "false" - kubernetes.io/ingress.class: "nginx" - nginx.ingress.kubernetes.io/backend-protocol: "HTTP" + nginx.ingress.kubernetes.io/force-ssl-redirect: "false" + kubernetes.io/ingress.class: "nginx" + nginx.ingress.kubernetes.io/backend-protocol: "HTTP" spec: ingressClassName: nginx rules: - - host: crm.example.com - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: twentycrm-server - port: - name: http-tcp + - host: crm.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: twentycrm-server + port: + name: http-tcp diff --git a/packages/twenty-docker/k8s/manifests/pv-docker-data.yaml b/packages/twenty-docker/k8s/manifests/pv-docker-data.yaml new file mode 100644 index 000000000000..95fc52a26251 --- /dev/null +++ b/packages/twenty-docker/k8s/manifests/pv-docker-data.yaml @@ -0,0 +1,11 @@ +apiVersion: v1 +kind: PersistentVolume +metadata: + name: twentycrm-docker-data-pv +spec: + storageClassName: default + capacity: + storage: 100Mi + accessModes: + - ReadWriteOnce + persistentVolumeReclaimPolicy: Retain diff --git a/packages/twenty-docker/k8s/manifests/pvc-docker-data.yaml b/packages/twenty-docker/k8s/manifests/pvc-docker-data.yaml new file mode 100644 index 000000000000..12dd071a7f21 --- /dev/null +++ b/packages/twenty-docker/k8s/manifests/pvc-docker-data.yaml @@ -0,0 +1,13 @@ +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: twentycrm-docker-data-pvc + namespace: twentycrm +spec: + storageClassName: default + volumeName: twentycrm-docker-data-pv + accessModes: + - ReadWriteOnce + resources: + requests: + storage: 100Mi diff --git a/packages/twenty-docker/k8s/manifests/service-db.yaml b/packages/twenty-docker/k8s/manifests/service-db.yaml index bb0e38df6d6d..89dbd1464bed 100644 --- a/packages/twenty-docker/k8s/manifests/service-db.yaml +++ b/packages/twenty-docker/k8s/manifests/service-db.yaml @@ -6,9 +6,9 @@ metadata: spec: internalTrafficPolicy: Cluster ports: - - port: 5432 - protocol: TCP - targetPort: 5432 + - port: 5432 + protocol: TCP + targetPort: 5432 selector: app: twentycrm-db sessionAffinity: ClientIP diff --git a/packages/twenty-docker/k8s/manifests/service-redis.yaml b/packages/twenty-docker/k8s/manifests/service-redis.yaml new file mode 100644 index 000000000000..49f508897dfa --- /dev/null +++ b/packages/twenty-docker/k8s/manifests/service-redis.yaml @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: Service +metadata: + name: twentycrm-redis + namespace: twentycrm +spec: + internalTrafficPolicy: Cluster + ports: + - port: 6379 + protocol: TCP + targetPort: 6379 + selector: + app: twentycrm-redis + sessionAffinity: ClientIP + sessionAffinityConfig: + clientIP: + timeoutSeconds: 10800 + type: ClusterIP diff --git a/packages/twenty-docker/k8s/manifests/service-server.yaml b/packages/twenty-docker/k8s/manifests/service-server.yaml index 7fcc869a6edc..b45b28f312ff 100644 --- a/packages/twenty-docker/k8s/manifests/service-server.yaml +++ b/packages/twenty-docker/k8s/manifests/service-server.yaml @@ -6,10 +6,10 @@ metadata: spec: internalTrafficPolicy: Cluster ports: - - name: http-tcp - port: 3000 - protocol: TCP - targetPort: 3000 + - name: http-tcp + port: 3000 + protocol: TCP + targetPort: 3000 selector: app: twentycrm-server sessionAffinity: ClientIP diff --git a/packages/twenty-docker/k8s/terraform/.terraform-docs.yml b/packages/twenty-docker/k8s/terraform/.terraform-docs.yml index 00778168f3ee..792c543f4d30 100644 --- a/packages/twenty-docker/k8s/terraform/.terraform-docs.yml +++ b/packages/twenty-docker/k8s/terraform/.terraform-docs.yml @@ -15,12 +15,12 @@ output: # TwentyCRM Terraform Docs - This file was generated by [terraform-docs](https://terraform-docs.io/), for more information on how to install, configure and use visit their website. + This file was generated by [terraform-docs](https://terraform-docs.io/), for more information on how to install, configure, and use visit their website. - To update this `README.md` after changes to the Terraform code in this folder, run: `terraform-docs .` + To update this `README.md` after changes to the Terraform code in this folder, run: `terraform-docs -c `./.terraform-docs.yml .` To make configuration changes to how this doc is generated, see `./.terraform-docs.yml` - + {{ .Content }} @@ -45,4 +45,4 @@ settings: read-comments: true required: true sensitive: true - type: true \ No newline at end of file + type: true diff --git a/packages/twenty-docker/k8s/terraform/README.md b/packages/twenty-docker/k8s/terraform/README.md index 10a7ab557cb7..f6955300a63f 100644 --- a/packages/twenty-docker/k8s/terraform/README.md +++ b/packages/twenty-docker/k8s/terraform/README.md @@ -1,9 +1,9 @@ # TwentyCRM Terraform Docs -This file was generated by [terraform-docs](https://terraform-docs.io/), for more information on how to install, configure and use visit their website. +This file was generated by [terraform-docs](https://terraform-docs.io/), for more information on how to install, configure, and use visit their website. -To update this `README.md` after changes to the Terraform code in this folder, run: `terraform-docs .` +To update this `README.md` after changes to the Terraform code in this folder, run: `terraform-docs -c `./.terraform-docs.yml .` To make configuration changes to how this doc is generated, see `./.terraform-docs.yml` @@ -12,30 +12,37 @@ To make configuration changes to how this doc is generated, see `./.terraform-do | Name | Version | |------|---------| | [terraform](#requirement\_terraform) | >= 1.9.2 | -| [kubernetes](#requirement\_kubernetes) | >= 2.31.0 | +| [kubernetes](#requirement\_kubernetes) | >= 2.32.0 | +| [random](#requirement\_random) | >= 3.6.3 | ## Providers | Name | Version | |------|---------| -| [kubernetes](#provider\_kubernetes) | >= 2.31.0 | +| [kubernetes](#provider\_kubernetes) | >= 2.32.0 | +| [random](#provider\_random) | >= 3.6.3 | ## Resources | Name | Type | |------|------| | [kubernetes_deployment.twentycrm_db](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) | resource | +| [kubernetes_deployment.twentycrm_redis](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) | resource | | [kubernetes_deployment.twentycrm_server](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) | resource | | [kubernetes_deployment.twentycrm_worker](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/deployment) | resource | | [kubernetes_ingress.twentycrm](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/ingress) | resource | | [kubernetes_namespace.twentycrm](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/namespace) | resource | | [kubernetes_persistent_volume.db](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume) | resource | +| [kubernetes_persistent_volume.docker_data](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume) | resource | | [kubernetes_persistent_volume.server](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume) | resource | | [kubernetes_persistent_volume_claim.db](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume_claim) | resource | +| [kubernetes_persistent_volume_claim.docker_data](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume_claim) | resource | | [kubernetes_persistent_volume_claim.server](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume_claim) | resource | | [kubernetes_secret.twentycrm_tokens](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/secret) | resource | | [kubernetes_service.twentycrm_db](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service) | resource | +| [kubernetes_service.twentycrm_redis](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service) | resource | | [kubernetes_service.twentycrm_server](https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/service) | resource | +| [random_bytes.this](https://registry.terraform.io/providers/hashicorp/random/latest/docs/resources/bytes) | resource | ## Inputs @@ -43,22 +50,24 @@ To make configuration changes to how this doc is generated, see `./.terraform-do |------|-------------|------|---------|:--------:| | [twentycrm\_app\_hostname](#input\_twentycrm\_app\_hostname) | The protocol, DNS fully qualified hostname, and port used to access TwentyCRM in your environment. Ex: https://crm.example.com:443 | `string` | n/a | yes | | [twentycrm\_pgdb\_admin\_password](#input\_twentycrm\_pgdb\_admin\_password) | TwentyCRM password for postgres database. | `string` | n/a | yes | -| [twentycrm\_token\_accessToken](#input\_twentycrm\_token\_accessToken) | TwentyCRM access Token | `string` | n/a | yes | -| [twentycrm\_token\_fileToken](#input\_twentycrm\_token\_fileToken) | TwentyCRM file Token | `string` | n/a | yes | -| [twentycrm\_token\_loginToken](#input\_twentycrm\_token\_loginToken) | TwentyCRM login Token | `string` | n/a | yes | -| [twentycrm\_token\_refreshToken](#input\_twentycrm\_token\_refreshToken) | TwentyCRM refresh Token | `string` | n/a | yes | | [twentycrm\_app\_name](#input\_twentycrm\_app\_name) | A friendly name prefix to use for every component deployed. | `string` | `"twentycrm"` | no | | [twentycrm\_db\_image](#input\_twentycrm\_db\_image) | TwentyCRM image for database deployment. This defaults to latest. | `string` | `"twentycrm/twenty-postgres:latest"` | no | | [twentycrm\_db\_pv\_capacity](#input\_twentycrm\_db\_pv\_capacity) | Storage capacity provisioned for database persistent volume. | `string` | `"10Gi"` | no | | [twentycrm\_db\_pv\_path](#input\_twentycrm\_db\_pv\_path) | Local path to use to store the physical volume if using local storage on nodes. | `string` | `""` | no | | [twentycrm\_db\_pvc\_requests](#input\_twentycrm\_db\_pvc\_requests) | Storage capacity reservation for database persistent volume claim. | `string` | `"10Gi"` | no | | [twentycrm\_db\_replicas](#input\_twentycrm\_db\_replicas) | Number of replicas for the TwentyCRM database deployment. This defaults to 1. | `number` | `1` | no | +| [twentycrm\_docker\_data\_mount\_path](#input\_twentycrm\_docker\_data\_mount\_path) | TwentyCRM mount path for servers application data. Defaults to '/app/docker-data'. | `string` | `"/app/docker-data"` | no | +| [twentycrm\_docker\_data\_pv\_capacity](#input\_twentycrm\_docker\_data\_pv\_capacity) | Storage capacity provisioned for server persistent volume. | `string` | `"10Gi"` | no | +| [twentycrm\_docker\_data\_pv\_path](#input\_twentycrm\_docker\_data\_pv\_path) | Local path to use to store the physical volume if using local storage on nodes. | `string` | `""` | no | +| [twentycrm\_docker\_data\_pvc\_requests](#input\_twentycrm\_docker\_data\_pvc\_requests) | Storage capacity reservation for server persistent volume claim. | `string` | `"10Gi"` | no | | [twentycrm\_namespace](#input\_twentycrm\_namespace) | Namespace for all TwentyCRM resources | `string` | `"twentycrm"` | no | -| [twentycrm\_server\_data\_mount\_path](#input\_twentycrm\_server\_data\_mount\_path) | TwentyCRM mount path for servers application data. Defaults to '/app/docker-data'. | `string` | `"/app/docker-data"` | no | +| [twentycrm\_redis\_image](#input\_twentycrm\_redis\_image) | TwentyCRM image for Redis deployment. This defaults to latest. | `string` | `"redis/redis-stack-server:latest"` | no | +| [twentycrm\_redis\_replicas](#input\_twentycrm\_redis\_replicas) | Number of replicas for the TwentyCRM Redis deployment. This defaults to 1. | `number` | `1` | no | +| [twentycrm\_server\_data\_mount\_path](#input\_twentycrm\_server\_data\_mount\_path) | TwentyCRM mount path for servers application data. Defaults to '/app/packages/twenty-server/.local-storage'. | `string` | `"/app/packages/twenty-server/.local-storage"` | no | | [twentycrm\_server\_image](#input\_twentycrm\_server\_image) | TwentyCRM server image for the server deployment. This defaults to latest. This value is also used for the workers image. | `string` | `"twentycrm/twenty:latest"` | no | | [twentycrm\_server\_pv\_capacity](#input\_twentycrm\_server\_pv\_capacity) | Storage capacity provisioned for server persistent volume. | `string` | `"10Gi"` | no | | [twentycrm\_server\_pv\_path](#input\_twentycrm\_server\_pv\_path) | Local path to use to store the physical volume if using local storage on nodes. | `string` | `""` | no | | [twentycrm\_server\_pvc\_requests](#input\_twentycrm\_server\_pvc\_requests) | Storage capacity reservation for server persistent volume claim. | `string` | `"10Gi"` | no | | [twentycrm\_server\_replicas](#input\_twentycrm\_server\_replicas) | Number of replicas for the TwentyCRM server deployment. This defaults to 1. | `number` | `1` | no | | [twentycrm\_worker\_replicas](#input\_twentycrm\_worker\_replicas) | Number of replicas for the TwentyCRM worker deployment. This defaults to 1. | `number` | `1` | no | - \ No newline at end of file + diff --git a/packages/twenty-docker/k8s/terraform/deployment-redis.tf b/packages/twenty-docker/k8s/terraform/deployment-redis.tf new file mode 100644 index 000000000000..d867dac76ee0 --- /dev/null +++ b/packages/twenty-docker/k8s/terraform/deployment-redis.tf @@ -0,0 +1,60 @@ +resource "kubernetes_deployment" "twentycrm_redis" { + metadata { + name = "${var.twentycrm_app_name}-redis" + namespace = kubernetes_namespace.twentycrm.metadata.0.name + + labels = { + app = "${var.twentycrm_app_name}-redis" + } + } + + spec { + replicas = var.twentycrm_redis_replicas + selector { + match_labels = { + app = "${var.twentycrm_app_name}-redis" + } + } + + strategy { + type = "RollingUpdate" + rolling_update { + max_surge = "1" + max_unavailable = "1" + } + } + + template { + metadata { + labels = { + app = "${var.twentycrm_app_name}-redis" + } + } + + spec { + container { + image = var.twentycrm_redis_image + name = "redis" + + port { + container_port = 6379 + protocol = "TCP" + } + + resources { + requests = { + cpu = "250m" + memory = "1024Mi" + } + limits = { + cpu = "500m" + memory = "2048Mi" + } + } + } + dns_policy = "ClusterFirst" + restart_policy = "Always" + } + } + } +} diff --git a/packages/twenty-docker/k8s/terraform/deployment-server.tf b/packages/twenty-docker/k8s/terraform/deployment-server.tf index a3c1f9ac1d11..1868b17624da 100644 --- a/packages/twenty-docker/k8s/terraform/deployment-server.tf +++ b/packages/twenty-docker/k8s/terraform/deployment-server.tf @@ -37,20 +37,14 @@ resource "kubernetes_deployment" "twentycrm_server" { stdin = true tty = true - security_context { - allow_privilege_escalation = true - privileged = true - run_as_user = 1000 - } - env { name = "PORT" value = "3000" } - env { - name = "DEBUG_MODE" - value = false - } + # env { + # name = "DEBUG_MODE" + # value = false + # } env { name = "SERVER_URL" @@ -64,9 +58,16 @@ resource "kubernetes_deployment" "twentycrm_server" { env { name = "PG_DATABASE_URL" - value = "postgres://twenty:${var.twentycrm_pgdb_admin_password}@${var.twentycrm_app_name}-db.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local/default" + value = "postgres://twenty:${var.twentycrm_pgdb_admin_password}@${kubernetes_service.twentycrm_db.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local/default" + } + env { + name = "REDIS_HOST" + value = "${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local" + } + env { + name = "REDIS_PORT" + value = 6379 } - env { name = "ENABLE_DB_MIGRATIONS" value = "true" @@ -83,7 +84,15 @@ resource "kubernetes_deployment" "twentycrm_server" { } env { name = "MESSAGE_QUEUE_TYPE" - value = "pg-boss" + value = "bull-mq" + } + env { + name = "ACCESS_TOKEN_EXPIRES_IN" + value = "7d" + } + env { + name = "LOGIN_TOKEN_EXPIRES_IN" + value = "1h" } env { name = "ACCESS_TOKEN_SECRET" @@ -145,6 +154,11 @@ resource "kubernetes_deployment" "twentycrm_server" { name = "server-data" mount_path = var.twentycrm_server_data_mount_path } + + volume_mount { + name = "docker-data" + mount_path = var.twentycrm_docker_data_mount_path + } } volume { @@ -155,6 +169,14 @@ resource "kubernetes_deployment" "twentycrm_server" { } } + volume { + name = "docker-data" + + persistent_volume_claim { + claim_name = kubernetes_persistent_volume_claim.docker_data.metadata.0.name + } + } + dns_policy = "ClusterFirst" restart_policy = "Always" } @@ -162,6 +184,7 @@ resource "kubernetes_deployment" "twentycrm_server" { } depends_on = [ kubernetes_deployment.twentycrm_db, + kubernetes_deployment.twentycrm_redis, kubernetes_secret.twentycrm_tokens ] } diff --git a/packages/twenty-docker/k8s/terraform/deployment-worker.tf b/packages/twenty-docker/k8s/terraform/deployment-worker.tf index 9a005839ddda..78e5ea6dcc1d 100644 --- a/packages/twenty-docker/k8s/terraform/deployment-worker.tf +++ b/packages/twenty-docker/k8s/terraform/deployment-worker.tf @@ -50,7 +50,22 @@ resource "kubernetes_deployment" "twentycrm_worker" { env { name = "PG_DATABASE_URL" - value = "postgres://twenty:${var.twentycrm_pgdb_admin_password}@${var.twentycrm_app_name}-db.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local/default" + value = "postgres://twenty:${var.twentycrm_pgdb_admin_password}@${kubernetes_service.twentycrm_db.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local/default" + } + + env { + name = "CACHE_STORAGE_TYPE" + value = "redis" + } + + env { + name = "REDIS_HOST" + value = "${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local" + } + + env { + name = "REDIS_PORT" + value = 6379 } env { @@ -64,7 +79,7 @@ resource "kubernetes_deployment" "twentycrm_worker" { } env { name = "MESSAGE_QUEUE_TYPE" - value = "pg-boss" + value = "bull-mq" } env { @@ -110,11 +125,11 @@ resource "kubernetes_deployment" "twentycrm_worker" { resources { requests = { cpu = "250m" - memory = "256Mi" + memory = "1024Mi" } limits = { cpu = "1000m" - memory = "1024Mi" + memory = "2048Mi" } } } @@ -126,6 +141,8 @@ resource "kubernetes_deployment" "twentycrm_worker" { } depends_on = [ kubernetes_deployment.twentycrm_db, - kubernetes_secret.twentycrm_tokens + kubernetes_deployment.twentycrm_redis, + kubernetes_deployment.twentycrm_server, + kubernetes_secret.twentycrm_tokens, ] } diff --git a/packages/twenty-docker/k8s/terraform/main.tf b/packages/twenty-docker/k8s/terraform/main.tf index 66ae6e18e061..a0e208d15f5d 100644 --- a/packages/twenty-docker/k8s/terraform/main.tf +++ b/packages/twenty-docker/k8s/terraform/main.tf @@ -13,7 +13,11 @@ terraform { required_providers { kubernetes = { source = "hashicorp/kubernetes" - version = ">= 2.31.0" + version = ">= 2.32.0" + } + random = { + source = "hashicorp/random" + version = ">= 3.6.3" } } } diff --git a/packages/twenty-docker/k8s/terraform/pv-docker-data.tf b/packages/twenty-docker/k8s/terraform/pv-docker-data.tf new file mode 100644 index 000000000000..9195fff61c8a --- /dev/null +++ b/packages/twenty-docker/k8s/terraform/pv-docker-data.tf @@ -0,0 +1,19 @@ +resource "kubernetes_persistent_volume" "docker_data" { + metadata { + name = "${var.twentycrm_app_name}-docker-data-pv" + } + spec { + storage_class_name = "default" + capacity = { + storage = var.twentycrm_docker_data_pv_capacity + } + access_modes = ["ReadWriteOnce"] + # refer to Terraform Docs for your specific implementation requirements + # https://registry.terraform.io/providers/hashicorp/kubernetes/latest/docs/resources/persistent_volume + persistent_volume_source { + local { + path = var.twentycrm_docker_data_pv_path + } + } + } +} diff --git a/packages/twenty-docker/k8s/terraform/pvc-docker-data.tf b/packages/twenty-docker/k8s/terraform/pvc-docker-data.tf new file mode 100644 index 000000000000..daac13dcc3a3 --- /dev/null +++ b/packages/twenty-docker/k8s/terraform/pvc-docker-data.tf @@ -0,0 +1,15 @@ +resource "kubernetes_persistent_volume_claim" "docker_data" { + metadata { + name = "${var.twentycrm_app_name}-docker-data-pvc" + namespace = kubernetes_namespace.twentycrm.metadata.0.name + } + spec { + access_modes = ["ReadWriteOnce"] + resources { + requests = { + storage = var.twentycrm_docker_data_pvc_requests + } + } + volume_name = kubernetes_persistent_volume.docker_data.metadata.0.name + } +} diff --git a/packages/twenty-docker/k8s/terraform/secret.tf b/packages/twenty-docker/k8s/terraform/secret.tf index 664d07803ccc..2aa7ccf4765a 100644 --- a/packages/twenty-docker/k8s/terraform/secret.tf +++ b/packages/twenty-docker/k8s/terraform/secret.tf @@ -1,3 +1,18 @@ +locals { + tokens = [ + "accessToken", + "loginToken", + "refreshToken", + "fileToken" + ] +} + +resource "random_bytes" "this" { + for_each = toset(local.tokens) + + length = 32 +} + resource "kubernetes_secret" "twentycrm_tokens" { metadata { name = "tokens" @@ -5,11 +20,9 @@ resource "kubernetes_secret" "twentycrm_tokens" { } data = { - accessToken = var.twentycrm_token_accessToken - loginToken = var.twentycrm_token_loginToken - refreshToken = var.twentycrm_token_refreshToken - fileToken = var.twentycrm_token_fileToken + accessToken = random_bytes.this["accessToken"].base64 + loginToken = random_bytes.this["loginToken"].base64 + refreshToken = random_bytes.this["refreshToken"].base64 + fileToken = random_bytes.this["fileToken"].base64 } - - # type = "kubernetes.io/basic-auth" } diff --git a/packages/twenty-docker/k8s/terraform/service-redis.tf b/packages/twenty-docker/k8s/terraform/service-redis.tf new file mode 100644 index 000000000000..fab1c0051ccf --- /dev/null +++ b/packages/twenty-docker/k8s/terraform/service-redis.tf @@ -0,0 +1,18 @@ +resource "kubernetes_service" "twentycrm_redis" { + metadata { + name = "${var.twentycrm_app_name}-redis" + namespace = kubernetes_namespace.twentycrm.metadata.0.name + } + spec { + selector = { + app = "${var.twentycrm_app_name}-redis" + } + session_affinity = "ClientIP" + port { + port = 6379 + target_port = 6379 + } + + type = "ClusterIP" + } +} diff --git a/packages/twenty-docker/k8s/terraform/variables.tf b/packages/twenty-docker/k8s/terraform/variables.tf index 53255aaf1489..7b682db79a35 100644 --- a/packages/twenty-docker/k8s/terraform/variables.tf +++ b/packages/twenty-docker/k8s/terraform/variables.tf @@ -1,30 +1,6 @@ ###################### # Required Variables # ###################### -variable "twentycrm_token_accessToken" { - type = string - description = "TwentyCRM access Token" - sensitive = true -} - -variable "twentycrm_token_loginToken" { - type = string - description = "TwentyCRM login Token" - sensitive = true -} - -variable "twentycrm_token_refreshToken" { - type = string - description = "TwentyCRM refresh Token" - sensitive = true -} - -variable "twentycrm_token_fileToken" { - type = string - description = "TwentyCRM file Token" - sensitive = true -} - variable "twentycrm_pgdb_admin_password" { type = string description = "TwentyCRM password for postgres database." @@ -77,8 +53,8 @@ variable "twentycrm_db_replicas" { variable "twentycrm_server_data_mount_path" { type = string - default = "/app/docker-data" - description = "TwentyCRM mount path for servers application data. Defaults to '/app/docker-data'." + default = "/app/packages/twenty-server/.local-storage" + description = "TwentyCRM mount path for servers application data. Defaults to '/app/packages/twenty-server/.local-storage'." } variable "twentycrm_db_pv_path" { @@ -122,3 +98,39 @@ variable "twentycrm_namespace" { default = "twentycrm" description = "Namespace for all TwentyCRM resources" } + +variable "twentycrm_redis_replicas" { + type = number + default = 1 + description = "Number of replicas for the TwentyCRM Redis deployment. This defaults to 1." +} + +variable "twentycrm_redis_image" { + type = string + default = "redis/redis-stack-server:latest" + description = "TwentyCRM image for Redis deployment. This defaults to latest." +} + +variable "twentycrm_docker_data_mount_path" { + type = string + default = "/app/docker-data" + description = "TwentyCRM mount path for servers application data. Defaults to '/app/docker-data'." +} + +variable "twentycrm_docker_data_pv_path" { + type = string + default = "" + description = "Local path to use to store the physical volume if using local storage on nodes." +} + +variable "twentycrm_docker_data_pv_capacity" { + type = string + default = "100Mi" + description = "Storage capacity provisioned for server persistent volume." +} + +variable "twentycrm_docker_data_pvc_requests" { + type = string + default = "100Mi" + description = "Storage capacity reservation for server persistent volume claim." +} diff --git a/packages/twenty-emails/package.json b/packages/twenty-emails/package.json index 36af44f3da6e..e42c2b318a21 100644 --- a/packages/twenty-emails/package.json +++ b/packages/twenty-emails/package.json @@ -1,6 +1,6 @@ { "name": "twenty-emails", - "version": "0.24.2", + "version": "0.31.0", "description": "", "author": "", "private": true, diff --git a/packages/twenty-emails/src/emails/workflow-action.email.tsx b/packages/twenty-emails/src/emails/workflow-action.email.tsx deleted file mode 100644 index 2eaa3a451ebb..000000000000 --- a/packages/twenty-emails/src/emails/workflow-action.email.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { BaseEmail } from 'src/components/BaseEmail'; -import { Title } from 'src/components/Title'; -import { CallToAction } from 'src/components/CallToAction'; - -type WorkflowActionEmailProps = { - dangerousHTML?: string; - title?: string; - callToAction?: { - value: string; - href: string; - }; -}; -export const WorkflowActionEmail = ({ - dangerousHTML, - title, - callToAction, -}: WorkflowActionEmailProps) => { - return ( - - {title && } - {dangerousHTML && ( - <div dangerouslySetInnerHTML={{ __html: dangerousHTML }} /> - )} - {callToAction && ( - <CallToAction value={callToAction.value} href={callToAction.href} /> - )} - </BaseEmail> - ); -}; diff --git a/packages/twenty-emails/src/index.ts b/packages/twenty-emails/src/index.ts index 9fca13d73b55..ddecb05c8655 100644 --- a/packages/twenty-emails/src/index.ts +++ b/packages/twenty-emails/src/index.ts @@ -3,4 +3,3 @@ export * from './emails/delete-inactive-workspaces.email'; export * from './emails/password-reset-link.email'; export * from './emails/password-update-notify.email'; export * from './emails/send-invite-link.email'; -export * from './emails/workflow-action.email'; diff --git a/packages/twenty-front/.storybook/preview.tsx b/packages/twenty-front/.storybook/preview.tsx index f49ed56c58b9..1d67634e2a54 100644 --- a/packages/twenty-front/.storybook/preview.tsx +++ b/packages/twenty-front/.storybook/preview.tsx @@ -1,7 +1,7 @@ -import { useEffect } from 'react'; import { ThemeProvider } from '@emotion/react'; import { Preview } from '@storybook/react'; import { initialize, mswDecorator } from 'msw-storybook-addon'; +import { useEffect } from 'react'; import { useDarkMode } from 'storybook-dark-mode'; import { THEME_DARK, THEME_LIGHT, ThemeContextProvider } from 'twenty-ui'; @@ -13,12 +13,16 @@ import 'react-loading-skeleton/dist/skeleton.css'; initialize({ onUnhandledRequest: async (request: Request) => { const fileExtensionsToIgnore = - /\.(ts|tsx|js|jsx|svg|css|png)(\?v=[a-zA-Z0-9]+)?/; + /\.(ts|tsx|js|jsx|svg|css|png|woff2)(\?v=[a-zA-Z0-9]+)?/; if (fileExtensionsToIgnore.test(request.url)) { return; } + if (request.url.startsWith('http://localhost:3000/files/data:image')) { + return; + } + const requestBody = await request.json(); // eslint-disable-next-line no-console console.warn(`Unhandled ${request.method} request to ${request.url} diff --git a/packages/twenty-front/jest.config.ts b/packages/twenty-front/jest.config.ts index c71df7b77aff..8ed7f398db4e 100644 --- a/packages/twenty-front/jest.config.ts +++ b/packages/twenty-front/jest.config.ts @@ -2,6 +2,7 @@ import { JestConfigWithTsJest, pathsToModuleNameMapper } from 'ts-jest'; // eslint-disable-next-line @typescript-eslint/no-var-requires const tsConfig = require('./tsconfig.json'); +process.env.TZ = 'GMT'; const jestConfig: JestConfigWithTsJest = { // to enable logs, comment out the following line @@ -25,7 +26,7 @@ const jestConfig: JestConfigWithTsJest = { coverageThreshold: { global: { statements: 60, - lines: 60, + lines: 55, functions: 50, }, }, diff --git a/packages/twenty-front/nyc.config.cjs b/packages/twenty-front/nyc.config.cjs index 3fbf2dfd6204..8ae501c6910f 100644 --- a/packages/twenty-front/nyc.config.cjs +++ b/packages/twenty-front/nyc.config.cjs @@ -16,7 +16,7 @@ const modulesCoverage = { }; const pagesCoverage = { - branches: 40, + branches: 35, statements: 60, lines: 60, functions: 45, diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json index 434934ed719d..b7c173b7fed0 100644 --- a/packages/twenty-front/package.json +++ b/packages/twenty-front/package.json @@ -1,6 +1,6 @@ { "name": "twenty-front", - "version": "0.24.2", + "version": "0.31.0", "private": true, "type": "module", "scripts": { @@ -30,6 +30,9 @@ "workerDirectory": "public" }, "dependencies": { + "@nivo/calendar": "^0.87.0", + "@nivo/core": "^0.87.0", + "@nivo/line": "^0.87.0", "@xyflow/react": "^12.0.4", "transliteration": "^2.3.5" } diff --git a/packages/twenty-front/project.json b/packages/twenty-front/project.json index 4dcb7f444a00..3ed94b22f256 100644 --- a/packages/twenty-front/project.json +++ b/packages/twenty-front/project.json @@ -67,7 +67,7 @@ "test": {}, "storybook:build": { "options": { - "env": { "NODE_OPTIONS": "--max_old_space_size=6000" } + "env": { "NODE_OPTIONS": "--max_old_space_size=6500" } } }, "storybook:serve:dev": { diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx deleted file mode 100644 index 1c6adcfdf60e..000000000000 --- a/packages/twenty-front/src/App.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { StrictMode } from 'react'; -import { - createBrowserRouter, - createRoutesFromElements, - Outlet, - Route, - RouterProvider, - useLocation, -} from 'react-router-dom'; -import { useRecoilValue } from 'recoil'; - -import { ApolloProvider } from '@/apollo/components/ApolloProvider'; -import { AuthProvider } from '@/auth/components/AuthProvider'; -import { VerifyEffect } from '@/auth/components/VerifyEffect'; -import { ChromeExtensionSidecarEffect } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarEffect'; -import { ChromeExtensionSidecarProvider } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider'; -import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider'; -import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect'; -import { billingState } from '@/client-config/states/billingState'; -import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect'; -import indexAppPath from '@/navigation/utils/indexAppPath'; -import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider'; -import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider'; -import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider'; -import { AppPath } from '@/types/AppPath'; -import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager'; -import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope'; -import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider'; -import { BlankLayout } from '@/ui/layout/page/BlankLayout'; -import { DefaultLayout } from '@/ui/layout/page/DefaultLayout'; -import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; -import { PageTitle } from '@/ui/utilities/page-title/PageTitle'; -import { UserProvider } from '@/users/components/UserProvider'; -import { UserProviderEffect } from '@/users/components/UserProviderEffect'; -import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; -import { CommandMenuEffect } from '~/effect-components/CommandMenuEffect'; -import { GotoHotkeysEffect } from '~/effect-components/GotoHotkeysEffect'; -import { PageChangeEffect } from '~/effect-components/PageChangeEffect'; -import { Authorize } from '~/pages/auth/Authorize'; -import { Invite } from '~/pages/auth/Invite'; -import { PasswordReset } from '~/pages/auth/PasswordReset'; -import { SignInUp } from '~/pages/auth/SignInUp'; -import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect'; -import { NotFound } from '~/pages/not-found/NotFound'; -import { RecordIndexPage } from '~/pages/object-record/RecordIndexPage'; -import { RecordShowPage } from '~/pages/object-record/RecordShowPage'; -import { ChooseYourPlan } from '~/pages/onboarding/ChooseYourPlan'; -import { CreateProfile } from '~/pages/onboarding/CreateProfile'; -import { CreateWorkspace } from '~/pages/onboarding/CreateWorkspace'; -import { InviteTeam } from '~/pages/onboarding/InviteTeam'; -import { PaymentSuccess } from '~/pages/onboarding/PaymentSuccess'; -import { SyncEmails } from '~/pages/onboarding/SyncEmails'; -import { SettingsRoutes } from '~/SettingsRoutes'; -import { getPageTitleFromPath } from '~/utils/title-utils'; - -const ProvidersThatNeedRouterContext = () => { - const { pathname } = useLocation(); - const pageTitle = getPageTitleFromPath(pathname); - - return ( - <> - <ApolloProvider> - <ClientConfigProviderEffect /> - <ClientConfigProvider> - <ChromeExtensionSidecarEffect /> - <ChromeExtensionSidecarProvider> - <UserProviderEffect /> - <UserProvider> - <AuthProvider> - <ApolloMetadataClientProvider> - <ObjectMetadataItemsProvider> - <PrefetchDataProvider> - <AppThemeProvider> - <SnackBarProvider> - <DialogManagerScope dialogManagerScopeId="dialog-manager"> - <DialogManager> - <StrictMode> - <PromiseRejectionEffect /> - <CommandMenuEffect /> - <GotoHotkeysEffect /> - <PageTitle title={pageTitle} /> - <Outlet /> - </StrictMode> - </DialogManager> - </DialogManagerScope> - </SnackBarProvider> - </AppThemeProvider> - </PrefetchDataProvider> - <PageChangeEffect /> - </ObjectMetadataItemsProvider> - </ApolloMetadataClientProvider> - </AuthProvider> - </UserProvider> - </ChromeExtensionSidecarProvider> - </ClientConfigProvider> - </ApolloProvider> - </> - ); -}; - -const createRouter = ( - isBillingEnabled?: boolean, - isCRMMigrationEnabled?: boolean, - isServerlessFunctionSettingsEnabled?: boolean, -) => - createBrowserRouter( - createRoutesFromElements( - <Route - element={<ProvidersThatNeedRouterContext />} - // To switch state to `loading` temporarily to enable us - // to set scroll position before the page is rendered - loader={async () => Promise.resolve(null)} - > - <Route element={<DefaultLayout />}> - <Route path={AppPath.Verify} element={<VerifyEffect />} /> - <Route path={AppPath.SignInUp} element={<SignInUp />} /> - <Route path={AppPath.Invite} element={<Invite />} /> - <Route path={AppPath.ResetPassword} element={<PasswordReset />} /> - <Route path={AppPath.CreateWorkspace} element={<CreateWorkspace />} /> - <Route path={AppPath.CreateProfile} element={<CreateProfile />} /> - <Route path={AppPath.SyncEmails} element={<SyncEmails />} /> - <Route path={AppPath.InviteTeam} element={<InviteTeam />} /> - <Route path={AppPath.PlanRequired} element={<ChooseYourPlan />} /> - <Route - path={AppPath.PlanRequiredSuccess} - element={<PaymentSuccess />} - /> - <Route path={indexAppPath.getIndexAppPath()} element={<></>} /> - <Route path={AppPath.Impersonate} element={<ImpersonateEffect />} /> - <Route path={AppPath.RecordIndexPage} element={<RecordIndexPage />} /> - <Route path={AppPath.RecordShowPage} element={<RecordShowPage />} /> - <Route - path={AppPath.SettingsCatchAll} - element={ - <SettingsRoutes - isBillingEnabled={isBillingEnabled} - isCRMMigrationEnabled={isCRMMigrationEnabled} - isServerlessFunctionSettingsEnabled={ - isServerlessFunctionSettingsEnabled - } - /> - } - /> - <Route path={AppPath.NotFoundWildcard} element={<NotFound />} /> - </Route> - <Route element={<BlankLayout />}> - <Route path={AppPath.Authorize} element={<Authorize />} /> - </Route> - </Route>, - ), - ); - -export const App = () => { - const billing = useRecoilValue(billingState); - const isFreeAccessEnabled = useIsFeatureEnabled('IS_FREE_ACCESS_ENABLED'); - const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED'); - const isServerlessFunctionSettingsEnabled = useIsFeatureEnabled( - 'IS_FUNCTION_SETTINGS_ENABLED', - ); - - const isBillingPageEnabled = - billing?.isBillingEnabled && !isFreeAccessEnabled; - - return ( - <RouterProvider - router={createRouter( - isBillingPageEnabled, - isCRMMigrationEnabled, - isServerlessFunctionSettingsEnabled, - )} - /> - ); -}; diff --git a/packages/twenty-front/src/__stories__/App.stories.tsx b/packages/twenty-front/src/__stories__/AppRouter.stories.tsx similarity index 92% rename from packages/twenty-front/src/__stories__/App.stories.tsx rename to packages/twenty-front/src/__stories__/AppRouter.stories.tsx index c5314b5652a1..9d2fe91a6523 100644 --- a/packages/twenty-front/src/__stories__/App.stories.tsx +++ b/packages/twenty-front/src/__stories__/AppRouter.stories.tsx @@ -1,8 +1,8 @@ -import { HelmetProvider } from 'react-helmet-async'; import { getOperationName } from '@apollo/client/utilities'; import { jest } from '@storybook/jest'; import { Meta, StoryObj } from '@storybook/react'; import { graphql, HttpResponse } from 'msw'; +import { HelmetProvider } from 'react-helmet-async'; import { RecoilRoot } from 'recoil'; import { IconsProvider } from 'twenty-ui'; @@ -11,13 +11,14 @@ import indexAppPath from '@/navigation/utils/indexAppPath'; import { AppPath } from '@/types/AppPath'; import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; -import { App } from '~/App'; + +import { AppRouter } from '@/app/components/AppRouter'; import { graphqlMocks } from '~/testing/graphqlMocks'; import { mockedUserData } from '~/testing/mock-data/users'; -const meta: Meta<typeof App> = { - title: 'App/App', - component: App, +const meta: Meta<typeof AppRouter> = { + title: 'App/AppRouter', + component: AppRouter, decorators: [ (Story) => { return ( @@ -41,7 +42,7 @@ const meta: Meta<typeof App> = { }; export default meta; -export type Story = StoryObj<typeof App>; +export type Story = StoryObj<typeof AppRouter>; export const Default: Story = { play: async () => { diff --git a/packages/twenty-front/src/effect-components/GotoHotkeysEffect.tsx b/packages/twenty-front/src/effect-components/GotoHotkeysEffect.tsx deleted file mode 100644 index 1109066af34a..000000000000 --- a/packages/twenty-front/src/effect-components/GotoHotkeysEffect.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import { useGoToHotkeys } from '@/ui/utilities/hotkey/hooks/useGoToHotkeys'; - -export const GotoHotkeysEffect = () => { - useGoToHotkeys('p', '/objects/people'); - useGoToHotkeys('c', '/objects/companies'); - useGoToHotkeys('o', '/objects/opportunities'); - useGoToHotkeys('s', '/settings/profile'); - useGoToHotkeys('t', '/objects/tasks'); - - return <></>; -}; diff --git a/packages/twenty-front/src/generated-metadata/gql.ts b/packages/twenty-front/src/generated-metadata/gql.ts index a01821654262..7a5bc0d07778 100644 --- a/packages/twenty-front/src/generated-metadata/gql.ts +++ b/packages/twenty-front/src/generated-metadata/gql.ts @@ -25,15 +25,15 @@ const documents = { "\n \n query GetManyRemoteTables($input: FindManyRemoteTablesInput!) {\n findDistantTablesWithStatus(input: $input) {\n ...RemoteTableFields\n }\n }\n": types.GetManyRemoteTablesDocument, "\n \n query GetOneDatabaseConnection($input: RemoteServerIdInput!) {\n findOneRemoteServerById(input: $input) {\n ...RemoteServerFields\n }\n }\n": types.GetOneDatabaseConnectionDocument, "\n mutation CreateOneObjectMetadataItem($input: CreateOneObjectInput!) {\n createOneObject(input: $input) {\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.CreateOneObjectMetadataItemDocument, - "\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n defaultValue\n options\n }\n }\n": types.CreateOneFieldMetadataItemDocument, + "\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\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 defaultValue\n options\n }\n }\n": types.CreateOneFieldMetadataItemDocument, "\n mutation CreateOneRelationMetadata($input: CreateOneRelationInput!) {\n createOneRelation(input: $input) {\n id\n relationType\n fromObjectMetadataId\n toObjectMetadataId\n fromFieldMetadataId\n toFieldMetadataId\n createdAt\n updatedAt\n }\n }\n": types.CreateOneRelationMetadataDocument, - "\n mutation UpdateOneFieldMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateFieldInput!\n ) {\n updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n }\n }\n": types.UpdateOneFieldMetadataItemDocument, + "\n mutation UpdateOneFieldMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateFieldInput!\n ) {\n updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {\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.UpdateOneFieldMetadataItemDocument, "\n mutation UpdateOneObjectMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateObjectPayload!\n ) {\n updateOneObject(input: { id: $idToUpdate, update: $updatePayload }) {\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.UpdateOneObjectMetadataItemDocument, "\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 }\n }\n": types.DeleteOneFieldMetadataItemDocument, + "\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 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 createdAt\n updatedAt\n defaultValue\n options\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 sourceCodeHash\n runtime\n syncStatus\n latestVersion\n createdAt\n updatedAt\n }\n": types.ServerlessFunctionFieldsFragmentDoc, + "\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 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 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 \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 mutation ExecuteOneServerlessFunction(\n $input: ExecuteServerlessFunctionInput!\n ) {\n executeOneServerlessFunction(input: $input) {\n data\n duration\n status\n error\n }\n }\n": types.ExecuteOneServerlessFunctionDocument, @@ -110,7 +110,7 @@ export function graphql(source: "\n mutation CreateOneObjectMetadataItem($input /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n defaultValue\n options\n }\n }\n"): (typeof documents)["\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n defaultValue\n options\n }\n }\n"]; +export function graphql(source: "\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\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 defaultValue\n options\n }\n }\n"): (typeof documents)["\n mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) {\n createOneField(input: $input) {\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 defaultValue\n options\n }\n }\n"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -118,7 +118,7 @@ export function graphql(source: "\n mutation CreateOneRelationMetadata($input: /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\n mutation UpdateOneFieldMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateFieldInput!\n ) {\n updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n }\n }\n"): (typeof documents)["\n mutation UpdateOneFieldMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateFieldInput!\n ) {\n updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {\n id\n type\n name\n label\n description\n icon\n isCustom\n isActive\n isNullable\n createdAt\n updatedAt\n }\n }\n"]; +export function graphql(source: "\n mutation UpdateOneFieldMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateFieldInput!\n ) {\n updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {\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"): (typeof documents)["\n mutation UpdateOneFieldMetadataItem(\n $idToUpdate: UUID!\n $updatePayload: UpdateFieldInput!\n ) {\n updateOneField(input: { id: $idToUpdate, update: $updatePayload }) {\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"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -130,7 +130,7 @@ export function graphql(source: "\n mutation DeleteOneObjectMetadataItem($idToD /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ -export function graphql(source: "\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 }\n }\n"): (typeof 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 }\n }\n"]; +export function graphql(source: "\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"): (typeof 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"]; /** * The graphql function is used to parse GraphQL queries into a document that can be used by GraphQL clients. */ @@ -138,11 +138,11 @@ 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 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 createdAt\n updatedAt\n defaultValue\n options\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 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 createdAt\n updatedAt\n defaultValue\n options\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 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 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 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 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. */ -export function graphql(source: "\n fragment ServerlessFunctionFields on ServerlessFunction {\n id\n name\n description\n sourceCodeHash\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 sourceCodeHash\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 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"]; /** * 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 09fac64ea57e..64d5248023f6 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -245,13 +245,7 @@ export type CreateRemoteServerInput = { userMappingOptions?: InputMaybe<UserMappingOptions>; }; -export type CreateServerlessFunctionFromFileInput = { - description?: InputMaybe<Scalars['String']['input']>; - name: Scalars['String']['input']; -}; - export type CreateServerlessFunctionInput = { - code: Scalars['String']['input']; description?: InputMaybe<Scalars['String']['input']>; name: Scalars['String']['input']; }; @@ -375,6 +369,7 @@ export enum FieldMetadataType { RichText = 'RICH_TEXT', Select = 'SELECT', Text = 'TEXT', + TsVector = 'TS_VECTOR', Uuid = 'UUID' } @@ -441,6 +436,7 @@ export type Mutation = { activateWorkflowVersion: Scalars['Boolean']['output']; activateWorkspace: Workspace; addUserToWorkspace: User; + addUserToWorkspaceByInviteToken: User; authorizeApp: AuthorizeApp; challenge: LoginToken; checkoutSession: SessionEntity; @@ -450,7 +446,6 @@ export type Mutation = { createOneRelation: Relation; createOneRemoteServer: RemoteServer; createOneServerlessFunction: ServerlessFunction; - createOneServerlessFunctionFromFile: ServerlessFunction; deactivateWorkflowVersion: Scalars['Boolean']['output']; deleteCurrentWorkspace: Workspace; deleteOneField: Field; @@ -459,6 +454,7 @@ export type Mutation = { deleteOneRemoteServer: RemoteServer; deleteOneServerlessFunction: ServerlessFunction; deleteUser: User; + deleteWorkspaceInvitation: Scalars['String']['output']; disablePostgresProxy: PostgresCredentials; emailPasswordResetLink: EmailPasswordResetLink; enablePostgresProxy: PostgresCredentials; @@ -470,8 +466,9 @@ export type Mutation = { impersonate: Verify; publishServerlessFunction: ServerlessFunction; renewToken: AuthTokens; + resendWorkspaceInvitation: SendInvitationsOutput; runWorkflowVersion: WorkflowRun; - sendInviteLink: SendInviteLink; + sendInvitations: SendInvitationsOutput; signUp: LoginToken; skipSyncEmailOnboardingStep: OnboardingStepSuccess; syncRemoteTable: RemoteTable; @@ -508,6 +505,11 @@ export type MutationAddUserToWorkspaceArgs = { }; +export type MutationAddUserToWorkspaceByInviteTokenArgs = { + inviteToken: Scalars['String']['input']; +}; + + export type MutationAuthorizeAppArgs = { clientId: Scalars['String']['input']; codeChallenge?: InputMaybe<Scalars['String']['input']>; @@ -558,12 +560,6 @@ export type MutationCreateOneServerlessFunctionArgs = { }; -export type MutationCreateOneServerlessFunctionFromFileArgs = { - file: Scalars['Upload']['input']; - input: CreateServerlessFunctionFromFileInput; -}; - - export type MutationDeactivateWorkflowVersionArgs = { workflowVersionId: Scalars['String']['input']; }; @@ -594,6 +590,11 @@ export type MutationDeleteOneServerlessFunctionArgs = { }; +export type MutationDeleteWorkspaceInvitationArgs = { + appTokenId: Scalars['String']['input']; +}; + + export type MutationEmailPasswordResetLinkArgs = { email: Scalars['String']['input']; }; @@ -637,12 +638,17 @@ export type MutationRenewTokenArgs = { }; +export type MutationResendWorkspaceInvitationArgs = { + appTokenId: Scalars['String']['input']; +}; + + export type MutationRunWorkflowVersionArgs = { input: RunWorkflowVersionInput; }; -export type MutationSendInviteLinkArgs = { +export type MutationSendInvitationsArgs = { emails: Array<Scalars['String']['input']>; }; @@ -652,6 +658,7 @@ export type MutationSignUpArgs = { email: Scalars['String']['input']; password: Scalars['String']['input']; workspaceInviteHash?: InputMaybe<Scalars['String']['input']>; + workspacePersonalInviteToken?: InputMaybe<Scalars['String']['input']>; }; @@ -666,8 +673,8 @@ export type MutationSyncRemoteTableSchemaChangesArgs = { export type MutationTrackArgs = { - data: Scalars['JSON']['input']; - type: Scalars['String']['input']; + action: Scalars['String']['input']; + payload: Scalars['JSON']['input']; }; @@ -818,11 +825,12 @@ export type Query = { findManyRemoteServersByType: Array<RemoteServer>; findOneRemoteServerById: RemoteServer; findWorkspaceFromInviteHash: Workspace; + findWorkspaceInvitations: Array<WorkspaceInvitation>; getAISQLQuery: AisqlQueryResult; getAvailablePackages: Scalars['JSON']['output']; getPostgresCredentials?: Maybe<PostgresCredentials>; getProductPrices: ProductPricesEntity; - getServerlessFunctionSourceCode?: Maybe<Scalars['String']['output']>; + getServerlessFunctionSourceCode?: Maybe<Scalars['JSON']['output']>; getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal; getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal; getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal; @@ -1047,8 +1055,10 @@ export type RunWorkflowVersionInput = { workflowVersionId: Scalars['String']['input']; }; -export type SendInviteLink = { - __typename?: 'SendInviteLink'; +export type SendInvitationsOutput = { + __typename?: 'SendInvitationsOutput'; + errors: Array<Scalars['String']['output']>; + result: Array<WorkspaceInvitation>; /** Boolean that confirms query was dispatched */ success: Scalars['Boolean']['output']; }; @@ -1068,7 +1078,6 @@ export type ServerlessFunction = { latestVersion?: Maybe<Scalars['String']['output']>; name: Scalars['String']['output']; runtime: Scalars['String']['output']; - sourceCodeHash: Scalars['String']['output']; syncStatus: ServerlessFunctionSyncStatus; updatedAt: Scalars['DateTime']['output']; }; @@ -1310,7 +1319,7 @@ export type UpdateRemoteServerInput = { }; export type UpdateServerlessFunctionInput = { - code: Scalars['String']['input']; + code: Scalars['JSON']['input']; description?: InputMaybe<Scalars['String']['input']>; /** Id of the serverless function to execute */ id: Scalars['UUID']['input']; @@ -1454,6 +1463,13 @@ export type WorkspaceEdge = { node: Workspace; }; +export type WorkspaceInvitation = { + __typename?: 'WorkspaceInvitation'; + email: Scalars['String']['output']; + expiresAt: Scalars['DateTime']['output']; + id: Scalars['UUID']['output']; +}; + export type WorkspaceInviteHashValid = { __typename?: 'WorkspaceInviteHashValid'; isValid: Scalars['Boolean']['output']; @@ -1672,7 +1688,7 @@ export type CreateOneFieldMetadataItemMutationVariables = Exact<{ }>; -export type CreateOneFieldMetadataItemMutation = { __typename?: 'Mutation', createOneField: { __typename?: 'field', id: any, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isNullable?: boolean | null, createdAt: any, updatedAt: any, defaultValue?: any | null, options?: any | null } }; +export type CreateOneFieldMetadataItemMutation = { __typename?: 'Mutation', createOneField: { __typename?: 'field', id: any, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isNullable?: boolean | null, createdAt: any, updatedAt: any, settings?: any | null, defaultValue?: any | null, options?: any | null } }; export type CreateOneRelationMetadataMutationVariables = Exact<{ input: CreateOneRelationInput; @@ -1687,7 +1703,7 @@ export type UpdateOneFieldMetadataItemMutationVariables = Exact<{ }>; -export type UpdateOneFieldMetadataItemMutation = { __typename?: 'Mutation', updateOneField: { __typename?: 'field', id: any, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isNullable?: boolean | null, createdAt: any, updatedAt: any } }; +export type UpdateOneFieldMetadataItemMutation = { __typename?: 'Mutation', updateOneField: { __typename?: 'field', id: any, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isNullable?: boolean | null, createdAt: any, updatedAt: any, settings?: any | null } }; export type UpdateOneObjectMetadataItemMutationVariables = Exact<{ idToUpdate: Scalars['UUID']['input']; @@ -1709,7 +1725,7 @@ export type DeleteOneFieldMetadataItemMutationVariables = Exact<{ }>; -export type DeleteOneFieldMetadataItemMutation = { __typename?: 'Mutation', deleteOneField: { __typename?: 'field', id: any, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isNullable?: boolean | null, createdAt: any, updatedAt: any } }; +export type DeleteOneFieldMetadataItemMutation = { __typename?: 'Mutation', deleteOneField: { __typename?: 'field', id: any, type: FieldMetadataType, name: string, label: string, description?: string | null, icon?: string | null, isCustom?: boolean | null, isActive?: boolean | null, isNullable?: boolean | null, createdAt: any, updatedAt: any, settings?: any | null } }; export type DeleteOneRelationMetadataItemMutationVariables = Exact<{ idToDelete: Scalars['UUID']['input']; @@ -1724,23 +1740,23 @@ 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, 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, createdAt: any, updatedAt: any, defaultValue?: any | null, options?: 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, 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, 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, sourceCodeHash: string, 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, 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, sourceCodeHash: string, 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, createdAt: any, updatedAt: any } }; export type DeleteOneServerlessFunctionMutationVariables = Exact<{ input: DeleteServerlessFunctionInput; }>; -export type DeleteOneServerlessFunctionMutation = { __typename?: 'Mutation', deleteOneServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, sourceCodeHash: string, 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, createdAt: any, updatedAt: any } }; export type ExecuteOneServerlessFunctionMutationVariables = Exact<{ input: ExecuteServerlessFunctionInput; @@ -1754,14 +1770,14 @@ export type PublishOneServerlessFunctionMutationVariables = Exact<{ }>; -export type PublishOneServerlessFunctionMutation = { __typename?: 'Mutation', publishServerlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, sourceCodeHash: string, 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, 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, sourceCodeHash: string, 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, createdAt: any, updatedAt: any } }; export type FindManyAvailablePackagesQueryVariables = Exact<{ [key: string]: never; }>; @@ -1771,25 +1787,25 @@ 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, sourceCodeHash: string, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, createdAt: any, updatedAt: any } }> } }; +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 GetOneServerlessFunctionQueryVariables = Exact<{ id: Scalars['UUID']['input']; }>; -export type GetOneServerlessFunctionQuery = { __typename?: 'Query', serverlessFunction: { __typename?: 'ServerlessFunction', id: any, name: string, description?: string | null, sourceCodeHash: string, runtime: string, syncStatus: ServerlessFunctionSyncStatus, latestVersion?: string | null, createdAt: any, updatedAt: any } }; +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 FindOneServerlessFunctionSourceCodeQueryVariables = Exact<{ input: GetServerlessFunctionSourceCodeInput; }>; -export type FindOneServerlessFunctionSourceCodeQuery = { __typename?: 'Query', getServerlessFunctionSourceCode?: string | null }; +export type FindOneServerlessFunctionSourceCodeQuery = { __typename?: 'Query', getServerlessFunctionSourceCode?: any | null }; 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<RemoteServerFieldsFragment, unknown>; 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<RemoteTableFieldsFragment, unknown>; -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":"sourceCodeHash"}},{"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<ServerlessFunctionFieldsFragment, unknown>; +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<ServerlessFunctionFieldsFragment, unknown>; 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<CreateServerMutation, CreateServerMutationVariables>; 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<DeleteServerMutation, DeleteServerMutationVariables>; 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<SyncRemoteTableMutation, SyncRemoteTableMutationVariables>; @@ -1800,20 +1816,20 @@ export const GetManyDatabaseConnectionsDocument = {"kind":"Document","definition export const GetManyRemoteTablesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetManyRemoteTables"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"FindManyRemoteTablesInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findDistantTablesWithStatus"},"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<GetManyRemoteTablesQuery, GetManyRemoteTablesQueryVariables>; export const GetOneDatabaseConnectionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOneDatabaseConnection"},"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":"findOneRemoteServerById"},"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<GetOneDatabaseConnectionQuery, GetOneDatabaseConnectionQueryVariables>; export const CreateOneObjectMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneObjectMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateOneObjectInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneObject"},"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"}},{"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<CreateOneObjectMetadataItemMutation, CreateOneObjectMetadataItemMutationVariables>; -export const CreateOneFieldMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneFieldMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateOneFieldMetadataInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneField"},"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"}},{"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":"defaultValue"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}}]}}]} as unknown as DocumentNode<CreateOneFieldMetadataItemMutation, CreateOneFieldMetadataItemMutationVariables>; +export const CreateOneFieldMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneFieldMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateOneFieldMetadataInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneField"},"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"}},{"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"}},{"kind":"Field","name":{"kind":"Name","value":"defaultValue"}},{"kind":"Field","name":{"kind":"Name","value":"options"}}]}}]}}]} as unknown as DocumentNode<CreateOneFieldMetadataItemMutation, CreateOneFieldMetadataItemMutationVariables>; export const CreateOneRelationMetadataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneRelationMetadata"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateOneRelationInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneRelation"},"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"}},{"kind":"Field","name":{"kind":"Name","value":"relationType"}},{"kind":"Field","name":{"kind":"Name","value":"fromObjectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"toObjectMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"fromFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"toFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<CreateOneRelationMetadataMutation, CreateOneRelationMetadataMutationVariables>; -export const UpdateOneFieldMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateOneFieldMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToUpdate"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"updatePayload"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateFieldInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOneField"},"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":"idToUpdate"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"update"},"value":{"kind":"Variable","name":{"kind":"Name","value":"updatePayload"}}}]}}],"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"}}]}}]}}]} as unknown as DocumentNode<UpdateOneFieldMetadataItemMutation, UpdateOneFieldMetadataItemMutationVariables>; +export const UpdateOneFieldMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateOneFieldMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToUpdate"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"updatePayload"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateFieldInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOneField"},"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":"idToUpdate"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"update"},"value":{"kind":"Variable","name":{"kind":"Name","value":"updatePayload"}}}]}}],"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<UpdateOneFieldMetadataItemMutation, UpdateOneFieldMetadataItemMutationVariables>; export const UpdateOneObjectMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateOneObjectMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToUpdate"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"updatePayload"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateObjectPayload"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOneObject"},"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":"idToUpdate"}}},{"kind":"ObjectField","name":{"kind":"Name","value":"update"},"value":{"kind":"Variable","name":{"kind":"Name","value":"updatePayload"}}}]}}],"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<UpdateOneObjectMetadataItemMutation, UpdateOneObjectMetadataItemMutationVariables>; 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<DeleteOneObjectMetadataItemMutation, DeleteOneObjectMetadataItemMutationVariables>; -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"}}]}}]}}]} as unknown as DocumentNode<DeleteOneFieldMetadataItemMutation, DeleteOneFieldMetadataItemMutationVariables>; +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<DeleteOneFieldMetadataItemMutation, DeleteOneFieldMetadataItemMutationVariables>; 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<DeleteOneRelationMetadataItemMutation, DeleteOneRelationMetadataItemMutationVariables>; -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":"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":"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":"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<ObjectMetadataItemsQuery, ObjectMetadataItemsQueryVariables>; -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":"sourceCodeHash"}},{"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<CreateOneServerlessFunctionItemMutation, CreateOneServerlessFunctionItemMutationVariables>; -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":"sourceCodeHash"}},{"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<DeleteOneServerlessFunctionMutation, DeleteOneServerlessFunctionMutationVariables>; +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":"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":"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<ObjectMetadataItemsQuery, ObjectMetadataItemsQueryVariables>; +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<CreateOneServerlessFunctionItemMutation, CreateOneServerlessFunctionItemMutationVariables>; +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<DeleteOneServerlessFunctionMutation, DeleteOneServerlessFunctionMutationVariables>; 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<ExecuteOneServerlessFunctionMutation, ExecuteOneServerlessFunctionMutationVariables>; -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":"sourceCodeHash"}},{"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<PublishOneServerlessFunctionMutation, PublishOneServerlessFunctionMutationVariables>; -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":"sourceCodeHash"}},{"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<UpdateOneServerlessFunctionMutation, UpdateOneServerlessFunctionMutationVariables>; +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<PublishOneServerlessFunctionMutation, PublishOneServerlessFunctionMutationVariables>; +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<UpdateOneServerlessFunctionMutation, UpdateOneServerlessFunctionMutationVariables>; 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<FindManyAvailablePackagesQuery, FindManyAvailablePackagesQueryVariables>; -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":"sourceCodeHash"}},{"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<GetManyServerlessFunctionsQuery, GetManyServerlessFunctionsQueryVariables>; -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":"sourceCodeHash"}},{"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<GetOneServerlessFunctionQuery, GetOneServerlessFunctionQueryVariables>; +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<GetManyServerlessFunctionsQuery, GetManyServerlessFunctionsQueryVariables>; +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<GetOneServerlessFunctionQuery, GetOneServerlessFunctionQueryVariables>; 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<FindOneServerlessFunctionSourceCodeQuery, FindOneServerlessFunctionSourceCodeQueryVariables>; \ 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 02d20ed6fd4b..a1d58b1d9dd3 100644 --- a/packages/twenty-front/src/generated/graphql.tsx +++ b/packages/twenty-front/src/generated/graphql.tsx @@ -280,6 +280,7 @@ export enum FieldMetadataType { RichText = 'RICH_TEXT', Select = 'SELECT', Text = 'TEXT', + TsVector = 'TS_VECTOR', Uuid = 'UUID' } diff --git a/packages/twenty-front/src/index.tsx b/packages/twenty-front/src/index.tsx index 06527d80050b..2a9ce791fc41 100644 --- a/packages/twenty-front/src/index.tsx +++ b/packages/twenty-front/src/index.tsx @@ -1,42 +1,13 @@ import ReactDOM from 'react-dom/client'; -import { HelmetProvider } from 'react-helmet-async'; -import { RecoilRoot } from 'recoil'; -import { IconsProvider } from 'twenty-ui'; - -import { CaptchaProvider } from '@/captcha/components/CaptchaProvider'; -import { ApolloDevLogEffect } from '@/debug/components/ApolloDevLogEffect'; -import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver'; -import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary'; -import { ExceptionHandlerProvider } from '@/error-handler/components/ExceptionHandlerProvider'; -import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import '@emotion/react'; -import { App } from './App'; - -import './index.css'; +import { App } from '@/app/components/App'; import 'react-loading-skeleton/dist/skeleton.css'; +import './index.css'; const root = ReactDOM.createRoot( document.getElementById('root') ?? document.body, ); -root.render( - <RecoilRoot> - <AppErrorBoundary> - <CaptchaProvider> - <RecoilDebugObserverEffect /> - <ApolloDevLogEffect /> - <SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager"> - <IconsProvider> - <ExceptionHandlerProvider> - <HelmetProvider> - <App /> - </HelmetProvider> - </ExceptionHandlerProvider> - </IconsProvider> - </SnackBarProviderScope> - </CaptchaProvider> - </AppErrorBoundary> - </RecoilRoot>, -); +root.render(<App />); diff --git a/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx b/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx index 38803c9efecf..c8de2f64c46a 100644 --- a/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx +++ b/packages/twenty-front/src/loading/components/LeftPanelSkeletonLoader.tsx @@ -3,6 +3,7 @@ import { motion } from 'framer-motion'; import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; import { ANIMATION, BACKGROUND_LIGHT, GRAY_SCALE } from 'twenty-ui'; +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { DESKTOP_NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/DesktopNavDrawerWidths'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { MainNavigationDrawerItemsSkeletonLoader } from '~/loading/components/MainNavigationDrawerItemsSkeletonLoader'; @@ -67,7 +68,10 @@ export const LeftPanelSkeletonLoader = () => { highlightColor={BACKGROUND_LIGHT.transparent.lighter} borderRadius={4} > - <Skeleton width={96} height={16} /> + <Skeleton + width={96} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} + /> </SkeletonTheme> </StyledSkeletonTitleContainer> <StyledSkeletonContainer> diff --git a/packages/twenty-front/src/loading/components/MainNavigationDrawerItemsSkeletonLoader.tsx b/packages/twenty-front/src/loading/components/MainNavigationDrawerItemsSkeletonLoader.tsx index 2d660dfda236..bfa360a98070 100644 --- a/packages/twenty-front/src/loading/components/MainNavigationDrawerItemsSkeletonLoader.tsx +++ b/packages/twenty-front/src/loading/components/MainNavigationDrawerItemsSkeletonLoader.tsx @@ -1,3 +1,4 @@ +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import styled from '@emotion/styled'; import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; import { BACKGROUND_LIGHT, GRAY_SCALE } from 'twenty-ui'; @@ -26,9 +27,18 @@ export const MainNavigationDrawerItemsSkeletonLoader = ({ highlightColor={BACKGROUND_LIGHT.transparent.lighter} borderRadius={4} > - {title && <Skeleton width={48} height={13} />} + {title && ( + <Skeleton + width={48} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.xs} + /> + )} {Array.from({ length }).map((_, index) => ( - <Skeleton key={index} width={196} height={16} /> + <Skeleton + key={index} + width={196} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} + /> ))} </SkeletonTheme> </StyledSkeletonContainer> diff --git a/packages/twenty-front/src/loading/components/RightPanelSkeletonLoader.tsx b/packages/twenty-front/src/loading/components/RightPanelSkeletonLoader.tsx index 1c47a6cdcb6c..9e98594369ab 100644 --- a/packages/twenty-front/src/loading/components/RightPanelSkeletonLoader.tsx +++ b/packages/twenty-front/src/loading/components/RightPanelSkeletonLoader.tsx @@ -1,3 +1,4 @@ +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import styled from '@emotion/styled'; import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; import { @@ -60,7 +61,10 @@ const StyledSkeletonHeaderLoader = () => { highlightColor={BACKGROUND_LIGHT.transparent.lighter} borderRadius={4} > - <Skeleton height={16} width={104} /> + <Skeleton + height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} + width={104} + /> </SkeletonTheme> </StyledHeaderContainer> ); @@ -73,7 +77,7 @@ const StyledSkeletonAddLoader = () => { highlightColor={BACKGROUND_LIGHT.transparent.lighter} borderRadius={4} > - <Skeleton width={132} height={16} /> + <Skeleton width={132} height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} /> </SkeletonTheme> ); }; diff --git a/packages/twenty-front/src/loading/components/__stories__/PrefetchLoading.stories.tsx b/packages/twenty-front/src/loading/components/__stories__/PrefetchLoading.stories.tsx index 847680fbae61..16c87e7c1732 100644 --- a/packages/twenty-front/src/loading/components/__stories__/PrefetchLoading.stories.tsx +++ b/packages/twenty-front/src/loading/components/__stories__/PrefetchLoading.stories.tsx @@ -38,6 +38,6 @@ export const Default: Story = { await canvas.findByText('Tasks'); await canvas.findByText('People'); await canvas.findByText('Opportunities'); - await canvas.findByText('My Customs'); + await canvas.findByText('Rockets'); }, }; diff --git a/packages/twenty-front/src/modules/accounts/constants/GmailSendScope.ts b/packages/twenty-front/src/modules/accounts/constants/GmailSendScope.ts new file mode 100644 index 000000000000..6918d126f2b3 --- /dev/null +++ b/packages/twenty-front/src/modules/accounts/constants/GmailSendScope.ts @@ -0,0 +1 @@ +export const GMAIL_SEND_SCOPE = 'https://www.googleapis.com/auth/gmail.send'; diff --git a/packages/twenty-front/src/modules/accounts/types/ConnectedAccount.ts b/packages/twenty-front/src/modules/accounts/types/ConnectedAccount.ts index d8f42da6d53c..f0ba4f489296 100644 --- a/packages/twenty-front/src/modules/accounts/types/ConnectedAccount.ts +++ b/packages/twenty-front/src/modules/accounts/types/ConnectedAccount.ts @@ -13,5 +13,6 @@ export type ConnectedAccount = { authFailedAt: Date | null; messageChannels: MessageChannel[]; calendarChannels: CalendarChannel[]; + scopes: string[] | null; __typename: 'ConnectedAccount'; }; diff --git a/packages/twenty-front/src/modules/activities/components/ActivityList.tsx b/packages/twenty-front/src/modules/activities/components/ActivityList.tsx new file mode 100644 index 000000000000..b8b8b2f61d5f --- /dev/null +++ b/packages/twenty-front/src/modules/activities/components/ActivityList.tsx @@ -0,0 +1,16 @@ +import { Card } from '@/ui/layout/card/components/Card'; +import styled from '@emotion/styled'; + +const StyledList = styled(Card)` + & > :not(:last-child) { + border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; + } + + width: calc(100% - 2px); + + overflow: auto; +`; + +export const ActivityList = ({ children }: React.PropsWithChildren) => { + return <StyledList>{children}</StyledList>; +}; diff --git a/packages/twenty-front/src/modules/activities/components/ActivityRow.tsx b/packages/twenty-front/src/modules/activities/components/ActivityRow.tsx new file mode 100644 index 000000000000..00fdbb1a68f8 --- /dev/null +++ b/packages/twenty-front/src/modules/activities/components/ActivityRow.tsx @@ -0,0 +1,35 @@ +import { CardContent } from '@/ui/layout/card/components/CardContent'; +import styled from '@emotion/styled'; +import React from 'react'; + +const StyledRowContent = styled(CardContent)<{ + clickable?: boolean; +}>` + align-items: center; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + height: ${({ theme }) => theme.spacing(12)}; + padding: ${({ theme }) => theme.spacing(0, 4)}; + cursor: ${({ clickable }) => (clickable === true ? 'pointer' : 'default')}; +`; + +export const ActivityRow = ({ + children, + onClick, + disabled, +}: React.PropsWithChildren<{ + onClick?: (event: React.MouseEvent<HTMLDivElement>) => void; + disabled?: boolean; +}>) => { + const handleClick = (event: React.MouseEvent<HTMLDivElement>) => { + if (disabled !== true) { + onClick?.(event); + } + }; + + return ( + <StyledRowContent onClick={handleClick} clickable={disabled !== true}> + {children} + </StyledRowContent> + ); +}; diff --git a/packages/twenty-front/src/modules/activities/components/SkeletonLoader.tsx b/packages/twenty-front/src/modules/activities/components/SkeletonLoader.tsx index 21478ed73c32..0b65ab1143c3 100644 --- a/packages/twenty-front/src/modules/activities/components/SkeletonLoader.tsx +++ b/packages/twenty-front/src/modules/activities/components/SkeletonLoader.tsx @@ -1,6 +1,6 @@ -import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; const StyledSkeletonContainer = styled.div` align-items: center; @@ -25,6 +25,21 @@ const StyledSkeletonSubSectionContent = styled.div` justify-content: center; `; +export const SKELETON_LOADER_HEIGHT_SIZES = { + standard: { + xs: 13, + s: 16, + m: 24, + l: 32, + xl: 40, + }, + columns: { + s: 84, + m: 120, + xxl: 542, + }, +}; + const SkeletonColumnLoader = ({ height }: { height: number }) => { const theme = useTheme(); return ( @@ -55,15 +70,35 @@ export const SkeletonLoader = ({ borderRadius={4} > <StyledSkeletonContainer> - <Skeleton width={440} height={16} /> + <Skeleton + width={440} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} + /> {withSubSections && skeletonItems.map(({ id }, index) => ( <StyledSkeletonSubSection key={id}> - <SkeletonColumnLoader height={index === 1 ? 120 : 84} /> + <SkeletonColumnLoader + height={ + index === 1 + ? SKELETON_LOADER_HEIGHT_SIZES.columns.m + : SKELETON_LOADER_HEIGHT_SIZES.columns.s + } + /> <StyledSkeletonSubSectionContent> - <Skeleton width={400} height={24} /> - <Skeleton width={400} height={24} /> - {index === 1 && <Skeleton width={400} height={24} />} + <Skeleton + width={400} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.m} + /> + <Skeleton + width={400} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.m} + /> + {index === 1 && ( + <Skeleton + width={400} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.m} + /> + )} </StyledSkeletonSubSectionContent> </StyledSkeletonSubSection> ))} diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadPreview.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadPreview.tsx index bb7f8c1407f4..cfa2aa672853 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadPreview.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadPreview.tsx @@ -1,30 +1,15 @@ import styled from '@emotion/styled'; -import { useRef } from 'react'; import { useRecoilCallback } from 'recoil'; import { Avatar, GRAY_SCALE } from 'twenty-ui'; +import { ActivityRow } from '@/activities/components/ActivityRow'; import { EmailThreadNotShared } from '@/activities/emails/components/EmailThreadNotShared'; import { useEmailThread } from '@/activities/emails/hooks/useEmailThread'; import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/states/lastViewableEmailThreadIdState'; -import { CardContent } from '@/ui/layout/card/components/CardContent'; import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { MessageChannelVisibility, TimelineThread } from '~/generated/graphql'; import { formatToHumanReadableDate } from '~/utils/date-utils'; -const StyledCardContent = styled(CardContent)<{ - visibility: MessageChannelVisibility; -}>` - align-items: center; - display: flex; - gap: ${({ theme }) => theme.spacing(2)}; - height: ${({ theme }) => theme.spacing(12)}; - padding: ${({ theme }) => theme.spacing(0, 4)}; - cursor: ${({ visibility }) => - visibility === MessageChannelVisibility.ShareEverything - ? 'pointer' - : 'default'}; -`; - const StyledHeading = styled.div<{ unread: boolean }>` display: flex; overflow: hidden; @@ -82,16 +67,10 @@ const StyledReceivedAt = styled.div` `; type EmailThreadPreviewProps = { - divider?: boolean; thread: TimelineThread; }; -export const EmailThreadPreview = ({ - divider, - thread, -}: EmailThreadPreviewProps) => { - const cardRef = useRef<HTMLDivElement>(null); - +export const EmailThreadPreview = ({ thread }: EmailThreadPreviewProps) => { const { openEmailThread } = useEmailThread(); const visibility = thread.visibility; @@ -143,12 +122,12 @@ export const EmailThreadPreview = ({ ], ); + const isDisabled = visibility !== MessageChannelVisibility.ShareEverything; + return ( - <StyledCardContent - ref={cardRef} + <ActivityRow onClick={(event) => handleThreadClick(event)} - divider={divider} - visibility={visibility} + disabled={isDisabled} > <StyledHeading unread={!thread.read}> <StyledParticipantsContainer> @@ -201,6 +180,6 @@ export const EmailThreadPreview = ({ <StyledReceivedAt> {formatToHumanReadableDate(thread.lastMessageReceivedAt)} </StyledReceivedAt> - </StyledCardContent> + </ActivityRow> ); }; 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 17f44524fea6..8a3eef7ea33f 100644 --- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx +++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx @@ -1,6 +1,7 @@ import styled from '@emotion/styled'; import { H1Title, H1TitleFontColor } from 'twenty-ui'; +import { ActivityList } from '@/activities/components/ActivityList'; import { CustomResolverFetchMoreLoader } from '@/activities/components/CustomResolverFetchMoreLoader'; import { SkeletonLoader } from '@/activities/components/SkeletonLoader'; import { EmailThreadPreview } from '@/activities/emails/components/EmailThreadPreview'; @@ -18,7 +19,6 @@ import { AnimatedPlaceholderEmptyTitle, EMPTY_PLACEHOLDER_TRANSITION_PROPS, } from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; -import { Card } from '@/ui/layout/card/components/Card'; import { Section } from '@/ui/layout/section/components/Section'; import { TimelineThread, TimelineThreadsWithTotal } from '~/generated/graphql'; @@ -106,15 +106,11 @@ export const EmailThreads = ({ fontColor={H1TitleFontColor.Primary} /> {!firstQueryLoading && ( - <Card> - {timelineThreads?.map((thread: TimelineThread, index: number) => ( - <EmailThreadPreview - key={index} - divider={index < timelineThreads.length - 1} - thread={thread} - /> + <ActivityList> + {timelineThreads?.map((thread: TimelineThread) => ( + <EmailThreadPreview key={thread.id} thread={thread} /> ))} - </Card> + </ActivityList> )} <CustomResolverFetchMoreLoader loading={isFetchingMore || firstQueryLoading} diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useRightDrawerEmailThread.test.tsx b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useRightDrawerEmailThread.test.tsx index f88c7b79a1b5..0af5cec8224a 100644 --- a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useRightDrawerEmailThread.test.tsx +++ b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useRightDrawerEmailThread.test.tsx @@ -1,56 +1,410 @@ -import { MockedProvider } from '@apollo/client/testing'; -import { renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; - -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; +import { renderHook, waitFor } from '@testing-library/react'; +import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; +import gql from 'graphql-tag'; +import { generateEmptyJestRecordNode } from '~/testing/jest/generateEmptyJestRecordNode'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { useRightDrawerEmailThread } from '../useRightDrawerEmailThread'; -jest.mock('@/object-record/hooks/useFindOneRecord', () => ({ - __esModule: true, - useFindOneRecord: jest.fn(), -})); +const mocks = [ + { + request: { + query: gql` + query FindOneMessageThread($objectRecordId: ID!) { + messageThread(filter: { id: { eq: $objectRecordId } }) { + __typename + id + } + } + `, + variables: { objectRecordId: '1' }, + }, + result: jest.fn(() => ({ + data: { + messageThread: { + id: '1', + __typename: 'MessageThread', + }, + }, + })), + }, + { + request: { + query: gql` + query FindManyMessages( + $filter: MessageFilterInput + $orderBy: [MessageOrderByInput] + $lastCursor: String + $limit: Int + ) { + messages( + filter: $filter + orderBy: $orderBy + first: $limit + after: $lastCursor + ) { + edges { + node { + __typename + createdAt + headerMessageId + id + messageParticipants { + edges { + node { + __typename + displayName + handle + id + person { + __typename + avatarUrl + city + companyId + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + emails { + primaryEmail + additionalEmails + } + id + intro + jobTitle + linkedinLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + name { + firstName + lastName + } + performanceRating + phones { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } + position + updatedAt + whatsapp { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } + workPreference + xLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + } + role + workspaceMember { + __typename + avatarUrl + colorScheme + createdAt + dateFormat + deletedAt + id + locale + name { + firstName + lastName + } + timeFormat + timeZone + updatedAt + userEmail + userId + } + } + } + } + messageThread { + __typename + id + } + receivedAt + subject + text + } + cursor + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + totalCount + } + } + `, + variables: { + filter: { messageThreadId: { eq: '1' } }, + orderBy: [{ receivedAt: 'AscNullsLast' }], + lastCursor: undefined, + limit: 10, + }, + }, + result: jest.fn(() => ({ + data: { + messages: { + edges: [ + { + node: generateEmptyJestRecordNode({ + objectNameSingular: 'message', + input: { + id: '1', + text: 'Message 1', + createdAt: '2024-10-03T10:20:10.145Z', + }, + }), + cursor: '1', + }, + { + node: generateEmptyJestRecordNode({ + objectNameSingular: 'message', + input: { + id: '2', + text: 'Message 2', + createdAt: '2024-10-03T10:20:10.145Z', + }, + }), + cursor: '2', + }, + ], + totalCount: 2, + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: '1', + endCursor: '2', + }, + }, + }, + })), + }, + { + request: { + query: gql` + query FindManyMessageParticipants( + $filter: MessageParticipantFilterInput + $orderBy: [MessageParticipantOrderByInput] + $lastCursor: String + $limit: Int + ) { + messageParticipants( + filter: $filter + orderBy: $orderBy + first: $limit + after: $lastCursor + ) { + edges { + node { + __typename + displayName + handle + id + messageId + person { + __typename + avatarUrl + city + companyId + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + emails { + primaryEmail + additionalEmails + } + id + intro + jobTitle + linkedinLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + name { + firstName + lastName + } + performanceRating + phones { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } + position + updatedAt + whatsapp { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } + workPreference + xLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + } + role + workspaceMember { + __typename + avatarUrl + colorScheme + createdAt + dateFormat + deletedAt + id + locale + name { + firstName + lastName + } + timeFormat + timeZone + updatedAt + userEmail + userId + } + } + cursor + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + totalCount + } + } + `, + variables: { + filter: { messageId: { in: ['1', '2'] }, role: { eq: 'from' } }, + orderBy: undefined, + lastCursor: undefined, + limit: undefined, + }, + }, + result: jest.fn(() => ({ + data: { + messageParticipants: { + edges: [ + { + node: generateEmptyJestRecordNode({ + objectNameSingular: 'messageParticipant', + input: { + id: 'messageParticipant-1', + role: 'from', + messageId: '1', + }, + }), + cursor: '1', + }, + { + node: generateEmptyJestRecordNode({ + objectNameSingular: 'messageParticipant', + input: { + id: 'messageParticipant-2', + role: 'from', + messageId: '2', + }, + }), + cursor: '2', + }, + ], + totalCount: 2, + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: '1', + endCursor: '2', + }, + }, + }, + })), + }, +]; -jest.mock('@/object-record/hooks/useFindManyRecords', () => ({ - __esModule: true, - useFindManyRecords: jest.fn(), -})); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, + onInitializeRecoilSnapshot: ({ set }) => { + set(viewableRecordIdState, '1'); + }, +}); describe('useRightDrawerEmailThread', () => { it('should return correct values', async () => { - const mockThread = { id: '1' }; - const mockMessages = [ - { id: '1', text: 'Message 1' }, - { id: '2', text: 'Message 2' }, + { + __typename: 'Message', + createdAt: '2024-10-03T10:20:10.145Z', + headerMessageId: '', + id: '1', + messageParticipants: [], + messageThread: null, + receivedAt: null, + sender: { + __typename: 'MessageParticipant', + displayName: '', + handle: '', + id: 'messageParticipant-1', + messageId: '1', + person: null, + role: 'from', + workspaceMember: null, + }, + subject: '', + text: 'Message 1', + }, + { + __typename: 'Message', + createdAt: '2024-10-03T10:20:10.145Z', + headerMessageId: '', + id: '2', + messageParticipants: [], + messageThread: null, + receivedAt: null, + sender: { + __typename: 'MessageParticipant', + displayName: '', + handle: '', + id: 'messageParticipant-2', + messageId: '2', + person: null, + role: 'from', + workspaceMember: null, + }, + subject: '', + text: 'Message 2', + }, ]; - const mockFetchMoreRecords = jest.fn(); - - (useFindOneRecord as jest.Mock).mockReturnValue({ - record: mockThread, - loading: false, - fetchMoreRecords: mockFetchMoreRecords, - }); - - (useFindManyRecords as jest.Mock).mockReturnValue({ - records: mockMessages, - loading: false, - fetchMoreRecords: mockFetchMoreRecords, - }); - const { result } = renderHook(() => useRightDrawerEmailThread(), { - wrapper: ({ children }) => ( - <MockedProvider mocks={[]} addTypename={false}> - <RecoilRoot>{children}</RecoilRoot> - </MockedProvider> - ), + wrapper: Wrapper, }); - expect(result.current.thread).toBeDefined(); - expect(result.current.messages).toEqual(mockMessages); - expect(result.current.threadLoading).toBeFalsy(); - expect(result.current.fetchMoreMessages).toBeInstanceOf(Function); + await waitFor(() => { + expect(result.current.thread).toBeDefined(); + expect(result.current.messages).toEqual(mockMessages); + expect(result.current.threadLoading).toBeFalsy(); + expect(result.current.fetchMoreMessages).toBeInstanceOf(Function); + }); }); }); diff --git a/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx b/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx index e7706f3026a0..903924fa93af 100644 --- a/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/AttachmentList.tsx @@ -6,6 +6,7 @@ import { useUploadAttachmentFile } from '@/activities/files/hooks/useUploadAttac import { Attachment } from '@/activities/files/types/Attachment'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { ActivityList } from '@/activities/components/ActivityList'; import { AttachmentRow } from './AttachmentRow'; type AttachmentListProps = { @@ -22,6 +23,9 @@ const StyledContainer = styled.div` flex-direction: column; justify-content: center; padding: ${({ theme }) => theme.spacing(2, 6, 6)}; + + width: calc(100% - ${({ theme }) => theme.spacing(12)}); + height: 100%; `; @@ -44,21 +48,11 @@ const StyledCount = styled.span` margin-left: ${({ theme }) => theme.spacing(2)}; `; -const StyledAttachmentContainer = styled.div` - align-items: flex-start; - align-self: stretch; - background: ${({ theme }) => theme.background.secondary}; - border: 1px solid ${({ theme }) => theme.border.color.medium}; - border-radius: ${({ theme }) => theme.border.radius.md}; - display: flex; - flex-flow: column nowrap; - justify-content: center; - width: 100%; -`; - const StyledDropZoneContainer = styled.div` height: 100%; width: 100%; + + overflow: auto; `; export const AttachmentList = ({ @@ -91,11 +85,11 @@ export const AttachmentList = ({ onUploadFile={onUploadFile} /> ) : ( - <StyledAttachmentContainer> + <ActivityList> {attachments.map((attachment) => ( <AttachmentRow key={attachment.id} attachment={attachment} /> ))} - </StyledAttachmentContainer> + </ActivityList> )} </StyledDropZoneContainer> </StyledContainer> diff --git a/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx b/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx index e642e460092d..09f7f1ea4a09 100644 --- a/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx +++ b/packages/twenty-front/src/modules/activities/files/components/AttachmentRow.tsx @@ -1,3 +1,4 @@ +import { ActivityRow } from '@/activities/components/ActivityRow'; import { AttachmentDropdown } from '@/activities/files/components/AttachmentDropdown'; import { AttachmentIcon } from '@/activities/files/components/AttachmentIcon'; import { Attachment } from '@/activities/files/types/Attachment'; @@ -13,26 +14,19 @@ import { TextInput } from '@/ui/input/components/TextInput'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { useMemo, useState } from 'react'; -import { IconCalendar } from 'twenty-ui'; +import { IconCalendar, OverflowingTextWithTooltip } from 'twenty-ui'; import { formatToHumanReadableDate } from '~/utils/date-utils'; import { getFileAbsoluteURI } from '~/utils/file/getFileAbsoluteURI'; -const StyledRow = styled.div` - align-items: center; - align-self: stretch; - border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; - color: ${({ theme }) => theme.font.color.primary}; - display: flex; - justify-content: space-between; - padding: ${({ theme }) => theme.spacing(2)}; - height: 32px; -`; - const StyledLeftContent = styled.div` align-items: center; display: flex; gap: ${({ theme }) => theme.spacing(3)}; + + width: 100%; + overflow: auto; + flex: 1; `; const StyledRightContent = styled.div` @@ -52,11 +46,19 @@ const StyledLink = styled.a` color: ${({ theme }) => theme.font.color.primary}; display: flex; text-decoration: none; + + width: 100%; + :hover { color: ${({ theme }) => theme.font.color.secondary}; } `; +const StyledLinkContainer = styled.div` + overflow: auto; + width: 100%; +`; + export const AttachmentRow = ({ attachment }: { attachment: Attachment }) => { const theme = useTheme(); const [isEditing, setIsEditing] = useState(false); @@ -117,7 +119,7 @@ export const AttachmentRow = ({ attachment }: { attachment: Attachment }) => { return ( <FieldContext.Provider value={fieldContext as GenericFieldContextType}> - <StyledRow> + <ActivityRow disabled> <StyledLeftContent> <AttachmentIcon attachmentType={attachment.type} /> {isEditing ? ( @@ -129,12 +131,14 @@ export const AttachmentRow = ({ attachment }: { attachment: Attachment }) => { onKeyDown={handleOnKeyDown} /> ) : ( - <StyledLink - href={getFileAbsoluteURI(attachment.fullPath)} - target="__blank" - > - {attachment.name} - </StyledLink> + <StyledLinkContainer> + <StyledLink + href={getFileAbsoluteURI(attachment.fullPath)} + target="__blank" + > + <OverflowingTextWithTooltip text={attachment.name} /> + </StyledLink> + </StyledLinkContainer> )} </StyledLeftContent> <StyledRightContent> @@ -154,7 +158,7 @@ export const AttachmentRow = ({ attachment }: { attachment: Attachment }) => { onRename={handleRename} /> </StyledRightContent> - </StyledRow> + </ActivityRow> </FieldContext.Provider> ); }; diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx index 260e931a3ae2..7d1426f4cb75 100644 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx +++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx @@ -9,7 +9,7 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members'; const cache = new InMemoryCache(); diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useCreateActivityInDB.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useCreateActivityInDB.test.tsx index 1b56bc49d5da..baddb1029bda 100644 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useCreateActivityInDB.test.tsx +++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useCreateActivityInDB.test.tsx @@ -1,13 +1,11 @@ -import { MockedProvider, MockedResponse } from '@apollo/client/testing'; +import { MockedResponse } from '@apollo/client/testing'; import { act, renderHook } from '@testing-library/react'; import gql from 'graphql-tag'; import pick from 'lodash.pick'; -import { ReactNode } from 'react'; -import { RecoilRoot } from 'recoil'; import { useCreateActivityInDB } from '@/activities/hooks/useCreateActivityInDB'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { mockedTasks } from '~/testing/mock-data/tasks'; const mockedDate = '2024-03-15T12:00:00.000Z'; @@ -26,14 +24,44 @@ const mocks: MockedResponse[] = [ mutation CreateOneTask($input: TaskCreateInput!) { createTask(data: $input) { __typename - updatedAt + assignee { + __typename + id + name { + firstName + lastName + } + } + assigneeId + attachments { + edges { + node { + __typename + activityId + authorId + companyId + createdAt + deletedAt + fullPath + id + name + noteId + opportunityId + personId + rocketId + taskId + type + updatedAt + } + } + } + body createdAt dueAt id status - body - assigneeId title + updatedAt } } `, @@ -56,15 +84,9 @@ const mocks: MockedResponse[] = [ }, ]; -const Wrapper = ({ children }: { children: ReactNode }) => ( - <RecoilRoot> - <MockedProvider mocks={mocks} addTypename={false}> - <SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager"> - {children} - </SnackBarProviderScope> - </MockedProvider> - </RecoilRoot> -); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); describe('useCreateActivityInDB', () => { it('Should create activity in DB', async () => { diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawer.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawer.test.tsx index 665702704d1b..855c0b55bd29 100644 --- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawer.test.tsx +++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawer.test.tsx @@ -1,7 +1,6 @@ -import { MockedProvider, MockedResponse } from '@apollo/client/testing'; +import { MockedResponse } from '@apollo/client/testing'; import { act, renderHook } from '@testing-library/react'; -import { ReactNode } from 'react'; -import { RecoilRoot, useRecoilValue, useSetRecoilState } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { useOpenCreateActivityDrawer } from '@/activities/hooks/useOpenCreateActivityDrawer'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; @@ -9,7 +8,8 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import gql from 'graphql-tag'; import pick from 'lodash.pick'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { mockedTasks } from '~/testing/mock-data/tasks'; const mockedDate = '2024-03-15T12:00:00.000Z'; @@ -61,13 +61,9 @@ const mocks: MockedResponse[] = [ }, ]; -const Wrapper = ({ children }: { children: ReactNode }) => ( - <RecoilRoot> - <MockedProvider addTypename={false} mocks={mocks}> - {children} - </MockedProvider> - </RecoilRoot> -); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); const mockObjectMetadataItems = generatedMockObjectMetadataItems; diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts index 400c1f398a95..1cc4af08a81e 100644 --- a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts +++ b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts @@ -15,6 +15,7 @@ import { Task } from '@/activities/types/Task'; import { TaskTarget } from '@/activities/types/TaskTarget'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; +import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading'; import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; import { ActivityTargetableObject } from '../types/ActivityTargetableEntity'; @@ -52,7 +53,9 @@ export const useOpenCreateActivityDrawer = ({ const setViewableRecordNameSingular = useSetRecoilState( viewableRecordNameSingularState, ); - + const setIsNewViewableRecordLoading = useSetRecoilState( + isNewViewableRecordLoadingState, + ); const setIsUpsertingActivityInDB = useSetRecoilState( isUpsertingActivityInDBState, ); @@ -64,6 +67,11 @@ export const useOpenCreateActivityDrawer = ({ targetableObjects: ActivityTargetableObject[]; customAssignee?: WorkspaceMember; }) => { + setIsNewViewableRecordLoading(true); + openRightDrawer(RightDrawerPages.ViewRecord); + setViewableRecordId(null); + setViewableRecordNameSingular(activityObjectNameSingular); + const activity = await createOneActivity({ assigneeId: customAssignee?.id, }); @@ -101,10 +109,9 @@ export const useOpenCreateActivityDrawer = ({ setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false }); setViewableRecordId(activity.id); - setViewableRecordNameSingular(activityObjectNameSingular); - openRightDrawer(RightDrawerPages.ViewRecord); setIsUpsertingActivityInDB(false); + setIsNewViewableRecordLoading(false); }; return openCreateActivityDrawer; diff --git a/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskGroups.stories.tsx b/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskGroups.stories.tsx index 444049cc1844..aefa6f2ed59a 100644 --- a/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskGroups.stories.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskGroups.stories.tsx @@ -42,5 +42,8 @@ export const WithTasks: Story = { }, parameters: { msw: graphqlMocks, + container: { + width: '500px', + }, }, }; diff --git a/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskList.stories.tsx b/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskList.stories.tsx index 1113febb6223..c2a65b772be9 100644 --- a/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskList.stories.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/__stories__/TaskList.stories.tsx @@ -3,6 +3,7 @@ import { ComponentDecorator } from 'twenty-ui'; import { TaskList } from '@/activities/tasks/components/TaskList'; import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; +import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { graphqlMocks } from '~/testing/graphqlMocks'; import { mockedTasks } from '~/testing/mock-data/tasks'; @@ -10,13 +11,21 @@ import { mockedTasks } from '~/testing/mock-data/tasks'; const meta: Meta<typeof TaskList> = { title: 'Modules/Activity/TaskList', component: TaskList, - decorators: [MemoryRouterDecorator, ComponentDecorator, SnackBarDecorator], + decorators: [ + ComponentDecorator, + MemoryRouterDecorator, + ObjectMetadataItemsDecorator, + SnackBarDecorator, + ], args: { title: 'Tasks', tasks: mockedTasks, }, parameters: { msw: graphqlMocks, + container: { + width: '500px', + }, }, }; diff --git a/packages/twenty-front/src/modules/activities/tasks/components/ObjectTasks.tsx b/packages/twenty-front/src/modules/activities/tasks/components/ObjectTasks.tsx index a798a818d865..9e2dd7a4bc54 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/ObjectTasks.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/ObjectTasks.tsx @@ -20,7 +20,7 @@ export const ObjectTasks = ({ return ( <StyledContainer> <ObjectFilterDropdownScope filterScopeId="entity-tasks-filter-scope"> - <TaskGroups targetableObjects={[targetableObject]} showAddButton /> + <TaskGroups targetableObjects={[targetableObject]} /> </ObjectFilterDropdownScope> </StyledContainer> ); 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 a6e4999773bc..16ebbec0f38a 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx @@ -27,18 +27,15 @@ import { TaskList } from './TaskList'; const StyledContainer = styled.div` display: flex; flex-direction: column; + width: 100%; `; type TaskGroupsProps = { filterDropdownId?: string; targetableObjects?: ActivityTargetableObject[]; - showAddButton?: boolean; }; -export const TaskGroups = ({ - targetableObjects, - showAddButton, -}: TaskGroupsProps) => { +export const TaskGroups = ({ targetableObjects }: TaskGroupsProps) => { const { tasks, tasksLoading } = useTasks({ targetableObjects: targetableObjects ?? [], }); @@ -93,7 +90,11 @@ export const TaskGroups = ({ const sortedTasksByStatus = Object.entries( groupBy(tasks, ({ status }) => status), - ).toSorted(([statusA], [statusB]) => statusB.localeCompare(statusA)); + ).sort(([statusA], [statusB]) => statusB.localeCompare(statusA)); + + const hasTodoStatus = sortedTasksByStatus.some( + ([status]) => status === 'TODO', + ); return ( <StyledContainer> @@ -103,7 +104,7 @@ export const TaskGroups = ({ title={status} tasks={tasksByStatus} button={ - showAddButton && ( + (status === 'TODO' || !hasTodoStatus) && ( <AddTaskButton activityTargetableObjects={targetableObjects} /> ) } diff --git a/packages/twenty-front/src/modules/activities/tasks/components/TaskList.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TaskList.tsx index 04e56c8d65ed..56082db0dc4c 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/TaskList.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskList.tsx @@ -1,6 +1,7 @@ import styled from '@emotion/styled'; import { ReactElement } from 'react'; +import { ActivityList } from '@/activities/components/ActivityList'; import { Task } from '@/activities/types/Task'; import { TaskRow } from './TaskRow'; @@ -12,11 +13,14 @@ type TaskListProps = { const StyledContainer = styled.div` align-items: flex-start; + width: 100%; align-self: stretch; display: flex; flex-direction: column; justify-content: center; - padding: 8px 24px; + padding: 8px ${({ theme }) => theme.spacing(6)}; + + width: calc(100% - ${({ theme }) => theme.spacing(12)}); `; const StyledTitleBar = styled.div` @@ -38,13 +42,6 @@ const StyledCount = styled.span` margin-left: ${({ theme }) => theme.spacing(2)}; `; -const StyledTaskRows = styled.div` - background-color: ${({ theme }) => theme.background.secondary}; - border: 1px solid ${({ theme }) => theme.border.color.light}; - border-radius: ${({ theme }) => theme.border.radius.md}; - width: 100%; -`; - export const TaskList = ({ title, tasks, button }: TaskListProps) => ( <> {tasks && tasks.length > 0 && ( @@ -57,11 +54,11 @@ export const TaskList = ({ title, tasks, button }: TaskListProps) => ( )} {button} </StyledTitleBar> - <StyledTaskRows> + <ActivityList> {tasks.map((task) => ( <TaskRow key={task.id} task={task} /> ))} - </StyledTaskRows> + </ActivityList> </StyledContainer> )} </> diff --git a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx index efd4323143b0..ad46a8a43b15 100644 --- a/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskRow.tsx @@ -8,28 +8,12 @@ import { getActivitySummary } from '@/activities/utils/getActivitySummary'; import { Checkbox, CheckboxShape } from '@/ui/input/components/Checkbox'; import { beautifyExactDate, hasDatePassed } from '~/utils/date-utils'; +import { ActivityRow } from '@/activities/components/ActivityRow'; import { Task } from '@/activities/types/Task'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFieldContext } from '@/object-record/hooks/useFieldContext'; import { useCompleteTask } from '../hooks/useCompleteTask'; -const StyledContainer = styled.div` - align-items: center; - justify-content: space-between; - border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; - cursor: pointer; - display: flex; - height: ${({ theme }) => theme.spacing(12)}; - min-width: calc(100% - ${({ theme }) => theme.spacing(8)}); - max-width: calc(100% - ${({ theme }) => theme.spacing(8)}); - padding: 0 ${({ theme }) => theme.spacing(4)}; - overflow: hidden; - max-inline-size: 60px; - &:last-child { - border-bottom: 0; - } -`; - const StyledTaskBody = styled.div` color: ${({ theme }) => theme.font.color.tertiary}; display: flex; @@ -105,7 +89,7 @@ export const TaskRow = ({ task }: { task: Task }) => { }); return ( - <StyledContainer + <ActivityRow onClick={() => { openActivityRightDrawer(task.id); }} @@ -130,6 +114,14 @@ export const TaskRow = ({ task }: { task: Task }) => { </StyledTaskBody> </StyledLeftSideContainer> <StyledRightSideContainer> + {task.dueAt && ( + <StyledDueDate + isPast={hasDatePassed(task.dueAt) && task.status === 'TODO'} + > + <IconCalendar size={theme.icon.size.md} /> + {beautifyExactDate(task.dueAt)} + </StyledDueDate> + )} {TaskTargetsContextProvider && ( <TaskTargetsContextProvider> <ActivityTargetsInlineCell @@ -141,15 +133,7 @@ export const TaskRow = ({ task }: { task: Task }) => { /> </TaskTargetsContextProvider> )} - <StyledDueDate - isPast={ - !!task.dueAt && hasDatePassed(task.dueAt) && task.status === 'TODO' - } - > - <IconCalendar size={theme.icon.size.md} /> - {task.dueAt && beautifyExactDate(task.dueAt)} - </StyledDueDate> </StyledRightSideContainer> - </StyledContainer> + </ActivityRow> ); }; diff --git a/packages/twenty-front/src/modules/activities/tasks/hooks/__tests__/useCompleteTask.test.tsx b/packages/twenty-front/src/modules/activities/tasks/hooks/__tests__/useCompleteTask.test.tsx index ba3e2ac53482..814b72fb6ce1 100644 --- a/packages/twenty-front/src/modules/activities/tasks/hooks/__tests__/useCompleteTask.test.tsx +++ b/packages/twenty-front/src/modules/activities/tasks/hooks/__tests__/useCompleteTask.test.tsx @@ -1,11 +1,10 @@ -import { MockedProvider, MockedResponse } from '@apollo/client/testing'; +import { MockedResponse } from '@apollo/client/testing'; import { act, renderHook } from '@testing-library/react'; import gql from 'graphql-tag'; -import { ReactNode } from 'react'; -import { RecoilRoot } from 'recoil'; import { useCompleteTask } from '@/activities/tasks/hooks/useCompleteTask'; import { Task } from '@/activities/types/Task'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const task: Task = { id: '123', @@ -28,21 +27,123 @@ const mocks: MockedResponse[] = [ mutation UpdateOneTask($idToUpdate: ID!, $input: TaskUpdateInput!) { updateTask(id: $idToUpdate, data: $input) { __typename - updatedAt - createdAt - deletedAt - dueAt - id - status + assignee { + __typename + avatarUrl + colorScheme + createdAt + dateFormat + deletedAt + id + locale + name { + firstName + lastName + } + timeFormat + timeZone + updatedAt + userEmail + userId + } + assigneeId + attachments { + edges { + node { + __typename + activityId + authorId + companyId + createdAt + deletedAt + fullPath + id + name + noteId + opportunityId + personId + rocketId + taskId + type + updatedAt + } + } + } body + createdAt createdBy { source workspaceMemberId name } - assigneeId + deletedAt + dueAt + favorites { + edges { + node { + __typename + companyId + createdAt + deletedAt + id + noteId + opportunityId + personId + position + rocketId + taskId + updatedAt + viewId + workflowId + workspaceMemberId + } + } + } + id position + status + taskTargets { + edges { + node { + __typename + companyId + createdAt + deletedAt + id + opportunityId + personId + rocketId + taskId + updatedAt + } + } + } + timelineActivities { + edges { + node { + __typename + companyId + createdAt + deletedAt + happensAt + id + linkedObjectMetadataId + linkedRecordCachedName + linkedRecordId + name + noteId + opportunityId + personId + properties + rocketId + taskId + updatedAt + workspaceMemberId + } + } + } title + updatedAt } } `, @@ -72,13 +173,9 @@ const mocks: MockedResponse[] = [ }, ]; -const Wrapper = ({ children }: { children: ReactNode }) => ( - <RecoilRoot> - <MockedProvider mocks={mocks} addTypename={false}> - {children} - </MockedProvider> - </RecoilRoot> -); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); describe('useCompleteTask', () => { it('should complete task', async () => { diff --git a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/__tests__/useTimelineActivities.test.tsx b/packages/twenty-front/src/modules/activities/timelineActivities/hooks/__tests__/useTimelineActivities.test.tsx index 894d541c888b..2d1989cc68ea 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/hooks/__tests__/useTimelineActivities.test.tsx +++ b/packages/twenty-front/src/modules/activities/timelineActivities/hooks/__tests__/useTimelineActivities.test.tsx @@ -1,21 +1,16 @@ import { renderHook } from '@testing-library/react'; import { useTimelineActivities } from '@/activities/timelineActivities/hooks/useTimelineActivities'; -import { ReactNode } from 'react'; -import { getJestHookWrapper } from '~/testing/jest/getJestHookWrapper'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; jest.mock('@/object-record/hooks/useFindManyRecords', () => ({ useFindManyRecords: jest.fn(), })); -const Wrappers = getJestHookWrapper({ +const Wrapper = getJestMetadataAndApolloMocksWrapper({ apolloMocks: [], }); -const Wrapper = ({ children }: { children: ReactNode }) => ( - <Wrappers>{children}</Wrappers> -); - describe('useTimelineActivities', () => { afterEach(() => { jest.clearAllMocks(); 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/timelineActivities/rows/main-object/components/__stories__/EventRowMainObjectUpdated.stories.tsx index 0cb945bdc4b2..fe0b549d68da 100644 --- a/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/__stories__/EventRowMainObjectUpdated.stories.tsx +++ b/packages/twenty-front/src/modules/activities/timelineActivities/rows/main-object/components/__stories__/EventRowMainObjectUpdated.stories.tsx @@ -5,7 +5,7 @@ import { EventRowMainObjectUpdated } from '@/activities/timelineActivities/rows/ import { TimelineActivity } from '@/activities/timelineActivities/types/TimelineActivity'; import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; -import { mockedPersonObjectMetadataItem } from '~/testing/mock-data/metadata'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; const meta: Meta<typeof EventRowMainObjectUpdated> = { title: 'Modules/TimelineActivities/Rows/MainObject/EventRowMainObjectUpdated', @@ -35,7 +35,9 @@ const meta: Meta<typeof EventRowMainObjectUpdated> = { }, }, } as TimelineActivity, - mainObjectMetadataItem: mockedPersonObjectMetadataItem, + mainObjectMetadataItem: generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', + ), }, decorators: [ ComponentDecorator, diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts index 1a3a1aa8903b..0aa4c51e9d2c 100644 --- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts +++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect.ts @@ -93,6 +93,11 @@ export const triggerDeleteRecordsOptimisticEffect = ({ objectMetadataItems, }); - cache.evict({ id: cache.identify(recordToDelete) }); + cache.modify({ + id: cache.identify(recordToDelete), + fields: { + deletedAt: () => recordToDelete.deletedAt, + }, + }); }); }; diff --git a/packages/twenty-front/src/modules/app/components/App.tsx b/packages/twenty-front/src/modules/app/components/App.tsx new file mode 100644 index 000000000000..f760ee9f6fb4 --- /dev/null +++ b/packages/twenty-front/src/modules/app/components/App.tsx @@ -0,0 +1,32 @@ +import { AppRouter } from '@/app/components/AppRouter'; +import { CaptchaProvider } from '@/captcha/components/CaptchaProvider'; +import { ApolloDevLogEffect } from '@/debug/components/ApolloDevLogEffect'; +import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver'; +import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary'; +import { ExceptionHandlerProvider } from '@/error-handler/components/ExceptionHandlerProvider'; +import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; +import { HelmetProvider } from 'react-helmet-async'; +import { RecoilRoot } from 'recoil'; +import { IconsProvider } from 'twenty-ui'; + +export const App = () => { + return ( + <RecoilRoot> + <AppErrorBoundary> + <CaptchaProvider> + <RecoilDebugObserverEffect /> + <ApolloDevLogEffect /> + <SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager"> + <IconsProvider> + <ExceptionHandlerProvider> + <HelmetProvider> + <AppRouter /> + </HelmetProvider> + </ExceptionHandlerProvider> + </IconsProvider> + </SnackBarProviderScope> + </CaptchaProvider> + </AppErrorBoundary> + </RecoilRoot> + ); +}; diff --git a/packages/twenty-front/src/modules/app/components/AppRouter.tsx b/packages/twenty-front/src/modules/app/components/AppRouter.tsx new file mode 100644 index 000000000000..d8985e676332 --- /dev/null +++ b/packages/twenty-front/src/modules/app/components/AppRouter.tsx @@ -0,0 +1,27 @@ +import { createAppRouter } from '@/app/utils/createAppRouter'; +import { billingState } from '@/client-config/states/billingState'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import { RouterProvider } from 'react-router-dom'; +import { useRecoilValue } from 'recoil'; + +export const AppRouter = () => { + const billing = useRecoilValue(billingState); + const isFreeAccessEnabled = useIsFeatureEnabled('IS_FREE_ACCESS_ENABLED'); + const isCRMMigrationEnabled = useIsFeatureEnabled('IS_CRM_MIGRATION_ENABLED'); + const isServerlessFunctionSettingsEnabled = useIsFeatureEnabled( + 'IS_FUNCTION_SETTINGS_ENABLED', + ); + + const isBillingPageEnabled = + billing?.isBillingEnabled && !isFreeAccessEnabled; + + return ( + <RouterProvider + router={createAppRouter( + isBillingPageEnabled, + isCRMMigrationEnabled, + isServerlessFunctionSettingsEnabled, + )} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx new file mode 100644 index 000000000000..e5a24da4057a --- /dev/null +++ b/packages/twenty-front/src/modules/app/components/AppRouterProviders.tsx @@ -0,0 +1,66 @@ +import { ApolloProvider } from '@/apollo/components/ApolloProvider'; +import { CommandMenuEffect } from '@/app/effect-components/CommandMenuEffect'; +import { GotoHotkeys } from '@/app/effect-components/GotoHotkeysEffect'; +import { PageChangeEffect } from '@/app/effect-components/PageChangeEffect'; +import { AuthProvider } from '@/auth/components/AuthProvider'; +import { ChromeExtensionSidecarEffect } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarEffect'; +import { ChromeExtensionSidecarProvider } from '@/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider'; +import { ClientConfigProvider } from '@/client-config/components/ClientConfigProvider'; +import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect'; +import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect'; +import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider'; +import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider'; +import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider'; +import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager'; +import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope'; +import { SnackBarProvider } from '@/ui/feedback/snack-bar-manager/components/SnackBarProvider'; +import { AppThemeProvider } from '@/ui/theme/components/AppThemeProvider'; +import { PageTitle } from '@/ui/utilities/page-title/PageTitle'; +import { UserProvider } from '@/users/components/UserProvider'; +import { UserProviderEffect } from '@/users/components/UserProviderEffect'; +import { StrictMode } from 'react'; +import { Outlet, useLocation } from 'react-router-dom'; +import { getPageTitleFromPath } from '~/utils/title-utils'; + +export const AppRouterProviders = () => { + const { pathname } = useLocation(); + const pageTitle = getPageTitleFromPath(pathname); + + return ( + <ApolloProvider> + <ClientConfigProviderEffect /> + <ClientConfigProvider> + <ChromeExtensionSidecarEffect /> + <ChromeExtensionSidecarProvider> + <UserProviderEffect /> + <UserProvider> + <AuthProvider> + <ApolloMetadataClientProvider> + <ObjectMetadataItemsProvider> + <PrefetchDataProvider> + <AppThemeProvider> + <SnackBarProvider> + <DialogManagerScope dialogManagerScopeId="dialog-manager"> + <DialogManager> + <StrictMode> + <PromiseRejectionEffect /> + <CommandMenuEffect /> + <GotoHotkeys /> + <PageTitle title={pageTitle} /> + <Outlet /> + </StrictMode> + </DialogManager> + </DialogManagerScope> + </SnackBarProvider> + </AppThemeProvider> + </PrefetchDataProvider> + <PageChangeEffect /> + </ObjectMetadataItemsProvider> + </ApolloMetadataClientProvider> + </AuthProvider> + </UserProvider> + </ChromeExtensionSidecarProvider> + </ClientConfigProvider> + </ApolloProvider> + ); +}; diff --git a/packages/twenty-front/src/SettingsRoutes.tsx b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx similarity index 92% rename from packages/twenty-front/src/SettingsRoutes.tsx rename to packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx index 05e852e2bed0..5eede03959b5 100644 --- a/packages/twenty-front/src/SettingsRoutes.tsx +++ b/packages/twenty-front/src/modules/app/components/SettingsRoutes.tsx @@ -1,6 +1,7 @@ import { lazy, Suspense } from 'react'; import { Route, Routes } from 'react-router-dom'; +import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader'; import { AppPath } from '@/types/AppPath'; import { SettingsPath } from '@/types/SettingsPath'; @@ -64,7 +65,7 @@ const SettingsDevelopersApiKeysNew = lazy(() => const SettingsDevelopersWebhooksNew = lazy(() => import( - '~/pages/settings/developers/webhooks/SettingsDevelopersWebhooksNew' + '~/pages/settings/developers/webhooks/components/SettingsDevelopersWebhooksNew' ).then((module) => ({ default: module.SettingsDevelopersWebhooksNew, })), @@ -164,7 +165,7 @@ const SettingsObjects = lazy(() => const SettingsDevelopersWebhooksDetail = lazy(() => import( - '~/pages/settings/developers/webhooks/SettingsDevelopersWebhookDetail' + '~/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail' ).then((module) => ({ default: module.SettingsDevelopersWebhooksDetail, })), @@ -202,22 +203,21 @@ const SettingsIntegrationShowDatabaseConnection = lazy(() => })), ); -const SettingsObjectNewFieldStep1 = lazy(() => +const SettingsObjectNewFieldSelect = lazy(() => import( - '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1' + '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldSelect' ).then((module) => ({ - default: module.SettingsObjectNewFieldStep1, + default: module.SettingsObjectNewFieldSelect, })), ); -const SettingsObjectNewFieldStep2 = lazy(() => +const SettingsObjectNewFieldConfigure = lazy(() => import( - '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2' + '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldConfigure' ).then((module) => ({ - default: module.SettingsObjectNewFieldStep2, + default: module.SettingsObjectNewFieldConfigure, })), ); - const SettingsObjectFieldEdit = lazy(() => import('~/pages/settings/data-model/SettingsObjectFieldEdit').then( (module) => ({ @@ -245,7 +245,7 @@ export const SettingsRoutes = ({ isCRMMigrationEnabled, isServerlessFunctionSettingsEnabled, }: SettingsRoutesProps) => ( - <Suspense fallback={null}> + <Suspense fallback={<SettingsSkeletonLoader />}> <Routes> <Route path={SettingsPath.ProfilePage} element={<SettingsProfile />} /> <Route path={SettingsPath.Appearance} element={<SettingsAppearance />} /> @@ -345,12 +345,12 @@ export const SettingsRoutes = ({ element={<SettingsIntegrationShowDatabaseConnection />} /> <Route - path={SettingsPath.ObjectNewFieldStep1} - element={<SettingsObjectNewFieldStep1 />} + path={SettingsPath.ObjectNewFieldSelect} + element={<SettingsObjectNewFieldSelect />} /> <Route - path={SettingsPath.ObjectNewFieldStep2} - element={<SettingsObjectNewFieldStep2 />} + path={SettingsPath.ObjectNewFieldConfigure} + element={<SettingsObjectNewFieldConfigure />} /> <Route path={SettingsPath.ObjectFieldEdit} diff --git a/packages/twenty-front/src/effect-components/CommandMenuEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/CommandMenuEffect.tsx similarity index 89% rename from packages/twenty-front/src/effect-components/CommandMenuEffect.tsx rename to packages/twenty-front/src/modules/app/effect-components/CommandMenuEffect.tsx index ece319312e91..b210ae724276 100644 --- a/packages/twenty-front/src/effect-components/CommandMenuEffect.tsx +++ b/packages/twenty-front/src/modules/app/effect-components/CommandMenuEffect.tsx @@ -7,7 +7,7 @@ import { commandMenuCommandsState } from '@/command-menu/states/commandMenuComma export const CommandMenuEffect = () => { const setCommands = useSetRecoilState(commandMenuCommandsState); - const commands = COMMAND_MENU_COMMANDS; + const commands = Object.values(COMMAND_MENU_COMMANDS); useEffect(() => { setCommands(commands); }, [commands, setCommands]); diff --git a/packages/twenty-front/src/modules/app/effect-components/GoToHotkeyItemEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/GoToHotkeyItemEffect.tsx new file mode 100644 index 000000000000..a0b545302501 --- /dev/null +++ b/packages/twenty-front/src/modules/app/effect-components/GoToHotkeyItemEffect.tsx @@ -0,0 +1,12 @@ +import { useGoToHotkeys } from '@/ui/utilities/hotkey/hooks/useGoToHotkeys'; + +export const GoToHotkeyItemEffect = (props: { + hotkey: string; + pathToNavigateTo: string; +}) => { + const { hotkey, pathToNavigateTo } = props; + + useGoToHotkeys(hotkey, 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 new file mode 100644 index 000000000000..15d371f9f44a --- /dev/null +++ b/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx @@ -0,0 +1,18 @@ +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) => ( + <GoToHotkeyItemEffect + hotkey={objectMetadataItem.namePlural[0]} + pathToNavigateTo={`/objects/${objectMetadataItem.namePlural}`} + /> + )); +}; diff --git a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx similarity index 90% rename from packages/twenty-front/src/effect-components/PageChangeEffect.tsx rename to packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx index 05c99cc89d56..a8b05f4c0904 100644 --- a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx +++ b/packages/twenty-front/src/modules/app/effect-components/PageChangeEffect.tsx @@ -12,6 +12,8 @@ import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCapt import { isCaptchaScriptLoadedState } from '@/captcha/states/isCaptchaScriptLoadedState'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { CommandType } from '@/command-menu/types/Command'; +import { useNonSystemActiveObjectMetadataItems } from '@/object-metadata/hooks/useNonSystemActiveObjectMetadataItems'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; import { AppBasePath } from '@/types/AppBasePath'; @@ -43,7 +45,9 @@ export const PageChangeEffect = () => { const eventTracker = useEventTracker(); - const { addToCommandMenu, setToInitialCommandMenu } = useCommandMenu(); + const { addToCommandMenu, setObjectsInCommandMenu } = useCommandMenu(); + + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const openCreateActivity = useOpenCreateActivityDrawer({ activityObjectNameSingular: CoreObjectNameSingular.Task, @@ -146,8 +150,11 @@ export const PageChangeEffect = () => { } }, [isMatchingLocation, setHotkeyScope]); + const { nonSystemActiveObjectMetadataItems } = + useNonSystemActiveObjectMetadataItems(); + useEffect(() => { - setToInitialCommandMenu(); + setObjectsInCommandMenu(nonSystemActiveObjectMetadataItems); addToCommandMenu([ { @@ -162,7 +169,13 @@ export const PageChangeEffect = () => { }), }, ]); - }, [addToCommandMenu, setToInitialCommandMenu, openCreateActivity]); + }, [ + nonSystemActiveObjectMetadataItems, + addToCommandMenu, + setObjectsInCommandMenu, + openCreateActivity, + objectMetadataItems, + ]); useEffect(() => { setTimeout(() => { diff --git a/packages/twenty-front/src/modules/app/utils/createAppRouter.tsx b/packages/twenty-front/src/modules/app/utils/createAppRouter.tsx new file mode 100644 index 000000000000..0ddb70ac1a34 --- /dev/null +++ b/packages/twenty-front/src/modules/app/utils/createAppRouter.tsx @@ -0,0 +1,78 @@ +import { AppRouterProviders } from '@/app/components/AppRouterProviders'; +import { SettingsRoutes } from '@/app/components/SettingsRoutes'; +import { VerifyEffect } from '@/auth/components/VerifyEffect'; +import indexAppPath from '@/navigation/utils/indexAppPath'; +import { AppPath } from '@/types/AppPath'; +import { BlankLayout } from '@/ui/layout/page/BlankLayout'; +import { DefaultLayout } from '@/ui/layout/page/DefaultLayout'; +import { + createBrowserRouter, + createRoutesFromElements, + Route, +} from 'react-router-dom'; +import { Authorize } from '~/pages/auth/Authorize'; +import { Invite } from '~/pages/auth/Invite'; +import { PasswordReset } from '~/pages/auth/PasswordReset'; +import { SignInUp } from '~/pages/auth/SignInUp'; +import { ImpersonateEffect } from '~/pages/impersonate/ImpersonateEffect'; +import { NotFound } from '~/pages/not-found/NotFound'; +import { RecordIndexPage } from '~/pages/object-record/RecordIndexPage'; +import { RecordShowPage } from '~/pages/object-record/RecordShowPage'; +import { ChooseYourPlan } from '~/pages/onboarding/ChooseYourPlan'; +import { CreateProfile } from '~/pages/onboarding/CreateProfile'; +import { CreateWorkspace } from '~/pages/onboarding/CreateWorkspace'; +import { InviteTeam } from '~/pages/onboarding/InviteTeam'; +import { PaymentSuccess } from '~/pages/onboarding/PaymentSuccess'; +import { SyncEmails } from '~/pages/onboarding/SyncEmails'; + +export const createAppRouter = ( + isBillingEnabled?: boolean, + isCRMMigrationEnabled?: boolean, + isServerlessFunctionSettingsEnabled?: boolean, +) => + createBrowserRouter( + createRoutesFromElements( + <Route + element={<AppRouterProviders />} + // To switch state to `loading` temporarily to enable us + // to set scroll position before the page is rendered + loader={async () => Promise.resolve(null)} + > + <Route element={<DefaultLayout />}> + <Route path={AppPath.Verify} element={<VerifyEffect />} /> + <Route path={AppPath.SignInUp} element={<SignInUp />} /> + <Route path={AppPath.Invite} element={<Invite />} /> + <Route path={AppPath.ResetPassword} element={<PasswordReset />} /> + <Route path={AppPath.CreateWorkspace} element={<CreateWorkspace />} /> + <Route path={AppPath.CreateProfile} element={<CreateProfile />} /> + <Route path={AppPath.SyncEmails} element={<SyncEmails />} /> + <Route path={AppPath.InviteTeam} element={<InviteTeam />} /> + <Route path={AppPath.PlanRequired} element={<ChooseYourPlan />} /> + <Route + path={AppPath.PlanRequiredSuccess} + element={<PaymentSuccess />} + /> + <Route path={indexAppPath.getIndexAppPath()} element={<></>} /> + <Route path={AppPath.Impersonate} element={<ImpersonateEffect />} /> + <Route path={AppPath.RecordIndexPage} element={<RecordIndexPage />} /> + <Route path={AppPath.RecordShowPage} element={<RecordShowPage />} /> + <Route + path={AppPath.SettingsCatchAll} + element={ + <SettingsRoutes + isBillingEnabled={isBillingEnabled} + isCRMMigrationEnabled={isCRMMigrationEnabled} + isServerlessFunctionSettingsEnabled={ + isServerlessFunctionSettingsEnabled + } + /> + } + /> + <Route path={AppPath.NotFoundWildcard} element={<NotFound />} /> + </Route> + <Route element={<BlankLayout />}> + <Route path={AppPath.Authorize} element={<Authorize />} /> + </Route> + </Route>, + ), + ); diff --git a/packages/twenty-front/src/modules/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider.tsx b/packages/twenty-front/src/modules/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider.tsx index 3c4d8556a4bf..9409540ead23 100644 --- a/packages/twenty-front/src/modules/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider.tsx +++ b/packages/twenty-front/src/modules/chrome-extension-sidecar/components/ChromeExtensionSidecarProvider.tsx @@ -1,56 +1,46 @@ -import styled from '@emotion/styled'; -import { useRecoilValue } from 'recoil'; - -import { isLoadingTokensFromExtensionState } from '@/chrome-extension-sidecar/states/isLoadingTokensFromExtensionState'; -import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState'; -import { isDefined } from '~/utils/isDefined'; -import { isInFrame } from '~/utils/isInIframe'; - -const StyledContainer = styled.div` - align-items: center; - display: flex; - flex-direction: column; - height: 100vh; - justify-content: center; -`; - -const AppInaccessible = ({ message }: { message: string }) => { - return ( - <StyledContainer> - <img - src="/images/integrations/twenty-logo.svg" - alt="twenty-icon" - height={40} - width={40} - /> - <h3>{message}</h3> - </StyledContainer> - ); -}; +// const StyledContainer = styled.div` +// align-items: center; +// display: flex; +// flex-direction: column; +// height: 100vh; +// justify-content: center; +// `; + +// const AppInaccessible = ({ message }: { message: string }) => { +// return ( +// <StyledContainer> +// <img +// src="/images/integrations/twenty-logo.svg" +// alt="twenty-icon" +// height={40} +// width={40} +// /> +// <h3>{message}</h3> +// </StyledContainer> +// ); +// }; export const ChromeExtensionSidecarProvider: React.FC< React.PropsWithChildren > = ({ children }) => { - const isLoadingTokensFromExtension = useRecoilValue( - isLoadingTokensFromExtensionState, - ); - const chromeExtensionId = useRecoilValue(chromeExtensionIdState); - - if (!isInFrame()) return <>{children}</>; - - if (!isDefined(chromeExtensionId)) - return ( - <AppInaccessible message={`Twenty is not accessible inside an iframe.`} /> - ); - - if (isDefined(isLoadingTokensFromExtension) && !isLoadingTokensFromExtension) - return ( - <AppInaccessible - message={`Unauthorized access from iframe origin. If you're trying to access from chrome extension, - please check your chrome extension ID on your server. - `} - /> - ); - - return isLoadingTokensFromExtension && <>{children}</>; + return <>{children}</>; + + // TODO: this is conflictting with storybook tests + // if (!isInFrame()) return <>{children}</>; + + // if (!isDefined(chromeExtensionId)) + // return ( + // <AppInaccessible message={`Twenty is not accessible inside an iframe.`} /> + // ); + + // if (isDefined(isLoadingTokensFromExtension) && !isLoadingTokensFromExtension) + // return ( + // <AppInaccessible + // message={`Unauthorized access from iframe origin. If you're trying to access from chrome extension, + // please check your chrome extension ID on your server. + // `} + // /> + // ); + + // return isLoadingTokensFromExtension && <>{children}</>; }; 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 2b7d13f8c52b..d6cf7ff02ad3 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,9 @@ import { useKeyboardShortcutMenu } from '@/keyboard-shortcut-menu/hooks/useKeybo 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/Opportunity'; import { Person } from '@/people/types/Person'; import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; @@ -165,8 +167,21 @@ export const CommandMenu = () => { [closeCommandMenu], ); - const { records: people } = useFindManyRecords<Person>({ - skip: !isCommandMenuOpened, + const isTwentyOrmEnabled = useIsFeatureEnabled( + 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED', + ); + + const isWorkspaceMigratedForSearch = useIsFeatureEnabled( + 'IS_WORKSPACE_MIGRATED_FOR_SEARCH', + ); + + const isSearchEnabled = + useIsFeatureEnabled('IS_SEARCH_ENABLED') && + isTwentyOrmEnabled && + isWorkspaceMigratedForSearch; + + const { records: peopleFromFindMany } = useFindManyRecords<Person>({ + skip: !isCommandMenuOpened || isSearchEnabled, objectNameSingular: CoreObjectNameSingular.Person, filter: commandMenuSearch ? makeOrFilterVariables([ @@ -179,14 +194,28 @@ export const CommandMenu = () => { 'emails', ['primaryEmail'], ), - { phone: { ilike: `%${commandMenuSearch}%` } }, ]) : undefined, limit: 3, }); + const { records: peopleFromSearch } = useSearchRecords<Person>({ + skip: !isCommandMenuOpened || !isSearchEnabled, + objectNameSingular: CoreObjectNameSingular.Person, + limit: 3, + searchInput: commandMenuSearch ?? undefined, + }); - const { records: companies } = useFindManyRecords<Company>({ - skip: !isCommandMenuOpened, + const people = isSearchEnabled ? peopleFromSearch : peopleFromFindMany; + + const { records: companiesFromSearch } = useSearchRecords<Company>({ + skip: !isCommandMenuOpened || !isSearchEnabled, + objectNameSingular: CoreObjectNameSingular.Company, + limit: 3, + searchInput: commandMenuSearch ?? undefined, + }); + + const { records: companiesFromFindMany } = useFindManyRecords<Company>({ + skip: !isCommandMenuOpened || isSearchEnabled, objectNameSingular: CoreObjectNameSingular.Company, filter: commandMenuSearch ? { @@ -196,6 +225,10 @@ export const CommandMenu = () => { limit: 3, }); + const companies = isSearchEnabled + ? companiesFromSearch + : companiesFromFindMany; + const { records: notes } = useFindManyRecords<Note>({ skip: !isCommandMenuOpened, objectNameSingular: CoreObjectNameSingular.Note, @@ -208,8 +241,8 @@ export const CommandMenu = () => { limit: 3, }); - const { records: opportunities } = useFindManyRecords({ - skip: !isCommandMenuOpened, + const { records: opportunitiesFromFindMany } = useFindManyRecords({ + skip: !isCommandMenuOpened || isSearchEnabled, objectNameSingular: CoreObjectNameSingular.Opportunity, filter: commandMenuSearch ? { @@ -219,6 +252,17 @@ export const CommandMenu = () => { limit: 3, }); + const { records: opportunitiesFromSearch } = useSearchRecords<Opportunity>({ + skip: !isCommandMenuOpened || !isSearchEnabled, + objectNameSingular: CoreObjectNameSingular.Opportunity, + limit: 3, + searchInput: commandMenuSearch ?? undefined, + }); + + const opportunities = isSearchEnabled + ? opportunitiesFromSearch + : opportunitiesFromFindMany; + const peopleCommands = useMemo( () => people.map(({ id, name: { firstName, lastName } }) => ({ diff --git a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx index 16c8bf7f4104..a7e1dc95e33d 100644 --- a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx @@ -2,7 +2,7 @@ import { action } from '@storybook/addon-actions'; import { Meta, StoryObj } from '@storybook/react'; import { expect, userEvent, within } from '@storybook/test'; import { useEffect } from 'react'; -import { useSetRecoilState } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { IconCheckbox, IconNotes } from 'twenty-ui'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; @@ -20,6 +20,7 @@ import { } from '~/testing/mock-data/users'; import { sleep } from '~/utils/sleep'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { CommandMenu } from '../CommandMenu'; const companiesMock = getCompaniesMock(); @@ -35,14 +36,21 @@ const meta: Meta<typeof CommandMenu> = { const setCurrentWorkspaceMember = useSetRecoilState( currentWorkspaceMemberState, ); - const { addToCommandMenu, setToInitialCommandMenu, openCommandMenu } = + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + + const { addToCommandMenu, setObjectsInCommandMenu, openCommandMenu } = useCommandMenu(); setCurrentWorkspace(mockDefaultWorkspace); setCurrentWorkspaceMember(mockedWorkspaceMemberData); useEffect(() => { - setToInitialCommandMenu(); + const nonSystemActiveObjects = objectMetadataItems.filter( + (object) => !object.isSystem && object.isActive, + ); + + setObjectsInCommandMenu(nonSystemActiveObjects); + addToCommandMenu([ { id: 'create-task', @@ -62,7 +70,12 @@ const meta: Meta<typeof CommandMenu> = { }, ]); openCommandMenu(); - }, [addToCommandMenu, setToInitialCommandMenu, openCommandMenu]); + }, [ + addToCommandMenu, + setObjectsInCommandMenu, + openCommandMenu, + objectMetadataItems, + ]); return <Story />; }, diff --git a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuCommands.ts b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuCommands.ts index 3c7f03168d98..711fbff881e9 100644 --- a/packages/twenty-front/src/modules/command-menu/constants/CommandMenuCommands.ts +++ b/packages/twenty-front/src/modules/command-menu/constants/CommandMenuCommands.ts @@ -8,8 +8,8 @@ import { import { Command, CommandType } from '../types/Command'; -export const COMMAND_MENU_COMMANDS: Command[] = [ - { +export const COMMAND_MENU_COMMANDS: { [key: string]: Command } = { + people: { id: 'go-to-people', to: '/objects/people', label: 'Go to People', @@ -18,7 +18,7 @@ export const COMMAND_MENU_COMMANDS: Command[] = [ secondHotKey: 'P', Icon: IconUser, }, - { + companies: { id: 'go-to-companies', to: '/objects/companies', label: 'Go to Companies', @@ -27,7 +27,7 @@ export const COMMAND_MENU_COMMANDS: Command[] = [ secondHotKey: 'C', Icon: IconBuildingSkyscraper, }, - { + opportunities: { id: 'go-to-activities', to: '/objects/opportunities', label: 'Go to Opportunities', @@ -36,7 +36,7 @@ export const COMMAND_MENU_COMMANDS: Command[] = [ secondHotKey: 'O', Icon: IconTargetArrow, }, - { + settings: { id: 'go-to-settings', to: '/settings/profile', label: 'Go to Settings', @@ -45,7 +45,7 @@ export const COMMAND_MENU_COMMANDS: Command[] = [ secondHotKey: 'S', Icon: IconSettings, }, - { + tasks: { id: 'go-to-tasks', to: '/objects/tasks', label: 'Go to Tasks', @@ -54,4 +54,4 @@ export const COMMAND_MENU_COMMANDS: Command[] = [ secondHotKey: 'T', Icon: IconCheckbox, }, -]; +}; diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx index e1ee5501382f..b0502e58374a 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx @@ -1,6 +1,6 @@ +import { renderHook } from '@testing-library/react'; import { act } from 'react-dom/test-utils'; import { MemoryRouter } from 'react-router-dom'; -import { renderHook } from '@testing-library/react'; import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; @@ -107,13 +107,39 @@ describe('useCommandMenu', () => { expect(onClickMock).toHaveBeenCalledTimes(1); }); - it('should setToInitialCommandMenu command menu', () => { + it('should setObjectsInCommandMenu command menu', () => { const { result } = renderHooks(); act(() => { - result.current.commandMenu.setToInitialCommandMenu(); + result.current.commandMenu.setObjectsInCommandMenu([]); + }); + + expect(result.current.commandMenuCommands.length).toBe(1); + + act(() => { + result.current.commandMenu.setObjectsInCommandMenu([ + { + id: 'b88745ce-9021-4316-a018-8884e02d05ca', + nameSingular: 'task', + namePlural: 'tasks', + labelSingular: 'Task', + labelPlural: 'Tasks', + description: 'A task', + icon: 'IconCheckbox', + isCustom: false, + isRemote: false, + isActive: true, + isSystem: false, + createdAt: '2024-09-12T20:23:46.041Z', + updatedAt: '2024-09-13T08:36:53.426Z', + labelIdentifierFieldMetadataId: + 'ab7901eb-43e1-4dc7-8f3b-cdee2857eb9a', + imageIdentifierFieldMetadataId: null, + fields: [], + }, + ]); }); - expect(result.current.commandMenuCommands.length).toBe(5); + expect(result.current.commandMenuCommands.length).toBe(2); }); }); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts index 1a8e085064e2..d19c314a1c8b 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts @@ -1,6 +1,6 @@ +import { isNonEmptyString } from '@sniptt/guards'; import { useCallback } from 'react'; import { useNavigate } from 'react-router-dom'; -import { isNonEmptyString } from '@sniptt/guards'; import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { commandMenuSearchState } from '@/command-menu/states/commandMenuSearchState'; @@ -9,10 +9,13 @@ import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousH import { AppHotkeyScope } from '@/ui/utilities/hotkey/types/AppHotkeyScope'; import { isDefined } from '~/utils/isDefined'; -import { COMMAND_MENU_COMMANDS } from '../constants/CommandMenuCommands'; +import { COMMAND_MENU_COMMANDS } from '@/command-menu/constants/CommandMenuCommands'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { ALL_ICONS } from '@ui/display/icon/providers/internal/AllIcons'; +import { sortByProperty } from '~/utils/array/sortByProperty'; import { commandMenuCommandsState } from '../states/commandMenuCommandsState'; import { isCommandMenuOpenedState } from '../states/isCommandMenuOpenedState'; -import { Command } from '../types/Command'; +import { Command, CommandType } from '../types/Command'; export const useCommandMenu = () => { const navigate = useNavigate(); @@ -70,8 +73,27 @@ export const useCommandMenu = () => { [setCommands], ); - const setToInitialCommandMenu = () => { - setCommands(COMMAND_MENU_COMMANDS); + const setObjectsInCommandMenu = (menuItems: ObjectMetadataItem[]) => { + const formattedItems = [ + ...[ + ...menuItems.map( + (item) => + ({ + id: item.id, + to: `/objects/${item.namePlural}`, + label: `Go to ${item.labelPlural}`, + type: CommandType.Navigate, + firstHotKey: 'G', + secondHotKey: item.labelPlural[0], + Icon: ALL_ICONS[ + (item?.icon as keyof typeof ALL_ICONS) ?? 'IconArrowUpRight' + ], + }) as Command, + ), + ].sort(sortByProperty('label', 'asc')), + COMMAND_MENU_COMMANDS.settings, + ]; + setCommands(formattedItems); }; const onItemClick = useCallback( @@ -96,6 +118,6 @@ export const useCommandMenu = () => { toggleCommandMenu, addToCommandMenu, onItemClick, - setToInitialCommandMenu, + setObjectsInCommandMenu, }; }; diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentObjectMetadataIdState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentObjectMetadataIdState.ts new file mode 100644 index 000000000000..3227e53807df --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentObjectMetadataIdState.ts @@ -0,0 +1,8 @@ +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/contextStoreCurrentViewIdState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentViewIdState.ts new file mode 100644 index 000000000000..41af1cc1357b --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/states/contextStoreCurrentViewIdState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const contextStoreCurrentViewIdState = createState<string | null>({ + key: 'contextStoreCurrentViewIdState', + defaultValue: null, +}); diff --git a/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordIdsState.ts b/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordIdsState.ts new file mode 100644 index 000000000000..df0c3451172c --- /dev/null +++ b/packages/twenty-front/src/modules/context-store/states/contextStoreTargetedRecordIdsState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const contextStoreTargetedRecordIdsState = createState<string[]>({ + key: 'contextStoreTargetedRecordIdsState', + defaultValue: [], +}); diff --git a/packages/twenty-front/src/modules/favorites/components/FavoritesSkeletonLoader.tsx b/packages/twenty-front/src/modules/favorites/components/FavoritesSkeletonLoader.tsx index 3822a12bc9b6..5e5d1ea7f8ec 100644 --- a/packages/twenty-front/src/modules/favorites/components/FavoritesSkeletonLoader.tsx +++ b/packages/twenty-front/src/modules/favorites/components/FavoritesSkeletonLoader.tsx @@ -1,6 +1,7 @@ -import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; const StyledSkeletonContainer = styled.div` display: flex; @@ -25,10 +26,19 @@ export const FavoritesSkeletonLoader = () => { borderRadius={4} > <StyledSkeletonContainer> - <Skeleton width={56} height={13} /> + <Skeleton + width={56} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.xs} + /> <StyledSkeletonColumn> - <Skeleton width={196} height={16} /> - <Skeleton width={196} height={16} /> + <Skeleton + width={196} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} + /> + <Skeleton + width={196} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} + /> </StyledSkeletonColumn> </StyledSkeletonContainer> </SkeletonTheme> diff --git a/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx b/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx index b89290526c98..cf106211405b 100644 --- a/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx +++ b/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx @@ -1,7 +1,6 @@ -import { useFavorites } from '@/favorites/hooks/useFavorites'; +import { useFilteredObjectMetadataItemsForWorkspaceFavorites } from '@/navigation/hooks/useObjectMetadataItemsInWorkspaceFavorites'; import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems'; 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'; @@ -9,34 +8,18 @@ import { View } from '@/views/types/View'; export const WorkspaceFavorites = () => { const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews); - const loading = useIsPrefetchLoading(); - - const { workspaceFavorites } = useFavorites(); - - const workspaceFavoriteIds = new Set( - workspaceFavorites.map((favorite) => favorite.recordId), - ); - const favoriteViewObjectMetadataIds = views.reduce<string[]>((acc, view) => { - if (workspaceFavoriteIds.has(view.id)) { - acc.push(view.objectMetadataId); - } - return acc; - }, []); - - const { objectMetadataItems } = useFilteredObjectMetadataItems(); - - const objectMetadataItemsToDisplay = objectMetadataItems.filter((item) => - favoriteViewObjectMetadataIds.includes(item.id), - ); + const { activeObjectMetadataItems: objectMetadataItemsToDisplay } = + useFilteredObjectMetadataItemsForWorkspaceFavorites(); + const loading = useIsPrefetchLoading(); if (loading) { return <NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader />; } return ( <NavigationDrawerSectionForObjectMetadataItems - sectionTitle={'Workspace Favorites'} + sectionTitle={'Workspace'} objectMetadataItems={objectMetadataItemsToDisplay} views={views} isRemote={false} diff --git a/packages/twenty-front/src/modules/favorites/hooks/__mocks__/useFavorites.ts b/packages/twenty-front/src/modules/favorites/hooks/__mocks__/useFavorites.ts index 98813ad384c7..cf0fe8e888b1 100644 --- a/packages/twenty-front/src/modules/favorites/hooks/__mocks__/useFavorites.ts +++ b/packages/twenty-front/src/modules/favorites/hooks/__mocks__/useFavorites.ts @@ -87,262 +87,264 @@ export const mocks = [ mutation CreateOneFavorite($input: FavoriteCreateInput!) { createFavorite(data: $input) { __typename - noteId - taskId - person { + company { __typename - name { - firstName - lastName + accountOwnerId + address { + addressStreet1 + addressStreet2 + addressCity + addressState + addressCountry + addressPostcode + addressLat + addressLng + } + annualRecurringRevenue { + amountMicros + currencyCode + } + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + domainName { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + employees + id + idealCustomerProfile + introVideo { + primaryLinkUrl + primaryLinkLabel + secondaryLinks } linkedinLink { primaryLinkUrl primaryLinkLabel secondaryLinks } - deletedAt - createdAt + name + position + tagline updatedAt - jobTitle - intro - workPrefereance - performanceRating + visaSponsorship + workPolicy xLink { primaryLinkUrl primaryLinkLabel secondaryLinks } - city - companyId - phones { - primaryPhoneNumber - primaryPhoneCountryCode - additionalPhones + } + companyId + createdAt + deletedAt + id + note { + __typename + body + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + id + position + title + updatedAt + } + noteId + opportunity { + __typename + amount { + amountMicros + currencyCode } + closeDate + companyId + createdAt createdBy { source workspaceMemberId name } + deletedAt id + name + pointOfContactId position + stage + updatedAt + } + opportunityId + person { + __typename + avatarUrl + city + companyId + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt emails { primaryEmail additionalEmails } - avatarUrl + id + intro + jobTitle + linkedinLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + name { + firstName + lastName + } + performanceRating + phones { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } + position + updatedAt whatsapp { primaryPhoneNumber primaryPhoneCountryCode additionalPhones } + workPreference + xLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } } - task { + personId + position + rocket { __typename - updatedAt createdAt - deletedAt - dueAt - id - status - body createdBy { source workspaceMemberId name } - assigneeId - position - title - } - rocketId - viewId - updatedAt - workflowId - personId - workspaceMemberId - note { - __typename deletedAt id + name position updatedAt + } + rocketId + task { + __typename + assigneeId + body + createdAt createdBy { source workspaceMemberId name } - body + deletedAt + dueAt + id + position + status title - createdAt + updatedAt } - createdAt + taskId + updatedAt view { __typename - id - type + createdAt + deletedAt icon - key + id isCompact kanbanFieldMetadataId + key + name objectMetadataId position - createdAt - deletedAt + type updatedAt - name } - opportunityId - position - deletedAt - id - companyId + viewId workflow { __typename - deletedAt - lastPublishedVersionId createdAt + deletedAt id - statuses + lastPublishedVersionId name position + statuses updatedAt } + workflowId workspaceMember { __typename + avatarUrl + colorScheme + createdAt + dateFormat + deletedAt + id + locale name { firstName lastName } - avatarUrl - userId - createdAt - timeZone - id timeFormat + timeZone updatedAt - locale userEmail - deletedAt - colorScheme - dateFormat + userId } - company { - __typename - updatedAt - domainName { - primaryLinkUrl - primaryLinkLabel - secondaryLinks - } - visaSponsorship - address { - addressStreet1 - addressStreet2 - addressCity - addressState - addressCountry - addressPostcode - addressLat - addressLng - } - position - employees - deletedAt - accountOwnerId - annualRecurringRevenue { - amountMicros - currencyCode - } - id - name - xLink { - primaryLinkUrl - primaryLinkLabel - secondaryLinks - } - createdAt - createdBy { - source - workspaceMemberId - name - } - workPolicy - introVideo { - primaryLinkUrl - primaryLinkLabel - secondaryLinks - } - linkedinLink { - primaryLinkUrl - primaryLinkLabel - secondaryLinks - } - tagline - idealCustomerProfile - } - rocket { - __typename - createdBy { - source - workspaceMemberId - name - } - updatedAt - name - position - createdAt - id - deletedAt - } - opportunity { - __typename - createdBy { - source - workspaceMemberId - name - } - amount { - amountMicros - currencyCode - } - stage - position - closeDate - id - name - pointOfContactId - companyId - updatedAt - deletedAt - createdAt - } - } - } - `, - variables: { - input: { - id: mockId, - personId: favoriteTargetObjectId, - position: 4, - workspaceMemberId: '1', - }, - }, - }, - result: jest.fn(() => ({ - data: { - createFavorite: { - id: favoriteId, - }, - }, - })), - }, - { - request: { - query: gql` - mutation DeleteOneFavorite($idToDelete: ID!) { - deleteFavorite(id: $idToDelete) { - id + workspaceMemberId + } + } + `, + variables: { + input: { + id: mockId, + personId: favoriteTargetObjectId, + position: 4, + workspaceMemberId: '1', + }, + }, + }, + result: jest.fn(() => ({ + data: { + createFavorite: { + id: favoriteId, + }, + }, + })), + }, + { + request: { + query: gql` + mutation DeleteOneFavorite($idToDelete: ID!) { + deleteFavorite(id: $idToDelete) { + __typename + deletedAt + id } } `, @@ -365,236 +367,236 @@ export const mocks = [ ) { updateFavorite(id: $idToUpdate, data: $input) { __typename - noteId - taskId - person { - __typename - name { - firstName - lastName - } - linkedinLink { - primaryLinkUrl - primaryLinkLabel - secondaryLinks - } - deletedAt - createdAt - updatedAt - jobTitle - intro - workPrefereance - performanceRating - xLink { - primaryLinkUrl - primaryLinkLabel - secondaryLinks - } - city - companyId - phones { - primaryPhoneNumber - primaryPhoneCountryCode - additionalPhones - } - createdBy { - source - workspaceMemberId - name - } - id - position - emails { - primaryEmail - additionalEmails - } - avatarUrl - whatsapp { - primaryPhoneNumber - primaryPhoneCountryCode - additionalPhones - } - } - task { - __typename - updatedAt - createdAt - deletedAt - dueAt - id - status - body - createdBy { - source - workspaceMemberId - name - } - assigneeId - position - title - } - rocketId - viewId - updatedAt - workflowId - personId - workspaceMemberId - note { - __typename - deletedAt - id - position - updatedAt - createdBy { - source - workspaceMemberId - name - } - body - title - createdAt - } - createdAt - view { - __typename - id - type - icon - key - isCompact - kanbanFieldMetadataId - objectMetadataId - position - createdAt - deletedAt - updatedAt - name - } - opportunityId - position - deletedAt - id - companyId - workflow { - __typename - deletedAt - lastPublishedVersionId - createdAt - id - statuses - name - position - updatedAt - } - workspaceMember { - __typename - name { - firstName - lastName - } - avatarUrl - userId - createdAt - timeZone - id - timeFormat - updatedAt - locale - userEmail - deletedAt - colorScheme - dateFormat - } - company { - __typename - updatedAt - domainName { - primaryLinkUrl - primaryLinkLabel - secondaryLinks - } - visaSponsorship - address { - addressStreet1 - addressStreet2 - addressCity - addressState - addressCountry - addressPostcode - addressLat - addressLng - } - position - employees - deletedAt - accountOwnerId - annualRecurringRevenue { - amountMicros - currencyCode - } - id - name - xLink { - primaryLinkUrl - primaryLinkLabel - secondaryLinks - } - createdAt - createdBy { - source - workspaceMemberId - name - } - workPolicy - introVideo { - primaryLinkUrl - primaryLinkLabel - secondaryLinks - } - linkedinLink { - primaryLinkUrl - primaryLinkLabel - secondaryLinks - } - tagline - idealCustomerProfile - } - rocket { - __typename - createdBy { - source - workspaceMemberId - name - } - updatedAt - name - position - createdAt - id - deletedAt - } - opportunity { - __typename - createdBy { - source + company { + __typename + accountOwnerId + address { + addressStreet1 + addressStreet2 + addressCity + addressState + addressCountry + addressPostcode + addressLat + addressLng + } + annualRecurringRevenue { + amountMicros + currencyCode + } + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + domainName { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + employees + id + idealCustomerProfile + introVideo { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + linkedinLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + name + position + tagline + updatedAt + visaSponsorship + workPolicy + xLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + } + companyId + createdAt + deletedAt + id + note { + __typename + body + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + id + position + title + updatedAt + } + noteId + opportunity { + __typename + amount { + amountMicros + currencyCode + } + closeDate + companyId + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + id + name + pointOfContactId + position + stage + updatedAt + } + opportunityId + person { + __typename + avatarUrl + city + companyId + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + emails { + primaryEmail + additionalEmails + } + id + intro + jobTitle + linkedinLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + name { + firstName + lastName + } + performanceRating + phones { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } + position + updatedAt + whatsapp { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } + workPreference + xLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + } + personId + position + rocket { + __typename + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + id + name + position + updatedAt + } + rocketId + task { + __typename + assigneeId + body + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + dueAt + id + position + status + title + updatedAt + } + taskId + updatedAt + view { + __typename + createdAt + deletedAt + icon + id + isCompact + kanbanFieldMetadataId + key + name + objectMetadataId + position + type + updatedAt + } + viewId + workflow { + __typename + createdAt + deletedAt + id + lastPublishedVersionId + name + position + statuses + updatedAt + } + workflowId + workspaceMember { + __typename + avatarUrl + colorScheme + createdAt + dateFormat + deletedAt + id + locale + name { + firstName + lastName + } + timeFormat + timeZone + updatedAt + userEmail + userId + } workspaceMemberId - name - } - amount { - amountMicros - currencyCode - } - stage - position - closeDate - id - name - pointOfContactId - companyId - updatedAt - deletedAt - createdAt - } } } `, diff --git a/packages/twenty-front/src/modules/favorites/hooks/__tests__/useFavorites.test.tsx b/packages/twenty-front/src/modules/favorites/hooks/__tests__/useFavorites.test.tsx index 3a30077ea285..9c984ececa94 100644 --- a/packages/twenty-front/src/modules/favorites/hooks/__tests__/useFavorites.test.tsx +++ b/packages/twenty-front/src/modules/favorites/hooks/__tests__/useFavorites.test.tsx @@ -1,16 +1,14 @@ -import { MockedProvider } from '@apollo/client/testing'; import { DropResult, ResponderProvided } from '@hello-pangea/dnd'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { ReactNode } from 'react'; -import { RecoilRoot, useSetRecoilState } from 'recoil'; +import { useSetRecoilState } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { useFavorites } from '@/favorites/hooks/useFavorites'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { favoriteId, favoriteTargetObjectRecord, @@ -29,15 +27,9 @@ jest.mock('@/object-record/hooks/useFindManyRecords', () => ({ useFindManyRecords: () => ({ records: initialFavorites }), })); -const Wrapper = ({ children }: { children: ReactNode }) => ( - <RecoilRoot> - <SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager"> - <MockedProvider mocks={mocks} addTypename={false}> - {children} - </MockedProvider> - </SnackBarProviderScope> - </RecoilRoot> -); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); describe('useFavorites', () => { it('should fetch favorites successfully', async () => { diff --git a/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts b/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts index 0580333afb90..01bad17167a5 100644 --- a/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts +++ b/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts @@ -5,6 +5,10 @@ export const detectTimeFormat = () => { const isHour12 = Intl.DateTimeFormat(navigator.language, { hour: 'numeric', }).resolvedOptions().hour12; - if (isDefined(isHour12) && isHour12) return TimeFormat.HOUR_12; + + if (isDefined(isHour12) && isHour12) { + return TimeFormat.HOUR_12; + } + return TimeFormat.HOUR_24; }; diff --git a/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx b/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx index da636fe025e5..a6d3b1045cc9 100644 --- a/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx +++ b/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx @@ -4,7 +4,6 @@ import { useRecoilValue, useSetRecoilState } from 'recoil'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { SettingsNavigationDrawerItems } from '@/settings/components/SettingsNavigationDrawerItems'; import { SupportDropdown } from '@/support/components/SupportDropdown'; -import { GithubVersionLink } from '@/ui/navigation/link/components/GithubVersionLink'; import { NavigationDrawer, NavigationDrawerProps, @@ -16,6 +15,7 @@ import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI'; import { useIsSettingsPage } from '../hooks/useIsSettingsPage'; import { currentMobileNavigationDrawerState } from '../states/currentMobileNavigationDrawerState'; +import { AdvancedSettingsToggle } from '@/ui/navigation/link/components/AdvancedSettingsToggle'; import { MainNavigationDrawerItems } from './MainNavigationDrawerItems'; export type AppNavigationDrawerProps = { @@ -44,7 +44,7 @@ export const AppNavigationDrawer = ({ isSubMenu: true, title: 'Exit Settings', children: <SettingsNavigationDrawerItems />, - footer: <GithubVersionLink />, + footer: <AdvancedSettingsToggle />, } : { logo: diff --git a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx index 88f18e97e66b..867b5b49f558 100644 --- a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx +++ b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx @@ -5,6 +5,7 @@ import { IconSearch, IconSettings } from 'twenty-ui'; import { useCommandMenu } from '@/command-menu/hooks/useCommandMenu'; import { CurrentWorkspaceMemberFavorites } from '@/favorites/components/CurrentWorkspaceMemberFavorites'; import { WorkspaceFavorites } from '@/favorites/components/WorkspaceFavorites'; +import { NavigationDrawerOpenedSection } from '@/object-metadata/components/NavigationDrawerOpenedSection'; import { NavigationDrawerSectionForObjectMetadataItemsWrapper } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper'; import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; @@ -44,6 +45,8 @@ export const MainNavigationDrawerItems = () => { </NavigationDrawerSection> )} + {isWorkspaceFavoriteEnabled && <NavigationDrawerOpenedSection />} + <CurrentWorkspaceMemberFavorites /> {isWorkspaceFavoriteEnabled ? ( diff --git a/packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts b/packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts index 1ec02c2fd071..a7fb3b4e51ed 100644 --- a/packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts +++ b/packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts @@ -6,7 +6,7 @@ import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePat import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; import { AppPath } from '@/types/AppPath'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { mockedUserData } from '~/testing/mock-data/users'; jest.mock('@/prefetch/hooks/usePrefetchedData'); diff --git a/packages/twenty-front/src/modules/navigation/hooks/useObjectMetadataItemsInWorkspaceFavorites.ts b/packages/twenty-front/src/modules/navigation/hooks/useObjectMetadataItemsInWorkspaceFavorites.ts new file mode 100644 index 000000000000..1c8abefe350e --- /dev/null +++ b/packages/twenty-front/src/modules/navigation/hooks/useObjectMetadataItemsInWorkspaceFavorites.ts @@ -0,0 +1,35 @@ +import { useFavorites } from '@/favorites/hooks/useFavorites'; +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; +import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; +import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; +import { View } from '@/views/types/View'; + +export const useFilteredObjectMetadataItemsForWorkspaceFavorites = () => { + const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews); + + const { workspaceFavorites } = useFavorites(); + + const workspaceFavoriteIds = new Set( + workspaceFavorites.map((favorite) => favorite.recordId), + ); + + const favoriteViewObjectMetadataIds = new Set( + views.reduce<string[]>((acc, view) => { + if (workspaceFavoriteIds.has(view.id)) { + acc.push(view.objectMetadataId); + } + return acc; + }, []), + ); + + const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); + + const activeObjectMetadataItemsInWorkspaceFavorites = + activeObjectMetadataItems.filter((item) => + favoriteViewObjectMetadataIds.has(item.id), + ); + + return { + activeObjectMetadataItems: activeObjectMetadataItemsInWorkspaceFavorites, + }; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx new file mode 100644 index 000000000000..fb17b643078f --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx @@ -0,0 +1,57 @@ +import { useParams } from 'react-router-dom'; + +import { useFilteredObjectMetadataItemsForWorkspaceFavorites } from '@/navigation/hooks/useObjectMetadataItemsInWorkspaceFavorites'; +import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems'; +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(); + const filteredActiveObjectMetadataItems = activeObjectMetadataItems.filter( + (item) => !item.isRemote, + ); + + const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews); + const loading = useIsPrefetchLoading(); + + const currentObjectNamePlural = useParams().objectNamePlural; + + const { activeObjectMetadataItems: workspaceFavoritesObjectMetadataItems } = + useFilteredObjectMetadataItemsForWorkspaceFavorites(); + + if (!currentObjectNamePlural) { + return; + } + + const objectMetadataItem = filteredActiveObjectMetadataItems.find( + (item) => item.namePlural === currentObjectNamePlural, + ); + + if (!objectMetadataItem) { + return; + } + + const shouldDisplayObjectInOpenedSection = + !workspaceFavoritesObjectMetadataItems + .map((item) => item.id) + .includes(objectMetadataItem.id); + + if (loading) { + return <NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader />; + } + + return ( + shouldDisplayObjectInOpenedSection && ( + <NavigationDrawerSectionForObjectMetadataItems + sectionTitle={'Opened'} + objectMetadataItems={[objectMetadataItem]} + views={views} + isRemote={false} + /> + ) + ); +}; diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader.tsx index 70e00d717330..70b965b1827f 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader.tsx @@ -1,3 +1,4 @@ +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; @@ -20,9 +21,18 @@ export const NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader: React. borderRadius={4} > <StyledSkeletonColumn> - <Skeleton width={196} height={16} /> - <Skeleton width={196} height={16} /> - <Skeleton width={196} height={16} /> + <Skeleton + width={196} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} + /> + <Skeleton + width={196} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} + /> + <Skeleton + width={196} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} + /> </StyledSkeletonColumn> </SkeletonTheme> ); diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx index cd3f0e2c2b06..c8659e1689c8 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx @@ -1,50 +1,65 @@ import { useEffect } from 'react'; -import { useRecoilState, useRecoilValue } from 'recoil'; +import { useRecoilCallback, useRecoilValue } from 'recoil'; import { useIsLogged } from '@/auth/hooks/useIsLogged'; import { currentUserState } from '@/auth/states/currentUserState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useFindManyObjectMetadataItems } from '@/object-metadata/hooks/useFindManyObjectMetadataItems'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; import { WorkspaceActivationStatus } from '~/generated/graphql'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; +const filterTsVectorFields = ( + objectMetadataItems: ObjectMetadataItem[], +): ObjectMetadataItem[] => { + return objectMetadataItems.map((item) => ({ + ...item, + fields: item.fields.filter( + (field) => field.type !== FieldMetadataType.TsVector, + ), + })); +}; + export const ObjectMetadataItemsLoadEffect = () => { const currentUser = useRecoilValue(currentUserState); const currentWorkspace = useRecoilValue(currentWorkspaceState); const isLoggedIn = useIsLogged(); - const { objectMetadataItems: newObjectMetadataItems, loading } = + const { objectMetadataItems: newObjectMetadataItems } = useFindManyObjectMetadataItems({ skip: !isLoggedIn, }); - const [objectMetadataItems, setObjectMetadataItems] = useRecoilState( - objectMetadataItemsState, + const updateObjectMetadataItems = useRecoilCallback( + ({ set, snapshot }) => + () => { + const filteredFields = filterTsVectorFields(newObjectMetadataItems); + const toSetObjectMetadataItems = + isUndefinedOrNull(currentUser) || + currentWorkspace?.activationStatus !== + WorkspaceActivationStatus.Active + ? generatedMockObjectMetadataItems + : filteredFields; + + if ( + !isDeeplyEqual( + snapshot.getLoadable(objectMetadataItemsState).getValue(), + toSetObjectMetadataItems, + ) + ) { + set(objectMetadataItemsState, toSetObjectMetadataItems); + } + }, + [currentUser, currentWorkspace?.activationStatus, newObjectMetadataItems], ); useEffect(() => { - const toSetObjectMetadataItems = - isUndefinedOrNull(currentUser) || - currentWorkspace?.activationStatus !== WorkspaceActivationStatus.Active - ? generatedMockObjectMetadataItems - : newObjectMetadataItems; - if ( - !loading && - !isDeeplyEqual(objectMetadataItems, toSetObjectMetadataItems) - ) { - setObjectMetadataItems(toSetObjectMetadataItems); - } - }, [ - currentUser, - currentWorkspace?.activationStatus, - loading, - newObjectMetadataItems, - objectMetadataItems, - setObjectMetadataItems, - ]); + updateObjectMetadataItems(); + }, [updateObjectMetadataItems]); return <></>; }; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/ApolloMetadataClientProvider.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/ApolloMetadataClientMockedProvider.tsx similarity index 100% rename from packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/ApolloMetadataClientProvider.tsx rename to packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/ApolloMetadataClientMockedProvider.tsx diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts index f3c3e93f1b2c..0e7470a10359 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useFieldMetadataItem.ts @@ -48,7 +48,18 @@ export const queries = { createMetadataField: gql` mutation CreateOneFieldMetadataItem($input: CreateOneFieldMetadataInput!) { createOneField(input: $input) { - ${baseFields} + id + type + name + label + description + icon + isCustom + isActive + isNullable + createdAt + updatedAt + settings defaultValue options } diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts index 558d2ff3034c..05c87497dcc6 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useColumnDefinitionsFromFieldMetadata.test.ts @@ -3,7 +3,7 @@ import { Nullable } from 'twenty-ui'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; describe('useColumnDefinitionsFromFieldMetadata', () => { it('should return empty definitions if no object is passed', () => { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx index 03443712d9de..cc92f241e31c 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx @@ -1,10 +1,8 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreateOneObjectMetadataItem'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { findManyViewsQuery, query, @@ -47,13 +45,9 @@ const mocks = [ }, ]; -const Wrapper = ({ children }: { children: ReactNode }) => ( - <RecoilRoot> - <MockedProvider mocks={mocks} addTypename={false}> - {children} - </MockedProvider> - </RecoilRoot> -); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); describe('useCreateOneObjectMetadataItem', () => { it('should work as expected', async () => { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx index 47b1a6c18c88..6795897e3cfd 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFieldMetadataItem.test.tsx @@ -1,6 +1,6 @@ -import { ReactNode } from 'react'; import { MockedProvider } from '@apollo/client/testing'; -import { act, renderHook } from '@testing-library/react'; +import { renderHook } from '@testing-library/react'; +import { act, ReactNode } from 'react'; import { RecoilRoot } from 'recoil'; import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFilteredObjectMetadataItems.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFilteredObjectMetadataItems.test.tsx index 4d16bb6d8af9..02e39712fe57 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFilteredObjectMetadataItems.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFilteredObjectMetadataItems.test.tsx @@ -10,7 +10,7 @@ import { } from '@/object-metadata/hooks/__mocks__/useFilteredObjectMetadataItems'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; const mocks = [ { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetObjectOrderByField.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetObjectOrderByField.test.tsx index 21ec56ad60b4..41220e695b94 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetObjectOrderByField.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetObjectOrderByField.test.tsx @@ -1,15 +1,11 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; import { renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; import { useGetObjectOrderByField } from '@/object-metadata/hooks/useGetObjectOrderByField'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; -const Wrapper = ({ children }: { children: ReactNode }) => ( - <RecoilRoot> - <MockedProvider addTypename={false}>{children}</MockedProvider> - </RecoilRoot> -); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], +}); describe('useGetObjectOrderByField', () => { it('should work as expected', () => { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetObjectRecordIdentifierByNameSingular.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetObjectRecordIdentifierByNameSingular.test.tsx index a457bd27eff7..938796b78714 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetObjectRecordIdentifierByNameSingular.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetObjectRecordIdentifierByNameSingular.test.tsx @@ -3,7 +3,7 @@ import { RecoilRoot, useSetRecoilState } from 'recoil'; import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; describe('useGetObjectRecordIdentifierByNameSingular', () => { it('should work as expected', async () => { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetRelationMetadata.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetRelationMetadata.test.tsx index 12ef572b6adf..7455088f27bc 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetRelationMetadata.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetRelationMetadata.test.tsx @@ -5,7 +5,7 @@ import { RecoilRoot, useSetRecoilState } from 'recoil'; import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; const Wrapper = ({ children }: { children: ReactNode }) => ( <RecoilRoot> diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useMapToObjectRecordIdentifier.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useMapToObjectRecordIdentifier.test.tsx index d0157678c04a..8792fb31299a 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useMapToObjectRecordIdentifier.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useMapToObjectRecordIdentifier.test.tsx @@ -1,7 +1,11 @@ import { renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; + +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], +}); describe('useMapToObjectRecordIdentifier', () => { it('should work as expected', async () => { @@ -18,7 +22,7 @@ describe('useMapToObjectRecordIdentifier', () => { }); }, { - wrapper: RecoilRoot, + wrapper: Wrapper, }, ); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItem.test.tsx index ee757d7ff7b2..876846f2bd4b 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItem.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItem.test.tsx @@ -1,16 +1,12 @@ -import { MockedProvider } from '@apollo/client/testing'; import { renderHook } from '@testing-library/react'; -import { ReactNode } from 'react'; -import { RecoilRoot } from 'recoil'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; -const Wrapper = ({ children }: { children: ReactNode }) => ( - <RecoilRoot> - <MockedProvider addTypename={false}>{children}</MockedProvider> - </RecoilRoot> -); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], +}); // Split into tests for each new hook describe('useObjectMetadataItem', () => { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useNonSystemActiveObjectMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useNonSystemActiveObjectMetadataItems.ts new file mode 100644 index 000000000000..a33b80e1d125 --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useNonSystemActiveObjectMetadataItems.ts @@ -0,0 +1,20 @@ +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { useMemo } from 'react'; +import { useRecoilValue } from 'recoil'; + +export const useNonSystemActiveObjectMetadataItems = () => { + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + + const nonSystemActiveObjectMetadataItems = useMemo( + () => + objectMetadataItems.filter( + (objectMetadataItem) => + !objectMetadataItem.isSystem && objectMetadataItem.isActive, + ), + [objectMetadataItems], + ); + + return { + nonSystemActiveObjectMetadataItems, + }; +}; diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts index fb08e4c96a2c..0a8d3e8600a0 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts @@ -1,37 +1,23 @@ import { useRecoilValue } from 'recoil'; -import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { ObjectMetadataItemNotFoundError } from '@/object-metadata/errors/ObjectMetadataNotFoundError'; import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { isDefined } from '~/utils/isDefined'; -import { WorkspaceActivationStatus } from '~/generated/graphql'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; import { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentifier'; export const useObjectMetadataItem = ({ objectNameSingular, }: ObjectMetadataItemIdentifier) => { - const currentWorkspace = useRecoilValue(currentWorkspaceState); - - let objectMetadataItem = useRecoilValue( + const objectMetadataItem = useRecoilValue( objectMetadataItemFamilySelector({ objectName: objectNameSingular, objectNameType: 'singular', }), ); - let objectMetadataItems = useRecoilValue(objectMetadataItemsState); - - if (currentWorkspace?.activationStatus !== WorkspaceActivationStatus.Active) { - objectMetadataItem = - generatedMockObjectMetadataItems.find( - (objectMetadataItem) => - objectMetadataItem.nameSingular === objectNameSingular, - ) ?? null; - objectMetadataItems = generatedMockObjectMetadataItems; - } + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); if (!isDefined(objectMetadataItem)) { throw new ObjectMetadataItemNotFoundError( diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts index 25208307eabd..88ac0baab21d 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts @@ -3,7 +3,7 @@ import { useRecoilValue } from 'recoil'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; import { WorkspaceActivationStatus } from '~/generated/graphql'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { isDefined } from '~/utils/isDefined'; export const useObjectNamePluralFromSingular = ({ diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts index 5a94a5191b9f..2e5127d8fcd9 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts @@ -3,7 +3,7 @@ import { useRecoilValue } from 'recoil'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { objectMetadataItemFamilySelector } from '@/object-metadata/states/objectMetadataItemFamilySelector'; import { WorkspaceActivationStatus } from '~/generated/graphql'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { isDefined } from '~/utils/isDefined'; export const useObjectNameSingularFromPlural = ({ diff --git a/packages/twenty-front/src/modules/object-metadata/types/FieldMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/types/FieldMetadataItem.ts index 61ce60263dfc..8a403a55a379 100644 --- a/packages/twenty-front/src/modules/object-metadata/types/FieldMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/types/FieldMetadataItem.ts @@ -17,7 +17,7 @@ export type FieldMetadataItemOption = { export type FieldMetadataItem = Omit< Field, - '__typename' | 'defaultValue' | 'options' | 'settings' | 'relationDefinition' + '__typename' | 'defaultValue' | 'options' | 'relationDefinition' > & { __typename?: string; defaultValue?: any; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectMetadataItemBySingularName.test.ts b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectMetadataItemBySingularName.test.ts index ea2f1d5eee19..92a7e414b822 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectMetadataItemBySingularName.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectMetadataItemBySingularName.test.ts @@ -1,5 +1,5 @@ import { getObjectMetadataItemByNameSingular } from '@/object-metadata/utils/getObjectMetadataItemBySingularName'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; describe('getObjectMetadataItemBySingularName', () => { it('should work as expected', () => { diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectOrderByField.test.ts b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectOrderByField.test.ts index 78c0e7047a4a..cc5691412fd3 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectOrderByField.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectOrderByField.test.ts @@ -1,5 +1,5 @@ import { getOrderByFieldForObjectMetadataItem } from '@/object-metadata/utils/getObjectOrderByField'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; describe('getObjectOrderByField', () => { it('should work as expected', () => { diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectSlug.test.ts b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectSlug.test.ts index c43c9d039e7c..526c6fe47635 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectSlug.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/getObjectSlug.test.ts @@ -1,5 +1,5 @@ import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; describe('getObjectSlug', () => { it('should work as expected', () => { diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/isObjectMetadataAvailableForRelation.test.ts b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/isObjectMetadataAvailableForRelation.test.ts index caefb7feed45..ece3ae39de06 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/isObjectMetadataAvailableForRelation.test.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/isObjectMetadataAvailableForRelation.test.ts @@ -1,5 +1,5 @@ import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; describe('isObjectMetadataAvailableForRelation', () => { it('should work as expected', () => { 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.tsx index 755ce29ac677..7208246e7e49 100644 --- 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.tsx @@ -1,5 +1,5 @@ import { mapFieldMetadataToGraphQLQuery } from '@/object-metadata/utils/mapFieldMetadataToGraphQLQuery'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { normalizeGQLField } from '~/utils/normalizeGQLField'; const personObjectMetadataItem = generatedMockObjectMetadataItems.find( 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.tsx index c47eae446715..d2650b69807f 100644 --- 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.tsx @@ -1,5 +1,5 @@ import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { normalizeGQLQuery } from '~/utils/normalizeGQLQuery'; const personObjectMetadataItem = generatedMockObjectMetadataItems.find( diff --git a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts index 8ba9ebe23315..f372cd2eb3ac 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition.ts @@ -54,5 +54,6 @@ export const formatFieldMetadataItemAsFieldDefinition = ({ metadata: fieldDefintionMetadata, type: field.type, }), + settings: field.settings, }; }; diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getDisabledFieldMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/utils/getDisabledFieldMetadataItems.ts deleted file mode 100644 index 52f781ccc203..000000000000 --- a/packages/twenty-front/src/modules/object-metadata/utils/getDisabledFieldMetadataItems.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; - -export const getDisabledFieldMetadataItems = ( - objectMetadataItem: Pick<ObjectMetadataItem, 'fields'>, -) => - objectMetadataItem.fields.filter( - (fieldMetadataItem) => - !fieldMetadataItem.isActive && !fieldMetadataItem.isSystem, - ); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts index f9e248815d99..9e705d428ced 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts @@ -18,6 +18,7 @@ export const mapObjectMetadataToGraphQLQuery = ({ const fieldsThatShouldBeQueried = objectMetadataItem?.fields .filter((field) => field.isActive) + .sort((fieldA, fieldB) => fieldA.name.localeCompare(fieldB.name)) .filter((field) => shouldFieldBeQueried({ field, diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapSoftDeleteFieldsToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapSoftDeleteFieldsToGraphQLQuery.ts new file mode 100644 index 000000000000..701e524b56ab --- /dev/null +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapSoftDeleteFieldsToGraphQLQuery.ts @@ -0,0 +1,16 @@ +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; + +export const mapSoftDeleteFieldsToGraphQLQuery = ( + objectMetadataItem: Pick<ObjectMetadataItem, 'fields'>, +): string => { + const softDeleteFields = ['id', 'deletedAt']; + + const fieldsThatShouldBeQueried = objectMetadataItem.fields.filter( + (field) => field.isActive && softDeleteFields.includes(field.name), + ); + + return `{ + __typename + ${fieldsThatShouldBeQueried.map((field) => field.name).join('\n')} + }`; +}; 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 0e8d60c66fb9..cbb1b2c46b3d 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 @@ -1,11 +1,12 @@ -import { mockedCompanyObjectMetadataItem } from '~/testing/mock-data/metadata'; - +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { objectMetadataItemSchema } from '../objectMetadataItemSchema'; describe('objectMetadataItemSchema', () => { it('validates a valid object metadata item', () => { // Given - const validObjectMetadataItem = mockedCompanyObjectMetadataItem; + const validObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'company', + ); // When const result = objectMetadataItemSchema.parse(validObjectMetadataItem); diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/__tests__/getRecordNodeFromRecord.test.ts b/packages/twenty-front/src/modules/object-record/cache/utils/__tests__/getRecordNodeFromRecord.test.ts index 219d05f854e5..4641974a0632 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/__tests__/getRecordNodeFromRecord.test.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/__tests__/getRecordNodeFromRecord.test.ts @@ -1,10 +1,8 @@ import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { - mockedObjectMetadataItems, - mockedPersonObjectMetadataItem, -} from '~/testing/mock-data/metadata'; + import { getPeopleMock } from '~/testing/mock-data/people'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { getRecordNodeFromRecord } from '../getRecordNodeFromRecord'; const peopleMock = getPeopleMock(); @@ -12,11 +10,18 @@ const peopleMock = getPeopleMock(); describe('getRecordNodeFromRecord', () => { it('computes relation records cache references by default', () => { // Given - const objectMetadataItems: ObjectMetadataItem[] = mockedObjectMetadataItems; - const objectMetadataItem: Pick< - ObjectMetadataItem, - 'fields' | 'namePlural' | 'nameSingular' - > = mockedPersonObjectMetadataItem; + const objectMetadataItems: ObjectMetadataItem[] = + generatedMockObjectMetadataItems; + const objectMetadataItem: + | Pick<ObjectMetadataItem, 'fields' | 'namePlural' | 'nameSingular'> + | undefined = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', + ); + + if (!objectMetadataItem) { + throw new Error('Object metadata item not found'); + } + const recordGqlFields = { name: true, company: true, @@ -47,11 +52,18 @@ describe('getRecordNodeFromRecord', () => { it('does not compute relation records cache references when `computeReferences` is false', () => { // Given - const objectMetadataItems: ObjectMetadataItem[] = mockedObjectMetadataItems; - const objectMetadataItem: Pick< - ObjectMetadataItem, - 'fields' | 'namePlural' | 'nameSingular' - > = mockedPersonObjectMetadataItem; + const objectMetadataItems: ObjectMetadataItem[] = + generatedMockObjectMetadataItems; + const objectMetadataItem: + | Pick<ObjectMetadataItem, 'fields' | 'namePlural' | 'nameSingular'> + | undefined = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', + ); + + if (!objectMetadataItem) { + throw new Error('Object metadata item not found'); + } + const recordGqlFields = { name: true, company: true, diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts index d7faf5a77f29..2ab3b25344fa 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordNodeFromRecord.ts @@ -65,7 +65,9 @@ export const getRecordNodeFromRecord = <T extends ObjectRecord>({ RelationDefinitionType.OneToMany ) { const oneToManyObjectMetadataItem = objectMetadataItems.find( - (item) => item.namePlural === fieldName, + (item) => + item.namePlural === + field.relationDefinition?.targetObjectMetadata.namePlural, ); if (!oneToManyObjectMetadataItem) { diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts index 72573ea2133e..6c1615b17afa 100644 --- a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts +++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationFilter.ts @@ -92,6 +92,7 @@ export type LinksFilter = { export type ActorFilter = { name?: StringFilter; + source?: IsFilter; }; export type EmailsFilter = { diff --git a/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationSearchResult.ts b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationSearchResult.ts new file mode 100644 index 000000000000..7cd2f5e314b5 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/graphql/types/RecordGqlOperationSearchResult.ts @@ -0,0 +1,5 @@ +import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection'; + +export type RecordGqlOperationSearchResult = { + [objectNamePlural: string]: RecordGqlConnection; +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/personFragment.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/personFragment.ts deleted file mode 100644 index 8a2b3f08567d..000000000000 --- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/personFragment.ts +++ /dev/null @@ -1,48 +0,0 @@ -export const PERSON_FRAGMENT = ` - __typename - name { - firstName - lastName - } - linkedinLink { - primaryLinkUrl - primaryLinkLabel - secondaryLinks - } - deletedAt - createdAt - updatedAt - jobTitle - intro - workPrefereance - performanceRating - xLink { - primaryLinkUrl - primaryLinkLabel - secondaryLinks - } - city - companyId - phones { - primaryPhoneNumber - primaryPhoneCountryCode - additionalPhones - } - createdBy { - source - workspaceMemberId - name - } - id - position - emails { - primaryEmail - additionalEmails - } - avatarUrl - whatsapp { - primaryPhoneNumber - primaryPhoneCountryCode - additionalPhones - } -` diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/personFragments.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/personFragments.ts new file mode 100644 index 000000000000..21cf8b2848b2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/personFragments.ts @@ -0,0 +1,327 @@ +export const PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS = ` + __typename + avatarUrl + city + companyId + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + emails { + primaryEmail + additionalEmails + } + id + intro + jobTitle + linkedinLink{ + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + name { + firstName + lastName + } + performanceRating + phones { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } + position + updatedAt + whatsapp { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } + workPreference + xLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } +` + +export const PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS = ` + __typename + activityTargets { + edges { + node { + __typename + activityId + companyId + createdAt + deletedAt + id + opportunityId + personId + rocketId + updatedAt + } + } + } + attachments { + edges { + node { + __typename + activityId + authorId + companyId + createdAt + deletedAt + fullPath + id + name + noteId + opportunityId + personId + rocketId + taskId + type + updatedAt + } + } + } + avatarUrl + calendarEventParticipants { + edges { + node { + __typename + calendarEventId + createdAt + deletedAt + displayName + handle + id + isOrganizer + personId + responseStatus + updatedAt + workspaceMemberId + } + } + } + city + company { + __typename + accountOwnerId + address { + addressStreet1 + addressStreet2 + addressCity + addressState + addressCountry + addressPostcode + addressLat + addressLng + } + annualRecurringRevenue { + amountMicros + currencyCode + } + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + domainName { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + employees + id + idealCustomerProfile + introVideo { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + linkedinLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + name + position + tagline + updatedAt + visaSponsorship + workPolicy + xLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + } + companyId + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + emails { + primaryEmail + additionalEmails + } + favorites { + edges { + node { + __typename + companyId + createdAt + deletedAt + id + noteId + opportunityId + personId + position + rocketId + taskId + updatedAt + viewId + workflowId + workspaceMemberId + } + } + } + id + intro + jobTitle + linkedinLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + messageParticipants { + edges { + node { + __typename + createdAt + deletedAt + displayName + handle + id + messageId + personId + role + updatedAt + workspaceMemberId + } + } + } + name { + firstName + lastName + } + noteTargets { + edges { + node { + __typename + companyId + createdAt + deletedAt + id + noteId + opportunityId + personId + rocketId + updatedAt + } + } + } + performanceRating + phones { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } + pointOfContactForOpportunities { + edges { + node { + __typename + amount { + amountMicros + currencyCode + } + closeDate + companyId + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + id + name + pointOfContactId + position + stage + updatedAt + } + } + } + position + taskTargets { + edges { + node { + __typename + companyId + createdAt + deletedAt + id + opportunityId + personId + rocketId + taskId + updatedAt + } + } + } + timelineActivities { + edges { + node { + __typename + companyId + createdAt + deletedAt + happensAt + id + linkedObjectMetadataId + linkedRecordCachedName + linkedRecordId + name + noteId + opportunityId + personId + properties + rocketId + taskId + updatedAt + workspaceMemberId + } + } + } + updatedAt + whatsapp { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } + workPreference + xLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } +` diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useCreateManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useCreateManyRecords.ts index fd28307168b5..b9d5b32b8b36 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useCreateManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useCreateManyRecords.ts @@ -1,12 +1,12 @@ import { gql } from '@apollo/client'; -import { PERSON_FRAGMENT } from '@/object-record/hooks/__mocks__/personFragment'; +import { PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; import { Person } from '@/people/types/Person'; export const query = gql` mutation CreatePeople($data: [PersonCreateInput!]!, $upsert: Boolean) { createPeople(data: $data, upsert: $upsert) { - ${PERSON_FRAGMENT} + ${PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS} } } `; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useCreateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useCreateOneRecord.ts index 6804a92c3525..6a0261794f3c 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useCreateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useCreateOneRecord.ts @@ -1,10 +1,10 @@ -import { PERSON_FRAGMENT } from '@/object-record/hooks/__mocks__/personFragment'; +import { PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; import { gql } from '@apollo/client'; export const query = gql` mutation CreateOnePerson($input: PersonCreateInput!) { createPerson(data: $input) { - ${PERSON_FRAGMENT} + ${PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS} } } `; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useDeleteOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useDeleteOneRecord.ts index f31b210e089b..2e7ce9bc5320 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useDeleteOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useDeleteOneRecord.ts @@ -3,6 +3,8 @@ import { gql } from '@apollo/client'; export const query = gql` mutation DeleteOnePerson($idToDelete: ID!) { deletePerson(id: $idToDelete) { + __typename + deletedAt id } } diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindDuplicateRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindDuplicateRecords.ts index 2e50e02aac84..784e178fc785 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindDuplicateRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindDuplicateRecords.ts @@ -1,4 +1,4 @@ -import { PERSON_FRAGMENT } from '@/object-record/hooks/__mocks__/personFragment'; +import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; import { gql } from '@apollo/client'; import { getPeopleMock } from '~/testing/mock-data/people'; @@ -9,7 +9,7 @@ export const query = gql` personDuplicates(ids: $ids) { edges { node { - ${PERSON_FRAGMENT} + ${PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS} } cursor } diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindOneRecord.ts index 26ac2981aa37..075dabb052f5 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindOneRecord.ts @@ -1,12 +1,12 @@ import { gql } from '@apollo/client'; -import { PERSON_FRAGMENT } from '@/object-record/hooks/__mocks__/personFragment'; +import { PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; import { responseData as person } from './useUpdateOneRecord'; export const query = gql` query FindOnePerson($objectRecordId: ID!) { person(filter: { id: { eq: $objectRecordId } }) { - ${PERSON_FRAGMENT} + ${PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS} } } `; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useUpdateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useUpdateOneRecord.ts index 15bb27bc76b1..1c109b9e3ed4 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useUpdateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useUpdateOneRecord.ts @@ -1,10 +1,10 @@ -import { PERSON_FRAGMENT } from '@/object-record/hooks/__mocks__/personFragment'; +import { PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; import { gql } from '@apollo/client'; export const query = gql` mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) { updatePerson(id: $idToUpdate, data: $input) { - ${PERSON_FRAGMENT} + ${PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS} } } `; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecords.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecords.test.tsx index 9eb14eadd799..761680c43081 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecords.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecords.test.tsx @@ -1,8 +1,5 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; import { mocked } from '@storybook/test'; import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; import { v4 } from 'uuid'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; @@ -12,6 +9,7 @@ import { variables, } from '@/object-record/hooks/__mocks__/useCreateManyRecords'; import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; jest.mock('uuid', () => ({ v4: jest.fn(), @@ -37,13 +35,9 @@ const mocks = [ }, ]; -const Wrapper = ({ children }: { children: ReactNode }) => ( - <RecoilRoot> - <MockedProvider mocks={mocks} addTypename={false}> - {children} - </MockedProvider> - </RecoilRoot> -); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); describe('useCreateManyRecords', () => { it('works as expected', async () => { diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecordsMutation.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecordsMutation.test.tsx index 3b4d1ad42d4c..a5ab3efec117 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecordsMutation.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateManyRecordsMutation.test.tsx @@ -1,18 +1,22 @@ import { renderHook } from '@testing-library/react'; import { print } from 'graphql'; -import { RecoilRoot } from 'recoil'; -import { PERSON_FRAGMENT } from '@/object-record/hooks/__mocks__/personFragment'; +import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; import { useCreateManyRecordsMutation } from '@/object-record/hooks/useCreateManyRecordsMutation'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const expectedQueryTemplate = ` mutation CreatePeople($data: [PersonCreateInput!]!, $upsert: Boolean) { createPeople(data: $data, upsert: $upsert) { - ${PERSON_FRAGMENT} + ${PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS} } } `.replace(/\s/g, ''); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], +}); + describe('useCreateManyRecordsMutation', () => { it('should return a valid createManyRecordsMutation', () => { const objectNameSingular = 'person'; @@ -23,7 +27,7 @@ describe('useCreateManyRecordsMutation', () => { objectNameSingular, }), { - wrapper: RecoilRoot, + wrapper: Wrapper, }, ); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecord.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecord.test.tsx index 837af4dd2bf6..db26b860517a 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecord.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecord.test.tsx @@ -1,7 +1,4 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { @@ -9,6 +6,7 @@ import { responseData, } from '@/object-record/hooks/__mocks__/useCreateOneRecord'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const personId = 'a7286b9a-c039-4a89-9567-2dfa7953cda9'; const input = { name: { firstName: 'John', lastName: 'Doe' } }; @@ -31,13 +29,9 @@ const mocks = [ }, ]; -const Wrapper = ({ children }: { children: ReactNode }) => ( - <RecoilRoot> - <MockedProvider mocks={mocks} addTypename={false}> - {children} - </MockedProvider> - </RecoilRoot> -); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); describe('useCreateOneRecord', () => { it('works as expected', async () => { diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecordMutation.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecordMutation.test.tsx index 18db35f815b0..376a085cde7c 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecordMutation.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useCreateOneRecordMutation.test.tsx @@ -1,18 +1,22 @@ import { renderHook } from '@testing-library/react'; import { print } from 'graphql'; -import { RecoilRoot } from 'recoil'; -import { PERSON_FRAGMENT } from '@/object-record/hooks/__mocks__/personFragment'; +import { PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; import { useCreateOneRecordMutation } from '@/object-record/hooks/useCreateOneRecordMutation'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const expectedQueryTemplate = ` mutation CreateOnePerson($input: PersonCreateInput!) { createPerson(data: $input) { - ${PERSON_FRAGMENT} + ${PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS} } } `.replace(/\s/g, ''); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], +}); + describe('useCreateOneRecordMutation', () => { it('should return a valid createOneRecordMutation', () => { const objectNameSingular = 'person'; @@ -23,7 +27,7 @@ describe('useCreateOneRecordMutation', () => { objectNameSingular, }), { - wrapper: RecoilRoot, + wrapper: Wrapper, }, ); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecords.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecords.test.tsx index 1c7dd33d33e6..ada53864a7a6 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecords.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecords.test.tsx @@ -1,7 +1,4 @@ -import { MockedProvider } from '@apollo/client/testing'; import { act, renderHook } from '@testing-library/react'; -import { ReactNode } from 'react'; -import { RecoilRoot } from 'recoil'; import { query, @@ -9,6 +6,7 @@ import { variables, } from '@/object-record/hooks/__mocks__/useDeleteManyRecords'; import { useDeleteManyRecords } from '@/object-record/hooks/useDeleteManyRecords'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const people = [ 'a7286b9a-c039-4a89-9567-2dfa7953cda9', @@ -29,13 +27,9 @@ const mocks = [ }, ]; -const Wrapper = ({ children }: { children: ReactNode }) => ( - <RecoilRoot> - <MockedProvider mocks={mocks} addTypename={false}> - {children} - </MockedProvider> - </RecoilRoot> -); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); describe('useDeleteManyRecords', () => { it('works as expected', async () => { diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecordsMutation.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecordsMutation.test.tsx index 7f96b1be6114..cc5b85d6b664 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecordsMutation.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteManyRecordsMutation.test.tsx @@ -1,8 +1,8 @@ import { renderHook } from '@testing-library/react'; import { print } from 'graphql'; -import { RecoilRoot } from 'recoil'; import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const expectedQueryTemplate = ` mutation DeleteManyPeople($filter: PersonFilterInput!) { @@ -12,6 +12,10 @@ const expectedQueryTemplate = ` } `.replace(/\s/g, ''); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], +}); + describe('useDeleteManyRecordsMutation', () => { it('should return a valid deleteManyRecordsMutation', () => { const objectNameSingular = 'person'; @@ -22,7 +26,7 @@ describe('useDeleteManyRecordsMutation', () => { objectNameSingular, }), { - wrapper: RecoilRoot, + wrapper: Wrapper, }, ); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecord.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecord.test.tsx index 731a468a2835..0c347d309520 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecord.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecord.test.tsx @@ -1,7 +1,5 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; -import { act, renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; +import { renderHook } from '@testing-library/react'; +import { act } from 'react'; import { query, @@ -9,6 +7,7 @@ import { variables, } from '@/object-record/hooks/__mocks__/useDeleteOneRecord'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const personId = 'a7286b9a-c039-4a89-9567-2dfa7953cda9'; @@ -26,13 +25,9 @@ const mocks = [ }, ]; -const Wrapper = ({ children }: { children: ReactNode }) => ( - <RecoilRoot> - <MockedProvider mocks={mocks} addTypename={false}> - {children} - </MockedProvider> - </RecoilRoot> -); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); describe('useDeleteOneRecord', () => { it('works as expected', async () => { diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecordMutation.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecordMutation.test.tsx index 3bbc51f65a85..859355818ae8 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecordMutation.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useDeleteOneRecordMutation.test.tsx @@ -1,17 +1,23 @@ import { renderHook } from '@testing-library/react'; import { print } from 'graphql'; -import { RecoilRoot } from 'recoil'; import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const expectedQueryTemplate = ` mutation DeleteOnePerson($idToDelete: ID!) { deletePerson(id: $idToDelete) { + __typename + deletedAt id } } `.replace(/\s/g, ''); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], +}); + describe('useDeleteOneRecordMutation', () => { it('should return a valid deleteOneRecordMutation', () => { const objectNameSingular = 'person'; @@ -22,7 +28,7 @@ describe('useDeleteOneRecordMutation', () => { objectNameSingular, }), { - wrapper: RecoilRoot, + wrapper: Wrapper, }, ); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFetchAllRecordIds.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFetchAllRecordIds.test.tsx index 02095502e9ed..80b57d7dc5bd 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFetchAllRecordIds.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFetchAllRecordIds.test.tsx @@ -1,7 +1,6 @@ -import { MockedProvider } from '@apollo/client/testing'; import { act, renderHook } from '@testing-library/react'; -import { ReactNode, useEffect } from 'react'; -import { RecoilRoot, useRecoilState } from 'recoil'; +import { useEffect } from 'react'; +import { useRecoilState } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { @@ -16,8 +15,8 @@ import { variablesThirdRequest, } from '@/object-record/hooks/__mocks__/useFetchAllRecordIds'; import { useFetchAllRecordIds } from '@/object-record/hooks/useFetchAllRecordIds'; -import { SnackBarManagerScopeInternalContext } from '@/ui/feedback/snack-bar-manager/scopes/scope-internal-context/SnackBarManagerScopeInternalContext'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; const mocks = [ { @@ -52,22 +51,12 @@ const mocks = [ }, ]; +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); + describe('useFetchAllRecordIds', () => { it('fetches all record ids with fetch more synchronous loop', async () => { - const Wrapper = ({ children }: { children: ReactNode }) => ( - <RecoilRoot> - <SnackBarManagerScopeInternalContext.Provider - value={{ - scopeId: 'snack-bar-manager', - }} - > - <MockedProvider mocks={mocks} addTypename={false}> - {children} - </MockedProvider> - </SnackBarManagerScopeInternalContext.Provider> - </RecoilRoot> - ); - const { result } = renderHook( () => { const [, setObjectMetadataItems] = useRecoilState( diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindDuplicateRecords.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindDuplicateRecords.test.tsx index e8616d1da1a7..61cd950ff76f 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindDuplicateRecords.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindDuplicateRecords.test.tsx @@ -1,11 +1,8 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; import { renderHook, waitFor } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; import { useFindDuplicateRecords } from '@/object-record/hooks/useFindDuplicateRecords'; -import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { query, responseData, @@ -24,15 +21,9 @@ const mocks = [ }, ]; -const Wrapper = ({ children }: { children: ReactNode }) => ( - <RecoilRoot> - <SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager"> - <MockedProvider mocks={mocks} addTypename={false}> - {children} - </MockedProvider> - </SnackBarProviderScope> - </RecoilRoot> -); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); describe('useFindDuplicateRecords', () => { it('should fetch duplicate records and return the correct data', async () => { diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindDuplicateRecordsQuery.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindDuplicateRecordsQuery.test.tsx index e7d0ad73348f..10cb78a400d3 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindDuplicateRecordsQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindDuplicateRecordsQuery.test.tsx @@ -1,16 +1,16 @@ import { renderHook } from '@testing-library/react'; import { print } from 'graphql'; -import { RecoilRoot } from 'recoil'; -import { PERSON_FRAGMENT } from '@/object-record/hooks/__mocks__/personFragment'; +import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; import { useFindDuplicateRecordsQuery } from '@/object-record/hooks/useFindDuplicatesRecordsQuery'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const expectedQueryTemplate = ` query FindDuplicatePerson($ids: [ID!]!) { personDuplicates(ids: $ids) { edges { node { - ${PERSON_FRAGMENT} + ${PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS} } cursor } @@ -23,6 +23,10 @@ const expectedQueryTemplate = ` } `.replace(/\s/g, ''); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], +}); + describe('useFindDuplicateRecordsQuery', () => { it('should return a valid findDuplicateRecordsQuery', () => { const objectNameSingular = 'person'; @@ -33,7 +37,7 @@ describe('useFindDuplicateRecordsQuery', () => { objectNameSingular, }), { - wrapper: RecoilRoot, + wrapper: Wrapper, }, ); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx index d86769f1d219..5d85b80c98da 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx @@ -1,7 +1,5 @@ -import { MockedProvider } from '@apollo/client/testing'; import { renderHook } from '@testing-library/react'; -import { ReactNode } from 'react'; -import { RecoilRoot, useSetRecoilState } from 'recoil'; +import { useSetRecoilState } from 'recoil'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; @@ -11,8 +9,8 @@ import { variables, } from '@/object-record/hooks/__mocks__/useFindManyRecords'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; const mocks = [ { @@ -28,29 +26,10 @@ const mocks = [ }, ]; -const Wrapper = ({ children }: { children: ReactNode }) => ( - <RecoilRoot> - <SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager"> - <MockedProvider mocks={mocks} addTypename={false}> - {children} - </MockedProvider> - </SnackBarProviderScope> - </RecoilRoot> -); - +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); describe('useFindManyRecords', () => { - it('should skip fetch if currentWorkspaceMember is undefined', async () => { - const { result } = renderHook( - () => useFindManyRecords({ objectNameSingular: 'person' }), - { - wrapper: Wrapper, - }, - ); - - expect(result.current.loading).toBe(false); - expect(result.current.error).toBeUndefined(); - }); - it('should work as expected', async () => { const onCompleted = jest.fn(); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecordsQuery.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecordsQuery.test.tsx index 3d2213a589f4..0c7fd0afbeb2 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecordsQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecordsQuery.test.tsx @@ -1,16 +1,16 @@ import { renderHook } from '@testing-library/react'; import { print } from 'graphql'; -import { RecoilRoot } from 'recoil'; -import { PERSON_FRAGMENT } from '@/object-record/hooks/__mocks__/personFragment'; +import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; import { useFindManyRecordsQuery } from '@/object-record/hooks/useFindManyRecordsQuery'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const expectedQueryTemplate = ` query FindManyPeople($filter: PersonFilterInput, $orderBy: [PersonOrderByInput], $lastCursor: String, $limit: Int) { people(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor) { edges { node { - ${PERSON_FRAGMENT} + ${PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS} } cursor } @@ -25,6 +25,10 @@ const expectedQueryTemplate = ` } `.replace(/\s/g, ''); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], +}); + describe('useFindManyRecordsQuery', () => { it('should return a valid findManyRecordsQuery', () => { const objectNameSingular = 'person'; @@ -37,7 +41,7 @@ describe('useFindManyRecordsQuery', () => { computeReferences, }), { - wrapper: RecoilRoot, + wrapper: Wrapper, }, ); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindOneRecord.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindOneRecord.test.tsx index 62e11efe51dd..e7249d9caf4b 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindOneRecord.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindOneRecord.test.tsx @@ -1,15 +1,12 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; import { renderHook, waitFor } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; import { query, - responseData, variables, } from '@/object-record/hooks/__mocks__/useFindOneRecord'; import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; -import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; +import { generateEmptyJestRecordNode } from '~/testing/jest/generateEmptyJestRecordNode'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const mocks = [ { @@ -19,21 +16,19 @@ const mocks = [ }, result: jest.fn(() => ({ data: { - person: responseData, + person: generateEmptyJestRecordNode({ + objectNameSingular: 'person', + input: { id: '6205681e-7c11-40b4-9e32-f523dbe54590' }, + withDepthOneRelation: true, + }), }, })), }, ]; -const Wrapper = ({ children }: { children: ReactNode }) => ( - <RecoilRoot> - <SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager"> - <MockedProvider mocks={mocks} addTypename={false}> - {children} - </MockedProvider> - </SnackBarProviderScope> - </RecoilRoot> -); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); const objectRecordId = '6205681e-7c11-40b4-9e32-f523dbe54590'; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindOneRecordQuery.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindOneRecordQuery.test.tsx index 386e0d55f84f..32b2a169139d 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindOneRecordQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindOneRecordQuery.test.tsx @@ -1,18 +1,22 @@ import { renderHook } from '@testing-library/react'; import { print } from 'graphql'; -import { RecoilRoot } from 'recoil'; -import { PERSON_FRAGMENT } from '@/object-record/hooks/__mocks__/personFragment'; +import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; import { useFindOneRecordQuery } from '@/object-record/hooks/useFindOneRecordQuery'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const expectedQueryTemplate = ` query FindOnePerson($objectRecordId: ID!) { person(filter: { id: { eq: $objectRecordId } }) { - ${PERSON_FRAGMENT} + ${PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS} } } `.replace(/\s/g, ''); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], +}); + describe('useFindOneRecordQuery', () => { it('should return a valid findOneRecordQuery', () => { const objectNameSingular = 'person'; @@ -23,7 +27,7 @@ describe('useFindOneRecordQuery', () => { objectNameSingular, }), { - wrapper: RecoilRoot, + wrapper: Wrapper, }, ); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.test.tsx index feb33c81b30a..2cdf074ad1d5 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.test.tsx @@ -5,7 +5,7 @@ import { RecoilRoot } from 'recoil'; import { useGenerateCombinedFindManyRecordsQuery } from '@/object-record/multiple-objects/hooks/useGenerateCombinedFindManyRecordsQuery'; import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; const Wrapper = ({ children }: { children: ReactNode }) => ( <RecoilRoot> diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useLazyFindOneRecord.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useLazyFindOneRecord.test.tsx index 013889b934cd..77833552d8db 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useLazyFindOneRecord.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useLazyFindOneRecord.test.tsx @@ -1,7 +1,4 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; import { query, @@ -9,7 +6,7 @@ import { variables, } from '@/object-record/hooks/__mocks__/useFindOneRecord'; import { useLazyFindOneRecord } from '@/object-record/hooks/useLazyFindOneRecord'; -import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const mocks = [ { @@ -25,15 +22,9 @@ const mocks = [ }, ]; -const Wrapper = ({ children }: { children: ReactNode }) => ( - <RecoilRoot> - <SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager"> - <MockedProvider mocks={mocks} addTypename={false}> - {children} - </MockedProvider> - </SnackBarProviderScope> - </RecoilRoot> -); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); const objectRecordId = '6205681e-7c11-40b4-9e32-f523dbe54590'; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useObjectRecordTable.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useObjectRecordTable.test.tsx index 627991b7f41d..4cd4cdbffc11 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useObjectRecordTable.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useObjectRecordTable.test.tsx @@ -1,13 +1,12 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; import { expect } from '@storybook/test'; import { renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; +import { ReactNode } from 'react'; +import { mocks } from '@/auth/hooks/__mocks__/useAuth'; import { useLoadRecordIndexTable } from '@/object-record/record-index/hooks/useLoadRecordIndexTable'; import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope'; -import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/getScopeIdFromComponentId'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const recordTableId = 'people'; const objectNameSingular = 'person'; @@ -17,20 +16,22 @@ const ObjectNamePluralSetter = ({ children }: { children: ReactNode }) => { return <>{children}</>; }; +const HookMockWrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); + const Wrapper = ({ children }: { children: ReactNode }) => { return ( - <RecoilRoot> + <HookMockWrapper> <ObjectNamePluralSetter> <RecordTableScope recordTableScopeId={getScopeIdFromComponentId(recordTableId)} onColumnsChange={onColumnsChange} > - <SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager"> - <MockedProvider addTypename={false}>{children}</MockedProvider> - </SnackBarProviderScope> + {children} </RecordTableScope> </ObjectNamePluralSetter> - </RecoilRoot> + </HookMockWrapper> ); }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecord.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecord.test.tsx index eb6e7048d316..d32ef37508b1 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecord.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecord.test.tsx @@ -1,7 +1,4 @@ -import { MockedProvider } from '@apollo/client/testing'; import { act, renderHook } from '@testing-library/react'; -import { ReactNode } from 'react'; -import { RecoilRoot } from 'recoil'; import { query, @@ -9,6 +6,7 @@ import { variables, } from '@/object-record/hooks/__mocks__/useUpdateOneRecord'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const person = { id: '36abbb63-34ed-4a16-89f5-f549ac55d0f9' }; const update = { @@ -37,13 +35,9 @@ const mocks = [ }, ]; -const Wrapper = ({ children }: { children: ReactNode }) => ( - <RecoilRoot> - <MockedProvider mocks={mocks} addTypename={false}> - {children} - </MockedProvider> - </RecoilRoot> -); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); const idToUpdate = '36abbb63-34ed-4a16-89f5-f549ac55d0f9'; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecordMutation.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecordMutation.test.tsx index 7581e1612582..be862743e9b8 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecordMutation.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useUpdateOneRecordMutation.test.tsx @@ -1,18 +1,22 @@ import { renderHook } from '@testing-library/react'; import { print } from 'graphql'; -import { RecoilRoot } from 'recoil'; -import { PERSON_FRAGMENT } from '@/object-record/hooks/__mocks__/personFragment'; +import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; import { useUpdateOneRecordMutation } from '@/object-record/hooks/useUpdateOneRecordMutation'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { normalizeGQLQuery } from '~/utils/normalizeGQLQuery'; const expectedQueryTemplate = ` mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) { updatePerson(id: $idToUpdate, data: $input) { - ${PERSON_FRAGMENT} + ${PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS} } }`; +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], +}); + describe('useUpdateOneRecordMutation', () => { it('should return a valid createManyRecordsMutation', () => { const objectNameSingular = 'person'; @@ -23,7 +27,7 @@ describe('useUpdateOneRecordMutation', () => { objectNameSingular, }), { - wrapper: RecoilRoot, + wrapper: Wrapper, }, ); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts index b9313fae3ecc..017dad72de49 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateManyRecords.ts @@ -2,9 +2,11 @@ import { useApolloClient } from '@apollo/client'; import { v4 } from 'uuid'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; +import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache'; +import { deleteRecordFromCache } from '@/object-record/cache/utils/deleteRecordFromCache'; import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; @@ -67,7 +69,7 @@ export const useCreateManyRecords = < }, ); - const recordsCreatedInCache = []; + const recordsCreatedInCache: ObjectRecord[] = []; for (const recordToCreate of sanitizedCreateManyRecordsInput) { if (recordToCreate.id === null) { @@ -98,26 +100,46 @@ export const useCreateManyRecords = < objectMetadataItem.namePlural, ); - const createdObjects = await apolloClient.mutate({ - mutation: createManyRecordsMutation, - variables: { - data: sanitizedCreateManyRecordsInput, - upsert: upsert, - }, - update: (cache, { data }) => { - const records = data?.[mutationResponseField]; + const createdObjects = await apolloClient + .mutate({ + mutation: createManyRecordsMutation, + variables: { + data: sanitizedCreateManyRecordsInput, + upsert: upsert, + }, + update: (cache, { data }) => { + const records = data?.[mutationResponseField]; + + if (!records?.length || skipPostOptmisticEffect) return; - if (!records?.length || skipPostOptmisticEffect) return; + triggerCreateRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToCreate: records, + objectMetadataItems, + shouldMatchRootQueryFilter, + }); + }, + }) + .catch((error: Error) => { + recordsCreatedInCache.forEach((recordToDelete) => { + deleteRecordFromCache({ + objectMetadataItems, + objectMetadataItem, + cache: apolloClient.cache, + recordToDelete, + }); + }); - triggerCreateRecordsOptimisticEffect({ - cache, + triggerDeleteRecordsOptimisticEffect({ + cache: apolloClient.cache, objectMetadataItem, - recordsToCreate: records, + recordsToDelete: recordsCreatedInCache, objectMetadataItems, - shouldMatchRootQueryFilter, }); - }, - }); + + throw error; + }); return createdObjects.data?.[mutationResponseField] ?? []; }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts index f34ce0692f7a..2e9d79239094 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useCreateOneRecord.ts @@ -3,9 +3,11 @@ import { useState } from 'react'; import { v4 } from 'uuid'; import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; +import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useCreateOneRecordInCache } from '@/object-record/cache/hooks/useCreateOneRecordInCache'; +import { deleteRecordFromCache } from '@/object-record/cache/utils/deleteRecordFromCache'; import { getObjectTypename } from '@/object-record/cache/utils/getObjectTypename'; import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; @@ -85,27 +87,49 @@ export const useCreateOneRecord = < const mutationResponseField = getCreateOneRecordMutationResponseField(objectNameSingular); - const createdObject = await apolloClient.mutate({ - mutation: createOneRecordMutation, - variables: { - input: sanitizedInput, - }, - update: (cache, { data }) => { - const record = data?.[mutationResponseField]; - - if (!record || skipPostOptmisticEffect) return; + const createdObject = await apolloClient + .mutate({ + mutation: createOneRecordMutation, + variables: { + input: sanitizedInput, + }, + update: (cache, { data }) => { + const record = data?.[mutationResponseField]; + + if (!record || skipPostOptmisticEffect) return; + + triggerCreateRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToCreate: [record], + objectMetadataItems, + shouldMatchRootQueryFilter, + }); + + setLoading(false); + }, + }) + .catch((error: Error) => { + if (!recordCreatedInCache) { + throw error; + } + + deleteRecordFromCache({ + objectMetadataItems, + objectMetadataItem, + cache: apolloClient.cache, + recordToDelete: recordCreatedInCache, + }); - triggerCreateRecordsOptimisticEffect({ - cache, + triggerDeleteRecordsOptimisticEffect({ + cache: apolloClient.cache, objectMetadataItem, - recordsToCreate: [record], + recordsToDelete: [recordCreatedInCache], objectMetadataItems, - shouldMatchRootQueryFilter, }); - setLoading(false); - }, - }); + throw error; + }); return createdObject.data?.[mutationResponseField] ?? null; }; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts index 35a65a507765..38bd825d55de 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteManyRecords.ts @@ -1,10 +1,12 @@ import { useApolloClient } from '@apollo/client'; +import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { apiConfigState } from '@/client-config/states/apiConfigState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; +import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { DEFAULT_MUTATION_BATCH_SIZE } from '@/object-record/constants/DefaultMutationBatchSize'; import { useDeleteManyRecordsMutation } from '@/object-record/hooks/useDeleteManyRecordsMutation'; import { getDeleteManyRecordsMutationResponseField } from '@/object-record/utils/getDeleteManyRecordsMutationResponseField'; @@ -65,38 +67,74 @@ export const useDeleteManyRecords = ({ (batchIndex + 1) * mutationPageSize, ); - const deletedRecordsResponse = await apolloClient.mutate({ - mutation: deleteManyRecordsMutation, - variables: { - filter: { id: { in: batchIds } }, - }, - optimisticResponse: options?.skipOptimisticEffect - ? undefined - : { - [mutationResponseField]: batchIds.map((idToDelete) => ({ - __typename: capitalize(objectNameSingular), - id: idToDelete, + const deletedRecordsResponse = await apolloClient + .mutate({ + mutation: deleteManyRecordsMutation, + variables: { + filter: { id: { in: batchIds } }, + }, + optimisticResponse: options?.skipOptimisticEffect + ? undefined + : { + [mutationResponseField]: batchIds.map((idToDelete) => ({ + __typename: capitalize(objectNameSingular), + id: idToDelete, + })), + }, + update: options?.skipOptimisticEffect + ? undefined + : (cache, { data }) => { + const records = data?.[mutationResponseField]; + + if (!records?.length) return; + + const cachedRecords = records + .map((record) => getRecordFromCache(record.id, cache)) + .filter(isDefined); + + triggerDeleteRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToDelete: cachedRecords, + objectMetadataItems, + }); + }, + }) + .catch((error: Error) => { + const cachedRecords = batchIds.map((idToDelete) => + getRecordFromCache(idToDelete, apolloClient.cache), + ); + + cachedRecords.forEach((cachedRecord) => { + if (!cachedRecord) { + return; + } + + updateRecordFromCache({ + objectMetadataItems, + objectMetadataItem, + cache: apolloClient.cache, + record: { + ...cachedRecord, + deletedAt: null, + }, + }); + }); + + triggerCreateRecordsOptimisticEffect({ + cache: apolloClient.cache, + objectMetadataItem, + objectMetadataItems, + recordsToCreate: cachedRecords + .filter(isDefined) + .map((cachedRecord) => ({ + ...cachedRecord, + deletedAt: null, })), - }, - update: options?.skipOptimisticEffect - ? undefined - : (cache, { data }) => { - const records = data?.[mutationResponseField]; - - if (!records?.length) return; - - const cachedRecords = records - .map((record) => getRecordFromCache(record.id, cache)) - .filter(isDefined); - - triggerDeleteRecordsOptimisticEffect({ - cache, - objectMetadataItem, - recordsToDelete: cachedRecords, - objectMetadataItems, - }); - }, - }); + }); + + throw error; + }); const deletedRecordsForThisBatch = deletedRecordsResponse.data?.[mutationResponseField] ?? []; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts index bf7dcd778fd0..b39871ba2753 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecord.ts @@ -1,17 +1,18 @@ -import { useCallback } from 'react'; import { useApolloClient } from '@apollo/client'; +import { useCallback } from 'react'; +import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; +import { updateRecordFromCache } from '@/object-record/cache/utils/updateRecordFromCache'; import { useDeleteOneRecordMutation } from '@/object-record/hooks/useDeleteOneRecordMutation'; import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField'; import { capitalize } from '~/utils/string/capitalize'; type useDeleteOneRecordProps = { objectNameSingular: string; - refetchFindManyQuery?: boolean; }; export const useDeleteOneRecord = ({ @@ -38,32 +39,72 @@ export const useDeleteOneRecord = ({ const deleteOneRecord = useCallback( async (idToDelete: string) => { - const deletedRecord = await apolloClient.mutate({ - mutation: deleteOneRecordMutation, - variables: { idToDelete }, - optimisticResponse: { - [mutationResponseField]: { - __typename: capitalize(objectNameSingular), - id: idToDelete, + const currentTimestamp = new Date().toISOString(); + + const deletedRecord = await apolloClient + .mutate({ + mutation: deleteOneRecordMutation, + variables: { + idToDelete: idToDelete, }, - }, - update: (cache, { data }) => { - const record = data?.[mutationResponseField]; + optimisticResponse: { + [mutationResponseField]: { + __typename: capitalize(objectNameSingular), + id: idToDelete, + deletedAt: currentTimestamp, + }, + }, + update: (cache, { data }) => { + const record = data?.[mutationResponseField]; + + if (!record) return; + + const cachedRecord = getRecordFromCache(record.id, cache); - if (!record) return; + if (!cachedRecord) return; - const cachedRecord = getRecordFromCache(record.id, cache); + triggerDeleteRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToDelete: [cachedRecord], + objectMetadataItems, + }); + }, + }) + .catch((error: Error) => { + const cachedRecord = getRecordFromCache( + idToDelete, + apolloClient.cache, + ); + + if (!cachedRecord) { + throw error; + } - if (!cachedRecord) return; + updateRecordFromCache({ + objectMetadataItems, + objectMetadataItem, + cache: apolloClient.cache, + record: { + ...cachedRecord, + deletedAt: null, + }, + }); - triggerDeleteRecordsOptimisticEffect({ - cache, + triggerCreateRecordsOptimisticEffect({ + cache: apolloClient.cache, objectMetadataItem, - recordsToDelete: [cachedRecord], objectMetadataItems, + recordsToCreate: [ + { + ...cachedRecord, + deletedAt: null, + }, + ], }); - }, - }); + + throw error; + }); return deletedRecord.data?.[mutationResponseField] ?? null; }, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecordMutation.ts index ea8a3d458c77..ae7557bed7d5 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecordMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDeleteOneRecordMutation.ts @@ -1,6 +1,7 @@ import gql from 'graphql-tag'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { mapSoftDeleteFieldsToGraphQLQuery } from '@/object-metadata/utils/mapSoftDeleteFieldsToGraphQLQuery'; import { EMPTY_MUTATION } from '@/object-record/constants/EmptyMutation'; import { getDeleteOneRecordMutationResponseField } from '@/object-record/utils/getDeleteOneRecordMutationResponseField'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; @@ -26,12 +27,11 @@ export const useDeleteOneRecordMutation = ({ ); const deleteOneRecordMutation = gql` - mutation DeleteOne${capitalizedObjectName}($idToDelete: ID!) { - ${mutationResponseField}(id: $idToDelete) { - id - } - } - `; + mutation DeleteOne${capitalizedObjectName}($idToDelete: ID!) { + ${mutationResponseField}(id: $idToDelete) + ${mapSoftDeleteFieldsToGraphQLQuery(objectMetadataItem)} + } +`; return { deleteOneRecordMutation, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts index 08f4d092a135..3ba7283b9666 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDestroyManyRecords.ts @@ -1,5 +1,6 @@ import { useApolloClient } from '@apollo/client'; +import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { apiConfigState } from '@/client-config/states/apiConfigState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; @@ -65,38 +66,54 @@ export const useDestroyManyRecords = ({ (batchIndex + 1) * mutationPageSize, ); - const destroyedRecordsResponse = await apolloClient.mutate({ - mutation: destroyManyRecordsMutation, - variables: { - filter: { id: { in: batchIds } }, - }, - optimisticResponse: options?.skipOptimisticEffect - ? undefined - : { - [mutationResponseField]: batchIds.map((idToDestroy) => ({ - __typename: capitalize(objectNameSingular), - id: idToDestroy, - })), - }, - update: options?.skipOptimisticEffect - ? undefined - : (cache, { data }) => { - const records = data?.[mutationResponseField]; - - if (!records?.length) return; - - const cachedRecords = records - .map((record) => getRecordFromCache(record.id, cache)) - .filter(isDefined); - - triggerDeleteRecordsOptimisticEffect({ - cache, - objectMetadataItem, - recordsToDelete: cachedRecords, - objectMetadataItems, - }); - }, - }); + const originalRecords = idsToDestroy + .map((recordId) => getRecordFromCache(recordId, apolloClient.cache)) + .filter(isDefined); + + const destroyedRecordsResponse = await apolloClient + .mutate({ + mutation: destroyManyRecordsMutation, + variables: { + filter: { id: { in: batchIds } }, + }, + optimisticResponse: options?.skipOptimisticEffect + ? undefined + : { + [mutationResponseField]: batchIds.map((idToDestroy) => ({ + __typename: capitalize(objectNameSingular), + id: idToDestroy, + })), + }, + update: options?.skipOptimisticEffect + ? undefined + : (cache, { data }) => { + const records = data?.[mutationResponseField]; + + if (!records?.length) return; + + const cachedRecords = records + .map((record) => getRecordFromCache(record.id, cache)) + .filter(isDefined); + + triggerDeleteRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToDelete: cachedRecords, + objectMetadataItems, + }); + }, + }) + .catch((error: Error) => { + if (originalRecords.length > 0) { + triggerCreateRecordsOptimisticEffect({ + cache: apolloClient.cache, + objectMetadataItem, + recordsToCreate: originalRecords, + objectMetadataItems, + }); + } + throw error; + }); const destroyedRecordsForThisBatch = destroyedRecordsResponse.data?.[mutationResponseField] ?? []; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecord.ts index fc5d75d0a42f..91446a87262a 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useDestroyOneRecord.ts @@ -1,12 +1,15 @@ import { useApolloClient } from '@apollo/client'; import { useCallback } from 'react'; +import { triggerCreateRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerCreateRecordsOptimisticEffect'; import { triggerDeleteRecordsOptimisticEffect } from '@/apollo/optimistic-effect/utils/triggerDeleteRecordsOptimisticEffect'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { useGetRecordFromCache } from '@/object-record/cache/hooks/useGetRecordFromCache'; import { useDestroyOneRecordMutation } from '@/object-record/hooks/useDestroyOneRecordMutation'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getDestroyOneRecordMutationResponseField } from '@/object-record/utils/getDestroyOneRecordMutationResponseField'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { capitalize } from '~/utils/string/capitalize'; type useDestroyOneRecordProps = { @@ -38,32 +41,49 @@ export const useDestroyOneRecord = ({ const destroyOneRecord = useCallback( async (idToDestroy: string) => { - const deletedRecord = await apolloClient.mutate({ - mutation: destroyOneRecordMutation, - variables: { idToDestroy }, - optimisticResponse: { - [mutationResponseField]: { - __typename: capitalize(objectNameSingular), - id: idToDestroy, + const originalRecord: ObjectRecord | null = getRecordFromCache( + idToDestroy, + apolloClient.cache, + ); + + const deletedRecord = await apolloClient + .mutate({ + mutation: destroyOneRecordMutation, + variables: { idToDestroy }, + optimisticResponse: { + [mutationResponseField]: { + __typename: capitalize(objectNameSingular), + id: idToDestroy, + }, }, - }, - update: (cache, { data }) => { - const record = data?.[mutationResponseField]; + update: (cache, { data }) => { + const record = data?.[mutationResponseField]; - if (!record) return; + if (!record) return; - const cachedRecord = getRecordFromCache(record.id, cache); + const cachedRecord = getRecordFromCache(record.id, cache); - if (!cachedRecord) return; + if (!cachedRecord) return; - triggerDeleteRecordsOptimisticEffect({ - cache, - objectMetadataItem, - recordsToDelete: [cachedRecord], - objectMetadataItems, - }); - }, - }); + triggerDeleteRecordsOptimisticEffect({ + cache, + objectMetadataItem, + recordsToDelete: [cachedRecord], + objectMetadataItems, + }); + }, + }) + .catch((error: Error) => { + if (!isUndefinedOrNull(originalRecord)) { + triggerCreateRecordsOptimisticEffect({ + cache: apolloClient.cache, + objectMetadataItem, + recordsToCreate: [originalRecord], + objectMetadataItems, + }); + } + throw error; + }); return deletedRecord.data?.[mutationResponseField] ?? null; }, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts index bd75db4a75a4..7b73f918fd74 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindDuplicateRecords.ts @@ -1,5 +1,5 @@ -import { useMemo } from 'react'; import { useQuery } from '@apollo/client'; +import { useMemo } from 'react'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts index c5f22c9f640e..0dac0caa6d68 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts @@ -1,7 +1,5 @@ import { useQuery, WatchQueryFetchPolicy } from '@apollo/client'; -import { useRecoilValue } from 'recoil'; -import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; import { RecordGqlOperationFindManyResult } from '@/object-record/graphql/types/RecordGqlOperationFindManyResult'; @@ -36,7 +34,6 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({ onCompleted, cursorFilter, }: UseFindManyRecordsParams<T>) => { - const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, }); @@ -66,7 +63,7 @@ export const useFindManyRecords = <T extends ObjectRecord = ObjectRecord>({ const { data, loading, error, fetchMore } = useQuery<RecordGqlOperationFindManyResult>(findManyRecordsQuery, { - skip: skip || !objectMetadataItem || !currentWorkspaceMember, + skip: skip || !objectMetadataItem, variables: { filter, orderBy, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useRestoreManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useRestoreManyRecords.ts index 55bd5cc5e865..66af0949fef0 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useRestoreManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useRestoreManyRecords.ts @@ -62,22 +62,27 @@ export const useRestoreManyRecords = ({ objectMetadataItem.namePlural, )}`; - const restoredRecordsResponse = await apolloClient.mutate({ - mutation: restoreManyRecordsMutation, - refetchQueries: [findOneQueryName, findManyQueryName], - variables: { - filter: { id: { in: batchIds } }, - }, - optimisticResponse: options?.skipOptimisticEffect - ? undefined - : { - [mutationResponseField]: batchIds.map((idToRestore) => ({ - __typename: capitalize(objectNameSingular), - id: idToRestore, - deletedAt: null, - })), - }, - }); + const restoredRecordsResponse = await apolloClient + .mutate({ + mutation: restoreManyRecordsMutation, + refetchQueries: [findOneQueryName, findManyQueryName], + variables: { + filter: { id: { in: batchIds } }, + }, + optimisticResponse: options?.skipOptimisticEffect + ? undefined + : { + [mutationResponseField]: batchIds.map((idToRestore) => ({ + __typename: capitalize(objectNameSingular), + id: idToRestore, + deletedAt: null, + })), + }, + }) + .catch((error: Error) => { + // TODO: revert optimistic effect (once optimistic effect is fixed) + throw error; + }); const restoredRecordsForThisBatch = restoredRecordsResponse.data?.[mutationResponseField] ?? []; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useSearchRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useSearchRecords.ts new file mode 100644 index 000000000000..afedd99b80be --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useSearchRecords.ts @@ -0,0 +1,94 @@ +import { useQuery, WatchQueryFetchPolicy } from '@apollo/client'; +import { useRecoilValue } from 'recoil'; + +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { ObjectMetadataItemIdentifier } from '@/object-metadata/types/ObjectMetadataItemIdentifier'; +import { getRecordsFromRecordConnection } from '@/object-record/cache/utils/getRecordsFromRecordConnection'; +import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; +import { RecordGqlOperationSearchResult } from '@/object-record/graphql/types/RecordGqlOperationSearchResult'; +import { RecordGqlOperationVariables } from '@/object-record/graphql/types/RecordGqlOperationVariables'; +import { useSearchRecordsQuery } from '@/object-record/hooks/useSearchRecordsQuery'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { useMemo } from 'react'; +import { logError } from '~/utils/logError'; + +export type UseSearchRecordsParams = ObjectMetadataItemIdentifier & + RecordGqlOperationVariables & { + onError?: (error?: Error) => void; + skip?: boolean; + recordGqlFields?: RecordGqlOperationGqlRecordFields; + fetchPolicy?: WatchQueryFetchPolicy; + searchInput?: string; + }; + +export const useSearchRecords = <T extends ObjectRecord = ObjectRecord>({ + objectNameSingular, + searchInput, + limit, + skip, + recordGqlFields, + fetchPolicy, +}: UseSearchRecordsParams) => { + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + const { searchRecordsQuery } = useSearchRecordsQuery({ + objectNameSingular, + recordGqlFields, + }); + + const { enqueueSnackBar } = useSnackBar(); + + const { data, loading, error } = useQuery<RecordGqlOperationSearchResult>( + searchRecordsQuery, + { + skip: + skip || !objectMetadataItem || !currentWorkspaceMember || !searchInput, + variables: { + search: searchInput, + limit: limit, + }, + fetchPolicy: fetchPolicy, + onError: (error) => { + logError( + `useSearchRecords for "${objectMetadataItem.namePlural}" error : ` + + error, + ); + enqueueSnackBar( + `Error during useSearchRecords for "${objectMetadataItem.namePlural}", ${error.message}`, + { + variant: SnackBarVariant.Error, + }, + ); + }, + }, + ); + + const queryResponseField = getSearchRecordsQueryResponseField( + objectMetadataItem.namePlural, + ); + + const result = data?.[queryResponseField]; + + const records = useMemo( + () => + result + ? (getRecordsFromRecordConnection({ + recordConnection: result, + }) as T[]) + : [], + [result], + ); + + return { + objectMetadataItem, + records: records, + loading, + error, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useSearchRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useSearchRecordsQuery.ts new file mode 100644 index 000000000000..6cc3972caa9c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/hooks/useSearchRecordsQuery.ts @@ -0,0 +1,33 @@ +import { useRecoilValue } from 'recoil'; + +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; +import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; +import { generateSearchRecordsQuery } from '@/object-record/utils/generateSearchRecordsQuery'; + +export const useSearchRecordsQuery = ({ + objectNameSingular, + recordGqlFields, + computeReferences, +}: { + objectNameSingular: string; + recordGqlFields?: RecordGqlOperationGqlRecordFields; + computeReferences?: boolean; +}) => { + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular, + }); + + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + + const searchRecordsQuery = generateSearchRecordsQuery({ + objectMetadataItem, + objectMetadataItems, + recordGqlFields, + computeReferences, + }); + + return { + searchRecordsQuery, + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts index 427c48547172..c87cbf9246a9 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useUpdateOneRecord.ts @@ -108,26 +108,48 @@ export const useUpdateOneRecord = < const mutationResponseField = getUpdateOneRecordMutationResponseField(objectNameSingular); - const updatedRecord = await apolloClient.mutate({ - mutation: updateOneRecordMutation, - variables: { - idToUpdate, - input: sanitizedInput, - }, - update: (cache, { data }) => { - const record = data?.[mutationResponseField]; - - if (!record || !cachedRecord) return; + const updatedRecord = await apolloClient + .mutate({ + mutation: updateOneRecordMutation, + variables: { + idToUpdate, + input: sanitizedInput, + }, + update: (cache, { data }) => { + const record = data?.[mutationResponseField]; + + if (!record || !cachedRecord) return; + + triggerUpdateRecordOptimisticEffect({ + cache, + objectMetadataItem, + currentRecord: cachedRecord, + updatedRecord: record, + objectMetadataItems, + }); + }, + }) + .catch((error: Error) => { + if (!cachedRecord) { + throw error; + } + updateRecordFromCache({ + objectMetadataItems, + objectMetadataItem, + cache: apolloClient.cache, + record: cachedRecord, + }); triggerUpdateRecordOptimisticEffect({ - cache, + cache: apolloClient.cache, objectMetadataItem, - currentRecord: cachedRecord, - updatedRecord: record, + currentRecord: optimisticRecordWithConnection, + updatedRecord: cachedRecordWithConnection, objectMetadataItems, }); - }, - }); + + throw error; + }); return updatedRecord?.data?.[mutationResponseField] ?? null; }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton.tsx index 885a6c8eb840..b88f421c9857 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownButton.tsx @@ -2,6 +2,7 @@ import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdow import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; +import { useCallback } from 'react'; import { MultipleFiltersButton } from './MultipleFiltersButton'; import { MultipleFiltersDropdownContent } from './MultipleFiltersDropdownContent'; @@ -13,12 +14,18 @@ type MultipleFiltersDropdownButtonProps = { export const MultipleFiltersDropdownButton = ({ hotkeyScope, }: MultipleFiltersDropdownButtonProps) => { - const { resetFilter } = useFilterDropdown(); + const { resetFilter, setIsObjectFilterDropdownOperandSelectUnfolded } = + useFilterDropdown(); + + const handleDropdownClose = useCallback(() => { + resetFilter(); + setIsObjectFilterDropdownOperandSelectUnfolded(false); + }, [resetFilter, setIsObjectFilterDropdownOperandSelectUnfolded]); return ( <Dropdown dropdownId={OBJECT_FILTER_DROPDOWN_ID} - onClose={resetFilter} + onClose={handleDropdownClose} clickableComponent={<MultipleFiltersButton />} dropdownComponents={<MultipleFiltersDropdownContent />} dropdownHotkeyScope={hotkeyScope} diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx index ec3df12f0d1d..81ee172b3545 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx @@ -1,11 +1,15 @@ -import { useRecoilValue } from 'recoil'; - +import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput'; import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; -import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; -import { ObjectFilterDropdownRatingInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput'; +import { ObjectFilterDropdownRecordSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect'; +import { ObjectFilterDropdownSourceSelect } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect'; +import { ObjectFilterDropdownTextSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput'; +import { isActorSourceCompositeFilter } from '@/object-record/object-filter-dropdown/utils/isActorSourceCompositeFilter'; +import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; +import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; import { MultipleFiltersDropdownFilterOnFilterChangedEffect } from './MultipleFiltersDropdownFilterOnFilterChangedEffect'; import { ObjectFilterDropdownDateInput } from './ObjectFilterDropdownDateInput'; import { ObjectFilterDropdownFilterSelect } from './ObjectFilterDropdownFilterSelect'; @@ -13,8 +17,21 @@ import { ObjectFilterDropdownNumberInput } from './ObjectFilterDropdownNumberInp import { ObjectFilterDropdownOperandButton } from './ObjectFilterDropdownOperandButton'; import { ObjectFilterDropdownOperandSelect } from './ObjectFilterDropdownOperandSelect'; import { ObjectFilterDropdownOptionSelect } from './ObjectFilterDropdownOptionSelect'; -import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect'; -import { ObjectFilterDropdownTextSearchInput } from './ObjectFilterDropdownTextSearchInput'; + +const StyledContainer = styled.div` + position: relative; +`; + +const StyledOperandSelectContainer = styled.div` + background: ${({ theme }) => theme.background.secondary}; + box-shadow: ${({ theme }) => theme.boxShadow.light}; + border-radius: ${({ theme }) => theme.border.radius.md}; + left: 10px; + position: absolute; + top: 10px; + width: 100%; + z-index: 1000; +`; type MultipleFiltersDropdownContentProps = { filterDropdownId?: string; @@ -24,85 +41,104 @@ export const MultipleFiltersDropdownContent = ({ filterDropdownId, }: MultipleFiltersDropdownContentProps) => { const { - isObjectFilterDropdownOperandSelectUnfoldedState, filterDefinitionUsedInDropdownState, selectedOperandInDropdownState, + isObjectFilterDropdownOperandSelectUnfoldedState, } = useFilterDropdown({ filterDropdownId }); const isObjectFilterDropdownOperandSelectUnfolded = useRecoilValue( isObjectFilterDropdownOperandSelectUnfoldedState, ); + const filterDefinitionUsedInDropdown = useRecoilValue( filterDefinitionUsedInDropdownState, ); + const selectedOperandInDropdown = useRecoilValue( selectedOperandInDropdownState, ); - const isEmptyOperand = + + const isConfigurable = selectedOperandInDropdown && - [ViewFilterOperand.IsEmpty, ViewFilterOperand.IsNotEmpty].includes( - selectedOperandInDropdown, - ); + [ + ViewFilterOperand.Is, + ViewFilterOperand.IsNotNull, + ViewFilterOperand.IsNot, + ViewFilterOperand.LessThan, + ViewFilterOperand.GreaterThan, + ViewFilterOperand.IsBefore, + ViewFilterOperand.IsAfter, + ViewFilterOperand.Contains, + ViewFilterOperand.DoesNotContain, + ViewFilterOperand.IsRelative, + ].includes(selectedOperandInDropdown); return ( - <> + <StyledContainer> {!filterDefinitionUsedInDropdown ? ( <ObjectFilterDropdownFilterSelect /> - ) : isObjectFilterDropdownOperandSelectUnfolded ? ( - <ObjectFilterDropdownOperandSelect /> - ) : isEmptyOperand ? ( - <ObjectFilterDropdownOperandButton /> ) : ( - selectedOperandInDropdown && ( - <> - <ObjectFilterDropdownOperandButton /> - <DropdownMenuSeparator /> - {[ - 'TEXT', - 'EMAIL', - 'EMAILS', - 'PHONE', - 'FULL_NAME', - 'LINK', - 'LINKS', - 'ADDRESS', - 'ACTOR', - 'ARRAY', - 'PHONES', - ].includes(filterDefinitionUsedInDropdown.type) && ( - <ObjectFilterDropdownTextSearchInput /> - )} - {['NUMBER', 'CURRENCY'].includes( - filterDefinitionUsedInDropdown.type, - ) && <ObjectFilterDropdownNumberInput />} - {filterDefinitionUsedInDropdown.type === 'RATING' && ( - <ObjectFilterDropdownRatingInput /> - )} - {['DATE_TIME', 'DATE'].includes( - filterDefinitionUsedInDropdown.type, - ) && <ObjectFilterDropdownDateInput />} - {filterDefinitionUsedInDropdown.type === 'RELATION' && ( - <> - <ObjectFilterDropdownSearchInput /> - <DropdownMenuSeparator /> - <ObjectFilterDropdownRecordSelect /> - </> - )} - {filterDefinitionUsedInDropdown.type === 'SELECT' && ( - <> - <ObjectFilterDropdownSearchInput /> - <DropdownMenuSeparator /> - <ObjectFilterDropdownOptionSelect /> - </> - )} - </> - ) + <> + <ObjectFilterDropdownOperandButton /> + {isObjectFilterDropdownOperandSelectUnfolded && ( + <StyledOperandSelectContainer> + <ObjectFilterDropdownOperandSelect /> + </StyledOperandSelectContainer> + )} + {isConfigurable && selectedOperandInDropdown && ( + <> + {[ + 'TEXT', + 'EMAIL', + 'EMAILS', + 'PHONE', + 'FULL_NAME', + 'LINK', + 'LINKS', + 'ADDRESS', + 'ACTOR', + 'ARRAY', + 'PHONES', + ].includes(filterDefinitionUsedInDropdown.type) && + !isActorSourceCompositeFilter( + filterDefinitionUsedInDropdown, + ) && <ObjectFilterDropdownTextSearchInput />} + {['NUMBER', 'CURRENCY'].includes( + filterDefinitionUsedInDropdown.type, + ) && <ObjectFilterDropdownNumberInput />} + {filterDefinitionUsedInDropdown.type === 'RATING' && ( + <ObjectFilterDropdownRatingInput /> + )} + {['DATE_TIME', 'DATE'].includes( + filterDefinitionUsedInDropdown.type, + ) && <ObjectFilterDropdownDateInput />} + {filterDefinitionUsedInDropdown.type === 'RELATION' && ( + <> + <ObjectFilterDropdownSearchInput /> + <ObjectFilterDropdownRecordSelect /> + </> + )} + {isActorSourceCompositeFilter(filterDefinitionUsedInDropdown) && ( + <> + <DropdownMenuSeparator /> + <ObjectFilterDropdownSourceSelect /> + </> + )} + {filterDefinitionUsedInDropdown.type === 'SELECT' && ( + <> + <ObjectFilterDropdownSearchInput /> + <ObjectFilterDropdownOptionSelect /> + </> + )} + </> + )} + </> )} <MultipleFiltersDropdownFilterOnFilterChangedEffect filterDefinitionUsedInDropdownType={ filterDefinitionUsedInDropdown?.type } /> - </> + </StyledContainer> ); }; 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 20d1dee8389f..3961f28c836b 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 @@ -2,10 +2,19 @@ import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +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 { + VariableDateViewFilterValueDirection, + VariableDateViewFilterValueUnit, +} from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; +import { resolveFilterValue } from '@/views/utils/view-filter-value/resolveFilterValue'; import { useState } from 'react'; +import { isDefined } from 'twenty-ui'; import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { isDefined } from '~/utils/isDefined'; export const ObjectFilterDropdownDateInput = () => { const { @@ -23,28 +32,35 @@ export const ObjectFilterDropdownDateInput = () => { selectedOperandInDropdownState, ); - const selectedFilter = useRecoilValue(selectedFilterState); + const selectedFilter = useRecoilValue(selectedFilterState) as + | (Filter & { definition: { type: 'DATE' | 'DATE_TIME' } }) + | null + | undefined; + + const initialFilterValue = selectedFilter + ? resolveFilterValue(selectedFilter) + : null; const [internalDate, setInternalDate] = useState<Date | null>( - selectedFilter?.value ? new Date(selectedFilter.value) : new Date(), + initialFilterValue instanceof Date ? initialFilterValue : null, ); const isDateTimeInput = filterDefinitionUsedInDropdown?.type === FieldMetadataType.DateTime; - const handleChange = (date: Date | null) => { - setInternalDate(date); + const handleAbsoluteDateChange = (newDate: Date | null) => { + setInternalDate(newDate); if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) return; selectFilter?.({ id: selectedFilter?.id ? selectedFilter.id : v4(), fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId, - value: isDefined(date) ? date.toISOString() : '', + value: newDate?.toISOString() ?? '', operand: selectedOperandInDropdown, - displayValue: isDefined(date) + displayValue: isDefined(newDate) ? isDateTimeInput - ? date.toLocaleString() - : date.toLocaleDateString() + ? newDate.toLocaleString() + : newDate.toLocaleDateString() : '', definition: filterDefinitionUsedInDropdown, }); @@ -52,11 +68,56 @@ export const ObjectFilterDropdownDateInput = () => { setIsObjectFilterDropdownUnfolded(false); }; + const handleRelativeDateChange = ( + relativeDate: { + direction: VariableDateViewFilterValueDirection; + amount?: number; + unit: VariableDateViewFilterValueUnit; + } | null, + ) => { + if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) return; + + const value = relativeDate + ? computeVariableDateViewFilterValue( + relativeDate.direction, + relativeDate.amount, + relativeDate.unit, + ) + : ''; + + selectFilter?.({ + id: selectedFilter?.id ? selectedFilter.id : v4(), + fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId, + value, + operand: selectedOperandInDropdown, + displayValue: getRelativeDateDisplayValue(relativeDate), + definition: filterDefinitionUsedInDropdown, + }); + + setIsObjectFilterDropdownUnfolded(false); + }; + + const isRelativeOperand = + selectedOperandInDropdown === ViewFilterOperand.IsRelative; + + const resolvedValue = selectedFilter + ? resolveFilterValue(selectedFilter) + : null; + + const relativeDate = + resolvedValue && !(resolvedValue instanceof Date) + ? resolvedValue + : undefined; + return ( <InternalDatePicker + relativeDate={relativeDate} + highlightedDateRange={relativeDate} + isRelative={isRelativeOperand} date={internalDate} - onChange={handleChange} - onMouseSelect={handleChange} + onChange={handleAbsoluteDateChange} + onRelativeDateChange={handleRelativeDateChange} + onMouseSelect={handleAbsoluteDateChange} isDateTimeInput={isDateTimeInput} /> ); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx index 59ee04d92518..5eb45abae62b 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx @@ -3,16 +3,25 @@ import { useState } from 'react'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; -import { ObjectFilterDropdownFilterSelectMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem'; +import { ObjectFilterDropdownFilterSelectCompositeFieldSubMenu } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu'; import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; +import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter'; +import { CompositeFilterableFieldType } from '@/object-record/object-filter-dropdown/types/CompositeFilterableFieldType'; +import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; import { FiltersHotkeyScope } from '@/object-record/object-filter-dropdown/types/FiltersHotkeyScope'; +import { isCompositeField } from '@/object-record/object-filter-dropdown/utils/isCompositeField'; +import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; import { SelectableItem } from '@/ui/layout/selectable-list/components/SelectableItem'; import { SelectableList } from '@/ui/layout/selectable-list/components/SelectableList'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; -import { isDefined } from 'twenty-ui'; +import { useRecoilValue } from 'recoil'; +import { isDefined, useIcons } from 'twenty-ui'; +import { getOperandsForFilterDefinition } from '../utils/getOperandsForFilterType'; export const StyledInput = styled.input` background: transparent; @@ -41,7 +50,22 @@ export const StyledInput = styled.input` `; export const ObjectFilterDropdownFilterSelect = () => { - const [searchText, setSearchText] = useState(''); + const [subMenuFieldType, setSubMenuFieldType] = + useState<CompositeFilterableFieldType | null>(null); + + const [firstLevelFilterDefinition, setFirstLevelFilterDefinition] = + useState<FilterDefinition | null>(null); + + const { + setFilterDefinitionUsedInDropdown, + setSelectedOperandInDropdown, + setObjectFilterDropdownSearchInput, + objectFilterDropdownSearchInputState, + } = useFilterDropdown(); + + const objectFilterDropdownSearchInput = useRecoilValue( + objectFilterDropdownSearchInputState, + ); const availableFilterDefinitions = useRecoilComponentValueV2( availableFilterDefinitionsComponentState, @@ -50,7 +74,9 @@ export const ObjectFilterDropdownFilterSelect = () => { const sortedAvailableFilterDefinitions = [...availableFilterDefinitions] .sort((a, b) => a.label.localeCompare(b.label)) .filter((item) => - item.label.toLocaleLowerCase().includes(searchText.toLocaleLowerCase()), + item.label + .toLocaleLowerCase() + .includes(objectFilterDropdownSearchInput.toLocaleLowerCase()), ); const selectableListItemIds = sortedAvailableFilterDefinitions.map( @@ -75,37 +101,96 @@ export const ObjectFilterDropdownFilterSelect = () => { selectFilter({ filterDefinition: selectedFilterDefinition }); }; + const setHotkeyScope = useSetHotkeyScope(); + const { getIcon } = useIcons(); + + const handleSelectFilter = (availableFilterDefinition: FilterDefinition) => { + setFilterDefinitionUsedInDropdown(availableFilterDefinition); + + if ( + availableFilterDefinition.type === 'RELATION' || + availableFilterDefinition.type === 'SELECT' + ) { + setHotkeyScope(RelationPickerHotkeyScope.RelationPicker); + } + + setSelectedOperandInDropdown( + getOperandsForFilterDefinition(availableFilterDefinition)[0], + ); + + setObjectFilterDropdownSearchInput(''); + }; + + const handleSubMenuBack = () => { + setSubMenuFieldType(null); + setFirstLevelFilterDefinition(null); + }; + + const shouldShowFirstLevelMenu = !isDefined(subMenuFieldType); + return ( <> - <StyledInput - value={searchText} - autoFocus - placeholder="Search fields" - onChange={(event: React.ChangeEvent<HTMLInputElement>) => - setSearchText(event.target.value) - } - /> - <SelectableList - hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton} - selectableItemIdArray={selectableListItemIds} - selectableListId={OBJECT_FILTER_DROPDOWN_ID} - onEnter={handleEnter} - > - <DropdownMenuItemsContainer> - {sortedAvailableFilterDefinitions.map( - (availableFilterDefinition, index) => ( - <SelectableItem - itemId={availableFilterDefinition.fieldMetadataId} - > - <ObjectFilterDropdownFilterSelectMenuItem - key={`select-filter-${index}`} - filterDefinition={availableFilterDefinition} - /> - </SelectableItem> - ), - )} - </DropdownMenuItemsContainer> - </SelectableList> + {shouldShowFirstLevelMenu ? ( + <> + <StyledInput + value={objectFilterDropdownSearchInput} + autoFocus + placeholder="Search fields" + onChange={(event: React.ChangeEvent<HTMLInputElement>) => + setObjectFilterDropdownSearchInput(event.target.value) + } + /> + <SelectableList + hotkeyScope={FiltersHotkeyScope.ObjectFilterDropdownButton} + selectableItemIdArray={selectableListItemIds} + selectableListId={OBJECT_FILTER_DROPDOWN_ID} + onEnter={handleEnter} + > + <DropdownMenuItemsContainer> + {[...availableFilterDefinitions] + .sort((a, b) => a.label.localeCompare(b.label)) + .filter((item) => + item.label + .toLocaleLowerCase() + .includes( + objectFilterDropdownSearchInput.toLocaleLowerCase(), + ), + ) + .map((availableFilterDefinition, index) => ( + <SelectableItem + itemId={availableFilterDefinition.fieldMetadataId} + > + <MenuItem + key={`select-filter-${index}`} + testId={`select-filter-${index}`} + onClick={() => { + if (isCompositeField(availableFilterDefinition.type)) { + setSubMenuFieldType(availableFilterDefinition.type); + setFirstLevelFilterDefinition( + availableFilterDefinition, + ); + } else { + handleSelectFilter(availableFilterDefinition); + } + }} + LeftIcon={getIcon(availableFilterDefinition.iconName)} + text={availableFilterDefinition.label} + hasSubMenu={isCompositeField( + availableFilterDefinition.type, + )} + /> + </SelectableItem> + ))} + </DropdownMenuItemsContainer> + </SelectableList> + </> + ) : ( + <ObjectFilterDropdownFilterSelectCompositeFieldSubMenu + fieldType={subMenuFieldType} + firstLevelFieldDefinition={firstLevelFilterDefinition} + onBack={handleSubMenuBack} + /> + )} </> ); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx new file mode 100644 index 000000000000..a84046570924 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectCompositeFieldSubMenu.tsx @@ -0,0 +1,98 @@ +import { StyledInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect'; +import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { CompositeFilterableFieldType } from '@/object-record/object-filter-dropdown/types/CompositeFilterableFieldType'; +import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; +import { getCompositeSubFieldLabel } from '@/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel'; +import { getFilterableFieldTypeLabel } from '@/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel'; +import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType'; +import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; +import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; +import { useState } from 'react'; +import { IconApps, IconChevronLeft, useIcons } from 'twenty-ui'; + +type ObjectFilterDropdownFilterSelectCompositeFieldSubMenuProps = { + fieldType: CompositeFilterableFieldType; + firstLevelFieldDefinition: FilterDefinition | null; + onBack: () => void; +}; + +export const ObjectFilterDropdownFilterSelectCompositeFieldSubMenu = ({ + fieldType, + firstLevelFieldDefinition, + onBack, +}: ObjectFilterDropdownFilterSelectCompositeFieldSubMenuProps) => { + const [searchText, setSearchText] = useState(''); + + const { getIcon } = useIcons(); + + const { + setFilterDefinitionUsedInDropdown, + setSelectedOperandInDropdown, + setObjectFilterDropdownSearchInput, + } = useFilterDropdown(); + + const handleSelectFilter = (definition: FilterDefinition | null) => { + if (definition !== null) { + setFilterDefinitionUsedInDropdown(definition); + + setSelectedOperandInDropdown( + getOperandsForFilterDefinition(definition)[0], + ); + + setObjectFilterDropdownSearchInput(''); + } + }; + + const options = SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[ + fieldType + ].filterableSubFields + .sort((a, b) => a.localeCompare(b)) + .filter((item) => + item.toLocaleLowerCase().includes(searchText.toLocaleLowerCase()), + ); + + return ( + <> + <DropdownMenuHeader StartIcon={IconChevronLeft} onClick={onBack}> + {getFilterableFieldTypeLabel(fieldType)} + </DropdownMenuHeader> + <StyledInput + value={searchText} + autoFocus + placeholder="Search fields" + onChange={(event: React.ChangeEvent<HTMLInputElement>) => + setSearchText(event.target.value) + } + /> + <DropdownMenuItemsContainer> + <MenuItem + key={`select-filter-${-1}`} + testId={`select-filter-${-1}`} + onClick={() => { + handleSelectFilter(firstLevelFieldDefinition); + }} + LeftIcon={IconApps} + text={`Any ${getFilterableFieldTypeLabel(fieldType)} field`} + /> + {options.map((subFieldName, index) => ( + <MenuItem + key={`select-filter-${index}`} + testId={`select-filter-${index}`} + onClick={() => + firstLevelFieldDefinition && + handleSelectFilter({ + ...firstLevelFieldDefinition, + label: getCompositeSubFieldLabel(fieldType, subFieldName), + compositeFieldName: subFieldName, + }) + } + text={getCompositeSubFieldLabel(fieldType, subFieldName)} + LeftIcon={getIcon(firstLevelFieldDefinition?.iconName)} + /> + ))} + </DropdownMenuItemsContainer> + </> + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx index eb04750c9b8c..a1a12802fd1c 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelectMenuItem.tsx @@ -1,5 +1,6 @@ import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; import { useSelectFilter } from '@/object-record/object-filter-dropdown/hooks/useSelectFilter'; + import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; import { useSelectableList } from '@/ui/layout/selectable-list/hooks/useSelectableList'; import { MenuItemSelect } from '@/ui/navigation/menu-item/components/MenuItemSelect'; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandButton.tsx index 4aa675ec3539..3931c76547e1 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandButton.tsx @@ -10,19 +10,11 @@ export const ObjectFilterDropdownOperandButton = () => { const { selectedOperandInDropdownState, setIsObjectFilterDropdownOperandSelectUnfolded, - isObjectFilterDropdownOperandSelectUnfoldedState, } = useFilterDropdown(); const selectedOperandInDropdown = useRecoilValue( selectedOperandInDropdownState, ); - const isObjectFilterDropdownOperandSelectUnfolded = useRecoilValue( - isObjectFilterDropdownOperandSelectUnfoldedState, - ); - - if (isObjectFilterDropdownOperandSelectUnfolded) { - return null; - } return ( <DropdownMenuHeader diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx index 5f500b916461..b33fcbc162e3 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx @@ -2,14 +2,14 @@ import { useRecoilValue } from 'recoil'; import { v4 } from 'uuid'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; -import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { isDefined } from '~/utils/isDefined'; +import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue'; import { getOperandLabel } from '../utils/getOperandLabel'; -import { getOperandsForFilterType } from '../utils/getOperandsForFilterType'; +import { getOperandsForFilterDefinition } from '../utils/getOperandsForFilterType'; export const ObjectFilterDropdownOperandSelect = () => { const { @@ -31,27 +31,30 @@ export const ObjectFilterDropdownOperandSelect = () => { const selectedFilter = useRecoilValue(selectedFilterState); - const operandsForFilterType = getOperandsForFilterType( - filterDefinitionUsedInDropdown?.type, - ); + const operandsForFilterType = isDefined(filterDefinitionUsedInDropdown) + ? getOperandsForFilterDefinition(filterDefinitionUsedInDropdown) + : []; const handleOperandChange = (newOperand: ViewFilterOperand) => { - const isEmptyOperand = [ + const isValuelessOperand = [ ViewFilterOperand.IsEmpty, ViewFilterOperand.IsNotEmpty, + ViewFilterOperand.IsInPast, + ViewFilterOperand.IsInFuture, + ViewFilterOperand.IsToday, ].includes(newOperand); setSelectedOperandInDropdown(newOperand); setIsObjectFilterDropdownOperandSelectUnfolded(false); - if (isEmptyOperand) { + if (isValuelessOperand && isDefined(filterDefinitionUsedInDropdown)) { selectFilter?.({ id: v4(), fieldMetadataId: filterDefinitionUsedInDropdown?.fieldMetadataId ?? '', displayValue: '', operand: newOperand, value: '', - definition: filterDefinitionUsedInDropdown as FilterDefinition, + definition: filterDefinitionUsedInDropdown, }); return; } @@ -60,12 +63,19 @@ export const ObjectFilterDropdownOperandSelect = () => { isDefined(filterDefinitionUsedInDropdown) && isDefined(selectedFilter) ) { + const { value, displayValue } = getInitialFilterValue( + filterDefinitionUsedInDropdown.type, + newOperand, + selectedFilter.value, + selectedFilter.displayValue, + ); + selectFilter?.({ id: selectedFilter.id ? selectedFilter.id : v4(), fieldMetadataId: selectedFilter.fieldMetadataId, - displayValue: selectedFilter.displayValue, + displayValue, operand: newOperand, - value: selectedFilter.value, + value, definition: filterDefinitionUsedInDropdown, }); } diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx index ddaaf2e6ad92..c55496ee61d3 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx @@ -4,9 +4,9 @@ import { v4 } from 'uuid'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; -import { MultipleRecordSelectDropdown } from '@/object-record/select/components/MultipleRecordSelectDropdown'; +import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown'; import { useRecordsForSelect } from '@/object-record/select/hooks/useRecordsForSelect'; -import { SelectableRecord } from '@/object-record/select/types/SelectableRecord'; +import { SelectableItem } from '@/object-record/select/types/SelectableItem'; import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters'; import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { isDefined } from '~/utils/isDefined'; @@ -66,7 +66,7 @@ export const ObjectFilterDropdownRecordSelect = ({ }); const handleMultipleRecordSelectChange = ( - recordToSelect: SelectableRecord, + recordToSelect: SelectableItem, newSelectedValue: boolean, ) => { if (loading) { @@ -134,15 +134,15 @@ export const ObjectFilterDropdownRecordSelect = ({ }; return ( - <MultipleRecordSelectDropdown + <MultipleSelectDropdown selectableListId="object-filter-record-select-id" hotkeyScope={RelationPickerHotkeyScope.RelationPicker} - recordsToSelect={recordsToSelect} - filteredSelectedRecords={filteredSelectedRecords} - selectedRecords={selectedRecords} + itemsToSelect={recordsToSelect} + filteredSelectedItems={filteredSelectedRecords} + selectedItems={selectedRecords} onChange={handleMultipleRecordSelectChange} searchFilter={objectFilterDropdownSearchInput} - loadingRecords={loading} + loadingItems={loading} /> ); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx new file mode 100644 index 000000000000..153abd72b506 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSourceSelect.tsx @@ -0,0 +1,137 @@ +import { useState } from 'react'; +import { useRecoilValue } from 'recoil'; +import { v4 } from 'uuid'; + +import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { getActorSourceMultiSelectOptions } from '@/object-record/object-filter-dropdown/utils/getActorSourceMultiSelectOptions'; +import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; +import { MultipleSelectDropdown } from '@/object-record/select/components/MultipleSelectDropdown'; +import { SelectableItem } from '@/object-record/select/types/SelectableItem'; +import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters'; +import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { isDefined } from '~/utils/isDefined'; + +export const EMPTY_FILTER_VALUE = '[]'; +export const MAX_ITEMS_TO_DISPLAY = 3; + +type ObjectFilterDropdownSourceSelectProps = { + viewComponentId?: string; +}; + +export const ObjectFilterDropdownSourceSelect = ({ + viewComponentId, +}: ObjectFilterDropdownSourceSelectProps) => { + const { + filterDefinitionUsedInDropdownState, + objectFilterDropdownSearchInputState, + selectedOperandInDropdownState, + selectedFilterState, + setObjectFilterDropdownSelectedRecordIds, + objectFilterDropdownSelectedRecordIdsState, + selectFilter, + emptyFilterButKeepDefinition, + } = useFilterDropdown(); + + const { deleteCombinedViewFilter } = + useDeleteCombinedViewFilters(viewComponentId); + + const { currentViewWithCombinedFiltersAndSorts } = + useGetCurrentView(viewComponentId); + + const filterDefinitionUsedInDropdown = useRecoilValue( + filterDefinitionUsedInDropdownState, + ); + const objectFilterDropdownSearchInput = useRecoilValue( + objectFilterDropdownSearchInputState, + ); + const selectedOperandInDropdown = useRecoilValue( + selectedOperandInDropdownState, + ); + const objectFilterDropdownSelectedRecordIds = useRecoilValue( + objectFilterDropdownSelectedRecordIdsState, + ); + const [fieldId] = useState(v4()); + + const selectedFilter = useRecoilValue(selectedFilterState); + + const sourceTypes = getActorSourceMultiSelectOptions( + objectFilterDropdownSelectedRecordIds, + ); + + const filteredSelectedItems = sourceTypes.filter((option) => + objectFilterDropdownSelectedRecordIds.includes(option.id), + ); + + const handleMultipleItemSelectChange = ( + itemToSelect: SelectableItem, + newSelectedValue: boolean, + ) => { + const newSelectedItemIds = newSelectedValue + ? [...objectFilterDropdownSelectedRecordIds, itemToSelect.id] + : objectFilterDropdownSelectedRecordIds.filter( + (id) => id !== itemToSelect.id, + ); + + if (newSelectedItemIds.length === 0) { + emptyFilterButKeepDefinition(); + deleteCombinedViewFilter(fieldId); + return; + } + + setObjectFilterDropdownSelectedRecordIds(newSelectedItemIds); + + const selectedItemNames = sourceTypes + .filter((option) => newSelectedItemIds.includes(option.id)) + .map((option) => option.name); + + const filterDisplayValue = + selectedItemNames.length > MAX_ITEMS_TO_DISPLAY + ? `${selectedItemNames.length} source types` + : selectedItemNames.join(', '); + + if ( + isDefined(filterDefinitionUsedInDropdown) && + isDefined(selectedOperandInDropdown) + ) { + const newFilterValue = + newSelectedItemIds.length > 0 + ? JSON.stringify(newSelectedItemIds) + : EMPTY_FILTER_VALUE; + + const viewFilter = + currentViewWithCombinedFiltersAndSorts?.viewFilters.find( + (viewFilter) => + viewFilter.fieldMetadataId === + filterDefinitionUsedInDropdown.fieldMetadataId, + ); + + const filterId = viewFilter?.id ?? fieldId; + + selectFilter({ + id: selectedFilter?.id ? selectedFilter.id : filterId, + definition: filterDefinitionUsedInDropdown, + operand: selectedOperandInDropdown || ViewFilterOperand.Is, + displayValue: filterDisplayValue, + fieldMetadataId: filterDefinitionUsedInDropdown.fieldMetadataId, + value: newFilterValue, + }); + } + }; + + return ( + <MultipleSelectDropdown + selectableListId="object-filter-source-select-id" + hotkeyScope={RelationPickerHotkeyScope.RelationPicker} + itemsToSelect={sourceTypes.filter( + (item) => + !filteredSelectedItems.some((selected) => selected.id === item.id), + )} + filteredSelectedItems={filteredSelectedItems} + selectedItems={filteredSelectedItems} + onChange={handleMultipleItemSelectChange} + searchFilter={objectFilterDropdownSearchInput} + loadingItems={false} + /> + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx index ddfb5125b9dc..571645a5c81e 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx @@ -13,7 +13,7 @@ import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; -import { getOperandsForFilterType } from '../utils/getOperandsForFilterType'; +import { getOperandsForFilterDefinition } from '../utils/getOperandsForFilterType'; import { GenericEntityFilterChip } from './GenericEntityFilterChip'; import { ObjectFilterDropdownRecordSelect } from './ObjectFilterDropdownRecordSelect'; import { ObjectFilterDropdownSearchInput } from './ObjectFilterDropdownSearchInput'; @@ -36,14 +36,16 @@ export const SingleEntityObjectFilterDropdownButton = ({ ); const selectedFilter = useRecoilValue(selectedFilterState); - const availableFilter = availableFilterDefinitions[0]; + const availableFilterDefinition = availableFilterDefinitions[0]; React.useEffect(() => { - setFilterDefinitionUsedInDropdown(availableFilter); - const defaultOperand = getOperandsForFilterType(availableFilter?.type)[0]; + setFilterDefinitionUsedInDropdown(availableFilterDefinition); + const defaultOperand = getOperandsForFilterDefinition( + availableFilterDefinition, + )[0]; setSelectedOperandInDropdown(defaultOperand); }, [ - availableFilter, + availableFilterDefinition, setFilterDefinitionUsedInDropdown, setSelectedOperandInDropdown, ]); @@ -62,7 +64,7 @@ export const SingleEntityObjectFilterDropdownButton = ({ filter={selectedFilter} Icon={ selectedFilter.operand === ViewFilterOperand.IsNotNull - ? availableFilter.SelectAllIcon + ? availableFilterDefinition.SelectAllIcon : undefined } /> diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilter.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilter.ts index 3954d8d6dc9f..c716971dc010 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilter.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useSelectFilter.ts @@ -1,8 +1,10 @@ import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; -import { getOperandsForFilterType } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType'; +import { getInitialFilterValue } from '@/object-record/object-filter-dropdown/utils/getInitialFilterValue'; +import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType'; import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope'; +import { v4 } from 'uuid'; type SelectFilterParams = { filterDefinition: FilterDefinition; @@ -13,6 +15,7 @@ export const useSelectFilter = () => { setFilterDefinitionUsedInDropdown, setSelectedOperandInDropdown, setObjectFilterDropdownSearchInput, + selectFilter: filterDropdownSelectFilter, } = useFilterDropdown(); const setHotkeyScope = useSetHotkeyScope(); @@ -28,9 +31,25 @@ export const useSelectFilter = () => { } setSelectedOperandInDropdown( - getOperandsForFilterType(filterDefinition.type)?.[0], + getOperandsForFilterDefinition(filterDefinition)[0], ); + const { value, displayValue } = getInitialFilterValue( + filterDefinition.type, + getOperandsForFilterDefinition(filterDefinition)[0], + ); + + if (value !== '') { + filterDropdownSelectFilter({ + id: v4(), + fieldMetadataId: filterDefinition.fieldMetadataId, + displayValue, + operand: getOperandsForFilterDefinition(filterDefinition)[0], + value, + definition: filterDefinition, + }); + } + setObjectFilterDropdownSearchInput(''); }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/CompositeFilterableFieldType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/CompositeFilterableFieldType.ts new file mode 100644 index 000000000000..0bdf399feb6b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/CompositeFilterableFieldType.ts @@ -0,0 +1,5 @@ +import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType'; +import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType'; + +export type CompositeFilterableFieldType = FilterableFieldType & + CompositeFieldType; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/Filter.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/Filter.ts index 52ed99ac5531..4d2eddb8756e 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/Filter.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/Filter.ts @@ -1,5 +1,4 @@ import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; - import { FilterDefinition } from './FilterDefinition'; export type Filter = { diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterDefinition.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterDefinition.ts index 954562f4fed3..b516bbeced7f 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterDefinition.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterDefinition.ts @@ -1,14 +1,15 @@ import { IconComponent } from 'twenty-ui'; -import { FilterType } from './FilterType'; +import { FilterableFieldType } from './FilterableFieldType'; export type FilterDefinition = { fieldMetadataId: string; label: string; iconName: string; - type: FilterType; + type: FilterableFieldType; relationObjectMetadataNamePlural?: string; relationObjectMetadataNameSingular?: string; selectAllLabel?: string; SelectAllIcon?: IconComponent; + compositeFieldName?: string; }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterableFieldType.ts similarity index 55% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterType.ts rename to packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterableFieldType.ts index 1803a88a54e4..833ebbd3a24f 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterType.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/types/FilterableFieldType.ts @@ -1,4 +1,8 @@ -export type FilterType = +import { FieldType } from '@/settings/data-model/types/FieldType'; +import { PickLiteral } from '~/types/PickLiteral'; + +export type FilterableFieldType = PickLiteral< + FieldType, | 'TEXT' | 'PHONE' | 'PHONES' @@ -17,4 +21,5 @@ export type FilterType = | 'RATING' | 'MULTI_SELECT' | 'ACTOR' - | 'ARRAY'; + | 'ARRAY' +>; 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.tsx index 7b4b9516e7f4..66ea29aa06cc 100644 --- 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.tsx @@ -1,7 +1,8 @@ -import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; -import { getOperandsForFilterType } from '../getOperandsForFilterType'; +import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType'; +import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; +import { getOperandsForFilterDefinition } from '../getOperandsForFilterType'; describe('getOperandsForFilterType', () => { const emptyOperands = [ @@ -19,6 +20,16 @@ describe('getOperandsForFilterType', () => { ViewFilterOperand.LessThan, ]; + const dateOperands = [ + ViewFilterOperand.Is, + ViewFilterOperand.IsRelative, + ViewFilterOperand.IsInPast, + ViewFilterOperand.IsInFuture, + ViewFilterOperand.IsToday, + ViewFilterOperand.IsBefore, + ViewFilterOperand.IsAfter, + ]; + const relationOperand = [ViewFilterOperand.Is, ViewFilterOperand.IsNot]; const testCases = [ @@ -31,7 +42,8 @@ describe('getOperandsForFilterType', () => { ['ACTOR', [...containsOperands, ...emptyOperands]], ['CURRENCY', [...numberOperands, ...emptyOperands]], ['NUMBER', [...numberOperands, ...emptyOperands]], - ['DATE_TIME', [...numberOperands, ...emptyOperands]], + ['DATE', [...dateOperands, ...emptyOperands]], + ['DATE_TIME', [...dateOperands, ...emptyOperands]], ['RELATION', [...relationOperand, ...emptyOperands]], [undefined, []], [null, []], @@ -40,7 +52,9 @@ describe('getOperandsForFilterType', () => { testCases.forEach(([filterType, expectedOperands]) => { it(`should return correct operands for FilterType.${filterType}`, () => { - const result = getOperandsForFilterType(filterType as FilterType); + const result = getOperandsForFilterDefinition({ + type: filterType as FilterableFieldType, + } as FilterDefinition); expect(result).toEqual(expectedOperands); }); }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getActorSourceMultiSelectOptions.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getActorSourceMultiSelectOptions.ts new file mode 100644 index 000000000000..b116a6fdb9a8 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getActorSourceMultiSelectOptions.ts @@ -0,0 +1,64 @@ +import { SelectableItem } from '@/object-record/select/types/SelectableItem'; +import { + IconApi, + IconCsv, + IconGmail, + IconGoogleCalendar, + IconRobot, + IconSettingsAutomation, + IconUserCircle, +} from 'twenty-ui'; + +export const getActorSourceMultiSelectOptions = ( + selectedSourceNames: string[], +): SelectableItem[] => { + return [ + { + id: 'MANUAL', + name: 'User', + isSelected: selectedSourceNames.includes('MANUAL'), + AvatarIcon: IconUserCircle, + isIconInverted: true, + }, + { + id: 'IMPORT', + name: 'Import', + isSelected: selectedSourceNames.includes('IMPORT'), + AvatarIcon: IconCsv, + isIconInverted: true, + }, + { + id: 'API', + name: 'Api', + isSelected: selectedSourceNames.includes('API'), + AvatarIcon: IconApi, + isIconInverted: true, + }, + { + id: 'EMAIL', + name: 'Email', + isSelected: selectedSourceNames.includes('EMAIL'), + AvatarIcon: IconGmail, + }, + { + id: 'CALENDAR', + name: 'Calendar', + isSelected: selectedSourceNames.includes('CALENDAR'), + AvatarIcon: IconGoogleCalendar, + }, + { + id: 'WORKFLOW', + name: 'Workflow', + isSelected: selectedSourceNames.includes('WORKFLOW'), + AvatarIcon: IconSettingsAutomation, + isIconInverted: true, + }, + { + id: 'SYSTEM', + name: 'System', + isSelected: selectedSourceNames.includes('SYSTEM'), + AvatarIcon: IconRobot, + isIconInverted: true, + }, + ]; +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel.ts new file mode 100644 index 000000000000..36c6f04a46c9 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getCompositeSubFieldLabel.ts @@ -0,0 +1,12 @@ +import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; +import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType'; + +export const getCompositeSubFieldLabel = ( + compositeFieldType: CompositeFieldType, + subFieldName: (typeof SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS)[CompositeFieldType]['subFields'][number], +): string => { + return ( + SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS[compositeFieldType] + .labelBySubField as any + )[subFieldName]; +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel.ts new file mode 100644 index 000000000000..69f9334e0cf2 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getFilterableFieldTypeLabel.ts @@ -0,0 +1,8 @@ +import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType'; +import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs'; + +export const getFilterableFieldTypeLabel = ( + filterableFieldType: FilterableFieldType, +) => { + return SETTINGS_FIELD_TYPE_CONFIGS[filterableFieldType].label; +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getInitialFilterValue.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getInitialFilterValue.ts new file mode 100644 index 000000000000..12c9ecb74d7d --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getInitialFilterValue.ts @@ -0,0 +1,43 @@ +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 { z } from 'zod'; + +export const getInitialFilterValue = ( + newType: FilterableFieldType, + newOperand: ViewFilterOperand, + oldValue?: string, + oldDisplayValue?: string, +): Pick<Filter, 'value' | 'displayValue'> | Record<string, never> => { + switch (newType) { + case 'DATE': + case 'DATE_TIME': { + const activeDatePickerOperands = [ + ViewFilterOperand.IsBefore, + ViewFilterOperand.Is, + ViewFilterOperand.IsAfter, + ]; + + if (activeDatePickerOperands.includes(newOperand)) { + const date = z.coerce.date().safeParse(oldValue).data ?? new Date(); + const value = date.toISOString(); + const displayValue = + newType === 'DATE' + ? date.toLocaleString() + : date.toLocaleDateString(); + + return { value, displayValue }; + } + + if (newOperand === ViewFilterOperand.IsRelative) { + return { value: '', displayValue: '' }; + } + break; + } + } + + return { + value: oldValue ?? '', + displayValue: oldDisplayValue ?? '', + }; +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandLabel.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandLabel.ts index 9c9e297ef960..b68049b51750 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandLabel.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandLabel.ts @@ -12,6 +12,10 @@ export const getOperandLabel = ( return 'Greater than'; case ViewFilterOperand.LessThan: return 'Less than'; + case ViewFilterOperand.IsBefore: + return 'Is before'; + case ViewFilterOperand.IsAfter: + return 'Is after'; case ViewFilterOperand.Is: return 'Is'; case ViewFilterOperand.IsNot: @@ -22,6 +26,14 @@ export const getOperandLabel = ( return 'Is empty'; case ViewFilterOperand.IsNotEmpty: return 'Is not empty'; + case ViewFilterOperand.IsRelative: + return 'Is relative'; + case ViewFilterOperand.IsInPast: + return 'Is in past'; + case ViewFilterOperand.IsInFuture: + return 'Is in future'; + case ViewFilterOperand.IsToday: + return 'Is today'; default: return ''; } @@ -47,6 +59,16 @@ export const getOperandLabelShort = ( return '\u00A0> '; case ViewFilterOperand.LessThan: return '\u00A0< '; + case ViewFilterOperand.IsBefore: + return '\u00A0< '; + case ViewFilterOperand.IsAfter: + return '\u00A0> '; + case ViewFilterOperand.IsInPast: + return ': Past'; + case ViewFilterOperand.IsInFuture: + return ': Future'; + case ViewFilterOperand.IsToday: + return ': Today'; default: return ': '; } diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts index f75dca40f76c..ba263c173c4c 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getOperandsForFilterType.ts @@ -1,9 +1,9 @@ +import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; +import { isActorSourceCompositeFilter } from '@/object-record/object-filter-dropdown/utils/isActorSourceCompositeFilter'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; -import { FilterType } from '../types/FilterType'; - -export const getOperandsForFilterType = ( - filterType: FilterType | null | undefined, +export const getOperandsForFilterDefinition = ( + filterDefinition: FilterDefinition, ): ViewFilterOperand[] => { const emptyOperands = [ ViewFilterOperand.IsEmpty, @@ -12,7 +12,7 @@ export const getOperandsForFilterType = ( const relationOperands = [ViewFilterOperand.Is, ViewFilterOperand.IsNot]; - switch (filterType) { + switch (filterDefinition.type) { case 'TEXT': case 'EMAIL': case 'EMAILS': @@ -21,7 +21,6 @@ export const getOperandsForFilterType = ( case 'PHONE': case 'LINK': case 'LINKS': - case 'ACTOR': case 'ARRAY': case 'PHONES': return [ @@ -31,13 +30,23 @@ export const getOperandsForFilterType = ( ]; case 'CURRENCY': case 'NUMBER': - case 'DATE_TIME': - case 'DATE': return [ ViewFilterOperand.GreaterThan, ViewFilterOperand.LessThan, ...emptyOperands, ]; + case 'DATE_TIME': + case 'DATE': + return [ + ViewFilterOperand.Is, + ViewFilterOperand.IsRelative, + ViewFilterOperand.IsInPast, + ViewFilterOperand.IsInFuture, + ViewFilterOperand.IsToday, + ViewFilterOperand.IsBefore, + ViewFilterOperand.IsAfter, + ...emptyOperands, + ]; case 'RATING': return [ ViewFilterOperand.Is, @@ -49,6 +58,21 @@ export const getOperandsForFilterType = ( return [...relationOperands, ...emptyOperands]; case 'SELECT': return [...relationOperands]; + case 'ACTOR': { + if (isActorSourceCompositeFilter(filterDefinition)) { + return [ + ViewFilterOperand.Is, + ViewFilterOperand.IsNot, + ...emptyOperands, + ]; + } + + return [ + ViewFilterOperand.Contains, + ViewFilterOperand.DoesNotContain, + ...emptyOperands, + ]; + } default: return []; } 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 new file mode 100644 index 000000000000..fb59e540180b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getRelativeDateDisplayValue.ts @@ -0,0 +1,28 @@ +import { + VariableDateViewFilterValueDirection, + VariableDateViewFilterValueUnit, +} from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; +import { plural } from 'pluralize'; +import { capitalize } from '~/utils/string/capitalize'; +export const getRelativeDateDisplayValue = ( + relativeDate: { + direction: VariableDateViewFilterValueDirection; + amount?: number; + unit: VariableDateViewFilterValueUnit; + } | null, +) => { + if (!relativeDate) return ''; + const { direction, amount, unit } = relativeDate; + + const directionStr = capitalize(direction.toLowerCase()); + const amountStr = direction === 'THIS' ? '' : amount; + const unitStr = amount + ? amount > 1 + ? plural(unit.toLowerCase()) + : unit.toLowerCase() + : undefined; + + return [directionStr, amountStr, unitStr] + .filter((item) => item !== undefined) + .join(' '); +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSettingsNonCompositeFieldTypeLabels.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSettingsNonCompositeFieldTypeLabels.ts new file mode 100644 index 000000000000..67a40d16400c --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSettingsNonCompositeFieldTypeLabels.ts @@ -0,0 +1,10 @@ +import { SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs'; +import { SettingsNonCompositeFieldType } from '@/settings/data-model/types/SettingsNonCompositeFieldType'; + +export const getSettingsNonCompositeFieldTypeLabel = ( + settingsNonCompositeFieldType: SettingsNonCompositeFieldType, +) => { + return SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS[ + settingsNonCompositeFieldType + ].label; +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSubMenuOptions.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSubMenuOptions.ts new file mode 100644 index 000000000000..0c3bfb1fb097 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/getSubMenuOptions.ts @@ -0,0 +1,21 @@ +import { FilterableFieldType } from '@/object-record/object-filter-dropdown/types/FilterableFieldType'; + +export const getSubMenuOptions = (subMenu: FilterableFieldType | null) => { + switch (subMenu) { + case 'ACTOR': + return [ + { + name: 'Creation Source', + icon: 'IconPlug', + type: 'SOURCE', + }, + { + name: 'Creator Name', + icon: 'IconId', + type: 'ACTOR', + }, + ]; + default: + return []; + } +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isActorSourceCompositeFilter.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isActorSourceCompositeFilter.ts new file mode 100644 index 000000000000..0d1d5046ab41 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isActorSourceCompositeFilter.ts @@ -0,0 +1,11 @@ +import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; +import { FieldActorValue } from '@/object-record/record-field/types/FieldMetadata'; + +export const isActorSourceCompositeFilter = ( + filterDefinition: FilterDefinition, +) => { + return ( + filterDefinition.compositeFieldName === + ('source' satisfies keyof FieldActorValue) + ); +}; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isCompositeField.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isCompositeField.ts new file mode 100644 index 000000000000..6de44cd44e18 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/utils/isCompositeField.ts @@ -0,0 +1,8 @@ +import { + COMPOSITE_FIELD_TYPES, + CompositeFieldType, +} from '@/settings/data-model/types/CompositeFieldType'; +import { FieldType } from '@/settings/data-model/types/FieldType'; + +export const isCompositeField = (type: FieldType): type is CompositeFieldType => + COMPOSITE_FIELD_TYPES.includes(type as any); diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx index 35869601c988..dbbc8d829745 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx @@ -39,6 +39,21 @@ export const StyledInput = styled.input` } `; +const StyledContainer = styled.div` + position: relative; +`; + +const StyledSelectedSortDirectionContainer = styled.div` + background: ${({ theme }) => theme.background.secondary}; + box-shadow: ${({ theme }) => theme.boxShadow.light}; + border-radius: ${({ theme }) => theme.border.radius.md}; + left: 10px; + position: absolute; + top: 10px; + width: 100%; + z-index: 1000; +`; + export type ObjectSortDropdownButtonProps = { sortDropdownId: string; hotkeyScope: HotkeyScope; @@ -95,60 +110,61 @@ export const ObjectSortDropdownButton = ({ } dropdownComponents={ <> - {isSortDirectionMenuUnfolded ? ( - <DropdownMenuItemsContainer> - {SORT_DIRECTIONS.map((sortOrder, index) => ( - <MenuItem - key={index} - onClick={() => { - setSelectedSortDirection(sortOrder); - setIsSortDirectionMenuUnfolded(false); - }} - text={sortOrder === 'asc' ? 'Ascending' : 'Descending'} - /> - ))} - </DropdownMenuItemsContainer> - ) : ( - <> - <DropdownMenuHeader - EndIcon={IconChevronDown} - onClick={() => setIsSortDirectionMenuUnfolded(true)} - > - {selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'} - </DropdownMenuHeader> - <StyledInput - autoFocus - value={objectSortDropdownSearchInput} - placeholder="Search fields" - onChange={(event) => - setObjectSortDropdownSearchInput(event.target.value) - } - /> + {isSortDirectionMenuUnfolded && ( + <StyledSelectedSortDirectionContainer> <DropdownMenuItemsContainer> - {[...availableSortDefinitions] - .sort((a, b) => a.label.localeCompare(b.label)) - .filter((item) => - item.label - .toLocaleLowerCase() - .includes( - objectSortDropdownSearchInput.toLocaleLowerCase(), - ), - ) - .map((availableSortDefinition, index) => ( - <MenuItem - testId={`select-sort-${index}`} - key={index} - onClick={() => { - setObjectSortDropdownSearchInput(''); - handleAddSort(availableSortDefinition); - }} - LeftIcon={getIcon(availableSortDefinition.iconName)} - text={availableSortDefinition.label} - /> - ))} + {SORT_DIRECTIONS.map((sortOrder, index) => ( + <MenuItem + key={index} + onClick={() => { + setSelectedSortDirection(sortOrder); + setIsSortDirectionMenuUnfolded(false); + }} + text={sortOrder === 'asc' ? 'Ascending' : 'Descending'} + /> + ))} </DropdownMenuItemsContainer> - </> + </StyledSelectedSortDirectionContainer> )} + <StyledContainer> + <DropdownMenuHeader + EndIcon={IconChevronDown} + onClick={() => setIsSortDirectionMenuUnfolded(true)} + > + {selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'} + </DropdownMenuHeader> + <StyledInput + autoFocus + value={objectSortDropdownSearchInput} + placeholder="Search fields" + onChange={(event) => + setObjectSortDropdownSearchInput(event.target.value) + } + /> + <DropdownMenuItemsContainer> + {[...availableSortDefinitions] + .sort((a, b) => a.label.localeCompare(b.label)) + .filter((item) => + item.label + .toLocaleLowerCase() + .includes( + objectSortDropdownSearchInput.toLocaleLowerCase(), + ), + ) + .map((availableSortDefinition, index) => ( + <MenuItem + testId={`select-sort-${index}`} + key={index} + onClick={() => { + setObjectSortDropdownSearchInput(''); + handleAddSort(availableSortDefinition); + }} + LeftIcon={getIcon(availableSortDefinition.iconName)} + text={availableSortDefinition.label} + /> + ))} + </DropdownMenuItemsContainer> + </StyledContainer> </> } onClose={handleDropdownButtonClose} 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 4bfb9f2557fc..d336467e8db3 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 @@ -25,7 +25,8 @@ 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 } from 'twenty-ui'; +import { AvatarChipVariant, IconEye, IconEyeOff } from 'twenty-ui'; +import { useDebouncedCallback } from 'use-debounce'; import { useAddNewCard } from '../../record-board-column/hooks/useAddNewCard'; const StyledBoardCard = styled.div<{ selected: boolean }>` @@ -86,7 +87,7 @@ export const StyledBoardCardHeader = styled.div<{ font-weight: ${({ theme }) => theme.font.weight.medium}; height: 24px; padding-bottom: ${({ theme, showCompactView }) => - theme.spacing(showCompactView ? 0 : 1)}; + theme.spacing(showCompactView ? 2 : 1)}; padding-left: ${({ theme }) => theme.spacing(2)}; padding-right: ${({ theme }) => theme.spacing(2)}; padding-top: ${({ theme }) => theme.spacing(2)}; @@ -141,10 +142,6 @@ const StyledRecordInlineCellPlaceholder = styled.div` height: 24px; `; -const StyledRecordInlineCell = styled(RecordInlineCell)` - height: 24px; -`; - export const RecordBoardCard = ({ isCreating = false, onCreateSuccess, @@ -166,7 +163,7 @@ export const RecordBoardCard = ({ } = useRecordBoardStates(); const isCompactModeActive = useRecoilValue(isCompactModeActiveState); - const [isCardInCompactMode, setIsCardInCompactMode] = useState(true); + const [isCardExpanded, setIsCardExpanded] = useState(false); const [isCurrentCardSelected, setIsCurrentCardSelected] = useRecoilState( isRecordBoardCardSelectedFamilyState(recordId), @@ -205,11 +202,11 @@ export const RecordBoardCard = ({ </StyledFieldContainer> ); - const onMouseLeaveBoard = () => { - if (isCompactModeActive) { - setIsCardInCompactMode(true); + const onMouseLeaveBoard = useDebouncedCallback(() => { + if (isCompactModeActive && isCardExpanded) { + setIsCardExpanded(false); } - }; + }, 800); const useUpdateOneRecordHook: RecordUpdateHook = () => { const updateEntity = ({ variables }: RecordUpdateHookParams) => { @@ -289,11 +286,11 @@ export const RecordBoardCard = ({ {isCompactModeActive && ( <StyledCompactIconContainer className="compact-icon-container"> <LightIconButton - Icon={IconEye} + Icon={isCardExpanded ? IconEyeOff : IconEye} accent="tertiary" onClick={(e) => { e.stopPropagation(); - setIsCardInCompactMode(false); + setIsCardExpanded((prev) => !prev); }} /> </StyledCompactIconContainer> @@ -314,7 +311,7 @@ export const RecordBoardCard = ({ </StyledBoardCardHeader> <AnimatedEaseInOut - isOpen={!isCardInCompactMode || !isCompactModeActive} + isOpen={isCardExpanded || !isCompactModeActive} initial={false} > <StyledBoardCardBody> @@ -342,13 +339,14 @@ export const RecordBoardCard = ({ metadata: fieldDefinition.metadata, type: fieldDefinition.type, }), + settings: fieldDefinition.settings, }, useUpdateRecord: useUpdateOneRecordHook, hotkeyScope: InlineCellHotkeyScope.InlineCell, }} > {inView ? ( - <StyledRecordInlineCell /> + <RecordInlineCell /> ) : ( <StyledRecordInlineCellPlaceholder /> )} diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader.tsx index 4aeafd96f1fd..afaf8e9d78f4 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardContainerSkeletonLoader.tsx @@ -1,7 +1,8 @@ -import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { StyledBoardCardBody, StyledBoardCardHeader, @@ -43,7 +44,10 @@ export const RecordBoardColumnCardContainerSkeletonLoader = ({ > <StyledBoardCardHeader showCompactView={isCompactModeActive}> <StyledSkeletonTitle> - <Skeleton width={titleSkeletonWidth} height={16} /> + <Skeleton + width={titleSkeletonWidth} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} + /> </StyledSkeletonTitle> </StyledBoardCardHeader> <StyledSeparator /> @@ -51,8 +55,14 @@ export const RecordBoardColumnCardContainerSkeletonLoader = ({ skeletonItems.map(({ id }) => ( <StyledBoardCardBody key={id}> <StyledSkeletonIconAndText> - <Skeleton width={16} height={16} /> - <Skeleton width={151} height={16} /> + <Skeleton + width={16} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} + /> + <Skeleton + width={151} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} + /> </StyledSkeletonIconAndText> </StyledBoardCardBody> ))} diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx index 93a41e91043f..9dbaa619426a 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer.tsx @@ -12,6 +12,7 @@ import { RecordBoardColumnFetchMoreLoader } from '@/object-record/record-board/r import { RecordBoardColumnNewButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewButton'; import { RecordBoardColumnNewOpportunityButton } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnNewOpportunityButton'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; +import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled'; import { getNumberOfCardsPerColumnForSkeletonLoading } from '@/object-record/record-board/record-board-column/utils/getNumberOfCardsPerColumnForSkeletonLoading'; import { isRecordIndexBoardColumnLoadingFamilyState } from '@/object-record/states/isRecordBoardColumnLoadingFamilyState'; @@ -64,6 +65,8 @@ export const RecordBoardColumnCardsContainer = ({ const numberOfFields = visibleFieldDefinitions.length; const isCompactModeActive = useRecoilValue(isCompactModeActiveState); + const { isOpportunitiesCompanyFieldDisabled } = + useIsOpportunitiesCompanyFieldDisabled(); return ( <StyledColumnCardsContainer @@ -107,8 +110,11 @@ export const RecordBoardColumnCardsContainer = ({ > <StyledNewButtonContainer> {objectMetadataItem.nameSingular === - CoreObjectNameSingular.Opportunity ? ( - <RecordBoardColumnNewOpportunityButton /> + CoreObjectNameSingular.Opportunity && + !isOpportunitiesCompanyFieldDisabled ? ( + <RecordBoardColumnNewOpportunityButton + columnId={columnDefinition.id} + /> ) : ( <RecordBoardColumnNewButton columnId={columnDefinition.id} /> )} 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 1b07479660ce..e0ae827280d9 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 @@ -7,8 +7,8 @@ import { RecordBoardContext } from '@/object-record/record-board/contexts/Record import { RecordBoardCard } from '@/object-record/record-board/record-board-card/components/RecordBoardCard'; import { RecordBoardColumnDropdownMenu } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnDropdownMenu'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; -import { useAddNewOpportunity } from '@/object-record/record-board/record-board-column/hooks/useAddNewOpportunity'; import { useColumnNewCardActions } from '@/object-record/record-board/record-board-column/hooks/useColumnNewCardActions'; +import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled'; 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'; @@ -90,21 +90,17 @@ export const RecordBoardColumnHeader = () => { const boardColumnTotal = 0; const { - isCreatingCard, - handleAddNewOpportunityClick, - handleCancel, + newRecord, + handleNewButtonClick, + handleCreateSuccess, handleEntitySelect, - } = useAddNewOpportunity('first'); - - const { newRecord, handleNewButtonClick, handleCreateSuccess } = - useColumnNewCardActions(columnDefinition.id); + } = useColumnNewCardActions(columnDefinition.id); + const { isOpportunitiesCompanyFieldDisabled } = + useIsOpportunitiesCompanyFieldDisabled(); const isOpportunity = - objectMetadataItem.nameSingular === CoreObjectNameSingular.Opportunity; - - const handleClick = isOpportunity - ? handleAddNewOpportunityClick - : () => handleNewButtonClick('first'); + objectMetadataItem.nameSingular === CoreObjectNameSingular.Opportunity && + !isOpportunitiesCompanyFieldDisabled; return ( <> @@ -152,7 +148,7 @@ export const RecordBoardColumnHeader = () => { <LightIconButton accent="tertiary" Icon={IconPlus} - onClick={handleClick} + onClick={() => handleNewButtonClick('first', isOpportunity)} /> </StyledHeaderActions> )} @@ -165,23 +161,26 @@ export const RecordBoardColumnHeader = () => { stageId={columnDefinition.id} /> )} - {newRecord?.isCreating && newRecord.position === 'first' && ( - <RecordBoardCard - isCreating={true} - onCreateSuccess={() => handleCreateSuccess('first')} - position="first" - /> - )} - {isCreatingCard && ( - <SingleEntitySelect - disableBackgroundBlur - onCancel={handleCancel} - onEntitySelected={handleEntitySelect} - relationObjectNameSingular={CoreObjectNameSingular.Company} - relationPickerScopeId="relation-picker" - selectedRelationRecordIds={[]} - /> - )} + {newRecord?.isCreating && + newRecord.position === 'first' && + (newRecord.isOpportunity ? ( + <SingleEntitySelect + disableBackgroundBlur + onCancel={() => handleCreateSuccess('first', columnDefinition.id)} + onEntitySelected={(company) => + company && handleEntitySelect('first', company) + } + relationObjectNameSingular={CoreObjectNameSingular.Company} + relationPickerScopeId="relation-picker" + selectedRelationRecordIds={[]} + /> + ) : ( + <RecordBoardCard + isCreating={true} + onCreateSuccess={() => handleCreateSuccess('first')} + position="first" + /> + ))} </> ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewButton.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewButton.tsx index 6c116ede2025..428b9921f55d 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewButton.tsx @@ -30,7 +30,11 @@ export const RecordBoardColumnNewButton = ({ const { newRecord, handleNewButtonClick, handleCreateSuccess } = useColumnNewCardActions(columnId); - if (newRecord.isCreating && newRecord.position === 'last') { + if ( + newRecord.isCreating && + newRecord.position === 'last' && + !newRecord.isOpportunity + ) { return ( <RecordBoardCard isCreating={true} @@ -41,7 +45,7 @@ export const RecordBoardColumnNewButton = ({ } return ( - <StyledNewButton onClick={() => handleNewButtonClick('last')}> + <StyledNewButton onClick={() => handleNewButtonClick('last', false)}> <IconPlus size={theme.icon.size.md} /> New </StyledNewButton> diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewOpportunityButton.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewOpportunityButton.tsx index 6464948dcfcc..55e9e2e1e581 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewOpportunityButton.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnNewOpportunityButton.tsx @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; import { IconPlus } from 'twenty-ui'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useAddNewOpportunity } from '@/object-record/record-board/record-board-column/hooks/useAddNewOpportunity'; +import { useColumnNewCardActions } from '@/object-record/record-board/record-board-column/hooks/useColumnNewCardActions'; import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect'; const StyledButton = styled.button` @@ -23,27 +23,36 @@ const StyledButton = styled.button` } `; -export const RecordBoardColumnNewOpportunityButton = () => { +export const RecordBoardColumnNewOpportunityButton = ({ + columnId, +}: { + columnId: string; +}) => { const theme = useTheme(); + const { - isCreatingCard, - handleAddNewOpportunityClick, - handleCancel, + newRecord, + handleNewButtonClick, handleEntitySelect, - } = useAddNewOpportunity('last'); + handleCreateSuccess, + } = useColumnNewCardActions(columnId); return ( <> - {isCreatingCard ? ( + {newRecord.isCreating && + newRecord.position === 'last' && + newRecord.isOpportunity ? ( <SingleEntitySelect disableBackgroundBlur - onCancel={handleCancel} - onEntitySelected={handleEntitySelect} + onCancel={() => handleCreateSuccess('last', columnId, false)} + onEntitySelected={(company) => + company ? handleEntitySelect('last', company) : null + } relationObjectNameSingular={CoreObjectNameSingular.Company} relationPickerScopeId="relation-picker" selectedRelationRecordIds={[]} /> ) : ( - <StyledButton onClick={handleAddNewOpportunityClick}> + <StyledButton onClick={() => handleNewButtonClick('last', true)}> <IconPlus size={theme.icon.size.md} /> New </StyledButton> diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAddNewCard.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAddNewCard.ts index cbc3d2e6b981..97cb8c5d6052 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAddNewCard.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAddNewCard.ts @@ -1,14 +1,31 @@ import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; import { recordBoardNewRecordByColumnIdSelector } from '@/object-record/record-board/states/selectors/recordBoardNewRecordByColumnIdSelector'; +import { useEntitySelectSearch } from '@/object-record/relation-picker/hooks/useEntitySelectSearch'; +import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; +import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; +import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import { useCallback, useContext } from 'react'; -import { useRecoilCallback } from 'recoil'; +import { RecoilState, useRecoilCallback } from 'recoil'; import { v4 as uuidv4 } from 'uuid'; +type SetFunction = <T>( + recoilVal: RecoilState<T>, + valOrUpdater: T | ((currVal: T) => T), +) => void; + export const useAddNewCard = () => { const columnContext = useContext(RecordBoardColumnContext); const { createOneRecord, selectFieldMetadataItem } = useContext(RecordBoardContext); + const { resetSearchFilter } = useEntitySelectSearch({ + relationPickerScopeId: 'relation-picker', + }); + + const { + goBackToPreviousHotkeyScope, + setHotkeyScopeAndMemorizePreviousScope, + } = usePreviousHotkeyScope(); const getColumnDefinitionId = useCallback( (columnId?: string) => { @@ -21,8 +38,13 @@ export const useAddNewCard = () => { [columnContext], ); - const addNewCard = useCallback( - (set: any, columnDefinitionId: string, position: 'first' | 'last') => { + const addNewItem = useCallback( + ( + set: SetFunction, + columnDefinitionId: string, + position: 'first' | 'last', + isOpportunity: boolean, + ) => { set( recordBoardNewRecordByColumnIdSelector({ familyKey: columnDefinitionId, @@ -33,6 +55,8 @@ export const useAddNewCard = () => { columnId: columnDefinitionId, isCreating: true, position, + isOpportunity, + company: null, }, ); }, @@ -44,12 +68,19 @@ export const useAddNewCard = () => { labelIdentifier: string, labelValue: string, position: 'first' | 'last', + isOpportunity: boolean, + company?: EntityForSelect, ) => { - if (labelValue !== '') { + if ( + (isOpportunity && company !== null) || + (!isOpportunity && labelValue !== '') + ) { createOneRecord({ [selectFieldMetadataItem.name]: columnContext?.columnDefinition.value, position, - [labelIdentifier.toLowerCase()]: labelValue, + ...(isOpportunity + ? { companyId: company?.id, name: company?.name } + : { [labelIdentifier.toLowerCase()]: labelValue }), }); } }, @@ -62,18 +93,34 @@ export const useAddNewCard = () => { labelIdentifier: string, labelValue: string, position: 'first' | 'last', + isOpportunity: boolean, columnId?: string, ): void => { const columnDefinitionId = getColumnDefinitionId(columnId); - addNewCard(set, columnDefinitionId, position); - createRecord(labelIdentifier, labelValue, position); + addNewItem(set, columnDefinitionId, position, isOpportunity); + if (isOpportunity) { + setHotkeyScopeAndMemorizePreviousScope( + RelationPickerHotkeyScope.RelationPicker, + ); + } else { + createRecord(labelIdentifier, labelValue, position, isOpportunity); + } }, - [addNewCard, createRecord, getColumnDefinitionId], + [ + addNewItem, + createRecord, + getColumnDefinitionId, + setHotkeyScopeAndMemorizePreviousScope, + ], ); const handleCreateSuccess = useRecoilCallback( ({ set }) => - (position: 'first' | 'last', columnId?: string): void => { + ( + position: 'first' | 'last', + columnId?: string, + isOpportunity = false, + ): void => { const columnDefinitionId = getColumnDefinitionId(columnId); set( recordBoardNewRecordByColumnIdSelector({ @@ -85,10 +132,16 @@ export const useAddNewCard = () => { columnId: columnDefinitionId, isCreating: false, position, + isOpportunity: Boolean(isOpportunity), + company: null, }, ); + resetSearchFilter(); + if (isOpportunity === true) { + goBackToPreviousHotkeyScope(); + } }, - [getColumnDefinitionId], + [getColumnDefinitionId, goBackToPreviousHotkeyScope, resetSearchFilter], ); const handleCreate = ( @@ -98,7 +151,13 @@ export const useAddNewCard = () => { onCreateSuccess?: () => void, ) => { if (labelValue.trim() !== '' && position !== undefined) { - handleAddNewCardClick(labelIdentifier, labelValue.trim(), position); + handleAddNewCardClick( + labelIdentifier, + labelValue.trim(), + position, + false, + '', + ); onCreateSuccess?.(); } }; @@ -125,11 +184,25 @@ export const useAddNewCard = () => { handleCreate(labelIdentifier, labelValue, position, onCreateSuccess); }; + const handleEntitySelect = useCallback( + ( + position: 'first' | 'last', + company: EntityForSelect, + columnId?: string, + ) => { + const columnDefinitionId = getColumnDefinitionId(columnId); + createRecord('', '', position, true, company); + handleCreateSuccess(position, columnDefinitionId, true); + }, + [createRecord, handleCreateSuccess, getColumnDefinitionId], + ); + return { handleAddNewCardClick, handleCreateSuccess, handleCreate, handleBlur, handleInputEnter, + handleEntitySelect, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAddNewOpportunity.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAddNewOpportunity.ts deleted file mode 100644 index d5ce6341646d..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useAddNewOpportunity.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext'; -import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext'; -import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled'; -import { useEntitySelectSearch } from '@/object-record/relation-picker/hooks/useEntitySelectSearch'; -import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; -import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; -import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; -import { useCallback, useContext, useState } from 'react'; - -export const useAddNewOpportunity = (position: string) => { - const [isCreatingCard, setIsCreatingCard] = useState(false); - - const { columnDefinition } = useContext(RecordBoardColumnContext); - const { createOneRecord, selectFieldMetadataItem } = - useContext(RecordBoardContext); - - const { - goBackToPreviousHotkeyScope, - setHotkeyScopeAndMemorizePreviousScope, - } = usePreviousHotkeyScope(); - const { resetSearchFilter } = useEntitySelectSearch({ - relationPickerScopeId: 'relation-picker', - }); - const { isOpportunitiesCompanyFieldDisabled } = - useIsOpportunitiesCompanyFieldDisabled(); - const handleEntitySelect = useCallback( - (company?: EntityForSelect) => { - setIsCreatingCard(false); - goBackToPreviousHotkeyScope(); - resetSearchFilter(); - createOneRecord({ - name: company?.name, - companyId: company?.id, - position: position, - [selectFieldMetadataItem.name]: columnDefinition.value, - }); - }, - [ - columnDefinition, - createOneRecord, - goBackToPreviousHotkeyScope, - resetSearchFilter, - selectFieldMetadataItem, - position, - ], - ); - - const handleAddNewOpportunityClick = useCallback(() => { - if (isOpportunitiesCompanyFieldDisabled) { - handleEntitySelect(); - } else { - setIsCreatingCard(true); - } - setHotkeyScopeAndMemorizePreviousScope( - RelationPickerHotkeyScope.RelationPicker, - ); - }, [ - setHotkeyScopeAndMemorizePreviousScope, - isOpportunitiesCompanyFieldDisabled, - handleEntitySelect, - ]); - - const handleCancel = useCallback(() => { - resetSearchFilter(); - goBackToPreviousHotkeyScope(); - setIsCreatingCard(false); - }, [goBackToPreviousHotkeyScope, resetSearchFilter]); - - return { - isCreatingCard, - handleEntitySelect, - handleAddNewOpportunityClick, - handleCancel, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useColumnNewCardActions.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useColumnNewCardActions.ts index 8fd3ab2f4ad2..eb498d5ae447 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useColumnNewCardActions.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/hooks/useColumnNewCardActions.ts @@ -12,7 +12,8 @@ export const useColumnNewCardActions = (columnId: string) => { (field) => field.isLabelIdentifier, ); - const { handleAddNewCardClick, handleCreateSuccess } = useAddNewCard(); + const { handleAddNewCardClick, handleCreateSuccess, handleEntitySelect } = + useAddNewCard(); const newRecord = useRecoilValue( recordBoardNewRecordByColumnIdSelector({ @@ -21,11 +22,15 @@ export const useColumnNewCardActions = (columnId: string) => { }), ); - const handleNewButtonClick = (position: 'first' | 'last') => { + const handleNewButtonClick = ( + position: 'first' | 'last', + isOpportunity: boolean, + ) => { handleAddNewCardClick( labelIdentifierField?.label ?? '', '', position, + isOpportunity, columnId, ); }; @@ -34,5 +39,6 @@ export const useColumnNewCardActions = (columnId: string) => { newRecord, handleNewButtonClick, handleCreateSuccess, + handleEntitySelect, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardNewRecordByColumnIdComponentFamilyState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardNewRecordByColumnIdComponentFamilyState.ts index bc362d973c70..e3286f253d60 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardNewRecordByColumnIdComponentFamilyState.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardNewRecordByColumnIdComponentFamilyState.ts @@ -1,3 +1,4 @@ +import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; import { createComponentFamilyState } from '@/ui/utilities/state/component-state/utils/createComponentFamilyState'; export type NewCard = { @@ -5,6 +6,8 @@ export type NewCard = { columnId: string; isCreating: boolean; position: 'first' | 'last'; + isOpportunity: boolean; + company: EntityForSelect | null; }; export const recordBoardNewRecordByColumnIdComponentFamilyState = @@ -15,5 +18,7 @@ export const recordBoardNewRecordByColumnIdComponentFamilyState = columnId: '', isCreating: false, position: 'last', + isOpportunity: false, + company: null, }, }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/__mocks__/fieldDefinitions.ts b/packages/twenty-front/src/modules/object-record/record-field/__mocks__/fieldDefinitions.ts index e688a2a64544..90cd7f176e26 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/__mocks__/fieldDefinitions.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/__mocks__/fieldDefinitions.ts @@ -9,10 +9,8 @@ import { FieldTextMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { - mockedCompanyObjectMetadataItem, - mockedPersonObjectMetadataItem, -} from '~/testing/mock-data/metadata'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; + export const fieldMetadataId = 'fieldMetadataId'; export const textfieldDefinition: FieldDefinition<FieldTextMetadata> = { @@ -24,7 +22,16 @@ export const textfieldDefinition: FieldDefinition<FieldTextMetadata> = { metadata: { placeHolder: 'John Doe', fieldName: 'userName' }, }; -const relationFieldMetadataItem = mockedPersonObjectMetadataItem.fields?.find( +const mockedPersonObjectMetadataItem = generatedMockObjectMetadataItems.find( + ({ nameSingular }) => nameSingular === 'person', +); + +if (!mockedPersonObjectMetadataItem) { + throw new Error('Person object metadata item not found'); +} + + +const relationFieldMetadataItem = mockedPersonObjectMetadataItem?.fields?.find( ({ name }) => name === 'company', ); @@ -91,7 +98,15 @@ export const ratingFieldDefinition: FieldDefinition<FieldRatingMetadata> = { }, }; -const booleanFieldMetadataItem = mockedCompanyObjectMetadataItem.fields?.find( +const mockedCompanyObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'company', +); + +if (!mockedCompanyObjectMetadataItem) { + throw new Error('Company object metadata item not found'); +} + +const booleanFieldMetadataItem = mockedCompanyObjectMetadataItem?.fields?.find( ({ name }) => name === 'idealCustomerProfile', ); export const booleanFieldDefinition = formatFieldMetadataItemAsFieldDefinition({ diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx index 2e8756c2e8f0..4eea3aae834f 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/usePersistField.test.tsx @@ -1,11 +1,11 @@ import { gql } from '@apollo/client'; -import { MockedProvider, MockedResponse } from '@apollo/client/testing'; +import { MockedResponse } from '@apollo/client/testing'; import { act, renderHook, waitFor } from '@testing-library/react'; import { ReactNode } from 'react'; -import { RecoilRoot, useRecoilValue } from 'recoil'; +import { useRecoilValue } from 'recoil'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { PERSON_FRAGMENT } from '@/object-record/hooks/__mocks__/personFragment'; +import { PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { phonesFieldDefinition, @@ -20,11 +20,12 @@ import { usePersistField } from '@/object-record/record-field/hooks/usePersistFi import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition'; import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const query = gql` mutation UpdateOnePerson($idToUpdate: ID!, $input: PersonUpdateInput!) { updatePerson(id: $idToUpdate, data: $input) { - ${PERSON_FRAGMENT} + ${PERSON_FRAGMENT_WITH_DEPTH_ONE_RELATIONS} } } `; @@ -72,6 +73,10 @@ const mocks: MockedResponse[] = [ const recordId = 'recordId'; +const JestMetadataAndApolloMocksWrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); + const getWrapper = (fieldDefinition: FieldDefinition<FieldMetadata>) => ({ children }: { children: ReactNode }) => { @@ -91,7 +96,7 @@ const getWrapper = }; return ( - <MockedProvider mocks={mocks} addTypename={false}> + <JestMetadataAndApolloMocksWrapper> <FieldContext.Provider value={{ fieldDefinition, @@ -101,9 +106,9 @@ const getWrapper = useUpdateRecord: useUpdateOneRecordMutation, }} > - <RecoilRoot>{children}</RecoilRoot> + {children} </FieldContext.Provider> - </MockedProvider> + </JestMetadataAndApolloMocksWrapper> ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx index 86a2037c5d2c..5ec442483de4 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/__tests__/useToggleEditOnlyInput.test.tsx @@ -1,8 +1,7 @@ import { gql } from '@apollo/client'; -import { MockedProvider, MockedResponse } from '@apollo/client/testing'; -import { act, renderHook, waitFor } from '@testing-library/react'; -import { ReactNode } from 'react'; -import { RecoilRoot } from 'recoil'; +import { MockedResponse } from '@apollo/client/testing'; +import { renderHook, waitFor } from '@testing-library/react'; +import { ReactNode, act } from 'react'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; @@ -13,6 +12,8 @@ import { RecordUpdateHookParams, } from '@/object-record/record-field/contexts/FieldContext'; import { useToggleEditOnlyInput } from '@/object-record/record-field/hooks/useToggleEditOnlyInput'; +import { generateEmptyJestRecordNode } from '~/testing/jest/generateEmptyJestRecordNode'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; const recordId = 'recordId'; @@ -26,13 +27,42 @@ const mocks: MockedResponse[] = [ ) { updateCompany(id: $idToUpdate, data: $input) { __typename - updatedAt - domainName { - primaryLinkUrl - primaryLinkLabel - secondaryLinks + accountOwner { + __typename + avatarUrl + colorScheme + createdAt + dateFormat + deletedAt + id + locale + name { + firstName + lastName + } + timeFormat + timeZone + updatedAt + userEmail + userId + } + accountOwnerId + activityTargets { + edges { + node { + __typename + activityId + companyId + createdAt + deletedAt + id + opportunityId + personId + rocketId + updatedAt + } + } } - visaSponsorship address { addressStreet1 addressStreet2 @@ -43,20 +73,31 @@ const mocks: MockedResponse[] = [ addressLat addressLng } - position - employees - deletedAt - accountOwnerId annualRecurringRevenue { amountMicros currencyCode } - id - name - xLink { - primaryLinkUrl - primaryLinkLabel - secondaryLinks + attachments { + edges { + node { + __typename + activityId + authorId + companyId + createdAt + deletedAt + fullPath + id + name + noteId + opportunityId + personId + rocketId + taskId + type + updatedAt + } + } } createdAt createdBy { @@ -64,7 +105,36 @@ const mocks: MockedResponse[] = [ workspaceMemberId name } - workPolicy + deletedAt + domainName { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + employees + favorites { + edges { + node { + __typename + companyId + createdAt + deletedAt + id + noteId + opportunityId + personId + position + rocketId + taskId + updatedAt + viewId + workflowId + workspaceMemberId + } + } + } + id + idealCustomerProfile introVideo { primaryLinkUrl primaryLinkLabel @@ -75,8 +145,151 @@ const mocks: MockedResponse[] = [ primaryLinkLabel secondaryLinks } + name + noteTargets { + edges { + node { + __typename + companyId + createdAt + deletedAt + id + noteId + opportunityId + personId + rocketId + updatedAt + } + } + } + opportunities { + edges { + node { + __typename + amount { + amountMicros + currencyCode + } + closeDate + companyId + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + id + name + pointOfContactId + position + stage + updatedAt + } + } + } + people { + edges { + node { + __typename + avatarUrl + city + companyId + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + emails { + primaryEmail + additionalEmails + } + id + intro + jobTitle + linkedinLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + name { + firstName + lastName + } + performanceRating + phones { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } + position + updatedAt + whatsapp { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } + workPreference + xLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + } + } + } + position tagline - idealCustomerProfile + taskTargets { + edges { + node { + __typename + companyId + createdAt + deletedAt + id + opportunityId + personId + rocketId + taskId + updatedAt + } + } + } + timelineActivities { + edges { + node { + __typename + companyId + createdAt + deletedAt + happensAt + id + linkedObjectMetadataId + linkedRecordCachedName + linkedRecordId + name + noteId + opportunityId + personId + properties + rocketId + taskId + updatedAt + workspaceMemberId + } + } + } + updatedAt + visaSponsorship + workPolicy + xLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } } } `, @@ -87,8 +300,12 @@ const mocks: MockedResponse[] = [ }, result: jest.fn(() => ({ data: { - updateWorkspaceMember: { - id: 'recordId', + updateCompany: { + ...generateEmptyJestRecordNode({ + objectNameSingular: CoreObjectNameSingular.Company, + input: { id: recordId }, + withDepthOneRelation: true, + }), }, }, })), @@ -111,8 +328,13 @@ const Wrapper = ({ children }: { children: ReactNode }) => { return [updateEntity, { loading: false }]; }; + const JestMetadataAndApolloMocksWrapper = + getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, + }); + return ( - <MockedProvider mocks={mocks} addTypename={false}> + <JestMetadataAndApolloMocksWrapper> <FieldContext.Provider value={{ fieldDefinition: booleanFieldDefinition, @@ -122,9 +344,9 @@ const Wrapper = ({ children }: { children: ReactNode }) => { useUpdateRecord: useUpdateOneRecordMutation, }} > - <RecoilRoot>{children}</RecoilRoot> + {children} </FieldContext.Provider> - </MockedProvider> + </JestMetadataAndApolloMocksWrapper> ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/NumberFieldDisplay.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/NumberFieldDisplay.tsx index 087a4117c47b..cb30dbed3776 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/NumberFieldDisplay.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/NumberFieldDisplay.tsx @@ -2,7 +2,11 @@ import { useNumberFieldDisplay } from '@/object-record/record-field/meta-types/h import { NumberDisplay } from '@/ui/field/display/components/NumberDisplay'; export const NumberFieldDisplay = () => { - const { fieldValue } = useNumberFieldDisplay(); - - return <NumberDisplay value={fieldValue} />; + const { fieldValue, fieldDefinition } = useNumberFieldDisplay(); + return ( + <NumberDisplay + value={fieldValue} + decimals={fieldDefinition.settings?.decimals} + /> + ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/DateTimeFieldDisplay.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/DateTimeFieldDisplay.perf.stories.tsx index 079b84520e91..9e2cb6b78622 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/DateTimeFieldDisplay.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/DateTimeFieldDisplay.perf.stories.tsx @@ -51,6 +51,6 @@ export const Elipsis: Story = { export const Performance = getProfilingStory({ componentName: 'DateTimeFieldDisplay', averageThresholdInMs: 0.1, - numberOfRuns: 50, - numberOfTestsPerRun: 100, + numberOfRuns: 30, + numberOfTestsPerRun: 30, }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/EmailFieldDisplay.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/EmailsFieldDisplay.perf.stories.tsx similarity index 55% rename from packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/EmailFieldDisplay.perf.stories.tsx rename to packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/EmailsFieldDisplay.perf.stories.tsx index b901caa9c1cd..283224dd88a5 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/EmailFieldDisplay.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/EmailsFieldDisplay.perf.stories.tsx @@ -1,19 +1,22 @@ import { Meta, StoryObj } from '@storybook/react'; import { ComponentDecorator } from 'twenty-ui'; -import { EmailFieldDisplay } from '@/object-record/record-field/meta-types/display/components/EmailFieldDisplay'; +import { EmailsFieldDisplay } from '@/object-record/record-field/meta-types/display/components/EmailsFieldDisplay'; import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator'; import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory'; const meta: Meta = { - title: 'UI/Data/Field/Display/EmailFieldDisplay', + title: 'UI/Data/Field/Display/EmailsFieldDisplay', decorators: [ MemoryRouterDecorator, - getFieldDecorator('person', 'email'), + getFieldDecorator('person', 'emails', { + primaryEmail: 'test@test.com', + additionalEmails: ['toto@test.com'], + }), ComponentDecorator, ], - component: EmailFieldDisplay, + component: EmailsFieldDisplay, args: {}, parameters: { chromatic: { disableSnapshot: true }, @@ -22,25 +25,25 @@ const meta: Meta = { export default meta; -type Story = StoryObj<typeof EmailFieldDisplay>; +type Story = StoryObj<typeof EmailsFieldDisplay>; export const Default: Story = {}; export const Elipsis: Story = { parameters: { - container: { width: 50 }, + container: { width: 100 }, }, decorators: [ - getFieldDecorator( - 'person', - 'email', - 'asdasdasdaksjdhkajshdkajhasmdkamskdsd@asdkjhaksjdhaksjd.com', - ), + getFieldDecorator('person', 'emails', { + primaryEmail: + 'asdasdasdaksjdhkajshdkajhasmdkamskdsd@asdkjhaksjdhaksjd.com', + additionalEmails: [], + }), ], }; export const Performance = getProfilingStory({ - componentName: 'EmailFieldDisplay', + componentName: 'EmailsFieldDisplay', averageThresholdInMs: 0.5, numberOfRuns: 50, numberOfTestsPerRun: 100, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/JsonFieldDisplay.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/JsonFieldDisplay.perf.stories.tsx deleted file mode 100644 index be1567d863b0..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/JsonFieldDisplay.perf.stories.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { Meta, StoryObj } from '@storybook/react'; -import { ComponentDecorator } from 'twenty-ui'; - -import { JsonFieldDisplay } from '@/object-record/record-field/meta-types/display/components/JsonFieldDisplay'; -import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator'; -import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; -import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory'; - -const meta: Meta = { - title: 'UI/Data/Field/Display/JsonFieldDisplay', - decorators: [ - MemoryRouterDecorator, - getFieldDecorator('company', 'testRawJson', { - key1: 'value1', - key2: 'value2', - }), - ComponentDecorator, - ], - component: JsonFieldDisplay, - args: {}, - parameters: { - chromatic: { disableSnapshot: true }, - }, -}; - -export default meta; - -type Story = StoryObj<typeof JsonFieldDisplay>; - -export const Default: Story = {}; - -export const Elipsis: Story = { - parameters: { - container: { width: 50 }, - }, -}; - -export const Performance = getProfilingStory({ - componentName: 'JsonFieldDisplay', - averageThresholdInMs: 0.1, - numberOfRuns: 50, - numberOfTestsPerRun: 100, -}); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/MultiSelectFieldDisplay.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/MultiSelectFieldDisplay.perf.stories.tsx index ec9a8291d2c5..0eae95f09db0 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/MultiSelectFieldDisplay.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/MultiSelectFieldDisplay.perf.stories.tsx @@ -23,7 +23,7 @@ const meta: Meta = { title: 'UI/Data/Field/Display/MultiSelectFieldDisplay', decorators: [ MemoryRouterDecorator, - getFieldDecorator('company', 'testMultiSelect', [ + getFieldDecorator('company', 'workPolicy', [ 'Option 1', 'Option 2', 'Option 3', diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/PhoneFieldDisplay.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/PhonesFieldDisplay.perf.stories.tsx similarity index 61% rename from packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/PhoneFieldDisplay.perf.stories.tsx rename to packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/PhonesFieldDisplay.perf.stories.tsx index fed4bbe247a3..94f37ee5cfb6 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/PhoneFieldDisplay.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/PhonesFieldDisplay.perf.stories.tsx @@ -1,19 +1,19 @@ import { Meta, StoryObj } from '@storybook/react'; import { ComponentDecorator } from 'twenty-ui'; -import { PhoneFieldDisplay } from '@/object-record/record-field/meta-types/display/components/PhoneFieldDisplay'; +import { PhonesFieldDisplay } from '@/object-record/record-field/meta-types/display/components/PhonesFieldDisplay'; import { getFieldDecorator } from '~/testing/decorators/getFieldDecorator'; import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator'; import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory'; const meta: Meta = { - title: 'UI/Data/Field/Display/PhoneFieldDisplay', + title: 'UI/Data/Field/Display/PhonesFieldDisplay', decorators: [ MemoryRouterDecorator, - getFieldDecorator('person', 'phone'), + getFieldDecorator('person', 'phones'), ComponentDecorator, ], - component: PhoneFieldDisplay, + component: PhonesFieldDisplay, args: {}, parameters: { chromatic: { disableSnapshot: true }, @@ -22,7 +22,7 @@ const meta: Meta = { export default meta; -type Story = StoryObj<typeof PhoneFieldDisplay>; +type Story = StoryObj<typeof PhonesFieldDisplay>; export const Default: Story = {}; @@ -33,11 +33,17 @@ export const Elipsis: Story = { }; export const WrongNumber: Story = { - decorators: [getFieldDecorator('person', 'phone', 'sdklaskdj')], + decorators: [ + getFieldDecorator('person', 'phones', { + primaryPhoneNumber: '123-456-7890', + primaryPhoneCountryCode: '+1', + additionalPhones: null, + }), + ], }; export const Performance = getProfilingStory({ - componentName: 'PhoneFieldDisplay', + componentName: 'PhonesFieldDisplay', averageThresholdInMs: 0.5, numberOfRuns: 20, numberOfTestsPerRun: 100, diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RatingFieldDisplay.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RatingFieldDisplay.perf.stories.tsx index 3c31dc6d5682..3d4b5950aea0 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RatingFieldDisplay.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RatingFieldDisplay.perf.stories.tsx @@ -10,7 +10,7 @@ const meta: Meta = { title: 'UI/Data/Field/Display/RatingFieldDisplay', decorators: [ MemoryRouterDecorator, - getFieldDecorator('company', 'testRating'), + getFieldDecorator('person', 'performanceRating'), ComponentDecorator, ], component: RatingFieldDisplay, @@ -30,5 +30,5 @@ export const Performance = getProfilingStory({ componentName: 'RatingFieldDisplay', averageThresholdInMs: 0.5, numberOfRuns: 30, - numberOfTestsPerRun: 50, + numberOfTestsPerRun: 30, }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RelationToOneFieldDisplay.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RelationToOneFieldDisplay.perf.stories.tsx index 49a076d80a47..989de5e7439f 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RelationToOneFieldDisplay.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/display/components/__stories__/perf/RelationToOneFieldDisplay.perf.stories.tsx @@ -30,7 +30,7 @@ export const Default: Story = {}; export const Performance = getProfilingStory({ componentName: 'RelationFieldDisplay', - averageThresholdInMs: 0.2, + averageThresholdInMs: 0.22, numberOfRuns: 20, numberOfTestsPerRun: 100, }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useNumberField.ts b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useNumberField.ts index 5bdceda11e73..097bcb8beef5 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useNumberField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/hooks/useNumberField.ts @@ -5,10 +5,11 @@ import { useRecordFieldInput } from '@/object-record/record-field/hooks/useRecor import { FieldNumberValue } from '@/object-record/record-field/types/FieldMetadata'; import { recordStoreFamilySelector } from '@/object-record/record-store/states/selectors/recordStoreFamilySelector'; import { FieldMetadataType } from '~/generated-metadata/graphql'; + import { - canBeCastAsIntegerOrNull, - castAsIntegerOrNull, -} from '~/utils/cast-as-integer-or-null'; + canBeCastAsNumberOrNull, + castAsNumberOrNull, +} from '~/utils/cast-as-number-or-null'; import { FieldContext } from '../../contexts/FieldContext'; import { usePersistField } from '../../hooks/usePersistField'; @@ -32,11 +33,11 @@ export const useNumberField = () => { const persistField = usePersistField(); const persistNumberField = (newValue: string) => { - if (!canBeCastAsIntegerOrNull(newValue)) { + if (!canBeCastAsNumberOrNull(newValue)) { return; } - const castedValue = castAsIntegerOrNull(newValue); + const castedValue = castAsNumberOrNull(newValue); persistField(castedValue); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx index 811c96987a1d..89a4e054e217 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/CurrencyFieldInput.tsx @@ -7,6 +7,7 @@ import { CurrencyInput } from '@/ui/field/input/components/CurrencyInput'; import { FieldInputOverlay } from '../../../../../ui/field/input/components/FieldInputOverlay'; import { useCurrencyField } from '../../hooks/useCurrencyField'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { FieldInputEvent } from './DateTimeFieldInput'; type CurrencyFieldInputProps = { @@ -108,7 +109,7 @@ export const CurrencyFieldInput = ({ const handleSelect = (newValue: string) => { setDraftValue({ - amount: draftValue?.amount ?? '', + amount: isUndefinedOrNull(draftValue?.amount) ? '' : draftValue?.amount, currencyCode: newValue as CurrencyCode, }); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx index 02ce963192b0..d933aeabcd06 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/EmailsFieldInput.tsx @@ -1,6 +1,7 @@ import { useEmailsField } from '@/object-record/record-field/meta-types/hooks/useEmailsField'; import { EmailsFieldMenuItem } from '@/object-record/record-field/meta-types/input/components/EmailsFieldMenuItem'; -import { useMemo } from 'react'; +import { emailSchema } from '@/object-record/record-field/validation-schemas/emailSchema'; +import { useCallback, useMemo } from 'react'; import { isDefined } from 'twenty-ui'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { MultiItemFieldInput } from './MultiItemFieldInput'; @@ -29,6 +30,16 @@ export const EmailsFieldInput = ({ onCancel }: EmailsFieldInputProps) => { }); }; + const validateInput = useCallback( + (input: string) => ({ + isValid: emailSchema.safeParse(input).success, + errorMessage: '', + }), + [], + ); + + const isPrimaryEmail = (index: number) => index === 0 && emails?.length > 1; + return ( <MultiItemFieldInput items={emails} @@ -36,6 +47,7 @@ export const EmailsFieldInput = ({ onCancel }: EmailsFieldInputProps) => { onCancel={onCancel} placeholder="Email" fieldMetadataType={FieldMetadataType.Emails} + validateInput={validateInput} renderItem={({ value: email, index, @@ -46,7 +58,7 @@ export const EmailsFieldInput = ({ onCancel }: EmailsFieldInputProps) => { <EmailsFieldMenuItem key={index} dropdownId={`${hotkeyScope}-emails-${index}`} - isPrimary={index === 0} + isPrimary={isPrimaryEmail(index)} email={email} onEdit={handleEdit} onSetAsPrimary={handleSetPrimary} diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx index c205039450f0..e52cc95c041f 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/LinksFieldInput.tsx @@ -42,6 +42,8 @@ export const LinksFieldInput = ({ onCancel }: LinksFieldInputProps) => { }); }; + const isPrimaryLink = (index: number) => index === 0 && links?.length > 1; + return ( <MultiItemFieldInput items={links} @@ -49,7 +51,10 @@ export const LinksFieldInput = ({ onCancel }: LinksFieldInputProps) => { onCancel={onCancel} placeholder="URL" fieldMetadataType={FieldMetadataType.Links} - validateInput={(input) => absoluteUrlSchema.safeParse(input).success} + validateInput={(input) => ({ + isValid: absoluteUrlSchema.safeParse(input).success, + errorMessage: '', + })} formatInput={(input) => ({ url: input, label: '' })} renderItem={({ value: link, @@ -61,7 +66,7 @@ export const LinksFieldInput = ({ onCancel }: LinksFieldInputProps) => { <LinksFieldMenuItem key={index} dropdownId={`${hotkeyScope}-links-${index}`} - isPrimary={index === 0} + isPrimary={isPrimaryLink(index)} label={link.label} onEdit={handleEdit} onSetAsPrimary={handleSetPrimary} 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 a73e49498805..7e3e93ec2c48 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 @@ -30,7 +30,7 @@ type MultiItemFieldInputProps<T> = { onPersist: (updatedItems: T[]) => void; onCancel?: () => void; placeholder: string; - validateInput?: (input: string) => boolean; + validateInput?: (input: string) => { isValid: boolean; errorMessage: string }; formatInput?: (input: string) => T; renderItem: (props: { value: T; @@ -74,8 +74,21 @@ export const MultiItemFieldInput = <T,>({ const [isInputDisplayed, setIsInputDisplayed] = useState(false); const [inputValue, setInputValue] = useState(''); const [itemToEditIndex, setItemToEditIndex] = useState(-1); + const [errorData, setErrorData] = useState({ + isValid: true, + errorMessage: '', + }); const isAddingNewItem = itemToEditIndex === -1; + const handleOnChange = (value: string) => { + setInputValue(value); + if (!validateInput) return; + + if (errorData.isValid) { + setErrorData(errorData); + } + }; + const handleAddButtonClick = () => { setItemToEditIndex(-1); setIsInputDisplayed(true); @@ -105,7 +118,13 @@ export const MultiItemFieldInput = <T,>({ }; const handleSubmitInput = () => { - if (validateInput !== undefined && !validateInput(inputValue)) return; + if (validateInput !== undefined) { + const validationData = validateInput(inputValue) ?? { isValid: true }; + if (!validationData.isValid) { + setErrorData(validationData); + return; + } + } const newItem = formatInput ? formatInput(inputValue) @@ -160,6 +179,7 @@ export const MultiItemFieldInput = <T,>({ placeholder={placeholder} value={inputValue} hotkeyScope={hotkeyScope} + hasError={!errorData.isValid} renderInput={ renderInput ? (props) => @@ -170,7 +190,7 @@ export const MultiItemFieldInput = <T,>({ }) : undefined } - onChange={(event) => setInputValue(event.target.value)} + onChange={(event) => handleOnChange(event.target.value)} onEnter={handleSubmitInput} rightComponent={ <LightIconButton diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/PhonesFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/PhonesFieldInput.tsx index 6667acf2031f..54fb7ab8ded9 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/PhonesFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/PhonesFieldInput.tsx @@ -78,6 +78,8 @@ export const PhonesFieldInput = ({ onCancel }: PhonesFieldInputProps) => { }); }; + const isPrimaryPhone = (index: number) => index === 0 && phones?.length > 1; + return ( <MultiItemFieldInput items={phones} @@ -108,7 +110,7 @@ export const PhonesFieldInput = ({ onCancel }: PhonesFieldInputProps) => { <PhonesFieldMenuItem key={index} dropdownId={`${hotkeyScope}-phones-${index}`} - isPrimary={index === 0} + isPrimary={isPrimaryPhone(index)} phone={phone} onEdit={handleEdit} onSetAsPrimary={handleSetPrimary} diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/CurrencyCode.ts b/packages/twenty-front/src/modules/object-record/record-field/types/CurrencyCode.ts index 9e86b3c87939..fcc4c3c3267a 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/CurrencyCode.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/CurrencyCode.ts @@ -1,20 +1,154 @@ export enum CurrencyCode { + AED = 'AED', + AFN = 'AFN', + ALL = 'ALL', + AMD = 'AMD', + ANG = 'ANG', + AOA = 'AOA', + ARS = 'ARS', + AUD = 'AUD', + AWG = 'AWG', + AZN = 'AZN', + BAM = 'BAM', + BBD = 'BBD', + BDT = 'BDT', + BGN = 'BGN', + BHD = 'BHD', + BIF = 'BIF', + BMD = 'BMD', + BND = 'BND', + BOB = 'BOB', + BRL = 'BRL', + BSD = 'BSD', + BTN = 'BTN', + BWP = 'BWP', + BYN = 'BYN', + BZD = 'BZD', CAD = 'CAD', + CDF = 'CDF', CHF = 'CHF', + CLP = 'CLP', CNY = 'CNY', + COP = 'COP', + CRC = 'CRC', + CUP = 'CUP', + CVE = 'CVE', CZK = 'CZK', + DJF = 'DJF', + DKK = 'DKK', + DOP = 'DOP', + DZD = 'DZD', + EGP = 'EGP', + ERN = 'ERN', + ETB = 'ETB', EUR = 'EUR', + FJD = 'FJD', + FKP = 'FKP', GBP = 'GBP', + GEL = 'GEL', + GHS = 'GHS', + GIP = 'GIP', + GMD = 'GMD', + GNF = 'GNF', + GTQ = 'GTQ', + GYD = 'GYD', HKD = 'HKD', + HNL = 'HNL', + HTG = 'HTG', + HUF = 'HUF', + IDR = 'IDR', + ILS = 'ILS', + INR = 'INR', + IQD = 'IQD', + IRR = 'IRR', + ISK = 'ISK', + JMD = 'JMD', + JOD = 'JOD', JPY = 'JPY', - USD = 'USD', - NOK = 'NOK', - SEK = 'SEK', - BHT = 'BHT', + KES = 'KES', + KGS = 'KGS', + KHR = 'KHR', + KMF = 'KMF', + KPW = 'KPW', + KRW = 'KRW', + KWD = 'KWD', + KYD = 'KYD', + KZT = 'KZT', + LAK = 'LAK', + LBP = 'LBP', + LKR = 'LKR', + LRD = 'LRD', + LSL = 'LSL', + LYD = 'LYD', MAD = 'MAD', + MDL = 'MDL', + MGA = 'MGA', + MKD = 'MKD', + MMK = 'MMK', + MNT = 'MNT', + MOP = 'MOP', + MRU = 'MRU', + MUR = 'MUR', + MVR = 'MVR', + MWK = 'MWK', + MXN = 'MXN', + MYR = 'MYR', + MZN = 'MZN', + NAD = 'NAD', + NGN = 'NGN', + NIO = 'NIO', + NOK = 'NOK', + NPR = 'NPR', + NZD = 'NZD', + OMR = 'OMR', + PAB = 'PAB', + PEN = 'PEN', + PGK = 'PGK', + PHP = 'PHP', + PKR = 'PKR', + PLN = 'PLN', + PYG = 'PYG', QAR = 'QAR', - AED = 'AED', - KRW = 'KRW', - BRL = 'BRL', - AUD = 'AUD', + RON = 'RON', + RSD = 'RSD', + RUB = 'RUB', + RWF = 'RWF', + SAR = 'SAR', + SBD = 'SBD', + SCR = 'SCR', + SDG = 'SDG', + SEK = 'SEK', + SGD = 'SGD', + SHP = 'SHP', + SLE = 'SLE', + SOS = 'SOS', + SRD = 'SRD', + SSP = 'SSP', + STN = 'STN', + SVC = 'SVC', + SYP = 'SYP', + SZL = 'SZL', + THB = 'THB', + TJS = 'TJS', + TMT = 'TMT', + TND = 'TND', + TOP = 'TOP', + TRY = 'TRY', + TTD = 'TTD', + TWD = 'TWD', + TZS = 'TZS', + UAH = 'UAH', + UGX = 'UGX', + USD = 'USD', + UYU = 'UYU', + UZS = 'UZS', + VES = 'VES', + VND = 'VND', + VUV = 'VUV', + WST = 'WST', + XCD = 'XCD', + YER = 'YER', + ZAR = 'ZAR', + ZMW = 'ZMW', + ZWG = 'ZWG', } diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldDefinition.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldDefinition.ts index 16f05f2418d2..1d8107c17953 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldDefinition.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldDefinition.ts @@ -16,4 +16,7 @@ export type FieldDefinition<T extends FieldMetadata> = { infoTooltipContent?: string; defaultValue?: any; editButtonIcon?: IconComponent; + settings?: { + decimals?: number; + }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts index a2a3339e1977..c6994db5a5f8 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts @@ -177,7 +177,7 @@ export type FieldMetadata = | FieldArrayMetadata; export type FieldTextValue = string; -export type FieldUUidValue = string; +export type FieldUUidValue = string; // TODO: can we replace with a template literal type, or maybe overkill ? export type FieldDateTimeValue = string | null; export type FieldDateValue = string | null; export type FieldNumberValue = number | null; @@ -225,6 +225,8 @@ export type FieldRelationValue< export type Json = ZodHelperLiteral | { [key: string]: Json } | Json[]; export type FieldJsonValue = Record<string, Json> | Json[] | null; +export type FieldRichTextValue = Record<string, Json> | Json[] | null; + export type FieldActorValue = { source: string; workspaceMemberId?: string; diff --git a/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromFieldValue.ts b/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromFieldValue.ts index 725ca65cc1b8..298ef3c9a4a9 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromFieldValue.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/utils/computeDraftValueFromFieldValue.ts @@ -9,6 +9,7 @@ import { isFieldRelation } from '@/object-record/record-field/types/guards/isFie import { computeEmptyDraftValue } from '@/object-record/record-field/utils/computeEmptyDraftValue'; import { isFieldValueEmpty } from '@/object-record/record-field/utils/isFieldValueEmpty'; import { isDefined } from '~/utils/isDefined'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; type computeDraftValueFromFieldValueParams<FieldValue> = { fieldDefinition: Pick<FieldDefinition<FieldMetadata>, 'type'>; @@ -32,7 +33,9 @@ export const computeDraftValueFromFieldValue = <FieldValue>({ } return { - amount: fieldValue?.amountMicros ? fieldValue.amountMicros / 1000000 : '', + amount: isUndefinedOrNull(fieldValue?.amountMicros) + ? '' + : (fieldValue.amountMicros / 1000000).toString(), currencyCode: fieldValue?.currencyCode ?? '', } as unknown as FieldInputDraftValue<FieldValue>; } diff --git a/packages/twenty-front/src/modules/object-record/record-field/validation-schemas/emailSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/validation-schemas/emailSchema.ts new file mode 100644 index 000000000000..fae5812b3ad7 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/validation-schemas/emailSchema.ts @@ -0,0 +1,3 @@ +import { z } from 'zod'; + +export const emailSchema = z.string().email(); diff --git a/packages/twenty-front/src/modules/object-record/record-field/validation-schemas/numberFieldDefaultValueSchema.ts b/packages/twenty-front/src/modules/object-record/record-field/validation-schemas/numberFieldDefaultValueSchema.ts new file mode 100644 index 000000000000..48981e4a5a34 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/validation-schemas/numberFieldDefaultValueSchema.ts @@ -0,0 +1,5 @@ +import { z } from 'zod'; + +export const numberFieldDefaultValueSchema = z.object({ + decimals: z.number().nullable(), +}); diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingBooleanFilter.spec.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingBooleanFilter.test.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingBooleanFilter.spec.ts rename to packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingBooleanFilter.test.ts diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingCurrencyFilter.spec.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingCurrencyFilter.test.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingCurrencyFilter.spec.ts rename to packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingCurrencyFilter.test.ts diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingDateFilter.spec.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingDateFilter.test.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingDateFilter.spec.ts rename to packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingDateFilter.test.ts diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingFloatFilter.spec.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingFloatFilter.test.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingFloatFilter.spec.ts rename to packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingFloatFilter.test.ts diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingStringFilter.spec.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingStringFilter.test.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingStringFilter.spec.ts rename to packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingStringFilter.test.ts diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingUUIDFilter.spec.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingUUIDFilter.test.ts similarity index 100% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isMatchingUUIDFilter.spec.ts rename to packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isMatchingUUIDFilter.test.ts diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.spec.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isRecordMatchingFilter.test.ts similarity index 98% rename from packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.spec.ts rename to packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isRecordMatchingFilter.test.ts index ed0b22070777..1c5e1d2b4f46 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/isRecordMatchingFilter.spec.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/isRecordMatchingFilter.test.ts @@ -1,10 +1,11 @@ import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter'; import { getCompaniesMock } from '~/testing/mock-data/companies'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { Company } from '@/companies/types/Company'; import { getCompanyDomainName } from '@/object-metadata/utils/getCompanyDomainName'; -import { isRecordMatchingFilter } from './isRecordMatchingFilter'; +import { isRecordMatchingFilter } from '@/object-record/record-filter/utils/isRecordMatchingFilter'; +import { expect } from '@storybook/test'; const companiesMock = getCompaniesMock(); 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__/turnObjectDropdownFilterIntoQueryFilter.test.ts new file mode 100644 index 000000000000..6486ca29b92e --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/__tests__/turnObjectDropdownFilterIntoQueryFilter.test.ts @@ -0,0 +1,1063 @@ +import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; +import { turnObjectDropdownFilterIntoQueryFilter } from '@/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { getCompaniesMock } from '~/testing/mock-data/companies'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; + +const companiesMock = getCompaniesMock(); + +const companyMockObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'company', +)!; + +const personMockObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', +)!; + +jest.useFakeTimers().setSystemTime(new Date('2020-01-01')); + +describe('turnObjectDropdownFilterIntoQueryFilter', () => { + it('should work as expected for single filter', () => { + const companyMockNameFieldMetadataId = + companyMockObjectMetadataItem.fields.find( + (field) => field.name === 'name', + ); + + const nameFilter: Filter = { + id: 'company-name-filter', + value: companiesMock[0].name, + fieldMetadataId: companyMockNameFieldMetadataId?.id, + displayValue: companiesMock[0].name, + operand: ViewFilterOperand.Contains, + definition: { + type: 'TEXT', + fieldMetadataId: companyMockNameFieldMetadataId?.id, + label: 'Name', + iconName: 'text', + }, + }; + + const result = turnObjectDropdownFilterIntoQueryFilter( + [nameFilter], + companyMockObjectMetadataItem.fields, + ); + + expect(result).toEqual({ + name: { + ilike: '%Linkedin%', + }, + }); + }); + + it('should work as expected for multiple filters', () => { + const companyMockNameFieldMetadataId = + companyMockObjectMetadataItem.fields.find( + (field) => field.name === 'name', + ); + + const companyMockEmployeesFieldMetadataId = + companyMockObjectMetadataItem.fields.find( + (field) => field.name === 'employees', + ); + + const nameFilter: Filter = { + id: 'company-name-filter', + value: companiesMock[0].name, + fieldMetadataId: companyMockNameFieldMetadataId?.id, + displayValue: companiesMock[0].name, + operand: ViewFilterOperand.Contains, + definition: { + type: 'TEXT', + fieldMetadataId: companyMockNameFieldMetadataId?.id, + label: 'Name', + iconName: 'text', + }, + }; + + const employeesFilter: Filter = { + id: 'company-employees-filter', + value: '1000', + fieldMetadataId: companyMockEmployeesFieldMetadataId?.id, + displayValue: '1000', + operand: ViewFilterOperand.GreaterThan, + definition: { + type: 'NUMBER', + fieldMetadataId: companyMockEmployeesFieldMetadataId?.id, + label: 'Employees', + iconName: 'number', + }, + }; + + const result = turnObjectDropdownFilterIntoQueryFilter( + [nameFilter, employeesFilter], + companyMockObjectMetadataItem.fields, + ); + + expect(result).toEqual({ + and: [ + { + name: { + ilike: '%Linkedin%', + }, + }, + { + employees: { + gte: 1000, + }, + }, + ], + }); + }); +}); + +describe('should work as expected for the different field types', () => { + it('address field type', () => { + const companyMockAddressFieldMetadataId = + companyMockObjectMetadataItem.fields.find( + (field) => field.name === 'address', + ); + + const addressFilterContains: Filter = { + id: 'company-address-filter-contains', + value: '123 Main St', + fieldMetadataId: companyMockAddressFieldMetadataId?.id, + displayValue: '123 Main St', + operand: ViewFilterOperand.Contains, + definition: { + type: 'ADDRESS', + fieldMetadataId: companyMockAddressFieldMetadataId?.id, + label: 'Address', + iconName: 'address', + }, + }; + + const addressFilterDoesNotContain: Filter = { + id: 'company-address-filter-does-not-contain', + value: '123 Main St', + fieldMetadataId: companyMockAddressFieldMetadataId?.id, + displayValue: '123 Main St', + operand: ViewFilterOperand.DoesNotContain, + definition: { + type: 'ADDRESS', + fieldMetadataId: companyMockAddressFieldMetadataId?.id, + label: 'Address', + iconName: 'address', + }, + }; + + const addressFilterIsEmpty: Filter = { + id: 'company-address-filter-is-empty', + value: '', + fieldMetadataId: companyMockAddressFieldMetadataId?.id, + displayValue: '', + operand: ViewFilterOperand.IsEmpty, + definition: { + type: 'ADDRESS', + fieldMetadataId: companyMockAddressFieldMetadataId?.id, + label: 'Address', + iconName: 'address', + }, + }; + + const addressFilterIsNotEmpty: Filter = { + id: 'company-address-filter-is-not-empty', + value: '', + fieldMetadataId: companyMockAddressFieldMetadataId?.id, + displayValue: '', + operand: ViewFilterOperand.IsNotEmpty, + definition: { + type: 'ADDRESS', + fieldMetadataId: companyMockAddressFieldMetadataId?.id, + label: 'Address', + iconName: 'address', + }, + }; + + const result = turnObjectDropdownFilterIntoQueryFilter( + [ + addressFilterContains, + addressFilterDoesNotContain, + addressFilterIsEmpty, + addressFilterIsNotEmpty, + ], + companyMockObjectMetadataItem.fields, + ); + + expect(result).toEqual({ + and: [ + { + or: [ + { + address: { + addressStreet1: { + ilike: '%123 Main St%', + }, + }, + }, + { + address: { + addressStreet2: { + ilike: '%123 Main St%', + }, + }, + }, + { + address: { + addressCity: { + ilike: '%123 Main St%', + }, + }, + }, + { + address: { + addressState: { + ilike: '%123 Main St%', + }, + }, + }, + { + address: { + addressCountry: { + ilike: '%123 Main St%', + }, + }, + }, + { + address: { + addressPostcode: { + ilike: '%123 Main St%', + }, + }, + }, + ], + }, + { + and: [ + { + not: { + address: { + addressStreet1: { + ilike: '%123 Main St%', + }, + }, + }, + }, + { + not: { + address: { + addressStreet2: { + ilike: '%123 Main St%', + }, + }, + }, + }, + { + not: { + address: { + addressCity: { + ilike: '%123 Main St%', + }, + }, + }, + }, + ], + }, + { + and: [ + { + or: [ + { + address: { + addressStreet1: { + ilike: '', + }, + }, + }, + { + address: { + addressStreet1: { + is: 'NULL', + }, + }, + }, + ], + }, + { + or: [ + { + address: { + addressStreet2: { + ilike: '', + }, + }, + }, + { + address: { + addressStreet2: { + is: 'NULL', + }, + }, + }, + ], + }, + { + or: [ + { + address: { + addressCity: { + ilike: '', + }, + }, + }, + { + address: { + addressCity: { + is: 'NULL', + }, + }, + }, + ], + }, + { + or: [ + { + address: { + addressState: { + ilike: '', + }, + }, + }, + { + address: { + addressState: { + is: 'NULL', + }, + }, + }, + ], + }, + { + or: [ + { + address: { + addressCountry: { + ilike: '', + }, + }, + }, + { + address: { + addressCountry: { + is: 'NULL', + }, + }, + }, + ], + }, + { + or: [ + { + address: { + addressPostcode: { + ilike: '', + }, + }, + }, + { + address: { + addressPostcode: { + is: 'NULL', + }, + }, + }, + ], + }, + ], + }, + { + not: { + and: [ + { + or: [ + { + address: { + addressStreet1: { + ilike: '', + }, + }, + }, + { + address: { + addressStreet1: { + is: 'NULL', + }, + }, + }, + ], + }, + { + or: [ + { + address: { + addressStreet2: { + ilike: '', + }, + }, + }, + { + address: { + addressStreet2: { + is: 'NULL', + }, + }, + }, + ], + }, + { + or: [ + { + address: { + addressCity: { + ilike: '', + }, + }, + }, + { + address: { + addressCity: { + is: 'NULL', + }, + }, + }, + ], + }, + { + or: [ + { + address: { + addressState: { + ilike: '', + }, + }, + }, + { + address: { + addressState: { + is: 'NULL', + }, + }, + }, + ], + }, + { + or: [ + { + address: { + addressCountry: { + ilike: '', + }, + }, + }, + { + address: { + addressCountry: { + is: 'NULL', + }, + }, + }, + ], + }, + { + or: [ + { + address: { + addressPostcode: { + ilike: '', + }, + }, + }, + { + address: { + addressPostcode: { + is: 'NULL', + }, + }, + }, + ], + }, + ], + }, + }, + ], + }); + }); + + it('phones field type', () => { + const personMockPhonesFieldMetadataId = + personMockObjectMetadataItem.fields.find( + (field) => field.name === 'phones', + ); + + const phonesFilterContains: Filter = { + id: 'person-phones-filter-contains', + value: '1234567890', + fieldMetadataId: personMockPhonesFieldMetadataId?.id, + displayValue: '1234567890', + operand: ViewFilterOperand.Contains, + definition: { + type: 'PHONES', + fieldMetadataId: personMockPhonesFieldMetadataId?.id, + label: 'Phones', + iconName: 'phone', + }, + }; + + const phonesFilterDoesNotContain: Filter = { + id: 'person-phones-filter-does-not-contain', + value: '1234567890', + fieldMetadataId: personMockPhonesFieldMetadataId?.id, + displayValue: '1234567890', + operand: ViewFilterOperand.DoesNotContain, + definition: { + type: 'PHONES', + fieldMetadataId: personMockPhonesFieldMetadataId?.id, + label: 'Phones', + iconName: 'phone', + }, + }; + + const phonesFilterIsEmpty: Filter = { + id: 'person-phones-filter-is-empty', + value: '', + fieldMetadataId: personMockPhonesFieldMetadataId?.id, + displayValue: '', + operand: ViewFilterOperand.IsEmpty, + definition: { + type: 'PHONES', + fieldMetadataId: personMockPhonesFieldMetadataId?.id, + label: 'Phones', + iconName: 'phone', + }, + }; + + const phonesFilterIsNotEmpty: Filter = { + id: 'person-phones-filter-is-not-empty', + value: '', + fieldMetadataId: personMockPhonesFieldMetadataId?.id, + displayValue: '', + operand: ViewFilterOperand.IsNotEmpty, + definition: { + type: 'PHONES', + fieldMetadataId: personMockPhonesFieldMetadataId?.id, + label: 'Phones', + iconName: 'phone', + }, + }; + + const result = turnObjectDropdownFilterIntoQueryFilter( + [ + phonesFilterContains, + phonesFilterDoesNotContain, + phonesFilterIsEmpty, + phonesFilterIsNotEmpty, + ], + personMockObjectMetadataItem.fields, + ); + + expect(result).toEqual({ + and: [ + { + or: [ + { + phones: { + primaryPhoneNumber: { + ilike: '%1234567890%', + }, + }, + }, + { + phones: { + primaryPhoneCountryCode: { + ilike: '%1234567890%', + }, + }, + }, + ], + }, + { + and: [ + { + not: { + phones: { + primaryPhoneNumber: { + ilike: '%1234567890%', + }, + }, + }, + }, + { + not: { + phones: { + primaryPhoneCountryCode: { + ilike: '%1234567890%', + }, + }, + }, + }, + ], + }, + { + and: [ + { + or: [ + { + phones: { + primaryPhoneNumber: { + is: 'NULL', + }, + }, + }, + { + phones: { + primaryPhoneNumber: { + ilike: '', + }, + }, + }, + ], + }, + { + or: [ + { + phones: { + primaryPhoneCountryCode: { + is: 'NULL', + }, + }, + }, + { + phones: { + primaryPhoneCountryCode: { + ilike: '', + }, + }, + }, + ], + }, + ], + }, + { + not: { + and: [ + { + or: [ + { + phones: { + primaryPhoneNumber: { + is: 'NULL', + }, + }, + }, + { + phones: { + primaryPhoneNumber: { + ilike: '', + }, + }, + }, + ], + }, + { + or: [ + { + phones: { + primaryPhoneCountryCode: { + is: 'NULL', + }, + }, + }, + { + phones: { + primaryPhoneCountryCode: { + ilike: '', + }, + }, + }, + ], + }, + ], + }, + }, + ], + }); + }); + + it('emails field type', () => { + const personMockEmailFieldMetadataId = + personMockObjectMetadataItem.fields.find( + (field) => field.name === 'emails', + ); + + const emailsFilterContains: Filter = { + id: 'person-emails-filter-contains', + value: 'test@test.com', + fieldMetadataId: personMockEmailFieldMetadataId?.id, + displayValue: 'test@test.com', + operand: ViewFilterOperand.Contains, + definition: { + type: 'EMAILS', + fieldMetadataId: personMockEmailFieldMetadataId?.id, + iconName: 'email', + label: 'Emails', + }, + }; + + const emailsFilterDoesNotContain: Filter = { + id: 'person-emails-filter-does-not-contain', + value: 'test@test.com', + fieldMetadataId: personMockEmailFieldMetadataId?.id, + displayValue: 'test@test.com', + operand: ViewFilterOperand.DoesNotContain, + definition: { + type: 'EMAILS', + fieldMetadataId: personMockEmailFieldMetadataId?.id, + label: 'Emails', + iconName: 'email', + }, + }; + + const emailsFilterIsEmpty: Filter = { + id: 'person-emails-filter-is-empty', + value: '', + fieldMetadataId: personMockEmailFieldMetadataId?.id, + displayValue: '', + operand: ViewFilterOperand.IsEmpty, + definition: { + type: 'EMAILS', + label: 'Emails', + iconName: 'email', + fieldMetadataId: personMockEmailFieldMetadataId?.id, + }, + }; + + const emailsFilterIsNotEmpty: Filter = { + id: 'person-emails-filter-is-not-empty', + value: '', + fieldMetadataId: personMockEmailFieldMetadataId?.id, + displayValue: '', + operand: ViewFilterOperand.IsNotEmpty, + definition: { + type: 'EMAILS', + label: 'Emails', + iconName: 'email', + fieldMetadataId: personMockEmailFieldMetadataId?.id, + }, + }; + + const result = turnObjectDropdownFilterIntoQueryFilter( + [ + emailsFilterContains, + emailsFilterDoesNotContain, + emailsFilterIsEmpty, + emailsFilterIsNotEmpty, + ], + personMockObjectMetadataItem.fields, + ); + + expect(result).toEqual({ + and: [ + { + or: [ + { + emails: { + primaryEmail: { + ilike: '%test@test.com%', + }, + }, + }, + ], + }, + { + and: [ + { + not: { + emails: { + primaryEmail: { + ilike: '%test@test.com%', + }, + }, + }, + }, + ], + }, + { + or: [ + { + emails: { + primaryEmail: { + ilike: '', + }, + }, + }, + { + emails: { + primaryEmail: { + is: 'NULL', + }, + }, + }, + ], + }, + { + not: { + or: [ + { + emails: { + primaryEmail: { + ilike: '', + }, + }, + }, + { + emails: { + primaryEmail: { + is: 'NULL', + }, + }, + }, + ], + }, + }, + ], + }); + }); + + it('date field type', () => { + const companyMockDateFieldMetadataId = + companyMockObjectMetadataItem.fields.find( + (field) => field.name === 'createdAt', + ); + + const dateFilterIsAfter: Filter = { + id: 'company-date-filter-is-after', + value: '2024-09-17T20:46:58.922Z', + fieldMetadataId: companyMockDateFieldMetadataId?.id, + displayValue: '2024-09-17T20:46:58.922Z', + operand: ViewFilterOperand.IsAfter, + definition: { + type: 'DATE_TIME', + fieldMetadataId: companyMockDateFieldMetadataId?.id, + label: 'Created At', + iconName: 'date', + }, + }; + + const dateFilterIsBefore: Filter = { + id: 'company-date-filter-is-before', + value: '2024-09-17T20:46:58.922Z', + fieldMetadataId: companyMockDateFieldMetadataId?.id, + displayValue: '2024-09-17T20:46:58.922Z', + operand: ViewFilterOperand.IsBefore, + definition: { + type: 'DATE_TIME', + fieldMetadataId: companyMockDateFieldMetadataId?.id, + label: 'Created At', + iconName: 'date', + }, + }; + + const dateFilterIs: Filter = { + id: 'company-date-filter-is', + value: '2024-09-17T20:46:58.922Z', + fieldMetadataId: companyMockDateFieldMetadataId?.id, + displayValue: '2024-09-17T20:46:58.922Z', + operand: ViewFilterOperand.Is, + definition: { + type: 'DATE_TIME', + fieldMetadataId: companyMockDateFieldMetadataId?.id, + label: 'Created At', + iconName: 'date', + }, + }; + + const dateFilterIsEmpty: Filter = { + id: 'company-date-filter-is-empty', + value: '', + fieldMetadataId: companyMockDateFieldMetadataId?.id, + displayValue: '', + operand: ViewFilterOperand.IsEmpty, + definition: { + type: 'DATE_TIME', + fieldMetadataId: companyMockDateFieldMetadataId?.id, + label: 'Created At', + iconName: 'date', + }, + }; + + const dateFilterIsNotEmpty: Filter = { + id: 'company-date-filter-is-not-empty', + value: '', + fieldMetadataId: companyMockDateFieldMetadataId?.id, + displayValue: '', + operand: ViewFilterOperand.IsNotEmpty, + definition: { + type: 'DATE_TIME', + fieldMetadataId: companyMockDateFieldMetadataId?.id, + label: 'Created At', + iconName: 'date', + }, + }; + + const result = turnObjectDropdownFilterIntoQueryFilter( + [ + dateFilterIsAfter, + dateFilterIsBefore, + dateFilterIs, + dateFilterIsEmpty, + dateFilterIsNotEmpty, + ], + companyMockObjectMetadataItem.fields, + ); + + expect(result).toEqual({ + and: [ + { + createdAt: { + gt: '2024-09-17T20:46:58.922Z', + }, + }, + { + createdAt: { + lt: '2024-09-17T20:46:58.922Z', + }, + }, + { + and: [ + { + createdAt: { + lte: '2024-09-17T23:59:59.999Z', + }, + }, + { + createdAt: { + gte: '2024-09-17T00:00:00.000Z', + }, + }, + ], + }, + { + createdAt: { + is: 'NULL', + }, + }, + { + not: { + createdAt: { + is: 'NULL', + }, + }, + }, + ], + }); + }); + + it('number field type', () => { + const companyMockEmployeesFieldMetadataId = + companyMockObjectMetadataItem.fields.find( + (field) => field.name === 'employees', + ); + + const employeesFilterIsGreaterThan: Filter = { + id: 'company-employees-filter-is-greater-than', + value: '1000', + fieldMetadataId: companyMockEmployeesFieldMetadataId?.id, + displayValue: '1000', + operand: ViewFilterOperand.GreaterThan, + definition: { + type: 'NUMBER', + fieldMetadataId: companyMockEmployeesFieldMetadataId?.id, + label: 'Employees', + iconName: 'number', + }, + }; + + const employeesFilterIsLessThan: Filter = { + id: 'company-employees-filter-is-less-than', + value: '1000', + fieldMetadataId: companyMockEmployeesFieldMetadataId?.id, + displayValue: '1000', + operand: ViewFilterOperand.LessThan, + definition: { + type: 'NUMBER', + fieldMetadataId: companyMockEmployeesFieldMetadataId?.id, + label: 'Employees', + iconName: 'number', + }, + }; + + const employeesFilterIsEmpty: Filter = { + id: 'company-employees-filter-is-empty', + value: '', + fieldMetadataId: companyMockEmployeesFieldMetadataId?.id, + displayValue: '', + operand: ViewFilterOperand.IsEmpty, + definition: { + type: 'NUMBER', + fieldMetadataId: companyMockEmployeesFieldMetadataId?.id, + label: 'Employees', + iconName: 'number', + }, + }; + + const employeesFilterIsNotEmpty: Filter = { + id: 'company-employees-filter-is-not-empty', + value: '', + fieldMetadataId: companyMockEmployeesFieldMetadataId?.id, + displayValue: '', + operand: ViewFilterOperand.IsNotEmpty, + definition: { + type: 'NUMBER', + fieldMetadataId: companyMockEmployeesFieldMetadataId?.id, + label: 'Employees', + iconName: 'number', + }, + }; + + const result = turnObjectDropdownFilterIntoQueryFilter( + [ + employeesFilterIsGreaterThan, + employeesFilterIsLessThan, + employeesFilterIsEmpty, + employeesFilterIsNotEmpty, + ], + companyMockObjectMetadataItem.fields, + ); + + expect(result).toEqual({ + and: [ + { + employees: { + gte: 1000, + }, + }, + { + employees: { + lte: 1000, + }, + }, + { + employees: { + is: 'NULL', + }, + }, + { + not: { + employees: { + is: 'NULL', + }, + }, + }, + ], + }); + }); +}); diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/applyEmptyFilters.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/applyEmptyFilters.ts new file mode 100644 index 000000000000..5d4890f42e8f --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/applyEmptyFilters.ts @@ -0,0 +1,339 @@ +import { + ActorFilter, + AddressFilter, + CurrencyFilter, + DateFilter, + EmailsFilter, + FloatFilter, + RecordGqlOperationFilter, + RelationFilter, + StringFilter, + URLFilter, + UUIDFilter, +} from '@/object-record/graphql/types/RecordGqlOperationFilter'; +import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { isNonEmptyString } from '@sniptt/guards'; +import { Field } from '~/generated/graphql'; +import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields'; + +// TODO: fix this +export const applyEmptyFilters = ( + operand: ViewFilterOperand, + correspondingField: Pick<Field, 'id' | 'name'>, + objectRecordFilters: RecordGqlOperationFilter[], + definition: FilterDefinition, +) => { + let emptyRecordFilter: RecordGqlOperationFilter = {}; + + const compositeFieldName = definition.compositeFieldName; + + const isCompositeField = isNonEmptyString(compositeFieldName); + + switch (definition.type) { + case 'TEXT': + case 'EMAIL': + case 'PHONE': + emptyRecordFilter = { + or: [ + { [correspondingField.name]: { ilike: '' } as StringFilter }, + { [correspondingField.name]: { is: 'NULL' } as StringFilter }, + ], + }; + break; + case 'PHONES': { + if (!isCompositeField) { + const phonesFilter = generateILikeFiltersForCompositeFields( + '', + correspondingField.name, + ['primaryPhoneNumber', 'primaryPhoneCountryCode'], + true, + ); + + emptyRecordFilter = { + and: phonesFilter, + }; + break; + } else { + emptyRecordFilter = { + or: [ + { + [correspondingField.name]: { + [compositeFieldName]: { ilike: '' }, + } as StringFilter, + }, + { + [correspondingField.name]: { + [compositeFieldName]: { is: 'NULL' }, + } as StringFilter, + }, + ], + }; + break; + } + } + case 'CURRENCY': + emptyRecordFilter = { + or: [ + { + [correspondingField.name]: { + amountMicros: { is: 'NULL' }, + } as CurrencyFilter, + }, + ], + }; + break; + case 'FULL_NAME': { + if (!isCompositeField) { + const fullNameFilters = generateILikeFiltersForCompositeFields( + '', + correspondingField.name, + ['firstName', 'lastName'], + true, + ); + + emptyRecordFilter = { + and: fullNameFilters, + }; + } else { + emptyRecordFilter = { + or: [ + { + [correspondingField.name]: { + [compositeFieldName]: { ilike: '' }, + }, + }, + { + [correspondingField.name]: { + [compositeFieldName]: { is: 'NULL' }, + }, + }, + ], + }; + } + break; + } + case 'LINK': + emptyRecordFilter = { + or: [ + { [correspondingField.name]: { url: { ilike: '' } } as URLFilter }, + { + [correspondingField.name]: { url: { is: 'NULL' } } as URLFilter, + }, + ], + }; + break; + case 'LINKS': { + if (!isCompositeField) { + const linksFilters = generateILikeFiltersForCompositeFields( + '', + correspondingField.name, + ['primaryLinkLabel', 'primaryLinkUrl'], + true, + ); + + emptyRecordFilter = { + and: linksFilters, + }; + } else { + emptyRecordFilter = { + or: [ + { + [correspondingField.name]: { + [compositeFieldName]: { ilike: '' }, + } as URLFilter, + }, + { + [correspondingField.name]: { + [compositeFieldName]: { is: 'NULL' }, + } as URLFilter, + }, + ], + }; + } + break; + } + case 'ADDRESS': + if (!isCompositeField) { + emptyRecordFilter = { + and: [ + { + or: [ + { + [correspondingField.name]: { + addressStreet1: { ilike: '' }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressStreet1: { is: 'NULL' }, + } as AddressFilter, + }, + ], + }, + { + or: [ + { + [correspondingField.name]: { + addressStreet2: { ilike: '' }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressStreet2: { is: 'NULL' }, + } as AddressFilter, + }, + ], + }, + { + or: [ + { + [correspondingField.name]: { + addressCity: { ilike: '' }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressCity: { is: 'NULL' }, + } as AddressFilter, + }, + ], + }, + { + or: [ + { + [correspondingField.name]: { + addressState: { ilike: '' }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressState: { is: 'NULL' }, + } as AddressFilter, + }, + ], + }, + { + or: [ + { + [correspondingField.name]: { + addressCountry: { ilike: '' }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressCountry: { is: 'NULL' }, + } as AddressFilter, + }, + ], + }, + { + or: [ + { + [correspondingField.name]: { + addressPostcode: { ilike: '' }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressPostcode: { is: 'NULL' }, + } as AddressFilter, + }, + ], + }, + ], + }; + } else { + emptyRecordFilter = { + or: [ + { + [correspondingField.name]: { + [compositeFieldName]: { ilike: '' }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + [compositeFieldName]: { is: 'NULL' }, + } as AddressFilter, + }, + ], + }; + } + break; + case 'NUMBER': + emptyRecordFilter = { + [correspondingField.name]: { is: 'NULL' } as FloatFilter, + }; + break; + case 'RATING': + emptyRecordFilter = { + [correspondingField.name]: { is: 'NULL' } as StringFilter, + }; + break; + case 'DATE': + case 'DATE_TIME': + emptyRecordFilter = { + [correspondingField.name]: { is: 'NULL' } as DateFilter, + }; + break; + case 'SELECT': + emptyRecordFilter = { + [correspondingField.name]: { is: 'NULL' } as UUIDFilter, + }; + break; + case 'RELATION': + emptyRecordFilter = { + [correspondingField.name + 'Id']: { is: 'NULL' } as RelationFilter, + }; + break; + case 'ACTOR': + emptyRecordFilter = { + or: [ + { + [correspondingField.name]: { + name: { ilike: '' }, + } as ActorFilter, + }, + { + [correspondingField.name]: { + name: { is: 'NULL' }, + } as ActorFilter, + }, + ], + }; + break; + case 'EMAILS': + emptyRecordFilter = { + or: [ + { + [correspondingField.name]: { + primaryEmail: { ilike: '' }, + } as EmailsFilter, + }, + { + [correspondingField.name]: { + primaryEmail: { is: 'NULL' }, + } as EmailsFilter, + }, + ], + }; + break; + default: + throw new Error(`Unsupported empty filter type ${definition.type}`); + } + + switch (operand) { + case ViewFilterOperand.IsEmpty: + objectRecordFilters.push(emptyRecordFilter); + break; + case ViewFilterOperand.IsNotEmpty: + objectRecordFilters.push({ + not: emptyRecordFilter, + }); + break; + default: + throw new Error( + `Unknown operand ${operand} for ${definition.type} filter`, + ); + } +}; 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 3b0e9f8c2c72..ea849199cdda 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 @@ -13,7 +13,6 @@ import { URLFilter, UUIDFilter, } from '@/object-record/graphql/types/RecordGqlOperationFilter'; -import { FilterType } from '@/object-record/object-filter-dropdown/types/FilterType'; import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { Field } from '~/generated/graphql'; @@ -25,261 +24,17 @@ import { convertLessThanRatingToArrayOfRatingValues, convertRatingToRatingValue, } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRatingInput'; -import { Filter } from '../../object-filter-dropdown/types/Filter'; - -export type ObjectDropdownFilter = Omit<Filter, 'definition'> & { - definition: { - type: Filter['definition']['type']; - }; -}; - -const applyEmptyFilters = ( - operand: ViewFilterOperand, - correspondingField: Pick<Field, 'id' | 'name'>, - objectRecordFilters: RecordGqlOperationFilter[], - filterType: FilterType, -) => { - let emptyRecordFilter: RecordGqlOperationFilter = {}; - - switch (filterType) { - case 'TEXT': - case 'EMAIL': - case 'PHONE': - emptyRecordFilter = { - or: [ - { [correspondingField.name]: { ilike: '' } as StringFilter }, - { [correspondingField.name]: { is: 'NULL' } as StringFilter }, - ], - }; - break; - case 'PHONES': { - const phonesFilter = generateILikeFiltersForCompositeFields( - '', - correspondingField.name, - ['primaryPhoneNumber', 'primaryPhoneCountryCode'], - true, - ); - - emptyRecordFilter = { - and: phonesFilter, - }; - break; - } - case 'CURRENCY': - emptyRecordFilter = { - or: [ - { - [correspondingField.name]: { - amountMicros: { is: 'NULL' }, - } as CurrencyFilter, - }, - ], - }; - break; - case 'FULL_NAME': { - const fullNameFilters = generateILikeFiltersForCompositeFields( - '', - correspondingField.name, - ['firstName', 'lastName'], - true, - ); - - emptyRecordFilter = { - and: fullNameFilters, - }; - break; - } - case 'LINK': - emptyRecordFilter = { - or: [ - { [correspondingField.name]: { url: { ilike: '' } } as URLFilter }, - { - [correspondingField.name]: { url: { is: 'NULL' } } as URLFilter, - }, - ], - }; - break; - case 'LINKS': { - const linksFilters = generateILikeFiltersForCompositeFields( - '', - correspondingField.name, - ['primaryLinkLabel', 'primaryLinkUrl'], - true, - ); - - emptyRecordFilter = { - and: linksFilters, - }; - break; - } - case 'ADDRESS': - emptyRecordFilter = { - and: [ - { - or: [ - { - [correspondingField.name]: { - addressStreet1: { ilike: '' }, - } as AddressFilter, - }, - { - [correspondingField.name]: { - addressStreet1: { is: 'NULL' }, - } as AddressFilter, - }, - ], - }, - { - or: [ - { - [correspondingField.name]: { - addressStreet2: { ilike: '' }, - } as AddressFilter, - }, - { - [correspondingField.name]: { - addressStreet2: { is: 'NULL' }, - } as AddressFilter, - }, - ], - }, - { - or: [ - { - [correspondingField.name]: { - addressCity: { ilike: '' }, - } as AddressFilter, - }, - { - [correspondingField.name]: { - addressCity: { is: 'NULL' }, - } as AddressFilter, - }, - ], - }, - { - or: [ - { - [correspondingField.name]: { - addressState: { ilike: '' }, - } as AddressFilter, - }, - { - [correspondingField.name]: { - addressState: { is: 'NULL' }, - } as AddressFilter, - }, - ], - }, - { - or: [ - { - [correspondingField.name]: { - addressCountry: { ilike: '' }, - } as AddressFilter, - }, - { - [correspondingField.name]: { - addressCountry: { is: 'NULL' }, - } as AddressFilter, - }, - ], - }, - { - or: [ - { - [correspondingField.name]: { - addressPostcode: { ilike: '' }, - } as AddressFilter, - }, - { - [correspondingField.name]: { - addressPostcode: { is: 'NULL' }, - } as AddressFilter, - }, - ], - }, - ], - }; - break; - case 'NUMBER': - emptyRecordFilter = { - [correspondingField.name]: { is: 'NULL' } as FloatFilter, - }; - break; - case 'RATING': - emptyRecordFilter = { - [correspondingField.name]: { is: 'NULL' } as StringFilter, - }; - break; - case 'DATE': - case 'DATE_TIME': - emptyRecordFilter = { - [correspondingField.name]: { is: 'NULL' } as DateFilter, - }; - break; - case 'SELECT': - emptyRecordFilter = { - [correspondingField.name]: { is: 'NULL' } as UUIDFilter, - }; - break; - case 'RELATION': - emptyRecordFilter = { - [correspondingField.name + 'Id']: { is: 'NULL' } as RelationFilter, - }; - break; - case 'ACTOR': - emptyRecordFilter = { - or: [ - { - [correspondingField.name]: { - name: { ilike: '' }, - } as ActorFilter, - }, - { - [correspondingField.name]: { - name: { is: 'NULL' }, - } as ActorFilter, - }, - ], - }; - break; - case 'EMAILS': - emptyRecordFilter = { - or: [ - { - [correspondingField.name]: { - primaryEmail: { ilike: '' }, - } as EmailsFilter, - }, - { - [correspondingField.name]: { - primaryEmail: { is: 'NULL' }, - } as EmailsFilter, - }, - ], - }; - break; - default: - throw new Error(`Unsupported empty filter type ${filterType}`); - } - - switch (operand) { - case ViewFilterOperand.IsEmpty: - objectRecordFilters.push(emptyRecordFilter); - break; - case ViewFilterOperand.IsNotEmpty: - objectRecordFilters.push({ - not: emptyRecordFilter, - }); - break; - default: - throw new Error(`Unknown operand ${operand} for ${filterType} filter`); - } -}; +import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; +import { isActorSourceCompositeFilter } from '@/object-record/object-filter-dropdown/utils/isActorSourceCompositeFilter'; +import { applyEmptyFilters } from '@/object-record/record-filter/utils/applyEmptyFilters'; +import { resolveFilterValue } from '@/views/utils/view-filter-value/resolveFilterValue'; +import { endOfDay, roundToNearestMinutes, startOfDay } from 'date-fns'; +import { z } from 'zod'; +// TODO: break this down into smaller functions and make the whole thing immutable +// Especially applyEmptyFilters export const turnObjectDropdownFilterIntoQueryFilter = ( - rawUIFilters: ObjectDropdownFilter[], + rawUIFilters: Filter[], fields: Pick<Field, 'id' | 'name'>[], ): RecordGqlOperationFilter | undefined => { const objectRecordFilters: RecordGqlOperationFilter[] = []; @@ -289,9 +44,16 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( (field) => field.id === rawUIFilter.fieldMetadataId, ); + const compositeFieldName = rawUIFilter.definition.compositeFieldName; + + const isCompositeFieldFiter = isNonEmptyString(compositeFieldName); + const isEmptyOperand = [ ViewFilterOperand.IsEmpty, ViewFilterOperand.IsNotEmpty, + ViewFilterOperand.IsInPast, + ViewFilterOperand.IsInFuture, + ViewFilterOperand.IsToday, ].includes(rawUIFilter.operand); if (!correspondingField) { @@ -305,8 +67,6 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( } switch (rawUIFilter.definition.type) { - case 'EMAIL': - case 'PHONE': case 'TEXT': switch (rawUIFilter.operand) { case ViewFilterOperand.Contains: @@ -331,7 +91,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( rawUIFilter.operand, correspondingField, objectRecordFilters, - rawUIFilter.definition.type, + rawUIFilter.definition, ); break; default: @@ -341,37 +101,132 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( } break; case 'DATE': - case 'DATE_TIME': + case 'DATE_TIME': { + const resolvedFilterValue = resolveFilterValue(rawUIFilter); + const now = roundToNearestMinutes(new Date()); + const date = + resolvedFilterValue instanceof Date ? resolvedFilterValue : now; + switch (rawUIFilter.operand) { - case ViewFilterOperand.GreaterThan: + case ViewFilterOperand.IsAfter: { objectRecordFilters.push({ [correspondingField.name]: { - gte: rawUIFilter.value, + gt: date.toISOString(), } as DateFilter, }); break; - case ViewFilterOperand.LessThan: + } + case ViewFilterOperand.IsBefore: { objectRecordFilters.push({ [correspondingField.name]: { - lte: rawUIFilter.value, + lt: date.toISOString(), } as DateFilter, }); break; + } case ViewFilterOperand.IsEmpty: - case ViewFilterOperand.IsNotEmpty: + case ViewFilterOperand.IsNotEmpty: { applyEmptyFilters( rawUIFilter.operand, correspondingField, objectRecordFilters, - rawUIFilter.definition.type, + rawUIFilter.definition, ); break; + } + case ViewFilterOperand.IsRelative: { + const dateRange = z + .object({ start: z.date(), end: z.date() }) + .safeParse(resolvedFilterValue).data; + + const defaultDateRange = resolveFilterValue({ + value: 'PAST_1_DAY', + definition: { + type: 'DATE', + }, + operand: ViewFilterOperand.IsRelative, + }); + + if (!defaultDateRange) { + throw new Error('Failed to resolve default date range'); + } + + const { start, end } = dateRange ?? defaultDateRange; + + objectRecordFilters.push({ + and: [ + { + [correspondingField.name]: { + gte: start.toISOString(), + } as DateFilter, + }, + { + [correspondingField.name]: { + lte: end.toISOString(), + } as DateFilter, + }, + ], + }); + break; + } + case ViewFilterOperand.Is: { + const isValid = resolvedFilterValue instanceof Date; + const date = isValid ? resolvedFilterValue : now; + + objectRecordFilters.push({ + and: [ + { + [correspondingField.name]: { + lte: endOfDay(date).toISOString(), + } as DateFilter, + }, + { + [correspondingField.name]: { + gte: startOfDay(date).toISOString(), + } as DateFilter, + }, + ], + }); + break; + } + case ViewFilterOperand.IsInPast: + objectRecordFilters.push({ + [correspondingField.name]: { + lte: now.toISOString(), + } as DateFilter, + }); + break; + case ViewFilterOperand.IsInFuture: + objectRecordFilters.push({ + [correspondingField.name]: { + gte: now.toISOString(), + } as DateFilter, + }); + break; + case ViewFilterOperand.IsToday: { + objectRecordFilters.push({ + and: [ + { + [correspondingField.name]: { + lte: endOfDay(now).toISOString(), + } as DateFilter, + }, + { + [correspondingField.name]: { + gte: startOfDay(now).toISOString(), + } as DateFilter, + }, + ], + }); + break; + } default: throw new Error( - `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, + `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, // ); } break; + } case 'RATING': switch (rawUIFilter.operand) { case ViewFilterOperand.Is: @@ -405,7 +260,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( rawUIFilter.operand, correspondingField, objectRecordFilters, - rawUIFilter.definition.type, + rawUIFilter.definition, ); break; default: @@ -436,7 +291,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( rawUIFilter.operand, correspondingField, objectRecordFilters, - rawUIFilter.definition.type, + rawUIFilter.definition, ); break; default: @@ -491,7 +346,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( rawUIFilter.operand, correspondingField, objectRecordFilters, - rawUIFilter.definition.type, + rawUIFilter.definition, ); break; default: @@ -524,7 +379,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( rawUIFilter.operand, correspondingField, objectRecordFilters, - rawUIFilter.definition.type, + rawUIFilter.definition, ); break; default: @@ -561,7 +416,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( rawUIFilter.operand, correspondingField, objectRecordFilters, - rawUIFilter.definition.type, + rawUIFilter.definition, ); break; default: @@ -576,20 +431,43 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( correspondingField.name, ['primaryLinkLabel', 'primaryLinkUrl'], ); + switch (rawUIFilter.operand) { case ViewFilterOperand.Contains: - objectRecordFilters.push({ - or: linksFilters, - }); + if (!isCompositeFieldFiter) { + objectRecordFilters.push({ + or: linksFilters, + }); + } else { + objectRecordFilters.push({ + [correspondingField.name]: { + [compositeFieldName]: { + ilike: `%${rawUIFilter.value}%`, + }, + }, + }); + } break; case ViewFilterOperand.DoesNotContain: - objectRecordFilters.push({ - and: linksFilters.map((filter) => { - return { - not: filter, - }; - }), - }); + if (!isCompositeFieldFiter) { + objectRecordFilters.push({ + and: linksFilters.map((filter) => { + return { + not: filter, + }; + }), + }); + } else { + objectRecordFilters.push({ + not: { + [correspondingField.name]: { + [compositeFieldName]: { + ilike: `%${rawUIFilter.value}%`, + }, + }, + }, + }); + } break; case ViewFilterOperand.IsEmpty: case ViewFilterOperand.IsNotEmpty: @@ -597,7 +475,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( rawUIFilter.operand, correspondingField, objectRecordFilters, - rawUIFilter.definition.type, + rawUIFilter.definition, ); break; default: @@ -615,18 +493,40 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( ); switch (rawUIFilter.operand) { case ViewFilterOperand.Contains: - objectRecordFilters.push({ - or: fullNameFilters, - }); + if (!isCompositeFieldFiter) { + objectRecordFilters.push({ + or: fullNameFilters, + }); + } else { + objectRecordFilters.push({ + [correspondingField.name]: { + [compositeFieldName]: { + ilike: `%${rawUIFilter.value}%`, + }, + }, + }); + } break; case ViewFilterOperand.DoesNotContain: - objectRecordFilters.push({ - and: fullNameFilters.map((filter) => { - return { - not: filter, - }; - }), - }); + if (!isCompositeFieldFiter) { + objectRecordFilters.push({ + and: fullNameFilters.map((filter) => { + return { + not: filter, + }; + }), + }); + } else { + objectRecordFilters.push({ + not: { + [correspondingField.name]: { + [compositeFieldName]: { + ilike: `%${rawUIFilter.value}%`, + }, + }, + }, + }); + } break; case ViewFilterOperand.IsEmpty: case ViewFilterOperand.IsNotEmpty: @@ -634,7 +534,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( rawUIFilter.operand, correspondingField, objectRecordFilters, - rawUIFilter.definition.type, + rawUIFilter.definition, ); break; default: @@ -647,85 +547,107 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( case 'ADDRESS': switch (rawUIFilter.operand) { case ViewFilterOperand.Contains: - objectRecordFilters.push({ - or: [ - { - [correspondingField.name]: { - addressStreet1: { - ilike: `%${rawUIFilter.value}%`, - }, - } as AddressFilter, - }, - { - [correspondingField.name]: { - addressStreet2: { - ilike: `%${rawUIFilter.value}%`, - }, - } as AddressFilter, - }, - { - [correspondingField.name]: { - addressCity: { - ilike: `%${rawUIFilter.value}%`, - }, - } as AddressFilter, - }, - { - [correspondingField.name]: { - addressState: { - ilike: `%${rawUIFilter.value}%`, - }, - } as AddressFilter, - }, - { - [correspondingField.name]: { - addressCountry: { - ilike: `%${rawUIFilter.value}%`, - }, - } as AddressFilter, - }, - { - [correspondingField.name]: { - addressPostcode: { - ilike: `%${rawUIFilter.value}%`, - }, - } as AddressFilter, - }, - ], - }); - break; - case ViewFilterOperand.DoesNotContain: - objectRecordFilters.push({ - and: [ - { - not: { + if (!isCompositeFieldFiter) { + objectRecordFilters.push({ + or: [ + { [correspondingField.name]: { addressStreet1: { ilike: `%${rawUIFilter.value}%`, }, } as AddressFilter, }, - }, - { - not: { + { [correspondingField.name]: { addressStreet2: { ilike: `%${rawUIFilter.value}%`, }, } as AddressFilter, }, - }, - { - not: { + { [correspondingField.name]: { addressCity: { ilike: `%${rawUIFilter.value}%`, }, } as AddressFilter, }, + { + [correspondingField.name]: { + addressState: { + ilike: `%${rawUIFilter.value}%`, + }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressCountry: { + ilike: `%${rawUIFilter.value}%`, + }, + } as AddressFilter, + }, + { + [correspondingField.name]: { + addressPostcode: { + ilike: `%${rawUIFilter.value}%`, + }, + } as AddressFilter, + }, + ], + }); + } else { + objectRecordFilters.push({ + [correspondingField.name]: { + [compositeFieldName]: { + ilike: `%${rawUIFilter.value}%`, + } as AddressFilter, }, - ], - }); + }); + } + break; + case ViewFilterOperand.DoesNotContain: + if (!isCompositeFieldFiter) { + objectRecordFilters.push({ + and: [ + { + not: { + [correspondingField.name]: { + addressStreet1: { + ilike: `%${rawUIFilter.value}%`, + }, + } as AddressFilter, + }, + }, + { + not: { + [correspondingField.name]: { + addressStreet2: { + ilike: `%${rawUIFilter.value}%`, + }, + } as AddressFilter, + }, + }, + { + not: { + [correspondingField.name]: { + addressCity: { + ilike: `%${rawUIFilter.value}%`, + }, + } as AddressFilter, + }, + }, + ], + }); + } else { + objectRecordFilters.push({ + not: { + [correspondingField.name]: { + [compositeFieldName]: { + ilike: `%${rawUIFilter.value}%`, + } as AddressFilter, + }, + }, + }); + } break; case ViewFilterOperand.IsEmpty: case ViewFilterOperand.IsNotEmpty: @@ -733,7 +655,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( rawUIFilter.operand, correspondingField, objectRecordFilters, - rawUIFilter.definition.type, + rawUIFilter.definition, ); break; default: @@ -748,7 +670,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( rawUIFilter.operand, correspondingField, objectRecordFilters, - rawUIFilter.definition.type, + rawUIFilter.definition, ); break; } @@ -794,48 +716,78 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( break; } case 'ACTOR': - switch (rawUIFilter.operand) { - case ViewFilterOperand.Contains: - objectRecordFilters.push({ - or: [ - { - [correspondingField.name]: { - name: { - ilike: `%${rawUIFilter.value}%`, - }, - } as ActorFilter, + if (isActorSourceCompositeFilter(rawUIFilter.definition)) { + const parsedRecordIds = JSON.parse(rawUIFilter.value) as string[]; + + switch (rawUIFilter.operand) { + case ViewFilterOperand.Is: + objectRecordFilters.push({ + [correspondingField.name]: { + source: { + in: parsedRecordIds, + } as RelationFilter, }, - ], - }); - break; - case ViewFilterOperand.DoesNotContain: - objectRecordFilters.push({ - and: [ - { + }); + + break; + case ViewFilterOperand.IsNot: + if (parsedRecordIds.length > 0) { + objectRecordFilters.push({ not: { + [correspondingField.name]: { + source: { + in: parsedRecordIds, + } as RelationFilter, + }, + }, + }); + } + break; + } + } else { + switch (rawUIFilter.operand) { + case ViewFilterOperand.Contains: + objectRecordFilters.push({ + or: [ + { [correspondingField.name]: { name: { ilike: `%${rawUIFilter.value}%`, }, } as ActorFilter, }, - }, - ], - }); - break; - case ViewFilterOperand.IsEmpty: - case ViewFilterOperand.IsNotEmpty: - applyEmptyFilters( - rawUIFilter.operand, - correspondingField, - objectRecordFilters, - rawUIFilter.definition.type, - ); - break; - default: - throw new Error( - `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.type} filter`, - ); + ], + }); + break; + case ViewFilterOperand.DoesNotContain: + objectRecordFilters.push({ + and: [ + { + not: { + [correspondingField.name]: { + name: { + ilike: `%${rawUIFilter.value}%`, + }, + } as ActorFilter, + }, + }, + ], + }); + break; + case ViewFilterOperand.IsEmpty: + case ViewFilterOperand.IsNotEmpty: + applyEmptyFilters( + rawUIFilter.operand, + correspondingField, + objectRecordFilters, + rawUIFilter.definition, + ); + break; + default: + throw new Error( + `Unknown operand ${rawUIFilter.operand} for ${rawUIFilter.definition.label} filter`, + ); + } } break; case 'EMAILS': @@ -874,7 +826,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( rawUIFilter.operand, correspondingField, objectRecordFilters, - rawUIFilter.definition.type, + rawUIFilter.definition, ); break; default: @@ -910,7 +862,7 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( rawUIFilter.operand, correspondingField, objectRecordFilters, - rawUIFilter.definition.type, + rawUIFilter.definition, ); break; default: 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 ba3abec91e75..c4ad79ed412f 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,6 +2,8 @@ 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 { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { getObjectSlug } from '@/object-metadata/utils/getObjectSlug'; import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar'; @@ -129,10 +131,33 @@ export const RecordIndexBoardDataLoaderEffect = ({ callback: resetRecordSelection, }); + const setContextStoreTargetedRecordIds = useSetRecoilState( + contextStoreTargetedRecordIdsState, + ); + + const setContextStoreCurrentObjectMetadataItem = useSetRecoilState( + contextStoreCurrentObjectMetadataIdState, + ); + useEffect(() => { setActionBarEntries?.(); setContextMenuEntries?.(); }, [setActionBarEntries, setContextMenuEntries]); + useEffect(() => { + setContextStoreTargetedRecordIds(selectedRecordIds); + setContextStoreCurrentObjectMetadataItem(objectMetadataItem?.id); + + return () => { + setContextStoreTargetedRecordIds([]); + setContextStoreCurrentObjectMetadataItem(null); + }; + }, [ + objectMetadataItem?.id, + selectedRecordIds, + setContextStoreCurrentObjectMetadataItem, + setContextStoreTargetedRecordIds, + ]); + return <></>; }; 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 ea224bda6f81..4c3826fa8b01 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 @@ -1,24 +1,21 @@ +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; import { useAddNewCard } from '@/object-record/record-board/record-board-column/hooks/useAddNewCard'; +import { useIsOpportunitiesCompanyFieldDisabled } from '@/object-record/record-board/record-board-column/hooks/useIsOpportunitiesCompanyFieldDisabled'; import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition'; import { RecordIndexPageKanbanAddMenuItem } from '@/object-record/record-index/components/RecordIndexPageKanbanAddMenuItem'; import { RecordIndexRootPropsContext } from '@/object-record/record-index/contexts/RecordIndexRootPropsContext'; -import { useRecordIndexPageKanbanAddButton } from '@/object-record/record-index/hooks/useRecordIndexPageKanbanAddButton'; -import { SingleEntitySelect } from '@/object-record/relation-picker/components/SingleEntitySelect'; -import { useEntitySelectSearch } from '@/object-record/relation-picker/hooks/useEntitySelectSearch'; -import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; -import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; +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'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; -import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; import styled from '@emotion/styled'; -import { useCallback, useContext, useState } from 'react'; +import { useCallback, useContext } from 'react'; import { useRecoilValue } from 'recoil'; -import { IconPlus, isDefined } from 'twenty-ui'; +import { IconPlus } from 'twenty-ui'; const StyledDropdownMenuItemsContainer = styled(DropdownMenuItemsContainer)` width: 100%; @@ -30,13 +27,21 @@ const StyledDropDownMenu = styled(DropdownMenu)` export const RecordIndexPageKanbanAddButton = () => { const dropdownId = `record-index-page-add-button-dropdown`; - const [isSelectingCompany, setIsSelectingCompany] = useState(false); - const [selectedColumnDefinition, setSelectedColumnDefinition] = - useState<RecordBoardColumnDefinition>(); - const { recordIndexId, objectNamePlural } = useContext( + const { recordIndexId, objectNameSingular } = useContext( RecordIndexRootPropsContext, ); + const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular }); + + const recordIndexKanbanFieldMetadataId = useRecoilValue( + recordIndexKanbanFieldMetadataIdState, + ); + + const selectFieldMetadataItem = objectMetadataItem.fields.find( + (field) => field.id === recordIndexKanbanFieldMetadataId, + ); + const isOpportunity = + objectMetadataItem.nameSingular === CoreObjectNameSingular.Opportunity; const { columnIdsState, visibleFieldDefinitionsState } = useRecordBoardStates(recordIndexId); @@ -48,73 +53,32 @@ export const RecordIndexPageKanbanAddButton = () => { (field) => field.isLabelIdentifier, ); - const { - setHotkeyScopeAndMemorizePreviousScope, - goBackToPreviousHotkeyScope, - } = usePreviousHotkeyScope(); - const { resetSearchFilter } = useEntitySelectSearch({ - relationPickerScopeId: 'relation-picker', - }); - const { closeDropdown } = useDropdown(dropdownId); - - const { selectFieldMetadataItem, isOpportunity, createOpportunity } = - useRecordIndexPageKanbanAddButton({ - objectNamePlural, - }); - + const { isOpportunitiesCompanyFieldDisabled } = + useIsOpportunitiesCompanyFieldDisabled(); const { handleAddNewCardClick } = useAddNewCard(); const handleItemClick = useCallback( (columnDefinition: RecordBoardColumnDefinition) => { - if (isOpportunity) { - setIsSelectingCompany(true); - setSelectedColumnDefinition(columnDefinition); - setHotkeyScopeAndMemorizePreviousScope( - RelationPickerHotkeyScope.RelationPicker, - ); - } else { - handleAddNewCardClick( - labelIdentifierField?.label ?? '', - '', - 'first', - columnDefinition.id, - ); - closeDropdown(); - } + const isOpportunityEnabled = + isOpportunity && !isOpportunitiesCompanyFieldDisabled; + handleAddNewCardClick( + labelIdentifierField?.label ?? '', + '', + 'first', + isOpportunityEnabled, + columnDefinition.id, + ); + closeDropdown(); }, [ isOpportunity, handleAddNewCardClick, - setHotkeyScopeAndMemorizePreviousScope, closeDropdown, labelIdentifierField, + isOpportunitiesCompanyFieldDisabled, ], ); - const handleEntitySelect = useCallback( - (company?: EntityForSelect) => { - setIsSelectingCompany(false); - goBackToPreviousHotkeyScope(); - resetSearchFilter(); - if (isDefined(company) && isDefined(selectedColumnDefinition)) { - createOpportunity(company, selectedColumnDefinition); - } - closeDropdown(); - }, - [ - createOpportunity, - goBackToPreviousHotkeyScope, - resetSearchFilter, - selectedColumnDefinition, - closeDropdown, - ], - ); - - const handleCancel = useCallback(() => { - resetSearchFilter(); - goBackToPreviousHotkeyScope(); - setIsSelectingCompany(false); - }, [goBackToPreviousHotkeyScope, resetSearchFilter]); if (!selectFieldMetadataItem) { return null; @@ -137,27 +101,16 @@ export const RecordIndexPageKanbanAddButton = () => { dropdownId={dropdownId} dropdownComponents={ <StyledDropDownMenu> - {isOpportunity && isSelectingCompany ? ( - <SingleEntitySelect - disableBackgroundBlur - onCancel={handleCancel} - onEntitySelected={handleEntitySelect} - relationObjectNameSingular={CoreObjectNameSingular.Company} - relationPickerScopeId="relation-picker" - selectedRelationRecordIds={[]} - /> - ) : ( - <StyledDropdownMenuItemsContainer> - {columnIds.map((columnId) => ( - <RecordIndexPageKanbanAddMenuItem - key={columnId} - columnId={columnId} - recordIndexId={recordIndexId} - onItemClick={handleItemClick} - /> - ))} - </StyledDropdownMenuItemsContainer> - )} + <StyledDropdownMenuItemsContainer> + {columnIds.map((columnId) => ( + <RecordIndexPageKanbanAddMenuItem + key={columnId} + columnId={columnId} + recordIndexId={recordIndexId} + onItemClick={handleItemClick} + /> + ))} + </StyledDropdownMenuItemsContainer> </StyledDropDownMenu> } dropdownHotkeyScope={{ scope: dropdownId }} 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 428537f69467..ce8bb6572d8c 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,6 +1,8 @@ import { useEffect } from 'react'; -import { useRecoilValue } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; +import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useRecordActionBar } from '@/object-record/record-action-bar/hooks/useRecordActionBar'; @@ -34,6 +36,14 @@ export const RecordIndexTableContainerEffect = ({ recordTableId, }); + const setContextStoreTargetedRecordIds = useSetRecoilState( + contextStoreTargetedRecordIdsState, + ); + + const setContextStoreCurrentObjectMetadataItem = useSetRecoilState( + contextStoreCurrentObjectMetadataIdState, + ); + const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, }); @@ -111,5 +121,20 @@ export const RecordIndexTableContainerEffect = ({ ); }, [setRecordCountInCurrentView, setOnEntityCountChange]); + useEffect(() => { + setContextStoreTargetedRecordIds(selectedRowIds); + setContextStoreCurrentObjectMetadataItem(objectMetadataItem?.id); + + return () => { + setContextStoreTargetedRecordIds([]); + setContextStoreCurrentObjectMetadataItem(null); + }; + }, [ + objectMetadataItem?.id, + selectedRowIds, + setContextStoreCurrentObjectMetadataItem, + setContextStoreTargetedRecordIds, + ]); + return <></>; }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts index 41d4fc49d99c..f15fe3ea9661 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useHandleToggleColumnFilter.ts @@ -5,7 +5,8 @@ import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/u import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { getFilterTypeFromFieldType } from '@/object-metadata/utils/formatFieldMetadataItemsAsFilterDefinitions'; import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; -import { getOperandsForFilterType } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType'; +import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; +import { getOperandsForFilterDefinition } from '@/object-record/object-filter-dropdown/utils/getOperandsForFilterType'; import { useDropdownV2 } from '@/ui/layout/dropdown/hooks/useDropdownV2'; import { useUpsertCombinedViewFilters } from '@/views/hooks/useUpsertCombinedViewFilters'; import { isDefined } from '~/utils/isDefined'; @@ -42,7 +43,15 @@ export const useHandleToggleColumnFilter = ({ correspondingColumnDefinition?.type, ); - const availableOperandsForFilter = getOperandsForFilterType(filterType); + const filterDefinition = { + label: correspondingColumnDefinition.label, + iconName: correspondingColumnDefinition.iconName, + fieldMetadataId, + type: filterType, + } satisfies FilterDefinition; + + const availableOperandsForFilter = + getOperandsForFilterDefinition(filterDefinition); const defaultOperand = availableOperandsForFilter[0]; @@ -51,12 +60,7 @@ export const useHandleToggleColumnFilter = ({ fieldMetadataId, operand: defaultOperand, displayValue: '', - definition: { - label: correspondingColumnDefinition.label, - iconName: correspondingColumnDefinition.iconName, - fieldMetadataId, - type: filterType, - }, + definition: filterDefinition, value: '', }; 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 36947097459e..df178df4c4fd 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 @@ -1,5 +1,6 @@ import { useRecoilValue } from 'recoil'; +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; @@ -9,6 +10,7 @@ import { useRecordTableRecordGqlFields } from '@/object-record/record-index/hook import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; import { SIGN_IN_BACKGROUND_MOCK_COMPANIES } from '@/sign-in-background-mock/constants/SignInBackgroundMockCompanies'; +import { isNull } from '@sniptt/guards'; import { WorkspaceActivationStatus } from '~/generated/graphql'; export const useFindManyParams = ( @@ -43,6 +45,7 @@ export const useLoadRecordIndexTable = (objectNameSingular: string) => { const { setRecordTableData, setIsRecordTableInitialLoading } = useRecordTable(); const currentWorkspace = useRecoilValue(currentWorkspaceState); + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const params = useFindManyParams(objectNameSingular); const recordGqlFields = useRecordTableRecordGqlFields({ objectMetadataItem }); @@ -63,6 +66,7 @@ export const useLoadRecordIndexTable = (objectNameSingular: string) => { onError: () => { setIsRecordTableInitialLoading(false); }, + skip: isNull(currentWorkspaceMember), }); return { diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddButton.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddButton.ts deleted file mode 100644 index ad5754dec815..000000000000 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useRecordIndexPageKanbanAddButton.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; -import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; -import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/RecordBoardColumnDefinition'; -import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; -import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; -import { useRecoilValue } from 'recoil'; -import { isDefined } from 'twenty-ui'; - -type useRecordIndexPageKanbanAddButtonProps = { - objectNamePlural: string; -}; - -export const useRecordIndexPageKanbanAddButton = ({ - objectNamePlural, -}: useRecordIndexPageKanbanAddButtonProps) => { - const { objectNameSingular } = useObjectNameSingularFromPlural({ - objectNamePlural, - }); - const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular }); - - const recordIndexKanbanFieldMetadataId = useRecoilValue( - recordIndexKanbanFieldMetadataIdState, - ); - const { createOneRecord } = useCreateOneRecord({ objectNameSingular }); - - const selectFieldMetadataItem = objectMetadataItem.fields.find( - (field) => field.id === recordIndexKanbanFieldMetadataId, - ); - const isOpportunity = - objectMetadataItem.nameSingular === CoreObjectNameSingular.Opportunity; - - const createOpportunity = ( - company: EntityForSelect, - columnDefinition: RecordBoardColumnDefinition, - ) => { - if (isDefined(selectFieldMetadataItem)) { - createOneRecord({ - name: company.name, - companyId: company.id, - position: 'first', - [selectFieldMetadataItem.name]: columnDefinition?.value, - }); - } - }; - - return { - selectFieldMetadataItem, - isOpportunity, - createOpportunity, - }; -}; diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/constants/ExportTableDataDefaultPageSize.ts b/packages/twenty-front/src/modules/object-record/record-index/options/constants/ExportTableDataDefaultPageSize.ts new file mode 100644 index 000000000000..1a58deeb28ba --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/options/constants/ExportTableDataDefaultPageSize.ts @@ -0,0 +1 @@ +export const EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE = 200; 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__/useTableData.test.tsx index 752deafc8e7f..d670b908cb22 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__/useTableData.test.tsx @@ -1,18 +1,18 @@ -import { act, renderHook, waitFor } from '@testing-library/react'; -import { ReactNode } from 'react'; +import { renderHook, waitFor } from '@testing-library/react'; +import { act } from 'react'; import { percentage, sleep, useTableData } from '../useTableData'; +import { PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS } from '@/object-record/hooks/__mocks__/personFragments'; import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoard'; import { recordBoardKanbanFieldMetadataNameComponentState } from '@/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState'; import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard'; -import { SnackBarManagerScopeInternalContext } from '@/ui/feedback/snack-bar-manager/scopes/scope-internal-context/SnackBarManagerScopeInternalContext'; import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; import { ViewType } from '@/views/types/ViewType'; -import { MockedProvider, MockedResponse } from '@apollo/client/testing'; +import { MockedResponse } from '@apollo/client/testing'; import gql from 'graphql-tag'; -import { BrowserRouter as Router } from 'react-router-dom'; -import { RecoilRoot, useRecoilValue } from 'recoil'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { useRecoilValue } from 'recoil'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; const defaultResponseData = { pageInfo: { @@ -23,10 +23,10 @@ const defaultResponseData = { }, totalCount: 1, }; + const mockPerson = { __typename: 'Person', updatedAt: '2021-08-03T19:20:06.000Z', - myCustomObjectId: '123', whatsapp: { primaryPhoneNumber: '+1', primaryPhoneCountryCode: '234-567-890', @@ -41,7 +41,10 @@ const mockPerson = { firstName: 'firstName', lastName: 'lastName', }, - email: 'email', + emails: { + primaryEmail: 'email', + additionalEmails: [], + }, position: 'position', createdBy: { source: 'source', @@ -57,7 +60,7 @@ const mockPerson = { }, performanceRating: 1, createdAt: '2021-08-03T19:20:06.000Z', - phone: { + phones: { primaryPhoneNumber: '+1', primaryPhoneCountryCode: '234-567-890', additionalPhones: [], @@ -66,8 +69,10 @@ const mockPerson = { city: 'city', companyId: '1', intro: 'intro', - workPreference: 'workPrefereance', + deletedAt: null, + workPreference: 'workPreference', }; + const mocks: MockedResponse[] = [ { request: { @@ -86,52 +91,7 @@ const mocks: MockedResponse[] = [ ) { edges { node { - __typename - name { - firstName - lastName - } - linkedinLink { - primaryLinkUrl - primaryLinkLabel - secondaryLinks - } - deletedAt - createdAt - updatedAt - jobTitle - intro - workPrefereance - performanceRating - xLink { - primaryLinkUrl - primaryLinkLabel - secondaryLinks - } - city - companyId - phones { - primaryPhoneNumber - primaryPhoneCountryCode - additionalPhones - } - createdBy { - source - workspaceMemberId - name - } - id - position - emails { - primaryEmail - additionalEmails - } - avatarUrl - whatsapp { - primaryPhoneNumber - primaryPhoneCountryCode - additionalPhones - } + ${PERSON_FRAGMENT_WITH_DEPTH_ZERO_RELATIONS} } cursor } @@ -167,21 +127,9 @@ const mocks: MockedResponse[] = [ }, ]; -const Wrapper = ({ children }: { children: ReactNode }) => ( - <SnackBarManagerScopeInternalContext.Provider - value={{ - scopeId: 'snack-bar-manager', - }} - > - <Router> - <RecoilRoot> - <MockedProvider addTypename={false} mocks={mocks}> - {children} - </MockedProvider> - </RecoilRoot> - </Router> - </SnackBarManagerScopeInternalContext.Provider> -); +const WrapperWithResponse = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: mocks, +}); const graphqlEmptyResponse = [ { @@ -197,21 +145,9 @@ const graphqlEmptyResponse = [ }, ]; -const WrapperWithEmptyResponse = ({ children }: { children: ReactNode }) => ( - <SnackBarManagerScopeInternalContext.Provider - value={{ - scopeId: 'snack-bar-manager', - }} - > - <Router> - <RecoilRoot> - <MockedProvider addTypename={false} mocks={graphqlEmptyResponse}> - {children} - </MockedProvider> - </RecoilRoot> - </Router> - </SnackBarManagerScopeInternalContext.Provider> -); +const WrapperWithEmptyResponse = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: graphqlEmptyResponse, +}); describe('useTableData', () => { const recordIndexId = 'people'; @@ -225,6 +161,7 @@ describe('useTableData', () => { useTableData({ recordIndexId, objectNameSingular, + pageSize: 30, callback, delayMs: 0, viewType: ViewType.Kanban, @@ -249,10 +186,11 @@ describe('useTableData', () => { recordIndexId, objectNameSingular, callback, + pageSize: 30, delayMs: 0, }), - { wrapper: Wrapper }, + { wrapper: WrapperWithResponse }, ); await act(async () => { @@ -292,7 +230,7 @@ describe('useTableData', () => { }; }, { - wrapper: Wrapper, + wrapper: WrapperWithResponse, }, ); @@ -340,8 +278,10 @@ describe('useTableData', () => { relationObjectMetadataNameSingular: '', relationType: undefined, targetFieldMetadataName: '', + settings: {}, }, position: 7, + settings: {}, showLabel: undefined, size: 100, type: 'DATE_TIME', @@ -379,7 +319,7 @@ describe('useTableData', () => { }; }, { - wrapper: Wrapper, + wrapper: WrapperWithResponse, }, ); diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts index 8a535604dee8..532b8e0aa59b 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts @@ -2,6 +2,7 @@ import { json2csv } from 'json-2-csv'; 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, @@ -142,7 +143,7 @@ export const useExportTableData = ({ filename, maximumRequests = 100, objectNameSingular, - pageSize = 30, + pageSize = EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE, recordIndexId, viewType, }: UseExportTableDataOptions) => { 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/useTableData.ts index 1e6255276919..98294115c5d2 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/useTableData.ts @@ -9,6 +9,7 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { isDefined } from '~/utils/isDefined'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; +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'; @@ -43,7 +44,7 @@ export const useTableData = ({ delayMs, maximumRequests = 100, objectNameSingular, - pageSize = 30, + pageSize = EXPORT_TABLE_DATA_DEFAULT_PAGE_SIZE, recordIndexId, callback, viewType = ViewType.Table, diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx index e2c8b2bcfa03..55f3ccbf751e 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx @@ -24,15 +24,13 @@ import { type RecordInlineCellProps = { readonly?: boolean; loading?: boolean; - isCentered?: boolean; }; export const RecordInlineCell = ({ readonly, loading, - isCentered, }: RecordInlineCellProps) => { - const { fieldDefinition, recordId } = useContext(FieldContext); + const { fieldDefinition, recordId, isCentered } = useContext(FieldContext); const buttonIcon = useGetButtonIcon(); const isFieldInputOnly = useIsFieldInputOnly(); @@ -90,7 +88,7 @@ export const RecordInlineCell = ({ label: fieldDefinition.label, labelWidth: fieldDefinition.labelWidth, showLabel: fieldDefinition.showLabel, - isCentered: isCentered, + isCentered, editModeContent: ( <FieldInput recordFieldInputdId={getRecordFieldInputId( diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx index c77c19fa88c4..740039656f2d 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellContainer.tsx @@ -4,8 +4,8 @@ import { ReactElement, useContext } from 'react'; import { AppTooltip, IconComponent, - TooltipDelay, OverflowingTextWithTooltip, + TooltipDelay, } from 'twenty-ui'; import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; @@ -42,6 +42,7 @@ const StyledValueContainer = styled.div` display: flex; flex-grow: 1; min-width: 0; + position: relative; `; const StyledLabelContainer = styled.div<{ width?: number }>` @@ -55,11 +56,9 @@ const StyledInlineCellBaseContainer = styled.div` box-sizing: border-box; width: 100%; display: flex; - + height: 24px; gap: ${({ theme }) => theme.spacing(1)}; - user-select: none; - justify-content: center; `; @@ -81,7 +80,6 @@ export type RecordInlineCellContainerProps = { isDisplayModeFixHeight?: boolean; disableHoverEffect?: boolean; loading?: boolean; - isCentered?: boolean; }; export const RecordInlineCellContainer = () => { diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx index 7d6c6e4d50ed..a6a4bec4559d 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellEditMode.tsx @@ -1,17 +1,16 @@ +import { RecordInlineCellContext } from '@/object-record/record-inline-cell/components/RecordInlineCellContext'; import styled from '@emotion/styled'; import { autoUpdate, flip, offset, useFloating } from '@floating-ui/react'; import { useContext } from 'react'; import { createPortal } from 'react-dom'; -import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; - const StyledInlineCellEditModeContainer = styled.div` align-items: center; display: flex; + width: 100%; + position: absolute; height: 24px; - - margin-left: -${({ theme }) => theme.spacing(1)}; `; const StyledInlineCellInput = styled.div` @@ -34,21 +33,21 @@ type RecordInlineCellEditModeProps = { export const RecordInlineCellEditMode = ({ children, }: RecordInlineCellEditModeProps) => { - const { isCentered } = useContext(FieldContext); + const { isCentered } = useContext(RecordInlineCellContext); const { refs, floatingStyles } = useFloating({ - placement: isCentered ? undefined : 'right-start', + placement: isCentered ? 'bottom' : 'bottom-start', middleware: [ flip(), offset( isCentered ? { - mainAxis: -32, - crossAxis: 160, + mainAxis: -26, + crossAxis: 0, } : { + mainAxis: -28, crossAxis: -4, - mainAxis: -4, }, ), ], diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellSkeletonLoader.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellSkeletonLoader.tsx index 2e673478dbd5..bd912d7b55bd 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellSkeletonLoader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCellSkeletonLoader.tsx @@ -1,6 +1,7 @@ -import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; import { useTheme } from '@emotion/react'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { StyledSkeletonDiv } from './RecordInlineCellContainer'; export const RecordInlineCellSkeletonLoader = () => { @@ -13,7 +14,10 @@ export const RecordInlineCellSkeletonLoader = () => { borderRadius={4} > <StyledSkeletonDiv> - <Skeleton width={154} height={16} /> + <Skeleton + width={154} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} + /> </StyledSkeletonDiv> </SkeletonTheme> ); diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader.tsx index 38ee35d74787..88a499952c1f 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/property-box/components/PropertyBoxSkeletonLoader.tsx @@ -1,6 +1,7 @@ -import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; const StyledSkeletonDiv = styled.div` align-items: center; @@ -22,8 +23,14 @@ export const PropertyBoxSkeletonLoader = () => { > {skeletonItems.map(({ id }) => ( <StyledSkeletonDiv key={id}> - <Skeleton width={92} height={16} /> - <Skeleton width={154} height={16} /> + <Skeleton + width={92} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} + /> + <Skeleton + width={154} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} + /> </StyledSkeletonDiv> ))} </SkeletonTheme> diff --git a/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx b/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx index 49b27cdfe3eb..3a747b0913a1 100644 --- a/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx +++ b/packages/twenty-front/src/modules/object-record/record-right-drawer/components/RightDrawerRecord.tsx @@ -1,23 +1,34 @@ import { useRecoilValue } from 'recoil'; +import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading'; import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; 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 { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; +import styled from '@emotion/styled'; + +const StyledRightDrawerRecord = styled.div` + height: ${({ theme }) => + useIsMobile() ? `calc(100% - ${theme.spacing(16)})` : '100%'}; +`; export const RightDrawerRecord = () => { const viewableRecordNameSingular = useRecoilValue( viewableRecordNameSingularState, ); + const isNewViewableRecordLoading = useRecoilValue( + isNewViewableRecordLoadingState, + ); const viewableRecordId = useRecoilValue(viewableRecordIdState); - if (!viewableRecordNameSingular) { + if (!viewableRecordNameSingular && !isNewViewableRecordLoading) { throw new Error(`Object name is not defined`); } - if (!viewableRecordId) { + if (!viewableRecordId && !isNewViewableRecordLoading) { throw new Error(`Record id is not defined`); } @@ -27,14 +38,19 @@ export const RightDrawerRecord = () => { ); return ( - <RecordFieldValueSelectorContextProvider> - <RecordValueSetterEffect recordId={objectRecordId} /> - <RecordShowContainer - objectNameSingular={objectNameSingular} - objectRecordId={objectRecordId} - loading={false} - isInRightDrawer={true} - /> - </RecordFieldValueSelectorContextProvider> + <StyledRightDrawerRecord> + <RecordFieldValueSelectorContextProvider> + {!isNewViewableRecordLoading && ( + <RecordValueSetterEffect recordId={objectRecordId} /> + )} + <RecordShowContainer + objectNameSingular={objectNameSingular} + objectRecordId={objectRecordId} + loading={false} + isInRightDrawer={true} + isNewRightDrawerItemLoading={isNewViewableRecordLoading} + /> + </RecordFieldValueSelectorContextProvider> + </StyledRightDrawerRecord> ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-right-drawer/states/isNewViewableRecordLoading.ts b/packages/twenty-front/src/modules/object-record/record-right-drawer/states/isNewViewableRecordLoading.ts new file mode 100644 index 000000000000..904677204cc6 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-right-drawer/states/isNewViewableRecordLoading.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const isNewViewableRecordLoadingState = createState<boolean>({ + key: 'activities/is-new-viewable-record-loading', + defaultValue: false, +}); 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 967825f3a5d7..9b1e10601ab4 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 @@ -26,12 +26,14 @@ import { RecordDetailRelationSection } from '@/object-record/record-show/record- 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, @@ -46,6 +48,7 @@ type RecordShowContainerProps = { objectRecordId: string; loading: boolean; isInRightDrawer?: boolean; + isNewRightDrawerItemLoading?: boolean; }; export const RecordShowContainer = ({ @@ -53,6 +56,7 @@ export const RecordShowContainer = ({ objectRecordId, loading, isInRightDrawer = false, + isNewRightDrawerItemLoading = false, }: RecordShowContainerProps) => { const { objectMetadataItem } = useObjectMetadataItem({ objectNameSingular, @@ -69,7 +73,7 @@ export const RecordShowContainer = ({ recordLoadingFamilyState(objectRecordId), ); - const [recordFromStore] = useRecoilState<any>( + const [recordFromStore] = useRecoilState<ObjectRecord | null>( recordStoreFamilyState(objectRecordId), ); @@ -79,7 +83,6 @@ export const RecordShowContainer = ({ recordId: objectRecordId, }), ); - const [uploadImage] = useUploadImageMutation(); const { updateOneRecord } = useUpdateOneRecord({ objectNameSingular }); @@ -162,52 +165,53 @@ export const RecordShowContainer = ({ const isMobile = useIsMobile() || isInRightDrawer; const isPrefetchLoading = useIsPrefetchLoading(); - const summaryCard = isDefined(recordFromStore) ? ( - <ShowPageSummaryCard - isMobile={isMobile} - id={objectRecordId} - logoOrAvatar={recordIdentifier?.avatarUrl ?? ''} - icon={Icon} - iconColor={IconColor} - avatarPlaceholder={recordIdentifier?.name ?? ''} - date={recordFromStore.createdAt ?? ''} - loading={isPrefetchLoading || loading || recordLoading} - title={ - <FieldContext.Provider - value={{ - recordId: objectRecordId, - recoilScopeId: - objectRecordId + labelIdentifierFieldMetadataItem?.id, - isLabelIdentifier: false, - fieldDefinition: { - type: - labelIdentifierFieldMetadataItem?.type || - FieldMetadataType.Text, - iconName: '', - fieldMetadataId: labelIdentifierFieldMetadataItem?.id ?? '', - label: labelIdentifierFieldMetadataItem?.label || '', - metadata: { - fieldName: labelIdentifierFieldMetadataItem?.name || '', - objectMetadataNameSingular: objectNameSingular, + const summaryCard = + !isNewRightDrawerItemLoading && isDefined(recordFromStore) ? ( + <ShowPageSummaryCard + isMobile={isMobile} + id={objectRecordId} + logoOrAvatar={recordIdentifier?.avatarUrl ?? ''} + icon={Icon} + iconColor={IconColor} + avatarPlaceholder={recordIdentifier?.name ?? ''} + date={recordFromStore.createdAt ?? ''} + loading={isPrefetchLoading || loading || recordLoading} + title={ + <FieldContext.Provider + value={{ + recordId: objectRecordId, + recoilScopeId: + objectRecordId + labelIdentifierFieldMetadataItem?.id, + isLabelIdentifier: false, + fieldDefinition: { + type: + labelIdentifierFieldMetadataItem?.type || + FieldMetadataType.Text, + iconName: '', + fieldMetadataId: labelIdentifierFieldMetadataItem?.id ?? '', + label: labelIdentifierFieldMetadataItem?.label || '', + metadata: { + fieldName: labelIdentifierFieldMetadataItem?.name || '', + objectMetadataNameSingular: objectNameSingular, + }, + defaultValue: labelIdentifierFieldMetadataItem?.defaultValue, }, - defaultValue: labelIdentifierFieldMetadataItem?.defaultValue, - }, - useUpdateRecord: useUpdateOneObjectRecordMutation, - hotkeyScope: InlineCellHotkeyScope.InlineCell, - isCentered: true, - }} - > - <RecordInlineCell readonly={isReadOnly} isCentered={true} /> - </FieldContext.Provider> - } - avatarType={recordIdentifier?.avatarType ?? 'rounded'} - onUploadPicture={ - objectNameSingular === 'person' ? onUploadPicture : undefined - } - /> - ) : ( - <></> - ); + useUpdateRecord: useUpdateOneObjectRecordMutation, + hotkeyScope: InlineCellHotkeyScope.InlineCell, + isCentered: !isMobile, + }} + > + <RecordInlineCell readonly={isReadOnly} /> + </FieldContext.Provider> + } + avatarType={recordIdentifier?.avatarType ?? 'rounded'} + onUploadPicture={ + objectNameSingular === 'person' ? onUploadPicture : undefined + } + /> + ) : ( + <ShowPageSummaryCardSkeletonLoader /> + ); const fieldsBox = ( <> diff --git a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPage.ts b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPage.ts index 7bdaf0b4284b..fb73b8ce9a52 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPage.ts +++ b/packages/twenty-front/src/modules/object-record/record-show/hooks/useRecordShowPage.ts @@ -23,10 +23,10 @@ export const useRecordShowPage = ( objectRecordId: paramObjectRecordId, } = useParams(); - const objectNameSingular = propsObjectNameSingular || paramObjectNameSingular; - const objectRecordId = propsObjectRecordId || paramObjectRecordId; + const objectNameSingular = propsObjectNameSingular ?? paramObjectNameSingular; + const objectRecordId = propsObjectRecordId ?? paramObjectRecordId; - if (!objectNameSingular || !objectRecordId) { + if (!isDefined(objectNameSingular) || !isDefined(objectRecordId)) { throw new Error('Object name or Record id is not defined'); } 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 23fc1f07096f..4e2d194036f6 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 @@ -24,12 +24,15 @@ import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRela import { RelationPickerScope } from '@/object-record/relation-picker/scopes/RelationPickerScope'; import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect'; 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'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { FilterQueryParams } from '@/views/hooks/internal/useViewFromQueryParams'; +import { View } from '@/views/types/View'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { RelationDefinitionType } from '~/generated-metadata/graphql'; @@ -69,7 +72,7 @@ export const RecordDetailRelationSection = ({ // TODO: use new relation type const isToOneObject = relationType === RelationDefinitionType.ManyToOne; - const isToManyObjects = RelationDefinitionType.OneToMany; + const isToManyObjects = relationType === RelationDefinitionType.OneToMany; const relationRecords: ObjectRecord[] = fieldValue && isToOneObject @@ -119,12 +122,21 @@ export const RecordDetailRelationSection = ({ scopeId: dropdownId, }); + const { records: views } = usePrefetchedData<View>(PrefetchKey.AllViews); + + const indexView = views.find( + (view) => + view.key === 'INDEX' && + view.objectMetadataId === relationObjectMetadataItem.id, + ); + const filterQueryParams: FilterQueryParams = { filter: { [relationFieldMetadataItem?.name || '']: { [ViewFilterOperand.Is]: [recordId], }, }, + view: indexView?.id, }; const filterLinkHref = `/objects/${ relationObjectMetadataItem.namePlural diff --git a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/__stories__/RecordDetailRelationSection.stories.tsx b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/__stories__/RecordDetailRelationSection.stories.tsx index 06cb79025bc5..aca53588e1be 100644 --- a/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/__stories__/RecordDetailRelationSection.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-show/record-detail-section/components/__stories__/RecordDetailRelationSection.stories.tsx @@ -9,15 +9,23 @@ import { RecordStoreDecorator } from '~/testing/decorators/RecordStoreDecorator' import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { graphqlMocks } from '~/testing/graphqlMocks'; import { getCompaniesMock } from '~/testing/mock-data/companies'; -import { mockedCompanyObjectMetadataItem } from '~/testing/mock-data/metadata'; import { getPeopleMock } from '~/testing/mock-data/people'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { RecordDetailRelationSection } from '../RecordDetailRelationSection'; const companiesMock = getCompaniesMock(); const peopleMock = getPeopleMock(); +const mockedCompanyObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'company', +); + +if (!mockedCompanyObjectMetadataItem) { + throw new Error('Company object metadata item not found'); +} + const meta: Meta<typeof RecordDetailRelationSection> = { title: 'Modules/ObjectRecord/RecordShow/RecordDetailSection/RecordDetailRelationSection', diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx index 9b68416e8192..b866e344842f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/__stories__/perf/RecordTableCell.perf.stories.tsx @@ -21,7 +21,7 @@ import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorato import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory'; import { RecordTableCellFieldContextWrapper } from '@/object-record/record-table/record-table-cell/components/RecordTableCellFieldContextWrapper'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { mockPerformance } from './mock'; const RelationFieldValueSetterEffect = () => { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSkeletonLoader.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSkeletonLoader.tsx index b010bdaecd6d..f62018d33388 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSkeletonLoader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSkeletonLoader.tsx @@ -1,6 +1,7 @@ -import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; const StyledSkeletonContainer = styled.div` padding-left: ${({ theme }) => theme.spacing(2)}; @@ -15,7 +16,10 @@ const StyledRecordTableCellLoader = ({ width }: { width?: number }) => { highlightColor={theme.background.transparent.lighter} borderRadius={4} > - <Skeleton width={width} height={16} /> + <Skeleton + width={width} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} + /> </SkeletonTheme> ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useUpsertRecord.test.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useUpsertRecord.test.tsx index e592107c5a1b..cf18193570e2 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useUpsertRecord.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useUpsertRecord.test.tsx @@ -10,7 +10,7 @@ import { textfieldDefinition } from '@/object-record/record-field/__mocks__/fiel import { FieldContext } from '@/object-record/record-field/contexts/FieldContext'; import { useUpsertRecord } from '@/object-record/record-table/record-table-cell/hooks/useUpsertRecord'; import { TableHotkeyScope } from '@/object-record/record-table/types/TableHotkeyScope'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; const draftValue = 'updated Name'; diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx index a21a0578ab0d..566ca51cd643 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeader.tsx @@ -50,7 +50,7 @@ const StyledTableHead = styled.thead<{ clip-path: inset(0px -4px 0px 0px); } @media (max-width: ${MOBILE_VIEWPORT}px) { - width: 35px; + width: 30px; max-width: 35px; } } diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx index f325edecc22f..266defb44511 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/MultiRecordSelect.tsx @@ -23,6 +23,7 @@ import { useRecoilValue, useSetRecoilState } from 'recoil'; import { Key } from 'ts-key-enum'; import { IconPlus, isDefined } from 'twenty-ui'; import { useDebouncedCallback } from 'use-debounce'; + export const StyledSelectableItem = styled(SelectableItem)` height: 100%; width: 100%; @@ -144,19 +145,19 @@ export const MultiRecordSelect = ({ )} </> )} - {isDefined(onCreate) && ( - <> - {objectRecordsIdsMultiSelect.length > 0 && ( - <DropdownMenuSeparator /> - )} + </DropdownMenuItemsContainer> + {isDefined(onCreate) && ( + <> + <DropdownMenuSeparator /> + <DropdownMenuItemsContainer> <CreateNewButton onClick={debouncedOnCreate} LeftIcon={IconPlus} text="Add New" /> - </> - )} - </DropdownMenuItemsContainer> + </DropdownMenuItemsContainer> + </> + )} </DropdownMenu> </> ); diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.test.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.test.tsx index 9eb2e7b3642e..0aeca9fcb3aa 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.test.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.test.tsx @@ -4,7 +4,7 @@ import { RecoilRoot, useSetRecoilState } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray } from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray'; import { RelationPickerScopeInternalContext } from '@/object-record/relation-picker/scopes/scope-internal-context/RelationPickerScopeInternalContext'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; const scopeId = 'scopeId'; const Wrapper = ({ children }: { children: React.ReactNode }) => ( diff --git a/packages/twenty-front/src/modules/object-record/select/components/MultipleRecordSelectDropdown.tsx b/packages/twenty-front/src/modules/object-record/select/components/MultipleSelectDropdown.tsx similarity index 55% rename from packages/twenty-front/src/modules/object-record/select/components/MultipleRecordSelectDropdown.tsx rename to packages/twenty-front/src/modules/object-record/select/components/MultipleSelectDropdown.tsx index 3914a41633fe..fba130110ddc 100644 --- a/packages/twenty-front/src/modules/object-record/select/components/MultipleRecordSelectDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/select/components/MultipleSelectDropdown.tsx @@ -1,9 +1,10 @@ +import styled from '@emotion/styled'; import { useEffect, useState } from 'react'; import { useRecoilValue } from 'recoil'; import { Key } from 'ts-key-enum'; -import { Avatar } from 'twenty-ui'; +import { AvatarChip } from 'twenty-ui'; -import { SelectableRecord } from '@/object-record/select/types/SelectableRecord'; +import { SelectableItem } from '@/object-record/select/types/SelectableItem'; import { DropdownMenuSkeletonItem } from '@/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; @@ -14,26 +15,36 @@ import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItemMultiSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; -export const MultipleRecordSelectDropdown = ({ +const StyledAvatarChip = styled(AvatarChip)` + &.avatar-icon-container { + color: ${({ theme }) => theme.font.color.secondary}; + gap: ${({ theme }) => theme.spacing(2)}; + padding-left: 0px; + padding-right: 0px; + font-size: ${({ theme }) => theme.font.size.md}; + } +`; + +export const MultipleSelectDropdown = ({ selectableListId, hotkeyScope, - recordsToSelect, - loadingRecords, - filteredSelectedRecords, + itemsToSelect, + loadingItems, + filteredSelectedItems, onChange, searchFilter, }: { selectableListId: string; hotkeyScope: string; - recordsToSelect: SelectableRecord[]; - filteredSelectedRecords: SelectableRecord[]; - selectedRecords: SelectableRecord[]; + itemsToSelect: SelectableItem[]; + filteredSelectedItems: SelectableItem[]; + selectedItems: SelectableItem[]; searchFilter: string; onChange: ( - changedRecordToSelect: SelectableRecord, + changedItemToSelect: SelectableItem, newSelectedValue: boolean, ) => void; - loadingRecords: boolean; + loadingItems: boolean; }) => { const { closeDropdown } = useDropdown(); const { selectedItemIdState } = useSelectableListStates({ @@ -44,32 +55,32 @@ export const MultipleRecordSelectDropdown = ({ const selectedItemId = useRecoilValue(selectedItemIdState); - const handleRecordSelectChange = ( - recordToSelect: SelectableRecord, + const handleItemSelectChange = ( + itemToSelect: SelectableItem, newSelectedValue: boolean, ) => { onChange( { - ...recordToSelect, + ...itemToSelect, isSelected: newSelectedValue, }, newSelectedValue, ); }; - const [recordsInDropdown, setRecordInDropdown] = useState([ - ...(filteredSelectedRecords ?? []), - ...(recordsToSelect ?? []), + const [itemsInDropdown, setItemInDropdown] = useState([ + ...(filteredSelectedItems ?? []), + ...(itemsToSelect ?? []), ]); useEffect(() => { - if (!loadingRecords) { - setRecordInDropdown([ - ...(filteredSelectedRecords ?? []), - ...(recordsToSelect ?? []), + if (!loadingItems) { + setItemInDropdown([ + ...(filteredSelectedItems ?? []), + ...(itemsToSelect ?? []), ]); } - }, [recordsToSelect, filteredSelectedRecords, loadingRecords]); + }, [itemsToSelect, filteredSelectedItems, loadingItems]); useScopedHotkeys( [Key.Escape], @@ -82,12 +93,12 @@ export const MultipleRecordSelectDropdown = ({ ); const showNoResult = - recordsToSelect?.length === 0 && + itemsToSelect?.length === 0 && searchFilter !== '' && - filteredSelectedRecords?.length === 0 && - !loadingRecords; + filteredSelectedItems?.length === 0 && + !loadingItems; - const selectableItemIds = recordsInDropdown.map((record) => record.id); + const selectableItemIds = itemsInDropdown.map((item) => item.id); return ( <SelectableList @@ -95,45 +106,46 @@ export const MultipleRecordSelectDropdown = ({ selectableItemIdArray={selectableItemIds} hotkeyScope={hotkeyScope} onEnter={(itemId) => { - const record = recordsInDropdown.findIndex( + const item = itemsInDropdown.findIndex( (entity) => entity.id === itemId, ); - const recordIsSelectedInDropwdown = filteredSelectedRecords.find( + const itemIsSelectedInDropwdown = filteredSelectedItems.find( (entity) => entity.id === itemId, ); - handleRecordSelectChange( - recordsInDropdown[record], - !recordIsSelectedInDropwdown, + handleItemSelectChange( + itemsInDropdown[item], + !itemIsSelectedInDropwdown, ); resetSelectedItem(); }} > <DropdownMenuItemsContainer hasMaxHeight> - {recordsInDropdown?.map((record) => { + {itemsInDropdown?.map((item) => { return ( <MenuItemMultiSelectAvatar - key={record.id} - selected={record.isSelected} - isKeySelected={record.id === selectedItemId} + key={item.id} + selected={item.isSelected} + isKeySelected={item.id === selectedItemId} onSelectChange={(newCheckedValue) => { resetSelectedItem(); - handleRecordSelectChange(record, newCheckedValue); + handleItemSelectChange(item, newCheckedValue); }} avatar={ - <Avatar - avatarUrl={record.avatarUrl} - placeholderColorSeed={record.id} - placeholder={record.name} - size="md" - type={record.avatarType ?? 'rounded'} + <StyledAvatarChip + className="avatar-icon-container" + name={item.name} + avatarUrl={item.avatarUrl} + LeftIcon={item.AvatarIcon} + avatarType={item.avatarType} + isIconInverted={item.isIconInverted} + placeholderColorSeed={item.id} /> } - text={record.name} /> ); })} {showNoResult && <MenuItem text="No result" />} - {loadingRecords && <DropdownMenuSkeletonItem />} + {loadingItems && <DropdownMenuSkeletonItem />} </DropdownMenuItemsContainer> </SelectableList> ); diff --git a/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts b/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts index 905d4fd8f138..73bacf2e7cf6 100644 --- a/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts +++ b/packages/twenty-front/src/modules/object-record/select/hooks/useRecordsForSelect.ts @@ -5,7 +5,7 @@ import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapTo import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { SelectableRecord } from '@/object-record/select/types/SelectableRecord'; +import { SelectableItem } from '@/object-record/select/types/SelectableItem'; import { getObjectFilterFields } from '@/object-record/select/utils/getObjectFilterFields'; import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables'; import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables'; @@ -109,19 +109,19 @@ export const useRecordsForSelect = ({ .map((record) => ({ ...record, isSelected: true, - })) as SelectableRecord[], + })) as SelectableItem[], filteredSelectedRecords: filteredSelectedRecordsData .map(mapToObjectRecordIdentifier) .map((record) => ({ ...record, isSelected: true, - })) as SelectableRecord[], + })) as SelectableItem[], recordsToSelect: recordsToSelectData .map(mapToObjectRecordIdentifier) .map((record) => ({ ...record, isSelected: false, - })) as SelectableRecord[], + })) as SelectableItem[], loading: recordsToSelectLoading || filteredSelectedRecordsLoading || diff --git a/packages/twenty-front/src/modules/object-record/select/types/SelectableItem.ts b/packages/twenty-front/src/modules/object-record/select/types/SelectableItem.ts new file mode 100644 index 000000000000..57122ebe702b --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/select/types/SelectableItem.ts @@ -0,0 +1,11 @@ +import { AvatarType, IconComponent } from 'twenty-ui'; + +export type SelectableItem<T = object> = T & { + id: string; + name: string; + avatarUrl?: string; + avatarType?: AvatarType; + AvatarIcon?: IconComponent; + isSelected: boolean; + isIconInverted?: boolean; +}; diff --git a/packages/twenty-front/src/modules/object-record/select/types/SelectableRecord.ts b/packages/twenty-front/src/modules/object-record/select/types/SelectableRecord.ts deleted file mode 100644 index ff08ac70b8e3..000000000000 --- a/packages/twenty-front/src/modules/object-record/select/types/SelectableRecord.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { AvatarType } from 'twenty-ui'; - -export type SelectableRecord = { - id: string; - name: string; - avatarUrl?: string; - avatarType?: AvatarType; - record: any; - isSelected: boolean; -}; 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/__tests__/useOpenObjectRecordsSpreasheetImportDialog.test.tsx index da745581eb75..a7e47f8ba713 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/__tests__/useOpenObjectRecordsSpreasheetImportDialog.test.tsx @@ -1,13 +1,12 @@ import { gql } from '@apollo/client'; -import { MockedProvider } from '@apollo/client/testing'; -import { act, renderHook, waitFor } from '@testing-library/react'; -import { ReactNode } from 'react'; -import { RecoilRoot, useRecoilValue } from 'recoil'; +import { renderHook, waitFor } from '@testing-library/react'; +import { act } from 'react'; +import { useRecoilValue } from 'recoil'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { spreadsheetImportDialogState } from '@/spreadsheet-import/states/spreadsheetImportDialogState'; -import { SnackBarManagerScopeInternalContext } from '@/ui/feedback/snack-bar-manager/scopes/scope-internal-context/SnackBarManagerScopeInternalContext'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; import { useOpenObjectRecordsSpreasheetImportDialog } from '../hooks/useOpenObjectRecordsSpreasheetImportDialog'; const companyId = 'cb2e9f4b-20c3-4759-9315-4ffeecfaf71a'; @@ -26,13 +25,42 @@ const companyMocks = [ ) { createCompanies(data: $data, upsert: $upsert) { __typename - updatedAt - domainName { - primaryLinkUrl - primaryLinkLabel - secondaryLinks + accountOwner { + __typename + avatarUrl + colorScheme + createdAt + dateFormat + deletedAt + id + locale + name { + firstName + lastName + } + timeFormat + timeZone + updatedAt + userEmail + userId + } + accountOwnerId + activityTargets { + edges { + node { + __typename + activityId + companyId + createdAt + deletedAt + id + opportunityId + personId + rocketId + updatedAt + } + } } - visaSponsorship address { addressStreet1 addressStreet2 @@ -43,20 +71,31 @@ const companyMocks = [ addressLat addressLng } - position - employees - deletedAt - accountOwnerId annualRecurringRevenue { amountMicros currencyCode } - id - name - xLink { - primaryLinkUrl - primaryLinkLabel - secondaryLinks + attachments { + edges { + node { + __typename + activityId + authorId + companyId + createdAt + deletedAt + fullPath + id + name + noteId + opportunityId + personId + rocketId + taskId + type + updatedAt + } + } } createdAt createdBy { @@ -64,7 +103,36 @@ const companyMocks = [ workspaceMemberId name } - workPolicy + deletedAt + domainName { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + employees + favorites { + edges { + node { + __typename + companyId + createdAt + deletedAt + id + noteId + opportunityId + personId + position + rocketId + taskId + updatedAt + viewId + workflowId + workspaceMemberId + } + } + } + id + idealCustomerProfile introVideo { primaryLinkUrl primaryLinkLabel @@ -75,8 +143,151 @@ const companyMocks = [ primaryLinkLabel secondaryLinks } + name + noteTargets { + edges { + node { + __typename + companyId + createdAt + deletedAt + id + noteId + opportunityId + personId + rocketId + updatedAt + } + } + } + opportunities { + edges { + node { + __typename + amount { + amountMicros + currencyCode + } + closeDate + companyId + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + id + name + pointOfContactId + position + stage + updatedAt + } + } + } + people { + edges { + node { + __typename + avatarUrl + city + companyId + createdAt + createdBy { + source + workspaceMemberId + name + } + deletedAt + emails { + primaryEmail + additionalEmails + } + id + intro + jobTitle + linkedinLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + name { + firstName + lastName + } + performanceRating + phones { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } + position + updatedAt + whatsapp { + primaryPhoneNumber + primaryPhoneCountryCode + additionalPhones + } + workPreference + xLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } + } + } + } + position tagline - idealCustomerProfile + taskTargets { + edges { + node { + __typename + companyId + createdAt + deletedAt + id + opportunityId + personId + rocketId + taskId + updatedAt + } + } + } + timelineActivities { + edges { + node { + __typename + companyId + createdAt + deletedAt + happensAt + id + linkedObjectMetadataId + linkedRecordCachedName + linkedRecordId + name + noteId + opportunityId + personId + properties + rocketId + taskId + updatedAt + workspaceMemberId + } + } + } + updatedAt + visaSponsorship + workPolicy + xLink { + primaryLinkUrl + primaryLinkLabel + secondaryLinks + } } } `, @@ -99,6 +310,9 @@ const companyMocks = [ createCompanies: [ { id: companyId, + favorites: { + edges: [], + }, }, ], }, @@ -112,17 +326,9 @@ const fakeCsv = () => { return new File([blob], 'fakeData.csv', { type: 'text/csv' }); }; -const Wrapper = ({ children }: { children: ReactNode }) => ( - <RecoilRoot> - <MockedProvider mocks={companyMocks} addTypename={false}> - <SnackBarManagerScopeInternalContext.Provider - value={{ scopeId: 'snack-bar-manager' }} - > - {children} - </SnackBarManagerScopeInternalContext.Provider> - </MockedProvider> - </RecoilRoot> -); +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: companyMocks, +}); // TODO: improve object metadata item seeds to have more field types to add tests on composite fields here describe('useSpreadsheetCompanyImport', () => { diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts b/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts index 35a697bc116d..030601f241bc 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/constants/CompositeFieldImportLabels.ts @@ -1,8 +1,10 @@ import { FieldAddressValue, FieldCurrencyValue, + FieldEmailsValue, FieldFullNameValue, FieldLinksValue, + FieldPhonesValue, } from '@/object-record/record-field/types/FieldMetadata'; import { CompositeFieldLabels } from '@/object-record/spreadsheet-import/types/CompositeFieldLabels'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -30,6 +32,13 @@ export const COMPOSITE_FIELD_IMPORT_LABELS = { primaryLinkUrlLabel: 'Link URL', primaryLinkLabelLabel: 'Link Label', } satisfies Partial<CompositeFieldLabels<FieldLinksValue>>, + [FieldMetadataType.Emails]: { + primaryEmailLabel: 'Email', + } satisfies Partial<CompositeFieldLabels<FieldEmailsValue>>, + [FieldMetadataType.Phones]: { + primaryPhoneCountryCodeLabel: 'Phone country code', + primaryPhoneNumberLabel: 'Phone number', + } satisfies Partial<CompositeFieldLabels<FieldPhonesValue>>, [FieldMetadataType.Actor]: { sourceLabel: 'Source', }, 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 7e4edf83926a..260a223f1791 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 @@ -15,6 +15,7 @@ export const useBuildAvailableFieldsForImport = () => { ) => { const availableFieldsForImport: AvailableFieldForImport[] = []; + // Todo: refactor this to avoid this else if syntax with duplicated code for (const fieldMetadataItem of fieldMetadataItems) { if (fieldMetadataItem.type === FieldMetadataType.FullName) { const { firstNameLabel, lastNameLabel } = @@ -155,6 +156,42 @@ export const useBuildAvailableFieldsForImport = () => { fieldMetadataItem.label, ), }); + } else if (fieldMetadataItem.type === FieldMetadataType.Emails) { + Object.entries( + COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.Emails], + ).forEach(([_, fieldLabel]) => { + availableFieldsForImport.push({ + icon: getIcon(fieldMetadataItem.icon), + label: `${fieldLabel} (${fieldMetadataItem.label})`, + key: `${fieldLabel} (${fieldMetadataItem.name})`, + fieldType: { + type: 'input', + }, + fieldValidationDefinitions: + getSpreadSheetFieldValidationDefinitions( + fieldMetadataItem.type, + `${fieldLabel} (${fieldMetadataItem.label})`, + ), + }); + }); + } else if (fieldMetadataItem.type === FieldMetadataType.Phones) { + Object.entries( + COMPOSITE_FIELD_IMPORT_LABELS[FieldMetadataType.Phones], + ).forEach(([_, fieldLabel]) => { + availableFieldsForImport.push({ + icon: getIcon(fieldMetadataItem.icon), + label: `${fieldLabel} (${fieldMetadataItem.label})`, + key: `${fieldLabel} (${fieldMetadataItem.name})`, + fieldType: { + type: 'input', + }, + fieldValidationDefinitions: + getSpreadSheetFieldValidationDefinitions( + fieldMetadataItem.type, + `${fieldLabel} (${fieldMetadataItem.label})`, + ), + }); + }); } else { availableFieldsForImport.push({ icon: getIcon(fieldMetadataItem.icon), 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/util/buildRecordFromImportedStructuredRow.ts index 5ddbe06096b5..68e641656660 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/util/buildRecordFromImportedStructuredRow.ts +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/util/buildRecordFromImportedStructuredRow.ts @@ -1,7 +1,9 @@ import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { FieldAddressValue, + FieldEmailsValue, FieldLinksValue, + FieldPhonesValue, } from '@/object-record/record-field/types/FieldMetadata'; import { COMPOSITE_FIELD_IMPORT_LABELS } from '@/object-record/spreadsheet-import/constants/CompositeFieldImportLabels'; import { ImportedStructuredRow } from '@/spreadsheet-import/types'; @@ -31,6 +33,8 @@ export const buildRecordFromImportedStructuredRow = ( CURRENCY: { amountMicrosLabel, currencyCodeLabel }, FULL_NAME: { firstNameLabel, lastNameLabel }, LINKS: { primaryLinkLabelLabel, primaryLinkUrlLabel }, + EMAILS: { primaryEmailLabel }, + PHONES: { primaryPhoneNumberLabel, primaryPhoneCountryCodeLabel }, } = COMPOSITE_FIELD_IMPORT_LABELS; for (const field of fields) { @@ -129,14 +133,48 @@ export const buildRecordFromImportedStructuredRow = ( } break; } - case FieldMetadataType.Link: - if (importedFieldValue !== undefined) { + case FieldMetadataType.Phones: { + if ( + isDefined( + importedStructuredRow[ + `${primaryPhoneCountryCodeLabel} (${field.name})` + ] || + importedStructuredRow[ + `${primaryPhoneNumberLabel} (${field.name})` + ], + ) + ) { recordToBuild[field.name] = { - label: field.name, - url: importedFieldValue || null, - }; + primaryPhoneCountryCode: castToString( + importedStructuredRow[ + `${primaryPhoneCountryCodeLabel} (${field.name})` + ], + ), + primaryPhoneNumber: castToString( + importedStructuredRow[ + `${primaryPhoneNumberLabel} (${field.name})` + ], + ), + additionalPhones: null, + } satisfies FieldPhonesValue; } break; + } + case FieldMetadataType.Emails: { + if ( + isDefined( + importedStructuredRow[`${primaryEmailLabel} (${field.name})`], + ) + ) { + recordToBuild[field.name] = { + primaryEmail: castToString( + importedStructuredRow[`${primaryEmailLabel} (${field.name})`], + ), + additionalEmails: null, + } satisfies FieldEmailsValue; + } + break; + } case FieldMetadataType.Relation: if ( isDefined(importedFieldValue) && diff --git a/packages/twenty-front/src/modules/object-record/utils/generateSearchRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/utils/generateSearchRecordsQuery.ts new file mode 100644 index 000000000000..070aaf020833 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/generateSearchRecordsQuery.ts @@ -0,0 +1,40 @@ +import gql from 'graphql-tag'; + +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; +import { RecordGqlOperationGqlRecordFields } from '@/object-record/graphql/types/RecordGqlOperationGqlRecordFields'; +import { getSearchRecordsQueryResponseField } from '@/object-record/utils/getSearchRecordsQueryResponseField'; +import { capitalize } from '~/utils/string/capitalize'; + +export type QueryCursorDirection = 'before' | 'after'; + +export const generateSearchRecordsQuery = ({ + objectMetadataItem, + objectMetadataItems, + recordGqlFields, + computeReferences, +}: { + objectMetadataItem: ObjectMetadataItem; + objectMetadataItems: ObjectMetadataItem[]; // TODO - what is this used for? + recordGqlFields?: RecordGqlOperationGqlRecordFields; + computeReferences?: boolean; +}) => gql` + query Search${capitalize(objectMetadataItem.namePlural)}($search: String, $limit: Int) { + ${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(searchInput: $search, limit: $limit){ + edges { + node ${mapObjectMetadataToGraphQLQuery({ + objectMetadataItems, + objectMetadataItem, + recordGqlFields, + computeReferences, + })} + } + pageInfo { + hasNextPage + hasPreviousPage + startCursor + endCursor + } + } +} +`; diff --git a/packages/twenty-front/src/modules/object-record/utils/getSearchRecordsQueryResponseField.ts b/packages/twenty-front/src/modules/object-record/utils/getSearchRecordsQueryResponseField.ts new file mode 100644 index 000000000000..fa6b7daf5b8a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/utils/getSearchRecordsQueryResponseField.ts @@ -0,0 +1,4 @@ +import { capitalize } from '~/utils/string/capitalize'; + +export const getSearchRecordsQueryResponseField = (objectNamePlural: string) => + `search${capitalize(objectNamePlural)}`; diff --git a/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts b/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts index cc3fb4ba46fa..c760351487a7 100644 --- a/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts +++ b/packages/twenty-front/src/modules/object-record/utils/sanitizeRecordInput.ts @@ -50,6 +50,8 @@ export const sanitizeRecordInput = ({ return undefined; } + // Todo: we should check that the fieldValue is a valid value + // (e.g. a string for a string field, following the right composite structure for composite fields) return [fieldName, fieldValue]; }) .filter(isDefined), diff --git a/packages/twenty-front/src/modules/opportunities/Opportunity.ts b/packages/twenty-front/src/modules/opportunities/Opportunity.ts new file mode 100644 index 000000000000..fe4212d5698d --- /dev/null +++ b/packages/twenty-front/src/modules/opportunities/Opportunity.ts @@ -0,0 +1,8 @@ +export type Opportunity = { + __typename: 'Opportunity'; + id: string; + createdAt: string; + updatedAt?: string; + deletedAt?: string | null; + name: string | null; +}; 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 83fa74e7b864..263b70decf0f 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 @@ -8,7 +8,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect'; import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { query, responseData, diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx index ab231c603a58..9556441e6267 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsContainer.tsx @@ -8,7 +8,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { SettingsAccountsCalendarChannelDetails } from '@/settings/accounts/components/SettingsAccountsCalendarChannelDetails'; import { SettingsAccountsCalendarChannelsGeneral } from '@/settings/accounts/components/SettingsAccountsCalendarChannelsGeneral'; -import { SettingsAccountsListEmptyStateCard } from '@/settings/accounts/components/SettingsAccountsListEmptyStateCard'; +import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection'; import { SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountCalendarChannelsTabListComponentId'; import { TabList } from '@/ui/layout/tab/components/TabList'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; @@ -56,7 +56,7 @@ export const SettingsAccountsCalendarChannelsContainer = () => { ]; if (!calendarChannels.length) { - return <SettingsAccountsListEmptyStateCard />; + return <SettingsNewAccountSection />; } return ( diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx index d2d15d02fdae..2c5e1102d3b3 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsMessageChannelsContainer.tsx @@ -6,8 +6,8 @@ import { MessageChannel } from '@/accounts/types/MessageChannel'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; -import { SettingsAccountsListEmptyStateCard } from '@/settings/accounts/components/SettingsAccountsListEmptyStateCard'; import { SettingsAccountsMessageChannelDetails } from '@/settings/accounts/components/SettingsAccountsMessageChannelDetails'; +import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection'; import { SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountMessageChannelsTabListComponentId'; import { TabList } from '@/ui/layout/tab/components/TabList'; import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; @@ -55,7 +55,7 @@ export const SettingsAccountsMessageChannelsContainer = () => { ]; if (!messageChannels.length) { - return <SettingsAccountsListEmptyStateCard />; + return <SettingsNewAccountSection />; } return ( diff --git a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsCalendarChannelDetails.stories.tsx b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsCalendarChannelDetails.stories.tsx index 5ef756baf4c6..a4e9f5294e83 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsCalendarChannelDetails.stories.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsCalendarChannelDetails.stories.tsx @@ -3,12 +3,18 @@ import { ComponentDecorator } from 'twenty-ui'; import { SettingsAccountsCalendarChannelDetails } from '@/settings/accounts/components/SettingsAccountsCalendarChannelDetails'; import { CalendarChannelVisibility } from '~/generated/graphql'; +import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; +import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; const meta: Meta<typeof SettingsAccountsCalendarChannelDetails> = { title: 'Modules/Settings/Accounts/CalendarChannels/SettingsAccountsCalendarChannelDetails', component: SettingsAccountsCalendarChannelDetails, - decorators: [ComponentDecorator], + decorators: [ + ComponentDecorator, + ObjectMetadataItemsDecorator, + SnackBarDecorator, + ], args: { calendarChannel: { id: '20202020-ef5a-4822-9e08-ce6e6a4dcb6a', diff --git a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsCalendarChannelsGeneral.stories.tsx b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsCalendarChannelsGeneral.stories.tsx index 49f279316908..83c05df92b98 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsCalendarChannelsGeneral.stories.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsCalendarChannelsGeneral.stories.tsx @@ -2,12 +2,18 @@ import { Meta, StoryObj } from '@storybook/react'; import { ComponentDecorator } from 'twenty-ui'; import { SettingsAccountsCalendarChannelsGeneral } from '@/settings/accounts/components/SettingsAccountsCalendarChannelsGeneral'; +import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; +import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; const meta: Meta<typeof SettingsAccountsCalendarChannelsGeneral> = { title: 'Modules/Settings/Accounts/CalendarChannels/SettingsAccountsCalendarChannelsGeneral', component: SettingsAccountsCalendarChannelsGeneral, - decorators: [ComponentDecorator], + decorators: [ + ComponentDecorator, + ObjectMetadataItemsDecorator, + SnackBarDecorator, + ], }; export default meta; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsMessageChannelDetails.stories.tsx b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsMessageChannelDetails.stories.tsx index 02264d8e66fe..a951150c94f3 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsMessageChannelDetails.stories.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsMessageChannelDetails.stories.tsx @@ -4,12 +4,18 @@ import { ComponentDecorator } from 'twenty-ui'; import { MessageChannelContactAutoCreationPolicy } from '@/accounts/types/MessageChannel'; import { SettingsAccountsMessageChannelDetails } from '@/settings/accounts/components/SettingsAccountsMessageChannelDetails'; import { MessageChannelVisibility } from '~/generated/graphql'; +import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; +import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; const meta: Meta<typeof SettingsAccountsMessageChannelDetails> = { title: 'Modules/Settings/Accounts/MessageChannels/SettingsAccountsMessageChannelDetails', component: SettingsAccountsMessageChannelDetails, - decorators: [ComponentDecorator], + decorators: [ + ComponentDecorator, + ObjectMetadataItemsDecorator, + SnackBarDecorator, + ], args: { messageChannel: { id: '20202020-ef5a-4822-9e08-ce6e6a4dcb6a', diff --git a/packages/twenty-front/src/modules/settings/accounts/hooks/useTriggerGoogleApisOAuth.ts b/packages/twenty-front/src/modules/settings/accounts/hooks/useTriggerGoogleApisOAuth.ts index 0c6ec18d28bc..921ebdea12ee 100644 --- a/packages/twenty-front/src/modules/settings/accounts/hooks/useTriggerGoogleApisOAuth.ts +++ b/packages/twenty-front/src/modules/settings/accounts/hooks/useTriggerGoogleApisOAuth.ts @@ -12,11 +12,17 @@ export const useTriggerGoogleApisOAuth = () => { const [generateTransientToken] = useGenerateTransientTokenMutation(); const triggerGoogleApisOAuth = useCallback( - async ( - redirectLocation?: AppPath, - messageVisibility?: MessageChannelVisibility, - calendarVisibility?: CalendarChannelVisibility, - ) => { + async ({ + redirectLocation, + messageVisibility, + calendarVisibility, + loginHint, + }: { + redirectLocation?: AppPath | string; + messageVisibility?: MessageChannelVisibility; + calendarVisibility?: CalendarChannelVisibility; + loginHint?: string; + } = {}) => { const authServerUrl = REACT_APP_SERVER_BASE_URL; const transientToken = await generateTransientToken(); @@ -38,6 +44,8 @@ export const useTriggerGoogleApisOAuth = () => { ? `&messageVisibility=${messageVisibility}` : ''; + params += loginHint ? `&loginHint=${loginHint}` : ''; + window.location.href = `${authServerUrl}/auth/google-apis?${params}`; }, [generateTransientToken], diff --git a/packages/twenty-front/src/modules/settings/components/SettingsCard.tsx b/packages/twenty-front/src/modules/settings/components/SettingsCard.tsx index 7383b6e902ee..972bac3d77a4 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsCard.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsCard.tsx @@ -27,6 +27,7 @@ const StyledCard = styled(Card)<{ width: 100%; & :hover { background-color: ${({ theme }) => theme.background.quaternary}; + cursor: pointer; } `; diff --git a/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx b/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx index 04f11020758a..e84a7d2734e2 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx @@ -13,13 +13,16 @@ import { IconMail, IconRocket, IconSettings, + IconTool, IconUserCircle, IconUsers, + MAIN_COLORS, } from 'twenty-ui'; import { useAuth } from '@/auth/hooks/useAuth'; import { billingState } from '@/client-config/states/billingState'; import { SettingsNavigationDrawerItem } from '@/settings/components/SettingsNavigationDrawerItem'; +import { useExpandedHeightAnimation } from '@/settings/hooks/useExpandedHeightAnimation'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { @@ -29,8 +32,11 @@ import { import { NavigationDrawerItemGroup } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemGroup'; import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection'; import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle'; +import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState'; import { getNavigationSubItemState } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemState'; import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; +import styled from '@emotion/styled'; +import { AnimatePresence, motion } from 'framer-motion'; import { matchPath, resolvePath, useLocation } from 'react-router-dom'; type SettingsNavigationItem = { @@ -41,7 +47,30 @@ type SettingsNavigationItem = { indentationLevel?: NavigationDrawerItemIndentationLevel; }; +const StyledIconContainer = styled.div` + border-right: 1px solid ${MAIN_COLORS.yellow}; + position: absolute; + left: ${({ theme }) => theme.spacing(-5)}; + margin-top: ${({ theme }) => theme.spacing(2)}; + height: 75%; +`; + +const StyledDeveloperSection = styled.div` + display: flex; + width: 100%; + gap: ${({ theme }) => theme.spacing(1)}; + position: relative; +`; + +const StyledIconTool = styled(IconTool)` + margin-right: ${({ theme }) => theme.spacing(0.5)}; +`; + export const SettingsNavigationDrawerItems = () => { + const isAdvancedModeEnabled = useRecoilValue(isAdvancedModeEnabledState); + const { contentRef, motionAnimationVariants } = useExpandedHeightAnimation( + isAdvancedModeEnabled, + ); const { signOut } = useAuth(); const billing = useRecoilValue(billingState); @@ -147,18 +176,6 @@ export const SettingsNavigationDrawerItems = () => { Icon={IconHierarchy2} matchSubPages /> - <SettingsNavigationDrawerItem - label="Developers" - path={SettingsPath.Developers} - Icon={IconCode} - /> - {isFunctionSettingsEnabled && ( - <SettingsNavigationDrawerItem - label="Functions" - path={SettingsPath.ServerlessFunctions} - Icon={IconFunction} - /> - )} <SettingsNavigationDrawerItem label="Integrations" path={SettingsPath.Integrations} @@ -172,6 +189,38 @@ export const SettingsNavigationDrawerItems = () => { /> )} </NavigationDrawerSection> + <AnimatePresence> + {isAdvancedModeEnabled && ( + <motion.div + ref={contentRef} + initial="initial" + animate="animate" + exit="exit" + variants={motionAnimationVariants} + > + <StyledDeveloperSection> + <StyledIconContainer> + <StyledIconTool size={12} color={MAIN_COLORS.yellow} /> + </StyledIconContainer> + <NavigationDrawerSection> + <NavigationDrawerSectionTitle label="Developers" /> + <SettingsNavigationDrawerItem + label="API & Webhooks" + path={SettingsPath.Developers} + Icon={IconCode} + /> + {isFunctionSettingsEnabled && ( + <SettingsNavigationDrawerItem + label="Functions" + path={SettingsPath.ServerlessFunctions} + Icon={IconFunction} + /> + )} + </NavigationDrawerSection> + </StyledDeveloperSection> + </motion.div> + )} + </AnimatePresence> <NavigationDrawerSection> <NavigationDrawerSectionTitle label="Other" /> <SettingsNavigationDrawerItem diff --git a/packages/twenty-front/src/modules/settings/components/SettingsSkeletonLoader.tsx b/packages/twenty-front/src/modules/settings/components/SettingsSkeletonLoader.tsx new file mode 100644 index 000000000000..812a5ca67b82 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/components/SettingsSkeletonLoader.tsx @@ -0,0 +1,52 @@ +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; +import { PageBody } from '@/ui/layout/page/PageBody'; +import { PageHeader } from '@/ui/layout/page/PageHeader'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; + +const StyledContainer = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; + +const StyledTitleLoaderContainer = styled.div` + margin: ${({ theme }) => theme.spacing(8, 8, 2)}; +`; + +export const SettingsSkeletonLoader = () => { + const theme = useTheme(); + return ( + <StyledContainer> + <PageHeader + title={ + <SkeletonTheme + baseColor={theme.background.tertiary} + highlightColor={theme.background.transparent.lighter} + borderRadius={4} + > + <Skeleton + height={SKELETON_LOADER_HEIGHT_SIZES.standard.m} + width={120} + />{' '} + </SkeletonTheme> + } + /> + <PageBody> + <StyledTitleLoaderContainer> + <SkeletonTheme + baseColor={theme.background.tertiary} + highlightColor={theme.background.transparent.lighter} + borderRadius={4} + > + <Skeleton + height={SKELETON_LOADER_HEIGHT_SIZES.standard.m} + width={200} + /> + </SkeletonTheme> + </StyledTitleLoaderContainer> + </PageBody> + </StyledContainer> + ); +}; 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 0521676c7cf0..18624a59fb34 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,3 +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'; @@ -6,20 +7,22 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconChevronDown } from 'twenty-ui'; - -type SettingsDataModelNewFieldBreadcrumbDropDownProps = { - isConfigureStep: boolean; - onBreadcrumbClick: (isConfigureStep: boolean) => void; -}; +import { + useLocation, + useNavigate, + useParams, + useSearchParams, +} from 'react-router-dom'; +import { IconChevronDown, isDefined } from 'twenty-ui'; const StyledContainer = styled.div` align-items: center; - color: ${({ theme }) => theme.font.color.secondary}; - cursor: pointer; + color: ${({ theme }) => theme.font.color.tertiary}; + cursor: default; display: flex; font-size: ${({ theme }) => theme.font.size.md}; `; + const StyledButtonContainer = styled.div` position: relative; width: 100%; @@ -33,10 +36,24 @@ const StyledDownChevron = styled(IconChevronDown)` transform: translateY(-50%); `; -const StyledMenuItem = styled(MenuItem)<{ selected?: boolean }>` +const StyledMenuItemWrapper = styled.div<{ disabled?: boolean }>` + cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')}; + width: 100%; +`; + +const StyledMenuItem = styled(MenuItem)<{ + selected?: boolean; + disabled?: boolean; +}>` background: ${({ theme, selected }) => selected ? theme.background.quaternary : 'transparent'}; - cursor: pointer; + opacity: ${({ disabled }) => (disabled ? 0.5 : 1)}; + pointer-events: ${({ disabled }) => (disabled ? 'none' : 'auto')}; + + &:hover { + background: ${({ theme, disabled }) => + disabled ? 'transparent' : theme.background.tertiary}; + } `; const StyledSpan = styled.span` @@ -48,19 +65,31 @@ const StyledButton = styled(Button)` padding-right: ${({ theme }) => theme.spacing(6)}; `; -export const SettingsDataModelNewFieldBreadcrumbDropDown = ({ - isConfigureStep, - onBreadcrumbClick, -}: SettingsDataModelNewFieldBreadcrumbDropDownProps) => { +export const SettingsDataModelNewFieldBreadcrumbDropDown = () => { const dropdownId = `settings-object-new-field-breadcrumb-dropdown`; - const { closeDropdown } = useDropdown(dropdownId); + const navigate = useNavigate(); + const location = useLocation(); + const { objectSlug = '' } = useParams(); + const [searchParams] = useSearchParams(); + const theme = useTheme(); - const handleClick = (step: boolean) => { - onBreadcrumbClick(step); + const fieldType = searchParams.get('fieldType') as SettingsFieldType; + const isConfigureStep = location.pathname.includes('/configure'); + + const handleClick = (step: 'select' | 'configure') => { + if (step === 'configure' && isDefined(fieldType)) { + navigate( + `/settings/objects/${objectSlug}/new-field/configure?fieldType=${fieldType}`, + ); + return; + } + + navigate( + `/settings/objects/${objectSlug}/new-field/select${fieldType ? `?fieldType=${fieldType}` : ''}`, + ); closeDropdown(); }; - const theme = useTheme(); return ( <StyledContainer> @@ -81,16 +110,21 @@ export const SettingsDataModelNewFieldBreadcrumbDropDown = ({ dropdownComponents={ <DropdownMenu> <DropdownMenuItemsContainer> - <StyledMenuItem - text="1. Type" - onClick={() => handleClick(false)} - selected={!isConfigureStep} - /> - <StyledMenuItem - text="2. Configure" - onClick={() => handleClick(true)} - selected={isConfigureStep} - /> + <StyledMenuItemWrapper> + <StyledMenuItem + text="1. Type" + onClick={() => handleClick('select')} + selected={!isConfigureStep} + /> + </StyledMenuItemWrapper> + <StyledMenuItemWrapper disabled={!isDefined(fieldType)}> + <StyledMenuItem + text="2. Configure" + onClick={() => handleClick('configure')} + selected={isConfigureStep} + disabled={!isDefined(fieldType)} + /> + </StyledMenuItemWrapper> </DropdownMenuItemsContainer> </DropdownMenu> } diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts new file mode 100644 index 000000000000..4181fd65e807 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsCompositeFieldTypeConfigs.ts @@ -0,0 +1,190 @@ +import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; +import { + FieldActorValue, + FieldAddressValue, + FieldCurrencyValue, + FieldEmailsValue, + FieldFullNameValue, + FieldLinksValue, + FieldLinkValue, + FieldPhonesValue, +} from '@/object-record/record-field/types/FieldMetadata'; +import { SettingsFieldTypeConfig } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs'; +import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType'; +import { + IllustrationIconCurrency, + IllustrationIconLink, + IllustrationIconMail, + IllustrationIconMap, + IllustrationIconPhone, + IllustrationIconSetting, + IllustrationIconUser, +} from 'twenty-ui'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +export type SettingsCompositeFieldTypeConfig<T> = SettingsFieldTypeConfig<T> & { + subFields: (keyof T)[]; + filterableSubFields: (keyof T)[]; + labelBySubField: Record<keyof T, string>; + exampleValue: T; +}; + +type SettingsCompositeFieldTypeConfigArray = Record< + CompositeFieldType, + SettingsCompositeFieldTypeConfig<any> +>; + +export const SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS = { + [FieldMetadataType.Currency]: { + label: 'Currency', + Icon: IllustrationIconCurrency, + subFields: ['amountMicros', 'currencyCode'], + filterableSubFields: ['amountMicros', 'currencyCode'], + labelBySubField: { + amountMicros: 'Amount', + currencyCode: 'Currency', + }, + exampleValue: { + amountMicros: 2000000000, + currencyCode: CurrencyCode.USD, + }, + category: 'Basic', + } as const satisfies SettingsCompositeFieldTypeConfig<FieldCurrencyValue>, + [FieldMetadataType.Emails]: { + label: 'Emails', + Icon: IllustrationIconMail, + subFields: ['primaryEmail', 'additionalEmails'], + filterableSubFields: ['primaryEmail'], + labelBySubField: { + primaryEmail: 'Primary Email', + additionalEmails: 'Additional Emails', + }, + exampleValue: { + primaryEmail: 'john@twenty.com', + additionalEmails: [ + 'tim@twenty.com', + 'timapple@twenty.com', + 'johnappletim@twenty.com', + ], + }, + category: 'Basic', + } as const satisfies SettingsCompositeFieldTypeConfig<FieldEmailsValue>, + [FieldMetadataType.Link]: { + label: 'Link', + Icon: IllustrationIconLink, + exampleValue: { url: 'www.twenty.com', label: '' }, + category: 'Basic', + subFields: ['url', 'label'], + filterableSubFields: ['url', 'label'], + labelBySubField: { + url: 'URL', + label: 'Label', + }, + } as const satisfies SettingsCompositeFieldTypeConfig<FieldLinkValue>, + [FieldMetadataType.Links]: { + label: 'Links', + Icon: IllustrationIconLink, + exampleValue: { + primaryLinkUrl: 'twenty.com', + primaryLinkLabel: '', + secondaryLinks: [{ url: 'twenty.com', label: 'Twenty' }], + }, + category: 'Basic', + subFields: ['primaryLinkUrl', 'primaryLinkLabel', 'secondaryLinks'], + filterableSubFields: ['primaryLinkUrl', 'primaryLinkLabel'], + labelBySubField: { + primaryLinkUrl: 'Link URL', + primaryLinkLabel: 'Link Label', + secondaryLinks: 'Secondary Links', + }, + } as const satisfies SettingsCompositeFieldTypeConfig<FieldLinksValue>, + [FieldMetadataType.Phones]: { + label: 'Phones', + Icon: IllustrationIconPhone, + exampleValue: { + primaryPhoneNumber: '234-567-890', + primaryPhoneCountryCode: '+1', + additionalPhones: [{ number: '234-567-890', countryCode: '+1' }], + }, + subFields: [ + 'primaryPhoneNumber', + 'primaryPhoneCountryCode', + 'additionalPhones', + ], + filterableSubFields: ['primaryPhoneNumber', 'primaryPhoneCountryCode'], + labelBySubField: { + primaryPhoneNumber: 'Primary Phone Number', + primaryPhoneCountryCode: 'Primary Phone Country Code', + additionalPhones: 'Additional Phones', + }, + category: 'Basic', + } as const satisfies SettingsCompositeFieldTypeConfig<FieldPhonesValue>, + [FieldMetadataType.FullName]: { + label: 'Full Name', + Icon: IllustrationIconUser, + exampleValue: { firstName: 'John', lastName: 'Doe' }, + category: 'Advanced', + subFields: ['firstName', 'lastName'], + filterableSubFields: ['firstName', 'lastName'], + labelBySubField: { + firstName: 'First Name', + lastName: 'Last Name', + }, + } as const satisfies SettingsCompositeFieldTypeConfig<FieldFullNameValue>, + [FieldMetadataType.Address]: { + label: 'Address', + Icon: IllustrationIconMap, + subFields: [ + 'addressStreet1', + 'addressStreet2', + 'addressCity', + 'addressState', + 'addressCountry', + 'addressPostcode', + 'addressLat', + 'addressLng', + ], + filterableSubFields: [ + 'addressStreet1', + 'addressStreet2', + 'addressCity', + 'addressState', + 'addressCountry', + 'addressPostcode', + ], + labelBySubField: { + addressStreet1: 'Address 1', + addressStreet2: 'Address 2', + addressCity: 'City', + addressState: 'State', + addressCountry: 'Country', + addressPostcode: 'Post Code', + addressLat: 'Latitude', + addressLng: 'Longitude', + }, + exampleValue: { + addressStreet1: '456 Oak Street', + addressStreet2: 'Unit 3B', + addressCity: 'Springfield', + addressState: 'California', + addressCountry: 'United States', + addressPostcode: '90210', + addressLat: 34.0522, + addressLng: -118.2437, + }, + category: 'Basic', + } as const satisfies SettingsCompositeFieldTypeConfig<FieldAddressValue>, + [FieldMetadataType.Actor]: { + label: 'Actor', + Icon: IllustrationIconSetting, + category: 'Basic', + subFields: ['source', 'name', 'workspaceMemberId'], + filterableSubFields: ['source', 'name', 'workspaceMemberId'], + labelBySubField: { + source: 'Source', + name: 'Name', + workspaceMemberId: 'Workspace Member ID', + }, + exampleValue: { source: 'source', name: 'name', workspaceMemberId: 'id' }, + } as const satisfies SettingsCompositeFieldTypeConfig<FieldActorValue>, +} as const satisfies SettingsCompositeFieldTypeConfigArray; diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldCurrencyCodes.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldCurrencyCodes.ts index 5e013b50ca26..681da4b3b611 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldCurrencyCodes.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldCurrencyCodes.ts @@ -1,18 +1,56 @@ import { + IconCoins, IconComponent, + IconCurrencyAfghani, + IconCurrencyBahraini, IconCurrencyBaht, + IconCurrencyForint, + IconCurrencyDinar, IconCurrencyDirham, IconCurrencyDollar, + IconCurrencyDollarAustralian, + IconCurrencyDollarBrunei, + IconCurrencyDollarCanadian, + IconCurrencyDollarGuyanese, + IconCurrencyDollarSingapore, + IconCurrencyDong, + IconCurrencyDram, IconCurrencyEuro, + IconCurrencyFlorin, IconCurrencyFrank, + IconCurrencyGuarani, + IconCurrencyHryvnia, + IconCurrencyIranianRial, + IconCurrencyKip, IconCurrencyKroneCzech, + IconCurrencyKroneDanish, IconCurrencyKroneSwedish, + IconCurrencyLari, + IconCurrencyLeu, + IconCurrencyLira, + IconCurrencyLyd, + IconCurrencyManat, + IconCurrencyNaira, + IconCurrencyPaanga, + IconCurrencyPeso, IconCurrencyPound, + IconCurrencyQuetzal, IconCurrencyReal, + IconCurrencyRenminbi, IconCurrencyRiyal, + IconCurrencyRubel, + IconCurrencyRufiyaa, + IconCurrencyRupee, + IconCurrencyRupeeNepalese, + IconCurrencyTaka, + IconCurrencyTenge, + IconCurrencyTugrik, + IconCurrencySom, + IconCurrencyShekel, IconCurrencyWon, IconCurrencyYen, IconCurrencyYuan, + IconCurrencyZloty, } from 'twenty-ui'; import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; @@ -21,76 +59,156 @@ export const SETTINGS_FIELD_CURRENCY_CODES: Record< CurrencyCode, { label: string; Icon: IconComponent } > = { - USD: { - label: 'United States dollar', - Icon: IconCurrencyDollar, - }, - EUR: { - label: 'Euro', - Icon: IconCurrencyEuro, - }, - JPY: { - label: 'Japanese yen', - Icon: IconCurrencyYen, - }, - GBP: { - label: 'British pound', - Icon: IconCurrencyPound, - }, - CAD: { - label: 'Canadian dollar', - Icon: IconCurrencyDollar, - }, - CHF: { - label: 'Swiss franc', - Icon: IconCurrencyFrank, - }, - CNY: { - label: 'Chinese yuan', - Icon: IconCurrencyYuan, - }, - CZK: { - label: 'Czech koruna', - Icon: IconCurrencyKroneCzech, - }, - HKD: { - label: 'Hong Kong dollar', - Icon: IconCurrencyDollar, - }, - NOK: { - label: 'Norwegian krone', - Icon: IconCurrencyKroneSwedish, - }, - SEK: { - label: 'Swedish krona', - Icon: IconCurrencyKroneSwedish, - }, - BHT: { - label: 'Thai Baht', - Icon: IconCurrencyBaht, - }, - MAD: { - label: 'Moroccan dirham', - Icon: IconCurrencyDirham, - }, - QAR: { - label: 'Qatari riyal', - Icon: IconCurrencyRiyal, - }, - AED: { - label: 'UAE dirham', - Icon: IconCurrencyDirham, - }, - KRW: { - label: 'South Korean won', - Icon: IconCurrencyWon, - }, - BRL: { - label: 'Brazilian real', - Icon: IconCurrencyReal, - }, - AUD: { - label: 'Australian dollar', - Icon: IconCurrencyDollar, - }, + AED: { label: 'UAE dirham', Icon: IconCurrencyDirham }, + AFN: { label: 'Afghan afghani', Icon: IconCurrencyAfghani }, + ALL: { label: 'Albanian lek', Icon: IconCurrencyLeu }, + AMD: { label: 'Armenian dram', Icon: IconCurrencyDram }, + ANG: { label: 'Netherlands Antillean guilder', Icon: IconCurrencyFlorin }, + AOA: { label: 'Angolan kwanza', Icon: IconCoins }, + ARS: { label: 'Argentine peso', Icon: IconCoins }, + AUD: { label: 'Australian dollar', Icon: IconCurrencyDollarAustralian }, + AWG: { label: 'Aruban florin', Icon: IconCurrencyFlorin }, + AZN: { label: 'Azerbaijani manat', Icon: IconCurrencyManat }, + BAM: { label: 'Bosnia and Herzegovina mark', Icon: IconCoins }, + BBD: { label: 'Barbados dollar', Icon: IconCurrencyDollar }, + BDT: { label: 'Bangladeshi taka', Icon: IconCurrencyTaka }, + BGN: { label: 'Bulgarian lev', Icon: IconCoins }, + BHD: { label: 'Bahraini dinar', Icon: IconCurrencyBahraini }, + BIF: { label: 'Burundian franc', Icon: IconCoins }, + BMD: { label: 'Bermudian dollar', Icon: IconCurrencyDollar }, + BND: { label: 'Brunei dollar', Icon: IconCurrencyDollarBrunei }, + BOB: { label: 'Boliviano', Icon: IconCoins }, + BRL: { label: 'Brazilian real', Icon: IconCurrencyReal }, + BSD: { label: 'Bahamian dollar', Icon: IconCurrencyDollar }, + BTN: { label: 'Bhutanese ngultrum', Icon: IconCurrencyDollar }, + BWP: { label: 'Botswana pula', Icon: IconCoins }, + BYN: { label: 'Belarusian ruble', Icon: IconCoins }, + BZD: { label: 'Belize dollar', Icon: IconCurrencyDollar }, + CAD: { label: 'Canadian dollar', Icon: IconCurrencyDollarCanadian }, + CDF: { label: 'Congolese franc', Icon: IconCoins }, + CHF: { label: 'Swiss franc', Icon: IconCurrencyFrank }, + CLP: { label: 'Chilean peso', Icon: IconCoins }, + CNY: { label: 'Chinese yuan', Icon: IconCurrencyYuan }, + COP: { label: 'Colombian peso', Icon: IconCoins }, + CRC: { label: 'Costa Rican colon', Icon: IconCoins }, + CUP: { label: 'Cuban peso', Icon: IconCoins }, + CVE: { label: 'Cape Verdean escudo', Icon: IconCoins }, + CZK: { label: 'Czech koruna', Icon: IconCurrencyKroneCzech }, + DJF: { label: 'Djiboutian franc', Icon: IconCoins }, + DKK: { label: 'Danish krone', Icon: IconCurrencyKroneDanish }, + DOP: { label: 'Dominican peso', Icon: IconCoins }, + DZD: { label: 'Algerian Dinar', Icon: IconCoins }, + EGP: { label: 'Egyptian pound', Icon: IconCoins }, + ERN: { label: 'Eritrean nakfa', Icon: IconCoins }, + ETB: { label: 'Ethiopian birr', Icon: IconCoins }, + EUR: { label: 'Euro', Icon: IconCurrencyEuro }, + FJD: { label: 'Fiji dollar', Icon: IconCurrencyDollar }, + FKP: { label: 'Falkland Islands pound', Icon: IconCoins }, + GBP: { label: 'British pound', Icon: IconCurrencyPound }, + GEL: { label: 'Georgian lari', Icon: IconCurrencyLari }, + GHS: { label: 'Ghanaian cedi', Icon: IconCoins }, + GIP: { label: 'Gibraltar pound', Icon: IconCoins }, + GMD: { label: 'Gambian dalasi', Icon: IconCoins }, + GNF: { label: 'Guinean franc', Icon: IconCoins }, + GTQ: { label: 'Guatemalan quetzal', Icon: IconCurrencyQuetzal }, + GYD: { label: 'Guyanese dollar', Icon: IconCurrencyDollarGuyanese }, + HKD: { label: 'Hong Kong dollar', Icon: IconCurrencyRenminbi }, + HNL: { label: 'Honduran lempira', Icon: IconCurrencyLeu }, + HTG: { label: 'Haitian gourde', Icon: IconCoins }, + HUF: { label: 'Hungarian forint', Icon: IconCurrencyForint }, + IDR: { label: 'Indonesian rupiah', Icon: IconCoins }, + ILS: { label: 'Israeli shekel', Icon: IconCurrencyShekel }, + INR: { label: 'Indian rupee', Icon: IconCurrencyRupee }, + IQD: { label: 'Iraqi dinar', Icon: IconCoins }, + IRR: { label: 'Iranian rial', Icon: IconCurrencyIranianRial }, + ISK: { label: 'Icelandic króna', Icon: IconCoins }, + JMD: { label: 'Jamaican dollar', Icon: IconCurrencyDollar }, + JOD: { label: 'Jordanian dinar', Icon: IconCoins }, + JPY: { label: 'Japanese yen', Icon: IconCurrencyYen }, + KES: { label: 'Kenyan shilling', Icon: IconCoins }, + KGS: { label: 'Kyrgyzstani som', Icon: IconCurrencySom }, + KHR: { label: 'Cambodian riel', Icon: IconCoins }, + KMF: { label: 'Comoro franc', Icon: IconCoins }, + KPW: { label: 'North Korean won', Icon: IconCurrencyWon }, + KRW: { label: 'South Korean won', Icon: IconCurrencyWon }, + KWD: { label: 'Kuwaiti dinar', Icon: IconCurrencyDinar }, + KYD: { label: 'Cayman Islands dollar', Icon: IconCurrencyDollar }, + KZT: { label: 'Kazakhstani tenge', Icon: IconCurrencyTenge }, + LAK: { label: 'Lao kip', Icon: IconCurrencyKip }, + LBP: { label: 'Lebanese pound', Icon: IconCoins }, + LKR: { label: 'Sri Lankan rupee', Icon: IconCoins }, + LRD: { label: 'Liberian dollar', Icon: IconCurrencyDollar }, + LSL: { label: 'Lesotho loti', Icon: IconCurrencyLeu }, + LYD: { label: 'Libyan dinar', Icon: IconCurrencyLyd }, + MAD: { label: 'Moroccan dirham', Icon: IconCurrencyDirham }, + MDL: { label: 'Moldovan leu', Icon: IconCurrencyLeu }, + MGA: { label: 'Malagasy ariary', Icon: IconCoins }, + MKD: { label: 'Macedonian denar', Icon: IconCoins }, + MMK: { label: 'Myanmar kyat', Icon: IconCoins }, + MNT: { label: 'Mongolian tögrög', Icon: IconCurrencyTugrik }, + MOP: { label: 'Macanese pataca', Icon: IconCurrencyRenminbi }, + MRU: { label: 'Mauritanian ouguiya', Icon: IconCoins }, + MUR: { label: 'Mauritian rupee', Icon: IconCoins }, + MVR: { label: 'Maldivian rufiyaa', Icon: IconCurrencyRufiyaa }, + MWK: { label: 'Malawian kwacha', Icon: IconCoins }, + MXN: { label: 'Mexican peso', Icon: IconCoins }, + MYR: { label: 'Malaysian ringgit', Icon: IconCoins }, + MZN: { label: 'Mozambican metical', Icon: IconCoins }, + NAD: { label: 'Namibian dollar', Icon: IconCurrencyDollar }, + NGN: { label: 'Nigerian naira', Icon: IconCurrencyNaira }, + NIO: { label: 'Nicaraguan córdoba', Icon: IconCoins }, + NOK: { label: 'Norwegian krone', Icon: IconCurrencyKroneSwedish }, + NPR: { label: 'Nepalese rupee', Icon: IconCurrencyRupeeNepalese }, + NZD: { label: 'New Zealand dollar', Icon: IconCurrencyDollar }, + OMR: { label: 'Omani rial', Icon: IconCoins }, + PAB: { label: 'Panamanian balboa', Icon: IconCoins }, + PEN: { label: 'Peruvian sol', Icon: IconCoins }, + PGK: { label: 'Papua New Guinean kina', Icon: IconCoins }, + PHP: { label: 'Philippine peso', Icon: IconCurrencyPeso }, + PKR: { label: 'Pakistani rupee', Icon: IconCoins }, + PLN: { label: 'Polish złoty', Icon: IconCurrencyZloty }, + PYG: { label: 'Paraguayan guaraní', Icon: IconCurrencyGuarani }, + QAR: { label: 'Qatari riyal', Icon: IconCurrencyRiyal }, + RON: { label: 'Romanian leu', Icon: IconCurrencyLeu }, + RSD: { label: 'Serbian dinar', Icon: IconCoins }, + RUB: { label: 'Russian ruble', Icon: IconCurrencyRubel }, + RWF: { label: 'Rwandan franc', Icon: IconCoins }, + SAR: { label: 'Saudi riyal', Icon: IconCoins }, + SBD: { label: 'Solomon Islands dollar', Icon: IconCurrencyDollar }, + SCR: { label: 'Seychelles rupee', Icon: IconCoins }, + SDG: { label: 'Sudanese pound', Icon: IconCoins }, + SEK: { label: 'Swedish krona', Icon: IconCurrencyKroneSwedish }, + SGD: { label: 'Singapore dollar', Icon: IconCurrencyDollarSingapore }, + SHP: { label: 'Saint Helena pound', Icon: IconCoins }, + SLE: { label: 'Sierra Leonean leone', Icon: IconCoins }, + SOS: { label: 'Somalian shilling', Icon: IconCoins }, + SRD: { label: 'Surinamese dollar', Icon: IconCurrencyDollar }, + SSP: { label: 'South Sudanese pound', Icon: IconCoins }, + STN: { label: 'São Tomé and Príncipe dobra', Icon: IconCoins }, + SVC: { label: 'Salvadoran colón', Icon: IconCoins }, + SYP: { label: 'Syrian pound', Icon: IconCoins }, + SZL: { label: 'Swazi lilangeni', Icon: IconCurrencyLeu }, + THB: { label: 'Thai Baht', Icon: IconCurrencyBaht }, + TJS: { label: 'Tajikistani somoni', Icon: IconCoins }, + TMT: { label: 'Turkmenistan manat', Icon: IconCurrencyManat }, + TND: { label: 'Tunisian dinar', Icon: IconCoins }, + TOP: { label: 'Tongan paʻanga', Icon: IconCurrencyPaanga }, + TRY: { label: 'Turkish lira', Icon: IconCurrencyLira }, + TTD: { label: 'Trinidad and Tobago dollar', Icon: IconCurrencyDollar }, + TWD: { label: 'Taiwanese dollar', Icon: IconCurrencyRenminbi }, + TZS: { label: 'Tanzanian shilling', Icon: IconCoins }, + UAH: { label: 'Ukrainian hryvnia', Icon: IconCurrencyHryvnia }, + UGX: { label: 'Ugandan shilling', Icon: IconCoins }, + USD: { label: 'United States dollar', Icon: IconCurrencyDollar }, + UYU: { label: 'Uruguayan peso', Icon: IconCoins }, + UZS: { label: 'Uzbekistani sum', Icon: IconCoins }, + VES: { label: 'Venezuelan bolívar', Icon: IconCoins }, + VND: { label: 'Vietnamese đồng', Icon: IconCurrencyDong }, + VUV: { label: 'Vanuatu vatu', Icon: IconCoins }, + WST: { label: 'Samoan tala', Icon: IconCoins }, + XCD: { label: 'East Caribbean dollar', Icon: IconCurrencyDollar }, + YER: { label: 'Yemeni rial', Icon: IconCoins }, + ZAR: { label: 'South African rand', Icon: IconCoins }, + ZMW: { label: 'Zambian kwacha', Icon: IconCoins }, + ZWG: { label: 'Zimbabwe Gold', Icon: IconCoins }, }; diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts index c8cea11fdeb3..1105be7ca947 100644 --- a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsFieldTypeConfigs.ts @@ -1,196 +1,7 @@ -import { - IconComponent, - IllustrationIconArray, - IllustrationIconCalendarEvent, - IllustrationIconCalendarTime, - IllustrationIconCurrency, - IllustrationIconJson, - IllustrationIconLink, - IllustrationIconMail, - IllustrationIconMap, - IllustrationIconNumbers, - IllustrationIconOneToMany, - IllustrationIconPhone, - IllustrationIconSetting, - IllustrationIconStar, - IllustrationIconTag, - IllustrationIconTags, - IllustrationIconText, - IllustrationIconToggle, - IllustrationIconUid, - IllustrationIconUser, -} from 'twenty-ui'; - -import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; -import { DEFAULT_DATE_VALUE } from '@/settings/data-model/constants/DefaultDateValue'; -import { SettingsFieldTypeCategoryType } from '@/settings/data-model/types/SettingsFieldTypeCategoryType'; -import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; - -DEFAULT_DATE_VALUE.setFullYear(DEFAULT_DATE_VALUE.getFullYear() + 2); - -export type SettingsFieldTypeConfig = { - label: string; - Icon: IconComponent; - exampleValue?: unknown; - category: SettingsFieldTypeCategoryType; -}; +import { SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsCompositeFieldTypeConfigs'; +import { SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs'; export const SETTINGS_FIELD_TYPE_CONFIGS = { - [FieldMetadataType.Uuid]: { - label: 'Unique ID', - Icon: IllustrationIconUid, - exampleValue: '00000000-0000-0000-0000-000000000000', - category: 'Advanced', - }, - [FieldMetadataType.Text]: { - label: 'Text', - Icon: IllustrationIconText, - exampleValue: - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum magna enim, dapibus non enim in, lacinia faucibus nunc. Sed interdum ante sed felis facilisis, eget ultricies neque molestie. Mauris auctor, justo eu volutpat cursus, libero erat tempus nulla, non sodales lorem lacus a est.', - category: 'Basic', - }, - [FieldMetadataType.Numeric]: { - label: 'Numeric', - Icon: IllustrationIconNumbers, - exampleValue: 2000, - category: 'Basic', - }, - [FieldMetadataType.Number]: { - label: 'Number', - Icon: IllustrationIconNumbers, - exampleValue: 2000, - category: 'Basic', - }, - [FieldMetadataType.Link]: { - label: 'Link', - Icon: IllustrationIconLink, - exampleValue: { url: 'www.twenty.com', label: '' }, - category: 'Basic', - }, - [FieldMetadataType.Links]: { - label: 'Links', - Icon: IllustrationIconLink, - exampleValue: { primaryLinkUrl: 'twenty.com', primaryLinkLabel: '' }, - category: 'Basic', - }, - [FieldMetadataType.Boolean]: { - label: 'True/False', - Icon: IllustrationIconToggle, - exampleValue: true, - category: 'Basic', - }, - [FieldMetadataType.DateTime]: { - label: 'Date and Time', - Icon: IllustrationIconCalendarTime, - exampleValue: DEFAULT_DATE_VALUE.toISOString(), - category: 'Basic', - }, - [FieldMetadataType.Date]: { - label: 'Date', - Icon: IllustrationIconCalendarEvent, - exampleValue: DEFAULT_DATE_VALUE.toISOString(), - category: 'Basic', - }, - [FieldMetadataType.Select]: { - label: 'Select', - Icon: IllustrationIconTag, - category: 'Basic', - }, - [FieldMetadataType.MultiSelect]: { - label: 'Multi-select', - Icon: IllustrationIconTags, - category: 'Basic', - }, - [FieldMetadataType.Currency]: { - label: 'Currency', - Icon: IllustrationIconCurrency, - exampleValue: { amountMicros: 2000000000, currencyCode: CurrencyCode.USD }, - category: 'Basic', - }, - [FieldMetadataType.Relation]: { - label: 'Relation', - Icon: IllustrationIconOneToMany, - category: 'Relation', - }, - [FieldMetadataType.Email]: { - label: 'Email', - Icon: IllustrationIconMail, - category: 'Basic', - }, - [FieldMetadataType.Emails]: { - label: 'Emails', - Icon: IllustrationIconMail, - exampleValue: { primaryEmail: 'john@twenty.com' }, - category: 'Basic', - }, - [FieldMetadataType.Phone]: { - label: 'Phone', - Icon: IllustrationIconPhone, - exampleValue: '+1234-567-890', - category: 'Basic', - }, - [FieldMetadataType.Phones]: { - label: 'Phones', - Icon: IllustrationIconPhone, - exampleValue: { - primaryPhoneNumber: '234-567-890', - primaryPhoneCountryCode: '+1', - }, - category: 'Basic', - }, - [FieldMetadataType.Rating]: { - label: 'Rating', - Icon: IllustrationIconStar, - exampleValue: '3', - category: 'Basic', - }, - [FieldMetadataType.FullName]: { - label: 'Full Name', - Icon: IllustrationIconUser, - exampleValue: { firstName: 'John', lastName: 'Doe' }, - category: 'Advanced', - }, - [FieldMetadataType.Address]: { - label: 'Address', - Icon: IllustrationIconMap, - exampleValue: { - addressStreet1: '456 Oak Street', - addressStreet2: 'Unit 3B', - addressCity: 'Springfield', - addressState: 'California', - addressCountry: 'United States', - addressPostcode: '90210', - addressLat: 34.0522, - addressLng: -118.2437, - }, - category: 'Basic', - }, - [FieldMetadataType.RawJson]: { - label: 'JSON', - Icon: IllustrationIconJson, - exampleValue: { key: 'value' }, - - category: 'Basic', - }, - [FieldMetadataType.RichText]: { - label: 'System', - Icon: IllustrationIconSetting, - exampleValue: { key: 'value' }, - category: 'Basic', - }, - [FieldMetadataType.Actor]: { - label: 'System', - Icon: IllustrationIconSetting, - category: 'Basic', - }, - [FieldMetadataType.Array]: { - label: 'Array', - Icon: IllustrationIconArray, - category: 'Basic', - exampleValue: ['value1', 'value2'], - }, -} as const satisfies Record< - SettingsSupportedFieldType, - SettingsFieldTypeConfig ->; + ...SETTINGS_COMPOSITE_FIELD_TYPE_CONFIGS, + ...SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS, +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs.ts b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs.ts new file mode 100644 index 000000000000..42f291b60eff --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs.ts @@ -0,0 +1,152 @@ +import { + IconComponent, + IllustrationIconArray, + IllustrationIconCalendarEvent, + IllustrationIconCalendarTime, + IllustrationIconJson, + IllustrationIconMail, + IllustrationIconNumbers, + IllustrationIconOneToMany, + IllustrationIconPhone, + IllustrationIconSetting, + IllustrationIconStar, + IllustrationIconTag, + IllustrationIconTags, + IllustrationIconText, + IllustrationIconToggle, + IllustrationIconUid, +} from 'twenty-ui'; + +import { + FieldArrayValue, + FieldBooleanValue, + FieldDateTimeValue, + FieldDateValue, + FieldEmailValue, + FieldJsonValue, + FieldMultiSelectValue, + FieldNumberValue, + FieldPhoneValue, + FieldRatingValue, + FieldRelationValue, + FieldRichTextValue, + FieldSelectValue, + FieldTextValue, + FieldUUidValue, +} from '@/object-record/record-field/types/FieldMetadata'; +import { DEFAULT_DATE_VALUE } from '@/settings/data-model/constants/DefaultDateValue'; +import { SettingsFieldTypeCategoryType } from '@/settings/data-model/types/SettingsFieldTypeCategoryType'; +import { SettingsNonCompositeFieldType } from '@/settings/data-model/types/SettingsNonCompositeFieldType'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +DEFAULT_DATE_VALUE.setFullYear(DEFAULT_DATE_VALUE.getFullYear() + 2); + +export type SettingsFieldTypeConfig<T> = { + label: string; + Icon: IconComponent; + exampleValue?: T; + category: SettingsFieldTypeCategoryType; +}; + +type SettingsNonCompositeFieldTypeConfigArray = Record< + SettingsNonCompositeFieldType, + SettingsFieldTypeConfig<any> +>; + +// TODO: can we derive this from backend definitions ? +export const SETTINGS_NON_COMPOSITE_FIELD_TYPE_CONFIGS: SettingsNonCompositeFieldTypeConfigArray = + { + [FieldMetadataType.Uuid]: { + label: 'Unique ID', + Icon: IllustrationIconUid, + exampleValue: '00000000-0000-0000-0000-000000000000', + category: 'Advanced', + } as const satisfies SettingsFieldTypeConfig<FieldUUidValue>, + [FieldMetadataType.Text]: { + label: 'Text', + Icon: IllustrationIconText, + exampleValue: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Vestibulum magna enim, dapibus non enim in, lacinia faucibus nunc. Sed interdum ante sed felis facilisis, eget ultricies neque molestie. Mauris auctor, justo eu volutpat cursus, libero erat tempus nulla, non sodales lorem lacus a est.', + category: 'Basic', + } as const satisfies SettingsFieldTypeConfig<FieldTextValue>, + [FieldMetadataType.Numeric]: { + label: 'Numeric', + Icon: IllustrationIconNumbers, + exampleValue: 2000, + category: 'Basic', + } as const satisfies SettingsFieldTypeConfig<FieldNumberValue>, + [FieldMetadataType.Number]: { + label: 'Number', + Icon: IllustrationIconNumbers, + exampleValue: 2000, + category: 'Basic', + } as const satisfies SettingsFieldTypeConfig<FieldNumberValue>, + [FieldMetadataType.Boolean]: { + label: 'True/False', + Icon: IllustrationIconToggle, + exampleValue: true, + category: 'Basic', + } as const satisfies SettingsFieldTypeConfig<FieldBooleanValue>, + [FieldMetadataType.DateTime]: { + label: 'Date and Time', + Icon: IllustrationIconCalendarTime, + exampleValue: DEFAULT_DATE_VALUE.toISOString(), + category: 'Basic', + } as const satisfies SettingsFieldTypeConfig<FieldDateTimeValue>, + [FieldMetadataType.Date]: { + label: 'Date', + Icon: IllustrationIconCalendarEvent, + exampleValue: DEFAULT_DATE_VALUE.toISOString(), + category: 'Basic', + } as const satisfies SettingsFieldTypeConfig<FieldDateValue>, + [FieldMetadataType.Select]: { + label: 'Select', + Icon: IllustrationIconTag, + category: 'Basic', + } as const satisfies SettingsFieldTypeConfig<FieldSelectValue>, + [FieldMetadataType.MultiSelect]: { + label: 'Multi-select', + Icon: IllustrationIconTags, + category: 'Basic', + } as const satisfies SettingsFieldTypeConfig<FieldMultiSelectValue>, + [FieldMetadataType.Relation]: { + label: 'Relation', + Icon: IllustrationIconOneToMany, + category: 'Relation', + } as const satisfies SettingsFieldTypeConfig<FieldRelationValue<any>>, + [FieldMetadataType.Email]: { + label: 'Email', + Icon: IllustrationIconMail, + category: 'Basic', + } as const satisfies SettingsFieldTypeConfig<FieldEmailValue>, + [FieldMetadataType.Phone]: { + label: 'Phone', + Icon: IllustrationIconPhone, + exampleValue: '+1234-567-890', + category: 'Basic', + } as const satisfies SettingsFieldTypeConfig<FieldPhoneValue>, + [FieldMetadataType.Rating]: { + label: 'Rating', + Icon: IllustrationIconStar, + exampleValue: 'RATING_3', + category: 'Basic', + } as const satisfies SettingsFieldTypeConfig<FieldRatingValue>, + [FieldMetadataType.RawJson]: { + label: 'JSON', + Icon: IllustrationIconJson, + exampleValue: { key: 'value' }, + category: 'Basic', + } as const satisfies SettingsFieldTypeConfig<FieldJsonValue>, + [FieldMetadataType.RichText]: { + label: 'Rich Text', + Icon: IllustrationIconSetting, + exampleValue: { key: 'value' }, + category: 'Basic', + } as const satisfies SettingsFieldTypeConfig<FieldRichTextValue>, + [FieldMetadataType.Array]: { + label: 'Array', + Icon: IllustrationIconArray, + category: 'Basic', + exampleValue: ['value1', 'value2'], + } as const satisfies SettingsFieldTypeConfig<FieldArrayValue>, + }; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm.tsx index d4771446e5d3..c3b82ccddd8c 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm.tsx @@ -3,10 +3,14 @@ import { Controller, useFormContext } from 'react-hook-form'; import { z } from 'zod'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fieldMetadataItemSchema'; import { getErrorMessageFromError } from '@/settings/data-model/fields/forms/utils/errorMessages'; +import { RelationType } from '@/settings/data-model/types/RelationType'; import { IconPicker } from '@/ui/input/components/IconPicker'; import { TextInput } from '@/ui/input/components/TextInput'; +import { useEffect, useState } from 'react'; +import { RelationDefinitionType } from '~/generated-metadata/graphql'; export const settingsDataModelFieldIconLabelFormSchema = ( existingOtherLabels: string[] = [], @@ -32,19 +36,47 @@ type SettingsDataModelFieldIconLabelFormProps = { disabled?: boolean; fieldMetadataItem?: FieldMetadataItem; maxLength?: number; + relationObjectMetadataItem?: ObjectMetadataItem; + relationType?: RelationType; }; export const SettingsDataModelFieldIconLabelForm = ({ disabled, fieldMetadataItem, maxLength, + relationObjectMetadataItem, + relationType, }: SettingsDataModelFieldIconLabelFormProps) => { const { control, trigger, formState: { errors }, + setValue, } = useFormContext<SettingsDataModelFieldIconLabelFormValues>(); + const [labelEditedManually, setLabelEditedManually] = useState(false); + const [iconEditedManually, setIconEditedManually] = useState(false); + + useEffect(() => { + if (labelEditedManually) return; + const label = [ + RelationDefinitionType.ManyToOne, + RelationDefinitionType.ManyToMany, + ].includes(relationType ?? RelationDefinitionType.OneToMany) + ? relationObjectMetadataItem?.labelPlural + : relationObjectMetadataItem?.labelSingular; + setValue('label', label ?? ''); + + if (iconEditedManually) return; + setValue('icon', relationObjectMetadataItem?.icon ?? 'IconUsers'); + }, [ + labelEditedManually, + iconEditedManually, + relationObjectMetadataItem, + setValue, + relationType, + ]); + return ( <StyledInputsContainer> <Controller @@ -55,7 +87,10 @@ export const SettingsDataModelFieldIconLabelForm = ({ <IconPicker disabled={disabled} selectedIconKey={value ?? ''} - onChange={({ iconKey }) => onChange(iconKey)} + onChange={({ iconKey }) => { + setIconEditedManually(true); + onChange(iconKey); + }} variant="primary" /> )} @@ -69,6 +104,7 @@ export const SettingsDataModelFieldIconLabelForm = ({ placeholder="Employees" value={value} onChange={(e) => { + setLabelEditedManually(true); onChange(e); trigger('label'); }} 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 a71cc1654bcf..882b57f7d1c1 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 @@ -11,6 +11,8 @@ import { settingsDataModelFieldCurrencyFormSchema } from '@/settings/data-model/ import { SettingsDataModelFieldCurrencySettingsFormCard } from '@/settings/data-model/fields/forms/currency/components/SettingsDataModelFieldCurrencySettingsFormCard'; import { settingsDataModelFieldDateFormSchema } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateForm'; import { SettingsDataModelFieldDateSettingsFormCard } from '@/settings/data-model/fields/forms/date/components/SettingsDataModelFieldDateSettingsFormCard'; +import { settingsDataModelFieldNumberFormSchema } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberForm'; +import { SettingsDataModelFieldNumberSettingsFormCard } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberSettingsFormCard'; import { settingsDataModelFieldRelationFormSchema } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm'; import { SettingsDataModelFieldRelationSettingsFormCard } from '@/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard'; import { @@ -52,6 +54,10 @@ const multiSelectFieldFormSchema = z .object({ type: z.literal(FieldMetadataType.MultiSelect) }) .merge(settingsDataModelFieldMultiSelectFormSchema); +const numberFieldFormSchema = z + .object({ type: z.literal(FieldMetadataType.Number) }) + .merge(settingsDataModelFieldNumberFormSchema); + const otherFieldsFormSchema = z.object({ type: z.enum( Object.keys( @@ -63,6 +69,7 @@ const otherFieldsFormSchema = z.object({ FieldMetadataType.MultiSelect, FieldMetadataType.Date, FieldMetadataType.DateTime, + FieldMetadataType.Number, ]), ) as [FieldMetadataType, ...FieldMetadataType[]], ), @@ -78,13 +85,17 @@ export const settingsDataModelFieldSettingsFormSchema = z.discriminatedUnion( relationFieldFormSchema, selectFieldFormSchema, multiSelectFieldFormSchema, + numberFieldFormSchema, otherFieldsFormSchema, ], ); type SettingsDataModelFieldSettingsFormCardProps = { isCreatingField?: boolean; - fieldMetadataItem: Pick<FieldMetadataItem, 'icon' | 'label' | 'type'> & + fieldMetadataItem: Pick< + FieldMetadataItem, + 'icon' | 'label' | 'type' | 'isCustom' + > & Partial<Omit<FieldMetadataItem, 'icon' | 'label' | 'type'>>; } & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>; @@ -163,6 +174,16 @@ export const SettingsDataModelFieldSettingsFormCard = ({ ); } + if (fieldMetadataItem.type === FieldMetadataType.Number) { + return ( + <SettingsDataModelFieldNumberSettingsFormCard + disabled={fieldMetadataItem.isCustom === false} + fieldMetadataItem={fieldMetadataItem} + objectMetadataItem={objectMetadataItem} + /> + ); + } + if ( fieldMetadataItem.type === FieldMetadataType.Select || fieldMetadataItem.type === FieldMetadataType.MultiSelect diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect.tsx deleted file mode 100644 index 14e68598e33d..000000000000 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; -import { SettingsCard } from '@/settings/components/SettingsCard'; -import { SETTINGS_FIELD_TYPE_CATEGORIES } from '@/settings/data-model/constants/SettingsFieldTypeCategories'; -import { SETTINGS_FIELD_TYPE_CATEGORY_DESCRIPTIONS } from '@/settings/data-model/constants/SettingsFieldTypeCategoryDescriptions'; -import { - SETTINGS_FIELD_TYPE_CONFIGS, - SettingsFieldTypeConfig, -} from '@/settings/data-model/constants/SettingsFieldTypeConfigs'; -import { useBooleanSettingsFormInitialValues } from '@/settings/data-model/fields/forms/boolean/hooks/useBooleanSettingsFormInitialValues'; -import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues'; -import { useSelectSettingsFormInitialValues } from '@/settings/data-model/fields/forms/select/hooks/useSelectSettingsFormInitialValues'; -import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; -import { TextInput } from '@/ui/input/components/TextInput'; -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 { z } from 'zod'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; - -export const settingsDataModelFieldTypeFormSchema = z.object({ - type: z.enum( - Object.keys(SETTINGS_FIELD_TYPE_CONFIGS) as [ - SettingsSupportedFieldType, - ...SettingsSupportedFieldType[], - ], - ), -}); - -export type SettingsDataModelFieldTypeFormValues = z.infer< - typeof settingsDataModelFieldTypeFormSchema ->; - -type SettingsDataModelFieldTypeSelectProps = { - className?: string; - excludedFieldTypes?: SettingsSupportedFieldType[]; - fieldMetadataItem?: Pick< - FieldMetadataItem, - 'defaultValue' | 'options' | 'type' - >; - onFieldTypeSelect: () => void; -}; - -const StyledTypeSelectContainer = styled.div` - display: flex; - flex-direction: column; - gap: inherit; - width: 100%; -`; - -const StyledContainer = styled.div` - display: flex; - gap: ${({ theme }) => theme.spacing(2)}; - justify-content: flex-start; - flex-wrap: wrap; - width: 100%; -`; - -const StyledCardContainer = styled.div` - display: flex; - - position: relative; - width: calc(50% - ${({ theme }) => theme.spacing(1)}); -`; - -const StyledSearchInput = styled(TextInput)` - width: 100%; -`; - -export const SettingsDataModelFieldTypeSelect = ({ - className, - excludedFieldTypes = [], - fieldMetadataItem, - onFieldTypeSelect, -}: SettingsDataModelFieldTypeSelectProps) => { - const theme = useTheme(); - const { control } = useFormContext<SettingsDataModelFieldTypeFormValues>(); - const [searchQuery, setSearchQuery] = useState(''); - const fieldTypeConfigs = Object.entries<SettingsFieldTypeConfig>( - SETTINGS_FIELD_TYPE_CONFIGS, - ).filter( - ([key, config]) => - !excludedFieldTypes.includes(key as SettingsSupportedFieldType) && - config.label.toLowerCase().includes(searchQuery.toLowerCase()), - ); - - const { resetDefaultValueField: resetBooleanDefaultValueField } = - useBooleanSettingsFormInitialValues({ fieldMetadataItem }); - - const { resetDefaultValueField: resetCurrencyDefaultValueField } = - useCurrencySettingsFormInitialValues({ fieldMetadataItem }); - - const { resetDefaultValueField: resetSelectDefaultValueField } = - useSelectSettingsFormInitialValues({ fieldMetadataItem }); - - const resetDefaultValueField = (nextValue: SettingsSupportedFieldType) => { - switch (nextValue) { - case FieldMetadataType.Boolean: - resetBooleanDefaultValueField(); - break; - case FieldMetadataType.Currency: - resetCurrencyDefaultValueField(); - break; - case FieldMetadataType.Select: - case FieldMetadataType.MultiSelect: - resetSelectDefaultValueField(); - break; - default: - break; - } - }; - - return ( - <Controller - name="type" - control={control} - defaultValue={ - fieldMetadataItem && fieldMetadataItem.type in fieldTypeConfigs - ? (fieldMetadataItem.type as SettingsSupportedFieldType) - : FieldMetadataType.Text - } - render={({ field: { onChange } }) => ( - <StyledTypeSelectContainer className={className}> - <Section> - <StyledSearchInput - LeftIcon={IconSearch} - placeholder="Search a type" - value={searchQuery} - onChange={setSearchQuery} - /> - </Section> - {SETTINGS_FIELD_TYPE_CATEGORIES.map((category) => ( - <Section key={category}> - <H2Title - title={category} - description={ - SETTINGS_FIELD_TYPE_CATEGORY_DESCRIPTIONS[category] - } - /> - <StyledContainer> - {fieldTypeConfigs - .filter(([, config]) => config.category === category) - .map(([key, config]) => ( - <StyledCardContainer> - <SettingsCard - key={key} - onClick={() => { - onChange(key as SettingsSupportedFieldType); - resetDefaultValueField( - key as SettingsSupportedFieldType, - ); - onFieldTypeSelect(); - }} - Icon={ - <config.Icon - size={theme.icon.size.xl} - stroke={theme.icon.stroke.sm} - /> - } - title={config.label} - /> - </StyledCardContainer> - ))} - </StyledContainer> - </Section> - ))} - </StyledTypeSelectContainer> - )} - /> - ); -}; 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 new file mode 100644 index 000000000000..962fb7aaba5d --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsObjectNewFieldSelector.tsx @@ -0,0 +1,160 @@ +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { SettingsCard } from '@/settings/components/SettingsCard'; +import { SETTINGS_FIELD_TYPE_CATEGORIES } from '@/settings/data-model/constants/SettingsFieldTypeCategories'; +import { SETTINGS_FIELD_TYPE_CATEGORY_DESCRIPTIONS } from '@/settings/data-model/constants/SettingsFieldTypeCategoryDescriptions'; +import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs'; +import { SettingsFieldTypeConfig } from '@/settings/data-model/constants/SettingsNonCompositeFieldTypeConfigs'; +import { useBooleanSettingsFormInitialValues } from '@/settings/data-model/fields/forms/boolean/hooks/useBooleanSettingsFormInitialValues'; +import { useCurrencySettingsFormInitialValues } from '@/settings/data-model/fields/forms/currency/hooks/useCurrencySettingsFormInitialValues'; +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 { FieldMetadataType } from '~/generated-metadata/graphql'; +import { SettingsDataModelFieldTypeFormValues } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldSelect'; + +type SettingsObjectNewFieldSelectorProps = { + className?: string; + excludedFieldTypes?: SettingsFieldType[]; + fieldMetadataItem?: Pick< + FieldMetadataItem, + 'defaultValue' | 'options' | 'type' + >; + + objectSlug: string; +}; + +const StyledTypeSelectContainer = styled.div` + display: flex; + flex-direction: column; + gap: inherit; + width: 100%; +`; + +const StyledContainer = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + justify-content: flex-start; + flex-wrap: wrap; + width: 100%; +`; + +const StyledCardContainer = styled.div` + display: flex; + + position: relative; + width: calc(50% - ${({ theme }) => theme.spacing(1)}); +`; + +const StyledSearchInput = styled(TextInput)` + width: 100%; +`; + +export const SettingsObjectNewFieldSelector = ({ + excludedFieldTypes = [], + fieldMetadataItem, + objectSlug, +}: SettingsObjectNewFieldSelectorProps) => { + const theme = useTheme(); + const { control, setValue } = + useFormContext<SettingsDataModelFieldTypeFormValues>(); + const [searchQuery, setSearchQuery] = useState(''); + const fieldTypeConfigs = Object.entries<SettingsFieldTypeConfig<any>>( + SETTINGS_FIELD_TYPE_CONFIGS, + ).filter( + ([key, config]) => + !excludedFieldTypes.includes(key as SettingsFieldType) && + config.label.toLowerCase().includes(searchQuery.toLowerCase()), + ); + + const { resetDefaultValueField: resetBooleanDefaultValueField } = + useBooleanSettingsFormInitialValues({ fieldMetadataItem }); + + const { resetDefaultValueField: resetCurrencyDefaultValueField } = + useCurrencySettingsFormInitialValues({ fieldMetadataItem }); + + const { resetDefaultValueField: resetSelectDefaultValueField } = + useSelectSettingsFormInitialValues({ fieldMetadataItem }); + + const resetDefaultValueField = (nextValue: SettingsFieldType) => { + switch (nextValue) { + case FieldMetadataType.Boolean: + resetBooleanDefaultValueField(); + break; + case FieldMetadataType.Currency: + resetCurrencyDefaultValueField(); + break; + case FieldMetadataType.Select: + case FieldMetadataType.MultiSelect: + resetSelectDefaultValueField(); + break; + default: + break; + } + }; + + return ( + <> + {' '} + <Section> + <StyledSearchInput + LeftIcon={IconSearch} + placeholder="Search a type" + value={searchQuery} + onChange={setSearchQuery} + /> + </Section> + <Controller + name="type" + control={control} + render={() => ( + <StyledTypeSelectContainer> + {SETTINGS_FIELD_TYPE_CATEGORIES.map((category) => ( + <Section key={category}> + <H2Title + title={category} + description={ + SETTINGS_FIELD_TYPE_CATEGORY_DESCRIPTIONS[category] + } + /> + <StyledContainer> + {fieldTypeConfigs + .filter(([, config]) => config.category === category) + .map(([key, config]) => ( + <StyledCardContainer key={key}> + <UndecoratedLink + to={`/settings/objects/${objectSlug}/new-field/configure?fieldType=${key}`} + fullWidth + onClick={() => { + setValue('type', key as SettingsFieldType); + resetDefaultValueField(key as SettingsFieldType); + }} + > + <SettingsCard + key={key} + Icon={ + <config.Icon + size={theme.icon.size.xl} + stroke={theme.icon.stroke.sm} + /> + } + title={config.label} + /> + </UndecoratedLink> + </StyledCardContainer> + ))} + </StyledContainer> + </Section> + ))} + </StyledTypeSelectContainer> + )} + /> + </> + ); +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldDescriptionForm.stories.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldDescriptionForm.stories.tsx index 7499adea0338..58f33bf2210f 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldDescriptionForm.stories.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldDescriptionForm.stories.tsx @@ -3,7 +3,7 @@ import { ComponentDecorator } from 'twenty-ui'; import { FormProviderDecorator } from '~/testing/decorators/FormProviderDecorator'; -import { mockedPersonObjectMetadataItem } from '~/testing/mock-data/metadata'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { SettingsDataModelFieldDescriptionForm } from '../SettingsDataModelFieldDescriptionForm'; const meta: Meta<typeof SettingsDataModelFieldDescriptionForm> = { @@ -25,11 +25,15 @@ type Story = StoryObj<typeof SettingsDataModelFieldDescriptionForm>; export const Default: Story = {}; +const mockedPersonObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.namePlural === 'person', +); + export const WithFieldMetadataItem: Story = { args: { - fieldMetadataItem: mockedPersonObjectMetadataItem.fields.find( + fieldMetadataItem: mockedPersonObjectMetadataItem?.fields.find( ({ description }) => description === 'description', - )!, + ), }, }; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldIconLabelForm.stories.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldIconLabelForm.stories.tsx index 150efdc46683..555db1bc92d1 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldIconLabelForm.stories.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldIconLabelForm.stories.tsx @@ -5,7 +5,7 @@ import { ComponentDecorator } from 'twenty-ui'; import { FormProviderDecorator } from '~/testing/decorators/FormProviderDecorator'; import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator'; -import { mockedPersonObjectMetadataItem } from '~/testing/mock-data/metadata'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { SettingsDataModelFieldIconLabelForm } from '../SettingsDataModelFieldIconLabelForm'; const StyledContainer = styled.div` @@ -32,11 +32,15 @@ type Story = StoryObj<typeof SettingsDataModelFieldIconLabelForm>; export const Default: Story = {}; +const mockedPersonObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.namePlural === 'person', +); + export const WithFieldMetadataItem: Story = { args: { - fieldMetadataItem: mockedPersonObjectMetadataItem.fields.find( + fieldMetadataItem: mockedPersonObjectMetadataItem?.fields.find( ({ name }) => name === 'name', - )!, + ), }, }; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldSettingsFormCard.stories.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldSettingsFormCard.stories.tsx index aff46aa2da0e..09aaf60c282c 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldSettingsFormCard.stories.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldSettingsFormCard.stories.tsx @@ -7,10 +7,18 @@ import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorato import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { graphqlMocks } from '~/testing/graphqlMocks'; -import { mockedCompanyObjectMetadataItem } from '~/testing/mock-data/metadata'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { SettingsDataModelFieldSettingsFormCard } from '../SettingsDataModelFieldSettingsFormCard'; +const mockedCompanyObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'company', +); + +if (!mockedCompanyObjectMetadataItem) { + throw new Error('Company object metadata item not found'); +} + const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( ({ type }) => type === FieldMetadataType.Text, )!; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldTypeSelect.stories.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldTypeSelect.stories.tsx deleted file mode 100644 index dbd3ea3c93bd..000000000000 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/__stories__/SettingsDataModelFieldTypeSelect.stories.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { Meta, StoryObj } from '@storybook/react'; -import { expect, userEvent, within } from '@storybook/test'; -import { ComponentDecorator } from 'twenty-ui'; - -import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { FormProviderDecorator } from '~/testing/decorators/FormProviderDecorator'; -import { graphqlMocks } from '~/testing/graphqlMocks'; - -import { SettingsDataModelFieldTypeSelect } from '../SettingsDataModelFieldTypeSelect'; - -const meta: Meta<typeof SettingsDataModelFieldTypeSelect> = { - title: - 'Modules/Settings/DataModel/Fields/Forms/SettingsDataModelFieldTypeSelect', - component: SettingsDataModelFieldTypeSelect, - decorators: [FormProviderDecorator, ComponentDecorator], - parameters: { - container: { width: 512 }, - msw: graphqlMocks, - }, -}; - -export default meta; -type Story = StoryObj<typeof SettingsDataModelFieldTypeSelect>; - -export const Default: Story = {}; - -export const WithOpenSelect: Story = { - play: async () => { - const canvas = within(document.body); - - const inputField = await canvas.findByText('Text'); - - await userEvent.click(inputField); - - const input = await canvas.findByText('Unique ID'); - await userEvent.click(input); - - await userEvent.click(inputField); - }, -}; - -export const WithExcludedFieldTypes: Story = { - args: { - excludedFieldTypes: [FieldMetadataType.Uuid, FieldMetadataType.Numeric], - }, - play: async () => { - const canvas = within(document.body); - - const inputField = await canvas.findByText('Text'); - - await userEvent.click(inputField); - - await canvas.findByText('Number'); - - expect(canvas.queryByText('Unique ID')).toBeNull(); - expect(canvas.queryByText('Numeric')).toBeNull(); - }, -}; 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 new file mode 100644 index 000000000000..706bcea37154 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberDecimalInput.tsx @@ -0,0 +1,168 @@ +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 { castAsNumberOrNull } from '~/utils/cast-as-number-or-null'; + +type SettingsDataModelFieldNumberDecimalsInputProps = { + value: number; + onChange: (value: number) => void; + disabled?: boolean; +}; + +const StyledCounterContainer = styled.div` + align-items: center; + background: ${({ theme }) => theme.background.noisy}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + border-radius: 4px; + display: flex; + flex-direction: column; + flex: 1; + gap: ${({ theme }) => theme.spacing(1)}; + justify-content: center; +`; + +const StyledExampleText = styled.div` + color: ${({ theme }) => theme.font.color.primary}; + width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + font-weight: ${({ theme }) => theme.font.weight.regular}; +`; + +const StyledCounterControlsIcons = styled.div` + align-items: center; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledCounterInnerContainer = styled.div` + align-items: center; + align-self: stretch; + display: flex; + gap: ${({ theme }) => theme.spacing(1)}; + padding: ${({ theme }) => theme.spacing(2)}; + height: 24px; +`; + +const StyledTextInput = styled(TextInput)` + width: ${({ theme }) => theme.spacing(16)}; + input { + width: ${({ theme }) => theme.spacing(16)}; + height: ${({ theme }) => theme.spacing(6)}; + text-align: center; + font-weight: ${({ theme }) => theme.font.weight.medium}; + background: ${({ theme }) => theme.background.noisy}; + } + input ~ div { + padding-right: ${({ theme }) => theme.spacing(0)}; + border-radius: ${({ theme }) => theme.spacing(1)}; + background: ${({ theme }) => theme.background.noisy}; + } +`; + +const StyledTitle = 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 StyledControlButton = styled(Button)` + height: ${({ theme }) => theme.spacing(6)}; + width: ${({ theme }) => theme.spacing(6)}; + padding: 0; + justify-content: center; + svg { + height: ${({ theme }) => theme.spacing(4)}; + width: ${({ theme }) => theme.spacing(4)}; + } +`; + +const StyledInfoButton = styled(Button)` + height: ${({ theme }) => theme.spacing(6)}; + width: ${({ theme }) => theme.spacing(6)}; + padding: 0; + justify-content: center; + svg { + color: ${({ theme }) => theme.font.color.extraLight}; + height: ${({ theme }) => theme.spacing(4)}; + width: ${({ theme }) => theme.spacing(4)}; + } +`; + +const MIN_VALUE = 0; +const MAX_VALUE = 100; +export const SettingsDataModelFieldNumberDecimalsInput = ({ + value, + onChange, + disabled, +}: SettingsDataModelFieldNumberDecimalsInputProps) => { + const exampleValue = (1000).toFixed(value); + + const handleIncrementCounter = () => { + if (value < MAX_VALUE) { + const newValue = value + 1; + onChange(newValue); + } + }; + + const handleDecrementCounter = () => { + if (value > MIN_VALUE) { + const newValue = value - 1; + onChange(newValue); + } + }; + + const handleTextInputChange = (value: string) => { + const castedNumber = castAsNumberOrNull(value); + if (castedNumber === null) { + onChange(MIN_VALUE); + return; + } + + if (castedNumber < MIN_VALUE) { + return; + } + + if (castedNumber > MAX_VALUE) { + onChange(MAX_VALUE); + return; + } + onChange(castedNumber); + }; + return ( + <> + <StyledTitle>Number of decimals</StyledTitle> + <StyledCounterContainer> + <StyledCounterInnerContainer> + <StyledExampleText>Example: {exampleValue}</StyledExampleText> + <StyledCounterControlsIcons> + <StyledInfoButton variant="tertiary" Icon={IconInfoCircle} /> + <StyledControlButton + variant="secondary" + onClick={handleDecrementCounter} + Icon={IconMinus} + disabled={disabled} + /> + <StyledTextInput + name="decimals" + fullWidth + value={value.toString()} + onChange={(value) => handleTextInputChange(value)} + disabled={disabled} + /> + <StyledControlButton + variant="secondary" + onClick={handleIncrementCounter} + Icon={IconPlus} + disabled={disabled} + /> + </StyledCounterControlsIcons> + </StyledCounterInnerContainer> + </StyledCounterContainer> + </> + ); +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberForm.tsx new file mode 100644 index 000000000000..3a80bf0e6104 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberForm.tsx @@ -0,0 +1,55 @@ +import { Controller, useFormContext } from 'react-hook-form'; +import { z } from 'zod'; + +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { numberFieldDefaultValueSchema } from '@/object-record/record-field/validation-schemas/numberFieldDefaultValueSchema'; +import { SettingsDataModelFieldNumberDecimalsInput } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberDecimalInput'; +import { CardContent } from '@/ui/layout/card/components/CardContent'; +import { DEFAULT_DECIMAL_VALUE } from '~/utils/format/number'; + +export const settingsDataModelFieldNumberFormSchema = z.object({ + settings: numberFieldDefaultValueSchema, +}); + +export type SettingsDataModelFieldNumberFormValues = z.infer< + typeof settingsDataModelFieldNumberFormSchema +>; + +type SettingsDataModelFieldNumberFormProps = { + disabled?: boolean; + fieldMetadataItem: Pick< + FieldMetadataItem, + 'icon' | 'label' | 'type' | 'defaultValue' | 'settings' + >; +}; + +export const SettingsDataModelFieldNumberForm = ({ + disabled, + fieldMetadataItem, +}: SettingsDataModelFieldNumberFormProps) => { + const { control } = useFormContext<SettingsDataModelFieldNumberFormValues>(); + + return ( + <CardContent> + <Controller + name="settings" + defaultValue={{ + decimals: + fieldMetadataItem?.settings?.decimals ?? DEFAULT_DECIMAL_VALUE, + }} + control={control} + render={({ field: { onChange, value } }) => { + const count = value?.decimals ?? 0; + + return ( + <SettingsDataModelFieldNumberDecimalsInput + value={count} + onChange={(value) => onChange({ decimals: value })} + disabled={disabled} + ></SettingsDataModelFieldNumberDecimalsInput> + ); + }} + /> + </CardContent> + ); +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberSettingsFormCard.tsx new file mode 100644 index 000000000000..edea86760fbf --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberSettingsFormCard.tsx @@ -0,0 +1,45 @@ +import styled from '@emotion/styled'; + +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; +import { SettingsDataModelPreviewFormCard } from '@/settings/data-model/components/SettingsDataModelPreviewFormCard'; +import { SettingsDataModelFieldNumberForm } from '@/settings/data-model/fields/forms/number/components/SettingsDataModelFieldNumberForm'; +import { + SettingsDataModelFieldPreviewCard, + SettingsDataModelFieldPreviewCardProps, +} from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard'; + +type SettingsDataModelFieldNumberSettingsFormCardProps = { + disabled?: boolean; + fieldMetadataItem: Pick< + FieldMetadataItem, + 'icon' | 'label' | 'type' | 'defaultValue' + >; +} & Pick<SettingsDataModelFieldPreviewCardProps, 'objectMetadataItem'>; + +const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` + display: grid; + flex: 1 1 100%; +`; + +export const SettingsDataModelFieldNumberSettingsFormCard = ({ + disabled, + fieldMetadataItem, + objectMetadataItem, +}: SettingsDataModelFieldNumberSettingsFormCardProps) => { + return ( + <SettingsDataModelPreviewFormCard + preview={ + <StyledFieldPreviewCard + fieldMetadataItem={fieldMetadataItem} + objectMetadataItem={objectMetadataItem} + /> + } + form={ + <SettingsDataModelFieldNumberForm + disabled={disabled} + fieldMetadataItem={fieldMetadataItem} + /> + } + /> + ); +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm.tsx index d30e05193f4b..9b0968c4f180 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationForm.tsx @@ -10,10 +10,13 @@ import { fieldMetadataItemSchema } from '@/object-metadata/validation-schemas/fi import { FIELD_NAME_MAXIMUM_LENGTH } from '@/settings/data-model/constants/FieldNameMaximumLength'; import { RELATION_TYPES } from '@/settings/data-model/constants/RelationTypes'; import { useRelationSettingsFormInitialValues } from '@/settings/data-model/fields/forms/relation/hooks/useRelationSettingsFormInitialValues'; +import { SettingsDataModelFieldPreviewCardProps } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard'; import { RelationType } from '@/settings/data-model/types/RelationType'; import { IconPicker } from '@/ui/input/components/IconPicker'; import { Select } from '@/ui/input/components/Select'; import { TextInput } from '@/ui/input/components/TextInput'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; +import { useEffect, useState } from 'react'; import { RelationDefinitionType } from '~/generated-metadata/graphql'; export const settingsDataModelFieldRelationFormSchema = z.object({ @@ -38,19 +41,19 @@ export type SettingsDataModelFieldRelationFormValues = z.infer< type SettingsDataModelFieldRelationFormProps = { fieldMetadataItem: Pick<FieldMetadataItem, 'type'>; + objectMetadataItem: SettingsDataModelFieldPreviewCardProps['objectMetadataItem']; }; const StyledContainer = styled.div` padding: ${({ theme }) => theme.spacing(4)}; `; -const StyledSelectsContainer = styled.div` +const StyledSelectsContainer = styled.div<{ isMobile: boolean }>` display: grid; gap: ${({ theme }) => theme.spacing(4)}; - grid-template-columns: 1fr 1fr; + grid-template-columns: ${({ isMobile }) => (isMobile ? '1fr' : '1fr 1fr')}; margin-bottom: ${({ theme }) => theme.spacing(4)}; `; - const StyledInputsLabel = styled.span` color: ${({ theme }) => theme.font.color.light}; display: block; @@ -66,7 +69,11 @@ const StyledInputsContainer = styled.div` `; const RELATION_TYPE_OPTIONS = Object.entries(RELATION_TYPES) - .filter(([value]) => 'ONE_TO_ONE' !== value) + .filter( + ([value]) => + RelationDefinitionType.OneToOne !== value && + RelationDefinitionType.ManyToMany !== value, + ) .map(([value, { label, Icon }]) => ({ label, value: value as RelationType, @@ -75,28 +82,53 @@ const RELATION_TYPE_OPTIONS = Object.entries(RELATION_TYPES) export const SettingsDataModelFieldRelationForm = ({ fieldMetadataItem, + objectMetadataItem, }: SettingsDataModelFieldRelationFormProps) => { - const { control, watch: watchFormValue } = - useFormContext<SettingsDataModelFieldRelationFormValues>(); + const { + control, + watch: watchFormValue, + setValue, + } = useFormContext<SettingsDataModelFieldRelationFormValues>(); const { getIcon } = useIcons(); const { objectMetadataItems, findObjectMetadataItemById } = useFilteredObjectMetadataItems(); + const [labelEditedManually, setLabelEditedManually] = useState(false); + const { disableFieldEdition, disableRelationEdition, initialRelationFieldMetadataItem, initialRelationObjectMetadataItem, initialRelationType, - } = useRelationSettingsFormInitialValues({ fieldMetadataItem }); + } = useRelationSettingsFormInitialValues({ + fieldMetadataItem, + objectMetadataItem, + }); const selectedObjectMetadataItem = findObjectMetadataItemById( watchFormValue('relation.objectMetadataId'), ); + const isMobile = useIsMobile(); + const relationType = watchFormValue('relation.type'); + + useEffect(() => { + if (labelEditedManually) return; + setValue( + 'relation.field.label', + [ + RelationDefinitionType.ManyToMany, + RelationDefinitionType.ManyToOne, + ].includes(relationType) + ? objectMetadataItem.labelPlural + : objectMetadataItem.labelSingular, + ); + }, [labelEditedManually, objectMetadataItem, relationType, setValue]); + return ( <StyledContainer> - <StyledSelectsContainer> + <StyledSelectsContainer isMobile={isMobile}> <Controller name="relation.type" control={control} @@ -163,7 +195,10 @@ export const SettingsDataModelFieldRelationForm = ({ disabled={disableFieldEdition} placeholder="Field name" value={value} - onChange={onChange} + onChange={(newValue) => { + setLabelEditedManually(true); + onChange(newValue); + }} fullWidth maxLength={FIELD_NAME_MAXIMUM_LENGTH} /> 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 7452d5f014e2..33509e08e632 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 @@ -14,8 +14,8 @@ import { SettingsDataModelFieldPreviewCard, SettingsDataModelFieldPreviewCardProps, } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard'; +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { FieldMetadataType } from '~/generated-metadata/graphql'; - type SettingsDataModelFieldRelationSettingsFormCardProps = { fieldMetadataItem: Pick<FieldMetadataItem, 'icon' | 'label' | 'type'> & Partial<Omit<FieldMetadataItem, 'icon' | 'label' | 'type'>>; @@ -27,14 +27,23 @@ const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)` flex: 1 1 100%; `; -const StyledPreviewContent = styled.div` +const StyledPreviewContent = styled.div<{ isMobile: boolean }>` display: flex; - flex-direction: column; gap: 6px; + flex-direction: ${({ isMobile }) => (isMobile ? 'column' : 'row')}; `; -const StyledRelationImage = styled.img<{ flip?: boolean }>` - transform: ${({ flip }) => (flip ? 'scaleX(-1) rotate(270deg)' : 'none')}; +const StyledRelationImage = styled.img<{ flip?: boolean; isMobile: boolean }>` + transform: ${({ flip, isMobile }) => { + let transform = ''; + if (isMobile) { + transform += 'rotate(90deg) '; + } + if (flip === true) { + transform += 'scaleX(-1)'; + } + return transform.trim(); + }}; margin: auto; width: 54px; `; @@ -46,7 +55,7 @@ export const SettingsDataModelFieldRelationSettingsFormCard = ({ const { watch: watchFormValue } = useFormContext<SettingsDataModelFieldRelationFormValues>(); const { findObjectMetadataItemById } = useFilteredObjectMetadataItems(); - + const isMobile = useIsMobile(); const { initialRelationObjectMetadataItem, initialRelationType, @@ -69,7 +78,7 @@ export const SettingsDataModelFieldRelationSettingsFormCard = ({ return ( <SettingsDataModelPreviewFormCard preview={ - <StyledPreviewContent> + <StyledPreviewContent isMobile={isMobile}> <StyledFieldPreviewCard fieldMetadataItem={fieldMetadataItem} shrink @@ -80,6 +89,7 @@ export const SettingsDataModelFieldRelationSettingsFormCard = ({ src={relationTypeConfig.imageSrc} flip={relationTypeConfig.isImageFlipped} alt={relationTypeConfig.label} + isMobile={isMobile} /> <StyledFieldPreviewCard fieldMetadataItem={{ @@ -104,6 +114,7 @@ export const SettingsDataModelFieldRelationSettingsFormCard = ({ form={ <SettingsDataModelFieldRelationForm fieldMetadataItem={fieldMetadataItem} + objectMetadataItem={objectMetadataItem} /> } /> diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/hooks/useRelationSettingsFormInitialValues.ts b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/hooks/useRelationSettingsFormInitialValues.ts index bd4b2badd0e2..298c3ad6dc24 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/hooks/useRelationSettingsFormInitialValues.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/hooks/useRelationSettingsFormInitialValues.ts @@ -2,15 +2,17 @@ import { useMemo } from 'react'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { isObjectMetadataAvailableForRelation } from '@/object-metadata/utils/isObjectMetadataAvailableForRelation'; +import { SettingsDataModelFieldPreviewCardProps } from '@/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard'; import { RelationDefinitionType } from '~/generated-metadata/graphql'; export const useRelationSettingsFormInitialValues = ({ fieldMetadataItem, + objectMetadataItem, }: { fieldMetadataItem?: Pick<FieldMetadataItem, 'type' | 'relationDefinition'>; + objectMetadataItem?: SettingsDataModelFieldPreviewCardProps['objectMetadataItem']; }) => { const { objectMetadataItems } = useFilteredObjectMetadataItems(); @@ -28,11 +30,13 @@ export const useRelationSettingsFormInitialValues = ({ const initialRelationObjectMetadataItem = useMemo( () => relationObjectMetadataItemFromFieldMetadata ?? - objectMetadataItems.find( - ({ nameSingular }) => nameSingular === CoreObjectNameSingular.Person, - ) ?? + objectMetadataItem ?? objectMetadataItems.filter(isObjectMetadataAvailableForRelation)[0], - [objectMetadataItems, relationObjectMetadataItemFromFieldMetadata], + [ + objectMetadataItem, + objectMetadataItems, + relationObjectMetadataItemFromFieldMetadata, + ], ); const initialRelationType = @@ -44,7 +48,12 @@ export const useRelationSettingsFormInitialValues = ({ disableRelationEdition: !!relationFieldMetadataItem, initialRelationFieldMetadataItem: relationFieldMetadataItem ?? { icon: initialRelationObjectMetadataItem.icon ?? 'IconUsers', - label: '', + label: [ + RelationDefinitionType.ManyToMany, + RelationDefinitionType.ManyToOne, + ].includes(initialRelationType) + ? initialRelationObjectMetadataItem.labelPlural + : initialRelationObjectMetadataItem.labelSingular, }, initialRelationObjectMetadataItem, initialRelationType, 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 9d3c97628f2e..f1fdc6425f57 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,6 +1,5 @@ import styled from '@emotion/styled'; import { DropResult } from '@hello-pangea/dnd'; -import { useState } from 'react'; import { Controller, useFormContext } from 'react-hook-form'; import { IconPlus } from 'twenty-ui'; import { z } from 'zod'; @@ -79,7 +78,6 @@ const StyledButton = styled(LightButton)` export const SettingsDataModelFieldSelectForm = ({ fieldMetadataItem, }: SettingsDataModelFieldSelectFormProps) => { - const [focusedOptionId, setFocusedOptionId] = useState(''); const { initialDefaultValue, initialOptions } = useSelectSettingsFormInitialValues({ fieldMetadataItem }); @@ -183,17 +181,13 @@ export const SettingsDataModelFieldSelectForm = ({ const handleAddOption = () => { const newOptions = getOptionsWithNewOption(); - setFormValue('options', newOptions); + setFormValue('options', newOptions, { shouldDirty: true }); }; const handleInputEnter = () => { const newOptions = getOptionsWithNewOption(); - setFormValue('options', newOptions); - - const lastOptionId = newOptions[newOptions.length - 1].id; - - setFocusedOptionId(lastOptionId); + setFormValue('options', newOptions, { shouldDirty: true }); }; return ( @@ -227,7 +221,7 @@ export const SettingsDataModelFieldSelectForm = ({ <SettingsDataModelFieldSelectFormOptionRow key={option.id} option={option} - focused={focusedOptionId === option.id} + isNewRow={index === options.length - 1} onChange={(nextOption) => { const nextOptions = toSpliced( options, 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 121aa15e0e07..d9be3fff7e0e 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 @@ -33,7 +33,7 @@ type SettingsDataModelFieldSelectFormOptionRowProps = { onRemoveAsDefault?: () => void; onInputEnter?: () => void; option: FieldMetadataItemOption; - focused?: boolean; + isNewRow?: boolean; }; const StyledRow = styled.div` @@ -67,7 +67,7 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({ onRemoveAsDefault, onInputEnter, option, - focused, + isNewRow, }: SettingsDataModelFieldSelectFormOptionRowProps) => { const theme = useTheme(); @@ -129,10 +129,11 @@ export const SettingsDataModelFieldSelectFormOptionRow = ({ value: getOptionValueFromLabel(label), }) } - focused={focused} RightIcon={isDefault ? IconCheck : undefined} maxLength={OPTION_VALUE_MAXIMUM_LENGTH} onInputEnter={handleInputEnter} + autoFocusOnMount={isNewRow} + autoSelectOnMount={isNewRow} /> <Dropdown dropdownId={dropdownIds.actions} diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema.ts b/packages/twenty-front/src/modules/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema.ts index 4d7a171f46a4..18ae87b973b0 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema.ts @@ -1,9 +1,8 @@ -import { z } from 'zod'; - import { settingsDataModelFieldDescriptionFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldDescriptionForm'; import { settingsDataModelFieldIconLabelFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm'; import { settingsDataModelFieldSettingsFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard'; -import { settingsDataModelFieldTypeFormSchema } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect'; +import { z } from 'zod'; +import { settingsDataModelFieldTypeFormSchema } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldSelect'; export const settingsFieldFormSchema = (existingOtherLabels?: string[]) => { return z diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/__stories__/SettingsDataModelFieldPreviewCard.stories.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/__stories__/SettingsDataModelFieldPreviewCard.stories.tsx index 3df8c6e7664a..6b0fb477eef2 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/__stories__/SettingsDataModelFieldPreviewCard.stories.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/__stories__/SettingsDataModelFieldPreviewCard.stories.tsx @@ -6,14 +6,23 @@ import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorato import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator'; import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator'; import { graphqlMocks } from '~/testing/graphqlMocks'; -import { - mockedCompanyObjectMetadataItem, - mockedOpportunityObjectMetadataItem, - mockedPersonObjectMetadataItem, -} from '~/testing/mock-data/metadata'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { SettingsDataModelFieldPreviewCard } from '../SettingsDataModelFieldPreviewCard'; +const mockedCompanyObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'company', +); + +const mockedOpportunityObjectMetadataItem = + generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'opportunity', + ); + +const mockedPersonObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', +); + const meta: Meta<typeof SettingsDataModelFieldPreviewCard> = { title: 'Modules/Settings/DataModel/Fields/Preview/SettingsDataModelFieldPreviewCard', @@ -38,7 +47,7 @@ type Story = StoryObj<typeof SettingsDataModelFieldPreviewCard>; export const LabelIdentifier: Story = { args: { - fieldMetadataItem: mockedPersonObjectMetadataItem.fields.find( + fieldMetadataItem: mockedPersonObjectMetadataItem?.fields.find( ({ name, type }) => name === 'name' && type === FieldMetadataType.FullName, ), @@ -47,7 +56,7 @@ export const LabelIdentifier: Story = { export const Text: Story = { args: { - fieldMetadataItem: mockedPersonObjectMetadataItem.fields.find( + fieldMetadataItem: mockedPersonObjectMetadataItem?.fields.find( ({ name, type }) => name === 'city' && type === FieldMetadataType.Text, ), }, @@ -55,7 +64,7 @@ export const Text: Story = { export const Boolean: Story = { args: { - fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find( + fieldMetadataItem: mockedCompanyObjectMetadataItem?.fields.find( ({ name, type }) => name === 'idealCustomerProfile' && type === FieldMetadataType.Boolean, ), @@ -65,7 +74,7 @@ export const Boolean: Story = { export const Currency: Story = { args: { - fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find( + fieldMetadataItem: mockedCompanyObjectMetadataItem?.fields.find( ({ name, type }) => name === 'annualRecurringRevenue' && type === FieldMetadataType.Currency, @@ -76,7 +85,7 @@ export const Currency: Story = { export const Date: Story = { args: { - fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find( + fieldMetadataItem: mockedCompanyObjectMetadataItem?.fields.find( ({ type }) => type === FieldMetadataType.DateTime, ), objectMetadataItem: mockedCompanyObjectMetadataItem, @@ -85,7 +94,7 @@ export const Date: Story = { export const Links: Story = { args: { - fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find( + fieldMetadataItem: mockedCompanyObjectMetadataItem?.fields.find( ({ name, type }) => name === 'linkedinLink' && type === FieldMetadataType.Links, ), @@ -95,7 +104,7 @@ export const Links: Story = { export const Number: Story = { args: { - fieldMetadataItem: mockedCompanyObjectMetadataItem.fields.find( + fieldMetadataItem: mockedCompanyObjectMetadataItem?.fields.find( ({ type }) => type === FieldMetadataType.Number, ), objectMetadataItem: mockedCompanyObjectMetadataItem, @@ -114,7 +123,7 @@ export const Rating: Story = { export const Relation: Story = { args: { - fieldMetadataItem: mockedPersonObjectMetadataItem.fields.find( + fieldMetadataItem: mockedPersonObjectMetadataItem?.fields.find( ({ name }) => name === 'company', ), relationObjectMetadataItem: mockedCompanyObjectMetadataItem, @@ -123,7 +132,7 @@ export const Relation: Story = { export const Select: Story = { args: { - fieldMetadataItem: mockedOpportunityObjectMetadataItem.fields.find( + fieldMetadataItem: mockedOpportunityObjectMetadataItem?.fields.find( ({ name, type }) => name === 'stage' && type === FieldMetadataType.Select, ), objectMetadataItem: mockedOpportunityObjectMetadataItem, diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreviewValue.test.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreviewValue.test.tsx index a3fbadbf1a24..c9c6b0d4367b 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreviewValue.test.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/hooks/__tests__/useFieldPreviewValue.test.tsx @@ -1,35 +1,34 @@ -import { ReactNode } from 'react'; -import { MockedProvider } from '@apollo/client/testing'; import { renderHook } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; -import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider'; import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem'; -import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import { FieldMetadataType } from '~/generated/graphql'; -import { - mockedCompanyObjectMetadataItem, - mockedOpportunityObjectMetadataItem, - mockedPersonObjectMetadataItem, -} from '~/testing/mock-data/metadata'; +import { getJestMetadataAndApolloMocksWrapper } from '~/testing/jest/getJestMetadataAndApolloMocksWrapper'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { useFieldPreviewValue } from '../useFieldPreviewValue'; -const Wrapper = ({ children }: { children: ReactNode }) => ( - <RecoilRoot> - <MockedProvider> - <SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager"> - <ObjectMetadataItemsProvider>{children}</ObjectMetadataItemsProvider> - </SnackBarProviderScope> - </MockedProvider> - </RecoilRoot> +const mockedCompanyObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'company', ); +const mockedOpportunityObjectMetadataItem = + generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'opportunity', + ); + +const mockedPersonObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', +); + +const Wrapper = getJestMetadataAndApolloMocksWrapper({ + apolloMocks: [], +}); + describe('useFieldPreviewValue', () => { it('returns null if skip is true', () => { // Given const fieldName = 'amount'; - const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find( + const fieldMetadataItem = mockedOpportunityObjectMetadataItem?.fields.find( ({ name, type }) => name === fieldName && type === FieldMetadataType.Currency, ); @@ -52,7 +51,7 @@ describe('useFieldPreviewValue', () => { it("returns the field's preview value for a Currency field", () => { // Given const fieldName = 'amount'; - const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find( + const fieldMetadataItem = mockedOpportunityObjectMetadataItem?.fields.find( ({ name, type }) => name === fieldName && type === FieldMetadataType.Currency, ); @@ -106,7 +105,7 @@ describe('useFieldPreviewValue', () => { it("returns the field's preview value for a Select field", () => { // Given const fieldName = 'stage'; - const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find( + const fieldMetadataItem = mockedOpportunityObjectMetadataItem?.fields.find( ({ name, type }) => name === fieldName && type === FieldMetadataType.Select, ); @@ -169,7 +168,7 @@ describe('useFieldPreviewValue', () => { it("returns the field's preview value for other field types", () => { // Given const fieldName = 'employees'; - const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( + const fieldMetadataItem = mockedCompanyObjectMetadataItem?.fields.find( ({ name }) => name === fieldName, ); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getCurrencyFieldPreviewValue.test.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getCurrencyFieldPreviewValue.test.ts index a8d9b9a4af7e..8eeda74fbb79 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getCurrencyFieldPreviewValue.test.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getCurrencyFieldPreviewValue.test.ts @@ -1,16 +1,22 @@ import { CurrencyCode } from '@/object-record/record-field/types/CurrencyCode'; import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { - mockedCompanyObjectMetadataItem, - mockedOpportunityObjectMetadataItem, -} from '~/testing/mock-data/metadata'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { getCurrencyFieldPreviewValue } from '../getCurrencyFieldPreviewValue'; +const mockedCompanyObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'company', +); + +const mockedOpportunityObjectMetadataItem = + generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'opportunity', + ); + describe('getCurrencyFieldPreviewValue', () => { it('returns null if the field is not a Currency field', () => { // Given - const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( + const fieldMetadataItem = mockedCompanyObjectMetadataItem?.fields.find( ({ type }) => type !== FieldMetadataType.Currency, ); @@ -26,7 +32,7 @@ describe('getCurrencyFieldPreviewValue', () => { }); const fieldName = 'amount'; - const fieldMetadataItem = mockedOpportunityObjectMetadataItem.fields.find( + const fieldMetadataItem = mockedOpportunityObjectMetadataItem?.fields.find( ({ name, type }) => name === fieldName && type === FieldMetadataType.Currency, ); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts index 7acf2b0cfe4c..c3a649dbad77 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getFieldPreviewValue.test.ts @@ -1,17 +1,21 @@ import { getFieldPreviewValue } from '@/settings/data-model/fields/preview/utils/getFieldPreviewValue'; import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig'; import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { - mockedCompanyObjectMetadataItem, - mockedCustomObjectMetadataItem, - mockedPersonObjectMetadataItem, -} from '~/testing/mock-data/metadata'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; + +const mockedCompanyObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'company', +); + +const mockedPersonObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', +); describe('getFieldPreviewValue', () => { it("returns the field's defaultValue from metadata if it exists", () => { // Given const fieldName = 'idealCustomerProfile'; - const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( + const fieldMetadataItem = mockedCompanyObjectMetadataItem?.fields.find( ({ name }) => name === fieldName, ); @@ -29,7 +33,7 @@ describe('getFieldPreviewValue', () => { it('returns a placeholder defaultValue if the field metadata does not have a defaultValue', () => { // Given const fieldName = 'employees'; - const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( + const fieldMetadataItem = mockedCompanyObjectMetadataItem?.fields.find( ({ name }) => name === fieldName, ); @@ -50,25 +54,7 @@ describe('getFieldPreviewValue', () => { it('returns null if the field is supported in Settings but has no pre-configured placeholder defaultValue', () => { // Given const fieldName = 'company'; - const fieldMetadataItem = mockedPersonObjectMetadataItem.fields.find( - ({ name }) => name === fieldName, - ); - - if (!fieldMetadataItem) { - throw new Error(`Field '${fieldName}' not found`); - } - - // When - const result = getFieldPreviewValue({ fieldMetadataItem }); - - // Then - expect(result).toBeNull(); - }); - - it('returns null if the field is not supported in Settings', () => { - // Given - const fieldName = 'position'; - const fieldMetadataItem = mockedCustomObjectMetadataItem.fields.find( + const fieldMetadataItem = mockedPersonObjectMetadataItem?.fields.find( ({ name }) => name === fieldName, ); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getMultiSelectFieldPreviewValue.test.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getMultiSelectFieldPreviewValue.test.ts index 824d6e820167..2737a829ee91 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getMultiSelectFieldPreviewValue.test.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getMultiSelectFieldPreviewValue.test.ts @@ -1,15 +1,20 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { - mockedCompanyObjectMetadataItem, - mockedCustomObjectMetadataItem, -} from '~/testing/mock-data/metadata'; - +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { getMultiSelectFieldPreviewValue } from '../getMultiSelectFieldPreviewValue'; +const mockedCompanyObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'company', +); + +const mockedOpportunityObjectMetadataItem = + generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'opportunity', + ); + describe('getMultiSelectFieldPreviewValue', () => { it('returns null if the field is not a Multi-Select field', () => { // Given - const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( + const fieldMetadataItem = mockedCompanyObjectMetadataItem?.fields.find( ({ type }) => type !== FieldMetadataType.MultiSelect, ); @@ -24,10 +29,11 @@ describe('getMultiSelectFieldPreviewValue', () => { expect(previewValue).toBeNull(); }); - const fieldName = 'priority'; - const selectFieldMetadataItem = mockedCustomObjectMetadataItem.fields.find( - ({ name, type }) => name === fieldName && type === FieldMetadataType.Select, - ); + const fieldName = 'stage'; + const selectFieldMetadataItem = + mockedOpportunityObjectMetadataItem?.fields.find( + ({ name }) => name === fieldName, + ); if (!selectFieldMetadataItem) { throw new Error(`Field '${fieldName}' not found`); @@ -52,7 +58,13 @@ describe('getMultiSelectFieldPreviewValue', () => { }); // Then - expect(previewValue).toEqual(['MEDIUM', 'LOW']); + expect(previewValue).toEqual([ + 'NEW', + 'SCREENING', + 'MEETING', + 'PROPOSAL', + 'CUSTOMER', + ]); }); it("returns all option values if no defaultValue was found in the field's metadata", () => { @@ -69,7 +81,13 @@ describe('getMultiSelectFieldPreviewValue', () => { }); // Then - expect(previewValue).toEqual(['LOW', 'MEDIUM', 'HIGH']); + expect(previewValue).toEqual([ + 'NEW', + 'SCREENING', + 'MEETING', + 'PROPOSAL', + 'CUSTOMER', + ]); expect(previewValue).toEqual( fieldMetadataItemWithDefaultValue.options?.map(({ value }) => value), ); @@ -89,7 +107,13 @@ describe('getMultiSelectFieldPreviewValue', () => { }); // Then - expect(previewValue).toEqual(['LOW', 'MEDIUM', 'HIGH']); + expect(previewValue).toEqual([ + 'NEW', + 'SCREENING', + 'MEETING', + 'PROPOSAL', + 'CUSTOMER', + ]); expect(previewValue).toEqual( fieldMetadataItemWithDefaultValue.options?.map(({ value }) => value), ); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getSelectFieldPreviewValue.test.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getSelectFieldPreviewValue.test.ts index 3572d58d9007..109feefb3ab1 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getSelectFieldPreviewValue.test.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/__tests__/getSelectFieldPreviewValue.test.ts @@ -1,15 +1,21 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { - mockedCompanyObjectMetadataItem, - mockedCustomObjectMetadataItem, -} from '~/testing/mock-data/metadata'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { getSelectFieldPreviewValue } from '../getSelectFieldPreviewValue'; +const mockedCompanyObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'company', +); + +const mockedOpportunityObjectMetadataItem = + generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'opportunity', + ); + describe('getSelectFieldPreviewValue', () => { it('returns null if the field is not a Select field', () => { // Given - const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( + const fieldMetadataItem = mockedCompanyObjectMetadataItem?.fields.find( ({ type }) => type !== FieldMetadataType.Select, ); @@ -24,9 +30,9 @@ describe('getSelectFieldPreviewValue', () => { expect(previewValue).toBeNull(); }); - const fieldName = 'priority'; - const fieldMetadataItem = mockedCustomObjectMetadataItem.fields.find( - ({ name, type }) => name === fieldName && type === FieldMetadataType.Select, + const fieldName = 'stage'; + const fieldMetadataItem = mockedOpportunityObjectMetadataItem?.fields.find( + ({ name }) => name === fieldName, ); if (!fieldMetadataItem) { @@ -35,7 +41,7 @@ describe('getSelectFieldPreviewValue', () => { it("returns the defaultValue as an option value if a valid defaultValue is found in the field's metadata", () => { // Given - const defaultValue = "'MEDIUM'"; + const defaultValue = "'NEW'"; const fieldMetadataItemWithDefaultValue = { ...fieldMetadataItem, defaultValue, @@ -47,7 +53,7 @@ describe('getSelectFieldPreviewValue', () => { }); // Then - expect(previewValue).toBe('MEDIUM'); + expect(previewValue).toBe('NEW'); }); it("returns the first option value if no defaultValue was found in the field's metadata", () => { @@ -64,7 +70,7 @@ describe('getSelectFieldPreviewValue', () => { }); // Then - expect(previewValue).toBe('LOW'); + expect(previewValue).toBe('NEW'); expect(previewValue).toBe( fieldMetadataItemWithDefaultValue.options?.[0]?.value, ); @@ -84,7 +90,7 @@ describe('getSelectFieldPreviewValue', () => { }); // Then - expect(previewValue).toBe('LOW'); + expect(previewValue).toBe('NEW'); expect(previewValue).toBe( fieldMetadataItemWithDefaultValue.options?.[0]?.value, ); diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getCurrencyFieldPreviewValue.ts b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getCurrencyFieldPreviewValue.ts index fc139304bd81..2fd5b32eae40 100644 --- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getCurrencyFieldPreviewValue.ts +++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/utils/getCurrencyFieldPreviewValue.ts @@ -16,9 +16,11 @@ export const getCurrencyFieldPreviewValue = ({ }): FieldCurrencyValue | null => { if (fieldMetadataItem.type !== FieldMetadataType.Currency) return null; - const placeholderDefaultValue = getSettingsFieldTypeConfig( + const currencyFieldTypeConfig = getSettingsFieldTypeConfig( FieldMetadataType.Currency, - ).exampleValue; + ); + + const placeholderDefaultValue = currencyFieldTypeConfig.exampleValue; return currencyFieldDefaultValueSchema .transform((value) => ({ diff --git a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDataType.tsx b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDataType.tsx index 4fe7cccf71ff..188e5b97211c 100644 --- a/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDataType.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/object-details/components/SettingsObjectFieldDataType.tsx @@ -3,7 +3,7 @@ import styled from '@emotion/styled'; import { Link } from 'react-router-dom'; import { IconComponent, IconTwentyStar } from 'twenty-ui'; -import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; +import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig'; import { FieldMetadataType } from '~/generated-metadata/graphql'; @@ -11,11 +11,11 @@ type SettingsObjectFieldDataTypeProps = { to?: string; Icon?: IconComponent; label?: string; - value: SettingsSupportedFieldType; + value: SettingsFieldType; }; const StyledDataType = styled.div<{ - value: SettingsSupportedFieldType; + value: SettingsFieldType; to?: string; }>` align-items: center; diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/__stories__/SettingsDataModelObjectAboutForm.stories.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/__stories__/SettingsDataModelObjectAboutForm.stories.tsx index 4345f59229e9..173106174fdf 100644 --- a/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/__stories__/SettingsDataModelObjectAboutForm.stories.tsx +++ b/packages/twenty-front/src/modules/settings/data-model/objects/forms/components/__stories__/SettingsDataModelObjectAboutForm.stories.tsx @@ -4,9 +4,12 @@ import { ComponentDecorator } from 'twenty-ui'; import { FormProviderDecorator } from '~/testing/decorators/FormProviderDecorator'; import { IconsProviderDecorator } from '~/testing/decorators/IconsProviderDecorator'; -import { mockedCompanyObjectMetadataItem } from '~/testing/mock-data/metadata'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { SettingsDataModelObjectAboutForm } from '../SettingsDataModelObjectAboutForm'; +const mockedCompanyObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'company', +); const StyledContainer = styled.div` flex: 1; diff --git a/packages/twenty-front/src/modules/settings/data-model/types/CompositeFieldType.ts b/packages/twenty-front/src/modules/settings/data-model/types/CompositeFieldType.ts new file mode 100644 index 000000000000..f1186c6d1047 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/types/CompositeFieldType.ts @@ -0,0 +1,21 @@ +import { FieldType } from '@/settings/data-model/types/FieldType'; +import { PickLiteral } from '~/types/PickLiteral'; + +// TODO: add to future fullstack shared package +export const COMPOSITE_FIELD_TYPES = [ + 'CURRENCY', + 'EMAILS', + 'LINK', + 'LINKS', + 'ADDRESS', + 'PHONES', + 'FULL_NAME', + 'ACTOR', +] as const; + +type CompositeFieldTypeBaseLiteral = (typeof COMPOSITE_FIELD_TYPES)[number]; + +export type CompositeFieldType = PickLiteral< + FieldType, + CompositeFieldTypeBaseLiteral +>; diff --git a/packages/twenty-front/src/modules/settings/data-model/types/FieldType.ts b/packages/twenty-front/src/modules/settings/data-model/types/FieldType.ts new file mode 100644 index 000000000000..66a579162c3c --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/types/FieldType.ts @@ -0,0 +1,3 @@ +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +export type FieldType = `${FieldMetadataType}`; diff --git a/packages/twenty-front/src/modules/settings/data-model/types/NonCompositeFieldType.ts b/packages/twenty-front/src/modules/settings/data-model/types/NonCompositeFieldType.ts new file mode 100644 index 000000000000..fdf439ed0e90 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/types/NonCompositeFieldType.ts @@ -0,0 +1,8 @@ +import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType'; +import { FieldType } from '@/settings/data-model/types/FieldType'; +import { ExcludeLiteral } from '~/types/ExcludeLiteral'; + +export type NonCompositeFieldType = ExcludeLiteral< + FieldType, + CompositeFieldType +>; diff --git a/packages/twenty-front/src/modules/settings/data-model/types/SettingsCompositeFieldType.ts b/packages/twenty-front/src/modules/settings/data-model/types/SettingsCompositeFieldType.ts new file mode 100644 index 000000000000..87b96acaaba0 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/types/SettingsCompositeFieldType.ts @@ -0,0 +1,8 @@ +import { CompositeFieldType } from '@/settings/data-model/types/CompositeFieldType'; +import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; +import { PickLiteral } from '~/types/PickLiteral'; + +export type SettingsCompositeFieldType = PickLiteral< + SettingsFieldType, + CompositeFieldType +>; diff --git a/packages/twenty-front/src/modules/settings/data-model/types/SettingsExcludedFieldType.ts b/packages/twenty-front/src/modules/settings/data-model/types/SettingsExcludedFieldType.ts new file mode 100644 index 000000000000..3c7041e9f233 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/types/SettingsExcludedFieldType.ts @@ -0,0 +1,7 @@ +import { FieldType } from '@/settings/data-model/types/FieldType'; +import { PickLiteral } from '~/types/PickLiteral'; + +export type SettingsExcludedFieldType = PickLiteral< + FieldType, + 'POSITION' | 'TS_VECTOR' +>; diff --git a/packages/twenty-front/src/modules/settings/data-model/types/SettingsFieldType.ts b/packages/twenty-front/src/modules/settings/data-model/types/SettingsFieldType.ts new file mode 100644 index 000000000000..98f2a491a6b5 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/types/SettingsFieldType.ts @@ -0,0 +1,8 @@ +import { FieldType } from '@/settings/data-model/types/FieldType'; +import { SettingsExcludedFieldType } from '@/settings/data-model/types/SettingsExcludedFieldType'; +import { ExcludeLiteral } from '~/types/ExcludeLiteral'; + +export type SettingsFieldType = ExcludeLiteral< + FieldType, + SettingsExcludedFieldType +>; diff --git a/packages/twenty-front/src/modules/settings/data-model/types/SettingsNonCompositeFieldType.ts b/packages/twenty-front/src/modules/settings/data-model/types/SettingsNonCompositeFieldType.ts new file mode 100644 index 000000000000..73aabf77df52 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/data-model/types/SettingsNonCompositeFieldType.ts @@ -0,0 +1,8 @@ +import { NonCompositeFieldType } from '@/settings/data-model/types/NonCompositeFieldType'; +import { SettingsExcludedFieldType } from '@/settings/data-model/types/SettingsExcludedFieldType'; +import { ExcludeLiteral } from '~/types/ExcludeLiteral'; + +export type SettingsNonCompositeFieldType = ExcludeLiteral< + NonCompositeFieldType, + SettingsExcludedFieldType +>; diff --git a/packages/twenty-front/src/modules/settings/data-model/types/SettingsSupportedFieldType.ts b/packages/twenty-front/src/modules/settings/data-model/types/SettingsSupportedFieldType.ts deleted file mode 100644 index 0149601685e3..000000000000 --- a/packages/twenty-front/src/modules/settings/data-model/types/SettingsSupportedFieldType.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { FieldMetadataType } from '~/generated-metadata/graphql'; - -export type SettingsSupportedFieldType = Exclude< - FieldMetadataType, - FieldMetadataType.Position ->; diff --git a/packages/twenty-front/src/modules/settings/data-model/utils/__tests__/getFieldPreviewValueFromRecord.test.ts b/packages/twenty-front/src/modules/settings/data-model/utils/__tests__/getFieldPreviewValueFromRecord.test.ts index 95b0aac275e1..750604ef1515 100644 --- a/packages/twenty-front/src/modules/settings/data-model/utils/__tests__/getFieldPreviewValueFromRecord.test.ts +++ b/packages/twenty-front/src/modules/settings/data-model/utils/__tests__/getFieldPreviewValueFromRecord.test.ts @@ -1,10 +1,14 @@ import { ObjectRecord } from '@/object-record/types/ObjectRecord'; -import { - mockedCompanyObjectMetadataItem, - mockedPersonObjectMetadataItem, -} from '~/testing/mock-data/metadata'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { getFieldPreviewValueFromRecord } from '../getFieldPreviewValueFromRecord'; +const mockedCompanyObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'company', +); + +const mockedPersonObjectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', +); describe('getFieldPreviewValueFromRecord', () => { describe('RELATION field', () => { @@ -21,9 +25,13 @@ describe('getFieldPreviewValueFromRecord', () => { }, __typename: 'Opportunity', }; - const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( + const fieldMetadataItem = mockedCompanyObjectMetadataItem?.fields.find( ({ name }) => name === 'people', - )!; + ); + + if (!fieldMetadataItem) { + throw new Error('Field not found'); + } // When const result = getFieldPreviewValueFromRecord({ @@ -43,9 +51,13 @@ describe('getFieldPreviewValueFromRecord', () => { company: relationRecord, __typename: 'Opportunity', }; - const fieldMetadataItem = mockedPersonObjectMetadataItem.fields.find( + const fieldMetadataItem = mockedPersonObjectMetadataItem?.fields.find( ({ name }) => name === 'company', - )!; + ); + + if (!fieldMetadataItem) { + throw new Error('Field not found'); + } // When const result = getFieldPreviewValueFromRecord({ @@ -62,9 +74,13 @@ describe('getFieldPreviewValueFromRecord', () => { it('returns the record field value', () => { // Given const record = { id: '', name: 'Twenty', __typename: 'Opportunity' }; - const fieldMetadataItem = mockedCompanyObjectMetadataItem.fields.find( + const fieldMetadataItem = mockedCompanyObjectMetadataItem?.fields.find( ({ name }) => name === 'name', - )!; + ); + + if (!fieldMetadataItem) { + throw new Error('Field not found'); + } // When const result = getFieldPreviewValueFromRecord({ diff --git a/packages/twenty-front/src/modules/settings/data-model/utils/getSettingsFieldTypeConfig.ts b/packages/twenty-front/src/modules/settings/data-model/utils/getSettingsFieldTypeConfig.ts index 3278a1dee42a..307ff6a3f6f6 100644 --- a/packages/twenty-front/src/modules/settings/data-model/utils/getSettingsFieldTypeConfig.ts +++ b/packages/twenty-front/src/modules/settings/data-model/utils/getSettingsFieldTypeConfig.ts @@ -1,13 +1,6 @@ import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs'; -import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; -import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; -export const getSettingsFieldTypeConfig = <T extends FieldMetadataType>( - fieldType: T, -) => - (isFieldTypeSupportedInSettings(fieldType) - ? SETTINGS_FIELD_TYPE_CONFIGS[fieldType] - : undefined) as T extends SettingsSupportedFieldType - ? (typeof SETTINGS_FIELD_TYPE_CONFIGS)[T] - : undefined; +export const getSettingsFieldTypeConfig = (fieldType: SettingsFieldType) => { + return SETTINGS_FIELD_TYPE_CONFIGS[fieldType as SettingsFieldType]; +}; diff --git a/packages/twenty-front/src/modules/settings/data-model/utils/isFieldTypeSupportedInSettings.ts b/packages/twenty-front/src/modules/settings/data-model/utils/isFieldTypeSupportedInSettings.ts index 4d9a377b704a..52171d87e40a 100644 --- a/packages/twenty-front/src/modules/settings/data-model/utils/isFieldTypeSupportedInSettings.ts +++ b/packages/twenty-front/src/modules/settings/data-model/utils/isFieldTypeSupportedInSettings.ts @@ -1,8 +1,7 @@ import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs'; -import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; -import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { FieldType } from '@/settings/data-model/types/FieldType'; +import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; export const isFieldTypeSupportedInSettings = ( - fieldType: FieldMetadataType, -): fieldType is SettingsSupportedFieldType => - fieldType in SETTINGS_FIELD_TYPE_CONFIGS; + fieldType: FieldType, +): fieldType is SettingsFieldType => fieldType in SETTINGS_FIELD_TYPE_CONFIGS; diff --git a/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysFieldItemTableRow.tsx b/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysFieldItemTableRow.tsx index 4d4dc8d45e6f..d5868b1665d4 100644 --- a/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysFieldItemTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysFieldItemTableRow.tsx @@ -1,13 +1,17 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconChevronRight } from 'twenty-ui'; +import { IconChevronRight, MOBILE_VIEWPORT } from 'twenty-ui'; import { ApiFieldItem } from '@/settings/developers/types/api-key/ApiFieldItem'; import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableRow } from '@/ui/layout/table/components/TableRow'; export const StyledApisFieldTableRow = styled(TableRow)` - grid-template-columns: 312px 132px 68px; + grid-template-columns: 312px auto 28px; + @media (max-width: ${MOBILE_VIEWPORT}px) { + width: 100%; + grid-template-columns: 12fr 4fr; + } `; const StyledNameTableCell = styled(TableCell)` @@ -18,6 +22,7 @@ const StyledNameTableCell = styled(TableCell)` const StyledIconTableCell = styled(TableCell)` justify-content: center; padding-right: ${({ theme }) => theme.spacing(1)}; + padding-left: 0; `; const StyledIconChevronRight = styled(IconChevronRight)` 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 ede12c34bf6c..0d1a9fc12660 100644 --- a/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysTable.tsx +++ b/packages/twenty-front/src/modules/settings/developers/components/SettingsApiKeysTable.tsx @@ -1,5 +1,3 @@ -import styled from '@emotion/styled'; - import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { SettingsApiKeysFieldItemTableRow } from '@/settings/developers/components/SettingsApiKeysFieldItemTableRow'; @@ -10,15 +8,25 @@ 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 { MOBILE_VIEWPORT } from 'twenty-ui'; const StyledTableBody = styled(TableBody)` border-bottom: 1px solid ${({ theme }) => theme.border.color.light}; - max-height: 260px; - overflow-y: auto; + @media (max-width: ${MOBILE_VIEWPORT}px) { + padding-top: ${({ theme }) => theme.spacing(3)}; + display: flex; + justify-content: space-between; + scroll-behavior: smooth; + } `; const StyledTableRow = styled(TableRow)` - grid-template-columns: 312px 132px 68px; + grid-template-columns: 312px auto 28px; + @media (max-width: ${MOBILE_VIEWPORT}px) { + width: 95%; + grid-template-columns: 20fr 2fr; + } `; export const SettingsApiKeysTable = () => { diff --git a/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookTableRow.tsx b/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookTableRow.tsx index 0c093d5ecbd2..d559f89a200a 100644 --- a/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookTableRow.tsx +++ b/packages/twenty-front/src/modules/settings/developers/components/SettingsDevelopersWebhookTableRow.tsx @@ -1,4 +1,3 @@ -import React from 'react'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { IconChevronRight } from 'twenty-ui'; @@ -8,12 +7,13 @@ import { TableCell } from '@/ui/layout/table/components/TableCell'; import { TableRow } from '@/ui/layout/table/components/TableRow'; export const StyledApisFieldTableRow = styled(TableRow)` - grid-template-columns: 444px 68px; + grid-template-columns: 1fr 28px; `; const StyledIconTableCell = styled(TableCell)` justify-content: center; padding-right: ${({ theme }) => theme.spacing(1)}; + padding-left: 0; `; const StyledUrlTableCell = styled(TableCell)` 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 new file mode 100644 index 000000000000..eb2e359fff16 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx @@ -0,0 +1,59 @@ +import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState'; +import styled from '@emotion/styled'; +import { ResponsiveLine } from '@nivo/line'; +import { Section } from '@react-email/components'; +import { useRecoilValue } from 'recoil'; +import { H2Title } from 'twenty-ui'; + +export type NivoLineInput = { + id: string | number; + color?: string; + data: Array<{ + x: number | string | Date; + y: number | string | Date; + }>; +}; +const StyledGraphContainer = styled.div` + height: 200px; + width: 100%; +`; +export const SettingsDeveloppersWebhookUsageGraph = () => { + const webhookGraphData = useRecoilValue(webhookGraphDataState); + + return ( + <> + {webhookGraphData.length ? ( + <Section> + <H2Title title="Statistics" /> + <StyledGraphContainer> + <ResponsiveLine + data={webhookGraphData} + colors={(d) => d.color} + margin={{ top: 0, right: 0, bottom: 50, left: 60 }} + xFormat="time:%Y-%m-%d %H:%M%" + xScale={{ + type: 'time', + useUTC: false, + format: '%Y-%m-%d %H:%M:%S', + precision: 'hour', + }} + yScale={{ + type: 'linear', + }} + axisBottom={{ + tickValues: 'every day', + format: '%b %d', + }} + enableTouchCrosshair={true} + enableGridY={false} + enableGridX={false} + enablePoints={false} + /> + </StyledGraphContainer> + </Section> + ) : ( + <></> + )} + </> + ); +}; diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect.tsx new file mode 100644 index 000000000000..0c26350243b2 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect.tsx @@ -0,0 +1,101 @@ +import { NivoLineInput } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph'; +import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState'; +import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; +import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; +import { useEffect } from 'react'; +import { useSetRecoilState } from 'recoil'; + +type SettingsDevelopersWebhookUsageGraphEffectProps = { + webhookId: string; +}; + +export const SettingsDevelopersWebhookUsageGraphEffect = ({ + webhookId, +}: SettingsDevelopersWebhookUsageGraphEffectProps) => { + const setWebhookGraphData = useSetRecoilState(webhookGraphDataState); + + const { enqueueSnackBar } = useSnackBar(); + + useEffect(() => { + const fetchData = async () => { + try { + const queryString = new URLSearchParams({ + webhookIdRequest: webhookId, + }).toString(); + const token = 'REPLACE_ME'; + const response = await fetch( + `https://api.eu-central-1.aws.tinybird.co/v0/pipes/getWebhooksAnalytics.json?${queryString}`, + { + headers: { + Authorization: 'Bearer ' + token, + }, + }, + ); + const result = await response.json(); + + if (!response.ok) { + enqueueSnackBar('Something went wrong while fetching webhook usage', { + variant: SnackBarVariant.Error, + }); + return; + } + + const graphInput = result.data + .flatMap( + (dataRow: { + start_interval: string; + failure_count: number; + success_count: number; + }) => [ + { + x: dataRow.start_interval, + y: dataRow.failure_count, + id: 'failure_count', + color: 'red', + }, + { + x: dataRow.start_interval, + y: dataRow.success_count, + id: 'success_count', + color: 'green', + }, + ], + ) + .reduce( + ( + acc: NivoLineInput[], + { + id, + x, + y, + color, + }: { id: string; x: string; y: number; color: string }, + ) => { + const existingGroupIndex = acc.findIndex( + (group) => group.id === id, + ); + const isExistingGroup = existingGroupIndex !== -1; + + if (isExistingGroup) { + return acc.map((group, index) => + index === existingGroupIndex + ? { ...group, data: [...group.data, { x, y }] } + : group, + ); + } else { + return [...acc, { id, color, data: [{ x, y }] }]; + } + }, + [], + ); + setWebhookGraphData(graphInput); + } catch (error) { + enqueueSnackBar('Something went wrong while fetching webhook usage', { + variant: SnackBarVariant.Error, + }); + } + }; + fetchData(); + }, [enqueueSnackBar, setWebhookGraphData, webhookId]); + return <></>; +}; diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/states/webhookGraphDataState.ts b/packages/twenty-front/src/modules/settings/developers/webhook/states/webhookGraphDataState.ts new file mode 100644 index 000000000000..bb91864e2782 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/developers/webhook/states/webhookGraphDataState.ts @@ -0,0 +1,7 @@ +import { NivoLineInput } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph'; +import { createState } from 'twenty-ui'; + +export const webhookGraphDataState = createState<NivoLineInput[]>({ + key: 'webhookGraphData', + defaultValue: [], +}); diff --git a/packages/twenty-front/src/modules/settings/hooks/useExpandedHeightAnimation.tsx b/packages/twenty-front/src/modules/settings/hooks/useExpandedHeightAnimation.tsx new file mode 100644 index 000000000000..e762990f0612 --- /dev/null +++ b/packages/twenty-front/src/modules/settings/hooks/useExpandedHeightAnimation.tsx @@ -0,0 +1,52 @@ +import { useEffect, useRef, useState } from 'react'; +import { isDefined } from 'twenty-ui'; + +const transitionValues = { + transition: { + opactity: { duration: 0.2 }, + height: { duration: 0.4 }, + }, +}; + +const commonStyles = { + opacity: 0, + height: 0, + ...transitionValues, +}; + +const advancedSectionAnimationConfig = ( + isExpanded: boolean, + measuredHeight: number, +) => ({ + initial: { + ...commonStyles, + }, + animate: { + opacity: 1, + height: isExpanded ? measuredHeight : 0, + ...transitionValues, + }, + exit: { + ...commonStyles, + }, +}); + +export const useExpandedHeightAnimation = (isExpanded: boolean) => { + const contentRef = useRef<HTMLDivElement>(null); + const [measuredHeight, setMeasuredHeight] = useState(0); + + useEffect(() => { + if (isDefined(contentRef.current)) { + setMeasuredHeight(contentRef.current.scrollHeight); + } + }, [isExpanded]); + + return { + contentRef, + measuredHeight, + motionAnimationVariants: advancedSectionAnimationConfig( + isExpanded, + measuredHeight, + ), + }; +}; diff --git a/packages/twenty-front/src/modules/settings/profile/components/ProfilePictureUploader.tsx b/packages/twenty-front/src/modules/settings/profile/components/ProfilePictureUploader.tsx index dcfcfaf5e7e1..6d9bf03e00e2 100644 --- a/packages/twenty-front/src/modules/settings/profile/components/ProfilePictureUploader.tsx +++ b/packages/twenty-front/src/modules/settings/profile/components/ProfilePictureUploader.tsx @@ -71,7 +71,7 @@ export const ProfilePictureUploader = () => { return result; } catch (error) { - setErrorMessage('An error occured while uploading the picture.'); + setErrorMessage('An error occurred while uploading the picture.'); } }; @@ -97,7 +97,7 @@ export const ProfilePictureUploader = () => { setCurrentWorkspaceMember({ ...currentWorkspaceMember, avatarUrl: null }); } catch (error) { - setErrorMessage('An error occured while removing the picture.'); + setErrorMessage('An error occurred while removing the picture.'); } }; diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionNewForm.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionNewForm.tsx index b9ba77a74ce8..46803ca08534 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionNewForm.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionNewForm.tsx @@ -1,9 +1,9 @@ -import { H2Title } from 'twenty-ui'; -import { Section } from '@/ui/layout/section/components/Section'; -import { TextInput } from '@/ui/input/components/TextInput'; +import { ServerlessFunctionNewFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState'; import { TextArea } from '@/ui/input/components/TextArea'; +import { TextInput } from '@/ui/input/components/TextInput'; +import { Section } from '@/ui/layout/section/components/Section'; import styled from '@emotion/styled'; -import { ServerlessFunctionNewFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState'; +import { H2Title } from 'twenty-ui'; const StyledInputsContainer = styled.div` display: flex; @@ -25,7 +25,7 @@ export const SettingsServerlessFunctionNewForm = ({ <TextInput placeholder="Name" fullWidth - focused + autoFocusOnMount value={formValues.name} onChange={onChange('name')} /> diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTable.tsx b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTable.tsx index d34186b3f738..286a90faadca 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTable.tsx +++ b/packages/twenty-front/src/modules/settings/serverless-functions/components/SettingsServerlessFunctionsTable.tsx @@ -1,7 +1,7 @@ +import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsServerlessFunctionsFieldItemTableRow } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsFieldItemTableRow'; import { SettingsServerlessFunctionsTableEmpty } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsTableEmpty'; import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions'; -import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { Table } from '@/ui/layout/table/components/Table'; @@ -10,7 +10,6 @@ import { TableHeader } from '@/ui/layout/table/components/TableHeader'; import { TableRow } from '@/ui/layout/table/components/TableRow'; import styled from '@emotion/styled'; import { ServerlessFunction } from '~/generated-metadata/graphql'; -import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount'; const StyledTableRow = styled(TableRow)` grid-template-columns: 312px 132px 68px; @@ -23,33 +22,31 @@ const StyledTableBody = styled(TableBody)` export const SettingsServerlessFunctionsTable = () => { const { serverlessFunctions } = useGetManyServerlessFunctions(); - useHotkeyScopeOnMount( - SettingsServerlessFunctionHotkeyScope.ServerlessFunction, - ); - return ( <> {serverlessFunctions.length ? ( - <Table> - <StyledTableRow> - <TableHeader>Name</TableHeader> - <TableHeader>Runtime</TableHeader> - <TableHeader></TableHeader> - </StyledTableRow> - <StyledTableBody> - {serverlessFunctions.map( - (serverlessFunction: ServerlessFunction) => ( - <SettingsServerlessFunctionsFieldItemTableRow - key={serverlessFunction.id} - serverlessFunction={serverlessFunction} - to={getSettingsPagePath(SettingsPath.ServerlessFunctions, { - id: serverlessFunction.id, - })} - /> - ), - )} - </StyledTableBody> - </Table> + <SettingsPageContainer> + <Table> + <StyledTableRow> + <TableHeader>Name</TableHeader> + <TableHeader>Runtime</TableHeader> + <TableHeader></TableHeader> + </StyledTableRow> + <StyledTableBody> + {serverlessFunctions.map( + (serverlessFunction: ServerlessFunction) => ( + <SettingsServerlessFunctionsFieldItemTableRow + key={serverlessFunction.id} + serverlessFunction={serverlessFunction} + to={getSettingsPagePath(SettingsPath.ServerlessFunctions, { + id: serverlessFunction.id, + })} + /> + ), + )} + </StyledTableBody> + </Table> + </SettingsPageContainer> ) : ( <SettingsServerlessFunctionsTableEmpty /> )} 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 6329eb7ac538..5f8886871359 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,9 +1,8 @@ -import { ServerlessFunctionFormValues } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState'; 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 { 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'; @@ -13,13 +12,16 @@ import { useNavigate } from 'react-router-dom'; 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; `; export const SettingsServerlessFunctionCodeEditorTab = ({ - formValues, + files, handleExecute, handlePublish, handleReset, @@ -28,15 +30,19 @@ export const SettingsServerlessFunctionCodeEditorTab = ({ onChange, setIsCodeValid, }: { - formValues: ServerlessFunctionFormValues; + files: File[]; handleExecute: () => void; handlePublish: () => void; handleReset: () => void; resetDisabled: boolean; publishDisabled: boolean; - onChange: (key: string) => (value: string) => void; + onChange: (filePath: string, value: string) => void; setIsCodeValid: (isCodeValid: boolean) => void; }) => { + const { activeTabIdState } = useTabList( + SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID, + ); + const activeTabId = useRecoilValue(activeTabIdState); const TestButton = ( <Button title="Test" @@ -68,21 +74,15 @@ export const SettingsServerlessFunctionCodeEditorTab = ({ /> ); - const TAB_LIST_COMPONENT_ID = 'serverless-function-editor'; - const HeaderTabList = ( <StyledTabList - tabListId={TAB_LIST_COMPONENT_ID} - tabs={[{ id: 'index.ts', title: 'index.ts' }]} + tabListId={SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID} + tabs={files.map((file) => { + return { id: file.path, title: file.path.split('/').at(-1) || '' }; + })} /> ); - const Header = ( - <CoreEditorHeader - leftNodes={[HeaderTabList]} - rightNodes={[ResetButton, PublishButton, TestButton]} - /> - ); const navigate = useNavigate(); useHotkeyScopeOnMount( SettingsServerlessFunctionHotkeyScope.ServerlessFunctionEditorTab, @@ -95,18 +95,25 @@ export const SettingsServerlessFunctionCodeEditorTab = ({ }, SettingsServerlessFunctionHotkeyScope.ServerlessFunctionEditorTab, ); + return ( <Section> <H2Title title="Code your function" description="Write your function (in typescript) below" /> - <CodeEditor - value={formValues.code} - onChange={onChange('code')} - setIsCodeValid={setIsCodeValid} - header={Header} + <CoreEditorHeader + leftNodes={[HeaderTabList]} + rightNodes={[ResetButton, PublishButton, TestButton]} /> + {activeTabId && ( + <CodeEditor + files={files} + currentFilePath={activeTabId} + onChange={(newCodeValue) => onChange(activeTabId, newCodeValue)} + setIsCodeValid={setIsCodeValid} + /> + )} </Section> ); }; 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 6eed09db5122..b2d54cbc03f9 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 @@ -44,28 +44,6 @@ export const SettingsServerlessFunctionTestTab = ({ settingsServerlessFunctionOutput.error || ''; - const InputHeader = ( - <CoreEditorHeader - title={'Input'} - rightNodes={[ - <Button - title="Run Function" - variant="primary" - accent="blue" - size="small" - Icon={IconPlayerPlay} - onClick={handleExecute} - />, - ]} - /> - ); - - const OutputHeader = ( - <CoreEditorHeader - leftNodes={[<SettingsServerlessFunctionsOutputMetadataInfo />]} - rightNodes={[<LightCopyIconButton copyText={result} />]} - /> - ); const navigate = useNavigate(); useHotkeyScopeOnMount( SettingsServerlessFunctionHotkeyScope.ServerlessFunctionTestTab, @@ -86,20 +64,52 @@ export const SettingsServerlessFunctionTestTab = ({ description='Insert a JSON input, then press "Run" to test your function.' /> <StyledInputsContainer> - <CodeEditor - value={settingsServerlessFunctionInput} - height={200} - onChange={setSettingsServerlessFunctionInput} - language={'json'} - header={InputHeader} - /> - <CodeEditor - value={result} - height={settingsServerlessFunctionCodeEditorOutputParams.height} - language={settingsServerlessFunctionCodeEditorOutputParams.language} - options={{ readOnly: true, domReadOnly: true }} - header={OutputHeader} - /> + <div> + <CoreEditorHeader + title={'Input'} + rightNodes={[ + <Button + title="Run Function" + variant="primary" + accent="blue" + size="small" + Icon={IconPlayerPlay} + onClick={handleExecute} + />, + ]} + /> + <CodeEditor + files={[ + { + content: settingsServerlessFunctionInput, + language: 'json', + path: 'input.json', + }, + ]} + currentFilePath={'input.json'} + height={200} + onChange={setSettingsServerlessFunctionInput} + /> + </div> + <div> + <CoreEditorHeader + leftNodes={[<SettingsServerlessFunctionsOutputMetadataInfo />]} + rightNodes={[<LightCopyIconButton copyText={result} />]} + /> + <CodeEditor + files={[ + { + content: result, + language: + settingsServerlessFunctionCodeEditorOutputParams.language, + path: 'result.any', + }, + ]} + currentFilePath={'result.any'} + height={settingsServerlessFunctionCodeEditorOutputParams.height} + options={{ readOnly: true, domReadOnly: true }} + /> + </div> </StyledInputsContainer> </Section> ); diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/constants/SettingsServerlessFunctionTabListComponentId.ts b/packages/twenty-front/src/modules/settings/serverless-functions/constants/SettingsServerlessFunctionTabListComponentId.ts new file mode 100644 index 000000000000..0c8c15a91dff --- /dev/null +++ b/packages/twenty-front/src/modules/settings/serverless-functions/constants/SettingsServerlessFunctionTabListComponentId.ts @@ -0,0 +1,2 @@ +export const SETTINGS_SERVERLESS_FUNCTION_TAB_LIST_COMPONENT_ID = + 'settings-serverless-function-editor-tab-list'; 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 2fd1e2506614..bbcad2f1c31f 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 @@ -5,7 +5,6 @@ export const SERVERLESS_FUNCTION_FRAGMENT = gql` id name description - sourceCodeHash runtime syncStatus latestVersion diff --git a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/__tests__/useServerlessFunctionUpdateFormState.test.ts b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/__tests__/useServerlessFunctionUpdateFormState.test.ts index e0799f13ac2e..36de554158df 100644 --- a/packages/twenty-front/src/modules/settings/serverless-functions/hooks/__tests__/useServerlessFunctionUpdateFormState.test.ts +++ b/packages/twenty-front/src/modules/settings/serverless-functions/hooks/__tests__/useServerlessFunctionUpdateFormState.test.ts @@ -1,5 +1,5 @@ -import { renderHook } from '@testing-library/react'; import { useServerlessFunctionUpdateFormState } from '@/settings/serverless-functions/hooks/useServerlessFunctionUpdateFormState'; +import { renderHook } from '@testing-library/react'; import { RecoilRoot } from 'recoil'; jest.mock( @@ -44,6 +44,6 @@ describe('useServerlessFunctionUpdateFormState', () => { const { formValues } = result.current; - expect(formValues).toEqual({ name: '', description: '', code: '' }); + expect(formValues).toEqual({ name: '', description: '', code: undefined }); }); }); 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 97780d3fca65..9e8a13483810 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 @@ -1,6 +1,6 @@ -import { Dispatch, SetStateAction, useState } from 'react'; import { useGetOneServerlessFunction } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunction'; import { useGetOneServerlessFunctionSourceCode } from '@/settings/serverless-functions/hooks/useGetOneServerlessFunctionSourceCode'; +import { Dispatch, SetStateAction, useState } from 'react'; import { FindOneServerlessFunctionSourceCodeQuery } from '~/generated-metadata/graphql'; export type ServerlessFunctionNewFormValues = { @@ -9,7 +9,7 @@ export type ServerlessFunctionNewFormValues = { }; export type ServerlessFunctionFormValues = ServerlessFunctionNewFormValues & { - code: string; + code: { [filePath: string]: string } | undefined; }; type SetServerlessFunctionFormValues = Dispatch< @@ -26,7 +26,7 @@ export const useServerlessFunctionUpdateFormState = ( const [formValues, setFormValues] = useState<ServerlessFunctionFormValues>({ name: '', description: '', - code: '', + code: undefined, }); const { serverlessFunction } = @@ -37,7 +37,7 @@ export const useServerlessFunctionUpdateFormState = ( version: 'draft', onCompleted: (data: FindOneServerlessFunctionSourceCodeQuery) => { const newState = { - code: data?.getServerlessFunctionSourceCode || '', + code: data?.getServerlessFunctionSourceCode || undefined, name: serverlessFunction?.name || '', description: serverlessFunction?.description || '', }; diff --git a/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImport.tsx b/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImport.tsx index 50bc89f4358a..032f632a97f1 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImport.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImport.tsx @@ -24,13 +24,16 @@ export const defaultSpreadsheetImportProps: Partial< export const SpreadsheetImport = <T extends string>( props: SpreadsheetImportProps<T>, ) => { + const mergedProps = { + ...defaultSpreadsheetImportProps, + ...props, + } as SpreadsheetImportProps<T>; + return ( - <ReactSpreadsheetImportContextProvider values={props}> - <ModalWrapper isOpen={props.isOpen} onClose={props.onClose}> + <ReactSpreadsheetImportContextProvider values={mergedProps}> + <ModalWrapper isOpen={mergedProps.isOpen} onClose={mergedProps.onClose}> <SpreadsheetImportStepperContainer /> </ModalWrapper> </ReactSpreadsheetImportContextProvider> ); }; - -SpreadsheetImport.defaultProps = defaultSpreadsheetImportProps; diff --git a/packages/twenty-front/src/modules/support/components/SupportButtonSkeletonLoader.tsx b/packages/twenty-front/src/modules/support/components/SupportButtonSkeletonLoader.tsx index 86d4d1b93231..78440fe021d2 100644 --- a/packages/twenty-front/src/modules/support/components/SupportButtonSkeletonLoader.tsx +++ b/packages/twenty-front/src/modules/support/components/SupportButtonSkeletonLoader.tsx @@ -1,5 +1,6 @@ -import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { useTheme } from '@emotion/react'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; export const SupportButtonSkeletonLoader = () => { const theme = useTheme(); @@ -9,7 +10,7 @@ export const SupportButtonSkeletonLoader = () => { highlightColor={theme.background.transparent.lighter} borderRadius={4} > - <Skeleton width={84} height={24} /> + <Skeleton width={84} height={SKELETON_LOADER_HEIGHT_SIZES.standard.m} /> </SkeletonTheme> ); }; diff --git a/packages/twenty-front/src/modules/types/SettingsPath.ts b/packages/twenty-front/src/modules/types/SettingsPath.ts index 92f152619c2a..96efe89cdb7a 100644 --- a/packages/twenty-front/src/modules/types/SettingsPath.ts +++ b/packages/twenty-front/src/modules/types/SettingsPath.ts @@ -12,8 +12,8 @@ export enum SettingsPath { ObjectOverview = 'objects/overview', ObjectDetail = 'objects/:objectSlug', ObjectEdit = 'objects/:objectSlug/edit', - ObjectNewFieldStep1 = 'objects/:objectSlug/new-field/step-1', - ObjectNewFieldStep2 = 'objects/:objectSlug/new-field/step-2', + ObjectNewFieldSelect = 'objects/:objectSlug/new-field/select', + ObjectNewFieldConfigure = 'objects/:objectSlug/new-field/configure', ObjectFieldEdit = 'objects/:objectSlug/:fieldSlug', NewObject = 'objects/new', NewServerlessFunction = 'functions/new', diff --git a/packages/twenty-front/src/modules/ui/field/display/components/ActorDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/ActorDisplay.tsx index ba107aa612f0..b5111d245a0a 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/ActorDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/ActorDisplay.tsx @@ -7,6 +7,7 @@ import { IconCalendar, IconCsv, IconGmail, + IconRobot, } from 'twenty-ui'; type ActorDisplayProps = Partial<FieldActorValue> & { @@ -29,12 +30,15 @@ export const ActorDisplay = ({ return IconGmail; case 'CALENDAR': return IconCalendar; + case 'SYSTEM': + return IconRobot; default: return undefined; } }, [source]); - const isIconInverted = source === 'API' || source === 'IMPORT'; + const isIconInverted = + source === 'API' || source === 'IMPORT' || source === 'SYSTEM'; return ( <AvatarChip diff --git a/packages/twenty-front/src/modules/ui/field/display/components/CurrencyDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/CurrencyDisplay.tsx index 55f641ca4d7a..f2af5ae75803 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/CurrencyDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/CurrencyDisplay.tsx @@ -5,6 +5,7 @@ import { FieldCurrencyValue } from '@/object-record/record-field/types/FieldMeta import { SETTINGS_FIELD_CURRENCY_CODES } from '@/settings/data-model/constants/SettingsFieldCurrencyCodes'; import { formatAmount } from '~/utils/format/formatAmount'; import { isDefined } from '~/utils/isDefined'; +import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; type CurrencyDisplayProps = { currencyValue: FieldCurrencyValue | null | undefined; @@ -29,7 +30,9 @@ export const CurrencyDisplay = ({ currencyValue }: CurrencyDisplayProps) => { ? SETTINGS_FIELD_CURRENCY_CODES[currencyValue?.currencyCode]?.Icon : null; - const amountToDisplay = (currencyValue?.amountMicros ?? 0) / 1000000; + const amountToDisplay = isUndefinedOrNull(currencyValue?.amountMicros) + ? null + : currencyValue?.amountMicros / 1000000; if (!shouldDisplayCurrency) { return <StyledEllipsisDisplay>{0}</StyledEllipsisDisplay>; @@ -46,7 +49,7 @@ export const CurrencyDisplay = ({ currencyValue }: CurrencyDisplayProps) => { />{' '} </> )} - {amountToDisplay !== 0 ? formatAmount(amountToDisplay) : ''} + {amountToDisplay !== null ? formatAmount(amountToDisplay) : ''} </StyledEllipsisDisplay> ); }; diff --git a/packages/twenty-front/src/modules/ui/field/display/components/NumberDisplay.tsx b/packages/twenty-front/src/modules/ui/field/display/components/NumberDisplay.tsx index 1834e502a051..cef5ff6e0b81 100644 --- a/packages/twenty-front/src/modules/ui/field/display/components/NumberDisplay.tsx +++ b/packages/twenty-front/src/modules/ui/field/display/components/NumberDisplay.tsx @@ -4,8 +4,11 @@ import { EllipsisDisplay } from './EllipsisDisplay'; type NumberDisplayProps = { value: string | number | null | undefined; + decimals?: number; }; -export const NumberDisplay = ({ value }: NumberDisplayProps) => ( - <EllipsisDisplay>{value && formatNumber(Number(value))}</EllipsisDisplay> +export const NumberDisplay = ({ value, decimals }: NumberDisplayProps) => ( + <EllipsisDisplay> + {value && formatNumber(Number(value), decimals)} + </EllipsisDisplay> ); diff --git a/packages/twenty-front/src/modules/ui/field/input/components/CurrencyInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/CurrencyInput.tsx index ced18f9c5d81..052f47954202 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/CurrencyInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/CurrencyInput.tsx @@ -1,7 +1,7 @@ -import { useEffect, useMemo, useRef, useState } from 'react'; -import { IMaskInput, IMaskInputProps } from 'react-imask'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { useEffect, useMemo, useRef, useState } from 'react'; +import { IMaskInput, IMaskInputProps } from 'react-imask'; import { IconComponent, TEXT_INPUT_STYLE } from 'twenty-ui'; import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents'; @@ -138,13 +138,13 @@ export const CurrencyInput = ({ mask={Number} thousandsSeparator={','} radix="." - unmask="typed" onAccept={(value: string) => handleChange(value)} inputRef={wrapperRef} autoComplete="off" placeholder={placeholder} autoFocus={autoFocus} value={value} + unmask /> </StyledContainer> ); diff --git a/packages/twenty-front/src/modules/ui/field/input/components/DateInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/DateInput.tsx index 1e74e447b5ac..c61ce4491ebe 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/DateInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/DateInput.tsx @@ -1,5 +1,5 @@ -import { useRef, useState } from 'react'; import styled from '@emotion/styled'; +import { useRef, useState } from 'react'; import { Nullable } from 'twenty-ui'; import { @@ -16,9 +16,6 @@ const StyledCalendarContainer = styled.div` border: 1px solid ${({ theme }) => theme.border.color.light}; border-radius: ${({ theme }) => theme.border.radius.md}; box-shadow: ${({ theme }) => theme.boxShadow.strong}; - top: 0; - - position: absolute; `; export type DateInputProps = { diff --git a/packages/twenty-front/src/modules/ui/field/input/components/TextAreaInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/TextAreaInput.tsx index c2da43576095..f9778f17ba05 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/TextAreaInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/TextAreaInput.tsx @@ -36,10 +36,10 @@ const StyledTextArea = styled(TextareaAutosize)` `; const StyledTextAreaContainer = styled.div` - border: ${({ theme }) => `1px solid ${theme.border.color.light}`}; + border: ${({ theme }) => `1px solid ${theme.border.color.medium}`}; position: relative; width: 100%; - padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(1)}; + padding: ${({ theme }) => theme.spacing(2)} ${({ theme }) => theme.spacing(0)}; border-radius: ${({ theme }) => theme.border.radius.sm}; background: ${({ theme }) => theme.background.primary}; `; 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 3c24d62cdef8..1240a6d6025e 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,22 +1,13 @@ 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 { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { useEffect } from 'react'; import { useGetAvailablePackages } from '@/settings/serverless-functions/hooks/useGetAvailablePackages'; import { isDefined } from '~/utils/isDefined'; -export const DEFAULT_CODE = `export const handler = async ( - event: object, - context: object -): Promise<object> => { - // Your code here - return {}; -} -`; - const StyledEditor = styled(Editor)` border: 1px solid ${({ theme }) => theme.border.color.medium}; border-top: none; @@ -24,25 +15,34 @@ const StyledEditor = styled(Editor)` ${({ theme }) => theme.border.radius.sm}; `; +export type File = { + language: string; + content: string; + path: string; +}; + type CodeEditorProps = Omit<EditorProps, 'onChange'> & { - header: React.ReactNode; + currentFilePath: string; + files: File[]; onChange?: (value: string) => void; setIsCodeValid?: (isCodeValid: boolean) => void; }; export const CodeEditor = ({ - value = DEFAULT_CODE, + currentFilePath, + files, onChange, setIsCodeValid, - language = 'typescript', height = 450, options = undefined, - header, }: 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, @@ -50,7 +50,57 @@ export const CodeEditor = ({ monaco.editor.defineTheme('codeEditorTheme', codeEditorTheme(theme)); monaco.editor.setTheme('codeEditorTheme'); - if (language === 'typescript') { + 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, @@ -71,43 +121,28 @@ export const CodeEditor = ({ setIsCodeValid?.(true); }; - useEffect(() => { - const style = document.createElement('style'); - style.innerHTML = ` - .monaco-editor .margin .line-numbers { - font-weight: bold; - } - `; - document.head.appendChild(style); - return () => { - document.head.removeChild(style); - }; - }, []); - return ( + isDefined(currentFile) && isDefined(availablePackages) && ( - <> - {header} - <StyledEditor - height={height} - language={language} - value={value} - onMount={handleEditorDidMount} - onChange={(value?: string) => value && onChange?.(value)} - onValidate={handleEditorValidation} - options={{ - ...options, - overviewRulerLanes: 0, - scrollbar: { - vertical: 'hidden', - horizontal: 'hidden', - }, - minimap: { - enabled: false, - }, - }} - /> - </> + <StyledEditor + height={height} + value={currentFile.content} + language={currentFile.language} + onMount={handleEditorDidMount} + onChange={(value?: string) => value && onChange?.(value)} + onValidate={handleEditorValidation} + options={{ + ...options, + overviewRulerLanes: 0, + scrollbar: { + vertical: 'hidden', + horizontal: 'hidden', + }, + minimap: { + enabled: false, + }, + }} + /> ) ); }; 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 f1f7cff90ab8..b0e8278b89cd 100644 --- a/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx @@ -1,5 +1,5 @@ -import { useMemo, useState } from 'react'; import styled from '@emotion/styled'; +import { useMemo, useState } from 'react'; import { useRecoilValue } from 'recoil'; import { IconApps, IconComponent, useIcons } from 'twenty-ui'; @@ -147,6 +147,8 @@ export const IconPicker = ({ [matchingSearchIconKeys], ); + const icon = selectedIconKey ? getIcon(selectedIconKey) : IconApps; + return ( <div className={className}> <Dropdown @@ -160,7 +162,7 @@ export const IconPicker = ({ : `(no icon selected)` }`} disabled={disabled} - Icon={selectedIconKey ? getIcon(selectedIconKey) : IconApps} + Icon={icon} variant={variant} /> } 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 c31a48a198ea..ba29909b509d 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 { useMemo, useRef, useState } from 'react'; +import React, { MouseEvent, useMemo, useRef, useState } from 'react'; import { IconChevronDown, IconComponent } from 'twenty-ui'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; @@ -11,6 +11,7 @@ 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'; export type SelectOption<Value extends string | number | null> = { value: Value; @@ -18,6 +19,12 @@ export type SelectOption<Value extends string | number | null> = { Icon?: IconComponent; }; +type CallToActionButton = { + text: string; + onClick: (event: MouseEvent<HTMLDivElement>) => void; + Icon?: IconComponent; +}; + export type SelectProps<Value extends string | number | null> = { className?: string; disabled?: boolean; @@ -32,6 +39,7 @@ export type SelectProps<Value extends string | number | null> = { options: SelectOption<Value>[]; value?: Value; withSearchInput?: boolean; + callToActionButton?: CallToActionButton; }; const StyledContainer = styled.div<{ fullWidth?: boolean }>` @@ -89,6 +97,7 @@ export const Select = <Value extends string | number | null>({ options, value, withSearchInput, + callToActionButton, }: SelectProps<Value>) => { const selectContainerRef = useRef<HTMLDivElement>(null); @@ -97,8 +106,8 @@ export const Select = <Value extends string | number | null>({ const selectedOption = options.find(({ value: key }) => key === value) || - options[0] || - emptyOption; + emptyOption || + options[0]; const filteredOptions = useMemo( () => searchInputValue @@ -109,7 +118,9 @@ export const Select = <Value extends string | number | null>({ [options, searchInputValue], ); - const isDisabled = disabledFromProps || options.length <= 1; + const isDisabled = + disabledFromProps || + (options.length <= 1 && !isDefined(callToActionButton)); const { closeDropdown } = useDropdown(dropdownId); @@ -177,6 +188,18 @@ export const Select = <Value extends string | number | null>({ ))} </DropdownMenuItemsContainer> )} + {!!callToActionButton && !!filteredOptions.length && ( + <DropdownMenuSeparator /> + )} + {!!callToActionButton && ( + <DropdownMenuItemsContainer hasMaxHeight> + <MenuItem + onClick={callToActionButton.onClick} + LeftIcon={callToActionButton.Icon} + text={callToActionButton.text} + /> + </DropdownMenuItemsContainer> + )} </> } dropdownHotkeyScope={{ scope: SelectHotkeyScope.Select }} diff --git a/packages/twenty-front/src/modules/ui/input/components/TextArea.tsx b/packages/twenty-front/src/modules/ui/input/components/TextArea.tsx index 9b50504c757b..b6bb9d545ab2 100644 --- a/packages/twenty-front/src/modules/ui/input/components/TextArea.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/TextArea.tsx @@ -1,5 +1,5 @@ import styled from '@emotion/styled'; -import { FocusEventHandler } from 'react'; +import { FocusEventHandler, useId } from 'react'; import TextareaAutosize from 'react-textarea-autosize'; import { usePreviousHotkeyScope } from '@/ui/utilities/hotkey/hooks/usePreviousHotkeyScope'; @@ -10,6 +10,7 @@ import { InputHotkeyScope } from '../types/InputHotkeyScope'; const MAX_ROWS = 5; export type TextAreaProps = { + label?: string; disabled?: boolean; minRows?: number; onChange?: (value: string) => void; @@ -18,6 +19,20 @@ export type TextAreaProps = { className?: string; }; +const StyledContainer = styled.div` + display: flex; + flex-direction: column; + width: 100%; +`; + +const StyledLabel = styled.label` + color: ${({ theme }) => theme.font.color.light}; + display: block; + font-size: ${({ theme }) => theme.font.size.xs}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + margin-bottom: ${({ theme }) => theme.spacing(1)}; +`; + const StyledTextArea = styled(TextareaAutosize)` background-color: ${({ theme }) => theme.background.transparent.lighter}; border: 1px solid ${({ theme }) => theme.border.color.medium}; @@ -48,6 +63,7 @@ const StyledTextArea = styled(TextareaAutosize)` `; export const TextArea = ({ + label, disabled, placeholder, minRows = 1, @@ -57,6 +73,8 @@ export const TextArea = ({ }: TextAreaProps) => { const computedMinRows = Math.min(minRows, MAX_ROWS); + const inputId = useId(); + const { goBackToPreviousHotkeyScope, setHotkeyScopeAndMemorizePreviousScope, @@ -71,18 +89,23 @@ export const TextArea = ({ }; return ( - <StyledTextArea - placeholder={placeholder} - maxRows={MAX_ROWS} - minRows={computedMinRows} - value={value} - onChange={(event) => - onChange?.(turnIntoEmptyStringIfWhitespacesOnly(event.target.value)) - } - onFocus={handleFocus} - onBlur={handleBlur} - disabled={disabled} - className={className} - /> + <StyledContainer> + {label && <StyledLabel htmlFor={inputId}>{label}</StyledLabel>} + + <StyledTextArea + id={inputId} + placeholder={placeholder} + maxRows={MAX_ROWS} + minRows={computedMinRows} + value={value} + onChange={(event) => + onChange?.(turnIntoEmptyStringIfWhitespacesOnly(event.target.value)) + } + onFocus={handleFocus} + onBlur={handleBlur} + disabled={disabled} + className={className} + /> + </StyledContainer> ); }; diff --git a/packages/twenty-front/src/modules/ui/input/components/TextInput.tsx b/packages/twenty-front/src/modules/ui/input/components/TextInput.tsx index b0841eaa710f..8e31f598822d 100644 --- a/packages/twenty-front/src/modules/ui/input/components/TextInput.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/TextInput.tsx @@ -14,7 +14,8 @@ export type TextInputProps = TextInputV2ComponentProps & { disableHotkeys?: boolean; onInputEnter?: () => void; dataTestId?: string; - focused?: boolean; + autoFocusOnMount?: boolean; + autoSelectOnMount?: boolean; }; export const TextInput = ({ @@ -22,7 +23,8 @@ export const TextInput = ({ onBlur, onInputEnter, disableHotkeys = false, - focused, + autoFocusOnMount, + autoSelectOnMount, dataTestId, ...props }: TextInputProps) => { @@ -31,11 +33,17 @@ export const TextInput = ({ const [isFocused, setIsFocused] = useState(false); useEffect(() => { - if (focused === true) { + if (autoFocusOnMount === true) { inputRef.current?.focus(); setIsFocused(true); } - }, [focused]); + }, [autoFocusOnMount]); + + useEffect(() => { + if (autoSelectOnMount === true) { + inputRef.current?.select(); + } + }, [autoSelectOnMount]); const { goBackToPreviousHotkeyScope, 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 f723e93e1c5a..eeceaee8934e 100644 --- a/packages/twenty-front/src/modules/ui/input/components/Toggle.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/Toggle.tsx @@ -16,7 +16,7 @@ type ContainerProps = { const StyledContainer = styled.div<ContainerProps>` align-items: center; background-color: ${({ theme, isOn, color }) => - isOn ? (color ?? theme.color.blue) : theme.background.quaternary}; + isOn ? (color ?? theme.color.blue) : theme.background.transparent.medium}; border-radius: 10px; cursor: pointer; display: flex; 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 d5ef7dc21c36..f24c7a1c9414 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 @@ -4,6 +4,7 @@ import { userEvent, within } from '@storybook/test'; import { ComponentDecorator } from 'twenty-ui'; import { Select, SelectProps } from '../Select'; +import { IconPlus } from 'packages/twenty-ui'; type RenderProps = SelectProps<string | number | null>; @@ -56,3 +57,13 @@ export const Disabled: Story = { export const WithSearch: Story = { args: { withSearchInput: true }, }; + +export const CallToActionButton: Story = { + args: { + callToActionButton: { + onClick: () => {}, + Icon: IconPlus, + text: 'Add action', + }, + }, +}; diff --git a/packages/twenty-front/src/modules/ui/input/components/__stories__/TextArea.stories.tsx b/packages/twenty-front/src/modules/ui/input/components/__stories__/TextArea.stories.tsx index 425ba4f4d835..6583f9cbec24 100644 --- a/packages/twenty-front/src/modules/ui/input/components/__stories__/TextArea.stories.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/__stories__/TextArea.stories.tsx @@ -1,7 +1,9 @@ -import { useState } from 'react'; import { Meta, StoryObj } from '@storybook/react'; +import { useState } from 'react'; import { ComponentDecorator } from 'twenty-ui'; +import { expect } from '@storybook/jest'; +import { userEvent, within } from '@storybook/test'; import { TextArea, TextAreaProps } from '../TextArea'; type RenderProps = TextAreaProps; @@ -37,3 +39,20 @@ export const Filled: Story = { export const Disabled: Story = { args: { disabled: true, value: 'Lorem Ipsum' }, }; + +export const WithLabel: Story = { + args: { label: 'My Textarea' }, + play: async () => { + const canvas = within(document.body); + + const label = await canvas.findByText('My Textarea'); + + expect(label).toBeVisible(); + + await userEvent.click(label); + + const input = await canvas.findByRole('textbox'); + + expect(input).toHaveFocus(); + }, +}; 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 new file mode 100644 index 000000000000..1efc985d34f6 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/AbsoluteDatePickerHeader.tsx @@ -0,0 +1,108 @@ +import styled from '@emotion/styled'; +import { DateTime } from 'luxon'; +import { IconChevronLeft, IconChevronRight } 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'; + +import { getMonthSelectOptions } from '@/ui/input/components/internal/date/utils/getMonthSelectOptions'; +import { + MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID, + MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID, +} from './InternalDatePicker'; + +const StyledCustomDatePickerHeader = styled.div` + align-items: center; + display: flex; + justify-content: flex-end; + padding-left: ${({ theme }) => theme.spacing(2)}; + padding-right: ${({ theme }) => theme.spacing(2)}; + padding-top: ${({ theme }) => theme.spacing(2)}; + + gap: ${({ theme }) => theme.spacing(1)}; +`; + +const years = Array.from( + { length: 200 }, + (_, i) => new Date().getFullYear() + 5 - i, +).map((year) => ({ label: year.toString(), value: year })); + +type AbsoluteDatePickerHeaderProps = { + date: Date; + onChange?: (date: Date | null) => void; + onChangeMonth: (month: number) => void; + onChangeYear: (year: number) => void; + onAddMonth: () => void; + onSubtractMonth: () => void; + prevMonthButtonDisabled: boolean; + nextMonthButtonDisabled: boolean; + isDateTimeInput?: boolean; + timeZone: string; +}; + +export const AbsoluteDatePickerHeader = ({ + date, + onChange, + onChangeMonth, + onChangeYear, + onAddMonth, + onSubtractMonth, + prevMonthButtonDisabled, + nextMonthButtonDisabled, + isDateTimeInput, + timeZone, +}: AbsoluteDatePickerHeaderProps) => { + const endOfDayDateTimeInLocalTimezone = DateTime.now().set({ + day: date.getDate(), + month: date.getMonth() + 1, + year: date.getFullYear(), + hour: 23, + minute: 59, + second: 59, + millisecond: 999, + }); + + const endOfDayInLocalTimezone = endOfDayDateTimeInLocalTimezone.toJSDate(); + + return ( + <> + <DateTimeInput + date={date} + isDateTimeInput={isDateTimeInput} + onChange={onChange} + userTimezone={timeZone} + /> + <StyledCustomDatePickerHeader> + <Select + dropdownId={MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID} + options={getMonthSelectOptions()} + disableBlur + onChange={onChangeMonth} + value={endOfDayInLocalTimezone.getMonth()} + fullWidth + /> + <Select + dropdownId={MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID} + onChange={onChangeYear} + value={endOfDayInLocalTimezone.getFullYear()} + options={years} + disableBlur + fullWidth + /> + <LightIconButton + Icon={IconChevronLeft} + onClick={onSubtractMonth} + size="medium" + disabled={prevMonthButtonDisabled} + /> + <LightIconButton + Icon={IconChevronRight} + onClick={onAddMonth} + size="medium" + disabled={nextMonthButtonDisabled} + /> + </StyledCustomDatePickerHeader> + </> + ); +}; 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 a3004373d059..e7a330c81ff3 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 @@ -2,52 +2,32 @@ import styled from '@emotion/styled'; import { DateTime } from 'luxon'; import ReactDatePicker from 'react-datepicker'; import { Key } from 'ts-key-enum'; -import { - IconCalendarX, - IconChevronLeft, - IconChevronRight, - OVERLAY_BACKGROUND, -} from 'twenty-ui'; +import { IconCalendarX, OVERLAY_BACKGROUND } from 'twenty-ui'; -import { LightIconButton } from '@/ui/input/button/components/LightIconButton'; import { DateTimeInput } from '@/ui/input/components/internal/date/components/DateTimeInput'; -import { Select } from '@/ui/input/components/Select'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { MenuItemLeftContent } from '@/ui/navigation/menu-item/internals/components/MenuItemLeftContent'; import { StyledHoverableMenuItemBase } from '@/ui/navigation/menu-item/internals/components/StyledMenuItemBase'; import { isDefined } from '~/utils/isDefined'; +import { AbsoluteDatePickerHeader } from '@/ui/input/components/internal/date/components/AbsoluteDatePickerHeader'; +import { RelativeDatePickerHeader } from '@/ui/input/components/internal/date/components/RelativeDatePickerHeader'; +import { getHighlightedDates } from '@/ui/input/components/internal/date/utils/getHighlightedDates'; import { UserContext } from '@/users/contexts/UserContext'; +import { + VariableDateViewFilterValueDirection, + VariableDateViewFilterValueUnit, +} from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; import { useContext } from 'react'; import 'react-datepicker/dist/react-datepicker.css'; -const months = [ - { label: 'January', value: 0 }, - { label: 'February', value: 1 }, - { label: 'March', value: 2 }, - { label: 'April', value: 3 }, - { label: 'May', value: 4 }, - { label: 'June', value: 5 }, - { label: 'July', value: 6 }, - { label: 'August', value: 7 }, - { label: 'September', value: 8 }, - { label: 'October', value: 9 }, - { label: 'November', value: 10 }, - { label: 'December', value: 11 }, -]; - -const years = Array.from( - { length: 200 }, - (_, i) => new Date().getFullYear() + 5 - i, -).map((year) => ({ label: year.toString(), value: year })); - export const MONTH_AND_YEAR_DROPDOWN_ID = 'date-picker-month-and-year-dropdown'; export const MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID = 'date-picker-month-and-year-dropdown-month-select'; export const MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID = 'date-picker-month-and-year-dropdown-year-select'; -const StyledContainer = styled.div` +const StyledContainer = styled.div<{ calendarDisabled?: boolean }>` & .react-datepicker { border-color: ${({ theme }) => theme.border.color.light}; background: transparent; @@ -207,6 +187,10 @@ const StyledContainer = styled.div` & .react-datepicker__month { margin-top: 0; + + pointer-events: ${({ calendarDisabled }) => + calendarDisabled ? 'none' : 'auto'}; + opacity: ${({ calendarDisabled }) => (calendarDisabled ? '0.5' : '1')}; } & .react-datepicker__day { @@ -288,21 +272,27 @@ const StyledButton = styled(MenuItemLeftContent)` justify-content: start; `; -const StyledCustomDatePickerHeader = styled.div` - align-items: center; - display: flex; - justify-content: flex-end; - padding-left: ${({ theme }) => theme.spacing(2)}; - padding-right: ${({ theme }) => theme.spacing(2)}; - padding-top: ${({ theme }) => theme.spacing(2)}; - - gap: ${({ theme }) => theme.spacing(1)}; -`; - type InternalDatePickerProps = { + isRelative?: boolean; date: Date | null; + relativeDate?: { + direction: VariableDateViewFilterValueDirection; + amount?: number; + unit: VariableDateViewFilterValueUnit; + }; + highlightedDateRange?: { + start: Date; + end: Date; + }; onMouseSelect?: (date: Date | null) => void; onChange?: (date: Date | null) => void; + onRelativeDateChange?: ( + relativeDate: { + direction: VariableDateViewFilterValueDirection; + amount?: number; + unit: VariableDateViewFilterValueUnit; + } | null, + ) => void; clearable?: boolean; isDateTimeInput?: boolean; onEnter?: (date: Date | null) => void; @@ -321,6 +311,10 @@ export const InternalDatePicker = ({ isDateTimeInput, keyboardEventsDisabled, onClear, + isRelative, + relativeDate, + onRelativeDateChange, + highlightedDateRange, }: InternalDatePickerProps) => { const internalDate = date ?? new Date(); @@ -469,15 +463,20 @@ export const InternalDatePicker = ({ const dateToUse = isDateTimeInput ? endOfDayInLocalTimezone : dateWithoutTime; + const highlightedDates = getHighlightedDates(highlightedDateRange); + + const selectedDates = isRelative ? highlightedDates : [dateToUse]; + return ( - <StyledContainer onKeyDown={handleKeyDown}> + <StyledContainer onKeyDown={handleKeyDown} calendarDisabled={isRelative}> <div className={clearable ? 'clearable ' : ''}> <ReactDatePicker open={true} selected={dateToUse} + selectedDates={selectedDates} openToDate={isDefined(dateToUse) ? dateToUse : undefined} disabledKeyboardNavigation - onChange={handleDateChange} + onChange={handleDateChange as any} customInput={ <DateTimeInput date={internalDate} @@ -489,47 +488,31 @@ export const InternalDatePicker = ({ renderCustomHeader={({ prevMonthButtonDisabled, nextMonthButtonDisabled, - }) => ( - <> - <DateTimeInput + }) => + isRelative ? ( + <RelativeDatePickerHeader + direction={relativeDate?.direction ?? 'PAST'} + amount={relativeDate?.amount} + unit={relativeDate?.unit ?? 'DAY'} + onChange={onRelativeDateChange} + /> + ) : ( + <AbsoluteDatePickerHeader date={internalDate} - isDateTimeInput={isDateTimeInput} onChange={onChange} - userTimezone={timeZone} + onChangeMonth={handleChangeMonth} + onChangeYear={handleChangeYear} + onAddMonth={handleAddMonth} + onSubtractMonth={handleSubtractMonth} + prevMonthButtonDisabled={prevMonthButtonDisabled} + nextMonthButtonDisabled={nextMonthButtonDisabled} + isDateTimeInput={isDateTimeInput} + timeZone={timeZone} /> - <StyledCustomDatePickerHeader> - <Select - dropdownId={MONTH_AND_YEAR_DROPDOWN_MONTH_SELECT_ID} - options={months} - disableBlur - onChange={handleChangeMonth} - value={endOfDayInLocalTimezone.getMonth()} - fullWidth - /> - <Select - dropdownId={MONTH_AND_YEAR_DROPDOWN_YEAR_SELECT_ID} - onChange={handleChangeYear} - value={endOfDayInLocalTimezone.getFullYear()} - options={years} - disableBlur - fullWidth - /> - <LightIconButton - Icon={IconChevronLeft} - onClick={handleSubtractMonth} - size="medium" - disabled={prevMonthButtonDisabled} - /> - <LightIconButton - Icon={IconChevronRight} - onClick={handleAddMonth} - size="medium" - disabled={nextMonthButtonDisabled} - /> - </StyledCustomDatePickerHeader> - </> - )} + ) + } onSelect={handleDateSelect} + selectsMultiple={isRelative} /> </div> {clearable && ( 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 new file mode 100644 index 000000000000..0a9328577dba --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/components/RelativeDatePickerHeader.tsx @@ -0,0 +1,113 @@ +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 { + VariableDateViewFilterValueDirection, + variableDateViewFilterValuePartsSchema, + VariableDateViewFilterValueUnit, +} from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; +import styled from '@emotion/styled'; +import { useEffect, useState } from 'react'; + +const StyledContainer = styled.div` + display: flex; + align-items: center; + gap: ${({ theme }) => theme.spacing(1)}; + padding: ${({ theme }) => theme.spacing(2)}; + padding-bottom: 0; +`; + +type RelativeDatePickerHeaderProps = { + direction: VariableDateViewFilterValueDirection; + amount?: number; + unit: VariableDateViewFilterValueUnit; + onChange?: (value: { + direction: VariableDateViewFilterValueDirection; + amount?: number; + unit: VariableDateViewFilterValueUnit; + }) => void; +}; + +export const RelativeDatePickerHeader = ( + props: RelativeDatePickerHeaderProps, +) => { + const [direction, setDirection] = useState(props.direction); + const [amountString, setAmountString] = useState( + props.amount ? props.amount.toString() : '', + ); + const [unit, setUnit] = useState(props.unit); + + useEffect(() => { + setAmountString(props.amount ? props.amount.toString() : ''); + setUnit(props.unit); + setDirection(props.direction); + }, [props.amount, props.unit, props.direction]); + + const textInputValue = direction === 'THIS' ? '' : amountString; + const textInputPlaceholder = direction === 'THIS' ? '-' : 'Number'; + + const isUnitPlural = props.amount && props.amount > 1 && direction !== 'THIS'; + const unitSelectOptions = RELATIVE_DATE_UNITS_SELECT_OPTIONS.map((unit) => ({ + ...unit, + label: `${unit.label}${isUnitPlural ? 's' : ''}`, + })); + + return ( + <StyledContainer> + <Select + disableBlur + dropdownId="direction-select" + value={direction} + onChange={(newDirection) => { + setDirection(newDirection); + if (props.amount === undefined && newDirection !== 'THIS') return; + props.onChange?.({ + direction: newDirection, + amount: props.amount, + unit: unit, + }); + }} + options={RELATIVE_DATE_DIRECTION_SELECT_OPTIONS} + /> + <TextInput + value={textInputValue} + onChange={(text) => { + const amountString = text.replace(/[^0-9]|^0+/g, ''); + const amount = parseInt(amountString); + + setAmountString(amountString); + + const valueParts = { + direction, + amount, + unit, + }; + + if ( + variableDateViewFilterValuePartsSchema.safeParse(valueParts).success + ) { + props.onChange?.(valueParts); + } + }} + placeholder={textInputPlaceholder} + disabled={direction === 'THIS'} + /> + <Select + disableBlur + dropdownId="unit-select" + value={unit} + onChange={(newUnit) => { + setUnit(newUnit); + if (direction !== 'THIS' && props.amount === undefined) return; + props.onChange?.({ + direction, + amount: props.amount, + unit: newUnit, + }); + }} + options={unitSelectOptions} + /> + </StyledContainer> + ); +}; 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 new file mode 100644 index 000000000000..d13926719f0f --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateDirectionSelectOptions.ts @@ -0,0 +1,13 @@ +import { VariableDateViewFilterValueDirection } from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; + +type RelativeDateDirectionOption = { + value: VariableDateViewFilterValueDirection; + label: string; +}; + +export const RELATIVE_DATE_DIRECTION_SELECT_OPTIONS: RelativeDateDirectionOption[] = + [ + { value: 'PAST', label: 'Past' }, + { value: 'THIS', label: 'This' }, + { value: 'NEXT', label: 'Next' }, + ]; 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 new file mode 100644 index 000000000000..bf65953f63bc --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/constants/RelativeDateUnitSelectOptions.ts @@ -0,0 +1,13 @@ +import { VariableDateViewFilterValueUnit } from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; + +type RelativeDateUnit = { + value: VariableDateViewFilterValueUnit; + label: string; +}; + +export const RELATIVE_DATE_UNITS_SELECT_OPTIONS: RelativeDateUnit[] = [ + { value: 'DAY', label: 'Day' }, + { value: 'WEEK', label: 'Week' }, + { value: 'MONTH', label: 'Month' }, + { value: 'YEAR', label: 'Year' }, +]; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getHighlightedDates.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getHighlightedDates.ts new file mode 100644 index 000000000000..813b36996833 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getHighlightedDates.ts @@ -0,0 +1,24 @@ +import { addDays, addMonths, startOfDay, subMonths } from 'date-fns'; + +export const getHighlightedDates = (highlightedDateRange?: { + start: Date; + end: Date; +}): Date[] => { + if (!highlightedDateRange) return []; + const { start, end } = highlightedDateRange; + + const highlightedDates: Date[] = []; + const currentDate = startOfDay(new Date()); + const minDate = subMonths(currentDate, 2); + const maxDate = addMonths(currentDate, 2); + + let dateToHighlight = start < minDate ? minDate : start; + const lastDate = end > maxDate ? maxDate : end; + + while (dateToHighlight <= lastDate) { + highlightedDates.push(dateToHighlight); + dateToHighlight = addDays(dateToHighlight, 1); + } + + return highlightedDates; +}; diff --git a/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getMonthSelectOptions.ts b/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getMonthSelectOptions.ts new file mode 100644 index 000000000000..3f5e395174ee --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/components/internal/date/utils/getMonthSelectOptions.ts @@ -0,0 +1,16 @@ +const getMonthName = (index: number): string => + new Intl.DateTimeFormat('en-US', { month: 'long' }).format( + new Date(0, index, 1), + ); + +const getMonthNames = (monthNames: string[] = []): string[] => { + if (monthNames.length === 12) return monthNames; + + return getMonthNames([...monthNames, getMonthName(monthNames.length)]); +}; + +export const getMonthSelectOptions = (): { label: string; value: number }[] => + getMonthNames().map((month, index) => ({ + label: month, + value: index, + })); diff --git a/packages/twenty-front/src/modules/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem.tsx b/packages/twenty-front/src/modules/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem.tsx index f4853bc48c69..820831f7c77b 100644 --- a/packages/twenty-front/src/modules/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem.tsx +++ b/packages/twenty-front/src/modules/ui/input/relation-picker/components/skeletons/DropdownMenuSkeletonItem.tsx @@ -1,6 +1,7 @@ -import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; const StyledDropdownMenuSkeletonContainer = styled.div` --horizontal-padding: ${({ theme }) => theme.spacing(1)}; @@ -21,7 +22,7 @@ export const DropdownMenuSkeletonItem = () => { baseColor={theme.background.quaternary} highlightColor={theme.background.secondary} > - <Skeleton height={16} /> + <Skeleton height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} /> </SkeletonTheme> </StyledDropdownMenuSkeletonContainer> ); diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuInput.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuInput.tsx index 5a123105c153..145da879ad11 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuInput.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuInput.tsx @@ -7,10 +7,14 @@ import { RGBA, TEXT_INPUT_STYLE } from 'twenty-ui'; import { useRegisterInputEvents } from '@/object-record/record-field/meta-types/input/hooks/useRegisterInputEvents'; import { useCombinedRefs } from '~/hooks/useCombinedRefs'; -const StyledInput = styled.input<{ withRightComponent?: boolean }>` +const StyledInput = styled.input<{ + withRightComponent?: boolean; + hasError?: boolean; +}>` ${TEXT_INPUT_STYLE} - border: 1px solid ${({ theme }) => theme.border.color.medium}; + border: 1px solid ${({ theme, hasError }) => + hasError ? theme.border.color.danger : theme.border.color.medium}; border-radius: ${({ theme }) => theme.border.radius.sm}; box-sizing: border-box; font-weight: ${({ theme }) => theme.font.weight.medium}; @@ -19,8 +23,10 @@ const StyledInput = styled.input<{ withRightComponent?: boolean }>` width: 100%; &:focus { - border-color: ${({ theme }) => theme.color.blue}; - box-shadow: 0px 0px 0px 3px ${({ theme }) => RGBA(theme.color.blue, 0.1)}; + ${({ theme, hasError = false }) => { + if (hasError) return ''; + return `box-shadow: 0px 0px 0px 3px ${RGBA(theme.color.blue, 0.1)}`; + }}; } ${({ withRightComponent }) => @@ -44,6 +50,12 @@ const StyledRightContainer = styled.div` transform: translateY(-50%); `; +const StyledErrorDiv = styled.div` + color: ${({ theme }) => theme.color.red}; + padding: 0 ${({ theme }) => theme.spacing(2)} + ${({ theme }) => theme.spacing(1)}; +`; + type HTMLInputProps = InputHTMLAttributes<HTMLInputElement>; export type DropdownMenuInputProps = HTMLInputProps & { @@ -60,6 +72,8 @@ export type DropdownMenuInputProps = HTMLInputProps & { autoFocus: HTMLInputProps['autoFocus']; placeholder: HTMLInputProps['placeholder']; }) => React.ReactNode; + error?: string | null; + hasError?: boolean; }; export const DropdownMenuInput = forwardRef< @@ -81,6 +95,8 @@ export const DropdownMenuInput = forwardRef< onTab, rightComponent, renderInput, + error = '', + hasError = false, }, ref, ) => { @@ -99,28 +115,32 @@ export const DropdownMenuInput = forwardRef< }); return ( - <StyledInputContainer className={className}> - {renderInput ? ( - renderInput({ - value, - onChange, - autoFocus, - placeholder, - }) - ) : ( - <StyledInput - autoFocus={autoFocus} - value={value} - placeholder={placeholder} - onChange={onChange} - ref={combinedRef} - withRightComponent={!!rightComponent} - /> - )} - {!!rightComponent && ( - <StyledRightContainer>{rightComponent}</StyledRightContainer> - )} - </StyledInputContainer> + <> + <StyledInputContainer className={className}> + {renderInput ? ( + renderInput({ + value, + onChange, + autoFocus, + placeholder, + }) + ) : ( + <StyledInput + hasError={hasError} + autoFocus={autoFocus} + value={value} + placeholder={placeholder} + onChange={onChange} + ref={combinedRef} + withRightComponent={!!rightComponent} + /> + )} + {!!rightComponent && ( + <StyledRightContainer>{rightComponent}</StyledRightContainer> + )} + </StyledInputContainer> + {error && <StyledErrorDiv>{error}</StyledErrorDiv>} + </> ); }, ); diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuSearchInput.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuSearchInput.tsx index b9f1bc87d286..ec761aa46680 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuSearchInput.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuSearchInput.tsx @@ -36,15 +36,15 @@ const StyledInput = styled.input` export const DropdownMenuSearchInput = forwardRef< HTMLInputElement, InputHTMLAttributes<HTMLInputElement> ->(({ value, onChange, placeholder = 'Search', type }) => { +>(({ value, onChange, placeholder = 'Search', type }, forwardedRef) => { const { inputRef } = useInputFocusWithoutScrollOnMount(); - + const ref = forwardedRef ?? inputRef; return ( <StyledDropdownMenuSearchInputContainer> <StyledInput autoComplete="off" {...{ onChange, placeholder, type, value }} - ref={inputRef} + ref={ref} /> </StyledDropdownMenuSearchInputContainer> ); diff --git a/packages/twenty-front/src/modules/ui/layout/hooks/__tests__/useIsMenuNavbarDisplayed.test.tsx b/packages/twenty-front/src/modules/ui/layout/hooks/__tests__/useIsMenuNavbarDisplayed.test.tsx deleted file mode 100644 index 990ff743bbf6..000000000000 --- a/packages/twenty-front/src/modules/ui/layout/hooks/__tests__/useIsMenuNavbarDisplayed.test.tsx +++ /dev/null @@ -1,40 +0,0 @@ -import * as reactRouterDom from 'react-router-dom'; - -import { useIsMenuNavbarDisplayed } from '../useIsMenuNavbarDisplayed'; - -jest.mock('react-router-dom', () => ({ - useLocation: jest.fn(), -})); - -const setupMockLocation = (pathname: string) => { - jest.spyOn(reactRouterDom, 'useLocation').mockReturnValueOnce({ - pathname, - state: undefined, - key: '', - search: '', - hash: '', - }); -}; - -describe('useIsMenuNavbarDisplayed', () => { - it('Should return true for paths starting with "/companies"', () => { - setupMockLocation('/companies'); - - const result = useIsMenuNavbarDisplayed(); - expect(result).toBeTruthy(); - }); - - it('Should return true for paths starting with "/companies/"', () => { - setupMockLocation('/companies/test-some-subpath'); - - const result = useIsMenuNavbarDisplayed(); - expect(result).toBeTruthy(); - }); - - it('Should return false for paths not starting with "/companies"', () => { - setupMockLocation('/test-path'); - - const result = useIsMenuNavbarDisplayed(); - expect(result).toBeFalsy(); - }); -}); diff --git a/packages/twenty-front/src/modules/ui/layout/hooks/useIsMenuNavbarDisplayed.ts b/packages/twenty-front/src/modules/ui/layout/hooks/useIsMenuNavbarDisplayed.ts deleted file mode 100644 index 08f6103f310d..000000000000 --- a/packages/twenty-front/src/modules/ui/layout/hooks/useIsMenuNavbarDisplayed.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { useLocation } from 'react-router-dom'; - -export const useIsMenuNavbarDisplayed = () => { - const currentPath = useLocation().pathname; - return currentPath.match(/^\/companies(\/.*)?$/) !== null; -}; diff --git a/packages/twenty-front/src/modules/ui/layout/page/DefaultLayout.tsx b/packages/twenty-front/src/modules/ui/layout/page/DefaultLayout.tsx index 4108f6a5cee6..0a05da36515e 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/DefaultLayout.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/DefaultLayout.tsx @@ -1,7 +1,4 @@ -import { css, Global, useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { AnimatePresence, LayoutGroup, motion } from 'framer-motion'; -import { Outlet } from 'react-router-dom'; +import { AuthModal } from '@/auth/components/AuthModal'; import { CommandMenu } from '@/command-menu/components/CommandMenu'; import { AppErrorBoundary } from '@/error-handler/components/AppErrorBoundary'; import { KeyboardShortcutMenu } from '@/keyboard-shortcut-menu/components/KeyboardShortcutMenu'; @@ -14,7 +11,10 @@ import { useShowAuthModal } from '@/ui/layout/hooks/useShowAuthModal'; import { DESKTOP_NAV_DRAWER_WIDTHS } from '@/ui/navigation/navigation-drawer/constants/DesktopNavDrawerWidths'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { useScreenSize } from '@/ui/utilities/screen-size/hooks/useScreenSize'; -import { AuthModal } from '@/auth/components/AuthModal'; +import { css, Global, useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { AnimatePresence, LayoutGroup, motion } from 'framer-motion'; +import { Outlet } from 'react-router-dom'; const StyledLayout = styled.div` background: ${({ theme }) => theme.background.noisy}; diff --git a/packages/twenty-front/src/modules/ui/layout/page/ShowPageContainer.tsx b/packages/twenty-front/src/modules/ui/layout/page/ShowPageContainer.tsx index 9a1aa473b987..79d8778660d2 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/ShowPageContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/ShowPageContainer.tsx @@ -6,7 +6,6 @@ import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; const StyledOuterContainer = styled.div` display: flex; - gap: ${({ theme }) => (useIsMobile() ? theme.spacing(3) : '0')}; height: 100%; width: 100%; diff --git a/packages/twenty-front/src/modules/ui/layout/page/SubMenuTopBarContainer.tsx b/packages/twenty-front/src/modules/ui/layout/page/SubMenuTopBarContainer.tsx index ef767a67a41e..9b26f71bc153 100644 --- a/packages/twenty-front/src/modules/ui/layout/page/SubMenuTopBarContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/page/SubMenuTopBarContainer.tsx @@ -1,6 +1,5 @@ import styled from '@emotion/styled'; import { JSX, ReactNode } from 'react'; -import { IconComponent } from 'twenty-ui'; import { InformationBannerWrapper } from '@/information-banner/components/InformationBannerWrapper'; import { @@ -14,7 +13,6 @@ type SubMenuTopBarContainerProps = { children: JSX.Element | JSX.Element[]; title?: string; actionButton?: ReactNode; - Icon: IconComponent; className?: string; links: BreadcrumbProps['links']; }; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx index d28366027c44..92ed242e7d6d 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx @@ -28,10 +28,18 @@ import { RightDrawerHotkeyScope } from '../types/RightDrawerHotkeyScope'; import { emitRightDrawerCloseEvent } from '@/ui/layout/right-drawer/utils/emitRightDrawerCloseEvent'; import { RightDrawerRouter } from './RightDrawerRouter'; -const StyledContainer = styled(motion.div)` +const StyledContainer = styled(motion.div)<{ isRightDrawerMinimized: boolean }>` background: ${({ theme }) => theme.background.primary}; - border-left: 1px solid ${({ theme }) => theme.border.color.medium}; - box-shadow: ${({ theme }) => theme.boxShadow.strong}; + border-left: ${({ theme, isRightDrawerMinimized }) => + isRightDrawerMinimized + ? `1px solid ${theme.border.color.strong}` + : `1px solid ${theme.border.color.medium}`}; + border-top: ${({ theme, isRightDrawerMinimized }) => + isRightDrawerMinimized ? `1px solid ${theme.border.color.strong}` : 'none'}; + border-top-left-radius: ${({ theme, isRightDrawerMinimized }) => + isRightDrawerMinimized ? theme.border.radius.md : '0'}; + box-shadow: ${({ theme, isRightDrawerMinimized }) => + isRightDrawerMinimized ? 'none' : theme.boxShadow.light}; height: 100dvh; overflow-x: hidden; position: fixed; @@ -157,6 +165,7 @@ export const RightDrawer = () => { return ( <StyledContainer + isRightDrawerMinimized={isRightDrawerMinimized} animate={targetVariantForAnimation} variants={animationVariants} transition={{ diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx index 972b2a75e3a2..a5640cfc045e 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx @@ -11,6 +11,7 @@ import { RightDrawerTopBar } from '@/ui/layout/right-drawer/components/RightDraw import { ComponentByRightDrawerPage } from '@/ui/layout/right-drawer/types/ComponentByRightDrawerPage'; import { RightDrawerWorkflowEditStep } from '@/workflow/components/RightDrawerWorkflowEditStep'; import { RightDrawerWorkflowSelectAction } from '@/workflow/components/RightDrawerWorkflowSelectAction'; +import { RightDrawerWorkflowViewStep } from '@/workflow/components/RightDrawerWorkflowViewStep'; import { isDefined } from 'twenty-ui'; import { rightDrawerPageState } from '../states/rightDrawerPageState'; import { RightDrawerPages } from '../types/RightDrawerPages'; @@ -41,6 +42,7 @@ const RIGHT_DRAWER_PAGES_CONFIG: ComponentByRightDrawerPage = { <RightDrawerWorkflowSelectAction /> ), [RightDrawerPages.WorkflowStepEdit]: <RightDrawerWorkflowEditStep />, + [RightDrawerPages.WorkflowStepView]: <RightDrawerWorkflowViewStep />, }; export const RightDrawerRouter = () => { diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx index 9176af20340a..b80b943e1730 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBar.tsx @@ -5,6 +5,7 @@ import { Chip, ChipAccent, ChipSize, useIcons } from 'twenty-ui'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { getBasePathToShowPage } from '@/object-metadata/utils/getBasePathToShowPage'; +import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading'; import { viewableRecordIdState } from '@/object-record/record-right-drawer/states/viewableRecordIdState'; import { viewableRecordNameSingularState } from '@/object-record/record-right-drawer/states/viewableRecordNameSingularState'; import { RightDrawerTopBarCloseButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarCloseButton'; @@ -66,6 +67,10 @@ export const RightDrawerTopBar = () => { viewableRecordNameSingularState, ); + const isNewViewableRecordLoading = useRecoilValue( + isNewViewableRecordLoadingState, + ); + const viewableRecordId = useRecoilValue(viewableRecordIdState); const { objectMetadataItem } = useObjectMetadataItem({ @@ -95,6 +100,7 @@ export const RightDrawerTopBar = () => { > {!isRightDrawerMinimized && ( <Chip + disabled={isNewViewableRecordLoading} label={label} leftComponent={<Icon size={theme.icon.size.md} />} size={ChipSize.Large} diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts index 7fc5d9849d89..85dc75ee18b6 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageIcons.ts @@ -7,4 +7,5 @@ export const RIGHT_DRAWER_PAGE_ICONS = { [RightDrawerPages.Copilot]: 'IconSparkles', [RightDrawerPages.WorkflowStepEdit]: 'IconSparkles', [RightDrawerPages.WorkflowStepSelectAction]: 'IconSparkles', + [RightDrawerPages.WorkflowStepView]: 'IconSparkles', }; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts index 749fb10384fc..9cba79382a0a 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/constants/RightDrawerPageTitles.ts @@ -7,4 +7,5 @@ export const RIGHT_DRAWER_PAGE_TITLES = { [RightDrawerPages.Copilot]: 'Copilot', [RightDrawerPages.WorkflowStepEdit]: 'Workflow', [RightDrawerPages.WorkflowStepSelectAction]: 'Workflow', + [RightDrawerPages.WorkflowStepView]: 'Workflow', }; diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts index f016669b48a2..68e20913a4f6 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts @@ -4,5 +4,6 @@ export enum RightDrawerPages { ViewRecord = 'view-record', Copilot = 'copilot', WorkflowStepSelectAction = 'workflow-step-select-action', + WorkflowStepView = 'workflow-step-view', WorkflowStepEdit = 'workflow-step-edit', } diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageActivityContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageActivityContainer.tsx index a69f54528cba..2e38456c4fb9 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageActivityContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageActivityContainer.tsx @@ -1,8 +1,10 @@ import { RichTextEditor } from '@/activities/components/RichTextEditor'; import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { isNewViewableRecordLoadingState } from '@/object-record/record-right-drawer/states/isNewViewableRecordLoading'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; const StyledShowPageActivityContainer = styled.div` margin-top: ${({ theme }) => theme.spacing(6)}; @@ -16,7 +18,11 @@ export const ShowPageActivityContainer = ({ 'targetObjectNameSingular' | 'id' >; }) => { - return ( + const isNewViewableRecordLoading = useRecoilValue( + isNewViewableRecordLoadingState, + ); + + return !isNewViewableRecordLoading ? ( <ScrollWrapper contextProviderName="showPageActivityContainer"> <StyledShowPageActivityContainer> <RichTextEditor @@ -30,5 +36,7 @@ export const ShowPageActivityContainer = ({ /> </StyledShowPageActivityContainer> </ScrollWrapper> + ) : ( + <></> ); }; 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 9cecd4558277..30efbdf71fa2 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 @@ -45,7 +45,6 @@ export const ShowPageMoreButton = ({ const handleDelete = () => { deleteOneRecord(recordId); closeDropdown(); - navigate(navigationMemorizedUrl, { replace: true }); }; const handleDestroy = () => { 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/ShowPageRightContainer.tsx index 0df732bfe2a9..449963c0135b 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/ShowPageRightContainer.tsx @@ -1,16 +1,3 @@ -import styled from '@emotion/styled'; -import { useRecoilValue } from 'recoil'; -import { - IconCalendarEvent, - IconCheckbox, - IconList, - IconMail, - IconNotes, - IconPaperclip, - IconSettings, - IconTimelineEvent, -} from 'twenty-ui'; - import { Calendar } from '@/activities/calendar/components/Calendar'; import { EmailThreads } from '@/activities/emails/components/EmailThreads'; import { Attachments } from '@/activities/files/components/Attachments'; @@ -19,20 +6,43 @@ import { ObjectTasks } from '@/activities/tasks/components/ObjectTasks'; import { TimelineActivities } from '@/activities/timelineActivities/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 { 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 { useTabList } from '@/ui/layout/tab/hooks/useTabList'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; -import { Workflow } from '@/workflow/components/Workflow'; +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'; const StyledShowPageRightContainer = styled.div<{ isMobile: boolean }>` display: flex; - flex: 1 0 0; flex-direction: column; + height: 100%; justify-content: start; width: 100%; height: 100%; + overflow: auto; `; const StyledTabListContainer = styled.div` @@ -57,6 +67,26 @@ const StyledGreyBox = styled.div<{ isInRightDrawer: boolean }>` isInRightDrawer ? theme.spacing(4) : ''}; `; +const StyledButtonContainer = styled.div` + align-items: center; + bottom: 0; + border-top: 1px solid ${({ theme }) => theme.border.color.light}; + display: flex; + justify-content: flex-end; + padding: ${({ theme }) => theme.spacing(2)}; + width: 100%; + box-sizing: border-box; + position: absolute; + width: 100%; +`; + +const StyledContentContainer = styled.div<{ isInRightDrawer: boolean }>` + flex: 1; + overflow-y: auto; + padding-bottom: ${({ theme, isInRightDrawer }) => + isInRightDrawer ? theme.spacing(16) : 0}; +`; + export const TAB_LIST_COMPONENT_ID = 'show-page-right-tab-list'; type ShowPageRightContainerProps = { @@ -103,11 +133,19 @@ export const ShowPageRightContainer = ({ isWorkflowEnabled && targetableObject.targetObjectNameSingular === CoreObjectNameSingular.Workflow; + const isWorkflowVersion = + isWorkflowEnabled && + targetableObject.targetObjectNameSingular === + CoreObjectNameSingular.WorkflowVersion; const shouldDisplayCalendarTab = isCompanyOrPerson; const shouldDisplayEmailsTab = emails && isCompanyOrPerson; - const isMobile = useIsMobile() || isInRightDrawer; + const isMobile = useIsMobile(); + + const isNewViewableRecordLoading = useRecoilValue( + isNewViewableRecordLoadingState, + ); const tabs = [ { @@ -125,13 +163,13 @@ export const ShowPageRightContainer = ({ id: 'fields', title: 'Fields', Icon: IconList, - hide: !isMobile, + hide: !(isMobile || isInRightDrawer), }, { id: 'timeline', title: 'Timeline', Icon: IconTimelineEvent, - hide: !timeline || isInRightDrawer || isWorkflow, + hide: !timeline || isInRightDrawer || isWorkflow || isWorkflowVersion, }, { id: 'tasks', @@ -143,7 +181,8 @@ export const ShowPageRightContainer = ({ CoreObjectNameSingular.Note || targetableObject.targetObjectNameSingular === CoreObjectNameSingular.Task || - isWorkflow, + isWorkflow || + isWorkflowVersion, }, { id: 'notes', @@ -155,13 +194,14 @@ export const ShowPageRightContainer = ({ CoreObjectNameSingular.Note || targetableObject.targetObjectNameSingular === CoreObjectNameSingular.Task || - isWorkflow, + isWorkflow || + isWorkflowVersion, }, { id: 'files', title: 'Files', Icon: IconPaperclip, - hide: !notes || isWorkflow, + hide: !notes || isWorkflow || isWorkflowVersion, }, { id: 'emails', @@ -181,6 +221,12 @@ export const ShowPageRightContainer = ({ Icon: IconSettings, hide: !isWorkflow, }, + { + id: 'workflowVersion', + title: 'Workflow Version', + Icon: IconSettings, + hide: !isWorkflowVersion, + }, ]; const renderActiveTabContent = () => { switch (activeTabId) { @@ -220,22 +266,69 @@ export const ShowPageRightContainer = ({ case 'calendar': return <Calendar targetableObject={targetableObject} />; case 'workflow': - return <Workflow targetableObject={targetableObject} />; + return ( + <> + <WorkflowVisualizerEffect workflowId={targetableObject.id} /> + + <WorkflowVisualizer targetableObject={targetableObject} /> + </> + ); + case 'workflowVersion': + return ( + <> + <WorkflowVersionVisualizerEffect + workflowVersionId={targetableObject.id} + /> + + <WorkflowVersionVisualizer + workflowVersionId={targetableObject.id} + /> + </> + ); default: return <></>; } }; + + 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<ObjectRecord | null>( + recordStoreFamilyState(targetableObject.id), + ); + return ( <StyledShowPageRightContainer isMobile={isMobile}> <StyledTabListContainer> <TabList - loading={loading} + loading={loading || isNewViewableRecordLoading} tabListId={`${TAB_LIST_COMPONENT_ID}-${isInRightDrawer}`} tabs={tabs} /> </StyledTabListContainer> {summaryCard} - {renderActiveTabContent()} + <StyledContentContainer isInRightDrawer={isInRightDrawer}> + {renderActiveTabContent()} + </StyledContentContainer> + {isInRightDrawer && recordFromStore && !recordFromStore.deletedAt && ( + <StyledButtonContainer> + <Button + Icon={IconTrash} + onClick={handleDelete} + disabled={isDeleting} + title={isDeleting ? 'Deleting...' : 'Delete'} + ></Button> + </StyledButtonContainer> + )} </StyledShowPageRightContainer> ); }; diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx index 137bad6002cf..dca1b2cea957 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCard.tsx @@ -1,3 +1,4 @@ +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; import { ChangeEvent, ReactNode, useRef } from 'react'; @@ -60,11 +61,12 @@ const StyledTitle = styled.div<{ isMobile: boolean }>` font-size: ${({ theme }) => theme.font.size.xl}; font-weight: ${({ theme }) => theme.font.weight.semiBold}; justify-content: ${({ isMobile }) => (isMobile ? 'flex-start' : 'center')}; - width: ${({ isMobile }) => (isMobile ? '' : '100%')}; + max-width: 90%; `; -const StyledAvatarWrapper = styled.div` - cursor: pointer; +const StyledAvatarWrapper = styled.div<{ isAvatarEditable: boolean }>` + cursor: ${({ isAvatarEditable }) => + isAvatarEditable ? 'pointer' : 'default'}; `; const StyledFileInput = styled.input` @@ -87,9 +89,9 @@ const StyledShowPageSummaryCardSkeletonLoader = () => { highlightColor={theme.background.transparent.lighter} borderRadius={4} > - <Skeleton width={40} height={40} /> + <Skeleton width={40} height={SKELETON_LOADER_HEIGHT_SIZES.standard.xl} /> <StyledSubSkeleton> - <Skeleton width={96} height={16} /> + <Skeleton width={96} height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} /> </StyledSubSkeleton> </SkeletonTheme> ); @@ -130,7 +132,7 @@ export const ShowPageSummaryCard = ({ return ( <StyledShowPageSummaryCard isMobile={isMobile}> - <StyledAvatarWrapper> + <StyledAvatarWrapper isAvatarEditable={!!onUploadPicture}> <Avatar avatarUrl={logoOrAvatar} onClick={onUploadPicture ? handleAvatarClick : undefined} diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCardSkeletonLoader.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCardSkeletonLoader.tsx new file mode 100644 index 000000000000..51b15f506c67 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageSummaryCardSkeletonLoader.tsx @@ -0,0 +1,36 @@ +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; + +const StyledContainer = styled.div` + display: flex; + gap: ${({ theme }) => theme.spacing(4)}; + height: ${({ theme }) => theme.spacing(19)}; + margin: ${({ theme }) => theme.spacing(4)}; +`; + +const StyledRectangularSkeleton = styled(Skeleton)` + height: ${({ theme }) => theme.spacing(4)}; + width: ${({ theme }) => theme.spacing(24)}; + margin: ${({ theme }) => theme.spacing(1)}; + border-radius: ${({ theme }) => theme.border.radius.sm}; +`; + +export const ShowPageSummaryCardSkeletonLoader = () => { + const theme = useTheme(); + return ( + <SkeletonTheme + baseColor={theme.background.tertiary} + highlightColor={theme.background.transparent.lighter} + > + <StyledContainer> + <Skeleton + height={SKELETON_LOADER_HEIGHT_SIZES.standard.xl} + width={40} + /> + <StyledRectangularSkeleton /> + </StyledContainer> + </SkeletonTheme> + ); +}; diff --git a/packages/twenty-front/src/modules/ui/layout/table/components/TableSection.tsx b/packages/twenty-front/src/modules/ui/layout/table/components/TableSection.tsx index cbec405327e2..84be28c93e60 100644 --- a/packages/twenty-front/src/modules/ui/layout/table/components/TableSection.tsx +++ b/packages/twenty-front/src/modules/ui/layout/table/components/TableSection.tsx @@ -1,8 +1,7 @@ -import { ReactNode, useState } from 'react'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { ReactNode, useState } from 'react'; import { IconChevronDown, IconChevronUp } from 'twenty-ui'; - import { TableBody } from './TableBody'; type TableSectionProps = { @@ -28,7 +27,7 @@ const StyledSectionHeader = styled.div<{ isExpanded: boolean }>` `; const StyledSection = styled.div<{ isExpanded: boolean }>` - max-height: ${({ isExpanded }) => (isExpanded ? '1000px' : 0)}; + max-height: ${({ isExpanded }) => (isExpanded ? 'fit-content' : 0)}; opacity: ${({ isExpanded }) => (isExpanded ? 1 : 0)}; overflow: hidden; transition: diff --git a/packages/twenty-front/src/modules/ui/navigation/link/components/AdvancedSettingsToggle.tsx b/packages/twenty-front/src/modules/ui/navigation/link/components/AdvancedSettingsToggle.tsx new file mode 100644 index 000000000000..e08f60031bf1 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/link/components/AdvancedSettingsToggle.tsx @@ -0,0 +1,64 @@ +import { Toggle } from '@/ui/input/components/Toggle'; +import { isAdvancedModeEnabledState } from '@/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState'; +import styled from '@emotion/styled'; +import { useRecoilState } from 'recoil'; +import { IconTool, MAIN_COLORS } from 'twenty-ui'; + +const StyledContainer = styled.div` + align-items: center; + display: flex; + width: 100%; + gap: ${({ theme }) => theme.spacing(2)}; + position: relative; +`; + +const StyledText = styled.span` + color: ${({ theme }) => theme.font.color.secondary}; + font-size: ${({ theme }) => theme.font.size.sm}; + font-weight: ${({ theme }) => theme.font.weight.medium}; + padding: ${({ theme }) => theme.spacing(1)}; +`; + +const StyledIconContainer = styled.div` + border-right: 1px solid ${MAIN_COLORS.yellow}; + height: 16px; + position: absolute; + left: ${({ theme }) => theme.spacing(-5)}; +`; + +const StyledToggleContainer = styled.div` + display: flex; + align-items: center; + justify-content: space-between; + width: 100%; +`; + +const StyledIconTool = styled(IconTool)` + margin-right: ${({ theme }) => theme.spacing(0.5)}; +`; + +export const AdvancedSettingsToggle = () => { + const [isAdvancedModeEnabled, setIsAdvancedModeEnabled] = useRecoilState( + isAdvancedModeEnabledState, + ); + + const onChange = (newValue: boolean) => { + setIsAdvancedModeEnabled(newValue); + }; + + return ( + <StyledContainer> + <StyledIconContainer> + <StyledIconTool size={12} color={MAIN_COLORS.yellow} /> + </StyledIconContainer> + <StyledToggleContainer> + <StyledText>Advanced:</StyledText> + <Toggle + onChange={onChange} + color={MAIN_COLORS.yellow} + value={isAdvancedModeEnabled} + /> + </StyledToggleContainer> + </StyledContainer> + ); +}; diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar.tsx index 7e8dff27ecfc..8086e4ad0a3c 100644 --- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemMultiSelectAvatar.tsx @@ -22,7 +22,7 @@ type MenuItemMultiSelectAvatarProps = { avatar?: ReactNode; selected: boolean; isKeySelected?: boolean; - text: string; + text?: string; className?: string; onSelectChange?: (selected: boolean) => void; }; diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemSelect.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemSelect.tsx index 5f6dee4cd9d1..7a5a976709f0 100644 --- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemSelect.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemSelect.tsx @@ -1,6 +1,6 @@ import { css, useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconCheck, IconComponent } from 'twenty-ui'; +import { IconCheck, IconChevronRight, IconComponent } from 'twenty-ui'; import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent'; import { StyledMenuItemBase } from '../internals/components/StyledMenuItemBase'; @@ -45,6 +45,7 @@ type MenuItemSelectProps = { onClick?: () => void; disabled?: boolean; hovered?: boolean; + hasSubMenu?: boolean; }; export const MenuItemSelect = ({ @@ -55,6 +56,7 @@ export const MenuItemSelect = ({ onClick, disabled, hovered, + hasSubMenu = false, }: MenuItemSelectProps) => { const theme = useTheme(); @@ -68,6 +70,12 @@ export const MenuItemSelect = ({ > <MenuItemLeftContent LeftIcon={LeftIcon} text={text} /> {selected && <IconCheck size={theme.icon.size.md} />} + {hasSubMenu && ( + <IconChevronRight + size={theme.icon.size.sm} + color={theme.font.color.tertiary} + /> + )} </StyledMenuItemSelect> ); }; 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 35fb3cae0d54..ce0bb6f70e6a 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 @@ -40,6 +40,7 @@ const StyledContainer = styled.div<{ isSubMenu?: boolean }>` ${({ isSubMenu, theme }) => isSubMenu ? css` + padding-left: ${theme.spacing(0)}; padding-right: ${theme.spacing(8)}; ` : ''} @@ -48,13 +49,10 @@ const StyledContainer = styled.div<{ isSubMenu?: boolean }>` width: 100%; } `; - const StyledItemsContainer = styled.div` display: flex; flex-direction: column; - gap: ${({ theme }) => theme.spacing(3)}; margin-bottom: auto; - overflow-y: auto; `; export const NavigationDrawer = ({ 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 bea1d0f8e46a..2ba98503329b 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 @@ -4,6 +4,8 @@ const StyledSection = styled.div` display: flex; flex-direction: column; gap: ${({ theme }) => theme.betweenSiblingsGap}; + width: 100%; + margin-bottom: ${({ theme }) => theme.spacing(3)}; `; export { StyledSection as NavigationDrawerSection }; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitleSkeletonLoader.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitleSkeletonLoader.tsx index fcd3ffe20eaa..5391ea3e320d 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitleSkeletonLoader.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitleSkeletonLoader.tsx @@ -1,6 +1,7 @@ -import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; const StyledSkeletonTitle = styled.div` margin-bottom: ${(props) => props.theme.spacing(2)}; @@ -16,7 +17,10 @@ export const NavigationDrawerSectionTitleSkeletonLoader = () => { borderRadius={4} > <StyledSkeletonTitle> - <Skeleton width={56} height={13} /> + <Skeleton + width={56} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.xs} + /> </StyledSkeletonTitle> </SkeletonTheme> ); diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState.ts b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState.ts new file mode 100644 index 000000000000..05fd34d432a3 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/states/isAdvancedModeEnabledState.ts @@ -0,0 +1,8 @@ +import { atom } from 'recoil'; +import { localStorageEffect } from '~/utils/recoil-effects'; + +export const isAdvancedModeEnabledState = atom<boolean>({ + key: 'isAdvancedModeEnabledAtom', + default: false, + effects: [localStorageEffect()], +}); diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx index bbaa9af31b03..b8b9fe56ffa4 100644 --- a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx +++ b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx @@ -9,6 +9,7 @@ import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { EditableFilterChip } from '@/views/components/EditableFilterChip'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; import { useDeleteCombinedViewFilters } from '@/views/hooks/useDeleteCombinedViewFilters'; import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; @@ -29,6 +30,7 @@ export const EditableFilterDropdownButton = ({ setFilterDefinitionUsedInDropdown, setSelectedOperandInDropdown, setSelectedFilter, + setIsObjectFilterDropdownOperandSelectUnfolded, } = useFilterDropdown({ filterDropdownId: viewFilterDropdownId, }); @@ -73,12 +75,22 @@ export const EditableFilterDropdownButton = ({ const { id: fieldId, value, operand } = viewFilter; if ( !value && - ![FilterOperand.IsEmpty, FilterOperand.IsNotEmpty].includes(operand) + ![ + FilterOperand.IsEmpty, + FilterOperand.IsNotEmpty, + ViewFilterOperand.IsInPast, + ViewFilterOperand.IsInFuture, + ViewFilterOperand.IsToday, + ].includes(operand) ) { deleteCombinedViewFilter(fieldId); } }, [viewFilter, deleteCombinedViewFilter]); + const handleDropdownClose = useCallback(() => { + setIsObjectFilterDropdownOperandSelectUnfolded(false); + }, [setIsObjectFilterDropdownOperandSelectUnfolded]); + return ( <Dropdown dropdownId={viewFilterDropdownId} @@ -94,6 +106,7 @@ export const EditableFilterDropdownButton = ({ dropdownOffset={{ y: 8, x: 0 }} dropdownPlacement="bottom-start" onClickOutside={handleDropdownClickOutside} + onClose={handleDropdownClose} /> ); }; diff --git a/packages/twenty-front/src/modules/views/components/QueryParamsFiltersEffect.tsx b/packages/twenty-front/src/modules/views/components/QueryParamsFiltersEffect.tsx index 10ecdfa83b87..f811df0ddf6b 100644 --- a/packages/twenty-front/src/modules/views/components/QueryParamsFiltersEffect.tsx +++ b/packages/twenty-front/src/modules/views/components/QueryParamsFiltersEffect.tsx @@ -1,23 +1,24 @@ import { useEffect } from 'react'; +import { useRecoilComponentValueV2 } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValueV2'; import { useSetRecoilComponentFamilyStateV2 } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentFamilyStateV2'; import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams'; -import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates'; +import { currentViewIdComponentState } from '@/views/states/currentViewIdComponentState'; import { unsavedToUpsertViewFiltersComponentFamilyState } from '@/views/states/unsavedToUpsertViewFiltersComponentFamilyState'; -import { isDefined } from 'twenty-ui'; export const QueryParamsFiltersEffect = () => { const { hasFiltersQueryParams, getFiltersFromQueryParams, viewIdQueryParam } = useViewFromQueryParams(); + const currentViewId = useRecoilComponentValueV2(currentViewIdComponentState); + const setUnsavedViewFilter = useSetRecoilComponentFamilyStateV2( unsavedToUpsertViewFiltersComponentFamilyState, - { viewId: viewIdQueryParam }, + { viewId: viewIdQueryParam ?? currentViewId }, ); const { resetUnsavedViewStates } = useResetUnsavedViewStates(); - const { currentViewId } = useGetCurrentView(); useEffect(() => { if (!hasFiltersQueryParams) { @@ -29,18 +30,11 @@ export const QueryParamsFiltersEffect = () => { setUnsavedViewFilter(filtersFromParams); } }); - - return () => { - if (isDefined(currentViewId)) { - resetUnsavedViewStates(currentViewId); - } - }; }, [ getFiltersFromQueryParams, hasFiltersQueryParams, resetUnsavedViewStates, setUnsavedViewFilter, - currentViewId, ]); return <></>; diff --git a/packages/twenty-front/src/modules/views/components/QueryParamsViewIdEffect.tsx b/packages/twenty-front/src/modules/views/components/QueryParamsViewIdEffect.tsx index 2ee24d43fdc3..f306f29bd7d5 100644 --- a/packages/twenty-front/src/modules/views/components/QueryParamsViewIdEffect.tsx +++ b/packages/twenty-front/src/modules/views/components/QueryParamsViewIdEffect.tsx @@ -1,3 +1,4 @@ +import { contextStoreCurrentViewIdState } from '@/context-store/states/contextStoreCurrentViewIdState'; import { useLastVisitedObjectMetadataItem } from '@/navigation/hooks/useLastVisitedObjectMetadataItem'; import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; @@ -7,6 +8,7 @@ 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'; @@ -37,6 +39,9 @@ export const QueryParamsViewIdEffect = () => { objectMetadataItemId?.id, lastVisitedObjectMetadataItemId, ); + const setContextStoreCurrentViewId = useSetRecoilState( + contextStoreCurrentViewIdState, + ); // // TODO: scope view bar per view id if possible // const { resetCurrentView } = useResetCurrentView(); @@ -59,6 +64,7 @@ export const QueryParamsViewIdEffect = () => { }); } setCurrentViewId(lastVisitedViewId); + setContextStoreCurrentViewId(lastVisitedViewId); return; } @@ -73,6 +79,7 @@ export const QueryParamsViewIdEffect = () => { }); } setCurrentViewId(viewIdQueryParam); + setContextStoreCurrentViewId(viewIdQueryParam); return; } @@ -87,8 +94,13 @@ export const QueryParamsViewIdEffect = () => { }); } setCurrentViewId(indexView.id); + setContextStoreCurrentViewId(indexView.id); return; } + + return () => { + setContextStoreCurrentViewId(null); + }; }, [ currentViewId, getFiltersFromQueryParams, @@ -96,6 +108,7 @@ export const QueryParamsViewIdEffect = () => { lastVisitedViewId, objectMetadataItemId?.id, objectNamePlural, + setContextStoreCurrentViewId, setCurrentViewId, setLastVisitedObjectMetadataItem, setLastVisitedView, diff --git a/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx b/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx index 0fab6fffdece..8b66bfc32b5d 100644 --- a/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx +++ b/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx @@ -41,6 +41,7 @@ const StyledChip = styled.div<{ variant: SortOrFitlerChipVariant }>` font-weight: ${({ theme }) => theme.font.weight.medium}; padding: ${({ theme }) => theme.spacing(1) + ' ' + theme.spacing(2)}; user-select: none; + white-space: nowrap; `; const StyledIcon = styled.div` @@ -52,6 +53,7 @@ const StyledIcon = styled.div` const StyledDelete = styled.div<{ variant: SortOrFitlerChipVariant }>` align-items: center; cursor: pointer; + padding: ${({ theme }) => theme.spacing(0.5)}; display: flex; font-size: ${({ theme }) => theme.font.size.sm}; margin-left: ${({ theme }) => theme.spacing(2)}; diff --git a/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx b/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx index fa40544755de..ffe89f6b6b60 100644 --- a/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx +++ b/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx @@ -27,7 +27,9 @@ const StyledContainer = styled.div` margin-right: ${({ theme }) => theme.spacing(2)}; position: relative; `; - +const StyledButton = styled(Button)` + padding: ${({ theme }) => theme.spacing(1)}; +`; export type UpdateViewButtonGroupProps = { hotkeyScope: HotkeyScope; }; @@ -99,7 +101,7 @@ export const UpdateViewButtonGroup = ({ dropdownId={UPDATE_VIEW_BUTTON_DROPDOWN_ID} dropdownHotkeyScope={hotkeyScope} clickableComponent={ - <Button + <StyledButton size="small" accent="blue" Icon={IconChevronDown} diff --git a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx index e3d443ace9e1..87b16a65fd14 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarDetails.tsx @@ -31,24 +31,27 @@ export type ViewBarDetailsProps = { const StyledBar = styled.div` align-items: center; + align-items: center; + border-top: 1px solid ${({ theme }) => theme.border.color.light}; border-top: 1px solid ${({ theme }) => theme.border.color.light}; display: flex; flex-direction: row; - min-height: 32px; justify-content: space-between; - z-index: 4; + min-height: 32px; padding-top: ${({ theme }) => theme.spacing(1)}; padding-bottom: ${({ theme }) => theme.spacing(1)}; + z-index: 4; `; const StyledChipcontainer = styled.div` align-items: center; display: flex; flex-direction: row; + overflow: scroll; gap: ${({ theme }) => theme.spacing(1)}; - min-height: 32px; - margin-left: ${({ theme }) => theme.spacing(2)}; - flex-wrap: wrap; + padding-top: ${({ theme }) => theme.spacing(1)}; + padding-bottom: ${({ theme }) => theme.spacing(0.5)}; + z-index: 1; `; const StyledCancelButton = styled.button` @@ -57,15 +60,8 @@ const StyledCancelButton = styled.button` color: ${({ theme }) => theme.font.color.tertiary}; cursor: pointer; font-weight: ${({ theme }) => theme.font.weight.medium}; - margin-left: auto; - margin-right: ${({ theme }) => theme.spacing(2)}; - padding: ${(props) => { - const horiz = props.theme.spacing(2); - const vert = props.theme.spacing(1); - return `${vert} ${horiz} ${vert} ${horiz}`; - }}; user-select: none; - + margin-right: ${({ theme }) => theme.spacing(2)}; &:hover { background-color: ${({ theme }) => theme.background.tertiary}; border-radius: ${({ theme }) => theme.spacing(1)}; @@ -73,8 +69,10 @@ const StyledCancelButton = styled.button` `; const StyledFilterContainer = styled.div` - align-items: center; display: flex; + align-items: center; + flex: 1; + overflow-x: hidden; `; const StyledSeperatorContainer = styled.div` diff --git a/packages/twenty-front/src/modules/views/components/ViewBarSkeletonLoader.tsx b/packages/twenty-front/src/modules/views/components/ViewBarSkeletonLoader.tsx index a7d0c1b247c1..a82565afa813 100644 --- a/packages/twenty-front/src/modules/views/components/ViewBarSkeletonLoader.tsx +++ b/packages/twenty-front/src/modules/views/components/ViewBarSkeletonLoader.tsx @@ -1,5 +1,6 @@ -import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; +import { SKELETON_LOADER_HEIGHT_SIZES } from '@/activities/components/SkeletonLoader'; import { useTheme } from '@emotion/react'; +import Skeleton, { SkeletonTheme } from 'react-loading-skeleton'; export const ViewBarSkeletonLoader = () => { const theme = useTheme(); @@ -9,7 +10,7 @@ export const ViewBarSkeletonLoader = () => { highlightColor={theme.background.transparent.lighter} borderRadius={4} > - <Skeleton width={140} height={16} /> + <Skeleton width={140} height={SKELETON_LOADER_HEIGHT_SIZES.standard.s} /> </SkeletonTheme> ); }; diff --git a/packages/twenty-front/src/modules/views/hooks/useChangeView.ts b/packages/twenty-front/src/modules/views/hooks/useChangeView.ts index 669de5157b1b..9ed9aeeff420 100644 --- a/packages/twenty-front/src/modules/views/hooks/useChangeView.ts +++ b/packages/twenty-front/src/modules/views/hooks/useChangeView.ts @@ -1,19 +1,11 @@ import { useResetUnsavedViewStates } from '@/views/hooks/useResetUnsavedViewStates'; -import { useSearchParams } from 'react-router-dom'; +import { useSetViewInUrl } from '@/views/hooks/useSetViewInUrl'; export const useChangeView = (viewBarComponentId?: string) => { const { resetUnsavedViewStates } = useResetUnsavedViewStates(viewBarComponentId); - const [, setSearchParams] = useSearchParams(); - - const setViewInUrl = (viewId: string) => { - setSearchParams(() => { - const searchParams = new URLSearchParams(); - searchParams.set('view', viewId); - return searchParams; - }); - }; + const { setViewInUrl } = useSetViewInUrl(); const changeView = async (viewId: string) => { setViewInUrl(viewId); diff --git a/packages/twenty-front/src/modules/views/hooks/useSetViewInUrl.ts b/packages/twenty-front/src/modules/views/hooks/useSetViewInUrl.ts new file mode 100644 index 000000000000..01e0397ffd6d --- /dev/null +++ b/packages/twenty-front/src/modules/views/hooks/useSetViewInUrl.ts @@ -0,0 +1,15 @@ +import { useSearchParams } from 'react-router-dom'; + +export const useSetViewInUrl = () => { + const [, setSearchParams] = useSearchParams(); + + const setViewInUrl = (viewId: string) => { + setSearchParams(() => { + const searchParams = new URLSearchParams(); + searchParams.set('view', viewId); + return searchParams; + }); + }; + + return { setViewInUrl }; +}; diff --git a/packages/twenty-front/src/modules/views/hooks/useUpsertCombinedViewFilters.ts b/packages/twenty-front/src/modules/views/hooks/useUpsertCombinedViewFilters.ts index fd33eb009583..7c5e2d99b956 100644 --- a/packages/twenty-front/src/modules/views/hooks/useUpsertCombinedViewFilters.ts +++ b/packages/twenty-front/src/modules/views/hooks/useUpsertCombinedViewFilters.ts @@ -106,15 +106,18 @@ export const useUpsertCombinedViewFilters = (viewBarComponentId?: string) => { return; } + const newValue = [ + ...unsavedToUpsertViewFilters, + { + ...upsertedFilter, + id: upsertedFilter.id, + __typename: 'ViewFilter', + } satisfies ViewFilter, + ] satisfies ViewFilter[]; + set( unsavedToUpsertViewFiltersCallbackState({ viewId: currentViewId }), - [ - ...unsavedToUpsertViewFilters, - { - ...upsertedFilter, - __typename: 'ViewFilter', - } satisfies ViewFilter, - ], + newValue, ); }, [ diff --git a/packages/twenty-front/src/modules/views/types/ViewFilter.ts b/packages/twenty-front/src/modules/views/types/ViewFilter.ts index 608e13918550..f5175cf41433 100644 --- a/packages/twenty-front/src/modules/views/types/ViewFilter.ts +++ b/packages/twenty-front/src/modules/views/types/ViewFilter.ts @@ -1,3 +1,4 @@ +import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; import { ViewFilterOperand } from './ViewFilterOperand'; export type ViewFilter = { @@ -11,4 +12,5 @@ export type ViewFilter = { createdAt?: string; updatedAt?: string; viewId?: string; + definition?: FilterDefinition; }; diff --git a/packages/twenty-front/src/modules/views/types/ViewFilterOperand.ts b/packages/twenty-front/src/modules/views/types/ViewFilterOperand.ts index 025d0085d49d..0d6446de9ea4 100644 --- a/packages/twenty-front/src/modules/views/types/ViewFilterOperand.ts +++ b/packages/twenty-front/src/modules/views/types/ViewFilterOperand.ts @@ -4,8 +4,14 @@ export enum ViewFilterOperand { IsNot = 'isNot', LessThan = 'lessThan', GreaterThan = 'greaterThan', + IsBefore = 'isBefore', + IsAfter = 'isAfter', Contains = 'contains', DoesNotContain = 'doesNotContain', IsEmpty = 'isEmpty', IsNotEmpty = 'isNotEmpty', + IsRelative = 'isRelative', + IsInPast = 'isInPast', + IsInFuture = 'isInFuture', + IsToday = 'isToday', } diff --git a/packages/twenty-front/src/modules/views/utils/__tests__/viewMapFunctions.test.ts b/packages/twenty-front/src/modules/views/utils/__tests__/viewMapFunctions.test.ts index 68acdb054655..c191b799d550 100644 --- a/packages/twenty-front/src/modules/views/utils/__tests__/viewMapFunctions.test.ts +++ b/packages/twenty-front/src/modules/views/utils/__tests__/viewMapFunctions.test.ts @@ -16,6 +16,7 @@ const baseDefinition = { fieldMetadataId: '05731f68-6e7a-4903-8374-c0b6a9063482', label: 'label', iconName: 'iconName', + fieldName: 'fieldName', }; describe('mapViewSortsToSorts', () => { diff --git a/packages/twenty-front/src/modules/views/utils/mapViewFieldsToColumnDefinitions.ts b/packages/twenty-front/src/modules/views/utils/mapViewFieldsToColumnDefinitions.ts index 139ac042751f..cbf5f19b35ae 100644 --- a/packages/twenty-front/src/modules/views/utils/mapViewFieldsToColumnDefinitions.ts +++ b/packages/twenty-front/src/modules/views/utils/mapViewFieldsToColumnDefinitions.ts @@ -50,6 +50,7 @@ export const mapViewFieldsToColumnDefinitions = ({ isSortable: correspondingColumnDefinition.isSortable, isFilterable: correspondingColumnDefinition.isFilterable, defaultValue: correspondingColumnDefinition.defaultValue, + settings: correspondingColumnDefinition.settings, } as ColumnDefinition<FieldMetadata>; }) .filter(isDefined); diff --git a/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts b/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts index 104ba6afdaae..773815c7ca58 100644 --- a/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts +++ b/packages/twenty-front/src/modules/views/utils/mapViewFiltersToFilters.ts @@ -23,7 +23,7 @@ export const mapViewFiltersToFilters = ( value: viewFilter.value, displayValue: viewFilter.displayValue, operand: viewFilter.operand, - definition: availableFilterDefinition, + definition: viewFilter.definition ?? availableFilterDefinition, }; }) .filter(isDefined); diff --git a/packages/twenty-front/src/modules/views/utils/view-filter-value/computeVariableDateViewFilterValue.ts b/packages/twenty-front/src/modules/views/utils/view-filter-value/computeVariableDateViewFilterValue.ts new file mode 100644 index 000000000000..1b09bc91348b --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/view-filter-value/computeVariableDateViewFilterValue.ts @@ -0,0 +1,10 @@ +import { + VariableDateViewFilterValueDirection, + VariableDateViewFilterValueUnit, +} from '@/views/utils/view-filter-value/resolveDateViewFilterValue'; + +export const computeVariableDateViewFilterValue = ( + direction: VariableDateViewFilterValueDirection, + amount: number | undefined, + unit: VariableDateViewFilterValueUnit, +) => `${direction}_${amount?.toString()}_${unit}`; diff --git a/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveDateViewFilterValue.ts b/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveDateViewFilterValue.ts new file mode 100644 index 000000000000..da940310505c --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveDateViewFilterValue.ts @@ -0,0 +1,190 @@ +import { ViewFilter } from '@/views/types/ViewFilter'; +import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; +import { + addDays, + addMonths, + addWeeks, + addYears, + endOfDay, + endOfMonth, + endOfWeek, + endOfYear, + roundToNearestMinutes, + startOfDay, + startOfMonth, + startOfWeek, + startOfYear, + subDays, + subMonths, + subWeeks, + subYears, +} from 'date-fns'; + +import { z } from 'zod'; + +const variableDateViewFilterValueDirectionSchema = z.enum([ + 'NEXT', + 'THIS', + 'PAST', +]); + +export type VariableDateViewFilterValueDirection = z.infer< + typeof variableDateViewFilterValueDirectionSchema +>; + +const variableDateViewFilterValueAmountSchema = z + .union([z.coerce.number().int().positive(), z.literal('undefined')]) + .transform((val) => (val === 'undefined' ? undefined : val)); + +export const variableDateViewFilterValueUnitSchema = z.enum([ + 'DAY', + 'WEEK', + 'MONTH', + 'YEAR', +]); + +export type VariableDateViewFilterValueUnit = z.infer< + typeof variableDateViewFilterValueUnitSchema +>; + +export const variableDateViewFilterValuePartsSchema = z + .object({ + direction: variableDateViewFilterValueDirectionSchema, + amount: variableDateViewFilterValueAmountSchema, + unit: variableDateViewFilterValueUnitSchema, + }) + .refine((data) => !(data.amount === undefined && data.direction !== 'THIS'), { + message: "Amount cannot be 'undefined' unless direction is 'THIS'", + }); + +const variableDateViewFilterValueSchema = z.string().transform((value) => { + const [direction, amount, unit] = value.split('_'); + + return variableDateViewFilterValuePartsSchema.parse({ + direction, + amount, + unit, + }); +}); + +const addUnit = ( + date: Date, + amount: number, + unit: VariableDateViewFilterValueUnit, +) => { + switch (unit) { + case 'DAY': + return addDays(date, amount); + case 'WEEK': + return addWeeks(date, amount); + case 'MONTH': + return addMonths(date, amount); + case 'YEAR': + return addYears(date, amount); + } +}; + +const subUnit = ( + date: Date, + amount: number, + unit: VariableDateViewFilterValueUnit, +) => { + switch (unit) { + case 'DAY': + return subDays(date, amount); + case 'WEEK': + return subWeeks(date, amount); + case 'MONTH': + return subMonths(date, amount); + case 'YEAR': + return subYears(date, amount); + } +}; + +const startOfUnit = (date: Date, unit: VariableDateViewFilterValueUnit) => { + switch (unit) { + case 'DAY': + return startOfDay(date); + case 'WEEK': + return startOfWeek(date); + case 'MONTH': + return startOfMonth(date); + case 'YEAR': + return startOfYear(date); + } +}; + +const endOfUnit = (date: Date, unit: VariableDateViewFilterValueUnit) => { + switch (unit) { + case 'DAY': + return endOfDay(date); + case 'WEEK': + return endOfWeek(date); + case 'MONTH': + return endOfMonth(date); + case 'YEAR': + return endOfYear(date); + } +}; + +const resolveVariableDateViewFilterValueFromRelativeDate = (relativeDate: { + direction: VariableDateViewFilterValueDirection; + amount?: number; + unit: VariableDateViewFilterValueUnit; +}) => { + const { direction, amount, unit } = relativeDate; + const now = roundToNearestMinutes(new Date()); + + switch (direction) { + case 'NEXT': + if (amount === undefined) throw new Error('Amount is required'); + return { + start: now, + end: addUnit(now, amount, unit), + ...relativeDate, + }; + case 'PAST': + if (amount === undefined) throw new Error('Amount is required'); + return { + start: subUnit(now, amount, unit), + end: now, + ...relativeDate, + }; + case 'THIS': + return { + start: startOfUnit(now, unit), + end: endOfUnit(now, unit), + ...relativeDate, + }; + } +}; + +const resolveVariableDateViewFilterValue = (value?: string | null) => { + if (!value) return null; + + const relativeDate = variableDateViewFilterValueSchema.parse(value); + return resolveVariableDateViewFilterValueFromRelativeDate(relativeDate); +}; + +export type ResolvedDateViewFilterValue<O extends ViewFilterOperand> = + O extends ViewFilterOperand.IsRelative + ? ReturnType<typeof resolveVariableDateViewFilterValue> + : Date | null; + +type PartialViewFilter<O extends ViewFilterOperand> = Pick< + ViewFilter, + 'value' +> & { operand: O }; + +export const resolveDateViewFilterValue = <O extends ViewFilterOperand>( + viewFilter: PartialViewFilter<O>, +): ResolvedDateViewFilterValue<O> => { + if (!viewFilter.value) return null; + + if (viewFilter.operand === ViewFilterOperand.IsRelative) { + return resolveVariableDateViewFilterValue( + viewFilter.value, + ) as ResolvedDateViewFilterValue<O>; + } + return new Date(viewFilter.value) as ResolvedDateViewFilterValue<O>; +}; diff --git a/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveFilterValue.ts b/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveFilterValue.ts new file mode 100644 index 000000000000..34afbb46ad1a --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveFilterValue.ts @@ -0,0 +1,42 @@ +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 { + resolveDateViewFilterValue, + ResolvedDateViewFilterValue, +} from './resolveDateViewFilterValue'; + +type ResolvedFilterValue< + T extends FilterableFieldType, + O extends ViewFilterOperand, +> = T extends 'DATE' | 'DATE_TIME' + ? ResolvedDateViewFilterValue<O> + : T extends 'NUMBER' + ? ReturnType<typeof resolveNumberViewFilterValue> + : string; + +type PartialFilter< + T extends FilterableFieldType, + O extends ViewFilterOperand, +> = Pick<Filter, 'value'> & { + definition: { type: T }; + operand: O; +}; + +export const resolveFilterValue = < + T extends FilterableFieldType, + O extends ViewFilterOperand, +>( + filter: PartialFilter<T, O>, +) => { + switch (filter.definition.type) { + case 'DATE': + case 'DATE_TIME': + return resolveDateViewFilterValue(filter) as ResolvedFilterValue<T, O>; + case 'NUMBER': + return resolveNumberViewFilterValue(filter) as ResolvedFilterValue<T, O>; + default: + return filter.value as ResolvedFilterValue<T, O>; + } +}; diff --git a/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveNumberViewFilterValue.ts b/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveNumberViewFilterValue.ts new file mode 100644 index 000000000000..4e26ca096332 --- /dev/null +++ b/packages/twenty-front/src/modules/views/utils/view-filter-value/resolveNumberViewFilterValue.ts @@ -0,0 +1,7 @@ +import { ViewFilter } from '@/views/types/ViewFilter'; + +export const resolveNumberViewFilterValue = ( + viewFilter: Pick<ViewFilter, 'value'>, +) => { + return viewFilter.value === '' ? null : +viewFilter.value; +}; diff --git a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx index 1d4fad3080cd..c3ce24018b9e 100644 --- a/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx +++ b/packages/twenty-front/src/modules/views/view-picker/components/ViewPickerContentCreateMode.tsx @@ -30,6 +30,7 @@ import { viewPickerIsPersistingComponentState } from '@/views/view-picker/states import { viewPickerKanbanFieldMetadataIdComponentState } from '@/views/view-picker/states/viewPickerKanbanFieldMetadataIdComponentState'; import { viewPickerSelectedIconComponentState } from '@/views/view-picker/states/viewPickerSelectedIconComponentState'; import { viewPickerTypeComponentState } from '@/views/view-picker/states/viewPickerTypeComponentState'; +import { useState } from 'react'; const StyledNoKanbanFieldAvailableContainer = styled.div` color: ${({ theme }) => theme.font.color.light}; @@ -41,6 +42,7 @@ const StyledNoKanbanFieldAvailableContainer = styled.div` export const ViewPickerContentCreateMode = () => { const { setViewPickerMode } = useViewPickerMode(); + const [hasManuallySelectedIcon, setHasManuallySelectedIcon] = useState(false); const [viewPickerInputName, setViewPickerInputName] = useRecoilComponentStateV2(viewPickerInputNameComponentState); @@ -87,9 +89,17 @@ export const ViewPickerContentCreateMode = () => { ViewsHotkeyScope.ListDropdown, ); + const defaultIcon = + viewPickerType === ViewType.Kanban ? 'IconLayoutKanban' : 'IconTable'; + + const selectedIcon = hasManuallySelectedIcon + ? viewPickerSelectedIcon + : defaultIcon; + const onIconChange = ({ iconKey }: { iconKey: string }) => { setViewPickerIsDirty(true); setViewPickerSelectedIcon(iconKey); + setHasManuallySelectedIcon(true); }; const handleClose = async () => { @@ -106,7 +116,7 @@ export const ViewPickerContentCreateMode = () => { <ViewPickerIconAndNameContainer> <IconPicker onChange={onIconChange} - selectedIconKey={viewPickerSelectedIcon} + selectedIconKey={selectedIcon} disableBlur onClose={() => setHotkeyScope(ViewsHotkeyScope.ListDropdown)} /> diff --git a/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForKanban.ts b/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForKanban.ts index 23e082c2c73c..1021a104d66a 100644 --- a/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForKanban.ts +++ b/packages/twenty-front/src/modules/views/view-picker/hooks/useGetAvailableFieldsForKanban.ts @@ -38,7 +38,7 @@ export const useGetAvailableFieldsForKanban = () => { navigate( `/settings/objects/${getObjectSlug( objectMetadataItem, - )}/new-field/step-2?fieldType=${FieldMetadataType.Select}`, + )}/new-field/configure?fieldType=${FieldMetadataType.Select}`, ); } else { navigate(`/settings/objects`); diff --git a/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowHeader.tsx b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowHeader.tsx index 4afb9839e7ea..cfea2f3b543e 100644 --- a/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowHeader.tsx +++ b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowHeader.tsx @@ -15,7 +15,7 @@ import { assertWorkflowWithCurrentVersionIsDefined } from '../utils/assertWorkfl export const RecordShowPageWorkflowHeader = ({ workflowId, }: { - workflowId: string | undefined; + workflowId: string; }) => { const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId); diff --git a/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx new file mode 100644 index 000000000000..d7d66c2dc878 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/RecordShowPageWorkflowVersionHeader.tsx @@ -0,0 +1,127 @@ +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 { useActivateWorkflowVersion } from '@/workflow/hooks/useActivateWorkflowVersion'; +import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion'; +import { useDeactivateWorkflowVersion } from '@/workflow/hooks/useDeactivateWorkflowVersion'; +import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion'; +import { Workflow, WorkflowVersion } from '@/workflow/types/Workflow'; +import { IconPencil, IconPlayerStop, IconPower, isDefined } from 'twenty-ui'; + +export const RecordShowPageWorkflowVersionHeader = ({ + workflowVersionId, +}: { + workflowVersionId: string; +}) => { + const workflowVersion = useWorkflowVersion(workflowVersionId); + + const workflowVersionRelatedWorkflowQuery = useFindOneRecord< + Pick<Workflow, '__typename' | 'id' | 'lastPublishedVersionId'> + >({ + objectNameSingular: CoreObjectNameSingular.Workflow, + objectRecordId: workflowVersion?.workflowId, + recordGqlFields: { + id: true, + lastPublishedVersionId: true, + }, + skip: !isDefined(workflowVersion), + }); + + // TODO: In the future, use the workflow.status property to determine if there is a draft version + const { + records: draftWorkflowVersions, + loading: loadingDraftWorkflowVersions, + } = useFindManyRecords<WorkflowVersion>({ + objectNameSingular: CoreObjectNameSingular.WorkflowVersion, + filter: { + workflowId: { + eq: workflowVersion?.workflow.id, + }, + status: { + eq: 'DRAFT', + }, + }, + skip: !isDefined(workflowVersion), + limit: 1, + }); + + const showUseAsDraftButton = + !loadingDraftWorkflowVersions && + isDefined(workflowVersion) && + !workflowVersionRelatedWorkflowQuery.loading && + isDefined(workflowVersionRelatedWorkflowQuery.record) && + workflowVersion.status !== 'DRAFT' && + workflowVersion.id !== + workflowVersionRelatedWorkflowQuery.record.lastPublishedVersionId; + + const hasAlreadyDraftVersion = + !loadingDraftWorkflowVersions && draftWorkflowVersions.length > 0; + + const isWaitingForWorkflowVersion = !isDefined(workflowVersion); + + const { activateWorkflowVersion } = useActivateWorkflowVersion(); + const { deactivateWorkflowVersion } = useDeactivateWorkflowVersion(); + const { createNewWorkflowVersion } = useCreateNewWorkflowVersion(); + + const { updateOneRecord: updateOneWorkflowVersion } = + useUpdateOneRecord<WorkflowVersion>({ + objectNameSingular: CoreObjectNameSingular.WorkflowVersion, + }); + + return ( + <> + {showUseAsDraftButton ? ( + <Button + title={`Use as Draft${hasAlreadyDraftVersion ? ' (override)' : ''}`} + variant="secondary" + Icon={IconPencil} + disabled={isWaitingForWorkflowVersion} + onClick={async () => { + if (hasAlreadyDraftVersion) { + await updateOneWorkflowVersion({ + idToUpdate: draftWorkflowVersions[0].id, + updateOneRecordInput: { + trigger: workflowVersion.trigger, + steps: workflowVersion.steps, + }, + }); + } else { + await createNewWorkflowVersion({ + workflowId: workflowVersion.workflow.id, + name: `v${workflowVersion.workflow.versions.length + 1}`, + status: 'DRAFT', + trigger: workflowVersion.trigger, + steps: workflowVersion.steps, + }); + } + }} + /> + ) : null} + + {workflowVersion?.status === 'DRAFT' || + workflowVersion?.status === 'DEACTIVATED' ? ( + <Button + title="Activate" + variant="secondary" + Icon={IconPower} + disabled={isWaitingForWorkflowVersion} + onClick={() => { + return activateWorkflowVersion(workflowVersion.id); + }} + /> + ) : workflowVersion?.status === 'ACTIVE' ? ( + <Button + title="Deactivate" + variant="secondary" + Icon={IconPlayerStop} + disabled={isWaitingForWorkflowVersion} + onClick={() => { + return deactivateWorkflowVersion(workflowVersion.id); + }} + /> + ) : null} + </> + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowEditStepContent.tsx b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowEditStepContent.tsx index 9fb36225678c..8154fbd7472a 100644 --- a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowEditStepContent.tsx +++ b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowEditStepContent.tsx @@ -1,57 +1,11 @@ -import { WorkflowEditActionForm } from '@/workflow/components/WorkflowEditActionForm'; -import { WorkflowEditTriggerForm } from '@/workflow/components/WorkflowEditTriggerForm'; -import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId'; +import { WorkflowStepDetail } from '@/workflow/components/WorkflowStepDetail'; import { useUpdateWorkflowVersionStep } from '@/workflow/hooks/useUpdateWorkflowVersionStep'; import { useUpdateWorkflowVersionTrigger } from '@/workflow/hooks/useUpdateWorkflowVersionTrigger'; import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState'; import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow'; -import { findStepPositionOrThrow } from '@/workflow/utils/findStepPositionOrThrow'; import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-ui'; -const getStepDefinitionOrThrow = ({ - stepId, - workflow, -}: { - stepId: string; - workflow: WorkflowWithCurrentVersion; -}) => { - const currentVersion = workflow.currentVersion; - if (!isDefined(currentVersion)) { - throw new Error('Expected to find a current version'); - } - - if (stepId === TRIGGER_STEP_ID) { - if (!isDefined(currentVersion.trigger)) { - return { - type: 'trigger', - definition: undefined, - } as const; - } - - return { - type: 'trigger', - definition: currentVersion.trigger, - } as const; - } - - if (!isDefined(currentVersion.steps)) { - throw new Error( - 'Malformed workflow version: missing steps information; be sure to create at least one step before trying to edit one', - ); - } - - const selectedNodePosition = findStepPositionOrThrow({ - steps: currentVersion.steps, - stepId: stepId, - }); - - return { - type: 'action', - definition: selectedNodePosition.steps[selectedNodePosition.index], - } as const; -}; - export const RightDrawerWorkflowEditStepContent = ({ workflow, }: { @@ -70,24 +24,12 @@ export const RightDrawerWorkflowEditStepContent = ({ stepId: workflowSelectedNode, }); - const stepDefinition = getStepDefinitionOrThrow({ - stepId: workflowSelectedNode, - workflow, - }); - - if (stepDefinition.type === 'trigger') { - return ( - <WorkflowEditTriggerForm - trigger={stepDefinition.definition} - onTriggerUpdate={updateTrigger} - /> - ); - } - return ( - <WorkflowEditActionForm - action={stepDefinition.definition} + <WorkflowStepDetail + stepId={workflowSelectedNode} + workflowVersion={workflow.currentVersion} onActionUpdate={updateStep} + onTriggerUpdate={updateTrigger} /> ); }; diff --git a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowViewStep.tsx b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowViewStep.tsx new file mode 100644 index 000000000000..f692f4649aee --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowViewStep.tsx @@ -0,0 +1,22 @@ +import { RightDrawerWorkflowViewStepContent } from '@/workflow/components/RightDrawerWorkflowViewStepContent'; +import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion'; +import { workflowVersionIdState } from '@/workflow/states/workflowVersionIdState'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +export const RightDrawerWorkflowViewStep = () => { + const workflowVersionId = useRecoilValue(workflowVersionIdState); + if (!isDefined(workflowVersionId)) { + throw new Error('Expected a workflow version id'); + } + + const workflowVersion = useWorkflowVersion(workflowVersionId); + + if (!isDefined(workflowVersion)) { + return null; + } + + return ( + <RightDrawerWorkflowViewStepContent workflowVersion={workflowVersion} /> + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowViewStepContent.tsx b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowViewStepContent.tsx new file mode 100644 index 000000000000..95a467fd53ac --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/RightDrawerWorkflowViewStepContent.tsx @@ -0,0 +1,26 @@ +import { WorkflowStepDetail } from '@/workflow/components/WorkflowStepDetail'; +import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState'; +import { WorkflowVersion } from '@/workflow/types/Workflow'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +export const RightDrawerWorkflowViewStepContent = ({ + workflowVersion, +}: { + workflowVersion: WorkflowVersion; +}) => { + const workflowSelectedNode = useRecoilValue(workflowSelectedNodeState); + if (!isDefined(workflowSelectedNode)) { + throw new Error( + 'Expected a node to be selected. Selecting a node is mandatory to edit it.', + ); + } + + return ( + <WorkflowStepDetail + stepId={workflowSelectedNode} + workflowVersion={workflowVersion} + readonly + /> + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/Workflow.tsx b/packages/twenty-front/src/modules/workflow/components/Workflow.tsx deleted file mode 100644 index a9e33c882136..000000000000 --- a/packages/twenty-front/src/modules/workflow/components/Workflow.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; -import { WorkflowDiagramCanvas } from '@/workflow/components/WorkflowDiagramCanvas'; -import { WorkflowEffect } from '@/workflow/components/WorkflowEffect'; -import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; -import { workflowDiagramState } from '@/workflow/states/workflowDiagramState'; -import styled from '@emotion/styled'; -import '@xyflow/react/dist/style.css'; -import { useRecoilValue } from 'recoil'; -import { isDefined } from 'twenty-ui'; - -const StyledFlowContainer = styled.div` - height: 100%; - width: 100%; - position: relative; - - /* Below we reset the default styling of Reactflow */ - .react-flow__node-input, - .react-flow__node-default, - .react-flow__node-output, - .react-flow__node-group { - padding: 0; - } - - --xy-node-border-radius: none; - --xy-node-border: none; - --xy-node-background-color: none; - --xy-node-boxshadow-hover: none; - --xy-node-boxshadow-selected: none; -`; - -export const Workflow = ({ - targetableObject, -}: { - targetableObject: ActivityTargetableObject; -}) => { - const workflowId = targetableObject.id; - - const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId); - const workflowDiagram = useRecoilValue(workflowDiagramState); - - return ( - <> - <WorkflowEffect - workflowId={workflowId} - workflowWithCurrentVersion={workflowWithCurrentVersion} - /> - - <StyledFlowContainer> - {isDefined(workflowDiagram) && isDefined(workflowWithCurrentVersion) ? ( - <WorkflowDiagramCanvas - diagram={workflowDiagram} - workflowWithCurrentVersion={workflowWithCurrentVersion} - /> - ) : null} - </StyledFlowContainer> - </> - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramBaseStepNode.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramBaseStepNode.tsx index 8c05d48baa09..8484a29e7eaf 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramBaseStepNode.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramBaseStepNode.tsx @@ -2,6 +2,7 @@ import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram'; import styled from '@emotion/styled'; import { Handle, Position } from '@xyflow/react'; import React from 'react'; +import { isDefined } from 'twenty-ui'; import { capitalize } from '~/utils/string/capitalize'; type Variant = 'placeholder'; @@ -76,16 +77,24 @@ export const StyledTargetHandle = styled(Handle)` visibility: hidden; `; +const StyledRightFloatingElementContainer = styled.div` + position: absolute; + transform: translateX(100%); + right: ${({ theme }) => theme.spacing(-2)}; +`; + export const WorkflowDiagramBaseStepNode = ({ nodeType, label, variant, Icon, + RightFloatingElement, }: { nodeType: WorkflowDiagramStepNodeData['nodeType']; label: string; variant?: Variant; Icon?: React.ReactNode; + RightFloatingElement?: React.ReactNode; }) => { return ( <StyledStepNodeContainer> @@ -101,6 +110,12 @@ export const WorkflowDiagramBaseStepNode = ({ {label} </StyledStepNodeLabel> + + {isDefined(RightFloatingElement) ? ( + <StyledRightFloatingElementContainer> + {RightFloatingElement} + </StyledRightFloatingElementContainer> + ) : null} </StyledStepNodeInnerContainer> <StyledSourceHandle type="source" position={Position.Bottom} /> diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvas.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasBase.tsx similarity index 63% rename from packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvas.tsx rename to packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasBase.tsx index 83b9c0c67ad3..79273d062aa6 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvas.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasBase.tsx @@ -1,14 +1,11 @@ -import { WorkflowDiagramCanvasEffect } from '@/workflow/components/WorkflowDiagramCanvasEffect'; -import { WorkflowDiagramCreateStepNode } from '@/workflow/components/WorkflowDiagramCreateStepNode'; -import { WorkflowDiagramEmptyTrigger } from '@/workflow/components/WorkflowDiagramEmptyTrigger'; -import { WorkflowDiagramStepNode } from '@/workflow/components/WorkflowDiagramStepNode'; import { WorkflowVersionStatusTag } from '@/workflow/components/WorkflowVersionStatusTag'; import { workflowDiagramState } from '@/workflow/states/workflowDiagramState'; -import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow'; +import { WorkflowVersionStatus } from '@/workflow/types/Workflow'; import { WorkflowDiagram, WorkflowDiagramEdge, WorkflowDiagramNode, + WorkflowDiagramNodeType, } from '@/workflow/types/WorkflowDiagram'; import { getOrganizedDiagram } from '@/workflow/utils/getOrganizedDiagram'; import styled from '@emotion/styled'; @@ -17,14 +14,36 @@ import { applyNodeChanges, Background, EdgeChange, + FitViewOptions, NodeChange, + NodeProps, ReactFlow, } from '@xyflow/react'; import '@xyflow/react/dist/style.css'; -import { useMemo } from 'react'; +import React, { useMemo } from 'react'; import { useSetRecoilState } from 'recoil'; import { GRAY_SCALE, isDefined } from 'twenty-ui'; +const StyledResetReactflowStyles = styled.div` + height: 100%; + width: 100%; + position: relative; + + /* Below we reset the default styling of Reactflow */ + .react-flow__node-input, + .react-flow__node-default, + .react-flow__node-output, + .react-flow__node-group { + padding: 0; + } + + --xy-node-border-radius: none; + --xy-node-border: none; + --xy-node-background-color: none; + --xy-node-boxshadow-hover: none; + --xy-node-boxshadow-selected: none; +`; + const StyledStatusTagContainer = styled.div` left: 0; top: 0; @@ -32,12 +51,31 @@ const StyledStatusTagContainer = styled.div` padding: ${({ theme }) => theme.spacing(2)}; `; -export const WorkflowDiagramCanvas = ({ +const defaultFitViewOptions: FitViewOptions = { + minZoom: 1.3, + maxZoom: 1.3, +}; + +export const WorkflowDiagramCanvasBase = ({ diagram, - workflowWithCurrentVersion, + status, + nodeTypes, + children, }: { diagram: WorkflowDiagram; - workflowWithCurrentVersion: WorkflowWithCurrentVersion; + status: WorkflowVersionStatus; + nodeTypes: Partial< + Record< + WorkflowDiagramNodeType, + React.ComponentType< + NodeProps & { + data: any; + type: any; + } + > + > + >; + children?: React.ReactNode; }) => { const { nodes, edges } = useMemo( () => getOrganizedDiagram(diagram), @@ -81,29 +119,26 @@ export const WorkflowDiagramCanvas = ({ }; return ( - <> + <StyledResetReactflowStyles> <ReactFlow - nodeTypes={{ - default: WorkflowDiagramStepNode, - 'create-step': WorkflowDiagramCreateStepNode, - 'empty-trigger': WorkflowDiagramEmptyTrigger, + onInit={({ fitView }) => { + fitView(defaultFitViewOptions); }} + nodeTypes={nodeTypes} fitView nodes={nodes.map((node) => ({ ...node, draggable: false }))} edges={edges} onNodesChange={handleNodesChange} onEdgesChange={handleEdgesChange} > - <WorkflowDiagramCanvasEffect /> - <Background color={GRAY_SCALE.gray25} size={2} /> - </ReactFlow> - <StyledStatusTagContainer> - <WorkflowVersionStatusTag - versionStatus={workflowWithCurrentVersion.currentVersion.status} - /> - </StyledStatusTagContainer> - </> + {children} + + <StyledStatusTagContainer> + <WorkflowVersionStatusTag versionStatus={status} /> + </StyledStatusTagContainer> + </ReactFlow> + </StyledResetReactflowStyles> ); }; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasEditable.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasEditable.tsx new file mode 100644 index 000000000000..ed953f511fa3 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasEditable.tsx @@ -0,0 +1,30 @@ +import { WorkflowDiagramCanvasBase } from '@/workflow/components/WorkflowDiagramCanvasBase'; +import { WorkflowDiagramCanvasEditableEffect } from '@/workflow/components/WorkflowDiagramCanvasEditableEffect'; +import { WorkflowDiagramCreateStepNode } from '@/workflow/components/WorkflowDiagramCreateStepNode'; +import { WorkflowDiagramEmptyTrigger } from '@/workflow/components/WorkflowDiagramEmptyTrigger'; +import { WorkflowDiagramStepNodeEditable } from '@/workflow/components/WorkflowDiagramStepNodeEditable'; +import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow'; +import { WorkflowDiagram } from '@/workflow/types/WorkflowDiagram'; + +export const WorkflowDiagramCanvasEditable = ({ + diagram, + workflowWithCurrentVersion, +}: { + diagram: WorkflowDiagram; + workflowWithCurrentVersion: WorkflowWithCurrentVersion; +}) => { + return ( + <WorkflowDiagramCanvasBase + key={workflowWithCurrentVersion.currentVersion.id} + diagram={diagram} + status={workflowWithCurrentVersion.currentVersion.status} + nodeTypes={{ + default: WorkflowDiagramStepNodeEditable, + 'create-step': WorkflowDiagramCreateStepNode, + 'empty-trigger': WorkflowDiagramEmptyTrigger, + }} + > + <WorkflowDiagramCanvasEditableEffect /> + </WorkflowDiagramCanvasBase> + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasEffect.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasEditableEffect.tsx similarity index 62% rename from packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasEffect.tsx rename to packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasEditableEffect.tsx index 4f6bfde09488..ad383a527b8c 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasEffect.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasEditableEffect.tsx @@ -1,33 +1,20 @@ import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; import { useStartNodeCreation } from '@/workflow/hooks/useStartNodeCreation'; -import { workflowDiagramTriggerNodeSelectionState } from '@/workflow/states/workflowDiagramTriggerNodeSelectionState'; +import { useTriggerNodeSelection } from '@/workflow/hooks/useTriggerNodeSelection'; import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState'; -import { - WorkflowDiagramEdge, - WorkflowDiagramNode, -} from '@/workflow/types/WorkflowDiagram'; -import { - OnSelectionChangeParams, - useOnSelectionChange, - useReactFlow, -} from '@xyflow/react'; -import { useCallback, useEffect } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { WorkflowDiagramNode } from '@/workflow/types/WorkflowDiagram'; +import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react'; +import { useCallback } from 'react'; +import { useSetRecoilState } from 'recoil'; import { isDefined } from 'twenty-ui'; -export const WorkflowDiagramCanvasEffect = () => { - const reactflow = useReactFlow<WorkflowDiagramNode, WorkflowDiagramEdge>(); - +export const WorkflowDiagramCanvasEditableEffect = () => { const { startNodeCreation } = useStartNodeCreation(); const { openRightDrawer, closeRightDrawer } = useRightDrawer(); const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState); - const workflowDiagramTriggerNodeSelection = useRecoilValue( - workflowDiagramTriggerNodeSelectionState, - ); - const handleSelectionChange = useCallback( ({ nodes }: OnSelectionChangeParams) => { const selectedNode = nodes[0] as WorkflowDiagramNode; @@ -65,15 +52,7 @@ export const WorkflowDiagramCanvasEffect = () => { onChange: handleSelectionChange, }); - useEffect(() => { - if (!isDefined(workflowDiagramTriggerNodeSelection)) { - return; - } - - reactflow.updateNode(workflowDiagramTriggerNodeSelection, { - selected: true, - }); - }, [reactflow, workflowDiagramTriggerNodeSelection]); + useTriggerNodeSelection(); return null; }; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasReadonly.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasReadonly.tsx new file mode 100644 index 000000000000..d6c50fa9034e --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasReadonly.tsx @@ -0,0 +1,28 @@ +import { WorkflowDiagramCanvasBase } from '@/workflow/components/WorkflowDiagramCanvasBase'; +import { WorkflowDiagramCanvasReadonlyEffect } from '@/workflow/components/WorkflowDiagramCanvasReadonlyEffect'; +import { WorkflowDiagramEmptyTrigger } from '@/workflow/components/WorkflowDiagramEmptyTrigger'; +import { WorkflowDiagramStepNodeReadonly } from '@/workflow/components/WorkflowDiagramStepNodeReadonly'; +import { WorkflowVersion } from '@/workflow/types/Workflow'; +import { WorkflowDiagram } from '@/workflow/types/WorkflowDiagram'; + +export const WorkflowDiagramCanvasReadonly = ({ + diagram, + workflowVersion, +}: { + diagram: WorkflowDiagram; + workflowVersion: WorkflowVersion; +}) => { + return ( + <WorkflowDiagramCanvasBase + key={workflowVersion.id} + diagram={diagram} + status={workflowVersion.status} + nodeTypes={{ + default: WorkflowDiagramStepNodeReadonly, + 'empty-trigger': WorkflowDiagramEmptyTrigger, + }} + > + <WorkflowDiagramCanvasReadonlyEffect /> + </WorkflowDiagramCanvasBase> + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasReadonlyEffect.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasReadonlyEffect.tsx new file mode 100644 index 000000000000..17cf9ae1df2e --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCanvasReadonlyEffect.tsx @@ -0,0 +1,39 @@ +import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer'; +import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages'; +import { useTriggerNodeSelection } from '@/workflow/hooks/useTriggerNodeSelection'; +import { workflowSelectedNodeState } from '@/workflow/states/workflowSelectedNodeState'; +import { WorkflowDiagramNode } from '@/workflow/types/WorkflowDiagram'; +import { OnSelectionChangeParams, useOnSelectionChange } from '@xyflow/react'; +import { useCallback } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +export const WorkflowDiagramCanvasReadonlyEffect = () => { + const { openRightDrawer, closeRightDrawer } = useRightDrawer(); + const setWorkflowSelectedNode = useSetRecoilState(workflowSelectedNodeState); + + const handleSelectionChange = useCallback( + ({ nodes }: OnSelectionChangeParams) => { + const selectedNode = nodes[0] as WorkflowDiagramNode; + const isClosingStep = isDefined(selectedNode) === false; + + if (isClosingStep) { + closeRightDrawer(); + + return; + } + + setWorkflowSelectedNode(selectedNode.id); + openRightDrawer(RightDrawerPages.WorkflowStepView); + }, + [closeRightDrawer, openRightDrawer, setWorkflowSelectedNode], + ); + + useOnSelectionChange({ + onChange: handleSelectionChange, + }); + + useTriggerNodeSelection(); + + return null; +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCreateStepNode.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCreateStepNode.tsx index 27706668b4b1..2e1b1328a0b1 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCreateStepNode.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramCreateStepNode.tsx @@ -12,7 +12,7 @@ export const WorkflowDiagramCreateStepNode = () => { <> <StyledTargetHandle type="target" position={Position.Top} /> - <IconButton Icon={IconPlus} /> + <IconButton Icon={IconPlus} size="small" /> </> ); }; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramEffect.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramEffect.tsx new file mode 100644 index 000000000000..8b714cc7613c --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramEffect.tsx @@ -0,0 +1,66 @@ +import { getSnapshotValue } from '@/ui/utilities/state/utils/getSnapshotValue'; +import { workflowDiagramState } from '@/workflow/states/workflowDiagramState'; +import { + WorkflowVersion, + WorkflowWithCurrentVersion, +} from '@/workflow/types/Workflow'; + +import { addCreateStepNodes } from '@/workflow/utils/addCreateStepNodes'; +import { getWorkflowVersionDiagram } from '@/workflow/utils/getWorkflowVersionDiagram'; +import { mergeWorkflowDiagrams } from '@/workflow/utils/mergeWorkflowDiagrams'; +import { useEffect } from 'react'; +import { useRecoilCallback, useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +export const WorkflowDiagramEffect = ({ + workflowWithCurrentVersion, +}: { + workflowWithCurrentVersion: WorkflowWithCurrentVersion | undefined; +}) => { + const setWorkflowDiagram = useSetRecoilState(workflowDiagramState); + + const computeAndMergeNewWorkflowDiagram = useRecoilCallback( + ({ snapshot, set }) => { + return (currentVersion: WorkflowVersion) => { + const previousWorkflowDiagram = getSnapshotValue( + snapshot, + workflowDiagramState, + ); + + const nextWorkflowDiagram = getWorkflowVersionDiagram(currentVersion); + + let mergedWorkflowDiagram = nextWorkflowDiagram; + if (isDefined(previousWorkflowDiagram)) { + mergedWorkflowDiagram = mergeWorkflowDiagrams( + previousWorkflowDiagram, + nextWorkflowDiagram, + ); + } + + const workflowDiagramWithCreateStepNodes = addCreateStepNodes( + mergedWorkflowDiagram, + ); + + set(workflowDiagramState, workflowDiagramWithCreateStepNodes); + }; + }, + [], + ); + + useEffect(() => { + const currentVersion = workflowWithCurrentVersion?.currentVersion; + if (!isDefined(currentVersion)) { + setWorkflowDiagram(undefined); + + return; + } + + computeAndMergeNewWorkflowDiagram(currentVersion); + }, [ + computeAndMergeNewWorkflowDiagram, + setWorkflowDiagram, + workflowWithCurrentVersion?.currentVersion, + ]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNode.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeBase.tsx similarity index 53% rename from packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNode.tsx rename to packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeBase.tsx index 0c0e6ce945ea..0fc4d8591051 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNode.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeBase.tsx @@ -1,8 +1,9 @@ import { WorkflowDiagramBaseStepNode } from '@/workflow/components/WorkflowDiagramBaseStepNode'; import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram'; +import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { IconCode, IconPlaylistAdd } from 'twenty-ui'; +import { IconCode, IconMail, IconPlaylistAdd } from 'twenty-ui'; const StyledStepNodeLabelIconContainer = styled.div` align-items: center; @@ -13,10 +14,12 @@ const StyledStepNodeLabelIconContainer = styled.div` padding: ${({ theme }) => theme.spacing(1)}; `; -export const WorkflowDiagramStepNode = ({ +export const WorkflowDiagramStepNodeBase = ({ data, + RightFloatingElement, }: { data: WorkflowDiagramStepNodeData; + RightFloatingElement?: React.ReactNode; }) => { const theme = useTheme(); @@ -32,16 +35,33 @@ export const WorkflowDiagramStepNode = ({ </StyledStepNodeLabelIconContainer> ); } + case 'condition': { + return null; + } case 'action': { - return ( - <StyledStepNodeLabelIconContainer> - <IconCode size={theme.icon.size.sm} color={theme.color.orange} /> - </StyledStepNodeLabelIconContainer> - ); + switch (data.actionType) { + case 'CODE': { + return ( + <StyledStepNodeLabelIconContainer> + <IconCode + size={theme.icon.size.sm} + color={theme.color.orange} + /> + </StyledStepNodeLabelIconContainer> + ); + } + case 'SEND_EMAIL': { + return ( + <StyledStepNodeLabelIconContainer> + <IconMail size={theme.icon.size.sm} color={theme.color.blue} /> + </StyledStepNodeLabelIconContainer> + ); + } + } } } - return null; + return assertUnreachable(data); }; return ( @@ -49,6 +69,7 @@ export const WorkflowDiagramStepNode = ({ nodeType={data.nodeType} label={data.label} Icon={renderStepIcon()} + RightFloatingElement={RightFloatingElement} /> ); }; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeEditable.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeEditable.tsx new file mode 100644 index 000000000000..cb8290fd732f --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeEditable.tsx @@ -0,0 +1,45 @@ +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'; +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'; + +export const WorkflowDiagramStepNodeEditable = ({ + id, + data, + selected, +}: { + id: string; + data: WorkflowDiagramStepNodeData; + selected?: boolean; +}) => { + const workflowId = useRecoilValue(workflowIdState); + + const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId); + assertWorkflowWithCurrentVersionIsDefined(workflowWithCurrentVersion); + + const { deleteOneStep } = useDeleteOneStep({ + workflow: workflowWithCurrentVersion, + stepId: id, + }); + + return ( + <WorkflowDiagramStepNodeBase + data={data} + RightFloatingElement={ + selected ? ( + <FloatingIconButton + Icon={IconTrash} + onClick={() => { + return deleteOneStep(); + }} + /> + ) : undefined + } + /> + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeReadonly.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeReadonly.tsx new file mode 100644 index 000000000000..054a9b7a8d89 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowDiagramStepNodeReadonly.tsx @@ -0,0 +1,10 @@ +import { WorkflowDiagramStepNodeBase } from '@/workflow/components/WorkflowDiagramStepNodeBase'; +import { WorkflowDiagramStepNodeData } from '@/workflow/types/WorkflowDiagram'; + +export const WorkflowDiagramStepNodeReadonly = ({ + data, +}: { + data: WorkflowDiagramStepNodeData; +}) => { + return <WorkflowDiagramStepNodeBase data={data} />; +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionForm.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionForm.tsx deleted file mode 100644 index 015952309d11..000000000000 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionForm.tsx +++ /dev/null @@ -1,103 +0,0 @@ -import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions'; -import { Select, SelectOption } from '@/ui/input/components/Select'; -import { WorkflowAction } from '@/workflow/types/Workflow'; -import { useTheme } from '@emotion/react'; -import styled from '@emotion/styled'; -import { IconCode, isDefined } from 'twenty-ui'; - -const StyledTriggerHeader = styled.div` - background-color: ${({ theme }) => theme.background.secondary}; - border-bottom: 1px solid ${({ theme }) => theme.border.color.medium}; - display: flex; - flex-direction: column; - padding: ${({ theme }) => theme.spacing(6)}; -`; - -const StyledTriggerHeaderTitle = styled.p` - color: ${({ theme }) => theme.font.color.primary}; - font-weight: ${({ theme }) => theme.font.weight.semiBold}; - font-size: ${({ theme }) => theme.font.size.xl}; - - margin: ${({ theme }) => theme.spacing(3)} 0; -`; - -const StyledTriggerHeaderType = styled.p` - color: ${({ theme }) => theme.font.color.tertiary}; - margin: 0; -`; - -const StyledTriggerHeaderIconContainer = styled.div` - align-self: flex-start; - display: flex; - justify-content: center; - align-items: center; - background-color: ${({ theme }) => theme.background.transparent.light}; - border-radius: ${({ theme }) => theme.border.radius.xs}; - padding: ${({ theme }) => theme.spacing(1)}; -`; - -const StyledTriggerSettings = styled.div` - padding: ${({ theme }) => theme.spacing(6)}; - display: flex; - flex-direction: column; - row-gap: ${({ theme }) => theme.spacing(4)}; -`; - -export const WorkflowEditActionForm = ({ - action, - onActionUpdate, -}: { - action: WorkflowAction; - onActionUpdate: (trigger: WorkflowAction) => void; -}) => { - const theme = useTheme(); - - const { serverlessFunctions } = useGetManyServerlessFunctions(); - - const availableFunctions: Array<SelectOption<string>> = [ - { label: 'None', value: '' }, - ...serverlessFunctions - .filter((serverlessFunction) => - isDefined(serverlessFunction.latestVersion), - ) - .map((serverlessFunction) => ({ - label: serverlessFunction.name, - value: serverlessFunction.id, - })), - ]; - - return ( - <> - <StyledTriggerHeader> - <StyledTriggerHeaderIconContainer> - <IconCode color={theme.color.orange} /> - </StyledTriggerHeaderIconContainer> - - <StyledTriggerHeaderTitle> - Code - Serverless Function - </StyledTriggerHeaderTitle> - - <StyledTriggerHeaderType>Code</StyledTriggerHeaderType> - </StyledTriggerHeader> - - <StyledTriggerSettings> - <Select - dropdownId="workflow-edit-action-function" - label="Function" - fullWidth - value={action.settings.serverlessFunctionId} - options={availableFunctions} - onChange={(updatedFunction) => { - onActionUpdate({ - ...action, - settings: { - ...action.settings, - serverlessFunctionId: updatedFunction, - }, - }); - }} - /> - </StyledTriggerSettings> - </> - ); -}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormBase.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormBase.tsx new file mode 100644 index 000000000000..77a50896c015 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormBase.tsx @@ -0,0 +1,61 @@ +import styled from '@emotion/styled'; +import React from 'react'; + +const StyledTriggerHeader = styled.div` + background-color: ${({ theme }) => theme.background.secondary}; + border-bottom: 1px solid ${({ theme }) => theme.border.color.medium}; + display: flex; + flex-direction: column; + padding: ${({ theme }) => theme.spacing(6)}; +`; + +const StyledTriggerHeaderTitle = styled.p` + color: ${({ theme }) => theme.font.color.primary}; + font-weight: ${({ theme }) => theme.font.weight.semiBold}; + font-size: ${({ theme }) => theme.font.size.xl}; + + margin: ${({ theme }) => theme.spacing(3)} 0; +`; + +const StyledTriggerHeaderType = styled.p` + color: ${({ theme }) => theme.font.color.tertiary}; + margin: 0; +`; + +const StyledTriggerHeaderIconContainer = styled.div` + align-self: flex-start; + display: flex; + justify-content: center; + align-items: center; + background-color: ${({ theme }) => theme.background.transparent.light}; + border-radius: ${({ theme }) => theme.border.radius.xs}; + padding: ${({ theme }) => theme.spacing(1)}; +`; + +export const WorkflowEditActionFormBase = ({ + ActionIcon, + actionTitle, + actionType, + children, +}: { + ActionIcon: React.ReactNode; + actionTitle: string; + actionType: string; + children: React.ReactNode; +}) => { + return ( + <> + <StyledTriggerHeader> + <StyledTriggerHeaderIconContainer> + {ActionIcon} + </StyledTriggerHeaderIconContainer> + + <StyledTriggerHeaderTitle>{actionTitle}</StyledTriggerHeaderTitle> + + <StyledTriggerHeaderType>{actionType}</StyledTriggerHeaderType> + </StyledTriggerHeader> + + {children} + </> + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx new file mode 100644 index 000000000000..be2a495202ac --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormSendEmail.tsx @@ -0,0 +1,237 @@ +import { TextArea } from '@/ui/input/components/TextArea'; +import { TextInput } from '@/ui/input/components/TextInput'; +import { WorkflowEditActionFormBase } from '@/workflow/components/WorkflowEditActionFormBase'; +import { WorkflowSendEmailStep } from '@/workflow/types/Workflow'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import React, { useEffect } from 'react'; +import { Controller, useForm } from 'react-hook-form'; +import { IconMail, IconPlus, isDefined } from 'twenty-ui'; +import { useDebouncedCallback } from 'use-debounce'; +import { Select, SelectOption } from '@/ui/input/components/Select'; +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { ConnectedAccount } from '@/accounts/types/ConnectedAccount'; +import { useRecoilValue } from 'recoil'; +import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; +import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth'; +import { workflowIdState } from '@/workflow/states/workflowIdState'; +import { GMAIL_SEND_SCOPE } from '@/accounts/constants/GmailSendScope'; + +const StyledTriggerSettings = styled.div` + padding: ${({ theme }) => theme.spacing(6)}; + display: flex; + flex-direction: column; + row-gap: ${({ theme }) => theme.spacing(4)}; +`; + +type WorkflowEditActionFormSendEmailProps = + | { + action: WorkflowSendEmailStep; + readonly: true; + } + | { + action: WorkflowSendEmailStep; + readonly?: false; + onActionUpdate: (action: WorkflowSendEmailStep) => void; + }; + +type SendEmailFormData = { + connectedAccountId: string; + subject: string; + body: string; +}; + +export const WorkflowEditActionFormSendEmail = ( + props: WorkflowEditActionFormSendEmailProps, +) => { + const theme = useTheme(); + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth(); + const workflowId = useRecoilValue(workflowIdState); + const redirectUrl = `/object/workflow/${workflowId}`; + + const form = useForm<SendEmailFormData>({ + defaultValues: { + connectedAccountId: '', + subject: '', + body: '', + }, + disabled: props.readonly, + }); + + const checkConnectedAccountScopes = async ( + connectedAccountId: string | null, + ) => { + const connectedAccount = accounts.find( + (account) => account.id === connectedAccountId, + ); + if (!isDefined(connectedAccount)) { + return; + } + const scopes = connectedAccount.scopes; + if ( + !isDefined(scopes) || + !isDefined(scopes.find((scope) => scope === GMAIL_SEND_SCOPE)) + ) { + await triggerGoogleApisOAuth({ + redirectLocation: redirectUrl, + loginHint: connectedAccount.handle, + }); + } + }; + + useEffect(() => { + form.setValue( + 'connectedAccountId', + props.action.settings.connectedAccountId ?? '', + ); + form.setValue('subject', props.action.settings.subject ?? ''); + form.setValue('body', props.action.settings.body ?? ''); + }, [props.action.settings, form]); + + const saveAction = useDebouncedCallback( + async (formData: SendEmailFormData, checkScopes = false) => { + if (props.readonly === true) { + return; + } + + props.onActionUpdate({ + ...props.action, + settings: { + ...props.action.settings, + connectedAccountId: formData.connectedAccountId, + subject: formData.subject, + body: formData.body, + }, + }); + + if (checkScopes === true) { + await checkConnectedAccountScopes(formData.connectedAccountId); + } + }, + 1_000, + ); + + useEffect(() => { + return () => { + saveAction.flush(); + }; + }, [saveAction]); + + const handleSave = (checkScopes = false) => + form.handleSubmit((formData: SendEmailFormData) => + saveAction(formData, checkScopes), + )(); + + const filter: { or: object[] } = { + or: [ + { + accountOwnerId: { + eq: currentWorkspaceMember?.id, + }, + }, + ], + }; + + if ( + isDefined(props.action.settings.connectedAccountId) && + props.action.settings.connectedAccountId !== '' + ) { + filter.or.push({ + id: { + eq: props.action.settings.connectedAccountId, + }, + }); + } + + const { records: accounts, loading } = useFindManyRecords<ConnectedAccount>({ + objectNameSingular: 'connectedAccount', + filter, + }); + + let emptyOption: SelectOption<string | null> = { label: 'None', value: null }; + const connectedAccountOptions: SelectOption<string | null>[] = []; + + accounts.forEach((account) => { + const selectOption = { + label: account.handle, + value: account.id, + }; + if (account.accountOwnerId === currentWorkspaceMember?.id) { + connectedAccountOptions.push(selectOption); + } else { + // This handle the case when the current connected account does not belong to the currentWorkspaceMember + // In that case, current connected account email is displayed, but cannot be selected + emptyOption = selectOption; + } + }); + + return ( + !loading && ( + <WorkflowEditActionFormBase + ActionIcon={<IconMail color={theme.color.blue} />} + actionTitle="Send Email" + actionType="Email" + > + <StyledTriggerSettings> + <Controller + name="connectedAccountId" + control={form.control} + render={({ field }) => ( + <Select + dropdownId="select-connected-account-id" + label="Account" + fullWidth + emptyOption={emptyOption} + value={field.value} + options={connectedAccountOptions} + callToActionButton={{ + onClick: () => + triggerGoogleApisOAuth({ redirectLocation: redirectUrl }), + Icon: IconPlus, + text: 'Add account', + }} + onChange={(connectedAccountId) => { + field.onChange(connectedAccountId); + handleSave(true); + }} + /> + )} + /> + <Controller + name="subject" + control={form.control} + render={({ field }) => ( + <TextInput + label="Subject" + placeholder="Enter email subject (use {{variable}} for dynamic content)" + value={field.value} + onChange={(email) => { + field.onChange(email); + handleSave(); + }} + /> + )} + /> + + <Controller + name="body" + control={form.control} + render={({ field }) => ( + <TextArea + label="Body" + placeholder="Enter email body (use {{variable}} for dynamic content)" + value={field.value} + minRows={4} + onChange={(email) => { + field.onChange(email); + handleSave(); + }} + /> + )} + /> + </StyledTriggerSettings> + </WorkflowEditActionFormBase> + ) + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormServerlessFunction.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormServerlessFunction.tsx new file mode 100644 index 000000000000..2ff3847bfd83 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditActionFormServerlessFunction.tsx @@ -0,0 +1,77 @@ +import { useGetManyServerlessFunctions } from '@/settings/serverless-functions/hooks/useGetManyServerlessFunctions'; +import { Select, SelectOption } from '@/ui/input/components/Select'; +import { WorkflowEditActionFormBase } from '@/workflow/components/WorkflowEditActionFormBase'; +import { WorkflowCodeStep } from '@/workflow/types/Workflow'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { IconCode, isDefined } from 'twenty-ui'; + +const StyledTriggerSettings = styled.div` + padding: ${({ theme }) => theme.spacing(6)}; + display: flex; + flex-direction: column; + row-gap: ${({ theme }) => theme.spacing(4)}; +`; + +type WorkflowEditActionFormServerlessFunctionProps = + | { + action: WorkflowCodeStep; + readonly: true; + } + | { + action: WorkflowCodeStep; + readonly?: false; + onActionUpdate: (action: WorkflowCodeStep) => void; + }; + +export const WorkflowEditActionFormServerlessFunction = ( + props: WorkflowEditActionFormServerlessFunctionProps, +) => { + const theme = useTheme(); + + const { serverlessFunctions } = useGetManyServerlessFunctions(); + + const availableFunctions: Array<SelectOption<string>> = [ + { label: 'None', value: '' }, + ...serverlessFunctions + .filter((serverlessFunction) => + isDefined(serverlessFunction.latestVersion), + ) + .map((serverlessFunction) => ({ + label: serverlessFunction.name, + value: serverlessFunction.id, + })), + ]; + + return ( + <WorkflowEditActionFormBase + ActionIcon={<IconCode color={theme.color.orange} />} + actionTitle="Code - Serverless Function" + actionType="Code" + > + <StyledTriggerSettings> + <Select + dropdownId="workflow-edit-action-function" + label="Function" + fullWidth + value={props.action.settings.serverlessFunctionId} + options={availableFunctions} + disabled={props.readonly} + onChange={(updatedFunction) => { + if (props.readonly === true) { + return; + } + + props.onActionUpdate({ + ...props.action, + settings: { + ...props.action.settings, + serverlessFunctionId: updatedFunction, + }, + }); + }} + /> + </StyledTriggerSettings> + </WorkflowEditActionFormBase> + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerForm.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerForm.tsx index 9a3960428162..0a2d8eeb8749 100644 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerForm.tsx +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowEditTriggerForm.tsx @@ -45,13 +45,23 @@ const StyledTriggerSettings = styled.div` row-gap: ${({ theme }) => theme.spacing(4)}; `; +type WorkflowEditTriggerFormProps = + | { + trigger: WorkflowTrigger | undefined; + readonly: true; + onTriggerUpdate?: undefined; + } + | { + trigger: WorkflowTrigger | undefined; + readonly?: false; + onTriggerUpdate: (trigger: WorkflowTrigger) => void; + }; + export const WorkflowEditTriggerForm = ({ trigger, + readonly, onTriggerUpdate, -}: { - trigger: WorkflowTrigger | undefined; - onTriggerUpdate: (trigger: WorkflowTrigger) => void; -}) => { +}: WorkflowEditTriggerFormProps) => { const theme = useTheme(); const { activeObjectMetadataItems } = useFilteredObjectMetadataItems(); @@ -102,9 +112,14 @@ export const WorkflowEditTriggerForm = ({ dropdownId="workflow-edit-trigger-record-type" label="Record Type" fullWidth + disabled={readonly} value={triggerEvent?.objectType} options={availableMetadata} onChange={(updatedRecordType) => { + if (readonly === true) { + return; + } + onTriggerUpdate( isDefined(trigger) && isDefined(triggerEvent) ? { @@ -129,7 +144,12 @@ export const WorkflowEditTriggerForm = ({ fullWidth value={triggerEvent?.event} options={OBJECT_EVENT_TRIGGERS} + disabled={readonly} onChange={(updatedEvent) => { + if (readonly === true) { + return; + } + onTriggerUpdate( isDefined(trigger) && isDefined(triggerEvent) ? { diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowEffect.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowEffect.tsx deleted file mode 100644 index 6171f2d64b15..000000000000 --- a/packages/twenty-front/src/modules/workflow/components/WorkflowEffect.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { workflowDiagramState } from '@/workflow/states/workflowDiagramState'; -import { workflowIdState } from '@/workflow/states/workflowIdState'; -import { WorkflowWithCurrentVersion } from '@/workflow/types/Workflow'; -import { addCreateStepNodes } from '@/workflow/utils/addCreateStepNodes'; -import { getWorkflowVersionDiagram } from '@/workflow/utils/getWorkflowVersionDiagram'; -import { useEffect } from 'react'; -import { useSetRecoilState } from 'recoil'; -import { isDefined } from 'twenty-ui'; - -type WorkflowEffectProps = { - workflowId: string; - workflowWithCurrentVersion: WorkflowWithCurrentVersion | undefined; -}; - -export const WorkflowEffect = ({ - workflowId, - workflowWithCurrentVersion, -}: WorkflowEffectProps) => { - const setWorkflowId = useSetRecoilState(workflowIdState); - const setWorkflowDiagram = useSetRecoilState(workflowDiagramState); - - useEffect(() => { - setWorkflowId(workflowId); - }, [setWorkflowId, workflowId]); - - useEffect(() => { - const currentVersion = workflowWithCurrentVersion?.currentVersion; - if (!isDefined(currentVersion)) { - setWorkflowDiagram(undefined); - - return; - } - - const lastWorkflowDiagram = getWorkflowVersionDiagram(currentVersion); - const workflowDiagramWithCreateStepNodes = - addCreateStepNodes(lastWorkflowDiagram); - - setWorkflowDiagram(workflowDiagramWithCreateStepNodes); - }, [setWorkflowDiagram, workflowWithCurrentVersion?.currentVersion]); - - return null; -}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowStepDetail.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowStepDetail.tsx new file mode 100644 index 000000000000..fa6af9f7a47f --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowStepDetail.tsx @@ -0,0 +1,80 @@ +import { WorkflowEditActionFormSendEmail } from '@/workflow/components/WorkflowEditActionFormSendEmail'; +import { WorkflowEditActionFormServerlessFunction } from '@/workflow/components/WorkflowEditActionFormServerlessFunction'; +import { WorkflowEditTriggerForm } from '@/workflow/components/WorkflowEditTriggerForm'; +import { + WorkflowAction, + WorkflowTrigger, + WorkflowVersion, +} from '@/workflow/types/Workflow'; +import { assertUnreachable } from '@/workflow/utils/assertUnreachable'; +import { getStepDefinitionOrThrow } from '@/workflow/utils/getStepDefinitionOrThrow'; +import { isDefined } from 'twenty-ui'; + +type WorkflowStepDetailProps = + | { + stepId: string; + workflowVersion: WorkflowVersion; + readonly: true; + onTriggerUpdate?: undefined; + onActionUpdate?: undefined; + } + | { + stepId: string; + workflowVersion: WorkflowVersion; + readonly?: false; + onTriggerUpdate: (trigger: WorkflowTrigger) => void; + onActionUpdate: (action: WorkflowAction) => void; + }; + +export const WorkflowStepDetail = ({ + stepId, + workflowVersion, + ...props +}: WorkflowStepDetailProps) => { + const stepDefinition = getStepDefinitionOrThrow({ + stepId, + workflowVersion, + }); + if (!isDefined(stepDefinition)) { + return null; + } + + switch (stepDefinition.type) { + case 'trigger': { + return ( + <WorkflowEditTriggerForm + trigger={stepDefinition.definition} + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + /> + ); + } + case 'action': { + switch (stepDefinition.definition.type) { + case 'CODE': { + return ( + <WorkflowEditActionFormServerlessFunction + action={stepDefinition.definition} + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + /> + ); + } + case 'SEND_EMAIL': { + return ( + <WorkflowEditActionFormSendEmail + action={stepDefinition.definition} + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + /> + ); + } + } + } + } + + return assertUnreachable( + stepDefinition, + `Expected the step to have an handler; ${JSON.stringify(stepDefinition)}`, + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowVersionVisualizer.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowVersionVisualizer.tsx new file mode 100644 index 000000000000..966688a7fa44 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowVersionVisualizer.tsx @@ -0,0 +1,23 @@ +import { WorkflowDiagramCanvasReadonly } from '@/workflow/components/WorkflowDiagramCanvasReadonly'; +import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion'; +import { workflowDiagramState } from '@/workflow/states/workflowDiagramState'; +import '@xyflow/react/dist/style.css'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +export const WorkflowVersionVisualizer = ({ + workflowVersionId, +}: { + workflowVersionId: string; +}) => { + const workflowVersion = useWorkflowVersion(workflowVersionId); + + const workflowDiagram = useRecoilValue(workflowDiagramState); + + return isDefined(workflowDiagram) && isDefined(workflowVersion) ? ( + <WorkflowDiagramCanvasReadonly + diagram={workflowDiagram} + workflowVersion={workflowVersion} + /> + ) : null; +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowVersionVisualizerEffect.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowVersionVisualizerEffect.tsx new file mode 100644 index 000000000000..270c5c944015 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowVersionVisualizerEffect.tsx @@ -0,0 +1,36 @@ +import { useWorkflowVersion } from '@/workflow/hooks/useWorkflowVersion'; +import { workflowDiagramState } from '@/workflow/states/workflowDiagramState'; +import { workflowVersionIdState } from '@/workflow/states/workflowVersionIdState'; +import { getWorkflowVersionDiagram } from '@/workflow/utils/getWorkflowVersionDiagram'; +import { useEffect } from 'react'; +import { useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +export const WorkflowVersionVisualizerEffect = ({ + workflowVersionId, +}: { + workflowVersionId: string; +}) => { + const workflowVersion = useWorkflowVersion(workflowVersionId); + + const setWorkflowVersionId = useSetRecoilState(workflowVersionIdState); + const setWorkflowDiagram = useSetRecoilState(workflowDiagramState); + + useEffect(() => { + setWorkflowVersionId(workflowVersionId); + }, [setWorkflowVersionId, workflowVersionId]); + + useEffect(() => { + if (!isDefined(workflowVersion)) { + setWorkflowDiagram(undefined); + + return; + } + + const nextWorkflowDiagram = getWorkflowVersionDiagram(workflowVersion); + + setWorkflowDiagram(nextWorkflowDiagram); + }, [setWorkflowDiagram, workflowVersion]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowVisualizer.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowVisualizer.tsx new file mode 100644 index 000000000000..20099a25538d --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowVisualizer.tsx @@ -0,0 +1,34 @@ +import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity'; +import { WorkflowDiagramCanvasEditable } from '@/workflow/components/WorkflowDiagramCanvasEditable'; +import { WorkflowDiagramEffect } from '@/workflow/components/WorkflowDiagramEffect'; +import { useWorkflowWithCurrentVersion } from '@/workflow/hooks/useWorkflowWithCurrentVersion'; +import { workflowDiagramState } from '@/workflow/states/workflowDiagramState'; +import '@xyflow/react/dist/style.css'; +import { useRecoilValue } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +export const WorkflowVisualizer = ({ + targetableObject, +}: { + targetableObject: ActivityTargetableObject; +}) => { + const workflowId = targetableObject.id; + + const workflowWithCurrentVersion = useWorkflowWithCurrentVersion(workflowId); + const workflowDiagram = useRecoilValue(workflowDiagramState); + + return ( + <> + <WorkflowDiagramEffect + workflowWithCurrentVersion={workflowWithCurrentVersion} + /> + + {isDefined(workflowDiagram) && isDefined(workflowWithCurrentVersion) ? ( + <WorkflowDiagramCanvasEditable + diagram={workflowDiagram} + workflowWithCurrentVersion={workflowWithCurrentVersion} + /> + ) : null} + </> + ); +}; diff --git a/packages/twenty-front/src/modules/workflow/components/WorkflowVisualizerEffect.tsx b/packages/twenty-front/src/modules/workflow/components/WorkflowVisualizerEffect.tsx new file mode 100644 index 000000000000..f3f7ed097650 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/components/WorkflowVisualizerEffect.tsx @@ -0,0 +1,17 @@ +import { workflowIdState } from '@/workflow/states/workflowIdState'; +import { useEffect } from 'react'; +import { useSetRecoilState } from 'recoil'; + +export const WorkflowVisualizerEffect = ({ + workflowId, +}: { + workflowId: string; +}) => { + const setWorkflowId = useSetRecoilState(workflowIdState); + + useEffect(() => { + setWorkflowId(workflowId); + }, [setWorkflowId, workflowId]); + + return null; +}; diff --git a/packages/twenty-front/src/modules/workflow/constants/Actions.ts b/packages/twenty-front/src/modules/workflow/constants/Actions.ts index 53c988420e59..b3415d391f98 100644 --- a/packages/twenty-front/src/modules/workflow/constants/Actions.ts +++ b/packages/twenty-front/src/modules/workflow/constants/Actions.ts @@ -11,4 +11,9 @@ export const ACTIONS: Array<{ type: 'CODE', icon: IconSettingsAutomation, }, + { + label: 'Send Email', + type: 'SEND_EMAIL', + icon: IconSettingsAutomation, + }, ]; diff --git a/packages/twenty-front/src/modules/workflow/hooks/useCreateNewWorkflowVersion.tsx b/packages/twenty-front/src/modules/workflow/hooks/useCreateNewWorkflowVersion.tsx index 3935f1fe9198..5426c8a1bb9a 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useCreateNewWorkflowVersion.tsx +++ b/packages/twenty-front/src/modules/workflow/hooks/useCreateNewWorkflowVersion.tsx @@ -2,11 +2,7 @@ import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSi import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { WorkflowVersion } from '@/workflow/types/Workflow'; -export const useCreateNewWorkflowVersion = ({ - workflowId, -}: { - workflowId: string; -}) => { +export const useCreateNewWorkflowVersion = () => { const { createOneRecord: createOneWorkflowVersion } = useCreateOneRecord<WorkflowVersion>({ objectNameSingular: CoreObjectNameSingular.WorkflowVersion, @@ -15,13 +11,10 @@ export const useCreateNewWorkflowVersion = ({ const createNewWorkflowVersion = ( workflowVersionData: Pick< WorkflowVersion, - 'name' | 'status' | 'trigger' | 'steps' + 'workflowId' | 'name' | 'status' | 'trigger' | 'steps' >, ) => { - return createOneWorkflowVersion({ - workflowId, - ...workflowVersionData, - }); + return createOneWorkflowVersion(workflowVersionData); }; return { diff --git a/packages/twenty-front/src/modules/workflow/hooks/useCreateStep.tsx b/packages/twenty-front/src/modules/workflow/hooks/useCreateStep.tsx index 6a11b1f42d52..e439edc2d883 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useCreateStep.tsx +++ b/packages/twenty-front/src/modules/workflow/hooks/useCreateStep.tsx @@ -32,9 +32,7 @@ export const useCreateStep = ({ objectNameSingular: CoreObjectNameSingular.WorkflowVersion, }); - const { createNewWorkflowVersion } = useCreateNewWorkflowVersion({ - workflowId: workflow.id, - }); + const { createNewWorkflowVersion } = useCreateNewWorkflowVersion(); const insertNodeAndSave = async ({ parentNodeId, @@ -66,6 +64,7 @@ export const useCreateStep = ({ } await createNewWorkflowVersion({ + workflowId: workflow.id, name: `v${workflow.versions.length + 1}`, status: 'DRAFT', trigger: workflow.currentVersion.trigger, diff --git a/packages/twenty-front/src/modules/workflow/hooks/useDeleteOneStep.tsx b/packages/twenty-front/src/modules/workflow/hooks/useDeleteOneStep.tsx new file mode 100644 index 000000000000..159a12958be0 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/hooks/useDeleteOneStep.tsx @@ -0,0 +1,76 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId'; +import { useCreateNewWorkflowVersion } from '@/workflow/hooks/useCreateNewWorkflowVersion'; +import { + WorkflowVersion, + WorkflowWithCurrentVersion, +} from '@/workflow/types/Workflow'; +import { removeStep } from '@/workflow/utils/removeStep'; + +export const useDeleteOneStep = ({ + stepId, + workflow, +}: { + stepId: string; + workflow: WorkflowWithCurrentVersion; +}) => { + const { updateOneRecord: updateOneWorkflowVersion } = + useUpdateOneRecord<WorkflowVersion>({ + objectNameSingular: CoreObjectNameSingular.WorkflowVersion, + }); + + const { createNewWorkflowVersion } = useCreateNewWorkflowVersion(); + + const deleteOneStep = async () => { + if (workflow.currentVersion.status !== 'DRAFT') { + const newVersionName = `v${workflow.versions.length + 1}`; + + if (stepId === TRIGGER_STEP_ID) { + await createNewWorkflowVersion({ + workflowId: workflow.id, + name: newVersionName, + status: 'DRAFT', + trigger: null, + steps: workflow.currentVersion.steps, + }); + } else { + await createNewWorkflowVersion({ + workflowId: workflow.id, + name: newVersionName, + status: 'DRAFT', + trigger: workflow.currentVersion.trigger, + steps: removeStep({ + steps: workflow.currentVersion.steps ?? [], + stepId, + }), + }); + } + + return; + } + + if (stepId === TRIGGER_STEP_ID) { + await updateOneWorkflowVersion({ + idToUpdate: workflow.currentVersion.id, + updateOneRecordInput: { + trigger: null, + }, + }); + } else { + await updateOneWorkflowVersion({ + idToUpdate: workflow.currentVersion.id, + updateOneRecordInput: { + steps: removeStep({ + steps: workflow.currentVersion.steps ?? [], + stepId, + }), + }, + }); + } + }; + + return { + deleteOneStep, + }; +}; diff --git a/packages/twenty-front/src/modules/workflow/hooks/useTriggerNodeSelection.tsx b/packages/twenty-front/src/modules/workflow/hooks/useTriggerNodeSelection.tsx new file mode 100644 index 000000000000..3a2a80a27ce6 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/hooks/useTriggerNodeSelection.tsx @@ -0,0 +1,36 @@ +import { workflowDiagramTriggerNodeSelectionState } from '@/workflow/states/workflowDiagramTriggerNodeSelectionState'; +import { + WorkflowDiagramEdge, + WorkflowDiagramNode, +} from '@/workflow/types/WorkflowDiagram'; +import { useReactFlow } from '@xyflow/react'; +import { useEffect } from 'react'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; +import { isDefined } from 'twenty-ui'; + +export const useTriggerNodeSelection = () => { + const reactflow = useReactFlow<WorkflowDiagramNode, WorkflowDiagramEdge>(); + + const workflowDiagramTriggerNodeSelection = useRecoilValue( + workflowDiagramTriggerNodeSelectionState, + ); + const setWorkflowDiagramTriggerNodeSelection = useSetRecoilState( + workflowDiagramTriggerNodeSelectionState, + ); + + useEffect(() => { + if (!isDefined(workflowDiagramTriggerNodeSelection)) { + return; + } + + reactflow.updateNode(workflowDiagramTriggerNodeSelection, { + selected: true, + }); + + setWorkflowDiagramTriggerNodeSelection(undefined); + }, [ + reactflow, + setWorkflowDiagramTriggerNodeSelection, + workflowDiagramTriggerNodeSelection, + ]); +}; diff --git a/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionStep.tsx b/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionStep.tsx index 91d9cd491baf..a3696eb4114f 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionStep.tsx +++ b/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionStep.tsx @@ -21,11 +21,9 @@ export const useUpdateWorkflowVersionStep = ({ objectNameSingular: CoreObjectNameSingular.WorkflowVersion, }); - const { createNewWorkflowVersion } = useCreateNewWorkflowVersion({ - workflowId: workflow.id, - }); + const { createNewWorkflowVersion } = useCreateNewWorkflowVersion(); - const updateStep = async (updatedStep: WorkflowStep) => { + const updateStep = async <T extends WorkflowStep>(updatedStep: T) => { if (!isDefined(workflow.currentVersion)) { throw new Error('Can not update an undefined workflow version.'); } @@ -48,6 +46,7 @@ export const useUpdateWorkflowVersionStep = ({ } await createNewWorkflowVersion({ + workflowId: workflow.id, name: `v${workflow.versions.length + 1}`, status: 'DRAFT', trigger: workflow.currentVersion.trigger, diff --git a/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionTrigger.tsx b/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionTrigger.tsx index ba9e28367287..e6703dae839e 100644 --- a/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionTrigger.tsx +++ b/packages/twenty-front/src/modules/workflow/hooks/useUpdateWorkflowVersionTrigger.tsx @@ -18,9 +18,7 @@ export const useUpdateWorkflowVersionTrigger = ({ objectNameSingular: CoreObjectNameSingular.WorkflowVersion, }); - const { createNewWorkflowVersion } = useCreateNewWorkflowVersion({ - workflowId: workflow.id, - }); + const { createNewWorkflowVersion } = useCreateNewWorkflowVersion(); const updateTrigger = async (updatedTrigger: WorkflowTrigger) => { if (!isDefined(workflow.currentVersion)) { @@ -39,6 +37,7 @@ export const useUpdateWorkflowVersionTrigger = ({ } await createNewWorkflowVersion({ + workflowId: workflow.id, name: `v${workflow.versions.length + 1}`, status: 'DRAFT', trigger: updatedTrigger, diff --git a/packages/twenty-front/src/modules/workflow/hooks/useWorkflowVersion.tsx b/packages/twenty-front/src/modules/workflow/hooks/useWorkflowVersion.tsx new file mode 100644 index 000000000000..d16da69838d3 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/hooks/useWorkflowVersion.tsx @@ -0,0 +1,36 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord'; +import { Workflow, WorkflowVersion } from '@/workflow/types/Workflow'; + +export const useWorkflowVersion = (workflowVersionId: string) => { + const { record: workflowVersion } = useFindOneRecord< + WorkflowVersion & { + workflow: Omit<Workflow, 'versions'> & { + versions: Array<{ __typename: string }>; + }; + } + >({ + objectNameSingular: CoreObjectNameSingular.WorkflowVersion, + objectRecordId: workflowVersionId, + recordGqlFields: { + id: true, + name: true, + createdAt: true, + updatedAt: true, + workflowId: true, + trigger: true, + steps: true, + status: true, + workflow: { + id: true, + name: true, + statuses: true, + versions: { + totalCount: true, + }, + }, + }, + }); + + return workflowVersion; +}; diff --git a/packages/twenty-front/src/modules/workflow/states/workflowVersionIdState.ts b/packages/twenty-front/src/modules/workflow/states/workflowVersionIdState.ts new file mode 100644 index 000000000000..2894697965dd --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/states/workflowVersionIdState.ts @@ -0,0 +1,6 @@ +import { createState } from 'twenty-ui'; + +export const workflowVersionIdState = createState<string | undefined>({ + key: 'workflowVersionIdState', + defaultValue: undefined, +}); diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts index 45500e219e7f..0ed8422846b9 100644 --- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts +++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts @@ -1,4 +1,4 @@ -type WorkflowBaseSettingsType = { +type BaseWorkflowStepSettings = { errorHandlingOptions: { retryOnFailure: { value: boolean; @@ -9,27 +9,38 @@ type WorkflowBaseSettingsType = { }; }; -export type WorkflowCodeSettingsType = WorkflowBaseSettingsType & { +export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & { serverlessFunctionId: string; }; -export type WorkflowActionType = 'CODE'; +export type WorkflowSendEmailStepSettings = BaseWorkflowStepSettings & { + connectedAccountId: string; + subject?: string; + body?: string; +}; -type CommonWorkflowAction = { +type BaseWorkflowStep = { id: string; name: string; valid: boolean; }; -type WorkflowCodeAction = CommonWorkflowAction & { +export type WorkflowCodeStep = BaseWorkflowStep & { type: 'CODE'; - settings: WorkflowCodeSettingsType; + settings: WorkflowCodeStepSettings; +}; + +export type WorkflowSendEmailStep = BaseWorkflowStep & { + type: 'SEND_EMAIL'; + settings: WorkflowSendEmailStepSettings; }; -export type WorkflowAction = WorkflowCodeAction; +export type WorkflowAction = WorkflowCodeStep | WorkflowSendEmailStep; export type WorkflowStep = WorkflowAction; +export type WorkflowActionType = WorkflowAction['type']; + export type WorkflowStepType = WorkflowStep['type']; export type WorkflowTriggerType = 'DATABASE_EVENT'; diff --git a/packages/twenty-front/src/modules/workflow/types/WorkflowDiagram.ts b/packages/twenty-front/src/modules/workflow/types/WorkflowDiagram.ts index 237daab24b93..fdad5d19f785 100644 --- a/packages/twenty-front/src/modules/workflow/types/WorkflowDiagram.ts +++ b/packages/twenty-front/src/modules/workflow/types/WorkflowDiagram.ts @@ -1,3 +1,4 @@ +import { WorkflowActionType } from '@/workflow/types/Workflow'; import { Edge, Node } from '@xyflow/react'; export type WorkflowDiagramNode = Node<WorkflowDiagramNodeData>; @@ -8,10 +9,16 @@ export type WorkflowDiagram = { edges: Array<WorkflowDiagramEdge>; }; -export type WorkflowDiagramStepNodeData = { - nodeType: 'trigger' | 'condition' | 'action'; - label: string; -}; +export type WorkflowDiagramStepNodeData = + | { + nodeType: 'trigger' | 'condition'; + label: string; + } + | { + nodeType: 'action'; + actionType: WorkflowActionType; + label: string; + }; export type WorkflowDiagramCreateStepNodeData = { nodeType: 'create-step'; @@ -21,3 +28,8 @@ export type WorkflowDiagramCreateStepNodeData = { export type WorkflowDiagramNodeData = | WorkflowDiagramStepNodeData | WorkflowDiagramCreateStepNodeData; + +export type WorkflowDiagramNodeType = + | 'default' + | 'empty-trigger' + | 'create-step'; diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts index ee4643fcb20b..ebf4f3c21020 100644 --- a/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts +++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts @@ -72,6 +72,7 @@ describe('generateWorkflowDiagram', () => { for (const [index, step] of steps.entries()) { expect(stepNodes[index].data).toEqual({ nodeType: 'action', + actionType: 'CODE', label: step.name, }); } diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/mergeWorkflowDiagrams.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/mergeWorkflowDiagrams.test.ts new file mode 100644 index 000000000000..38e3baabdca1 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/mergeWorkflowDiagrams.test.ts @@ -0,0 +1,72 @@ +import { WorkflowDiagram } from '@/workflow/types/WorkflowDiagram'; +import { mergeWorkflowDiagrams } from '../mergeWorkflowDiagrams'; + +it('Preserves the properties defined in the previous version but not in the next one', () => { + const previousDiagram: WorkflowDiagram = { + nodes: [ + { + data: { nodeType: 'action', label: '', actionType: 'CODE' }, + id: '1', + position: { x: 0, y: 0 }, + selected: true, + }, + ], + edges: [], + }; + const nextDiagram: WorkflowDiagram = { + nodes: [ + { + data: { nodeType: 'action', label: '', actionType: 'CODE' }, + id: '1', + position: { x: 0, y: 0 }, + }, + ], + edges: [], + }; + + expect(mergeWorkflowDiagrams(previousDiagram, nextDiagram)).toEqual({ + nodes: [ + { + data: { nodeType: 'action', label: '', actionType: 'CODE' }, + id: '1', + position: { x: 0, y: 0 }, + selected: true, + }, + ], + edges: [], + }); +}); + +it('Replaces duplicated properties with the next value', () => { + const previousDiagram: WorkflowDiagram = { + nodes: [ + { + data: { nodeType: 'action', label: '', actionType: 'CODE' }, + id: '1', + position: { x: 0, y: 0 }, + }, + ], + edges: [], + }; + const nextDiagram: WorkflowDiagram = { + nodes: [ + { + data: { nodeType: 'action', label: '2', actionType: 'CODE' }, + id: '1', + position: { x: 0, y: 0 }, + }, + ], + edges: [], + }; + + expect(mergeWorkflowDiagrams(previousDiagram, nextDiagram)).toEqual({ + nodes: [ + { + data: { nodeType: 'action', label: '2', actionType: 'CODE' }, + id: '1', + position: { x: 0, y: 0 }, + }, + ], + edges: [], + }); +}); diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/removeStep.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/removeStep.test.ts new file mode 100644 index 000000000000..01385411bf02 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/removeStep.test.ts @@ -0,0 +1,108 @@ +import { WorkflowStep, WorkflowVersion } from '@/workflow/types/Workflow'; +import { removeStep } from '../removeStep'; + +it('returns a deep copy of the provided steps array instead of mutating it', () => { + const stepToBeRemoved = { + id: 'step-1', + name: '', + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + serverlessFunctionId: 'first', + }, + type: 'CODE', + valid: true, + } satisfies WorkflowStep; + const workflowVersionInitial = { + __typename: 'WorkflowVersion', + status: 'ACTIVE', + createdAt: '', + id: '1', + name: '', + steps: [stepToBeRemoved], + trigger: { + settings: { eventName: 'company.created' }, + type: 'DATABASE_EVENT', + }, + updatedAt: '', + workflowId: '', + } satisfies WorkflowVersion; + + const stepsUpdated = removeStep({ + steps: workflowVersionInitial.steps, + stepId: stepToBeRemoved.id, + }); + + expect(workflowVersionInitial.steps).not.toBe(stepsUpdated); +}); + +it('removes a step in a non-empty steps array', () => { + const stepToBeRemoved: WorkflowStep = { + id: 'step-2', + name: '', + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, + type: 'CODE', + valid: true, + }; + const workflowVersionInitial = { + __typename: 'WorkflowVersion', + status: 'ACTIVE', + createdAt: '', + id: '1', + name: '', + steps: [ + { + id: 'step-1', + name: '', + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, + type: 'CODE', + valid: true, + }, + stepToBeRemoved, + { + id: 'step-3', + name: '', + settings: { + errorHandlingOptions: { + retryOnFailure: { value: true }, + continueOnFailure: { value: false }, + }, + serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997', + }, + type: 'CODE', + valid: true, + }, + ], + trigger: { + settings: { eventName: 'company.created' }, + type: 'DATABASE_EVENT', + }, + updatedAt: '', + workflowId: '', + } satisfies WorkflowVersion; + + const stepsUpdated = removeStep({ + steps: workflowVersionInitial.steps, + stepId: stepToBeRemoved.id, + }); + + const expectedUpdatedSteps: Array<WorkflowStep> = [ + workflowVersionInitial.steps[0], + workflowVersionInitial.steps[2], + ]; + expect(stepsUpdated).toEqual(expectedUpdatedSteps); +}); diff --git a/packages/twenty-front/src/modules/workflow/utils/assertUnreachable.ts b/packages/twenty-front/src/modules/workflow/utils/assertUnreachable.ts new file mode 100644 index 000000000000..042337aeebf2 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/utils/assertUnreachable.ts @@ -0,0 +1,3 @@ +export const assertUnreachable = (x: never, errorMessage?: string): never => { + throw new Error(errorMessage ?? "Didn't expect to get here."); +}; diff --git a/packages/twenty-front/src/modules/workflow/utils/findStepPosition.ts b/packages/twenty-front/src/modules/workflow/utils/findStepPosition.ts new file mode 100644 index 000000000000..3ae23dff0283 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/utils/findStepPosition.ts @@ -0,0 +1,41 @@ +import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId'; +import { WorkflowStep } from '@/workflow/types/Workflow'; +import { isDefined } from 'twenty-ui'; + +/** + * This function returns the reference of the array where the step should be positioned + * and at which index. + */ +export const findStepPosition = ({ + steps, + stepId, +}: { + steps: Array<WorkflowStep>; + stepId: string | undefined; +}): { steps: Array<WorkflowStep>; index: number } | undefined => { + if (!isDefined(stepId) || stepId === TRIGGER_STEP_ID) { + return { + steps, + index: 0, + }; + } + + for (const [index, step] of steps.entries()) { + if (step.id === stepId) { + return { + steps, + index, + }; + } + + // TODO: When condition will have been implemented, put recursivity here. + // if (step.type === "CONDITION") { + // return findNodePosition({ + // workflowSteps: step.conditions, + // stepId, + // }) + // } + } + + return undefined; +}; diff --git a/packages/twenty-front/src/modules/workflow/utils/findStepPositionOrThrow.ts b/packages/twenty-front/src/modules/workflow/utils/findStepPositionOrThrow.ts index bf10df7783fc..c20ffbcd4a68 100644 --- a/packages/twenty-front/src/modules/workflow/utils/findStepPositionOrThrow.ts +++ b/packages/twenty-front/src/modules/workflow/utils/findStepPositionOrThrow.ts @@ -1,41 +1,21 @@ -import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId'; import { WorkflowStep } from '@/workflow/types/Workflow'; +import { findStepPosition } from '@/workflow/utils/findStepPosition'; import { isDefined } from 'twenty-ui'; /** * This function returns the reference of the array where the step should be positioned * and at which index. */ -export const findStepPositionOrThrow = ({ - steps, - stepId, -}: { +export const findStepPositionOrThrow = (props: { steps: Array<WorkflowStep>; stepId: string | undefined; }): { steps: Array<WorkflowStep>; index: number } => { - if (!isDefined(stepId) || stepId === TRIGGER_STEP_ID) { - return { - steps, - index: 0, - }; + const result = findStepPosition(props); + if (!isDefined(result)) { + throw new Error( + `Couldn't locate the step. Unreachable step id: ${props.stepId}.`, + ); } - for (const [index, step] of steps.entries()) { - if (step.id === stepId) { - return { - steps, - index, - }; - } - - // TODO: When condition will have been implemented, put recursivity here. - // if (step.type === "CONDITION") { - // return findNodePosition({ - // workflowSteps: step.conditions, - // stepId, - // }) - // } - } - - throw new Error(`Couldn't locate the step. Unreachable step id: ${stepId}.`); + return result; }; diff --git a/packages/twenty-front/src/modules/workflow/utils/generateWorkflowDiagram.ts b/packages/twenty-front/src/modules/workflow/utils/generateWorkflowDiagram.ts index 4d0aa8995ae5..ff5a211e64dd 100644 --- a/packages/twenty-front/src/modules/workflow/utils/generateWorkflowDiagram.ts +++ b/packages/twenty-front/src/modules/workflow/utils/generateWorkflowDiagram.ts @@ -33,6 +33,7 @@ export const generateWorkflowDiagram = ({ id: nodeId, data: { nodeType: 'action', + actionType: step.type, label: step.name, }, position: { diff --git a/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts b/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts index 9cb8b27e86ff..48e8f9bd448f 100644 --- a/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts +++ b/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts @@ -26,6 +26,27 @@ export const getStepDefaultDefinition = ( }, }; } + case 'SEND_EMAIL': { + return { + id: newStepId, + name: 'Send Email', + type: 'SEND_EMAIL', + valid: false, + settings: { + connectedAccountId: '', + subject: '', + body: '', + errorHandlingOptions: { + continueOnFailure: { + value: false, + }, + retryOnFailure: { + value: false, + }, + }, + }, + }; + } default: { throw new Error(`Unknown type: ${type}`); } diff --git a/packages/twenty-front/src/modules/workflow/utils/getStepDefinitionOrThrow.ts b/packages/twenty-front/src/modules/workflow/utils/getStepDefinitionOrThrow.ts new file mode 100644 index 000000000000..47ba8d4df886 --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/utils/getStepDefinitionOrThrow.ts @@ -0,0 +1,45 @@ +import { TRIGGER_STEP_ID } from '@/workflow/constants/TriggerStepId'; +import { WorkflowVersion } from '@/workflow/types/Workflow'; +import { findStepPosition } from '@/workflow/utils/findStepPosition'; +import { isDefined } from 'twenty-ui'; + +export const getStepDefinitionOrThrow = ({ + stepId, + workflowVersion, +}: { + stepId: string; + workflowVersion: WorkflowVersion; +}) => { + if (stepId === TRIGGER_STEP_ID) { + if (!isDefined(workflowVersion.trigger)) { + return { + type: 'trigger', + definition: undefined, + } as const; + } + + return { + type: 'trigger', + definition: workflowVersion.trigger, + } as const; + } + + if (!isDefined(workflowVersion.steps)) { + throw new Error( + 'Malformed workflow version: missing steps information; be sure to create at least one step before trying to edit one', + ); + } + + const selectedNodePosition = findStepPosition({ + steps: workflowVersion.steps, + stepId: stepId, + }); + if (!isDefined(selectedNodePosition)) { + return undefined; + } + + return { + type: 'action', + definition: selectedNodePosition.steps[selectedNodePosition.index], + } as const; +}; diff --git a/packages/twenty-front/src/modules/workflow/utils/mergeWorkflowDiagrams.ts b/packages/twenty-front/src/modules/workflow/utils/mergeWorkflowDiagrams.ts new file mode 100644 index 000000000000..b52ca8f726eb --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/utils/mergeWorkflowDiagrams.ts @@ -0,0 +1,33 @@ +import { + WorkflowDiagram, + WorkflowDiagramNode, +} from '@/workflow/types/WorkflowDiagram'; + +const nodePropertiesToPreserve: Array<keyof WorkflowDiagramNode> = ['selected']; + +export const mergeWorkflowDiagrams = ( + previousDiagram: WorkflowDiagram, + nextDiagram: WorkflowDiagram, +): WorkflowDiagram => { + const lastNodes = nextDiagram.nodes.map((nextNode) => { + const previousNode = previousDiagram.nodes.find( + (previousNode) => previousNode.id === nextNode.id, + ); + + const nodeWithPreservedProperties = nodePropertiesToPreserve.reduce( + (nodeToSet, propertyToPreserve) => { + return Object.assign(nodeToSet, { + [propertyToPreserve]: previousNode?.[propertyToPreserve], + }); + }, + {} as Partial<WorkflowDiagramNode>, + ); + + return Object.assign(nodeWithPreservedProperties, nextNode); + }); + + return { + nodes: lastNodes, + edges: nextDiagram.edges, + }; +}; diff --git a/packages/twenty-front/src/modules/workflow/utils/removeStep.ts b/packages/twenty-front/src/modules/workflow/utils/removeStep.ts new file mode 100644 index 000000000000..eb6fe41dda5b --- /dev/null +++ b/packages/twenty-front/src/modules/workflow/utils/removeStep.ts @@ -0,0 +1,21 @@ +import { WorkflowStep } from '@/workflow/types/Workflow'; +import { findStepPositionOrThrow } from '@/workflow/utils/findStepPositionOrThrow'; + +export const removeStep = ({ + steps: stepsInitial, + stepId, +}: { + steps: Array<WorkflowStep>; + stepId: string | undefined; +}) => { + const steps = structuredClone(stepsInitial); + + const parentStepPosition = findStepPositionOrThrow({ + steps, + stepId, + }); + + parentStepPosition.steps.splice(parentStepPosition.index, 1); + + return steps; +}; diff --git a/packages/twenty-front/src/modules/workflow/utils/replaceStep.ts b/packages/twenty-front/src/modules/workflow/utils/replaceStep.ts index b08e2d33cf03..0d4d47977262 100644 --- a/packages/twenty-front/src/modules/workflow/utils/replaceStep.ts +++ b/packages/twenty-front/src/modules/workflow/utils/replaceStep.ts @@ -1,14 +1,14 @@ import { WorkflowStep } from '@/workflow/types/Workflow'; import { findStepPositionOrThrow } from '@/workflow/utils/findStepPositionOrThrow'; -export const replaceStep = ({ +export const replaceStep = <T extends WorkflowStep>({ steps: stepsInitial, stepId, stepToReplace, }: { steps: Array<WorkflowStep>; stepId: string; - stepToReplace: Partial<Omit<WorkflowStep, 'id'>>; + stepToReplace: Partial<Omit<T, 'id'>>; }) => { const steps = structuredClone(stepsInitial); diff --git a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts index 409723f1655e..9a79efa933bf 100644 --- a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts +++ b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts @@ -9,4 +9,9 @@ export type FeatureFlagKey = | 'IS_FREE_ACCESS_ENABLED' | 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED' | 'IS_WORKFLOW_ENABLED' - | 'IS_WORKSPACE_FAVORITE_ENABLED'; + | 'IS_WORKSPACE_FAVORITE_ENABLED' + | 'IS_SEARCH_ENABLED' + | 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED' + | 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED' + | 'IS_WORKSPACE_MIGRATED_FOR_SEARCH' + | 'IS_ANALYTICS_V2_ENABLED'; diff --git a/packages/twenty-front/src/pages/auth/PasswordReset.tsx b/packages/twenty-front/src/pages/auth/PasswordReset.tsx index 79ea7074021d..11cf5d016b42 100644 --- a/packages/twenty-front/src/pages/auth/PasswordReset.tsx +++ b/packages/twenty-front/src/pages/auth/PasswordReset.tsx @@ -10,6 +10,7 @@ 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'; import { useAuth } from '@/auth/hooks/useAuth'; @@ -174,7 +175,7 @@ export const PasswordReset = () => { highlightColor={theme.background.secondary} > <Skeleton - height={32} + height={SKELETON_LOADER_HEIGHT_SIZES.standard.m} count={2} style={{ marginBottom: theme.spacing(2), diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx index 3f697c21f07a..0ecb2cb569de 100644 --- a/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx +++ b/packages/twenty-front/src/pages/object-record/RecordShowPage.tsx @@ -10,7 +10,9 @@ import { PageBody } from '@/ui/layout/page/PageBody'; import { PageContainer } from '@/ui/layout/page/PageContainer'; import { PageTitle } from '@/ui/utilities/page-title/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 = () => { @@ -38,6 +40,7 @@ export const RecordShowPage = () => { return ( <RecordFieldValueSelectorContextProvider> <RecordValueSetterEffect recordId={objectRecordId} /> + <RecordShowPageContextStoreEffect recordId={objectRecordId} /> <PageContainer> <PageTitle title={pageTitle} /> <RecordShowPageHeader @@ -47,8 +50,11 @@ export const RecordShowPage = () => { > <> {objectNameSingular === CoreObjectNameSingular.Workflow ? ( - <RecordShowPageWorkflowHeader - workflowId={parameters.objectRecordId} + <RecordShowPageWorkflowHeader workflowId={objectRecordId} /> + ) : objectNameSingular === + CoreObjectNameSingular.WorkflowVersion ? ( + <RecordShowPageWorkflowVersionHeader + workflowVersionId={objectRecordId} /> ) : ( <RecordShowPageBaseHeader diff --git a/packages/twenty-front/src/pages/object-record/RecordShowPageContextStoreEffect.tsx b/packages/twenty-front/src/pages/object-record/RecordShowPageContextStoreEffect.tsx new file mode 100644 index 000000000000..a9a5514e285d --- /dev/null +++ b/packages/twenty-front/src/pages/object-record/RecordShowPageContextStoreEffect.tsx @@ -0,0 +1,43 @@ +import { contextStoreCurrentObjectMetadataIdState } from '@/context-store/states/contextStoreCurrentObjectMetadataIdState'; +import { contextStoreTargetedRecordIdsState } from '@/context-store/states/contextStoreTargetedRecordIdsState'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { useEffect } from 'react'; +import { useParams } from 'react-router-dom'; +import { useSetRecoilState } from 'recoil'; + +export const RecordShowPageContextStoreEffect = ({ + recordId, +}: { + recordId: string; +}) => { + const setContextStoreTargetedRecordIds = useSetRecoilState( + contextStoreTargetedRecordIdsState, + ); + + const setContextStoreCurrentObjectMetadataId = useSetRecoilState( + contextStoreCurrentObjectMetadataIdState, + ); + + const { objectNameSingular } = useParams(); + + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: objectNameSingular ?? '', + }); + + useEffect(() => { + setContextStoreTargetedRecordIds([recordId]); + setContextStoreCurrentObjectMetadataId(objectMetadataItem?.id); + + return () => { + setContextStoreTargetedRecordIds([]); + setContextStoreCurrentObjectMetadataId(null); + }; + }, [ + recordId, + setContextStoreTargetedRecordIds, + setContextStoreCurrentObjectMetadataId, + objectMetadataItem?.id, + ]); + + return null; +}; diff --git a/packages/twenty-front/src/pages/object-record/__stories__/RecordShowPage.stories.tsx b/packages/twenty-front/src/pages/object-record/__stories__/RecordShowPage.stories.tsx index 5f1b8c02129d..31d04e0c1edb 100644 --- a/packages/twenty-front/src/pages/object-record/__stories__/RecordShowPage.stories.tsx +++ b/packages/twenty-front/src/pages/object-record/__stories__/RecordShowPage.stories.tsx @@ -11,7 +11,6 @@ import { graphqlMocks } from '~/testing/graphqlMocks'; import { getPeopleMock, peopleQueryResult } from '~/testing/mock-data/people'; import { mockedWorkspaceMemberData } from '~/testing/mock-data/users'; -import { viewQueryResultMock } from '~/testing/mock-data/views'; import { RecordShowPage } from '../RecordShowPage'; const peopleMock = getPeopleMock(); @@ -63,13 +62,6 @@ const meta: Meta<PageDecoratorArgs> = { }, }); }), - graphql.query('FindManyViews', () => { - return HttpResponse.json({ - data: { - views: viewQueryResultMock, - }, - }); - }), graphqlMocks.handlers, ], }, diff --git a/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx b/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx index c6ddb99c82d6..c80eed892c75 100644 --- a/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx +++ b/packages/twenty-front/src/pages/onboarding/SyncEmails.tsx @@ -54,11 +54,11 @@ export const SyncEmails = () => { ? CalendarChannelVisibility.ShareEverything : CalendarChannelVisibility.Metadata; - await triggerGoogleApisOAuth( - AppPath.Index, - visibility, - calendarChannelVisibility, - ); + await triggerGoogleApisOAuth({ + redirectLocation: AppPath.Index, + messageVisibility: visibility, + calendarVisibility: calendarChannelVisibility, + }); }; const continueWithoutSync = async () => { diff --git a/packages/twenty-front/src/pages/settings/Releases.tsx b/packages/twenty-front/src/pages/settings/Releases.tsx index 60845a1b8c5f..3429ac9893ac 100644 --- a/packages/twenty-front/src/pages/settings/Releases.tsx +++ b/packages/twenty-front/src/pages/settings/Releases.tsx @@ -3,7 +3,6 @@ import React, { useEffect, useState } from 'react'; import rehypeStringify from 'rehype-stringify'; import remarkParse from 'remark-parse'; import remarkRehype from 'remark-rehype'; -import { IconRocket } from 'twenty-ui'; import { unified } from 'unified'; import { visit } from 'unist-util-visit'; @@ -107,7 +106,6 @@ export const Releases = () => { return ( <SubMenuTopBarContainer - Icon={IconRocket} title="Releases" links={[ { diff --git a/packages/twenty-front/src/pages/settings/SettingsBilling.tsx b/packages/twenty-front/src/pages/settings/SettingsBilling.tsx index ee057b7114a7..5a8c90a81a31 100644 --- a/packages/twenty-front/src/pages/settings/SettingsBilling.tsx +++ b/packages/twenty-front/src/pages/settings/SettingsBilling.tsx @@ -1,13 +1,10 @@ -import styled from '@emotion/styled'; import { useState } from 'react'; import { useRecoilValue, useSetRecoilState } from 'recoil'; import { - H1Title, H2Title, IconCalendarEvent, IconCircleX, IconCreditCard, - IconCurrencyDollar, } from 'twenty-ui'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; @@ -34,10 +31,6 @@ import { } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; -const StyledH1Title = styled(H1Title)` - margin-bottom: 0; -`; - type SwitchInfo = { newInterval: SubscriptionInterval; to: string; @@ -143,7 +136,6 @@ export const SettingsBilling = () => { return ( <SubMenuTopBarContainer - Icon={IconCurrencyDollar} title="Billing" links={[ { @@ -154,7 +146,6 @@ export const SettingsBilling = () => { ]} > <SettingsPageContainer> - <StyledH1Title title="Billing" /> <SettingsBillingCoverImage /> {displayPaymentFailInfo && ( <Info diff --git a/packages/twenty-front/src/pages/settings/SettingsProfile.tsx b/packages/twenty-front/src/pages/settings/SettingsProfile.tsx index 86b0e5637320..0ff302645941 100644 --- a/packages/twenty-front/src/pages/settings/SettingsProfile.tsx +++ b/packages/twenty-front/src/pages/settings/SettingsProfile.tsx @@ -1,4 +1,4 @@ -import { H2Title, IconUserCircle } from 'twenty-ui'; +import { H2Title } from 'twenty-ui'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { ChangePassword } from '@/settings/profile/components/ChangePassword'; @@ -13,7 +13,6 @@ import { Section } from '@/ui/layout/section/components/Section'; export const SettingsProfile = () => ( <SubMenuTopBarContainer - Icon={IconUserCircle} title="Profile" links={[ { diff --git a/packages/twenty-front/src/pages/settings/SettingsWorkspace.tsx b/packages/twenty-front/src/pages/settings/SettingsWorkspace.tsx index 359961d24d87..329c736f1834 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, IconSettings } from 'twenty-ui'; +import { H2Title } from 'twenty-ui'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { DeleteWorkspace } from '@/settings/profile/components/DeleteWorkspace'; @@ -9,10 +9,10 @@ import { WorkspaceLogoUploader } from '@/settings/workspace/components/Workspace import { SettingsPath } from '@/types/SettingsPath'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; +import { GithubVersionLink } from '@/ui/navigation/link/components/GithubVersionLink'; export const SettingsWorkspace = () => ( <SubMenuTopBarContainer - Icon={IconSettings} title="General" links={[ { @@ -41,6 +41,9 @@ export const SettingsWorkspace = () => ( <Section> <DeleteWorkspace /> </Section> + <Section> + <GithubVersionLink /> + </Section> </SettingsPageContainer> </SubMenuTopBarContainer> ); diff --git a/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx b/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx index 0079fb888501..25176fe7c38b 100644 --- a/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx +++ b/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx @@ -1,17 +1,16 @@ +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 { + Avatar, H2Title, - IconTrash, - IconUsers, - IconReload, IconMail, - StyledText, - Avatar, + IconReload, + IconTrash, + MOBILE_VIEWPORT, } from 'twenty-ui'; -import { isNonEmptyArray } from '@sniptt/guards'; -import { useTheme } from '@emotion/react'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; @@ -21,26 +20,26 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; 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/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'; import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember'; import { WorkspaceInviteLink } from '@/workspace/components/WorkspaceInviteLink'; import { WorkspaceInviteTeam } from '@/workspace/components/WorkspaceInviteTeam'; -import { useGetWorkspaceInvitationsQuery } from '~/generated/graphql'; -import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar'; -import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; -import { Table } from '@/ui/layout/table/components/Table'; -import { TableHeader } from '@/ui/layout/table/components/TableHeader'; -import { workspaceInvitationsState } from '../../modules/workspace-invitation/states/workspaceInvitationsStates'; -import { TableRow } from '../../modules/ui/layout/table/components/TableRow'; -import { TableCell } from '../../modules/ui/layout/table/components/TableCell'; -import { Status } from '../../modules/ui/display/status/components/Status'; import { formatDistanceToNow } from 'date-fns'; -import { useResendWorkspaceInvitation } from '../../modules/workspace-invitation/hooks/useResendWorkspaceInvitation'; +import { useGetWorkspaceInvitationsQuery } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; +import { Status } from '../../modules/ui/display/status/components/Status'; +import { TableCell } from '../../modules/ui/layout/table/components/TableCell'; +import { TableRow } from '../../modules/ui/layout/table/components/TableRow'; import { useDeleteWorkspaceInvitation } from '../../modules/workspace-invitation/hooks/useDeleteWorkspaceInvitation'; +import { useResendWorkspaceInvitation } from '../../modules/workspace-invitation/hooks/useResendWorkspaceInvitation'; +import { workspaceInvitationsState } from '../../modules/workspace-invitation/states/workspaceInvitationsStates'; const StyledButtonContainer = styled.div` align-items: center; @@ -53,6 +52,47 @@ const StyledTable = styled(Table)` margin-top: ${({ theme }) => theme.spacing(0.5)}; `; +const StyledTableRow = styled(TableRow)` + @media (max-width: ${MOBILE_VIEWPORT}px) { + display: grid; + grid-template-columns: 3fr; + } +`; +const StyledTableCell = styled(TableCell)` + padding: ${({ theme }) => theme.spacing(1)}; + @media (max-width: ${MOBILE_VIEWPORT}px) { + &:first-child { + max-width: 100%; + padding-top: 2px; + white-space: nowrap; + overflow: scroll; + scroll-behavior: smooth; + } + } +`; +const StyledIconWrapper = styled.div` + left: 2px; + margin-right: ${({ theme }) => theme.spacing(2)}; + position: relative; + top: 1px; +`; + +const StyledScrollableTextContainer = styled.div` + max-width: 100%; + overflow-x: auto; + white-space: pre-line; +`; + +const StyledTextContainer = styled.div` + color: ${({ theme }) => theme.font.color.secondary}; + max-width: max-content; + overflow-x: auto; + position: absolute; + @media (min-width: 360px) and (max-width: 420px) { + max-width: 150px; + margin-top: ${({ theme }) => theme.spacing(1)}; + } +`; const StyledTableHeaderRow = styled(Table)` margin-bottom: ${({ theme }) => theme.spacing(1.5)}; `; @@ -126,7 +166,6 @@ export const SettingsWorkspaceMembers = () => { return ( <SubMenuTopBarContainer - Icon={IconUsers} title="Members" links={[ { @@ -165,28 +204,25 @@ export const SettingsWorkspaceMembers = () => { <StyledTable key={workspaceMember.id}> <TableRow> <TableCell> - <StyledText - PrefixComponent={ - <Avatar - avatarUrl={workspaceMember.avatarUrl} - placeholderColorSeed={workspaceMember.id} - placeholder={workspaceMember.name.firstName ?? ''} - type="rounded" - size="sm" - /> - } - text={ - workspaceMember.name.firstName + + <StyledIconWrapper> + <Avatar + avatarUrl={workspaceMember.avatarUrl} + placeholderColorSeed={workspaceMember.id} + placeholder={workspaceMember.name.firstName ?? ''} + type="rounded" + size="sm" + /> + </StyledIconWrapper> + <StyledScrollableTextContainer> + {workspaceMember.name.firstName + ' ' + - workspaceMember.name.lastName - } - /> + workspaceMember.name.lastName} + </StyledScrollableTextContainer> </TableCell> <TableCell> - <StyledText - text={workspaceMember.userEmail} - color={theme.font.color.secondary} - /> + <StyledTextContainer> + {workspaceMember.userEmail} + </StyledTextContainer> </TableCell> <TableCell align={'right'}> {currentWorkspaceMember?.id !== workspaceMember.id && ( @@ -225,25 +261,27 @@ export const SettingsWorkspaceMembers = () => { </StyledTableHeaderRow> {workspaceInvitations?.map((workspaceInvitation) => ( <StyledTable key={workspaceInvitation.id}> - <TableRow gridAutoColumns={`1fr 1fr ${theme.spacing(22)}`}> - <TableCell> - <StyledText - PrefixComponent={ - <IconMail - size={theme.icon.size.md} - stroke={theme.icon.stroke.sm} - /> - } - text={workspaceInvitation.email} - /> - </TableCell> - <TableCell align={'right'}> + <StyledTableRow + gridAutoColumns={`1fr 1fr ${theme.spacing(22)}`} + > + <StyledTableCell> + <StyledIconWrapper> + <IconMail + size={theme.icon.size.md} + stroke={theme.icon.stroke.sm} + /> + </StyledIconWrapper> + <StyledScrollableTextContainer> + {workspaceInvitation.email} + </StyledScrollableTextContainer> + </StyledTableCell> + <StyledTableCell align={'right'}> <Status color={'gray'} text={getExpiresAtText(workspaceInvitation.expiresAt)} /> - </TableCell> - <TableCell align={'right'}> + </StyledTableCell> + <StyledTableCell align={'right'}> <StyledButtonContainer> <IconButton onClick={() => { @@ -266,8 +304,8 @@ export const SettingsWorkspaceMembers = () => { Icon={IconTrash} /> </StyledButtonContainer> - </TableCell> - </TableRow> + </StyledTableCell> + </StyledTableRow> </StyledTable> ))} </Table> diff --git a/packages/twenty-front/src/pages/settings/__stories__/SettingsAppearance.stories.tsx b/packages/twenty-front/src/pages/settings/__stories__/SettingsAppearance.stories.tsx index 1aa66449abcc..2b7ab8f8e528 100644 --- a/packages/twenty-front/src/pages/settings/__stories__/SettingsAppearance.stories.tsx +++ b/packages/twenty-front/src/pages/settings/__stories__/SettingsAppearance.stories.tsx @@ -38,6 +38,24 @@ export const Default: Story = { }, }; +export const DateTimeSettingsTimeFormat: Story = { + play: async () => { + const canvas = within(document.body); + + await canvas.findByText('Date and time'); + + const timeFormatSelect = await canvas.findByText('24h (08:33)'); + + userEvent.click(timeFormatSelect); + + const timeFormatOptions = await canvas.findByText('12h (8:33 AM)'); + + userEvent.click(timeFormatOptions); + + await canvas.findByText('12h (8:33 AM)'); + }, +}; + export const DateTimeSettingsTimezone: Story = { play: async () => { const canvas = within(document.body); @@ -77,21 +95,3 @@ export const DateTimeSettingsDateFormat: Story = { await canvas.findByText('Jun 13, 2022'); }, }; - -export const DateTimeSettingsTimeFormat: Story = { - play: async () => { - const canvas = within(document.body); - - await canvas.findByText('Date and time'); - - const timeFormatSelect = await canvas.findByText('24h (08:33)'); - - userEvent.click(timeFormatSelect); - - const timeFormatOptions = await canvas.findByText('12h (8:33 AM)'); - - userEvent.click(timeFormatOptions); - - await canvas.findByText('12h (8:33 AM)'); - }, -}; diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx index ed1ad2713231..5ff43e83219d 100644 --- a/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsAccounts.tsx @@ -1,5 +1,5 @@ import { useRecoilValue } from 'recoil'; -import { H2Title, IconAt } from 'twenty-ui'; +import { H2Title } from 'twenty-ui'; import { ConnectedAccount } from '@/accounts/types/ConnectedAccount'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; @@ -36,7 +36,6 @@ export const SettingsAccounts = () => { return ( <SubMenuTopBarContainer - Icon={IconAt} title="Account" links={[ { diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx index ff4a6cd6ac29..2dc86e72e114 100644 --- a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsCalendars.tsx @@ -4,12 +4,10 @@ import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; -import { IconCalendarEvent } from 'twenty-ui'; export const SettingsAccountsCalendars = () => { return ( <SubMenuTopBarContainer - Icon={IconCalendarEvent} title="Calendars" links={[ { diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmails.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmails.tsx index 6e2d574b95d4..4e5732ef81b4 100644 --- a/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmails.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsAccountsEmails.tsx @@ -4,11 +4,9 @@ import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; -import { IconMail } from 'twenty-ui'; export const SettingsAccountsEmails = () => ( <SubMenuTopBarContainer - Icon={IconMail} title="Emails" links={[ { diff --git a/packages/twenty-front/src/pages/settings/accounts/SettingsNewAccount.tsx b/packages/twenty-front/src/pages/settings/accounts/SettingsNewAccount.tsx index 440ebedb93a9..35e90d9b2cf0 100644 --- a/packages/twenty-front/src/pages/settings/accounts/SettingsNewAccount.tsx +++ b/packages/twenty-front/src/pages/settings/accounts/SettingsNewAccount.tsx @@ -3,12 +3,10 @@ import { SettingsPageContainer } from '@/settings/components/SettingsPageContain import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; -import { IconAt } from 'twenty-ui'; export const SettingsNewAccount = () => { return ( <SubMenuTopBarContainer - Icon={IconAt} title="New Account" links={[ { 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 5794ee4ae4e0..4c4dbc807c64 100644 --- a/packages/twenty-front/src/pages/settings/crm-migration/SettingsCRMMigration.tsx +++ b/packages/twenty-front/src/pages/settings/crm-migration/SettingsCRMMigration.tsx @@ -1,6 +1,5 @@ // @ts-expect-error external library has a typing issue import { RevertConnect } from '@revertdotdev/revert-react'; -import { IconSettings } from 'twenty-ui'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer'; @@ -18,7 +17,6 @@ export const SettingsCRMMigration = () => { const currentWorkspace = useRecoilValue(currentWorkspaceState); return ( <SubMenuTopBarContainer - Icon={IconSettings} title="Migrate" links={[ { 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 f3ddd5440865..136bad77026b 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsNewObject.tsx @@ -1,7 +1,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { FormProvider, useForm } from 'react-hook-form'; import { useNavigate } from 'react-router-dom'; -import { H2Title, IconHierarchy2 } from 'twenty-ui'; +import { H2Title } from 'twenty-ui'; import { z } from 'zod'; import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreateOneObjectMetadataItem'; @@ -69,7 +69,6 @@ export const SettingsNewObject = () => { // eslint-disable-next-line react/jsx-props-no-spreading <FormProvider {...formConfig}> <SubMenuTopBarContainer - Icon={IconHierarchy2} title="New Object" links={[ { 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 c69c27507c54..8a300ecf9266 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPageContent.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectDetailPageContent.tsx @@ -1,6 +1,5 @@ import { useUpdateOneObjectMetadataItem } from '@/object-metadata/hooks/useUpdateOneObjectMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { getDisabledFieldMetadataItems } from '@/object-metadata/utils/getDisabledFieldMetadataItems'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsObjectSummaryCard } from '@/settings/data-model/object-details/components/SettingsObjectSummaryCard'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; @@ -10,9 +9,8 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer' import { Section } from '@/ui/layout/section/components/Section'; import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; import styled from '@emotion/styled'; -import { isNonEmptyArray } from '@sniptt/guards'; import { useNavigate } from 'react-router-dom'; -import { H2Title, IconHierarchy2, IconPlus } from 'twenty-ui'; +import { H2Title, IconPlus } from 'twenty-ui'; import { SettingsObjectFieldTable } from '~/pages/settings/data-model/SettingsObjectFieldTable'; const StyledDiv = styled.div` @@ -40,14 +38,10 @@ export const SettingsObjectDetailPageContent = ({ navigate(getSettingsPagePath(SettingsPath.Objects)); }; - const disabledFieldMetadataItems = - getDisabledFieldMetadataItems(objectMetadataItem); - const shouldDisplayAddFieldButton = !objectMetadataItem.isRemote; return ( <SubMenuTopBarContainer - Icon={IconHierarchy2} title={objectMetadataItem.labelPlural} links={[ { @@ -80,13 +74,7 @@ export const SettingsObjectDetailPageContent = ({ /> {shouldDisplayAddFieldButton && ( <StyledDiv> - <UndecoratedLink - to={ - isNonEmptyArray(disabledFieldMetadataItems) - ? './new-field/step-1' - : './new-field/step-2' - } - > + <UndecoratedLink to={'./new-field/select'}> <Button Icon={IconPlus} title="Add Field" 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 8782a7b75b2d..3cbdfe3bdc7e 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectEdit.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectEdit.tsx @@ -5,7 +5,7 @@ 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, IconHierarchy2 } from 'twenty-ui'; +import { H2Title, IconArchive } from 'twenty-ui'; import { z } from 'zod'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; @@ -107,7 +107,6 @@ export const SettingsObjectEdit = () => { <RecordFieldValueSelectorContextProvider> <FormProvider {...formConfig}> <SubMenuTopBarContainer - Icon={IconHierarchy2} title="Edit" links={[ { 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 dd11d6dbb845..0f19c414ae5b 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectFieldEdit.tsx @@ -5,12 +5,7 @@ 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, - IconHierarchy2, -} from 'twenty-ui'; +import { H2Title, IconArchive, IconArchiveOff } from 'twenty-ui'; import { z } from 'zod'; import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; @@ -30,7 +25,7 @@ import { SettingsDataModelFieldDescriptionForm } from '@/settings/data-model/fie import { SettingsDataModelFieldIconLabelForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm'; import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard'; import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema'; -import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; +import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { AppPath } from '@/types/AppPath'; import { SettingsPath } from '@/types/SettingsPath'; @@ -42,9 +37,11 @@ import { Section } from '@/ui/layout/section/components/Section'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { isDefined } from '~/utils/isDefined'; +//TODO: fix this type type SettingsDataModelFieldEditFormValues = z.infer< ReturnType<typeof settingsFieldFormSchema> ->; +> & + any; const canPersistFieldMetadataItemUpdate = ( fieldMetadataItem: FieldMetadataItem, @@ -94,7 +91,7 @@ export const SettingsObjectFieldEdit = () => { resolver: zodResolver(settingsFieldFormSchema()), values: { icon: fieldMetadataItem?.icon ?? 'Icon', - type: fieldMetadataItem?.type as SettingsSupportedFieldType, + type: fieldMetadataItem?.type as SettingsFieldType, label: fieldMetadataItem?.label ?? '', description: fieldMetadataItem?.description, }, @@ -184,7 +181,6 @@ export const SettingsObjectFieldEdit = () => { {/* eslint-disable-next-line react/jsx-props-no-spreading */} <FormProvider {...formConfig}> <SubMenuTopBarContainer - Icon={IconHierarchy2} title={fieldMetadataItem?.label} links={[ { diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldConfigure.tsx similarity index 63% rename from packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx rename to packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldConfigure.tsx index 90a1e4d1020e..0b27c458859b 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep2.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldConfigure.tsx @@ -2,6 +2,7 @@ import { useCreateOneRelationMetadataItem } from '@/object-metadata/hooks/useCre import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; @@ -11,9 +12,8 @@ import { FIELD_NAME_MAXIMUM_LENGTH } from '@/settings/data-model/constants/Field import { SettingsDataModelFieldDescriptionForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldDescriptionForm'; import { SettingsDataModelFieldIconLabelForm } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldIconLabelForm'; import { SettingsDataModelFieldSettingsFormCard } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard'; -import { SettingsDataModelFieldTypeSelect } from '@/settings/data-model/fields/forms/components/SettingsDataModelFieldTypeSelect'; import { settingsFieldFormSchema } from '@/settings/data-model/fields/forms/validation-schemas/settingsFieldFormSchema'; -import { SettingsSupportedFieldType } from '@/settings/data-model/types/SettingsSupportedFieldType'; +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'; @@ -22,40 +22,37 @@ import { Section } from '@/ui/layout/section/components/Section'; import { View } from '@/views/types/View'; import { ViewType } from '@/views/types/ViewType'; import { useApolloClient } from '@apollo/client'; -import styled from '@emotion/styled'; import { zodResolver } from '@hookform/resolvers/zod'; import pick from 'lodash.pick'; import { useEffect, useState } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { useNavigate, useParams, useSearchParams } from 'react-router-dom'; -import { H1Title, H1TitleFontColor, H2Title, IconHierarchy2 } from 'twenty-ui'; +import { H2Title } from 'twenty-ui'; import { z } from 'zod'; import { FieldMetadataType } from '~/generated-metadata/graphql'; -import { isDefined } from '~/utils/isDefined'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; type SettingsDataModelNewFieldFormValues = z.infer< ReturnType<typeof settingsFieldFormSchema> ->; +> & + any; -const StyledH1Title = styled(H1Title)` - margin-bottom: 0; - padding-top: ${({ theme }) => theme.spacing(3)}; -`; -export const SettingsObjectNewFieldStep2 = () => { +export const SettingsObjectNewFieldConfigure = () => { const navigate = useNavigate(); const { objectSlug = '' } = useParams(); const [searchParams] = useSearchParams(); - const fieldType = searchParams.get('fieldType') as SettingsSupportedFieldType; + const fieldType = + (searchParams.get('fieldType') as SettingsFieldType) || + FieldMetadataType.Text; const { enqueueSnackBar } = useSnackBar(); - const [isConfigureStep, setIsConfigureStep] = useState(false); const { findActiveObjectMetadataItemBySlug } = useFilteredObjectMetadataItems(); const activeObjectMetadataItem = findActiveObjectMetadataItemBySlug(objectSlug); const { createMetadataField } = useFieldMetadataItem(); + const apolloClient = useApolloClient(); const formConfig = useForm<SettingsDataModelNewFieldFormValues>({ mode: 'onTouched', @@ -64,13 +61,20 @@ export const SettingsObjectNewFieldStep2 = () => { activeObjectMetadataItem?.fields.map((value) => value.name), ), ), + defaultValues: { + type: fieldType, + icon: 'IconUsers', + label: '', + description: '', + }, }); - useEffect(() => { - if (!activeObjectMetadataItem) { - navigate(AppPath.NotFound); - } - }, [activeObjectMetadataItem, navigate]); + const fieldMetadataItem: Pick<FieldMetadataItem, 'icon' | 'label' | 'type'> = + { + icon: formConfig.watch('icon'), + label: formConfig.watch('label') || 'Employees', + type: formConfig.watch('type'), + }; const [, setObjectViews] = useState<View[]>([]); const [, setRelationObjectViews] = useState<View[]>([]); @@ -83,7 +87,6 @@ export const SettingsObjectNewFieldStep2 = () => { }, onCompleted: async (views) => { if (isUndefinedOrNull(views)) return; - setObjectViews(views); }, }); @@ -101,15 +104,17 @@ export const SettingsObjectNewFieldStep2 = () => { }, onCompleted: async (views) => { if (isUndefinedOrNull(views)) return; - setRelationObjectViews(views); }, }); - const { createOneRelationMetadataItem: createOneRelationMetadata } = useCreateOneRelationMetadataItem(); - const apolloClient = useApolloClient(); + useEffect(() => { + if (!activeObjectMetadataItem) { + navigate(AppPath.NotFound); + } + }, [activeObjectMetadataItem, navigate]); if (!activeObjectMetadataItem) return null; @@ -158,17 +163,7 @@ export const SettingsObjectNewFieldStep2 = () => { }); } }; - - const excludedFieldTypes: SettingsSupportedFieldType[] = ( - [ - FieldMetadataType.Link, - FieldMetadataType.Numeric, - FieldMetadataType.RichText, - FieldMetadataType.Actor, - FieldMetadataType.Email, - FieldMetadataType.Phone, - ] as const - ).filter(isDefined); + if (!activeObjectMetadataItem) return null; return ( <RecordFieldValueSelectorContextProvider> @@ -176,96 +171,55 @@ export const SettingsObjectNewFieldStep2 = () => { {...formConfig} > <SubMenuTopBarContainer - Icon={IconHierarchy2} + title="2. Configure field" links={[ - { - children: 'Objects', - href: '/settings/objects', - }, + { children: 'Workspace', href: '/settings/workspace' }, + { children: 'Objects', href: '/settings/objects' }, { children: activeObjectMetadataItem.labelPlural, href: `/settings/objects/${objectSlug}`, }, - { - children: ( - <SettingsDataModelNewFieldBreadcrumbDropDown - isConfigureStep={isConfigureStep} - onBreadcrumbClick={setIsConfigureStep} - /> - ), - }, + + { children: <SettingsDataModelNewFieldBreadcrumbDropDown /> }, ]} actionButton={ - !activeObjectMetadataItem.isRemote && ( - <SaveAndCancelButtons - isSaveDisabled={!canSave} - isCancelDisabled={isSubmitting} - onCancel={() => { - if (!isConfigureStep) { - navigate(`/settings/objects/${objectSlug}`); - } else { - setIsConfigureStep(false); - } - }} - onSave={formConfig.handleSubmit(handleSave)} - /> - ) + <SaveAndCancelButtons + isSaveDisabled={!canSave} + isCancelDisabled={isSubmitting} + onCancel={() => + navigate( + `/settings/objects/${objectSlug}/new-field/select?fieldType=${fieldType}`, + ) + } + onSave={formConfig.handleSubmit(handleSave)} + /> } > <SettingsPageContainer> - <StyledH1Title - title={ - !isConfigureStep - ? '1. Select a field type' - : '2. Configure field' - } - fontColor={H1TitleFontColor.Primary} - /> - - {!isConfigureStep ? ( - <SettingsDataModelFieldTypeSelect - excludedFieldTypes={excludedFieldTypes} - fieldMetadataItem={{ - type: fieldType, - }} - onFieldTypeSelect={() => setIsConfigureStep(true)} + <Section> + <H2Title + title="Icon and Name" + description="The name and icon of this field" /> - ) : ( - <> - <Section> - <H2Title - title="Icon and Name" - description="The name and icon of this field" - /> - <SettingsDataModelFieldIconLabelForm - maxLength={FIELD_NAME_MAXIMUM_LENGTH} - /> - </Section> - <Section> - <H2Title - title="Values" - description="The values of this field" - /> - - <SettingsDataModelFieldSettingsFormCard - isCreatingField - fieldMetadataItem={{ - icon: formConfig.watch('icon'), - label: formConfig.watch('label') || 'Employees', - type: formConfig.watch('type'), - }} - objectMetadataItem={activeObjectMetadataItem} - /> - </Section> - <Section> - <H2Title - title="Description" - description="The description of this field" - /> - <SettingsDataModelFieldDescriptionForm /> - </Section> - </> - )} + <SettingsDataModelFieldIconLabelForm + maxLength={FIELD_NAME_MAXIMUM_LENGTH} + /> + </Section> + <Section> + <H2Title title="Values" description="The values of this field" /> + <SettingsDataModelFieldSettingsFormCard + isCreatingField + fieldMetadataItem={fieldMetadataItem} + objectMetadataItem={activeObjectMetadataItem} + /> + </Section> + <Section> + <H2Title + title="Description" + description="The description of this field" + /> + <SettingsDataModelFieldDescriptionForm /> + </Section> </SettingsPageContainer> </SubMenuTopBarContainer> </FormProvider> 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 new file mode 100644 index 000000000000..8c3f77e08b35 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldSelect.tsx @@ -0,0 +1,90 @@ +import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; +import { RecordFieldValueSelectorContextProvider } from '@/object-record/record-store/contexts/RecordFieldValueSelectorContext'; +import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; +import { SettingsDataModelNewFieldBreadcrumbDropDown } from '@/settings/data-model/components/SettingsDataModelNewFieldBreadcrumbDropDown'; +import { SETTINGS_FIELD_TYPE_CONFIGS } from '@/settings/data-model/constants/SettingsFieldTypeConfigs'; +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 { zodResolver } from '@hookform/resolvers/zod'; +import { useEffect } from 'react'; +import { FormProvider, useForm } from 'react-hook-form'; +import { useNavigate, useParams } from 'react-router-dom'; +import { isDefined } from 'twenty-ui'; +import { z } from 'zod'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; + +export const settingsDataModelFieldTypeFormSchema = z.object({ + type: z.enum( + Object.keys(SETTINGS_FIELD_TYPE_CONFIGS) as [ + SettingsFieldType, + ...SettingsFieldType[], + ], + ), +}); + +export type SettingsDataModelFieldTypeFormValues = z.infer< + typeof settingsDataModelFieldTypeFormSchema +>; + +export const SettingsObjectNewFieldSelect = () => { + const navigate = useNavigate(); + const { objectSlug = '' } = useParams(); + const { findActiveObjectMetadataItemBySlug } = + useFilteredObjectMetadataItems(); + const activeObjectMetadataItem = + findActiveObjectMetadataItemBySlug(objectSlug); + const formMethods = useForm({ + resolver: zodResolver(settingsDataModelFieldTypeFormSchema), + defaultValues: { + type: FieldMetadataType.Text, + }, + }); + const excludedFieldTypes: SettingsFieldType[] = ( + [ + FieldMetadataType.Link, + FieldMetadataType.Numeric, + FieldMetadataType.RichText, + FieldMetadataType.Actor, + FieldMetadataType.Email, + FieldMetadataType.Phone, + ] as const + ).filter(isDefined); + + useEffect(() => { + if (!activeObjectMetadataItem) { + navigate(AppPath.NotFound); + } + }, [activeObjectMetadataItem, navigate]); + + if (!activeObjectMetadataItem) return null; + + return ( + <RecordFieldValueSelectorContextProvider> + <FormProvider // eslint-disable-next-line react/jsx-props-no-spreading + {...formMethods} + > + <SubMenuTopBarContainer + title="1. Select a field type" + links={[ + { children: 'Workspace', href: '/settings/workspace' }, + { children: 'Objects', href: '/settings/objects' }, + { + children: activeObjectMetadataItem.labelPlural, + href: `/settings/objects/${objectSlug}`, + }, + { children: <SettingsDataModelNewFieldBreadcrumbDropDown /> }, + ]} + > + <SettingsPageContainer> + <SettingsObjectNewFieldSelector + objectSlug={objectSlug} + excludedFieldTypes={excludedFieldTypes} + /> + </SettingsPageContainer> + </SubMenuTopBarContainer> + </FormProvider> + </RecordFieldValueSelectorContextProvider> + ); +}; diff --git a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1.tsx b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1.tsx deleted file mode 100644 index d608e79967b7..000000000000 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldStep1.tsx +++ /dev/null @@ -1,133 +0,0 @@ -import styled from '@emotion/styled'; -import { useEffect } from 'react'; -import { useNavigate, useParams } from 'react-router-dom'; -import { H2Title, IconHierarchy2, IconPlus } from 'twenty-ui'; - -import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; -import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; -import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; - -import { useFieldMetadataItem } from '@/object-metadata/hooks/useFieldMetadataItem'; -import { settingsObjectFieldsFamilyState } from '@/settings/data-model/object-details/states/settingsObjectFieldsFamilyState'; -import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; -import { AppPath } from '@/types/AppPath'; -import { SettingsPath } from '@/types/SettingsPath'; -import { Button } from '@/ui/input/button/components/Button'; -import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; -import { Section } from '@/ui/layout/section/components/Section'; -import { useRecoilState } from 'recoil'; -import { SettingsObjectFieldTable } from '~/pages/settings/data-model/SettingsObjectFieldTable'; - -const StyledSection = styled(Section)` - display: flex; - flex-direction: column; -`; - -const StyledAddCustomFieldButton = styled(Button)` - align-self: flex-end; - margin-top: ${({ theme }) => theme.spacing(2)}; -`; - -export const SettingsObjectNewFieldStep1 = () => { - const navigate = useNavigate(); - - const { objectSlug = '' } = useParams(); - const { findActiveObjectMetadataItemBySlug } = - useFilteredObjectMetadataItems(); - - const activeObjectMetadataItem = - findActiveObjectMetadataItemBySlug(objectSlug); - - const [settingsObjectFields] = useRecoilState( - settingsObjectFieldsFamilyState({ - objectMetadataItemId: activeObjectMetadataItem?.id, - }), - ); - - const { activateMetadataField, deactivateMetadataField } = - useFieldMetadataItem(); - - const canSave = settingsObjectFields?.some( - (field, index) => - field.isActive !== activeObjectMetadataItem?.fields[index].isActive, - ); - - const handleSave = async () => { - if (!activeObjectMetadataItem || !settingsObjectFields) { - return; - } - - await Promise.all( - settingsObjectFields.map((fieldMetadataItem, index) => { - if ( - fieldMetadataItem.isActive === - activeObjectMetadataItem.fields[index].isActive - ) { - return undefined; - } - - return fieldMetadataItem.isActive - ? activateMetadataField(fieldMetadataItem) - : deactivateMetadataField(fieldMetadataItem); - }), - ); - - navigate(`/settings/objects/${objectSlug}`); - }; - - useEffect(() => { - if (!activeObjectMetadataItem) { - navigate(AppPath.NotFound); - return; - } - }, [activeObjectMetadataItem, navigate]); - - if (!activeObjectMetadataItem) return null; - - return ( - <SubMenuTopBarContainer - Icon={IconHierarchy2} - links={[ - { - children: 'Workspace', - href: getSettingsPagePath(SettingsPath.Workspace), - }, - { children: 'Objects', href: '/settings/objects' }, - { - children: activeObjectMetadataItem.labelPlural, - href: `/settings/objects/${objectSlug}`, - }, - { children: 'New Field' }, - ]} - actionButton={ - !activeObjectMetadataItem.isRemote && ( - <SaveAndCancelButtons - isSaveDisabled={!canSave} - onCancel={() => navigate(`/settings/objects/${objectSlug}`)} - onSave={handleSave} - /> - ) - } - > - <SettingsPageContainer> - <StyledSection> - <H2Title - title="Check deactivated fields" - description="Before creating a custom field, check if it already exists in the deactivated section." - /> - <SettingsObjectFieldTable - objectMetadataItem={activeObjectMetadataItem} - mode="new-field" - /> - <StyledAddCustomFieldButton - Icon={IconPlus} - title="Add Custom Field" - size="small" - variant="secondary" - to={`/settings/objects/${objectSlug}/new-field/step-2`} - /> - </StyledSection> - </SettingsPageContainer> - </SubMenuTopBarContainer> - ); -}; 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 6adcb2a9db59..215ae748bc99 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjectOverview.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjectOverview.tsx @@ -4,12 +4,10 @@ import { SettingsDataModelOverview } from '@/settings/data-model/graph-overview/ import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; -import { IconHierarchy2 } from 'twenty-ui'; export const SettingsObjectOverview = () => { return ( <SubMenuTopBarContainer - Icon={IconHierarchy2} links={[ { children: 'Workspace', 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 c31af9513c48..7c7e280569d7 100644 --- a/packages/twenty-front/src/pages/settings/data-model/SettingsObjects.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/SettingsObjects.tsx @@ -1,12 +1,6 @@ import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; -import { - H2Title, - IconChevronRight, - IconHierarchy2, - IconPlus, - IconSearch, -} from 'twenty-ui'; +import { H2Title, IconChevronRight, IconPlus, IconSearch } from 'twenty-ui'; import { useDeleteOneObjectMetadataItem } from '@/object-metadata/hooks/useDeleteOneObjectMetadataItem'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; @@ -133,7 +127,6 @@ export const SettingsObjects = () => { ); return ( <SubMenuTopBarContainer - Icon={IconHierarchy2} title="Data model" actionButton={ <UndecoratedLink to={getSettingsPagePath(SettingsPath.NewObject)}> diff --git a/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsNewObject.stories.tsx b/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsNewObject.stories.tsx index a90160ea1301..8815ff456414 100644 --- a/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsNewObject.stories.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsNewObject.stories.tsx @@ -28,6 +28,9 @@ export type Story = StoryObj<typeof SettingsNewObject>; export const WithStandardSelected: Story = { play: async () => { const canvas = within(document.body); + + await canvas.findByText('New Object'); + const listingInput = await canvas.findByPlaceholderText('Listing'); const pluralInput = await canvas.findByPlaceholderText('Listings'); const descriptionInput = await canvas.findByPlaceholderText( diff --git a/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectNewField/SettingsObjectNewFieldStep2.stories.tsx b/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectNewField/SettingsObjectNewFieldConfigure.stories.tsx similarity index 63% rename from packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectNewField/SettingsObjectNewFieldStep2.stories.tsx rename to packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectNewField/SettingsObjectNewFieldConfigure.stories.tsx index ccd81beaf145..82a36add751a 100644 --- a/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectNewField/SettingsObjectNewFieldStep2.stories.tsx +++ b/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectNewField/SettingsObjectNewFieldConfigure.stories.tsx @@ -1,5 +1,6 @@ import { Meta, StoryObj } from '@storybook/react'; import { userEvent, within } from '@storybook/test'; +import { SettingsObjectNewFieldConfigure } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldConfigure'; import { PageDecorator, @@ -7,15 +8,13 @@ import { } from '~/testing/decorators/PageDecorator'; import { graphqlMocks } from '~/testing/graphqlMocks'; -import { SettingsObjectNewFieldStep2 } from '../../SettingsObjectNewField/SettingsObjectNewFieldStep2'; - const meta: Meta<PageDecoratorArgs> = { title: - 'Pages/Settings/DataModel/SettingsObjectNewField/SettingsObjectNewFieldStep2', - component: SettingsObjectNewFieldStep2, + 'Pages/Settings/DataModel/SettingsObjectNewField/SettingsObjectNewFieldConfigure', + component: SettingsObjectNewFieldConfigure, decorators: [PageDecorator], args: { - routePath: '/settings/objects/:objectSlug/new-field/step-2', + routePath: '/settings/objects/:objectSlug/new-field/configure', routeParams: { ':objectSlug': 'companies' }, }, parameters: { @@ -25,21 +24,11 @@ const meta: Meta<PageDecoratorArgs> = { export default meta; -export type Story = StoryObj<typeof SettingsObjectNewFieldStep2>; +export type Story = StoryObj<typeof SettingsObjectNewFieldConfigure>; export const Default: Story = { play: async ({ canvasElement }) => { const canvas = within(canvasElement); - await canvas.findByText('Objects'); - await canvas.findByText('1. Select a field type'); - - const searchInput = await canvas.findByPlaceholderText('Search a type'); - - await userEvent.type(searchInput, 'Num'); - - const numberTypeButton = await canvas.findByText('Number'); - - await userEvent.click(numberTypeButton); await canvas.findByText('2. Configure field'); @@ -49,11 +38,10 @@ export const Default: Story = { const descriptionInput = await canvas.findByPlaceholderText( 'Write a description', ); - await userEvent.type(descriptionInput, 'Test description'); const saveButton = await canvas.findByText('Save'); - + await new Promise((resolve) => setTimeout(resolve, 5000)); await userEvent.click(saveButton); }, }; diff --git a/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectNewField/SettingsObjectNewFieldSelect.stories.tsx b/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectNewField/SettingsObjectNewFieldSelect.stories.tsx new file mode 100644 index 000000000000..90114698c3b1 --- /dev/null +++ b/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectNewField/SettingsObjectNewFieldSelect.stories.tsx @@ -0,0 +1,41 @@ +import { Meta, StoryObj } from '@storybook/react'; +import { userEvent, within } from '@storybook/test'; +import { SettingsObjectNewFieldSelect } from '~/pages/settings/data-model/SettingsObjectNewField/SettingsObjectNewFieldSelect'; + +import { + PageDecorator, + PageDecoratorArgs, +} from '~/testing/decorators/PageDecorator'; +import { graphqlMocks } from '~/testing/graphqlMocks'; + +const meta: Meta<PageDecoratorArgs> = { + title: + 'Pages/Settings/DataModel/SettingsObjectNewField/SettingsObjectNewFieldSelect', + component: SettingsObjectNewFieldSelect, + decorators: [PageDecorator], + args: { + routePath: '/settings/objects/:objectSlug/new-field/select', + routeParams: { ':objectSlug': 'companies' }, + }, + parameters: { + msw: graphqlMocks, + }, +}; + +export default meta; + +export type Story = StoryObj<typeof SettingsObjectNewFieldSelect>; + +export const Default: Story = { + play: async ({ canvasElement }) => { + const canvas = within(canvasElement); + await canvas.findByText('Objects'); + await canvas.findByText('1. Select a field type'); + const searchInput = await canvas.findByPlaceholderText('Search a type'); + await userEvent.type(searchInput, 'Rela'); + await new Promise((resolve) => setTimeout(resolve, 1500)); + await userEvent.clear(searchInput); + await userEvent.type(searchInput, 'Num'); + await new Promise((resolve) => setTimeout(resolve, 1500)); + }, +}; diff --git a/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectNewField/SettingsObjectNewFieldStep1.stories.tsx b/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectNewField/SettingsObjectNewFieldStep1.stories.tsx deleted file mode 100644 index 752bfcc713d4..000000000000 --- a/packages/twenty-front/src/pages/settings/data-model/__stories__/SettingsObjectNewField/SettingsObjectNewFieldStep1.stories.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import { Meta, StoryObj } from '@storybook/react'; -import { within } from '@storybook/test'; - -import { - PageDecorator, - PageDecoratorArgs, -} from '~/testing/decorators/PageDecorator'; -import { graphqlMocks } from '~/testing/graphqlMocks'; - -import { SettingsObjectNewFieldStep1 } from '../../SettingsObjectNewField/SettingsObjectNewFieldStep1'; - -const meta: Meta<PageDecoratorArgs> = { - title: - 'Pages/Settings/DataModel/SettingsObjectNewField/SettingsObjectNewFieldStep1', - component: SettingsObjectNewFieldStep1, - decorators: [PageDecorator], - args: { - routePath: '/settings/objects/:objectSlug/new-field/step-1', - routeParams: { ':objectSlug': 'companies' }, - }, - parameters: { - msw: graphqlMocks, - }, -}; - -export default meta; - -export type Story = StoryObj<typeof SettingsObjectNewFieldStep1>; - -export const Default: Story = { - play: async ({ canvasElement }) => { - const canvas = within(canvasElement); - await canvas.findByText('Objects'); - await canvas.findByText('Companies'); - await canvas.findByText('Check deactivated fields'); - await canvas.findByText('Add Custom Field'); - }, -}; diff --git a/packages/twenty-front/src/pages/settings/data-model/hooks/useMapFieldMetadataItemToSettingsObjectDetailTableItem.ts b/packages/twenty-front/src/pages/settings/data-model/hooks/useMapFieldMetadataItemToSettingsObjectDetailTableItem.ts index 2a965c0195b5..d221be0b6ccc 100644 --- a/packages/twenty-front/src/pages/settings/data-model/hooks/useMapFieldMetadataItemToSettingsObjectDetailTableItem.ts +++ b/packages/twenty-front/src/pages/settings/data-model/hooks/useMapFieldMetadataItemToSettingsObjectDetailTableItem.ts @@ -1,8 +1,11 @@ import { useGetRelationMetadata } from '@/object-metadata/hooks/useGetRelationMetadata'; import { FieldMetadataItem } from '@/object-metadata/types/FieldMetadataItem'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; +import { FieldType } from '@/settings/data-model/types/FieldType'; +import { SettingsFieldType } from '@/settings/data-model/types/SettingsFieldType'; import { getFieldIdentifierType } from '@/settings/data-model/utils/getFieldIdentifierType'; import { getSettingsFieldTypeConfig } from '@/settings/data-model/utils/getSettingsFieldTypeConfig'; +import { isFieldTypeSupportedInSettings } from '@/settings/data-model/utils/isFieldTypeSupportedInSettings'; import { SettingsObjectDetailTableItem } from '~/pages/settings/data-model/types/SettingsObjectDetailTableItem'; import { getSettingsObjectFieldType } from '~/pages/settings/data-model/utils/getSettingsObjectFieldType'; @@ -29,13 +32,17 @@ export const useMapFieldMetadataItemToSettingsObjectDetailTableItem = ( objectMetadataItem, ); + const fieldMetadataType = fieldMetadataItem.type as FieldType; + return { fieldMetadataItem, fieldType: fieldType ?? '', dataType: - relationObjectMetadataItem?.labelPlural ?? - getSettingsFieldTypeConfig(fieldMetadataItem.type)?.label ?? - '', + (relationObjectMetadataItem?.labelPlural ?? + isFieldTypeSupportedInSettings(fieldMetadataType)) + ? getSettingsFieldTypeConfig(fieldMetadataType as SettingsFieldType) + ?.label + : '', label: fieldMetadataItem.label, identifierType: identifierType, objectMetadataItem, diff --git a/packages/twenty-front/src/pages/settings/developers/SettingsDevelopers.tsx b/packages/twenty-front/src/pages/settings/developers/SettingsDevelopers.tsx index 4cb6cf888713..35ce036694e1 100644 --- a/packages/twenty-front/src/pages/settings/developers/SettingsDevelopers.tsx +++ b/packages/twenty-front/src/pages/settings/developers/SettingsDevelopers.tsx @@ -1,5 +1,6 @@ +import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import styled from '@emotion/styled'; -import { H2Title, IconCode, IconPlus } from 'twenty-ui'; +import { H2Title, IconPlus, MOBILE_VIEWPORT } from 'twenty-ui'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsApiKeysTable } from '@/settings/developers/components/SettingsApiKeysTable'; @@ -15,12 +16,22 @@ const StyledButtonContainer = styled.div` display: flex; justify-content: flex-end; padding-top: ${({ theme }) => theme.spacing(2)}; + @media (max-width: ${MOBILE_VIEWPORT}px) { + padding-top: ${({ theme }) => theme.spacing(5)}; + } +`; + +const StyledContainer = styled.div<{ isMobile: boolean }>` + display: flex; + flex-direction: column; + overflow: hidden; + gap: ${({ theme }) => theme.spacing(2)}; `; export const SettingsDevelopers = () => { + const isMobile = useIsMobile(); return ( <SubMenuTopBarContainer - Icon={IconCode} title="Developers" actionButton={<SettingsReadDocumentationButton />} links={[ @@ -32,38 +43,40 @@ export const SettingsDevelopers = () => { ]} > <SettingsPageContainer> - <Section> - <H2Title - title="API keys" - description="Active APIs keys created by you or your team." - /> - <SettingsApiKeysTable /> - <StyledButtonContainer> - <Button - Icon={IconPlus} - title="Create API key" - size="small" - variant="secondary" - to={'/settings/developers/api-keys/new'} + <StyledContainer isMobile={isMobile}> + <Section> + <H2Title + title="API keys" + description="Active APIs keys created by you or your team." /> - </StyledButtonContainer> - </Section> - <Section> - <H2Title - title="Webhooks" - description="Establish Webhook endpoints for notifications on asynchronous events." - /> - <SettingsWebhooksTable /> - <StyledButtonContainer> - <Button - Icon={IconPlus} - title="Create Webhook" - size="small" - variant="secondary" - to={'/settings/developers/webhooks/new'} + <SettingsApiKeysTable /> + <StyledButtonContainer> + <Button + Icon={IconPlus} + title="Create API key" + size="small" + variant="secondary" + to={'/settings/developers/api-keys/new'} + /> + </StyledButtonContainer> + </Section> + <Section> + <H2Title + title="Webhooks" + description="Establish Webhook endpoints for notifications on asynchronous events." /> - </StyledButtonContainer> - </Section> + <SettingsWebhooksTable /> + <StyledButtonContainer> + <Button + Icon={IconPlus} + title="Create Webhook" + size="small" + variant="secondary" + to={'/settings/developers/webhooks/new'} + /> + </StyledButtonContainer> + </Section> + </StyledContainer> </SettingsPageContainer> </SubMenuTopBarContainer> ); diff --git a/packages/twenty-front/src/pages/settings/developers/__stories__/webhooks/SettingsDevelopersWebhooksDetail.stories.tsx b/packages/twenty-front/src/pages/settings/developers/__stories__/webhooks/SettingsDevelopersWebhooksDetail.stories.tsx index ec430fcf3058..a77e38d73a5b 100644 --- a/packages/twenty-front/src/pages/settings/developers/__stories__/webhooks/SettingsDevelopersWebhooksDetail.stories.tsx +++ b/packages/twenty-front/src/pages/settings/developers/__stories__/webhooks/SettingsDevelopersWebhooksDetail.stories.tsx @@ -2,7 +2,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { within } from '@storybook/test'; import { graphql, HttpResponse } from 'msw'; -import { SettingsDevelopersWebhooksDetail } from '~/pages/settings/developers/webhooks/SettingsDevelopersWebhookDetail'; +import { SettingsDevelopersWebhooksDetail } from '~/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail'; import { PageDecorator, PageDecoratorArgs, diff --git a/packages/twenty-front/src/pages/settings/developers/__stories__/webhooks/SettingsDevelopersWebhooksNew.stories.tsx b/packages/twenty-front/src/pages/settings/developers/__stories__/webhooks/SettingsDevelopersWebhooksNew.stories.tsx index dd4adfb71305..430349183258 100644 --- a/packages/twenty-front/src/pages/settings/developers/__stories__/webhooks/SettingsDevelopersWebhooksNew.stories.tsx +++ b/packages/twenty-front/src/pages/settings/developers/__stories__/webhooks/SettingsDevelopersWebhooksNew.stories.tsx @@ -1,7 +1,7 @@ import { Meta, StoryObj } from '@storybook/react'; import { within } from '@storybook/test'; -import { SettingsDevelopersWebhooksNew } from '~/pages/settings/developers/webhooks/SettingsDevelopersWebhooksNew'; +import { SettingsDevelopersWebhooksNew } from '~/pages/settings/developers/webhooks/components/SettingsDevelopersWebhooksNew'; import { PageDecorator, PageDecoratorArgs, 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 107ce698d43d..7a9651b6893a 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, IconCode, IconRepeat, IconTrash } from 'twenty-ui'; +import { H2Title, IconRepeat, IconTrash } from 'twenty-ui'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; @@ -145,7 +145,6 @@ export const SettingsDevelopersApiKeyDetail = () => { <> {apiKeyData?.name && ( <SubMenuTopBarContainer - Icon={IconCode} title={apiKeyData?.name} links={[ { 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 5e4ebe5cf740..92951f0a5b43 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 @@ -1,7 +1,7 @@ import { DateTime } from 'luxon'; import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { H2Title, IconCode } from 'twenty-ui'; +import { H2Title } from 'twenty-ui'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; @@ -65,7 +65,6 @@ export const SettingsDevelopersApiKeysNew = () => { const canSave = !!formValues.name && createOneApiKey; return ( <SubMenuTopBarContainer - Icon={IconCode} title="New key" links={[ { diff --git a/packages/twenty-front/src/pages/settings/developers/webhooks/SettingsDevelopersWebhookDetail.tsx b/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx similarity index 90% rename from packages/twenty-front/src/pages/settings/developers/webhooks/SettingsDevelopersWebhookDetail.tsx rename to packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx index a45227408fb5..f88ba926c6b8 100644 --- a/packages/twenty-front/src/pages/settings/developers/webhooks/SettingsDevelopersWebhookDetail.tsx +++ b/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx @@ -1,7 +1,7 @@ import styled from '@emotion/styled'; import { useState } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import { H2Title, IconCode, IconTrash } from 'twenty-ui'; +import { H2Title, IconTrash } from 'twenty-ui'; import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; @@ -11,6 +11,8 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { Webhook } from '@/settings/developers/types/webhook/Webhook'; +import { SettingsDeveloppersWebhookUsageGraph } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph'; +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'; @@ -20,6 +22,7 @@ import { TextInput } from '@/ui/input/components/TextInput'; import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; +import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled'; const StyledFilterRow = styled.div` display: flex; @@ -63,6 +66,8 @@ export const SettingsDevelopersWebhooksDetail = () => { navigate(developerPath); }; + const isAnalyticsV2Enabled = useIsFeatureEnabled('IS_ANALYTICS_V2_ENABLED'); + const fieldTypeOptions = [ { value: '*', label: 'All Objects' }, ...objectMetadataItems.map((item) => ({ @@ -93,7 +98,6 @@ export const SettingsDevelopersWebhooksDetail = () => { return ( <SubMenuTopBarContainer - Icon={IconCode} title={webhookData.targetUrl} links={[ { @@ -174,6 +178,14 @@ export const SettingsDevelopersWebhooksDetail = () => { /> </StyledFilterRow> </Section> + {isAnalyticsV2Enabled ? ( + <> + <SettingsDevelopersWebhookUsageGraphEffect webhookId={webhookId} /> + <SettingsDeveloppersWebhookUsageGraph /> + </> + ) : ( + <></> + )} <Section> <H2Title title="Danger zone" description="Delete this integration" /> <Button diff --git a/packages/twenty-front/src/pages/settings/developers/webhooks/SettingsDevelopersWebhooksNew.tsx b/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhooksNew.tsx similarity index 97% rename from packages/twenty-front/src/pages/settings/developers/webhooks/SettingsDevelopersWebhooksNew.tsx rename to packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhooksNew.tsx index 7b75bd09f0fe..b473d3f28c76 100644 --- a/packages/twenty-front/src/pages/settings/developers/webhooks/SettingsDevelopersWebhooksNew.tsx +++ b/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhooksNew.tsx @@ -1,6 +1,6 @@ import { useState } from 'react'; import { useNavigate } from 'react-router-dom'; -import { H2Title, IconCode, isDefined } from 'twenty-ui'; +import { H2Title, isDefined } from 'twenty-ui'; import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; @@ -64,7 +64,6 @@ export const SettingsDevelopersWebhooksNew = () => { return ( <SubMenuTopBarContainer - Icon={IconCode} title="New Webhook" links={[ { diff --git a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationDatabase.tsx b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationDatabase.tsx index 2136ae1ae858..eb8ddd11f2cf 100644 --- a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationDatabase.tsx +++ b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationDatabase.tsx @@ -1,6 +1,6 @@ import { useEffect } from 'react'; import { useNavigate, useParams } from 'react-router-dom'; -import { H2Title, IconSettings } from 'twenty-ui'; +import { H2Title } from 'twenty-ui'; import { useGetDatabaseConnections } from '@/databases/hooks/useGetDatabaseConnections'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; @@ -42,7 +42,6 @@ export const SettingsIntegrationDatabase = () => { return ( <SubMenuTopBarContainer - Icon={IconSettings} title={integration.text} links={[ { diff --git a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationEditDatabaseConnection.tsx b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationEditDatabaseConnection.tsx index 5203f01de89e..92de8008e048 100644 --- a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationEditDatabaseConnection.tsx +++ b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationEditDatabaseConnection.tsx @@ -1,5 +1,3 @@ -import { IconSettings } from 'twenty-ui'; - import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsIntegrationEditDatabaseConnectionContainer } from '@/settings/integrations/database-connection/components/SettingsIntegrationEditDatabaseConnectionContainer'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; @@ -9,7 +7,6 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer' export const SettingsIntegrationEditDatabaseConnection = () => { return ( <SubMenuTopBarContainer - Icon={IconSettings} title="Edit connection" links={[ { diff --git a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx index 1440bd66100b..e10f696446f6 100644 --- a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx +++ b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx @@ -2,7 +2,7 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useEffect } from 'react'; import { FormProvider, useForm } from 'react-hook-form'; import { useNavigate, useParams } from 'react-router-dom'; -import { H2Title, IconSettings } from 'twenty-ui'; +import { H2Title } from 'twenty-ui'; import { z } from 'zod'; import { useCreateOneDatabaseConnection } from '@/databases/hooks/useCreateOneDatabaseConnection'; @@ -131,7 +131,6 @@ export const SettingsIntegrationNewDatabaseConnection = () => { return ( <SubMenuTopBarContainer - Icon={IconSettings} title="New" links={[ { diff --git a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationShowDatabaseConnection.tsx b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationShowDatabaseConnection.tsx index 04fe78e64422..76f1aefa24c8 100644 --- a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationShowDatabaseConnection.tsx +++ b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationShowDatabaseConnection.tsx @@ -1,5 +1,3 @@ -import { IconSettings } from 'twenty-ui'; - import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsIntegrationDatabaseConnectionShowContainer } from '@/settings/integrations/database-connection/components/SettingsIntegrationDatabaseConnectionShowContainer'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; @@ -9,7 +7,6 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer' export const SettingsIntegrationShowDatabaseConnection = () => { return ( <SubMenuTopBarContainer - Icon={IconSettings} title="Database Connection" links={[ { diff --git a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrations.tsx b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrations.tsx index acc3c5cc6777..bfd5db517c93 100644 --- a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrations.tsx +++ b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrations.tsx @@ -4,14 +4,12 @@ import { useSettingsIntegrationCategories } from '@/settings/integrations/hooks/ import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; -import { IconApps } from 'twenty-ui'; export const SettingsIntegrations = () => { const integrationCategories = useSettingsIntegrationCategories(); return ( <SubMenuTopBarContainer - Icon={IconApps} title="Integrations" links={[ { diff --git a/packages/twenty-front/src/pages/settings/integrations/__stories__/SettingsIntegrationDatabase.stories.tsx b/packages/twenty-front/src/pages/settings/integrations/__stories__/SettingsIntegrationDatabase.stories.tsx index e828bfa766e8..fc7cecd82dee 100644 --- a/packages/twenty-front/src/pages/settings/integrations/__stories__/SettingsIntegrationDatabase.stories.tsx +++ b/packages/twenty-front/src/pages/settings/integrations/__stories__/SettingsIntegrationDatabase.stories.tsx @@ -1,3 +1,4 @@ +import { expect } from '@storybook/jest'; import { Meta, StoryObj } from '@storybook/react'; import { within } from '@storybook/test'; @@ -33,6 +34,6 @@ export const Default: Story = { const canvas = within(canvasElement); sleep(1000); - await canvas.findByText('PostgreSQL database'); + expect(await canvas.findByText('PostgreSQL database')).toBeInTheDocument(); }, }; diff --git a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsDateFormatSelect.tsx b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsDateFormatSelect.tsx index c17f7a8e712c..dcadd5ba9460 100644 --- a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsDateFormatSelect.tsx +++ b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsDateFormatSelect.tsx @@ -1,6 +1,7 @@ import { formatInTimeZone } from 'date-fns-tz'; import { DateFormat } from '@/localization/constants/DateFormat'; +import { detectDateFormat } from '@/localization/utils/detectDateFormat'; import { detectTimeZone } from '@/localization/utils/detectTimeZone'; import { Select } from '@/ui/input/components/Select'; @@ -15,7 +16,12 @@ export const DateTimeSettingsDateFormatSelect = ({ timeZone, value, }: DateTimeSettingsDateFormatSelectProps) => { - const setTimeZone = timeZone === 'system' ? detectTimeZone() : timeZone; + const systemTimeZone = detectTimeZone(); + + const usedTimeZone = timeZone === 'system' ? systemTimeZone : timeZone; + + const systemDateFormat = detectDateFormat(); + return ( <Select dropdownId="datetime-settings-date-format" @@ -25,13 +31,17 @@ export const DateTimeSettingsDateFormatSelect = ({ value={value} options={[ { - label: `System settings`, + label: `System settings - ${formatInTimeZone( + Date.now(), + usedTimeZone, + systemDateFormat, + )}`, value: DateFormat.SYSTEM, }, { label: `${formatInTimeZone( Date.now(), - setTimeZone, + usedTimeZone, DateFormat.MONTH_FIRST, )}`, value: DateFormat.MONTH_FIRST, @@ -39,7 +49,7 @@ export const DateTimeSettingsDateFormatSelect = ({ { label: `${formatInTimeZone( Date.now(), - setTimeZone, + usedTimeZone, DateFormat.DAY_FIRST, )}`, value: DateFormat.DAY_FIRST, @@ -47,7 +57,7 @@ export const DateTimeSettingsDateFormatSelect = ({ { label: `${formatInTimeZone( Date.now(), - setTimeZone, + usedTimeZone, DateFormat.YEAR_FIRST, )}`, value: DateFormat.YEAR_FIRST, diff --git a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsTimeFormatSelect.tsx b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsTimeFormatSelect.tsx index 9aa24254b4b1..e06cde4cc0ff 100644 --- a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsTimeFormatSelect.tsx +++ b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsTimeFormatSelect.tsx @@ -1,6 +1,7 @@ import { formatInTimeZone } from 'date-fns-tz'; import { TimeFormat } from '@/localization/constants/TimeFormat'; +import { detectTimeFormat } from '@/localization/utils/detectTimeFormat'; import { detectTimeZone } from '@/localization/utils/detectTimeZone'; import { Select } from '@/ui/input/components/Select'; @@ -15,7 +16,12 @@ export const DateTimeSettingsTimeFormatSelect = ({ timeZone, value, }: DateTimeSettingsTimeFormatSelectProps) => { - const setTimeZone = timeZone === 'system' ? detectTimeZone() : timeZone; + const systemTimeZone = detectTimeZone(); + + const usedTimeZone = timeZone === 'system' ? systemTimeZone : timeZone; + + const systemTimeFormat = detectTimeFormat(); + return ( <Select dropdownId="datetime-settings-time-format" @@ -25,13 +31,17 @@ export const DateTimeSettingsTimeFormatSelect = ({ value={value} options={[ { - label: 'System settings', + label: `System Settings - ${formatInTimeZone( + Date.now(), + usedTimeZone, + systemTimeFormat, + )}`, value: TimeFormat.SYSTEM, }, { label: `24h (${formatInTimeZone( Date.now(), - setTimeZone, + usedTimeZone, TimeFormat.HOUR_24, )})`, value: TimeFormat.HOUR_24, @@ -39,7 +49,7 @@ export const DateTimeSettingsTimeFormatSelect = ({ { label: `12h (${formatInTimeZone( Date.now(), - setTimeZone, + usedTimeZone, TimeFormat.HOUR_12, )})`, value: TimeFormat.HOUR_12, diff --git a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsTimeZoneSelect.tsx b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsTimeZoneSelect.tsx index a48dea5db8de..348cfded0779 100644 --- a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsTimeZoneSelect.tsx +++ b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsTimeZoneSelect.tsx @@ -12,19 +12,22 @@ export const DateTimeSettingsTimeZoneSelect = ({ value = detectTimeZone(), onChange, }: DateTimeSettingsTimeZoneSelectProps) => { + const systemTimeZone = detectTimeZone(); + + const systemTimeZoneOption = findAvailableTimeZoneOption(systemTimeZone); + return ( <Select dropdownId="settings-accounts-calendar-time-zone" dropdownWidth={416} label="Time zone" fullWidth - value={ - value === 'system' - ? 'System settings' - : findAvailableTimeZoneOption(value)?.value - } + value={value} options={[ - { label: 'System settings', value: 'system' }, + { + label: `System settings - ${systemTimeZoneOption.label}`, + value: 'system', + }, ...AVAILABLE_TIMEZONE_OPTIONS, ]} onChange={onChange} 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 f3fe094bcb15..85ca252abfba 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,4 +1,4 @@ -import { H2Title, IconColorSwatch } from 'twenty-ui'; +import { H2Title } from 'twenty-ui'; import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; @@ -14,7 +14,6 @@ export const SettingsAppearance = () => { return ( <SubMenuTopBarContainer - Icon={IconColorSwatch} title="Experience" links={[ { 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 efd06b555139..2934066fd1a4 100644 --- a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx +++ b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionDetail.tsx @@ -21,9 +21,10 @@ import { useTabList } from '@/ui/layout/tab/hooks/useTabList'; import { useState } from 'react'; import { useParams } from 'react-router-dom'; import { useRecoilValue, useSetRecoilState } from 'recoil'; -import { IconCode, IconFunction, IconSettings, IconTestPipe } from 'twenty-ui'; +import { IconCode, IconSettings, IconTestPipe } from 'twenty-ui'; import { usePreventOverlapCallback } from '~/hooks/usePreventOverlapCallback'; import { isDefined } from '~/utils/isDefined'; +import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; const TAB_LIST_COMPONENT_ID = 'serverless-function-detail'; @@ -81,14 +82,24 @@ export const SettingsServerlessFunctionDetail = () => { }; }; + const onCodeChange = async (filePath: string, value: string) => { + setFormValues((prevState) => ({ + ...prevState, + code: { ...prevState.code, [filePath]: value }, + })); + await handleSave(); + }; + const resetDisabled = - !isDefined(latestVersionCode) || latestVersionCode === formValues.code; - const publishDisabled = !isCodeValid || latestVersionCode === formValues.code; + !isDefined(latestVersionCode) || + isDeeplyEqual(latestVersionCode, formValues.code); + const publishDisabled = + !isCodeValid || isDeeplyEqual(latestVersionCode, formValues.code); const handleReset = async () => { try { const newState = { - code: latestVersionCode || '', + code: latestVersionCode || {}, }; setFormValues((prevState) => ({ ...prevState, @@ -166,18 +177,30 @@ export const SettingsServerlessFunctionDetail = () => { { id: 'settings', title: 'Settings', Icon: IconSettings }, ]; + const files = formValues.code + ? Object.keys(formValues.code) + .map((key) => { + return { + path: key, + language: key === '.env' ? 'ini' : 'typescript', + content: formValues.code?.[key] || '', + }; + }) + .reverse() + : []; + const renderActiveTabContent = () => { switch (activeTabId) { case 'editor': return ( <SettingsServerlessFunctionCodeEditorTab - formValues={formValues} + files={files} handleExecute={handleExecute} handlePublish={handlePublish} handleReset={handleReset} resetDisabled={resetDisabled} publishDisabled={publishDisabled} - onChange={onChange} + onChange={onCodeChange} setIsCodeValid={setIsCodeValid} /> ); @@ -204,7 +227,6 @@ export const SettingsServerlessFunctionDetail = () => { return ( !loading && ( <SubMenuTopBarContainer - Icon={IconFunction} title={formValues.name} links={[ { 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 00dcabb77bdc..9cfd9b04ca42 100644 --- a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctions.tsx +++ b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctions.tsx @@ -1,4 +1,3 @@ -import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer'; import { SettingsServerlessFunctionsTable } from '@/settings/serverless-functions/components/SettingsServerlessFunctionsTable'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; @@ -6,12 +5,11 @@ import { Button } from '@/ui/input/button/components/Button'; import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'; import { Section } from '@/ui/layout/section/components/Section'; import { UndecoratedLink } from '@/ui/navigation/link/components/UndecoratedLink'; -import { IconFunction, IconPlus } from 'twenty-ui'; +import { IconPlus } from 'twenty-ui'; export const SettingsServerlessFunctions = () => { return ( <SubMenuTopBarContainer - Icon={IconFunction} title="Functions" actionButton={ <UndecoratedLink @@ -35,11 +33,9 @@ export const SettingsServerlessFunctions = () => { }, ]} > - <SettingsPageContainer> - <Section> - <SettingsServerlessFunctionsTable /> - </Section> - </SettingsPageContainer> + <Section> + <SettingsServerlessFunctionsTable /> + </Section> </SubMenuTopBarContainer> ); }; 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 24308d53a3d1..3e43717544ce 100644 --- a/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionsNew.tsx +++ b/packages/twenty-front/src/pages/settings/serverless-functions/SettingsServerlessFunctionsNew.tsx @@ -9,11 +9,9 @@ import { ServerlessFunctionNewFormValues } from '@/settings/serverless-functions import { SettingsServerlessFunctionHotkeyScope } from '@/settings/serverless-functions/types/SettingsServerlessFunctionHotKeyScope'; import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath'; import { SettingsPath } from '@/types/SettingsPath'; -import { DEFAULT_CODE } from '@/ui/input/code-editor/components/CodeEditor'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { useState } from 'react'; import { Key } from 'ts-key-enum'; -import { IconFunction } from 'twenty-ui'; import { useHotkeyScopeOnMount } from '~/hooks/useHotkeyScopeOnMount'; import { isDefined } from '~/utils/isDefined'; @@ -31,7 +29,6 @@ export const SettingsServerlessFunctionsNew = () => { const newServerlessFunction = await createOneServerlessFunction({ name: formValues.name, description: formValues.description, - code: DEFAULT_CODE, }); if (!isDefined(newServerlessFunction?.data)) { @@ -79,7 +76,6 @@ export const SettingsServerlessFunctionsNew = () => { return ( <SubMenuTopBarContainer - Icon={IconFunction} title="New Function" links={[ { diff --git a/packages/twenty-front/src/pages/settings/serverless-functions/__stories__/SettingsServerlessFunctionDetail.stories.tsx b/packages/twenty-front/src/pages/settings/serverless-functions/__stories__/SettingsServerlessFunctionDetail.stories.tsx index 8543e0376a6e..f99523f7d5d9 100644 --- a/packages/twenty-front/src/pages/settings/serverless-functions/__stories__/SettingsServerlessFunctionDetail.stories.tsx +++ b/packages/twenty-front/src/pages/settings/serverless-functions/__stories__/SettingsServerlessFunctionDetail.stories.tsx @@ -1,4 +1,3 @@ -import { DEFAULT_CODE } from '@/ui/input/code-editor/components/CodeEditor'; import { Meta, StoryObj } from '@storybook/react'; import { within } from '@storybook/test'; import { graphql, http, HttpResponse } from 'msw'; @@ -38,7 +37,6 @@ const meta: Meta<PageDecoratorArgs> = { description: '', syncStatus: 'READY', runtime: 'nodejs18.x', - sourceCodeHash: '42d2734b3dc8a7b45a16803ed7f417bc', updatedAt: '2024-02-24T10:23:10.673Z', createdAt: '2024-02-24T10:23:10.673Z', }, @@ -46,7 +44,7 @@ const meta: Meta<PageDecoratorArgs> = { }); }), http.get(getImageAbsoluteURI(SOURCE_CODE_FULL_PATH) || '', () => { - return HttpResponse.text(DEFAULT_CODE); + return HttpResponse.text('export const handler = () => {}'); }), ], }, diff --git a/packages/twenty-front/src/testing/decorators/ChipGeneratorsDecorator.tsx b/packages/twenty-front/src/testing/decorators/ChipGeneratorsDecorator.tsx index 283e7046e0ab..c107d5466387 100644 --- a/packages/twenty-front/src/testing/decorators/ChipGeneratorsDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/ChipGeneratorsDecorator.tsx @@ -3,7 +3,7 @@ import { useMemo } from 'react'; import { PreComputedChipGeneratorsContext } from '@/object-metadata/context/PreComputedChipGeneratorsContext'; import { getRecordChipGenerators } from '@/object-record/utils/getRecordChipGenerators'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; export const ChipGeneratorsDecorator: Decorator = (Story) => { const { chipGeneratorPerObjectPerField, identifierChipGeneratorPerObject } = diff --git a/packages/twenty-front/src/testing/decorators/PageDecorator.tsx b/packages/twenty-front/src/testing/decorators/PageDecorator.tsx index 2381241c4a06..244772c809e6 100644 --- a/packages/twenty-front/src/testing/decorators/PageDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/PageDecorator.tsx @@ -12,8 +12,7 @@ import { import { RecoilRoot } from 'recoil'; import { ClientConfigProviderEffect } from '@/client-config/components/ClientConfigProviderEffect'; -import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider'; -import { ApolloMetadataClientMockedProvider } from '@/object-metadata/hooks/__mocks__/ApolloMetadataClientProvider'; +import { ApolloMetadataClientMockedProvider } from '@/object-metadata/hooks/__mocks__/ApolloMetadataClientMockedProvider'; import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import { UserProviderEffect } from '@/users/components/UserProviderEffect'; import { ClientConfigProvider } from '~/modules/client-config/components/ClientConfigProvider'; @@ -22,6 +21,7 @@ import { UserProvider } from '~/modules/users/components/UserProvider'; import { mockedApolloClient } from '~/testing/mockedApolloClient'; import { RecoilDebugObserverEffect } from '@/debug/components/RecoilDebugObserver'; +import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider'; import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider'; import { IconsProvider } from 'twenty-ui'; import { FullHeightStorybookLayout } from '../FullHeightStorybookLayout'; @@ -65,31 +65,33 @@ const ApolloStorybookDevLogEffect = () => { const Providers = () => { return ( <RecoilRoot> - <RecoilDebugObserverEffect /> - <ApolloProvider client={mockedApolloClient}> - <ApolloStorybookDevLogEffect /> - <ApolloMetadataClientMockedProvider> - <UserProviderEffect /> - <UserProvider> - <ClientConfigProviderEffect /> - <ClientConfigProvider> - <FullHeightStorybookLayout> - <HelmetProvider> - <SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager"> - <IconsProvider> - <ObjectMetadataItemsProvider> - <PrefetchDataProvider> - <Outlet /> - </PrefetchDataProvider> - </ObjectMetadataItemsProvider> - </IconsProvider> - </SnackBarProviderScope> - </HelmetProvider> - </FullHeightStorybookLayout> - </ClientConfigProvider> - </UserProvider> - </ApolloMetadataClientMockedProvider> - </ApolloProvider> + <SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager"> + <RecoilDebugObserverEffect /> + <ApolloProvider client={mockedApolloClient}> + <ApolloStorybookDevLogEffect /> + <ClientConfigProviderEffect /> + <ClientConfigProvider> + <UserProviderEffect /> + <UserProvider> + <ApolloMetadataClientMockedProvider> + <ObjectMetadataItemsProvider> + <FullHeightStorybookLayout> + <HelmetProvider> + <SnackBarProviderScope snackBarManagerScopeId="snack-bar-manager"> + <IconsProvider> + <PrefetchDataProvider> + <Outlet /> + </PrefetchDataProvider> + </IconsProvider> + </SnackBarProviderScope> + </HelmetProvider> + </FullHeightStorybookLayout> + </ObjectMetadataItemsProvider> + </ApolloMetadataClientMockedProvider> + </UserProvider> + </ClientConfigProvider> + </ApolloProvider> + </SnackBarProviderScope> </RecoilRoot> ); }; diff --git a/packages/twenty-front/src/testing/decorators/RootDecorator.tsx b/packages/twenty-front/src/testing/decorators/RootDecorator.tsx index 9c2633037b5b..5808791d1ae2 100644 --- a/packages/twenty-front/src/testing/decorators/RootDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/RootDecorator.tsx @@ -2,7 +2,7 @@ import { ApolloProvider } from '@apollo/client'; import { Decorator } from '@storybook/react'; import { RecoilRoot } from 'recoil'; -import { ApolloMetadataClientMockedProvider } from '@/object-metadata/hooks/__mocks__/ApolloMetadataClientProvider'; +import { ApolloMetadataClientMockedProvider } from '@/object-metadata/hooks/__mocks__/ApolloMetadataClientMockedProvider'; import { InitializeHotkeyStorybookHookEffect } from '../InitializeHotkeyStorybookHook'; import { mockedApolloClient } from '../mockedApolloClient'; diff --git a/packages/twenty-front/src/testing/decorators/getFieldDecorator.tsx b/packages/twenty-front/src/testing/decorators/getFieldDecorator.tsx index 62cee6f5077b..e61885ed9a0d 100644 --- a/packages/twenty-front/src/testing/decorators/getFieldDecorator.tsx +++ b/packages/twenty-front/src/testing/decorators/getFieldDecorator.tsx @@ -12,7 +12,7 @@ import { import { recordStoreFamilyState } from '@/object-record/record-store/states/recordStoreFamilyState'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { getCompaniesMock } from '~/testing/mock-data/companies'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { getPeopleMock } from '~/testing/mock-data/people'; import { mockedTasks } from '~/testing/mock-data/tasks'; import { isDefined } from '~/utils/isDefined'; @@ -56,7 +56,7 @@ const RecordMockSetterEffect = ({ export const getFieldDecorator = ( - objectNameSingular: 'company' | 'person' | 'task', + objectNameSingular: 'company' | 'person' | 'task' | 'workflowVersions', fieldName: string, fieldValue?: any, ): Decorator => diff --git a/packages/twenty-front/src/testing/graphqlMocks.ts b/packages/twenty-front/src/testing/graphqlMocks.ts index c5d319b460b7..a69d6c17c660 100644 --- a/packages/twenty-front/src/testing/graphqlMocks.ts +++ b/packages/twenty-front/src/testing/graphqlMocks.ts @@ -11,7 +11,6 @@ import { getCompanyDuplicateMock, } from '~/testing/mock-data/companies'; import { mockedClientConfig } from '~/testing/mock-data/config'; -import { mockedObjectMetadataItemsQueryResult } from '~/testing/mock-data/metadata'; import { mockedNotes } from '~/testing/mock-data/notes'; import { getPeopleMock } from '~/testing/mock-data/people'; import { mockedRemoteTables } from '~/testing/mock-data/remote-tables'; @@ -19,6 +18,7 @@ import { mockedUserData } from '~/testing/mock-data/users'; import { mockedViewsData } from '~/testing/mock-data/views'; import { mockWorkspaceMembers } from '~/testing/mock-data/workspace-members'; +import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/mock-metadata-query-result'; import { mockedTasks } from '~/testing/mock-data/tasks'; import { mockedRemoteServers } from './mock-data/remote-servers'; import { mockedViewFieldsData } from './mock-data/view-fields'; @@ -58,7 +58,7 @@ export const graphqlMocks = { getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? '', () => { return HttpResponse.json({ - data: mockedObjectMetadataItemsQueryResult, + data: mockedStandardObjectMetadataQueryResult, }); }, ), @@ -297,7 +297,7 @@ export const graphqlMocks = { graphql.query('FindManyTasks', () => { return HttpResponse.json({ data: { - activities: { + tasks: { edges: mockedTasks.map(({ taskTargets, ...rest }) => ({ node: { ...rest, @@ -320,6 +320,26 @@ export const graphqlMocks = { }, }); }), + graphql.query('FindManyTaskTargets', () => { + return HttpResponse.json({ + data: { + taskTargets: { + edges: mockedTasks.flatMap((task) => + task.taskTargets.map((target) => ({ + node: target, + cursor: null, + })), + ), + pageInfo: { + hasNextPage: false, + hasPreviousPage: false, + startCursor: null, + endCursor: null, + }, + }, + }, + }); + }), graphql.query('FindManyFavorites', () => { return HttpResponse.json({ data: { diff --git a/packages/twenty-front/src/testing/jest/JestObjectMetadataItemSetter.tsx b/packages/twenty-front/src/testing/jest/JestObjectMetadataItemSetter.tsx index abd11cab1832..98e83cb833a5 100644 --- a/packages/twenty-front/src/testing/jest/JestObjectMetadataItemSetter.tsx +++ b/packages/twenty-front/src/testing/jest/JestObjectMetadataItemSetter.tsx @@ -2,7 +2,7 @@ import { ReactNode, useEffect, useState } from 'react'; import { useSetRecoilState } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { generatedMockObjectMetadataItems } from '~/testing/mock-data/objectMetadataItems'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; export const JestObjectMetadataItemSetter = ({ children, diff --git a/packages/twenty-front/src/testing/jest/generateEmptyJestRecordNode.ts b/packages/twenty-front/src/testing/jest/generateEmptyJestRecordNode.ts new file mode 100644 index 000000000000..f27e4f3a3cd8 --- /dev/null +++ b/packages/twenty-front/src/testing/jest/generateEmptyJestRecordNode.ts @@ -0,0 +1,37 @@ +import { getRecordNodeFromRecord } from '@/object-record/cache/utils/getRecordNodeFromRecord'; +import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields'; +import { prefillRecord } from '@/object-record/utils/prefillRecord'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; + +export const generateEmptyJestRecordNode = ({ + objectNameSingular, + input, + withDepthOneRelation = false, +}: { + objectNameSingular: string; + input: Record<string, unknown>; + withDepthOneRelation?: boolean; +}) => { + const objectMetadataItem = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === objectNameSingular, + ); + + if (!objectMetadataItem) { + throw new Error( + `ObjectMetadataItem not found for objectNameSingular: ${objectNameSingular} while generating empty Jest record node`, + ); + } + + const prefilledRecord = prefillRecord({ objectMetadataItem, input }); + + return getRecordNodeFromRecord({ + record: prefilledRecord, + objectMetadataItem, + objectMetadataItems: generatedMockObjectMetadataItems, + recordGqlFields: withDepthOneRelation + ? generateDepthOneRecordGqlFields({ + objectMetadataItem, + }) + : undefined, + }); +}; diff --git a/packages/twenty-front/src/testing/jest/getJestHookWrapper.tsx b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksWrapper.tsx similarity index 94% rename from packages/twenty-front/src/testing/jest/getJestHookWrapper.tsx rename to packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksWrapper.tsx index 3c8f21553323..ea0e6528f0bc 100644 --- a/packages/twenty-front/src/testing/jest/getJestHookWrapper.tsx +++ b/packages/twenty-front/src/testing/jest/getJestMetadataAndApolloMocksWrapper.tsx @@ -5,7 +5,7 @@ import { MutableSnapshot, RecoilRoot } from 'recoil'; import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope'; import { JestObjectMetadataItemSetter } from '~/testing/jest/JestObjectMetadataItemSetter'; -export const getJestHookWrapper = ({ +export const getJestMetadataAndApolloMocksWrapper = ({ apolloMocks, onInitializeRecoilSnapshot, }: { diff --git a/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts b/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts index c30b74ec2344..86428fa72919 100644 --- a/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts +++ b/packages/twenty-front/src/testing/mock-data/generated/mock-metadata-query-result.ts @@ -51,6 +51,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'ed0dfa31-8e2f-4b23-87e4-8fa55eb16729', type: 'TEXT', name: 'operation', @@ -72,6 +73,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '814795ef-6f2d-4798-a6f9-4e1c87c68d43', type: 'DATE_TIME', name: 'deletedAt', @@ -93,6 +95,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '1c802ccf-c0ae-4b04-8c1e-f77417e6c3f8', type: 'DATE_TIME', name: 'updatedAt', @@ -114,6 +117,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '6f210155-9cdc-48c6-9803-e20f63512024', type: 'DATE_TIME', name: 'createdAt', @@ -135,6 +139,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '5fa54f64-3363-4a21-89ca-30d4816d8c77', type: 'UUID', name: 'id', @@ -156,6 +161,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'a8876650-a7b6-4a9f-95b4-9ec1d6c232cc', type: 'TEXT', name: 'targetUrl', @@ -177,6 +183,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'afae3f60-bfeb-4faf-a899-b0eb0fefac51', type: 'TEXT', name: 'description', @@ -233,6 +240,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'ad82920a-857a-4357-8e4a-ed70961ba5d8', type: 'DATE_TIME', name: 'updatedAt', @@ -254,6 +262,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '89662f00-b57c-49f6-aa48-d1c84f5fd7c7', type: 'UUID', name: 'personId', @@ -275,6 +284,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '19270b1d-73b4-4aa9-8106-c1c81351ec53', type: 'UUID', name: 'rocketId', @@ -296,6 +306,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '0c796ac5-0592-455f-9a0a-66ad53c6e4cf', type: 'UUID', name: 'companyId', @@ -317,6 +328,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'dadac630-f64e-4d3d-9923-78ca579373f3', type: 'RELATION', name: 'rocket', @@ -364,6 +376,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '0fcdebbb-a332-45ec-ab46-c30d5d7f9ef0', type: 'DATE_TIME', name: 'createdAt', @@ -385,6 +398,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '37882bc2-c8d8-4cc5-bc13-4b820cc05b83', type: 'UUID', name: 'taskId', @@ -406,6 +420,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'da63ddb4-7c19-49f0-bf90-ac2cc9486ae7', type: 'DATE_TIME', name: 'deletedAt', @@ -427,6 +442,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '26cc6ba3-cff7-4b84-bf78-71823187a824', type: 'RELATION', name: 'person', @@ -474,6 +490,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e4414119-6f9c-465a-9ee2-95d1fc5eec01', type: 'UUID', name: 'opportunityId', @@ -495,6 +512,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '598688a1-9766-439d-abd3-c0a47c8f36a3', type: 'RELATION', name: 'opportunity', @@ -542,6 +560,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'cd7f57e8-a67f-4be9-a971-b5609cb0fb83', type: 'RELATION', name: 'company', @@ -589,6 +608,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '400c9e53-283b-42d8-a69f-5010fb75d977', type: 'UUID', name: 'id', @@ -610,6 +630,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'ec532542-a4dc-4722-99c3-fca6366db597', type: 'RELATION', name: 'task', @@ -692,6 +713,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '26c57af1-5c70-4a9d-974f-e54c6a77a2b4', type: 'RELATION', name: 'rocket', @@ -739,6 +761,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '434d1fd2-e6e0-4de7-9b15-706398e34d2d', type: 'DATE_TIME', name: 'deletedAt', @@ -760,6 +783,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '06fcb5e2-b2f7-4118-a9f0-34558429b72c', type: 'DATE_TIME', name: 'updatedAt', @@ -781,6 +805,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '4b6aaf36-4247-4bbb-b26d-64987b02f805', type: 'RELATION', name: 'company', @@ -828,6 +853,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c9bbd140-d9ab-4557-bd77-b446cd80774b', type: 'UUID', name: 'personId', @@ -849,6 +875,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b86fd021-b648-4a1e-b02e-080f0b280303', type: 'UUID', name: 'rocketId', @@ -870,6 +897,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'cd3a9c3d-a29e-4b27-9fdb-0e8959f21f10', type: 'UUID', name: 'opportunityId', @@ -891,6 +919,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e6a5a8b1-ebef-4e01-ba22-a5f86d894eb5', type: 'UUID', name: 'id', @@ -912,6 +941,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '9d568132-6cbc-4e87-95e8-7c2509549391', type: 'RELATION', name: 'note', @@ -959,6 +989,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '6f80afcd-8ed7-4bf9-a987-9de3d1cddc81', type: 'RELATION', name: 'opportunity', @@ -1006,6 +1037,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'dc63ee78-06ac-4312-b223-1d41a7ea2af4', type: 'UUID', name: 'noteId', @@ -1027,6 +1059,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '646f3b4b-0fad-495a-a90d-136593464c7f', type: 'UUID', name: 'companyId', @@ -1048,6 +1081,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '2b9481a9-c605-45d0-8aad-801a19c4b92c', type: 'DATE_TIME', name: 'createdAt', @@ -1069,6 +1103,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '77508442-f0de-4809-b690-3c998edfc0b5', type: 'RELATION', name: 'person', @@ -1151,6 +1186,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '27e79204-ba19-4791-8110-ec2bdc523e07', type: 'TEXT', name: 'messageThreadExternalId', @@ -1172,6 +1208,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '1d9f0c85-c16f-41e9-9241-2acd90781cdd', type: 'RELATION', name: 'message', @@ -1219,6 +1256,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '72a85b74-9803-4279-9f74-dafb833847fb', type: 'DATE_TIME', name: 'createdAt', @@ -1240,6 +1278,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '6cb121b9-32d1-44fe-af26-324d73ffe0ac', type: 'DATE_TIME', name: 'deletedAt', @@ -1261,6 +1300,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '9491ddd2-aea5-47fe-bd93-09fb6969b20c', type: 'TEXT', name: 'messageExternalId', @@ -1282,6 +1322,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f3eafb28-947a-4c7a-9464-24fa5549fb03', type: 'UUID', name: 'id', @@ -1303,6 +1344,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'dbfff493-11a0-4866-9ef6-e4e8418a661a', type: 'UUID', name: 'messageChannelId', @@ -1324,6 +1366,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '95488a9d-6473-47e8-aa62-a04d49238a2f', type: 'UUID', name: 'messageId', @@ -1345,6 +1388,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f4582d66-df12-4499-8ede-ab347427241b', type: 'RELATION', name: 'messageChannel', @@ -1392,6 +1436,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7bf79a5a-f5b7-495e-a336-4ddf85a5b2f7', type: 'SELECT', name: 'direction', @@ -1428,6 +1473,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'a18edc4a-1b58-4a78-8de4-9564479b09cc', type: 'DATE_TIME', name: 'updatedAt', @@ -1484,6 +1530,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'ea62fcb2-2161-47db-9151-19011419ac66', type: 'DATE_TIME', name: 'createdAt', @@ -1505,6 +1552,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e2f2b624-93bf-4d16-8517-bf66e43cabc4', type: 'RELATION', name: 'message', @@ -1552,6 +1600,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b92e3ff7-e5e0-4fec-b36e-cb496e2b57ae', type: 'UUID', name: 'id', @@ -1573,6 +1622,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'ef5a67e6-88dd-4c92-a9be-7ab0605804e7', type: 'DATE_TIME', name: 'deletedAt', @@ -1594,6 +1644,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f3db188f-7329-40bc-9978-e30e5c07d962', type: 'RELATION', name: 'person', @@ -1641,6 +1692,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '26c81156-529d-4bec-b5fb-f92e991907b5', type: 'UUID', name: 'messageId', @@ -1662,6 +1714,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'aa502069-91c1-4eeb-bc8e-b22240564fe1', type: 'TEXT', name: 'displayName', @@ -1683,6 +1736,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd205df4f-f9de-4f61-9a85-40f340a4de23', type: 'RELATION', name: 'workspaceMember', @@ -1730,6 +1784,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '0cf35f0a-eb64-47ef-88a4-55b92ca57c64', type: 'UUID', name: 'personId', @@ -1751,6 +1806,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e8550477-6034-45f4-8370-5a1cd75f7a55', type: 'UUID', name: 'workspaceMemberId', @@ -1772,6 +1828,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '8dcde658-c38a-4659-8546-89c60465d36e', type: 'TEXT', name: 'handle', @@ -1793,6 +1850,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '56ff0ed7-7916-4610-834c-a7c0657fa9e7', type: 'DATE_TIME', name: 'updatedAt', @@ -1814,6 +1872,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7a8597e3-bbce-4ff5-9f6e-9bf3c7fd43fa', type: 'SELECT', name: 'role', @@ -1899,6 +1958,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '9061081a-eed9-49b8-99ab-9a7b8ce7a355', type: 'BOOLEAN', name: 'isContactAutoCreationEnabled', @@ -1920,6 +1980,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'df104a7c-39e5-491d-8bde-6e3f75a3156b', type: 'DATE_TIME', name: 'syncedAt', @@ -1941,6 +2002,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '751ef307-be5e-4451-9434-e2bae8861873', type: 'BOOLEAN', name: 'excludeNonProfessionalEmails', @@ -1962,6 +2024,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '9a8520ed-6577-46b0-a732-1a08aafb0160', type: 'SELECT', name: 'visibility', @@ -2005,6 +2068,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '2f7f7ea2-c5d0-47bc-bb0e-e3afc6d82b91', type: 'DATE_TIME', name: 'updatedAt', @@ -2026,6 +2090,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'dafc931d-0466-4c83-91b3-72be3fdee12f', type: 'DATE_TIME', name: 'createdAt', @@ -2047,6 +2112,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '97ba10a8-da33-4d1f-a9c6-964f814f5fd7', type: 'DATE_TIME', name: 'deletedAt', @@ -2068,6 +2134,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'a3c22a32-9c6c-4294-abc4-7b3f9b6d8816', type: 'RELATION', name: 'messageChannelMessageAssociations', @@ -2115,6 +2182,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c1686e5a-1af6-45d9-a9f8-d2ecaef71526', type: 'RELATION', name: 'connectedAccount', @@ -2162,6 +2230,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7eaa0427-04b4-4cf8-9676-4e28c88a0fa8', type: 'DATE_TIME', name: 'syncStageStartedAt', @@ -2183,6 +2252,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '290318a3-e994-4dee-8dd1-6bebe92043af', type: 'SELECT', name: 'syncStatus', @@ -2240,6 +2310,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7238d6c1-c54c-4787-aa4f-79294220acdc', type: 'BOOLEAN', name: 'isSyncEnabled', @@ -2261,6 +2332,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '9416f68a-84cb-4b1f-805b-e58fb009cc44', type: 'TEXT', name: 'syncCursor', @@ -2282,6 +2354,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '09ec080b-c8fe-432f-aff1-c151861c3ec7', type: 'SELECT', name: 'contactAutoCreationPolicy', @@ -2326,6 +2399,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd30ddf0c-769a-4480-a9cf-fea5867f9eea', type: 'BOOLEAN', name: 'excludeGroupEmails', @@ -2347,6 +2421,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '839a7a90-3b1a-4f76-8207-5a0779ca909d', type: 'SELECT', name: 'type', @@ -2383,6 +2458,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b0026844-d54e-4fe2-af36-ed7ac7be1833', type: 'UUID', name: 'connectedAccountId', @@ -2404,6 +2480,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7ca7d194-3cdc-4ff9-a90f-6bfaedba8280', type: 'TEXT', name: 'handle', @@ -2425,6 +2502,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '5f87d969-7dae-4a25-8b11-9577aa11285e', type: 'UUID', name: 'id', @@ -2446,6 +2524,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'aafcc52e-d761-4b6a-97a6-1bc097e2ccd2', type: 'SELECT', name: 'syncStage', @@ -2510,6 +2589,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '33180dbd-4124-43eb-84c3-17e32b4848e4', type: 'NUMBER', name: 'throttleFailureCount', @@ -2566,6 +2646,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '8ca32bd7-4999-4562-9e41-698a458944ea', type: 'DATE_TIME', name: 'deletedAt', @@ -2587,6 +2668,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '236195db-49d1-4386-99b9-4518ab7586f2', type: 'RELATION', name: 'accountOwner', @@ -2634,6 +2716,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '919dadd4-8dda-479f-a8a7-b4ed89eafae5', type: 'RELATION', name: 'calendarChannels', @@ -2681,6 +2764,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '75c78041-39ca-4b46-bcff-6ed0af05248e', type: 'TEXT', name: 'handleAliases', @@ -2702,6 +2786,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd3f821eb-c8b1-48a5-aebb-1bfe23c4128e', type: 'DATE_TIME', name: 'updatedAt', @@ -2723,6 +2808,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '6f74d250-76f0-4eff-8e27-d1a12782517d', type: 'TEXT', name: 'lastSyncHistoryId', @@ -2744,6 +2830,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'fd6c04f4-8dba-47ac-a597-90300eb1a079', type: 'UUID', name: 'id', @@ -2765,6 +2852,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '8f7d7528-83a8-442e-b0b6-958f52a1de5e', type: 'DATE_TIME', name: 'authFailedAt', @@ -2786,6 +2874,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '762d59c0-f7e9-410f-b5dc-df66d764de0d', type: 'TEXT', name: 'handle', @@ -2808,6 +2897,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f45f5e44-f88a-490c-b112-df2465b612a3', type: 'TEXT', name: 'refreshToken', @@ -2829,6 +2919,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '2aa282f3-5542-47e8-adf3-4384e9ce5d10', type: 'UUID', name: 'accountOwnerId', @@ -2850,6 +2941,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '5f411e82-b626-4456-896c-d5a326e5e02a', type: 'TEXT', name: 'accessToken', @@ -2871,6 +2963,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c21734fd-6539-410f-bc1a-e91a3177b9c9', type: 'DATE_TIME', name: 'createdAt', @@ -2892,6 +2985,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '5b0a8a38-9d02-4e06-8f07-2f76f0ab70eb', type: 'TEXT', name: 'provider', @@ -2913,6 +3007,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '178f1e8a-cbbf-448e-95f3-d1262d9ff33d', type: 'RELATION', name: 'messageChannels', @@ -2995,6 +3090,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '9e7b7d2f-02fb-426b-8e3e-392225f5b6b3', type: 'RELATION', name: 'taskTargets', @@ -3042,6 +3138,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '78f1d502-397e-4cce-b096-a525b2d373e2', type: 'RELATION', name: 'noteTargets', @@ -3089,6 +3186,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c692555c-020e-46a4-b537-c9c4c7d3cd32', type: 'ACTOR', name: 'createdBy', @@ -3113,6 +3211,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '6d32aa8b-28c0-4e35-b45e-9643fd8e1c33', type: 'CURRENCY', name: 'amount', @@ -3137,6 +3236,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c859b99c-2554-483d-8ffe-4d29cb9c8459', type: 'RELATION', name: 'company', @@ -3184,6 +3284,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7ad6853d-d1e9-46a8-a77a-38eeae27e1d6', type: 'RELATION', name: 'pointOfContact', @@ -3231,6 +3332,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f6c45230-619b-4432-82fd-e5bed0c4e8f4', type: 'SELECT', name: 'stage', @@ -3288,6 +3390,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '5d3ca5b4-9468-4061-8c6b-ef03a9b123df', type: 'RELATION', name: 'favorites', @@ -3335,6 +3438,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '3bc37e42-10f2-4501-94e4-760a7c3fa38e', type: 'POSITION', name: 'position', @@ -3356,6 +3460,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '9019bf90-efbc-4499-a87b-0624bda5a559', type: 'RELATION', name: 'timelineActivities', @@ -3404,6 +3509,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c7472787-44e9-4641-9901-cc4909ca031d', type: 'DATE_TIME', name: 'closeDate', @@ -3425,6 +3531,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '01e75534-627f-4a49-bc28-c08170a71085', type: 'UUID', name: 'id', @@ -3446,6 +3553,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '6d5401c0-d456-42c4-85d4-9666900615ef', type: 'TEXT', name: 'name', @@ -3467,6 +3575,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'aa651efa-1576-4edc-9599-070666a76dda', type: 'RELATION', name: 'attachments', @@ -3514,6 +3623,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b8eb99bd-3485-44bb-8483-f0c600af4e92', type: 'UUID', name: 'pointOfContactId', @@ -3535,6 +3645,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '55fd22aa-80df-4c0b-b5ee-f19163d10a82', type: 'UUID', name: 'companyId', @@ -3556,6 +3667,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd487fe61-e181-4077-ba4e-9c7b466085ad', type: 'DATE_TIME', name: 'updatedAt', @@ -3577,6 +3689,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'bf11e668-6598-42ee-8c81-563641403ba9', type: 'DATE_TIME', name: 'deletedAt', @@ -3598,6 +3711,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '2e77023d-fdeb-454d-9368-ab638a68a0bb', type: 'DATE_TIME', name: 'createdAt', @@ -3619,6 +3733,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'ac708d8f-abd4-436f-aec7-9a8c4ec2cd28', type: 'RELATION', name: 'activityTargets', @@ -3701,6 +3816,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '4a2da98e-6880-47be-8673-165e1d77a910', type: 'RELATION', name: 'rocket', @@ -3748,6 +3864,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '03aab98d-e16c-48fb-91e7-bffc2300402d', type: 'UUID', name: 'opportunityId', @@ -3769,6 +3886,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e1d06322-0658-4c45-9c9b-8a42750c8751', type: 'RELATION', name: 'company', @@ -3816,6 +3934,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'ca812263-7b62-4e7f-8f31-bed3de2d4a94', type: 'DATE_TIME', name: 'updatedAt', @@ -3837,6 +3956,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '984d38ae-335d-41fb-ac29-9029ff43c4c6', type: 'UUID', name: 'companyId', @@ -3858,6 +3978,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b2531e1e-39ad-46c8-af56-853fe1cc7dd6', type: 'RELATION', name: 'activity', @@ -3905,6 +4026,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '26d681d7-4ef2-40eb-bd0c-7e0b7d6d6cb0', type: 'UUID', name: 'personId', @@ -3926,6 +4048,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'dc9c8fec-55d2-40b6-9fcd-d441024be60f', type: 'DATE_TIME', name: 'createdAt', @@ -3947,6 +4070,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '08806161-5d03-4777-8825-b5cff93de042', type: 'UUID', name: 'activityId', @@ -3968,6 +4092,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '9ff04b9e-f87c-44df-82ed-518748ca0d81', type: 'UUID', name: 'rocketId', @@ -3989,6 +4114,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '86438a20-0beb-4e73-93d9-cf91bfe9ac3f', type: 'UUID', name: 'id', @@ -4010,6 +4136,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '2f2ed506-e96e-48d3-8225-45a9c0b55e76', type: 'DATE_TIME', name: 'deletedAt', @@ -4031,6 +4158,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '867aff40-ddf5-4af8-a3f5-6359ab91eb2c', type: 'RELATION', name: 'person', @@ -4078,6 +4206,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c2708db4-5c1e-482b-bdd4-bd620612c15f', type: 'RELATION', name: 'opportunity', @@ -4160,6 +4289,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'eb926345-9d3c-4815-a044-bf1085b31cdc', type: 'UUID', name: 'assigneeId', @@ -4181,6 +4311,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7314a7d0-f318-4aa0-b8f3-db1953139d3f', type: 'TEXT', name: 'title', @@ -4202,6 +4333,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '3e63dfad-08ba-422f-88e5-0c1ebffd6496', type: 'RELATION', name: 'author', @@ -4249,6 +4381,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '5ceb1884-c3ff-4a31-9629-3ef599b1a461', type: 'DATE_TIME', name: 'updatedAt', @@ -4270,6 +4403,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'ffdb396b-200a-4dc5-a3aa-e2c23b180ac0', type: 'TEXT', name: 'body', @@ -4291,6 +4425,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '0f1cfe74-f960-44f9-91f3-fc9d25a4b96b', type: 'RELATION', name: 'comments', @@ -4338,6 +4473,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '248b356d-5a70-458d-a10b-92d70512497b', type: 'DATE_TIME', name: 'createdAt', @@ -4359,6 +4495,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '2f1f7b1a-2c98-42ca-8f6c-784406d7bad8', type: 'UUID', name: 'authorId', @@ -4380,6 +4517,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '799c1e27-3fa5-4411-bfa2-a1f494d2434a', type: 'DATE_TIME', name: 'reminderAt', @@ -4401,6 +4539,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'dd5354bb-dd0a-4426-ac75-87e9ab171dc4', type: 'RELATION', name: 'assignee', @@ -4448,6 +4587,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd7b4584a-d0ed-49f8-b14c-198036b60fe8', type: 'DATE_TIME', name: 'dueAt', @@ -4469,6 +4609,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '9b8808d3-be01-4595-9b20-ebb9553cd7db', type: 'DATE_TIME', name: 'completedAt', @@ -4490,6 +4631,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd0341ced-bdb0-4629-a816-d402df827bd7', type: 'TEXT', name: 'type', @@ -4511,6 +4653,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '1dc0b9af-181a-4f2c-bd01-9c4bf355b9de', type: 'RELATION', name: 'activityTargets', @@ -4558,6 +4701,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b31fdd11-115b-4405-8265-c03329338f0c', type: 'DATE_TIME', name: 'deletedAt', @@ -4579,6 +4723,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'bfa2ab3d-a55b-41ca-906b-9d497aee7ba8', type: 'RELATION', name: 'attachments', @@ -4626,6 +4771,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '33b0ccb9-19fc-42d6-b7af-87bc4411692e', type: 'UUID', name: 'id', @@ -4682,6 +4828,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e166c79b-99c1-4fb4-9575-5cf7e7f4811f', type: 'BOOLEAN', name: 'isOrganizer', @@ -4703,6 +4850,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '34f2ac5d-bb7e-446c-ab52-18e722345a24', type: 'UUID', name: 'workspaceMemberId', @@ -4724,6 +4872,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '0c215138-fc5b-4be2-8c37-0acc7cb4e5a1', type: 'SELECT', name: 'responseStatus', @@ -4774,6 +4923,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '16b867dd-9fb1-43ab-8254-2478004b30b3', type: 'UUID', name: 'id', @@ -4795,6 +4945,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'fdfea59a-624c-4af3-9901-1f86d1972b23', type: 'TEXT', name: 'displayName', @@ -4816,6 +4967,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '2f77bc8e-26dc-40b1-964a-f0748feb193a', type: 'RELATION', name: 'person', @@ -4863,6 +5015,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '597d153f-8724-4d61-8863-8bfae905721f', type: 'RELATION', name: 'calendarEvent', @@ -4910,6 +5063,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '83d94e19-f9f3-47bb-a277-6935af6ae69d', type: 'UUID', name: 'personId', @@ -4931,6 +5085,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'cc27c5b5-44cc-4f19-a56e-8ce172e2ab37', type: 'DATE_TIME', name: 'updatedAt', @@ -4952,6 +5107,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '22c34bd1-550b-4b5c-a2a4-fbb475a4420b', type: 'DATE_TIME', name: 'createdAt', @@ -4973,6 +5129,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7b7792da-246d-4015-855a-ea029f0b8a02', type: 'DATE_TIME', name: 'deletedAt', @@ -4994,6 +5151,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7794c913-c52f-4c06-921e-2b391b63e51e', type: 'UUID', name: 'calendarEventId', @@ -5015,6 +5173,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '30fd4e8a-2089-4443-ac06-5cd53d9a3fcf', type: 'RELATION', name: 'workspaceMember', @@ -5062,6 +5221,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c97d9e28-d807-4fff-ba2c-c72f99087f89', type: 'TEXT', name: 'handle', @@ -5118,6 +5278,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e8a5a86c-b1ce-4db1-8d51-d5b01d2b361b', type: 'DATE_TIME', name: 'updatedAt', @@ -5139,6 +5300,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '09a753b7-a5f9-4810-ae2f-389982f593c3', type: 'DATE_TIME', name: 'createdAt', @@ -5160,6 +5322,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7881aa82-a7ed-4090-923d-f1c0cf16a486', type: 'DATE_TIME', name: 'deletedAt', @@ -5181,6 +5344,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7f1d2847-9f16-457f-a57c-de36c401286a', type: 'UUID', name: 'recordId', @@ -5202,6 +5366,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '1db6f800-bb46-45a2-a324-cfe52362ed9a', type: 'UUID', name: 'id', @@ -5223,6 +5388,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '785dc6cb-77dc-4bdd-be44-60ad8f1f45da', type: 'UUID', name: 'workspaceMemberId', @@ -5244,6 +5410,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '3d6dc314-877d-4aaa-88bd-364dc50f780b', type: 'RELATION', name: 'workspaceMember', @@ -5291,6 +5458,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '80fe2b13-5cb0-48a0-9341-aec08481628f', type: 'RAW_JSON', name: 'context', @@ -5313,6 +5481,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '0383ac8e-6138-4e06-a828-bfc391e53d01', type: 'TEXT', name: 'name', @@ -5334,6 +5503,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f4f0343a-8241-492a-8156-18d40c75f46a', type: 'RAW_JSON', name: 'properties', @@ -5355,6 +5525,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'a8282a28-724f-487b-a9fa-f0d10c919237', type: 'TEXT', name: 'objectName', @@ -5376,6 +5547,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd85abbad-9616-456f-978c-4cced740490c', type: 'TEXT', name: 'objectMetadataId', @@ -5432,6 +5604,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '818ec7cd-9181-4c72-ad99-c19b00fde065', type: 'TEXT', name: 'syncCursor', @@ -5454,6 +5627,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '93658b38-cb56-4d2b-93f8-3a4c7714f7c4', type: 'TEXT', name: 'handle', @@ -5475,6 +5649,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'af61423d-0891-4e91-b6d3-2f7ed6363916', type: 'UUID', name: 'connectedAccountId', @@ -5496,6 +5671,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e921674b-2e9f-4f19-a067-920685cf9164', type: 'SELECT', name: 'visibility', @@ -5532,6 +5708,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '0abc3baf-2797-4f88-8565-a3ffa4468b55', type: 'DATE_TIME', name: 'deletedAt', @@ -5553,6 +5730,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e567e960-f3c8-4ef0-b810-d8a4bc6a4f7d', type: 'BOOLEAN', name: 'isSyncEnabled', @@ -5574,6 +5752,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'bb3531fe-a008-4505-820b-0cb2c365b05d', type: 'DATE_TIME', name: 'createdAt', @@ -5595,6 +5774,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd9a353c7-25ba-403f-8157-0d8bee911cec', type: 'UUID', name: 'id', @@ -5616,6 +5796,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd1596020-b595-4bf9-b3c1-782f9b49f41b', type: 'RELATION', name: 'calendarChannelEventAssociations', @@ -5663,6 +5844,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd757fd20-3568-411f-a2ba-ec2f7b30dc55', type: 'SELECT', name: 'syncStatus', @@ -5720,6 +5902,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '529b36bb-42d1-4361-ac0a-e683557cc879', type: 'DATE_TIME', name: 'updatedAt', @@ -5741,6 +5924,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '8d7cb56f-29f1-40ad-90cf-4497bee669a1', type: 'SELECT', name: 'contactAutoCreationPolicy', @@ -5792,6 +5976,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c933e11e-7820-42e4-a589-389c2f314add', type: 'RELATION', name: 'connectedAccount', @@ -5839,6 +6024,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '01e23a9d-d8b7-47c6-a1f8-7e012ae9f54a', type: 'DATE_TIME', name: 'syncStageStartedAt', @@ -5860,6 +6046,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '9c5f7d31-5b5e-4af1-9918-859ce66b6c08', type: 'SELECT', name: 'syncStage', @@ -5924,6 +6111,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '0c81da27-4ff4-4bf4-8937-9a8ee627e46c', type: 'NUMBER', name: 'throttleFailureCount', @@ -5945,6 +6133,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '5c73b1cc-ddda-46a8-a517-f0bb5f0c5f60', type: 'BOOLEAN', name: 'isContactAutoCreationEnabled', @@ -6001,6 +6190,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '0cc37cbd-c7ce-4898-b34d-5da7736e7b54', type: 'DATE_TIME', name: 'updatedAt', @@ -6022,6 +6212,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '537e956c-58bb-4ed4-8127-beb0f2d04dd2', type: 'RELATION', name: 'messages', @@ -6069,6 +6260,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '728f4580-9220-4130-9a25-c56669ad0e43', type: 'DATE_TIME', name: 'deletedAt', @@ -6090,6 +6282,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'fe0508ef-6e8e-4422-b822-67899af4aa58', type: 'DATE_TIME', name: 'createdAt', @@ -6111,6 +6304,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '823a20db-9431-422c-b0a1-4f559b992651', type: 'UUID', name: 'id', @@ -6167,6 +6361,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7011922c-2271-4960-81eb-b9f9ae3ae00c', type: 'DATE_TIME', name: 'updatedAt', @@ -6188,6 +6383,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '37abc2ae-9f44-4bc0-8277-e3ddfd54738c', type: 'RELATION', name: 'people', @@ -6235,6 +6431,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c264b8c8-1260-410b-aa34-d69b14bba19b', type: 'LINKS', name: 'domainName', @@ -6261,6 +6458,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '73c7c0bc-a328-488f-b357-fd9dc332ab75', type: 'BOOLEAN', name: 'visaSponsorship', @@ -6282,6 +6480,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b08a8600-2c4f-4e8a-8f32-6ffe7041e569', type: 'ADDRESS', name: 'address', @@ -6312,6 +6511,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '107fd869-fa4a-4ca5-b6b1-a918ec78851e', type: 'POSITION', name: 'position', @@ -6333,6 +6533,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '2f0ab2d8-3cdf-49d1-a8be-ef204e871968', type: 'NUMBER', name: 'employees', @@ -6354,6 +6555,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '9677b321-e8fc-4d1b-9d7f-145a5dea0001', type: 'RELATION', name: 'favorites', @@ -6401,6 +6603,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '8a21fcb9-5ee7-498c-a09d-1d3137be0540', type: 'DATE_TIME', name: 'deletedAt', @@ -6422,6 +6625,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '300545a5-6ca7-487a-8374-a662bab5d717', type: 'UUID', name: 'accountOwnerId', @@ -6444,6 +6648,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e41f22fd-ecd5-4b28-a2ed-a04d2a017c19', type: 'CURRENCY', name: 'annualRecurringRevenue', @@ -6469,6 +6674,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '24cc7f7b-e8d3-4c12-a3c5-caa5ccf61523', type: 'UUID', name: 'id', @@ -6490,6 +6696,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '8421d3d3-d5ac-4065-a431-95780fda2ce7', type: 'RELATION', name: 'opportunities', @@ -6537,6 +6744,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '2395970c-d0a2-43df-9f55-58299e930b34', type: 'RELATION', name: 'attachments', @@ -6584,6 +6792,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '3b52848e-e419-4361-8fbc-3d4ed19f1956', type: 'TEXT', name: 'name', @@ -6605,6 +6814,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '5f23c37a-9971-4060-a861-19f030848b90', type: 'LINKS', name: 'xLink', @@ -6630,6 +6840,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c5103301-686a-4c1f-86d1-69e32a4a34ae', type: 'DATE_TIME', name: 'createdAt', @@ -6651,6 +6862,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '81907e01-90e3-412a-9aa5-9ae8352b679d', type: 'ACTOR', name: 'createdBy', @@ -6675,6 +6887,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f8393bb6-05c2-44d7-bff8-4a7671b43f15', type: 'RELATION', name: 'timelineActivities', @@ -6722,6 +6935,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '4882e808-7cd2-487f-911f-ab2d9353e60d', type: 'RELATION', name: 'accountOwner', @@ -6770,6 +6984,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'a8cf3568-4d04-43cf-b8c2-2070a7eb0e4e', type: 'MULTI_SELECT', name: 'workPolicy', @@ -6813,6 +7028,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd69eb854-c043-43d9-a40e-65a0649fd1a9', type: 'RELATION', name: 'taskTargets', @@ -6860,6 +7076,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '531c4b2e-94a0-46f4-9395-277c3239413d', type: 'RELATION', name: 'noteTargets', @@ -6907,6 +7124,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'de10d71e-4cfa-4322-b967-761871e69bc0', type: 'LINKS', name: 'introVideo', @@ -6932,6 +7150,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e2e632fe-1c08-4c12-9aba-88f8595bf5be', type: 'RELATION', name: 'activityTargets', @@ -6979,6 +7198,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '16838104-8adf-45a3-93bf-e97551240b66', type: 'LINKS', name: 'linkedinLink', @@ -7004,6 +7224,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '195db745-305e-40f0-b740-d071c5c19214', type: 'TEXT', name: 'tagline', @@ -7025,6 +7246,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '2d8e886a-3ebf-474c-8c7a-909e9d7fcc6f', type: 'BOOLEAN', name: 'idealCustomerProfile', @@ -7082,6 +7304,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c496110c-239a-4dd4-bdbd-022ca4fdc62c', type: 'RELATION', name: 'calendarEvent', @@ -7129,6 +7352,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '8505bc8b-e978-401a-8983-f1f1e64aa26d', type: 'UUID', name: 'id', @@ -7150,6 +7374,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '18e21e78-56c2-492a-8082-f1e8ceb72e14', type: 'UUID', name: 'calendarChannelId', @@ -7171,6 +7396,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b61d8723-b739-42b6-b23d-b82e09b34669', type: 'DATE_TIME', name: 'deletedAt', @@ -7192,6 +7418,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '97a0be23-ba2e-4af3-8503-d4c27c293a37', type: 'DATE_TIME', name: 'createdAt', @@ -7213,6 +7440,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '906e35f2-e2d1-45d5-8326-cb7712a19e60', type: 'RELATION', name: 'calendarChannel', @@ -7260,6 +7488,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'aa298945-e331-40de-a6db-855b58b99d05', type: 'TEXT', name: 'eventExternalId', @@ -7281,6 +7510,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '0ec16ef5-2331-4a1e-b429-0e4fc3d9576f', type: 'DATE_TIME', name: 'updatedAt', @@ -7302,6 +7532,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '2c765e90-ce86-4f6b-a528-94e38b5aaf54', type: 'UUID', name: 'calendarEventId', @@ -7358,6 +7589,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '78eec6b7-ab9c-4dee-ac64-2b9e40b467f6', type: 'TEXT', name: 'name', @@ -7379,6 +7611,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd2653ce4-585b-4a38-9e09-4ae8e2afae51', type: 'DATE_TIME', name: 'expiresAt', @@ -7400,6 +7633,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '9e5cd8cb-e800-4b25-a9e6-cfb6d51623c6', type: 'DATE_TIME', name: 'updatedAt', @@ -7421,6 +7655,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '68704d45-b248-4e41-a07b-21d5874663bb', type: 'DATE_TIME', name: 'revokedAt', @@ -7442,6 +7677,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '77ef9f04-627b-4708-b400-076629ed9f20', type: 'DATE_TIME', name: 'createdAt', @@ -7463,6 +7699,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'dc4892f0-a890-4a49-90ae-5a7999665785', type: 'UUID', name: 'id', @@ -7484,6 +7721,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'bbe022a1-426f-40c2-ad1d-8294ef127c0b', type: 'DATE_TIME', name: 'deletedAt', @@ -7540,6 +7778,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '9d2499b3-403c-473f-b61f-61bbea97afeb', type: 'UUID', name: 'noteId', @@ -7561,6 +7800,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '0d232853-5ae9-43f3-ac4a-248c50e2a64e', type: 'UUID', name: 'taskId', @@ -7582,6 +7822,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '8e506212-1b66-4981-95d6-f0ac83f5d869', type: 'RELATION', name: 'person', @@ -7629,6 +7870,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'dc005581-351b-4fa0-9b39-da5bbe2554b7', type: 'RELATION', name: 'task', @@ -7676,6 +7918,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '86e97af0-ec16-4937-b5b6-e4531027be82', type: 'UUID', name: 'rocketId', @@ -7697,6 +7940,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c33a0768-3f55-4c7b-aa1a-07aeacf3fb85', type: 'UUID', name: 'viewId', @@ -7718,6 +7962,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'afc385d4-22d1-4f83-a67a-46450df368e9', type: 'DATE_TIME', name: 'updatedAt', @@ -7739,6 +7984,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '90a28b04-0214-424f-afad-172b7cd28073', type: 'UUID', name: 'workflowId', @@ -7760,6 +8006,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '6a245087-243d-450b-9f00-c951d417d4ef', type: 'UUID', name: 'personId', @@ -7781,6 +8028,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '14e0bf40-d4eb-4893-a5f2-3df3ce749996', type: 'UUID', name: 'workspaceMemberId', @@ -7802,6 +8050,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '0ecb3679-5ff7-4b55-9ed3-9927bc9e184b', type: 'RELATION', name: 'note', @@ -7849,6 +8098,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '3a561744-45bd-45a6-af94-bfb4d4f508fe', type: 'DATE_TIME', name: 'createdAt', @@ -7870,6 +8120,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '46ff8942-360f-4837-9a83-007739c8ba05', type: 'RELATION', name: 'view', @@ -7917,6 +8168,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '6e104959-9dfb-42a7-80da-0ddb0dab12f9', type: 'UUID', name: 'opportunityId', @@ -7938,6 +8190,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f112f2a8-4f21-454d-b3a7-fcd85f0eab72', type: 'NUMBER', name: 'position', @@ -7959,6 +8212,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e4102fb0-c6e3-47e8-a810-71e0b6453705', type: 'DATE_TIME', name: 'deletedAt', @@ -7980,6 +8234,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'a1555234-ff28-4a04-871c-31008b39e442', type: 'UUID', name: 'id', @@ -8001,6 +8256,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '10509b5e-71c0-49e8-9cb8-d0ff7ee8691b', type: 'UUID', name: 'companyId', @@ -8022,6 +8278,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'eed525bd-edf3-4030-9f09-8ff68226a6a0', type: 'RELATION', name: 'workflow', @@ -8069,6 +8326,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '5b559e4c-eb75-4a1e-b904-a486d2328b24', type: 'RELATION', name: 'workspaceMember', @@ -8116,6 +8374,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7155582f-4bbc-4643-be0c-38165b8a282f', type: 'RELATION', name: 'company', @@ -8163,6 +8422,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '11e6c0a0-e1fa-4931-a705-8725a79afe24', type: 'RELATION', name: 'rocket', @@ -8210,6 +8470,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '1d3858e1-e4aa-484f-b422-8bbefa9409c8', type: 'RELATION', name: 'opportunity', @@ -8292,6 +8553,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '974ab999-a946-409f-820c-aa2e2c21f3ce', type: 'DATE_TIME', name: 'updatedAt', @@ -8313,6 +8575,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '70a9b04f-0d11-41a1-bce8-e7ac8bb0ed5d', type: 'DATE_TIME', name: 'createdAt', @@ -8334,6 +8597,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'da1dda8a-b7b6-4166-b11b-740a8414706b', type: 'UUID', name: 'authorId', @@ -8355,6 +8619,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '3b07941b-15b6-4468-8e2f-52abf7ff36b3', type: 'TEXT', name: 'body', @@ -8376,6 +8641,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '8f985123-68d4-4e8a-b75b-85a75f0f071e', type: 'UUID', name: 'id', @@ -8397,6 +8663,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e48bc6a2-211c-46aa-9f22-1859aedac28e', type: 'RELATION', name: 'activity', @@ -8444,6 +8711,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c39eb95c-2e9f-4fd3-abc3-103986dd21bb', type: 'UUID', name: 'activityId', @@ -8465,6 +8733,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '571e3b8c-0c80-4292-93b1-c73b4d976b05', type: 'DATE_TIME', name: 'deletedAt', @@ -8486,6 +8755,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b1ead0df-5b1c-4c00-b0c5-05a67ec37327', type: 'RELATION', name: 'author', @@ -8568,6 +8838,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '0c1f5af5-c2d5-4f38-9fd4-6ce854e693d3', type: 'DATE_TIME', name: 'updatedAt', @@ -8589,6 +8860,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f06f5946-e54f-458d-9c77-47d6d8fbd995', type: 'DATE_TIME', name: 'endsAt', @@ -8610,6 +8882,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'bc8be6a6-4707-498a-9e79-8ffb86e92a43', type: 'RELATION', name: 'calendarEventParticipants', @@ -8657,6 +8930,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '6bfac067-9c6c-4b4d-bdaf-ecf7737b2599', type: 'TEXT', name: 'iCalUID', @@ -8678,6 +8952,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '45bd7c28-f450-4487-a556-1fd85be68beb', type: 'TEXT', name: 'title', @@ -8699,6 +8974,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '52d1ed74-b3d5-4339-b408-6f4c8dbc3969', type: 'DATE_TIME', name: 'createdAt', @@ -8720,6 +8996,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '4078641f-c8b4-418c-9e7a-0bfdc411c4d9', type: 'DATE_TIME', name: 'externalUpdatedAt', @@ -8741,6 +9018,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'ae4f8ff1-47f1-4b84-b61b-c519c939a409', type: 'TEXT', name: 'conferenceSolution', @@ -8762,6 +9040,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'a77794e2-adea-40cb-a0f9-a91a6d4494ed', type: 'DATE_TIME', name: 'startsAt', @@ -8783,6 +9062,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '62d6a06b-0083-4702-be85-edd6ca882816', type: 'TEXT', name: 'location', @@ -8804,27 +9084,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', - id: '567c7852-6dc5-4c6e-826d-e4b253614e60', - type: 'TEXT', - name: 'recurringEventExternalId', - label: 'Recurring Event ID', - description: 'Recurring Event ID', - icon: 'IconHistory', - isCustom: false, - isActive: true, - isSystem: false, - isNullable: false, - createdAt: '2024-09-25T13:45:32.757Z', - updatedAt: '2024-09-25T13:45:32.757Z', - defaultValue: "''", - options: null, - relationDefinition: null, - }, - }, - { - __typename: 'fieldEdge', - node: { - __typename: 'field', + settings: {}, id: 'c0ed400f-b0cf-4fe1-a929-89698dc020a5', type: 'DATE_TIME', name: 'externalCreatedAt', @@ -8846,6 +9106,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'da0d1cc9-be28-4b41-9ddf-48041702024b', type: 'TEXT', name: 'description', @@ -8867,6 +9128,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '52db308a-e428-4efd-95ed-2b0e19cbdc92', type: 'RELATION', name: 'calendarChannelEventAssociations', @@ -8914,6 +9176,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '63904769-6145-4230-b294-c4554c36a273', type: 'BOOLEAN', name: 'isCanceled', @@ -8935,6 +9198,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '48fac512-a780-4c44-b1dc-f178bd8ab3f8', type: 'DATE_TIME', name: 'deletedAt', @@ -8956,6 +9220,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '1ba833a8-68ca-4396-b3ed-9a7411e1dc4f', type: 'UUID', name: 'id', @@ -8977,6 +9242,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '3050381d-7191-4d18-be27-9a92cbefb57a', type: 'LINKS', name: 'conferenceLink', @@ -9002,6 +9268,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b6cf9c5d-e923-4026-9316-c3a513ce7c12', type: 'BOOLEAN', name: 'isFullDay', @@ -9058,6 +9325,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'ac76a0ff-f5c1-4127-b354-0b3ce2b3696b', type: 'RELATION', name: 'favorites', @@ -9105,6 +9373,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e32f7a81-d208-4c14-afb4-a4befc938670', type: 'RELATION', name: 'assignedActivities', @@ -9152,6 +9421,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '85df4525-aed1-4e46-b718-b5bc963da41d', type: 'RELATION', name: 'auditLogs', @@ -9199,6 +9469,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'a1ffe54a-6b76-47fa-bd25-df479a31eee2', type: 'FULL_NAME', name: 'name', @@ -9223,6 +9494,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7370da8c-294f-4671-91e5-7f87f4dccc1e', type: 'RELATION', name: 'calendarEventParticipants', @@ -9270,6 +9542,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '13531c23-42ca-4b1e-a6e3-3fcfad74a3e9', type: 'RELATION', name: 'connectedAccounts', @@ -9317,6 +9590,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'a5fc2711-0e3d-40ac-938a-8beafeac1f57', type: 'RELATION', name: 'timelineActivities', @@ -9364,6 +9638,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'bb548a08-0706-4021-833d-e527c23e2a48', type: 'RELATION', name: 'accountOwnerForCompanies', @@ -9411,6 +9686,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'fc4f41c0-01ea-428d-9e0c-e2496acc765b', type: 'TEXT', name: 'avatarUrl', @@ -9432,6 +9708,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f74a56bf-9230-4452-b6a0-099ba9f7de0d', type: 'RELATION', name: 'authoredComments', @@ -9479,6 +9756,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e2895a0d-0e4f-4673-a778-2b99cac1ab20', type: 'UUID', name: 'userId', @@ -9500,6 +9778,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '4cceec11-98cc-41c6-b08c-b04887a0ac22', type: 'DATE_TIME', name: 'createdAt', @@ -9521,6 +9800,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '1dc4eef9-d576-4106-b19e-2fc91777470d', type: 'TEXT', name: 'timeZone', @@ -9542,6 +9822,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '2f548d63-6cf6-4116-ae5a-03fcc35ffd1d', type: 'UUID', name: 'id', @@ -9563,6 +9844,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '6cc143e8-f88a-4d75-a52c-d5c7f7419d97', type: 'SELECT', name: 'timeFormat', @@ -9606,6 +9888,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b9f4e812-721f-4c83-9f1e-42b79042d905', type: 'DATE_TIME', name: 'updatedAt', @@ -9627,6 +9910,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '12e6ccfa-23ef-4b68-a043-6012bc7b9c67', type: 'TEXT', name: 'locale', @@ -9648,6 +9932,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b75d64ca-eb86-439d-ad61-3f23efec07e4', type: 'TEXT', name: 'userEmail', @@ -9669,6 +9954,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'dd63010d-24f1-4e62-9b49-b4728bb3bd81', type: 'DATE_TIME', name: 'deletedAt', @@ -9690,6 +9976,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f173a8e9-af79-4377-9b21-5cb1ca27bc87', type: 'TEXT', name: 'colorScheme', @@ -9711,6 +9998,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f76b3faa-0bc6-45ed-9654-0421171a1f1a', type: 'RELATION', name: 'authoredActivities', @@ -9758,6 +10046,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c5df2d21-5db8-4e72-82bf-aeb3ec984ca9', type: 'RELATION', name: 'authoredAttachments', @@ -9805,6 +10094,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd82db237-ca6d-4bee-8d69-dfa0f753707b', type: 'RELATION', name: 'messageParticipants', @@ -9852,6 +10142,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e64fef03-fda1-4b5c-8894-d1bee725e7e2', type: 'RELATION', name: 'blocklist', @@ -9899,6 +10190,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '3653df1b-5687-45c1-a2bf-afc261fe85a8', type: 'RELATION', name: 'assignedTasks', @@ -9946,6 +10238,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '18a0276f-ce5e-4fc7-81fc-c32f6dd844f3', type: 'SELECT', name: 'dateFormat', @@ -10031,6 +10324,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f58f100f-8ee9-4a0c-8f35-8bdcb561d586', type: 'DATE_TIME', name: 'createdAt', @@ -10052,6 +10346,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'a3740f53-215a-477d-82be-b57265731d83', type: 'DATE_TIME', name: 'deletedAt', @@ -10073,6 +10368,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '67dbcf4d-f15b-4703-ba84-4bc2b9903579', type: 'UUID', name: 'id', @@ -10094,6 +10390,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd26efc67-251a-4bdd-a585-177717e298a6', type: 'TEXT', name: 'eventName', @@ -10115,6 +10412,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c8d7ed48-3d0e-47fa-a4af-5a62e578c128', type: 'UUID', name: 'workflowId', @@ -10137,6 +10435,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'a424fba4-d3f4-41b1-bf9a-4b809ad628a9', type: 'RELATION', name: 'workflow', @@ -10184,6 +10483,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'aabd6970-7bf9-488b-8411-5bc654574d58', type: 'DATE_TIME', name: 'updatedAt', @@ -10240,6 +10540,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'ee4d98d0-37a4-42cc-8794-653099d4df54', type: 'BOOLEAN', name: 'isVisible', @@ -10261,6 +10562,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '643509ce-5666-4264-8b09-f095e23a1624', type: 'NUMBER', name: 'position', @@ -10282,6 +10584,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c672e744-21f3-4d23-abd8-fcc03fad503a', type: 'UUID', name: 'viewId', @@ -10303,6 +10606,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '49e8ada7-3a80-49c7-869a-3ebfdae35387', type: 'NUMBER', name: 'size', @@ -10324,6 +10628,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'dd7510e1-4f22-4376-8436-19d7d631ea77', type: 'DATE_TIME', name: 'createdAt', @@ -10345,6 +10650,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f74ba2c1-22ab-4827-85ad-d2dbbe2a9b51', type: 'RELATION', name: 'view', @@ -10392,6 +10698,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '959d36dc-493b-4e08-ae3d-38680bab1d0d', type: 'UUID', name: 'id', @@ -10413,6 +10720,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '300b7bf6-e1fa-4cd2-a601-7a125b7bf1b8', type: 'DATE_TIME', name: 'deletedAt', @@ -10434,6 +10742,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '0f7acd2a-ccd1-437e-b80a-bf4555a7c034', type: 'UUID', name: 'fieldMetadataId', @@ -10455,6 +10764,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '217be100-e82e-4aae-9566-b091823a5466', type: 'DATE_TIME', name: 'updatedAt', @@ -10511,6 +10821,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '330a6b96-c7eb-41ae-962e-eb528dc16aaf', type: 'DATE_TIME', name: 'updatedAt', @@ -10532,6 +10843,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '45fe30a3-141f-4908-be8b-b826f84edb75', type: 'UUID', name: 'viewId', @@ -10553,6 +10865,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7924f4d6-c92b-436f-a86d-26b2dcc521aa', type: 'TEXT', name: 'direction', @@ -10574,6 +10887,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'fc34ddd1-7f8a-4f23-a2b6-9a6d3165cc0a', type: 'DATE_TIME', name: 'deletedAt', @@ -10595,6 +10909,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e4f8f882-dec2-4d6b-a141-936e87d3fd27', type: 'UUID', name: 'id', @@ -10616,6 +10931,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '25ae0938-795a-491a-b029-e4672412e85f', type: 'RELATION', name: 'view', @@ -10663,6 +10979,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '540715e7-6531-4746-b567-b0a7fcda60ef', type: 'UUID', name: 'fieldMetadataId', @@ -10684,6 +11001,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '26889121-d2d9-49dc-86ac-51c64d123197', type: 'DATE_TIME', name: 'createdAt', @@ -10739,6 +11057,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '9aed4be2-3434-489f-a8a3-384311ee585e', type: 'RELATION', name: 'activityTargets', @@ -10786,6 +11105,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '13794481-6a3a-48cb-80c2-109b7558f7b3', type: 'RELATION', name: 'attachments', @@ -10833,6 +11153,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '79da64bc-eea3-476e-801b-f08c86a8c337', type: 'RELATION', name: 'favorites', @@ -10880,6 +11201,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '0a688ec2-f55c-4485-a7a9-9438c19bcbe3', type: 'ACTOR', name: 'createdBy', @@ -10904,6 +11226,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '17cbb6ed-1acc-43e9-a802-96a2b373a067', type: 'DATE_TIME', name: 'updatedAt', @@ -10925,6 +11248,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'ba68ee30-5dc6-47db-bf6f-db25d829feb5', type: 'TEXT', name: 'name', @@ -10946,6 +11270,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'ff73fd8a-9b60-4480-82d0-7b96c3c3aab6', type: 'POSITION', name: 'position', @@ -10967,6 +11292,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '83515891-eb29-472e-9cde-4a1d42b6855d', type: 'RELATION', name: 'taskTargets', @@ -11014,6 +11340,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '73428c65-a426-4c59-b50b-0dea5ffe9bf0', type: 'DATE_TIME', name: 'createdAt', @@ -11035,6 +11362,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '6c65c781-9def-4c6c-98d4-25d3c0c085b8', type: 'UUID', name: 'id', @@ -11056,6 +11384,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b2643f4a-319e-49d4-a7b7-cbfff4712bf7', type: 'DATE_TIME', name: 'deletedAt', @@ -11077,6 +11406,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '75f64c23-e9a5-4ada-8dc6-3c2c2ea27280', type: 'RELATION', name: 'noteTargets', @@ -11124,6 +11454,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'aeefa434-6843-4b47-92f6-3ce6d8e93860', type: 'RELATION', name: 'timelineActivities', @@ -11206,6 +11537,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'fa957d80-c61a-4298-aab9-54e8fba2110d', type: 'UUID', name: 'id', @@ -11227,6 +11559,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f966b379-0438-4b9c-8696-3edf18c197f7', type: 'RELATION', name: 'workspaceMember', @@ -11274,6 +11607,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '37f7671b-aa7c-4fed-b50c-9b7cc7f59aa8', type: 'TEXT', name: 'handle', @@ -11295,6 +11629,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '862261a3-5473-4e16-9220-16444ee99243', type: 'DATE_TIME', name: 'updatedAt', @@ -11316,6 +11651,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '1233f40e-cc97-4281-a8a8-c23de8961693', type: 'DATE_TIME', name: 'createdAt', @@ -11337,6 +11673,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'a75f6045-cf57-4b05-960b-5719ce2037c9', type: 'DATE_TIME', name: 'deletedAt', @@ -11358,6 +11695,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c30e3bff-ce4e-4e64-80e3-bf417ceefa25', type: 'UUID', name: 'workspaceMemberId', @@ -11414,6 +11752,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '2d00a390-5ae4-40d9-8d6c-6abbcdc0bc75', type: 'DATE_TIME', name: 'updatedAt', @@ -11435,6 +11774,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '2aab6f08-2cab-4beb-8775-1ad8b89c3313', type: 'ACTOR', name: 'createdBy', @@ -11459,6 +11799,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f40e2edc-c0dd-46d8-9a01-a0afd33a00db', type: 'UUID', name: 'id', @@ -11480,6 +11821,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '421de2e5-36a1-4fb2-ae00-4e2f62dd67e6', type: 'UUID', name: 'workflowVersionId', @@ -11502,6 +11844,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c9e837bb-4516-43ad-8756-7a9c2ad33ca9', type: 'DATE_TIME', name: 'createdAt', @@ -11523,6 +11866,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'aec86cc0-1fb6-4ff0-b1df-85b74ee6a974', type: 'UUID', name: 'workflowId', @@ -11544,6 +11888,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b38c3f7d-d455-4f1c-b674-66b86c0d56cc', type: 'RELATION', name: 'workflowVersion', @@ -11591,6 +11936,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '446d4c37-71ac-4a61-8b65-16d70250cbc5', type: 'SELECT', name: 'status', @@ -11641,6 +11987,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '0ed4a200-4d2e-4d4d-81b2-45240ae9e1b8', type: 'DATE_TIME', name: 'endedAt', @@ -11662,6 +12009,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'a5444b51-1ad5-46ae-b9d9-ef5f1def9232', type: 'RELATION', name: 'workflow', @@ -11709,6 +12057,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b388b202-371a-4663-9299-afc6be461a8c', type: 'DATE_TIME', name: 'deletedAt', @@ -11730,6 +12079,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '2e84a165-18af-492f-8662-14fbd5852b0c', type: 'DATE_TIME', name: 'startedAt', @@ -11786,6 +12136,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '6f41b722-fbc4-44d9-9315-51498900e157', type: 'DATE_TIME', name: 'updatedAt', @@ -11807,6 +12158,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '1d53ad9c-bdf4-4660-b4b6-e79162d13c39', type: 'RELATION', name: 'favorites', @@ -11854,6 +12206,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e7df66e0-50cc-4047-9b91-8f5ee1ff7246', type: 'DATE_TIME', name: 'createdAt', @@ -11875,6 +12228,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '34ec585e-b42e-4e63-8e4b-7f9bf6a4e79b', type: 'DATE_TIME', name: 'deletedAt', @@ -11896,6 +12250,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '22b735aa-95d7-44fe-a4a6-965171e3f7b7', type: 'DATE_TIME', name: 'dueAt', @@ -11917,6 +12272,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'fea3a2f3-73ca-4142-8d15-3b8877b68cee', type: 'UUID', name: 'id', @@ -11938,6 +12294,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '4f82dbad-cf61-4dd1-ad90-4cec71b288be', type: 'RELATION', name: 'attachments', @@ -11985,6 +12342,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b52cbdab-0c97-4601-b5ad-de766ec9f940', type: 'SELECT', name: 'status', @@ -12028,6 +12386,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '1eea9af9-53a3-4085-9391-4a2fad697eb7', type: 'RELATION', name: 'timelineActivities', @@ -12075,6 +12434,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '78360243-9830-4f40-a515-59cd8faf88b1', type: 'RICH_TEXT', name: 'body', @@ -12096,6 +12456,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '33d7eb92-8231-417c-8efe-eb837c6ccadf', type: 'ACTOR', name: 'createdBy', @@ -12120,6 +12481,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '5f9a8761-e5e6-492c-87ab-401c7b0e25cd', type: 'UUID', name: 'assigneeId', @@ -12141,6 +12503,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7a062a59-07da-4d2d-a0e8-f79f87e2e5e3', type: 'RELATION', name: 'assignee', @@ -12188,6 +12551,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '80932806-6350-4941-a291-4d1430275d65', type: 'RELATION', name: 'taskTargets', @@ -12235,6 +12599,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'daf389ba-49e6-4753-8a5a-a0330c5ab154', type: 'POSITION', name: 'position', @@ -12256,6 +12621,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '0413dccd-00a6-4abd-967a-45233e8cf666', type: 'TEXT', name: 'title', @@ -12312,6 +12678,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '38f5127c-ab53-45bc-90b9-a3883d697eb9', type: 'DATE_TIME', name: 'deletedAt', @@ -12333,6 +12700,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b2a7002e-19f2-4f16-8b5b-ea452aeb7104', type: 'TEXT', name: 'lastPublishedVersionId', @@ -12354,6 +12722,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd6fb9120-9aa5-4fdf-a84a-2805bb359855', type: 'RELATION', name: 'versions', @@ -12401,6 +12770,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '99d7e7e6-ac6a-4b74-9129-d4a9759bc928', type: 'DATE_TIME', name: 'createdAt', @@ -12422,6 +12792,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '06a5a424-930c-4465-9943-2a9c486f2038', type: 'UUID', name: 'id', @@ -12443,6 +12814,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '650b02ef-32b5-4b10-bdab-a49f4e3b7a9b', type: 'MULTI_SELECT', name: 'statuses', @@ -12484,6 +12856,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b14865e9-26a6-4ae8-930d-d22c21c7696c', type: 'RELATION', name: 'eventListeners', @@ -12532,6 +12905,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '4f32f14b-5009-4624-8b19-e65084368349', type: 'RELATION', name: 'runs', @@ -12579,6 +12953,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'bab376b1-a7d9-48b8-88b7-f302bc6d483d', type: 'TEXT', name: 'name', @@ -12600,6 +12975,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '141281c7-5b28-41d4-99bd-31c1e4c88e9b', type: 'RELATION', name: 'favorites', @@ -12647,6 +13023,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f21077b2-283f-4362-98fd-e5ea5f87d621', type: 'POSITION', name: 'position', @@ -12668,6 +13045,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b063853c-a442-4b52-8f89-f750c44a2e04', type: 'DATE_TIME', name: 'updatedAt', @@ -12725,6 +13103,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '6615546f-8744-4b36-83d5-59c1ef72e845', type: 'UUID', name: 'id', @@ -12746,6 +13125,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '299c9bdc-5137-4d2b-8225-ddb81a720bfe', type: 'UUID', name: 'rocketId', @@ -12767,6 +13147,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '8626f50f-1d39-40e7-a05b-528c42fe4313', type: 'RELATION', name: 'task', @@ -12814,6 +13195,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f2f27d9e-c959-4dee-aed4-f87c64229c3f', type: 'RELATION', name: 'rocket', @@ -12861,6 +13243,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '62e278a5-d719-4b49-a820-fc5ed358311e', type: 'DATE_TIME', name: 'happensAt', @@ -12882,6 +13265,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '145c8790-abd1-49fb-9857-650da78c6717', type: 'RELATION', name: 'company', @@ -12929,6 +13313,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '08dd3999-ca07-43cf-b872-a2bed317aa6a', type: 'TEXT', name: 'name', @@ -12950,6 +13335,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'bbdf103a-60cb-4c39-a982-1704e26f6735', type: 'UUID', name: 'noteId', @@ -12971,6 +13357,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '9e9d4989-fdf2-4e71-90af-aae2ef0b4923', type: 'RELATION', name: 'person', @@ -13018,6 +13405,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '1b496b65-948d-4614-889f-2a9e0a6292f3', type: 'UUID', name: 'linkedObjectMetadataId', @@ -13039,6 +13427,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'cd22914c-60d6-4e45-9936-9ff345d3a5bb', type: 'DATE_TIME', name: 'updatedAt', @@ -13060,6 +13449,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '77f9059d-33f5-4fa1-8641-4976d38eaeb7', type: 'TEXT', name: 'linkedRecordCachedName', @@ -13081,6 +13471,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '137a88ef-9c08-414c-adcc-2d450624acf8', type: 'RELATION', name: 'workspaceMember', @@ -13128,6 +13519,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b2df9370-1560-47f6-89b6-88293de46572', type: 'UUID', name: 'personId', @@ -13149,6 +13541,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '9cb24716-07e8-4c54-a0c8-7005a619314e', type: 'RAW_JSON', name: 'properties', @@ -13170,6 +13563,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'bc5277df-a1ec-4ae2-affe-e42aed88f0b9', type: 'UUID', name: 'taskId', @@ -13191,6 +13585,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'cda06c7c-9c62-4579-b3ba-2e8275c604b1', type: 'DATE_TIME', name: 'createdAt', @@ -13212,6 +13607,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '95fa7d72-fd85-4a32-a489-a5d03199debb', type: 'UUID', name: 'workspaceMemberId', @@ -13233,6 +13629,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '1d16560a-4a8f-448c-8289-70e3e98ad3b4', type: 'UUID', name: 'linkedRecordId', @@ -13254,6 +13651,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c646eaf5-2754-4924-a212-c5747b0d1d41', type: 'UUID', name: 'companyId', @@ -13275,6 +13673,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '88a829b3-6d66-4e53-b1ad-ed02e544e4d2', type: 'RELATION', name: 'opportunity', @@ -13322,6 +13721,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'db7ab4e8-9d93-4f45-bbbf-f68eb31776cb', type: 'RELATION', name: 'note', @@ -13369,6 +13769,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '1ee1c4bc-7281-4eff-bc32-6afaff324477', type: 'DATE_TIME', name: 'deletedAt', @@ -13390,6 +13791,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '225d1acc-2ccc-458d-acd5-06f7d8647a38', type: 'UUID', name: 'opportunityId', @@ -13447,6 +13849,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f926708e-cc19-4e5f-8684-d14a1a1bc7df', type: 'RELATION', name: 'pointOfContactForOpportunities', @@ -13495,6 +13898,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '5a063961-bffe-4d68-a0c8-e86b6b26f85e', type: 'FULL_NAME', name: 'name', @@ -13519,6 +13923,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd02d52c8-379b-476d-84d6-1222c2179db7', type: 'LINKS', name: 'linkedinLink', @@ -13544,6 +13949,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '504f7e23-1476-422b-ac1d-5d86d3d33022', type: 'RELATION', name: 'company', @@ -13591,6 +13997,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'daab9749-7d39-44cb-9557-23e0e257aaad', type: 'RELATION', name: 'attachments', @@ -13638,6 +14045,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '6e46c199-d4f9-4c60-b9bb-19da859991b4', type: 'RELATION', name: 'timelineActivities', @@ -13685,6 +14093,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'aa5e7931-e042-45d5-af4a-e4c22979a3b7', type: 'RELATION', name: 'calendarEventParticipants', @@ -13732,6 +14141,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'a8a3d68a-a1b1-4912-aa13-51f088c6a754', type: 'DATE_TIME', name: 'deletedAt', @@ -13753,6 +14163,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e442f814-f8a6-44e9-b60c-d736afec87dd', type: 'DATE_TIME', name: 'createdAt', @@ -13774,6 +14185,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'a91e4125-7885-4b3c-9672-0f6d2fc49c07', type: 'DATE_TIME', name: 'updatedAt', @@ -13795,6 +14207,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '4a9a6c3e-633d-47d6-99bc-b8e6e9b6aad9', type: 'TEXT', name: 'jobTitle', @@ -13816,6 +14229,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b801b0d2-2a24-42be-b878-ef439ef7ea78', type: 'TEXT', name: 'intro', @@ -13837,6 +14251,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c188f5b7-2259-4207-87d2-5232ec775029', type: 'RELATION', name: 'favorites', @@ -13884,9 +14299,10 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '5cdf5de5-6c7f-4787-b47d-de5e3787e670', type: 'MULTI_SELECT', - name: 'workPrefereance', + name: 'workPreference', label: 'Work Preference', description: "Person's Work Preference", icon: 'IconHome', @@ -13927,6 +14343,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '1315ca79-3abd-4b7f-917e-8949db3e01f3', type: 'RATING', name: 'performanceRating', @@ -13979,6 +14396,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '749e13b2-5430-4993-83db-635c6ff11d1a', type: 'LINKS', name: 'xLink', @@ -14004,6 +14422,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '0f7b1621-5da6-439a-927f-948fd2dd6f29', type: 'RELATION', name: 'noteTargets', @@ -14051,6 +14470,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '759da4e9-e9c6-4827-9802-5b7b4021448f', type: 'TEXT', name: 'city', @@ -14072,6 +14492,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '48f76fc3-3c82-463d-afa7-977011eed7c8', type: 'UUID', name: 'companyId', @@ -14093,6 +14514,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '23c64ee1-4935-4a25-b401-afc08a0967fd', type: 'RELATION', name: 'activityTargets', @@ -14140,6 +14562,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd4a5fecc-285b-42d7-8eb4-96fc2a6838c4', type: 'PHONES', name: 'phones', @@ -14165,6 +14588,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'bb5900a5-29ff-46bb-8bbb-7dbedd844e29', type: 'ACTOR', name: 'createdBy', @@ -14189,6 +14613,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '54fbea0f-ce3f-4d28-8fa4-abcfe1ae3d54', type: 'UUID', name: 'id', @@ -14210,6 +14635,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '718ee8a6-a294-4609-91d4-7ab0e83d996f', type: 'POSITION', name: 'position', @@ -14231,6 +14657,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '779bdf5a-a28d-48be-8d02-b6ca93851829', type: 'RELATION', name: 'messageParticipants', @@ -14278,6 +14705,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7f8249b8-5919-4fee-81cf-b4dfdb89cf4d', type: 'EMAILS', name: 'emails', @@ -14302,6 +14730,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'b328a712-5dc3-457a-aa56-8631f1b57248', type: 'RELATION', name: 'taskTargets', @@ -14349,6 +14778,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '377efd0d-9798-4522-b9db-73c19bf55e26', type: 'TEXT', name: 'avatarUrl', @@ -14370,6 +14800,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '3a34e81d-d5b1-417d-9831-a355040a6f44', type: 'PHONES', name: 'whatsapp', @@ -14430,6 +14861,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f115229b-f5b3-4623-ac80-f8bf3df5e077', type: 'RELATION', name: 'favorites', @@ -14477,6 +14909,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'ed46299b-4006-45b4-aaa4-2dc7c0c25613', type: 'DATE_TIME', name: 'deletedAt', @@ -14498,6 +14931,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '54952f24-63cc-475e-ab4c-b0f3ad6400bc', type: 'UUID', name: 'id', @@ -14519,6 +14953,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'bef43ac8-834a-4a86-8bfb-5bed6cd94a57', type: 'RELATION', name: 'noteTargets', @@ -14566,6 +15001,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '210b544b-46f7-422d-84e7-328ef270f081', type: 'RELATION', name: 'timelineActivities', @@ -14613,6 +15049,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd80706c2-3c5d-491f-9759-b05b25004799', type: 'POSITION', name: 'position', @@ -14634,6 +15071,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '2238e043-a33f-4e8d-99d6-386cd0d2ea3b', type: 'DATE_TIME', name: 'updatedAt', @@ -14655,6 +15093,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c996b13e-a787-44f7-ad7a-2ba22afd46bc', type: 'ACTOR', name: 'createdBy', @@ -14679,6 +15118,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '61375724-caaa-46b1-8e50-4b2f23afac71', type: 'RICH_TEXT', name: 'body', @@ -14700,6 +15140,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7ffc4d21-aa5d-4f0f-bc91-61c72660f2ba', type: 'TEXT', name: 'title', @@ -14721,6 +15162,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '27b7400b-e754-4db9-b895-0a9252a015bf', type: 'DATE_TIME', name: 'createdAt', @@ -14742,6 +15184,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '80479763-1c19-4465-a62f-cd5e37c9165a', type: 'RELATION', name: 'attachments', @@ -14824,6 +15267,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '4e4f0f26-89bd-41eb-bc00-a591e041ca7d', type: 'DATE_TIME', name: 'updatedAt', @@ -14845,6 +15289,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '362cb4b7-dda1-4136-9385-4f5402b4f700', type: 'UUID', name: 'id', @@ -14866,6 +15311,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd13848d4-b41e-43b0-87ca-ab40c00fd739', type: 'DATE_TIME', name: 'createdAt', @@ -14887,6 +15333,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '81685712-ecaa-4d04-a8d9-e6c93a6bfe7a', type: 'TEXT', name: 'displayValue', @@ -14908,6 +15355,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f72701d1-8337-4640-84b7-4570cfbe96aa', type: 'UUID', name: 'viewId', @@ -14929,6 +15377,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e323bb1b-8018-4b6f-b065-37c979caf7fc', type: 'DATE_TIME', name: 'deletedAt', @@ -14950,6 +15399,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '646c0bfc-071d-44f3-b8d4-428061106500', type: 'UUID', name: 'fieldMetadataId', @@ -14971,6 +15421,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '016a2312-ac04-4d46-b7c7-2d6e24e363c8', type: 'TEXT', name: 'value', @@ -14992,6 +15443,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'e09cc7ab-2750-4cd9-86e1-c257dac8b390', type: 'TEXT', name: 'operand', @@ -15013,6 +15465,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd1829b60-e886-4dac-8cf3-0d3b84da093d', type: 'RELATION', name: 'view', @@ -15095,6 +15548,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'dd208a4e-3e5e-4192-83a1-adbc5b9123a1', type: 'RAW_JSON', name: 'steps', @@ -15116,6 +15570,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'fe28de2e-8979-4cfd-9b66-03e67e93406b', type: 'UUID', name: 'id', @@ -15137,6 +15592,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'dcdf9413-04c8-44f7-ace7-a11950ce3019', type: 'SELECT', name: 'status', @@ -15187,6 +15643,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'ef3d8797-2f17-4f31-9970-697fc230df7f', type: 'RELATION', name: 'runs', @@ -15234,6 +15691,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'ebe58c33-17d2-418b-b33f-f5c3907e97d7', type: 'DATE_TIME', name: 'deletedAt', @@ -15255,6 +15713,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '1b223e47-9228-41c4-a420-ff6ed516393e', type: 'DATE_TIME', name: 'createdAt', @@ -15276,6 +15735,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '54cde78b-4bd4-436b-b10b-e6de37494161', type: 'RELATION', name: 'workflow', @@ -15323,6 +15783,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '4ebb0eb9-9ad6-4e5b-b01b-837b0e2c0718', type: 'RAW_JSON', name: 'trigger', @@ -15344,6 +15805,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'ef2dd597-faaa-4b1d-96b7-5953cd8c8539', type: 'TEXT', name: 'name', @@ -15365,6 +15827,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd9f1d5c8-ce95-48c3-a4f3-0909aea7e322', type: 'DATE_TIME', name: 'updatedAt', @@ -15386,6 +15849,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '182c0466-9e03-4500-8cf7-22673e05b299', type: 'UUID', name: 'workflowId', @@ -15442,6 +15906,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '426142c4-5a52-4105-a880-387b6dba6362', type: 'DATE_TIME', name: 'deletedAt', @@ -15463,6 +15928,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '4d459669-2ffd-4fd0-b28b-57d8a1eb9434', type: 'UUID', name: 'personId', @@ -15484,6 +15950,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '1a6dd2bf-3da7-492d-80de-a8891c5307b7', type: 'UUID', name: 'opportunityId', @@ -15505,6 +15972,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '3669c8b2-ba73-4e9b-be61-10a2023955fd', type: 'RELATION', name: 'activity', @@ -15552,6 +16020,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '023f376c-023a-4cae-89e8-961add0b3743', type: 'UUID', name: 'id', @@ -15573,6 +16042,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '6a5833a9-0741-44f0-948d-424a60d3264e', type: 'RELATION', name: 'note', @@ -15620,6 +16090,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '1d3d2f23-c3e6-4ee0-b62e-7668e4f8147d', type: 'TEXT', name: 'fullPath', @@ -15641,6 +16112,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd9a80298-6f72-4b2f-a859-2bd355d36735', type: 'RELATION', name: 'author', @@ -15688,6 +16160,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '42dbb8d7-320b-460d-a25e-943222ae2a9b', type: 'TEXT', name: 'type', @@ -15709,6 +16182,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'a7971798-8b85-477e-8a1e-9b2f2fb5da6d', type: 'UUID', name: 'rocketId', @@ -15730,6 +16204,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '610633e7-0221-4838-adaf-71943d18a5ca', type: 'RELATION', name: 'person', @@ -15777,6 +16252,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '26668e62-39e7-4842-a198-a657f44206f8', type: 'RELATION', name: 'task', @@ -15824,6 +16300,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '1433d739-29ed-450c-8216-6afea26d21fb', type: 'RELATION', name: 'company', @@ -15871,6 +16348,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '1a8466d6-8be7-458a-a7ea-cdf11b4fe31d', type: 'DATE_TIME', name: 'updatedAt', @@ -15892,6 +16370,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7dc968df-7127-4e05-b63e-f2e9809324ee', type: 'UUID', name: 'noteId', @@ -15913,6 +16392,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7ffec116-99d2-402d-adea-68b731be4c74', type: 'UUID', name: 'taskId', @@ -15934,6 +16414,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd76c9b48-5a3e-445f-be15-948de8ba2fc2', type: 'TEXT', name: 'name', @@ -15955,6 +16436,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'fa9d964c-3d30-4d8b-bc57-9b382053e9e3', type: 'RELATION', name: 'opportunity', @@ -16002,6 +16484,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '133dcd8e-aa07-4c9e-a337-92dabd5f7d03', type: 'UUID', name: 'activityId', @@ -16023,6 +16506,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'd10c5485-159b-4700-a709-37d3049a8778', type: 'DATE_TIME', name: 'createdAt', @@ -16044,6 +16528,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '60f8c4fb-1ab9-44a7-a5bd-e89a0349feb7', type: 'RELATION', name: 'rocket', @@ -16091,6 +16576,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'adaa359c-c834-44bb-8639-44ef1affce2f', type: 'UUID', name: 'authorId', @@ -16112,6 +16598,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '3486e0e2-053c-4e27-9d5d-c27b5dd739ea', type: 'UUID', name: 'companyId', @@ -16168,6 +16655,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '827beccf-c3ca-4f24-a349-5d7c8690ac95', type: 'TEXT', name: 'headerMessageId', @@ -16189,6 +16677,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f51b5646-06b0-45e4-960f-5f8eaeb18c83', type: 'DATE_TIME', name: 'deletedAt', @@ -16210,6 +16699,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f32d4ef7-cadd-4ce5-84ce-a95fd76fde05', type: 'DATE_TIME', name: 'updatedAt', @@ -16231,6 +16721,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'c36f257c-2318-4b61-9c63-666e1fc0810c', type: 'DATE_TIME', name: 'receivedAt', @@ -16252,6 +16743,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '86a6dca6-5ad5-4576-b8f4-4be343e573de', type: 'RELATION', name: 'messageChannelMessageAssociations', @@ -16299,6 +16791,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '606cef74-d9c3-4abc-b6ae-bb778f518e49', type: 'RELATION', name: 'messageParticipants', @@ -16346,6 +16839,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '21c34ce8-7505-4d6c-9fc8-218dd8532a25', type: 'UUID', name: 'id', @@ -16367,6 +16861,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '48f20f05-700d-4609-965b-8a954bf07e8d', type: 'UUID', name: 'messageThreadId', @@ -16388,6 +16883,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '11f38fa4-e7b8-4275-b0b2-59688cb2eed8', type: 'RELATION', name: 'messageThread', @@ -16435,6 +16931,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '2697ab29-9991-4188-9569-8bc6fc079ec6', type: 'TEXT', name: 'subject', @@ -16456,6 +16953,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f6a75fcf-f8a3-41dd-b1d0-efce8358e2d2', type: 'TEXT', name: 'text', @@ -16477,6 +16975,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '29f11f81-aab0-4b96-971f-24d84c81f1bf', type: 'DATE_TIME', name: 'createdAt', @@ -16533,6 +17032,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '61fe3c78-ab04-4a83-9a40-8560f7285abe', type: 'RELATION', name: 'favorites', @@ -16580,6 +17080,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '027418ee-6028-456f-a570-0b032d35b07f', type: 'RELATION', name: 'viewFields', @@ -16627,6 +17128,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '4385a72b-bb9d-4ac6-9c08-d9853c468726', type: 'UUID', name: 'id', @@ -16648,6 +17150,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '421bf244-013a-4962-970a-37150cf38057', type: 'TEXT', name: 'type', @@ -16669,6 +17172,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '8ee5c1d6-718a-4e43-b468-91563270ae35', type: 'TEXT', name: 'icon', @@ -16690,6 +17194,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '073a7e52-0c68-4853-a500-5470b026c914', type: 'SELECT', name: 'key', @@ -16719,6 +17224,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'ed944e8e-e84d-40ef-aa44-b423453c23f9', type: 'BOOLEAN', name: 'isCompact', @@ -16740,6 +17246,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '5454b125-73c2-438b-991b-eb361bcd6295', type: 'TEXT', name: 'kanbanFieldMetadataId', @@ -16761,6 +17268,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '7974ab3b-7d05-4738-a05b-b76840f98328', type: 'RELATION', name: 'viewFilters', @@ -16808,6 +17316,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '23b99490-be92-4edc-a939-07e0f64f13eb', type: 'RELATION', name: 'viewSorts', @@ -16855,6 +17364,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '36f1f251-121a-461c-aef5-aba2c9fa39a6', type: 'UUID', name: 'objectMetadataId', @@ -16876,6 +17386,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: '2fcf2221-da1f-4c19-acdc-adfb089ea219', type: 'POSITION', name: 'position', @@ -16897,6 +17408,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'a0e02b0d-6681-41da-a8a8-48c0bc5ba690', type: 'DATE_TIME', name: 'createdAt', @@ -16918,6 +17430,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'dbc64e7b-2b59-4e9e-9e1e-bcd9e9f5a57a', type: 'DATE_TIME', name: 'deletedAt', @@ -16939,6 +17452,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'f5c9b9fc-87fd-4a74-af09-28a330a53bec', type: 'DATE_TIME', name: 'updatedAt', @@ -16960,6 +17474,7 @@ export const mockedStandardObjectMetadataQueryResult: ObjectMetadataItemsQuery = __typename: 'fieldEdge', node: { __typename: 'field', + settings: {}, id: 'a24df79d-a05a-45e7-8791-b71957dff236', type: 'TEXT', name: 'name', diff --git a/packages/twenty-front/src/testing/mock-data/objectMetadataItems.ts b/packages/twenty-front/src/testing/mock-data/generatedMockObjectMetadataItems.ts similarity index 100% rename from packages/twenty-front/src/testing/mock-data/objectMetadataItems.ts rename to packages/twenty-front/src/testing/mock-data/generatedMockObjectMetadataItems.ts diff --git a/packages/twenty-front/src/testing/mock-data/metadata.ts b/packages/twenty-front/src/testing/mock-data/metadata.ts deleted file mode 100644 index f739afd46647..000000000000 --- a/packages/twenty-front/src/testing/mock-data/metadata.ts +++ /dev/null @@ -1,290 +0,0 @@ -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { mapPaginatedObjectMetadataItemsToObjectMetadataItems } from '@/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems'; -import { - FieldMetadataType, - ObjectEdge, - ObjectMetadataItemsQuery, -} from '~/generated-metadata/graphql'; -import { mockedStandardObjectMetadataQueryResult } from '~/testing/mock-data/generated/mock-metadata-query-result'; - -// TODO: replace with new mock -const customObjectMetadataItemEdge: ObjectEdge = { - __typename: 'objectEdge', - node: { - __typename: 'object', - id: 'efa1addc-a9cb-4789-b99e-a060fa84f982', - dataSourceId: 'd36e6a2d-28bc-459d-afd5-fe18e4405729', - nameSingular: 'myCustom', - namePlural: 'myCustoms', - labelSingular: 'My Custom', - labelPlural: 'My Customs', - description: 'A custom object example', - icon: 'IconLayoutCollage', - isCustom: true, - isRemote: false, - isActive: true, - isSystem: false, - createdAt: '2024-04-08T12:48:49.538Z', - updatedAt: '2024-04-08T12:48:49.538Z', - labelIdentifierFieldMetadataId: null, - imageIdentifierFieldMetadataId: null, - fields: { - __typename: 'ObjectFieldsConnection', - pageInfo: { - __typename: 'PageInfo', - hasNextPage: false, - hasPreviousPage: false, - startCursor: 'YXJyYXljb25uZWN0aW9uOjA=', - endCursor: 'YXJyYXljb25uZWN0aW9uOjEz', - }, - edges: [ - { - __typename: 'fieldEdge', - node: { - __typename: 'field', - id: 'ea83af89-be10-49af-a605-10c3392ae007', - type: 'RELATION', - name: 'companies', - label: 'Companies', - description: 'A custom Relation example', - icon: 'IconTag', - isCustom: false, - isActive: true, - isSystem: true, - options: null, - isNullable: true, - createdAt: '2024-04-08T12:48:49.538Z', - updatedAt: '2024-04-08T12:48:49.538Z', - defaultValue: null, - relationDefinition: { - relationId: '1ec22b36-9e3c-4f24-8cf6-6c387ec3f243', - __typename: 'RelationDefinition', - direction: 'ONE_TO_MANY', - sourceObjectMetadata: { - __typename: 'object', - id: 'efa1addc-a9cb-4789-b99e-a060fa84f982', - nameSingular: 'myCustom', - namePlural: 'myCustoms', - }, - sourceFieldMetadata: { - __typename: 'field', - id: 'ea83af89-be10-49af-a605-10c3392ae007', - name: 'companies', - }, - targetObjectMetadata: { - __typename: 'object', - id: 'dba899da-7d88-41ac-b70e-5ea612ab4b2e', - nameSingular: 'company', - namePlural: 'companies', - }, - targetFieldMetadata: { - __typename: 'field', - id: 'c9607ed7-168d-4743-a56a-689ffcfffe98', - name: 'myCustom', - }, - }, - }, - }, - { - __typename: 'fieldEdge', - node: { - __typename: 'field', - id: 'c5384d2a-9ec3-4e1b-b93f-86f53f122169', - type: 'UUID', - name: 'objectMetadataId', - label: 'Object Metadata Id', - description: 'View target object', - icon: null, - isCustom: false, - isActive: true, - isSystem: true, - options: null, - isNullable: false, - createdAt: '2024-04-08T12:48:49.538Z', - updatedAt: '2024-04-08T12:48:49.538Z', - defaultValue: null, - relationDefinition: null, - }, - }, - { - __typename: 'fieldEdge', - node: { - __typename: 'field', - id: 'bb4d96be-e4d9-47a9-812d-fcdfb063ebf3', - type: 'POSITION', - name: 'position', - label: 'Position', - description: 'View position', - icon: null, - isCustom: false, - isActive: true, - isSystem: true, - options: null, - isNullable: true, - createdAt: '2024-04-08T12:48:49.538Z', - updatedAt: '2024-04-08T12:48:49.538Z', - defaultValue: null, - relationDefinition: null, - }, - }, - { - __typename: 'fieldEdge', - node: { - __typename: 'field', - id: 'f20c68aa-3930-41c4-9f79-45dceda506df', - type: 'TEXT', - name: 'name', - label: 'Name', - description: 'Custom name', - icon: null, - isCustom: false, - isActive: true, - isSystem: true, - options: null, - isNullable: false, - createdAt: '2024-04-08T12:48:49.538Z', - updatedAt: '2024-04-08T12:48:49.538Z', - defaultValue: "''", - relationDefinition: null, - }, - }, - { - __typename: 'fieldEdge', - node: { - __typename: 'field', - id: 'a3ef848d-660a-4aef-9cd4-5baf25ce36ed', - type: 'DATE_TIME', - name: 'createdAt', - label: 'Creation date', - description: 'Creation date', - icon: 'IconCalendar', - isCustom: false, - isActive: true, - isSystem: true, - options: null, - isNullable: false, - createdAt: '2024-04-08T12:48:49.538Z', - updatedAt: '2024-04-08T12:48:49.538Z', - defaultValue: 'now', - relationDefinition: null, - }, - }, - { - __typename: 'fieldEdge', - node: { - __typename: 'field', - id: '92f3e27c-041d-45b2-b2bd-46db2b1aec3f', - type: 'DATE_TIME', - name: 'updatedAt', - label: 'Update date', - description: 'Update date', - icon: 'IconCalendar', - isCustom: false, - isActive: true, - isSystem: true, - options: null, - isNullable: false, - createdAt: '2024-04-08T12:48:49.538Z', - updatedAt: '2024-04-08T12:48:49.538Z', - defaultValue: 'now', - relationDefinition: null, - }, - }, - { - __typename: 'fieldEdge', - node: { - __typename: 'field', - id: '8d7987eb-99e8-4e54-a86c-86b3bd07d2be', - type: 'UUID', - name: 'id', - label: 'Id', - description: 'Id', - icon: 'Icon123', - isCustom: false, - isActive: true, - isSystem: true, - options: null, - isNullable: false, - createdAt: '2024-04-08T12:48:49.538Z', - updatedAt: '2024-04-08T12:48:49.538Z', - defaultValue: 'uuid', - relationDefinition: null, - }, - }, - { - __typename: 'fieldEdge', - node: { - __typename: 'field', - id: 'e07fcc3f-beec-4d91-8488-9d1d2cfa5f99', - type: FieldMetadataType.Select, - name: 'priority', - label: 'Priority', - description: 'A custom Select example', - icon: 'IconWarning', - isCustom: true, - isActive: true, - isSystem: false, - options: [ - { - id: '2b98dc02-0d99-4f3e-890e-e2e6b8f3196c', - value: 'LOW', - label: 'Low', - color: 'turquoise', - }, - { - id: 'd925a8de-d8ec-4b59-a079-64f4012e3311', - value: 'MEDIUM', - label: 'Medium', - color: 'yellow', - }, - { - id: '6f6e1421-8a42-4d4a-bf76-465b5f84b6d2', - value: 'HIGH', - label: 'High', - color: 'red', - }, - ], - isNullable: true, - createdAt: '2024-04-08T12:48:49.538Z', - updatedAt: '2024-04-08T12:48:49.538Z', - defaultValue: null, - relationDefinition: null, - }, - }, - ], - }, - }, -} as ObjectEdge; - -export const mockedObjectMetadataItemsQueryResult = { - ...mockedStandardObjectMetadataQueryResult, - objects: { - ...mockedStandardObjectMetadataQueryResult.objects, - edges: [ - ...mockedStandardObjectMetadataQueryResult.objects.edges, - customObjectMetadataItemEdge, - ], - }, -} as ObjectMetadataItemsQuery; - -export const mockedObjectMetadataItems = - mapPaginatedObjectMetadataItemsToObjectMetadataItems({ - pagedObjectMetadataItems: mockedObjectMetadataItemsQueryResult, - }); - -export const mockedCompanyObjectMetadataItem = mockedObjectMetadataItems?.find( - (object) => object.nameSingular === 'company', -) as ObjectMetadataItem; - -export const mockedPersonObjectMetadataItem = mockedObjectMetadataItems?.find( - (object) => object.nameSingular === 'person', -) as ObjectMetadataItem; - -export const mockedCustomObjectMetadataItem = mockedObjectMetadataItems?.find( - (object) => object.nameSingular === 'myCustom', -) as ObjectMetadataItem; - -export const mockedOpportunityObjectMetadataItem = - mockedObjectMetadataItems?.find( - (object) => object.nameSingular === 'opportunity', - ) as ObjectMetadataItem; diff --git a/packages/twenty-front/src/testing/mock-data/users.ts b/packages/twenty-front/src/testing/mock-data/users.ts index f7a4c2727e22..cc483bd1e570 100644 --- a/packages/twenty-front/src/testing/mock-data/users.ts +++ b/packages/twenty-front/src/testing/mock-data/users.ts @@ -26,6 +26,7 @@ type MockedUser = Pick< locale: string; defaultWorkspace: Workspace; workspaces: Array<{ workspace: Workspace }>; + workspaceMembers: WorkspaceMember[]; }; export const avatarUrl = @@ -107,6 +108,7 @@ export const mockedUserData: MockedUser = { defaultWorkspace: mockDefaultWorkspace, locale: 'en', workspaces: [{ workspace: mockDefaultWorkspace }], + workspaceMembers: [mockedWorkspaceMemberData], onboardingStatus: OnboardingStatus.Completed, userVars: {}, }; diff --git a/packages/twenty-front/src/testing/mock-data/view-fields.ts b/packages/twenty-front/src/testing/mock-data/view-fields.ts index b68cfb706a27..325e3610ccd7 100644 --- a/packages/twenty-front/src/testing/mock-data/view-fields.ts +++ b/packages/twenty-front/src/testing/mock-data/view-fields.ts @@ -1,240 +1,416 @@ +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; import { mockedViewsData } from './views'; +const companyObjectMetadata = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'company', +); + +const personObjectMetadata = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', +); + +const opportunityObjectMetadata = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'opportunity', +); + export const mockedViewFieldsData = [ // Companies { id: '79035310-e955-4986-a4a4-73f9d9949c6a', - fieldMetadataId: '9e123592-cd2b-471c-8143-3cc0b46089ef', + fieldMetadataId: companyObjectMetadata?.fields.find( + (field) => field.name === 'name', + )?.id, viewId: mockedViewsData[0].id, position: 0, isVisible: true, size: 180, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, { id: '2a96bbc8-d86d-439a-8e50-4b07ebd27750', - fieldMetadataId: 'domainName', + fieldMetadataId: companyObjectMetadata?.fields.find( + (field) => field.name === 'domainName', + )?.id, viewId: mockedViewsData[0].id, position: 1, isVisible: true, size: 100, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, { id: '0c1b4c7b-6a3d-4fb0-bf2b-5d7c8fb844ed', - fieldMetadataId: 'accountOwner', + fieldMetadataId: companyObjectMetadata?.fields.find( + (field) => field.name === 'accountOwner', + )?.id, viewId: mockedViewsData[0].id, position: 2, isVisible: true, size: 150, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, { id: 'cc7f9560-32b5-4b82-8fd9-b05fe77c8cf7', - fieldMetadataId: 'createdAt', + fieldMetadataId: companyObjectMetadata?.fields.find( + (field) => field.name === 'createdAt', + )?.id, viewId: mockedViewsData[0].id, position: 3, isVisible: true, size: 150, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, { id: '3de4d078-3396-4480-be2d-6f3b1a228b0d', - fieldMetadataId: 'employees', + fieldMetadataId: companyObjectMetadata?.fields.find( + (field) => field.name === 'employees', + )?.id, viewId: mockedViewsData[0].id, position: 4, isVisible: true, size: 150, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, { id: '4650c8fb-0f1e-4342-88dc-adedae1445f9', - fieldMetadataId: 'linkedin', + fieldMetadataId: companyObjectMetadata?.fields.find( + (field) => field.name === 'linkedinLink', + )?.id, viewId: mockedViewsData[0].id, position: 5, isVisible: true, size: 170, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, { id: '727430bf-6ff8-4c85-9828-cbe72ac0fc27', - fieldMetadataId: 'address', + fieldMetadataId: companyObjectMetadata?.fields.find( + (field) => field.name === 'address', + )?.id, viewId: mockedViewsData[0].id, position: 6, isVisible: true, size: 170, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, + __typename: 'ViewField', + }, + + // Companies v2 + { + id: '79035310-e955-4986-a4a4-73f9d9949c6a', + fieldMetadataId: companyObjectMetadata?.fields.find( + (field) => field.name === 'name', + )?.id, + viewId: mockedViewsData[3].id, + position: 0, + isVisible: true, + size: 180, + createdAt: '2021-09-01T00:00:00.000Z', + updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, + __typename: 'ViewField', + }, + { + id: '2a96bbc8-d86d-439a-8e50-4b07ebd27750', + fieldMetadataId: companyObjectMetadata?.fields.find( + (field) => field.name === 'domainName', + )?.id, + viewId: mockedViewsData[3].id, + position: 1, + isVisible: true, + size: 100, + createdAt: '2021-09-01T00:00:00.000Z', + updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, + __typename: 'ViewField', + }, + { + id: '0c1b4c7b-6a3d-4fb0-bf2b-5d7c8fb844ed', + fieldMetadataId: companyObjectMetadata?.fields.find( + (field) => field.name === 'accountOwner', + )?.id, + viewId: mockedViewsData[3].id, + position: 2, + isVisible: true, + size: 150, + createdAt: '2021-09-01T00:00:00.000Z', + updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, + __typename: 'ViewField', + }, + { + id: 'cc7f9560-32b5-4b82-8fd9-b05fe77c8cf7', + fieldMetadataId: companyObjectMetadata?.fields.find( + (field) => field.name === 'createdAt', + )?.id, + viewId: mockedViewsData[3].id, + position: 3, + isVisible: true, + size: 150, + createdAt: '2021-09-01T00:00:00.000Z', + updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, + __typename: 'ViewField', + }, + { + id: '3de4d078-3396-4480-be2d-6f3b1a228b0d', + fieldMetadataId: companyObjectMetadata?.fields.find( + (field) => field.name === 'employees', + )?.id, + viewId: mockedViewsData[3].id, + position: 4, + isVisible: true, + size: 150, + createdAt: '2021-09-01T00:00:00.000Z', + updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, + __typename: 'ViewField', + }, + { + id: '4650c8fb-0f1e-4342-88dc-adedae1445f9', + fieldMetadataId: companyObjectMetadata?.fields.find( + (field) => field.name === 'linkedinLink', + )?.id, + viewId: mockedViewsData[3].id, + position: 5, + isVisible: true, + size: 170, + createdAt: '2021-09-01T00:00:00.000Z', + updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, + __typename: 'ViewField', + }, + { + id: '727430bf-6ff8-4c85-9828-cbe72ac0fc27', + fieldMetadataId: companyObjectMetadata?.fields.find( + (field) => field.name === 'address', + )?.id, + viewId: mockedViewsData[3].id, + position: 6, + isVisible: true, + size: 170, + createdAt: '2021-09-01T00:00:00.000Z', + updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, // People { id: '28894146-4fde-4a16-a9ca-1a31b5b788b4', - fieldMetadataId: 'displayName', + fieldMetadataId: personObjectMetadata?.fields.find( + (field) => field.name === 'name', + )?.id, viewId: mockedViewsData[1].id, position: 0, isVisible: true, size: 210, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, { id: 'e1e24864-8601-4cd8-8a63-09c1285f2e39', - fieldMetadataId: 'email', + fieldMetadataId: personObjectMetadata?.fields.find( + (field) => field.name === 'emails', + )?.id, viewId: mockedViewsData[1].id, position: 1, isVisible: true, size: 150, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, { id: '5a1df716-7211-445a-9f16-9783a00998a7', - fieldMetadataId: 'company', + fieldMetadataId: personObjectMetadata?.fields.find( + (field) => field.name === 'company', + )?.id, viewId: mockedViewsData[1].id, position: 2, isVisible: true, size: 150, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, { id: 'a6e1197a-7e84-4d92-ace2-367c0bc46c49', - fieldMetadataId: 'phone', + fieldMetadataId: personObjectMetadata?.fields.find( + (field) => field.name === 'phones', + )?.id, viewId: mockedViewsData[1].id, position: 3, isVisible: true, size: 150, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, { id: 'c9343097-d14b-4559-a5fa-626c1527d39f', - fieldMetadataId: 'createdAt', + fieldMetadataId: personObjectMetadata?.fields.find( + (field) => field.name === 'createdAt', + )?.id, viewId: mockedViewsData[1].id, position: 4, isVisible: true, size: 150, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, { id: 'a873e5f0-fed6-47e9-a712-6854eab3ec77', - fieldMetadataId: 'city', + fieldMetadataId: personObjectMetadata?.fields.find( + (field) => field.name === 'city', + )?.id, viewId: mockedViewsData[1].id, position: 5, isVisible: true, size: 150, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, { id: '66f134b8-5329-422f-b88e-83e6bb707eb5', - fieldMetadataId: 'jobTitle', + fieldMetadataId: personObjectMetadata?.fields.find( + (field) => field.name === 'jobTitle', + )?.id, viewId: mockedViewsData[1].id, position: 6, isVisible: true, size: 150, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, { id: '648faa24-cabb-482a-8578-ba3f09906017', - fieldMetadataId: 'linkedin', + fieldMetadataId: personObjectMetadata?.fields.find( + (field) => field.name === 'linkedinLink', + )?.id, viewId: mockedViewsData[1].id, position: 7, isVisible: true, size: 150, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, { id: '3a9e7f0d-a4ce-4ad5-aac7-3a24eb1a412d', - fieldMetadataId: 'x', + fieldMetadataId: personObjectMetadata?.fields.find( + (field) => field.name === 'xLink', + )?.id, viewId: mockedViewsData[1].id, position: 8, isVisible: true, size: 150, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, // Opportunities { id: '35a42e2d-83dd-4b57-ada6-f90616da706d', - fieldMetadataId: 'amount', + fieldMetadataId: opportunityObjectMetadata?.fields.find( + (field) => field.name === 'name', + )?.id, viewId: mockedViewsData[2].id, position: 0, isVisible: true, size: 180, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, { id: '3159acd8-463f-458d-bf9a-af8ac6f57dc0', - fieldMetadataId: 'closeDate', + fieldMetadataId: opportunityObjectMetadata?.fields.find( + (field) => field.name === 'closeDate', + )?.id, viewId: mockedViewsData[2].id, position: 2, isVisible: true, size: 100, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, { id: 'afc0819d-b694-4e3c-a2e6-25261aa3ed2c', - fieldMetadataId: 'company', + fieldMetadataId: opportunityObjectMetadata?.fields.find( + (field) => field.name === 'company', + )?.id, viewId: mockedViewsData[2].id, position: 3, isVisible: true, size: 150, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, { id: 'ec0507bb-aedc-4695-ba96-d81bdeb9db83', - fieldMetadataId: 'createdAt', + fieldMetadataId: opportunityObjectMetadata?.fields.find( + (field) => field.name === 'createdAt', + )?.id, viewId: mockedViewsData[2].id, position: 4, isVisible: true, size: 150, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, { id: '3f1585f6-44f6-45c5-b840-bc05af5d0008', - fieldMetadataId: 'pointOfContact', + fieldMetadataId: opportunityObjectMetadata?.fields.find( + (field) => field.name === 'pointOfContact', + )?.id, viewId: mockedViewsData[2].id, position: 5, isVisible: true, size: 150, createdAt: '2021-09-01T00:00:00.000Z', updatedAt: '2021-09-01T00:00:00.000Z', + deletedAt: null, __typename: 'ViewField', }, ]; diff --git a/packages/twenty-front/src/testing/mock-data/views.ts b/packages/twenty-front/src/testing/mock-data/views.ts index d13c0dc3fdfb..3318e158b4f6 100644 --- a/packages/twenty-front/src/testing/mock-data/views.ts +++ b/packages/twenty-front/src/testing/mock-data/views.ts @@ -1,813 +1,22 @@ -import { RecordGqlConnection } from '@/object-record/graphql/types/RecordGqlConnection'; -import { ViewType } from '@/views/types/ViewType'; +import { generatedMockObjectMetadataItems } from '~/testing/mock-data/generatedMockObjectMetadataItems'; -export const viewQueryResultMock: RecordGqlConnection = { - __typename: 'ViewConnection', - totalCount: 6, - pageInfo: { - __typename: 'PageInfo', - hasNextPage: false, - hasPreviousPage: false, - startCursor: 'WyIyY2M5MGJjZC0wNzkzLTRkMzctYWZlOS1kZTVlY2NmYmFlNzEiXQ==', - endCursor: 'WyJmZjhlZGQyMi02NjVhLTQ5NWYtODljYy03MGFiOGZkNWMxYTYiXQ==', - }, - edges: [ - { - __typename: 'ViewEdge', - cursor: 'WyIyY2M5MGJjZC0wNzkzLTRkMzctYWZlOS1kZTVlY2NmYmFlNzEiXQ==', - node: { - __typename: 'View', - position: 1, - updatedAt: '2024-07-11T10:21:33.304Z', - key: null, - id: '2cc90bcd-0793-4d37-afe9-de5eccfbae71', - objectMetadataId: '9c293c05-f461-456a-b5a2-2710b5b30447', - createdAt: '2024-07-11T10:21:33.304Z', - icon: 'IconLayoutKanban', - isCompact: false, - name: 'By Stage', - type: 'kanban' as ViewType, - kanbanFieldMetadataId: 'f74de381-4392-4662-a890-5ed3b5bd847d', - viewSorts: { - __typename: 'ViewSortConnection', - edges: [], - }, - viewFilters: { - __typename: 'ViewFilterConnection', - edges: [], - }, - viewFields: { - __typename: 'ViewFieldConnection', - edges: [ - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 2, - id: '05ffd5e0-69b0-4774-843a-fbae12231e7d', - viewId: '2cc90bcd-0793-4d37-afe9-de5eccfbae71', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: 'c4e1b90f-bf9a-4a04-b67a-0f88263d8706', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 4, - id: '573ae123-1eed-4671-8fff-d9ac9455b1b4', - viewId: '2cc90bcd-0793-4d37-afe9-de5eccfbae71', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: '6e073ac2-034c-43ab-b0c6-206b1dd1174b', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 0, - id: 'ae4f318f-5059-41ba-b365-22daa0b3cb0e', - viewId: '2cc90bcd-0793-4d37-afe9-de5eccfbae71', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: '4ee7183a-f1f6-42a6-94e5-79f741357760', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 1, - id: 'b5ac37dc-9f64-412f-a598-611bdb5d27f8', - viewId: '2cc90bcd-0793-4d37-afe9-de5eccfbae71', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: '37593700-f3ac-43a2-9ce2-1b811fa3fbfc', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 5, - id: 'bda12277-2962-4b35-a549-665cbbe53483', - viewId: '2cc90bcd-0793-4d37-afe9-de5eccfbae71', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: '031bc747-1787-4e46-9320-562a8b75f3ff', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 3, - id: 'f43e660f-bbf8-4a2f-aeb1-54890ac40f4b', - viewId: '2cc90bcd-0793-4d37-afe9-de5eccfbae71', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: '2cc0fa2b-dbea-4fd0-b7f5-11fa54cd0242', - }, - }, - ], - }, - }, - }, - { - __typename: 'ViewEdge', - cursor: 'WyI1N2FkYTUyMy0zZDgzLTQzOTEtYThiOS0wZTkxOGUyNGE1MTkiXQ==', - node: { - __typename: 'View', - position: null, - updatedAt: '2024-07-12T09:52:15.595Z', - key: 'INDEX', - id: '57ada523-3d83-4391-a8b9-0e918e24a519', - objectMetadataId: '3561dbe5-39a2-40fa-a111-4af924e39908', - createdAt: '2024-07-12T09:52:15.595Z', - icon: 'IconListNumbers', - isCompact: false, - name: 'All Tests', - type: 'table', - kanbanFieldMetadataId: '', - viewSorts: { - __typename: 'ViewSortConnection', - edges: [], - }, - viewFilters: { - __typename: 'ViewFilterConnection', - edges: [], - }, - viewFields: { - __typename: 'ViewFieldConnection', - edges: [ - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-16T12:58:56.823Z', - position: 2, - id: '39a026d9-8362-4a4c-9b35-3d23218122a7', - viewId: '57ada523-3d83-4391-a8b9-0e918e24a519', - createdAt: '2024-07-16T12:58:56.823Z', - isVisible: true, - size: 100, - fieldMetadataId: '9918f304-99d9-4d5b-8351-c6b6f7cc38bb', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-12T09:52:15.598Z', - position: 0, - id: '3ab70930-e60a-4bfd-830a-57355121d889', - viewId: '57ada523-3d83-4391-a8b9-0e918e24a519', - createdAt: '2024-07-12T09:52:15.598Z', - isVisible: true, - size: 180, - fieldMetadataId: 'f7f485fc-0c14-4b70-a180-0508699a5c14', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-12T09:52:15.604Z', - position: 1, - id: '43ec0b2c-d94f-4eaf-a4bc-f00d409661b5', - viewId: '57ada523-3d83-4391-a8b9-0e918e24a519', - createdAt: '2024-07-12T09:52:15.604Z', - isVisible: true, - size: 180, - fieldMetadataId: '66645848-4100-4649-bc0e-d50281df2fd6', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-12T09:52:15.604Z', - position: 2, - id: '53f01c19-7042-4551-97d8-d36b6ae28602', - viewId: '57ada523-3d83-4391-a8b9-0e918e24a519', - createdAt: '2024-07-12T09:52:15.604Z', - isVisible: true, - size: 180, - fieldMetadataId: '1b3caa7a-343a-4b4b-8c2e-3371cd1dd237', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-16T12:59:52.966Z', - position: 3, - id: 'ecbc275e-f937-4d00-b035-6225e6f87c90', - viewId: '57ada523-3d83-4391-a8b9-0e918e24a519', - createdAt: '2024-07-16T12:59:46.864Z', - isVisible: true, - size: 209, - fieldMetadataId: '9e3e6ed9-7889-4979-bc15-c7803bf437f1', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-12T09:52:15.605Z', - position: 3, - id: 'ed9f32e7-cd08-4bcf-b78f-838371cd282a', - viewId: '57ada523-3d83-4391-a8b9-0e918e24a519', - createdAt: '2024-07-12T09:52:15.605Z', - isVisible: true, - size: 180, - fieldMetadataId: 'ffa953c4-d8e0-49af-b2ef-f16e238f4687', - }, - }, - ], - }, - }, - }, - { - __typename: 'ViewEdge', - cursor: 'WyI1ODJmMjI0Yy0zYzNmLTQxMjctYjFlZC0yOTcxZDI3ZTU0YTQiXQ==', - node: { - __typename: 'View', - position: 0, - updatedAt: '2024-07-11T10:21:33.304Z', - key: 'INDEX', - id: '582f224c-3c3f-4127-b1ed-2971d27e54a4', - objectMetadataId: '9c293c05-f461-456a-b5a2-2710b5b30447', - createdAt: '2024-07-11T10:21:33.304Z', - icon: 'IconTargetArrow', - isCompact: false, - name: 'All Opportunities', - type: 'table', - kanbanFieldMetadataId: '', - viewSorts: { - __typename: 'ViewSortConnection', - edges: [], - }, - viewFilters: { - __typename: 'ViewFilterConnection', - edges: [], - }, - viewFields: { - __typename: 'ViewFieldConnection', - edges: [ - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 2, - id: '0f0cacad-7f1d-4667-a0e8-466cddad3e65', - viewId: '582f224c-3c3f-4127-b1ed-2971d27e54a4', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: 'c4e1b90f-bf9a-4a04-b67a-0f88263d8706', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 1, - id: '19d0d674-9825-492d-bbd0-c1de494201dc', - viewId: '582f224c-3c3f-4127-b1ed-2971d27e54a4', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: '37593700-f3ac-43a2-9ce2-1b811fa3fbfc', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 5, - id: '77d6a102-7b8e-40c0-9d53-33e9a8d0df0f', - viewId: '582f224c-3c3f-4127-b1ed-2971d27e54a4', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: '031bc747-1787-4e46-9320-562a8b75f3ff', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 4, - id: '8d1da76d-4056-4675-b2e5-907021c1b482', - viewId: '582f224c-3c3f-4127-b1ed-2971d27e54a4', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: '6e073ac2-034c-43ab-b0c6-206b1dd1174b', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 3, - id: 'a5556adc-e4a0-4f71-aee3-2ff2a4e53b31', - viewId: '582f224c-3c3f-4127-b1ed-2971d27e54a4', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: '2cc0fa2b-dbea-4fd0-b7f5-11fa54cd0242', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 0, - id: 'fa8cdb32-24f0-483d-a9f6-bc92f2704452', - viewId: '582f224c-3c3f-4127-b1ed-2971d27e54a4', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: '4ee7183a-f1f6-42a6-94e5-79f741357760', - }, - }, - ], - }, - }, - }, - { - __typename: 'ViewEdge', - cursor: 'WyI2NTM3M2UxZS0xNmU1LTRlNWYtOWJjMS1jMDlkOTAxNTZmMjciXQ==', - node: { - __typename: 'View', - position: null, - updatedAt: '2024-07-11T15:41:08.076Z', - key: null, - id: '65373e1e-16e5-4e5f-9bc1-c09d90156f27', - objectMetadataId: 'b8115dc1-5304-4d22-b300-0b4efda42ebc', - createdAt: '2024-07-11T15:41:08.076Z', - icon: 'IconBuildingSkyscraper', - isCompact: false, - name: 'All Companies L', - type: 'kanban', - kanbanFieldMetadataId: '4ba829d2-c34a-40d0-9ae6-a65d11d2ff5a', - viewSorts: { - __typename: 'ViewSortConnection', - edges: [], - }, - viewFilters: { - __typename: 'ViewFilterConnection', - edges: [], - }, - viewFields: { - __typename: 'ViewFieldConnection', - edges: [ - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T15:41:09.141Z', - position: 5, - id: '00bf00d5-f257-4ed0-9a80-ce6d7fa2eace', - viewId: '65373e1e-16e5-4e5f-9bc1-c09d90156f27', - createdAt: '2024-07-11T15:41:09.141Z', - isVisible: true, - size: 170, - fieldMetadataId: 'f50611a0-d4b2-49a3-8110-1ca1282ad9c2', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T15:41:09.082Z', - position: 4, - id: '08ccb08d-d279-4738-bdc0-32a0f9b01390', - viewId: '65373e1e-16e5-4e5f-9bc1-c09d90156f27', - createdAt: '2024-07-11T15:41:09.082Z', - isVisible: true, - size: 150, - fieldMetadataId: '2334adb8-a0c5-408e-a449-6730f010aff1', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T15:41:09.115Z', - position: 2, - id: '478c93ae-1dcc-4d79-b821-b53431348abe', - viewId: '65373e1e-16e5-4e5f-9bc1-c09d90156f27', - createdAt: '2024-07-11T15:41:09.115Z', - isVisible: true, - size: 150, - fieldMetadataId: 'be572271-de80-4d55-ae25-6141ec48e1a7', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T15:41:09.083Z', - position: 7, - id: '4b28e7c9-f97b-4e86-80bf-ca7a1cc49f64', - viewId: '65373e1e-16e5-4e5f-9bc1-c09d90156f27', - createdAt: '2024-07-11T15:41:09.083Z', - isVisible: true, - size: 180, - fieldMetadataId: '4ba829d2-c34a-40d0-9ae6-a65d11d2ff5a', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T15:41:09.076Z', - position: 3, - id: '6402a5db-dc6f-433c-9de3-af19a6d71a28', - viewId: '65373e1e-16e5-4e5f-9bc1-c09d90156f27', - createdAt: '2024-07-11T15:41:09.076Z', - isVisible: true, - size: 150, - fieldMetadataId: '04f98129-3433-43f6-a5fa-5ede5314fafd', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T15:41:09.141Z', - position: 1, - id: '97938fb7-d3a2-42a1-8c04-7ff59d18e41c', - viewId: '65373e1e-16e5-4e5f-9bc1-c09d90156f27', - createdAt: '2024-07-11T15:41:09.141Z', - isVisible: true, - size: 100, - fieldMetadataId: '7b76bf52-04ff-4624-9dd5-26ef59be0d88', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T15:41:09.129Z', - position: 0, - id: 'c7429772-5214-49ee-9d96-c4c9ea929888', - viewId: '65373e1e-16e5-4e5f-9bc1-c09d90156f27', - createdAt: '2024-07-11T15:41:09.129Z', - isVisible: true, - size: 180, - fieldMetadataId: '716b202a-7f2f-4d7a-a78a-666db003d94f', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T15:41:09.083Z', - position: 6, - id: 'ec211171-6676-40d2-acc2-5fa13f11ed00', - viewId: '65373e1e-16e5-4e5f-9bc1-c09d90156f27', - createdAt: '2024-07-11T15:41:09.083Z', - isVisible: true, - size: 170, - fieldMetadataId: '479e7d9f-cd8a-4064-b009-65cb89a16c36', - }, - }, - ], - }, - }, - }, - { - __typename: 'ViewEdge', - cursor: 'WyJiZWU2NWJjNC05YmNiLTQ5YTgtOGVhNS0xYmQ5MjQxYjA5YzMiXQ==', - node: { - __typename: 'View', - position: 0, - updatedAt: '2024-07-11T10:21:33.304Z', - key: 'INDEX', - id: 'bee65bc4-9bcb-49a8-8ea5-1bd9241b09c3', - objectMetadataId: '48824ee2-367d-481f-b80b-ca1eeb85c4ab', - createdAt: '2024-07-11T10:21:33.304Z', - icon: 'IconUser', - isCompact: false, - name: 'All People', - type: 'table', - kanbanFieldMetadataId: '', - viewSorts: { - __typename: 'ViewSortConnection', - edges: [], - }, - viewFilters: { - __typename: 'ViewFilterConnection', - edges: [], - }, - viewFields: { - __typename: 'ViewFieldConnection', - edges: [ - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 3, - id: '2dc48490-3ee8-4ade-a979-d5326da33d43', - viewId: 'bee65bc4-9bcb-49a8-8ea5-1bd9241b09c3', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: '34ed07ad-067a-4f5f-bdee-21a37616f96b', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 0, - id: '67af4225-56e0-4ef9-bcfc-4a551d676c2b', - viewId: 'bee65bc4-9bcb-49a8-8ea5-1bd9241b09c3', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 210, - fieldMetadataId: 'c485ed46-3f8a-4ee6-af70-628b9f18ad47', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 5, - id: '796bdd63-cc83-4f8c-b538-9f8e9dfb1937', - viewId: 'bee65bc4-9bcb-49a8-8ea5-1bd9241b09c3', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: '4aadffed-1df4-4732-bb99-559f31a464af', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 7, - id: '80cb1229-5e05-45d4-89da-b2ec850ffb2f', - viewId: 'bee65bc4-9bcb-49a8-8ea5-1bd9241b09c3', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: 'a8298361-b7c8-4b6c-be6c-d33885e00237', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 8, - id: '835b104c-b9fc-4c9f-b659-3dc4bb54d9ef', - viewId: 'bee65bc4-9bcb-49a8-8ea5-1bd9241b09c3', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: '125cbc00-7efb-473d-b0a6-581d3cf868dd', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 6, - id: '8f494c43-6b63-4033-b303-0110698cf19c', - viewId: 'bee65bc4-9bcb-49a8-8ea5-1bd9241b09c3', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: '22566388-8ece-43dc-8205-371e662716d4', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 4, - id: 'b2d72e77-a323-4e2e-acef-598b6da04712', - viewId: 'bee65bc4-9bcb-49a8-8ea5-1bd9241b09c3', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: 'b328512c-ff13-431b-9c94-1018ef0bd53c', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 1, - id: 'c7e1a253-9af8-498a-b579-adab742acf2d', - viewId: 'bee65bc4-9bcb-49a8-8ea5-1bd9241b09c3', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: '2eb50615-376c-45e8-b99b-440a92a912d3', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 2, - id: 'cceae812-2687-49b5-a0c8-eb59956865e8', - viewId: 'bee65bc4-9bcb-49a8-8ea5-1bd9241b09c3', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: 'f34c04f8-ce2f-4c92-8dbc-9166c6e0d49f', - }, - }, - ], - }, - }, - }, - { - __typename: 'ViewEdge', - cursor: 'WyJmZjhlZGQyMi02NjVhLTQ5NWYtODljYy03MGFiOGZkNWMxYTYiXQ==', - node: { - __typename: 'View', - position: 0, - updatedAt: '2024-07-11T10:21:33.304Z', - key: 'INDEX', - id: 'ff8edd22-665a-495f-89cc-70ab8fd5c1a6', - objectMetadataId: 'b8115dc1-5304-4d22-b300-0b4efda42ebc', - createdAt: '2024-07-11T10:21:33.304Z', - icon: 'IconBuildingSkyscraper', - isCompact: false, - name: 'All Companies', - type: 'table', - kanbanFieldMetadataId: '', - viewSorts: { - __typename: 'ViewSortConnection', - edges: [], - }, - viewFilters: { - __typename: 'ViewFilterConnection', - edges: [], - }, - viewFields: { - __typename: 'ViewFieldConnection', - edges: [ - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 3, - id: '1cb6aeed-8011-495f-9371-20bace45814a', - viewId: 'ff8edd22-665a-495f-89cc-70ab8fd5c1a6', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: '04f98129-3433-43f6-a5fa-5ede5314fafd', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 6, - id: '3beb2130-bdc5-48d1-8cd0-22c5d0010ad2', - viewId: 'ff8edd22-665a-495f-89cc-70ab8fd5c1a6', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 170, - fieldMetadataId: '479e7d9f-cd8a-4064-b009-65cb89a16c36', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 4, - id: '5f73729b-9592-473a-8742-8e52b693c780', - viewId: 'ff8edd22-665a-495f-89cc-70ab8fd5c1a6', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: '2334adb8-a0c5-408e-a449-6730f010aff1', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T15:41:03.814Z', - position: 7, - id: '788627cb-0d3a-4659-ab4b-69deabf02f27', - viewId: 'ff8edd22-665a-495f-89cc-70ab8fd5c1a6', - createdAt: '2024-07-11T15:41:03.814Z', - isVisible: true, - size: 180, - fieldMetadataId: '4ba829d2-c34a-40d0-9ae6-a65d11d2ff5a', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 2, - id: 'ac5797a1-2d29-42d2-b9fb-d679a945eec5', - viewId: 'ff8edd22-665a-495f-89cc-70ab8fd5c1a6', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 150, - fieldMetadataId: 'be572271-de80-4d55-ae25-6141ec48e1a7', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 0, - id: 'ae98037e-38f7-4fbf-8ae1-c0b6754c6311', - viewId: 'ff8edd22-665a-495f-89cc-70ab8fd5c1a6', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 180, - fieldMetadataId: '716b202a-7f2f-4d7a-a78a-666db003d94f', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 5, - id: 'b4d9f94e-0c4b-4422-839a-f2ceb293fde1', - viewId: 'ff8edd22-665a-495f-89cc-70ab8fd5c1a6', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 170, - fieldMetadataId: 'f50611a0-d4b2-49a3-8110-1ca1282ad9c2', - }, - }, - { - __typename: 'ViewFieldEdge', - node: { - __typename: 'ViewField', - updatedAt: '2024-07-11T10:21:33.304Z', - position: 1, - id: 'd45b1412-ff6b-41e5-86df-0fb778033bb3', - viewId: 'ff8edd22-665a-495f-89cc-70ab8fd5c1a6', - createdAt: '2024-07-11T10:21:33.304Z', - isVisible: true, - size: 100, - fieldMetadataId: '7b76bf52-04ff-4624-9dd5-26ef59be0d88', - }, - }, - ], - }, - }, - }, - ], -}; +const companyObjectMetadata = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'company', +); + +const personObjectMetadata = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'person', +); + +const opportunityObjectMetadata = generatedMockObjectMetadataItems.find( + (item) => item.nameSingular === 'opportunity', +); export const mockedViewsData = [ { id: '37a8a866-eb17-4e76-9382-03143a2f6a80', name: 'All companies', - objectMetadataId: '701aecf9-eb1c-4d84-9d94-b954b231b64b', + objectMetadataId: companyObjectMetadata?.id, type: 'table', icon: 'IconSkyline', key: 'INDEX', @@ -822,7 +31,7 @@ export const mockedViewsData = [ { id: '6095799e-b48f-4e00-b071-10818083593a', name: 'All people', - objectMetadataId: 'person', + objectMetadataId: personObjectMetadata?.id, type: 'table', icon: 'IconPerson', key: 'INDEX', @@ -836,7 +45,7 @@ export const mockedViewsData = [ { id: 'e26f66b7-f890-4a5c-b4d2-ec09987b5308', name: 'All opportunities', - objectMetadataId: 'company', + objectMetadataId: opportunityObjectMetadata?.id, type: 'kanban', icon: 'IconOpportunity', key: 'INDEX', @@ -850,7 +59,7 @@ export const mockedViewsData = [ { id: '5c307222-1dd5-4ff3-ab06-8d990e9b3c74', name: 'All companies (v2)', - objectMetadataId: '701aecf9-eb1c-4d84-9d94-b954b231b64b', + objectMetadataId: companyObjectMetadata?.id, type: 'table', icon: 'IconSkyline', key: 'INDEX', diff --git a/packages/twenty-front/src/types/ExcludeLiteral.ts b/packages/twenty-front/src/types/ExcludeLiteral.ts new file mode 100644 index 000000000000..897d995b7a91 --- /dev/null +++ b/packages/twenty-front/src/types/ExcludeLiteral.ts @@ -0,0 +1 @@ +export type ExcludeLiteral<T, U extends T> = T extends U ? never : T; diff --git a/packages/twenty-front/src/types/PickLiteral.ts b/packages/twenty-front/src/types/PickLiteral.ts new file mode 100644 index 000000000000..545378336485 --- /dev/null +++ b/packages/twenty-front/src/types/PickLiteral.ts @@ -0,0 +1 @@ +export type PickLiteral<T, U extends T> = U; diff --git a/packages/twenty-front/src/utils/__tests__/cast-as-integer-or-null.test.ts b/packages/twenty-front/src/utils/__tests__/cast-as-integer-or-null.test.ts deleted file mode 100644 index cc077afdb27c..000000000000 --- a/packages/twenty-front/src/utils/__tests__/cast-as-integer-or-null.test.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { - canBeCastAsIntegerOrNull, - castAsIntegerOrNull, -} from '../cast-as-integer-or-null'; - -describe('canBeCastAsIntegerOrNull', () => { - it(`should return true if null`, () => { - expect(canBeCastAsIntegerOrNull(null)).toBeTruthy(); - }); - - it(`should return true if number`, () => { - expect(canBeCastAsIntegerOrNull(9)).toBeTruthy(); - }); - - it(`should return true if empty string`, () => { - expect(canBeCastAsIntegerOrNull('')).toBeTruthy(); - }); - - it(`should return true if integer string`, () => { - expect(canBeCastAsIntegerOrNull('9')).toBeTruthy(); - }); - - it(`should return false if undefined`, () => { - expect(canBeCastAsIntegerOrNull(undefined)).toBeFalsy(); - }); - - it(`should return false if non numeric string`, () => { - expect(canBeCastAsIntegerOrNull('9a')).toBeFalsy(); - }); - - it(`should return false if non numeric string #2`, () => { - expect(canBeCastAsIntegerOrNull('a9a')).toBeFalsy(); - }); - - it(`should return false if float`, () => { - expect(canBeCastAsIntegerOrNull(0.9)).toBeFalsy(); - }); - - it(`should return false if float string`, () => { - expect(canBeCastAsIntegerOrNull('0.9')).toBeFalsy(); - }); -}); - -describe('castAsIntegerOrNull', () => { - it(`should cast null to null`, () => { - expect(castAsIntegerOrNull(null)).toBe(null); - }); - - it(`should cast empty string to null`, () => { - expect(castAsIntegerOrNull('')).toBe(null); - }); - - it(`should cast an integer to an integer`, () => { - expect(castAsIntegerOrNull(9)).toBe(9); - }); - - it(`should cast an integer string to an integer`, () => { - expect(castAsIntegerOrNull('9')).toBe(9); - }); - - it(`should throw if trying to cast a float string to an integer`, () => { - expect(() => castAsIntegerOrNull('9.9')).toThrow(Error); - }); - - it(`should throw if trying to cast a non numeric string to an integer`, () => { - expect(() => castAsIntegerOrNull('9.9a')).toThrow(Error); - }); - - it(`should throw if trying to cast an undefined to an integer`, () => { - expect(() => castAsIntegerOrNull(undefined)).toThrow(Error); - }); -}); diff --git a/packages/twenty-front/src/utils/__tests__/cast-as-number-or-null.test.ts b/packages/twenty-front/src/utils/__tests__/cast-as-number-or-null.test.ts new file mode 100644 index 000000000000..082527de7ec7 --- /dev/null +++ b/packages/twenty-front/src/utils/__tests__/cast-as-number-or-null.test.ts @@ -0,0 +1,72 @@ +import { + canBeCastAsNumberOrNull, + castAsNumberOrNull, +} from '../cast-as-number-or-null'; + +describe('canBeCastAsNumberOrNull', () => { + it(`should return true if null`, () => { + expect(canBeCastAsNumberOrNull(null)).toBeTruthy(); + }); + + it(`should return true if number`, () => { + expect(canBeCastAsNumberOrNull(9)).toBeTruthy(); + }); + + it(`should return true if empty string`, () => { + expect(canBeCastAsNumberOrNull('')).toBeTruthy(); + }); + + it(`should return true if integer string`, () => { + expect(canBeCastAsNumberOrNull('9')).toBeTruthy(); + }); + + it(`should return false if undefined`, () => { + expect(canBeCastAsNumberOrNull(undefined)).toBeFalsy(); + }); + + it(`should return false if non numeric string`, () => { + expect(canBeCastAsNumberOrNull('9a')).toBeFalsy(); + }); + + it(`should return false if non numeric string #2`, () => { + expect(canBeCastAsNumberOrNull('a9a')).toBeFalsy(); + }); + + it(`should return true if float`, () => { + expect(canBeCastAsNumberOrNull(0.9)).toBeTruthy(); + }); + + it(`should return true if float string`, () => { + expect(canBeCastAsNumberOrNull('0.9')).toBeTruthy(); + }); +}); + +describe('castAsNumberOrNull', () => { + it(`should cast null to null`, () => { + expect(castAsNumberOrNull(null)).toBe(null); + }); + + it(`should cast empty string to null`, () => { + expect(castAsNumberOrNull('')).toBe(null); + }); + + it(`should cast an integer to an integer`, () => { + expect(castAsNumberOrNull(9)).toBe(9); + }); + + it(`should cast an integer string to an integer`, () => { + expect(castAsNumberOrNull('9')).toBe(9); + }); + + it(`should throw if trying to cast a float string to an integer`, () => { + expect(castAsNumberOrNull('9.9')).toBe(9.9); + }); + + it(`should throw if trying to cast a non numeric string to an integer`, () => { + expect(() => castAsNumberOrNull('9.9a')).toThrow(Error); + }); + + it(`should throw if trying to cast an undefined to an integer`, () => { + expect(() => castAsNumberOrNull(undefined)).toThrow(Error); + }); +}); diff --git a/packages/twenty-front/src/utils/array/sortByProperty.ts b/packages/twenty-front/src/utils/array/sortByProperty.ts new file mode 100644 index 000000000000..7cdb9b3b488b --- /dev/null +++ b/packages/twenty-front/src/utils/array/sortByProperty.ts @@ -0,0 +1,18 @@ +export const sortByProperty = + <T, K extends keyof T>(propertyName: K, sortBy: 'asc' | 'desc' = 'asc') => + (objectA: T, objectB: T) => { + const a = sortBy === 'asc' ? objectA : objectB; + const b = sortBy === 'asc' ? objectB : objectA; + + if (typeof a[propertyName] === 'string') { + return (a[propertyName] as string).localeCompare( + b[propertyName] as string, + ); + } else if (typeof a[propertyName] === 'number') { + return (a[propertyName] as number) - (b[propertyName] as number); + } else { + throw new Error( + 'Property type not supported in sortByProperty, only string and number are supported', + ); + } + }; diff --git a/packages/twenty-front/src/utils/cast-as-integer-or-null.ts b/packages/twenty-front/src/utils/cast-as-number-or-null.ts similarity index 82% rename from packages/twenty-front/src/utils/cast-as-integer-or-null.ts rename to packages/twenty-front/src/utils/cast-as-number-or-null.ts index 5cca0021dead..ef06e5b5a33e 100644 --- a/packages/twenty-front/src/utils/cast-as-integer-or-null.ts +++ b/packages/twenty-front/src/utils/cast-as-number-or-null.ts @@ -4,7 +4,7 @@ import { logError } from './logError'; const DEBUG_MODE = false; -export const canBeCastAsIntegerOrNull = ( +export const canBeCastAsNumberOrNull = ( probableNumberOrNull: string | undefined | number | null, ): probableNumberOrNull is number | null => { if (probableNumberOrNull === undefined) { @@ -16,7 +16,7 @@ export const canBeCastAsIntegerOrNull = ( if (isNumber(probableNumberOrNull)) { if (DEBUG_MODE) logError('typeof probableNumberOrNull === "number"'); - return Number.isInteger(probableNumberOrNull); + return true; } if (isNull(probableNumberOrNull)) { @@ -39,8 +39,8 @@ export const canBeCastAsIntegerOrNull = ( return false; } - if (Number.isInteger(stringAsNumber)) { - if (DEBUG_MODE) logError('Number.isInteger(stringAsNumber)'); + if (isNumber(stringAsNumber)) { + if (DEBUG_MODE) logError('isNumber(stringAsNumber)'); return true; } @@ -49,10 +49,10 @@ export const canBeCastAsIntegerOrNull = ( return false; }; -export const castAsIntegerOrNull = ( +export const castAsNumberOrNull = ( probableNumberOrNull: string | undefined | number | null, ): number | null => { - if (canBeCastAsIntegerOrNull(probableNumberOrNull) === false) { + if (canBeCastAsNumberOrNull(probableNumberOrNull) === false) { throw new Error('Cannot cast to number or null'); } diff --git a/packages/twenty-front/src/utils/format/__tests__/number.test.ts b/packages/twenty-front/src/utils/format/__tests__/number.test.ts index 37237e03dd53..8b2f6687f8f1 100644 --- a/packages/twenty-front/src/utils/format/__tests__/number.test.ts +++ b/packages/twenty-front/src/utils/format/__tests__/number.test.ts @@ -6,12 +6,15 @@ describe('formatNumber', () => { expect(formatNumber(123)).toEqual('123'); }); it(`Should format decimal numbers correctly`, () => { - expect(formatNumber(123.92)).toEqual('123.92'); + expect(formatNumber(123.92, 2)).toEqual('123.92'); }); it(`Should format large numbers correctly`, () => { expect(formatNumber(1234567)).toEqual('1,234,567'); }); it(`Should format large numbers with a decimal point correctly`, () => { - expect(formatNumber(7654321.89)).toEqual('7,654,321.89'); + expect(formatNumber(7654321.89, 2)).toEqual('7,654,321.89'); + }); + it('should format apply decimals correctly', () => { + expect(formatNumber(123.456, 2)).toEqual('123.46'); }); }); diff --git a/packages/twenty-front/src/utils/format/number.ts b/packages/twenty-front/src/utils/format/number.ts index 4937372d0cbd..a36cb6fffad8 100644 --- a/packages/twenty-front/src/utils/format/number.ts +++ b/packages/twenty-front/src/utils/format/number.ts @@ -1,2 +1,8 @@ -export const formatNumber = (value: number): string => - value.toLocaleString('en-US'); +export const DEFAULT_DECIMAL_VALUE = 0; + +export const formatNumber = (value: number, decimals?: number): string => { + return value.toLocaleString('en-US', { + minimumFractionDigits: decimals ?? DEFAULT_DECIMAL_VALUE, + maximumFractionDigits: decimals ?? DEFAULT_DECIMAL_VALUE, + }); +}; diff --git a/packages/twenty-server/jest.config.ts b/packages/twenty-server/jest.config.ts index 00c1b6f06fdb..f25c9e66dcca 100644 --- a/packages/twenty-server/jest.config.ts +++ b/packages/twenty-server/jest.config.ts @@ -7,7 +7,7 @@ const jestConfig: JestConfigWithTsJest = { displayName: 'twenty-server', rootDir: './', testEnvironment: 'node', - transformIgnorePatterns: ['../../node_modules/'], + transformIgnorePatterns: ['/node_modules/'], testRegex: '.*\\.spec\\.ts$', transform: { '^.+\\.(t|j)s$': 'ts-jest', diff --git a/packages/twenty-server/nest-cli.json b/packages/twenty-server/nest-cli.json index ce6375096831..992b41ad22ec 100644 --- a/packages/twenty-server/nest-cli.json +++ b/packages/twenty-server/nest-cli.json @@ -6,17 +6,21 @@ "builder": "swc", "typeCheck": true, "assets": [ + { + "include": "**/serverless/drivers/constants/base-typescript-project/**", + "outDir": "dist/assets" + }, { "include": "**/serverless/drivers/layers/*/package.json", - "outDir": "dist/src" + "outDir": "dist/assets" }, { "include": "**/serverless/drivers/layers/*/yarn.lock", - "outDir": "dist/src" + "outDir": "dist/assets" }, { "include": "**/serverless/drivers/layers/engine/**", - "outDir": "dist/src" + "outDir": "dist/assets" } ], "watchAssets": true diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index 1a60930c4a18..1f94d52eff93 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -1,6 +1,6 @@ { "name": "twenty-server", - "version": "0.24.2", + "version": "0.31.0", "description": "", "author": "", "private": true, diff --git a/packages/twenty-server/project.json b/packages/twenty-server/project.json index a31ff4fb101f..ed8ad716b6e0 100644 --- a/packages/twenty-server/project.json +++ b/packages/twenty-server/project.json @@ -77,6 +77,14 @@ "options": { "cwd": "packages/twenty-server", "command": "node dist/src/queue-worker/queue-worker.js" + }, + "configurations": { + "ci": { + "env": { + "MESSAGE_QUEUE_TYPE": "sync", + "CACHE_STORAGE_TYPE": "memory" + } + } } }, "typeorm": { diff --git a/packages/twenty-server/src/constants/assets-path.ts b/packages/twenty-server/src/constants/assets-path.ts new file mode 100644 index 000000000000..04766287433a --- /dev/null +++ b/packages/twenty-server/src/constants/assets-path.ts @@ -0,0 +1,8 @@ +import path from 'path'; + +// If the code is built through the testing module, assets are not output to the dist/assets directory. +const IS_BUILT_THROUGH_TESTING_MODULE = !__dirname.includes('/dist/'); + +export const ASSET_PATH = IS_BUILT_THROUGH_TESTING_MODULE + ? path.resolve(__dirname, `../`) + : path.resolve(__dirname, `../../assets`); diff --git a/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts b/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts index 50113a1c95b4..5f212a025a1d 100644 --- a/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts +++ b/packages/twenty-server/src/database/commands/data-seed-dev-workspace.command.ts @@ -40,6 +40,7 @@ import { DataSourceService } from 'src/engine/metadata-modules/data-source/data- import { FieldMetadataService } from 'src/engine/metadata-modules/field-metadata/field-metadata.service'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { ObjectMetadataService } from 'src/engine/metadata-modules/object-metadata/object-metadata.service'; +import { shouldSeedWorkspaceFavorite } from 'src/engine/utils/should-seed-workspace-favorite'; import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { viewPrefillData } from 'src/engine/workspace-manager/standard-objects-prefill-data/view'; import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; @@ -219,7 +220,14 @@ export class DataSeedWorkspaceCommand extends CommandRunner { await seedWorkspaceFavorites( viewDefinitionsWithId - .filter((view) => view.key === 'INDEX') + .filter( + (view) => + view.key === 'INDEX' && + shouldSeedWorkspaceFavorite( + view.objectMetadataId, + objectMetadataMap, + ), + ) .map((view) => view.id), entityManager, dataSourceMetadata.schema, diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-fix-view-filter-operand-for-date-time.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-fix-view-filter-operand-for-date-time.command.ts new file mode 100644 index 000000000000..f2e98d6600a7 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-fix-view-filter-operand-for-date-time.command.ts @@ -0,0 +1,110 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import { Command } from 'nest-commander'; +import { Any, Repository } from 'typeorm'; + +import { + ActiveWorkspacesCommandOptions, + ActiveWorkspacesCommandRunner, +} from 'src/database/commands/active-workspaces.command'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; +import { + FieldMetadataEntity, + FieldMetadataType, +} from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { ViewFilterWorkspaceEntity } from 'src/modules/view/standard-objects/view-filter.workspace-entity'; + +@Command({ + name: 'upgrade-0.30:fix-view-filter-operand-for-date-time', + description: 'Fix the view filter operand for date time fields', +}) +export class FixViewFilterOperandForDateTimeCommand extends ActiveWorkspacesCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository<Workspace>, + @InjectRepository(FieldMetadataEntity, 'metadata') + private readonly fieldMetadataRepository: Repository<FieldMetadataEntity>, + private readonly dataSourceService: DataSourceService, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) { + super(workspaceRepository); + } + + async executeActiveWorkspacesCommand( + _passedParam: string[], + _options: ActiveWorkspacesCommandOptions, + workspaceIds: string[], + ): Promise<void> { + for (const workspaceId of workspaceIds) { + try { + const dataSourceMetadata = + await this.dataSourceService.getLastDataSourceMetadataFromWorkspaceId( + workspaceId, + ); + + if (!dataSourceMetadata) { + throw new Error( + `Could not find dataSourceMetadata for workspace ${workspaceId}`, + ); + } + + const viewFilterRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace<ViewFilterWorkspaceEntity>( + workspaceId, + 'viewFilter', + ); + + const dateTimeFieldMetadata = await this.fieldMetadataRepository.find({ + where: { + workspaceId, + type: FieldMetadataType.DATE_TIME, + }, + }); + + const dateTimeFieldMetadataIds = dateTimeFieldMetadata.map( + (fieldMetadata) => fieldMetadata.id, + ); + + const lessThanUpdatedResult = await viewFilterRepository.update( + { + operand: 'lessThan', + fieldMetadataId: Any(dateTimeFieldMetadataIds), + }, + { + operand: 'isBefore', + }, + ); + + const greaterThanUpdatedResult = await viewFilterRepository.update( + { + operand: 'greaterThan', + fieldMetadataId: Any(dateTimeFieldMetadataIds), + }, + { + operand: 'isAfter', + }, + ); + + this.logger.log( + `Updated ${(lessThanUpdatedResult.affected ?? 0) + (greaterThanUpdatedResult.affected ?? 0)} view filters for workspace ${workspaceId}`, + ); + } catch (error) { + this.logger.log( + chalk.red( + `Error running command for workspace ${workspaceId}: ${error}`, + ), + ); + continue; + } finally { + this.logger.log( + chalk.green(`Finished running command for workspace ${workspaceId}.`), + ); + } + + this.logger.log(chalk.green(`Command completed!`)); + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command.ts index 87ff3e5b0f64..2e4969297a6b 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command.ts @@ -305,14 +305,6 @@ export class MigrateEmailFieldsToEmailsCommand extends ActiveWorkspacesCommandRu ) { this.logger.log(`Migrating person email field of type EMAIL to EMAILS`); - await this.migrateDataWithinTable({ - sourceColumnName: 'email', - targetColumnName: 'emailsPrimaryEmail', - tableName: 'person', - workspaceQueryRunner, - dataSourceMetadata, - }); - const personEmailFieldMetadata = await this.fieldMetadataRepository.findOne( { where: { @@ -322,6 +314,22 @@ export class MigrateEmailFieldsToEmailsCommand extends ActiveWorkspacesCommandRu }, ); + if (!personEmailFieldMetadata) { + this.logger.log( + `Could not find person email field with standardId ${PERSON_STANDARD_FIELD_IDS.email}, skipping migration`, + ); + + return; + } + + await this.migrateDataWithinTable({ + sourceColumnName: 'email', + targetColumnName: 'emailsPrimaryEmail', + tableName: 'person', + workspaceQueryRunner, + dataSourceMetadata, + }); + if (personEmailFieldMetadata) { await this.fieldMetadataService.deleteOneField( { diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-phone-fields-to-phones.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-phone-fields-to-phones.command.ts index 60aacf2605c3..6560bb9625dc 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-phone-fields-to-phones.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-migrate-phone-fields-to-phones.command.ts @@ -168,6 +168,16 @@ export class MigratePhoneFieldsToPhonesCommand extends ActiveWorkspacesCommandRu name: 'phones', } satisfies CreateFieldInput); + // StandardId and isCustom are not exposed in CreateFieldInput + await this.metadataDataSource.query( + `UPDATE "metadata"."fieldMetadata" SET "standardId" = $1, "isCustom" = $2 where "id"=$3`, + [ + PERSON_STANDARD_FIELD_IDS.phones, + 'false', + standardPersonPhonesField.id, + ], + ); + await this.viewService.removeFieldFromViews({ workspaceId: workspaceId, fieldId: standardPersonPhonesField.id, diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.command.ts index 144746f82933..25ff077ca5e1 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.command.ts @@ -5,6 +5,7 @@ import { Repository } from 'typeorm'; import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-workspaces.command'; import { FixEmailFieldsToEmailsCommand } from 'src/database/commands/upgrade-version/0-30/0-30-fix-email-field-migration.command'; +import { FixViewFilterOperandForDateTimeCommand } from 'src/database/commands/upgrade-version/0-30/0-30-fix-view-filter-operand-for-date-time.command'; import { MigrateEmailFieldsToEmailsCommand } from 'src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command'; import { MigratePhoneFieldsToPhonesCommand } from 'src/database/commands/upgrade-version/0-30/0-30-migrate-phone-fields-to-phones.command'; import { SetStaleMessageSyncBackToPendingCommand } from 'src/database/commands/upgrade-version/0-30/0-30-set-stale-message-sync-back-to-pending'; @@ -28,6 +29,7 @@ export class UpgradeTo0_30Command extends ActiveWorkspacesCommandRunner { private readonly setStaleMessageSyncBackToPendingCommand: SetStaleMessageSyncBackToPendingCommand, private readonly fixEmailFieldsToEmailsCommand: FixEmailFieldsToEmailsCommand, private readonly migratePhoneFieldsToPhones: MigratePhoneFieldsToPhonesCommand, + private readonly fixViewFilterOperandForDateTimeCommand: FixViewFilterOperandForDateTimeCommand, ) { super(workspaceRepository); } @@ -65,5 +67,10 @@ export class UpgradeTo0_30Command extends ActiveWorkspacesCommandRunner { options, workspaceIds, ); + await this.fixViewFilterOperandForDateTimeCommand.executeActiveWorkspacesCommand( + passedParam, + options, + workspaceIds, + ); } } diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module.ts index 3a7c51c8df6f..191bad9352cb 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-30/0-30-upgrade-version.module.ts @@ -2,6 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { FixEmailFieldsToEmailsCommand } from 'src/database/commands/upgrade-version/0-30/0-30-fix-email-field-migration.command'; +import { FixViewFilterOperandForDateTimeCommand } from 'src/database/commands/upgrade-version/0-30/0-30-fix-view-filter-operand-for-date-time.command'; import { MigrateEmailFieldsToEmailsCommand } from 'src/database/commands/upgrade-version/0-30/0-30-migrate-email-fields-to-emails.command'; import { MigratePhoneFieldsToPhonesCommand } from 'src/database/commands/upgrade-version/0-30/0-30-migrate-phone-fields-to-phones.command'; import { SetStaleMessageSyncBackToPendingCommand } from 'src/database/commands/upgrade-version/0-30/0-30-set-stale-message-sync-back-to-pending'; @@ -36,6 +37,7 @@ import { ViewModule } from 'src/modules/view/view.module'; SetStaleMessageSyncBackToPendingCommand, FixEmailFieldsToEmailsCommand, MigratePhoneFieldsToPhonesCommand, + FixViewFilterOperandForDateTimeCommand, ], }) export class UpgradeTo0_30CommandModule {} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-delete-name-column-standard-object-tables.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-delete-name-column-standard-object-tables.command.ts new file mode 100644 index 000000000000..91576604e642 --- /dev/null +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-delete-name-column-standard-object-tables.command.ts @@ -0,0 +1,121 @@ +import { InjectRepository } from '@nestjs/typeorm'; + +import chalk from 'chalk'; +import { Command } from 'nest-commander'; +import { Repository } from 'typeorm'; + +import { + ActiveWorkspacesCommandOptions, + ActiveWorkspacesCommandRunner, +} from 'src/database/commands/active-workspaces.command'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; + +@Command({ + name: 'upgrade-0.31:delete-name-column-standard-object-tables', + description: 'Delete name column from standard object tables', +}) +export class DeleteNameColumnStandardObjectTablesCommand extends ActiveWorkspacesCommandRunner { + constructor( + @InjectRepository(Workspace, 'core') + protected readonly workspaceRepository: Repository<Workspace>, + @InjectRepository(ObjectMetadataEntity, 'metadata') + private readonly objectMetadataRepository: Repository<ObjectMetadataEntity>, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) { + super(workspaceRepository); + } + + async executeActiveWorkspacesCommand( + _passedParam: string[], + options: ActiveWorkspacesCommandOptions, + workspaceIds: string[], + ): Promise<void> { + this.logger.log('Running command to fix migration'); + + for (const workspaceId of workspaceIds) { + this.logger.log(`Running command for workspace ${workspaceId}`); + + try { + this.logger.log( + chalk.green(`Deleting name columns from workspace ${workspaceId}.`), + ); + + const standardObjects = await this.objectMetadataRepository.find({ + where: { + isCustom: false, + workspaceId, + }, + relations: ['fields'], + }); + + const dataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace( + workspaceId, + ); + + dataSource.transaction(async (entityManager) => { + const queryRunner = entityManager.queryRunner; + + for (const standardObject of standardObjects) { + if (options.dryRun) { + this.logger.log( + chalk.yellow( + `Dry run mode enabled. Skipping deletion of name column for workspace ${workspaceId} and table ${standardObject.nameSingular}.`, + ), + ); + continue; + } + + const nameColumnExists = await queryRunner?.hasColumn( + standardObject.nameSingular, + 'name', + ); + + const nameFieldMetadataExists = standardObject.fields.some( + (field) => + field.name === 'name' && field.type === FieldMetadataType.TEXT, + ); + + if (nameFieldMetadataExists) { + this.logger.log( + chalk.yellow( + `Name field exists for workspace ${workspaceId} and table ${standardObject.nameSingular}. Skipping deletion.`, + ), + ); + continue; + } + + if (!nameColumnExists) { + this.logger.log( + chalk.yellow( + `Name column does not exist for workspace ${workspaceId} and table ${standardObject.nameSingular}. Skipping deletion.`, + ), + ); + continue; + } + + await queryRunner?.dropColumn(standardObject.nameSingular, 'name'); + } + }); + } catch (error) { + this.logger.log( + chalk.red( + `Running command on workspace ${workspaceId} failed with error: ${error}`, + ), + ); + continue; + } finally { + this.logger.log( + chalk.green(`Finished running command for workspace ${workspaceId}.`), + ); + + await this.twentyORMGlobalManager.destroyDataSourceForWorkspace( + workspaceId, + ); + } + } + } +} diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-upgrade-version.command.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-upgrade-version.command.ts index 1ab48a059db3..247e8cd4d20d 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-upgrade-version.command.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-upgrade-version.command.ts @@ -7,6 +7,7 @@ import { ActiveWorkspacesCommandRunner } from 'src/database/commands/active-work import { AddIndexKeyToTasksAndNotesViewsCommand } from 'src/database/commands/upgrade-version/0-31/0-31-add-index-key-to-tasks-and-notes-views.command'; import { BackfillWorkspaceFavoritesCommand } from 'src/database/commands/upgrade-version/0-31/0-31-backfill-workspace-favorites.command'; import { CleanViewsAssociatedWithOutdatedObjectsCommand } from 'src/database/commands/upgrade-version/0-31/0-31-clean-views-associated-with-outdated-objects.command'; +import { DeleteNameColumnStandardObjectTablesCommand } from 'src/database/commands/upgrade-version/0-31/0-31-delete-name-column-standard-object-tables.command'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { SyncWorkspaceMetadataCommand } from 'src/engine/workspace-manager/workspace-sync-metadata/commands/sync-workspace-metadata.command'; @@ -26,6 +27,7 @@ export class UpgradeTo0_31Command extends ActiveWorkspacesCommandRunner { private readonly backfillWorkspaceFavoritesCommand: BackfillWorkspaceFavoritesCommand, private readonly cleanViewsAssociatedWithOutdatedObjectsCommand: CleanViewsAssociatedWithOutdatedObjectsCommand, private readonly addIndexKeyToTasksAndNotesViewsCommand: AddIndexKeyToTasksAndNotesViewsCommand, + private readonly deleteNameColumnStandardObjectTablesCommand: DeleteNameColumnStandardObjectTablesCommand, ) { super(workspaceRepository); } @@ -58,5 +60,10 @@ export class UpgradeTo0_31Command extends ActiveWorkspacesCommandRunner { options, workspaceIds, ); + await this.deleteNameColumnStandardObjectTablesCommand.executeActiveWorkspacesCommand( + passedParam, + options, + workspaceIds, + ); } } diff --git a/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-upgrade-version.module.ts b/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-upgrade-version.module.ts index d53dc237e96a..e1f731cb5ba4 100644 --- a/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-upgrade-version.module.ts +++ b/packages/twenty-server/src/database/commands/upgrade-version/0-31/0-31-upgrade-version.module.ts @@ -4,6 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { AddIndexKeyToTasksAndNotesViewsCommand } from 'src/database/commands/upgrade-version/0-31/0-31-add-index-key-to-tasks-and-notes-views.command'; import { BackfillWorkspaceFavoritesCommand } from 'src/database/commands/upgrade-version/0-31/0-31-backfill-workspace-favorites.command'; import { CleanViewsAssociatedWithOutdatedObjectsCommand } from 'src/database/commands/upgrade-version/0-31/0-31-clean-views-associated-with-outdated-objects.command'; +import { DeleteNameColumnStandardObjectTablesCommand } from 'src/database/commands/upgrade-version/0-31/0-31-delete-name-column-standard-object-tables.command'; import { UpgradeTo0_31Command } from 'src/database/commands/upgrade-version/0-31/0-31-upgrade-version.command'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; @@ -20,6 +21,7 @@ import { WorkspaceSyncMetadataCommandsModule } from 'src/engine/workspace-manage BackfillWorkspaceFavoritesCommand, CleanViewsAssociatedWithOutdatedObjectsCommand, AddIndexKeyToTasksAndNotesViewsCommand, + DeleteNameColumnStandardObjectTablesCommand, ], }) export class UpgradeTo0_31CommandModule {} 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 8d3036235454..013f226635f2 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,26 @@ export const seedFeatureFlags = async ( workspaceId: workspaceId, value: true, }, + { + key: FeatureFlagKey.IsSearchEnabled, + workspaceId: workspaceId, + value: true, + }, + { + key: FeatureFlagKey.IsWorkspaceMigratedForSearch, + workspaceId: workspaceId, + value: true, + }, + { + key: FeatureFlagKey.IsAnalyticsV2Enabled, + workspaceId: workspaceId, + value: true, + }, + { + key: FeatureFlagKey.IsGmailSendEmailScopeEnabled, + workspaceId: workspaceId, + value: true, + }, ]) .execute(); }; diff --git a/packages/twenty-server/src/database/typeorm-seeds/metadata/fieldsMetadata.ts b/packages/twenty-server/src/database/typeorm-seeds/metadata/fieldsMetadata.ts index 330975f00d02..5f0f8219c8f3 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/metadata/fieldsMetadata.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/metadata/fieldsMetadata.ts @@ -15,6 +15,7 @@ export const getDevSeedCompanyCustomFields = ( icon: 'IconAdCircle', isActive: true, isNullable: false, + defaultValue: "''", objectMetadataId, }, { @@ -99,12 +100,19 @@ export const getDevSeedPeopleCustomFields = ( icon: 'IconBrandWhatsapp', isActive: true, isNullable: false, + defaultValue: [ + { + primaryPhoneNumber: '', + primaryPhoneCountryCode: '', + additionalPhones: {}, + }, + ], objectMetadataId, }, { workspaceId, type: FieldMetadataType.MULTI_SELECT, - name: 'workPrefereance', + name: 'workPreference', label: 'Work Preference', description: "Person's Work Preference", icon: 'IconHome', diff --git a/packages/twenty-server/src/database/typeorm-seeds/workspace/calendar-channel-event-association.ts b/packages/twenty-server/src/database/typeorm-seeds/workspace/calendar-channel-event-association.ts index 59530a45edd3..7e20a6ab5a33 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/workspace/calendar-channel-event-association.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/workspace/calendar-channel-event-association.ts @@ -14,6 +14,7 @@ export const seedCalendarChannelEventAssociations = async ( 'calendarChannelId', 'calendarEventId', 'eventExternalId', + 'recurringEventExternalId', ]) .orIgnore() .values([ @@ -22,6 +23,7 @@ export const seedCalendarChannelEventAssociations = async ( calendarChannelId: '59efdefe-a40f-4faf-bb9f-c6f9945b8203', calendarEventId: '86083141-1c0e-494c-a1b6-85b1c6fefaa5', eventExternalId: 'exampleExternalId', + recurringEventExternalId: 'exampleRecurringExternalId', }, ]) .execute(); diff --git a/packages/twenty-server/src/database/typeorm-seeds/workspace/calendar-events.ts b/packages/twenty-server/src/database/typeorm-seeds/workspace/calendar-events.ts index 7624c290cb7d..6969213f7689 100644 --- a/packages/twenty-server/src/database/typeorm-seeds/workspace/calendar-events.ts +++ b/packages/twenty-server/src/database/typeorm-seeds/workspace/calendar-events.ts @@ -24,7 +24,6 @@ export const seedCalendarEvents = async ( 'conferenceSolution', 'conferenceLinkPrimaryLinkLabel', 'conferenceLinkPrimaryLinkUrl', - 'recurringEventExternalId', ]) .orIgnore() .values([ @@ -43,7 +42,6 @@ export const seedCalendarEvents = async ( conferenceSolution: 'Zoom', conferenceLinkPrimaryLinkLabel: 'https://zoom.us/j/1234567890', conferenceLinkPrimaryLinkUrl: 'https://zoom.us/j/1234567890', - recurringEventExternalId: 'recurring1', }, ]) .execute(); diff --git a/packages/twenty-server/src/database/typeorm/core/core.datasource.ts b/packages/twenty-server/src/database/typeorm/core/core.datasource.ts index 0395f8cf2ae1..1f12bb4cd12f 100644 --- a/packages/twenty-server/src/database/typeorm/core/core.datasource.ts +++ b/packages/twenty-server/src/database/typeorm/core/core.datasource.ts @@ -17,6 +17,7 @@ export const typeORMCoreModuleOptions: TypeOrmModuleOptions = { synchronize: false, migrationsRun: false, migrationsTableName: '_typeorm_migrations', + metadataTableName: '_typeorm_generated_columns_and_materialized_views', migrations: [ `${isJest ? '' : 'dist/'}src/database/typeorm/core/migrations/*{.ts,.js}`, ], diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1728314605995-add_typeormGeneratedColumnsAndMaterializedViews.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1728314605995-add_typeormGeneratedColumnsAndMaterializedViews.ts new file mode 100644 index 000000000000..28c56236e658 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/core/migrations/1728314605995-add_typeormGeneratedColumnsAndMaterializedViews.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddTypeormGeneratedColumns1728314605995 + implements MigrationInterface +{ + name = 'AddTypeormGeneratedColumnsAndMaterializedViews1728314605995'; + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + CREATE TABLE "core"."_typeorm_generated_columns_and_materialized_views" ( + "type" character varying NOT NULL, + "database" character varying, + "schema" character varying, + "table" character varying, + "name" character varying, + "value" text + ) + `); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query( + `DROP TABLE "core"."_typeorm_generated_columns_and_materialized_views"`, + ); + } +} diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1725893697807-addIndexType.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1725893697807-addIndexType.ts new file mode 100644 index 000000000000..59a1828627ea --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/metadata/migrations/1725893697807-addIndexType.ts @@ -0,0 +1,26 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddIndexType1725893697807 implements MigrationInterface { + name = 'AddIndexType1725893697807'; + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query( + `CREATE TYPE "metadata"."indexMetadata_indextype_enum" AS ENUM('BTREE', 'GIN')`, + ); + + await queryRunner.query(` + ALTER TABLE metadata."indexMetadata" + ADD COLUMN "indexType" metadata."indexMetadata_indextype_enum" NOT NULL DEFAULT 'BTREE'; + `); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE metadata."indexMetadata" DROP COLUMN "indexType" + `); + + await queryRunner.query( + `DROP TYPE metadata."indexMetadata_indextype_enum"`, + ); + } +} diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1726240847733-removeServerlessSourceCodeHashColumn.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1726240847733-removeServerlessSourceCodeHashColumn.ts new file mode 100644 index 000000000000..277c739a3bcb --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/metadata/migrations/1726240847733-removeServerlessSourceCodeHashColumn.ts @@ -0,0 +1,19 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class RemoveServerlessSourceCodeHashColumn1726240847733 + implements MigrationInterface +{ + name = 'RemoveServerlessSourceCodeHashColumn1726240847733'; + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query( + `ALTER TABLE "metadata"."serverlessFunction" DROP COLUMN "sourceCodeHash"`, + ); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query( + `ALTER TABLE "metadata"."serverlessFunction" ADD "sourceCodeHash" character varying NOT NULL`, + ); + } +} diff --git a/packages/twenty-server/src/database/typeorm/metadata/migrations/1727699709905-addIsCustomColumnToIndexMetadata.ts b/packages/twenty-server/src/database/typeorm/metadata/migrations/1727699709905-addIsCustomColumnToIndexMetadata.ts new file mode 100644 index 000000000000..e40465ddad72 --- /dev/null +++ b/packages/twenty-server/src/database/typeorm/metadata/migrations/1727699709905-addIsCustomColumnToIndexMetadata.ts @@ -0,0 +1,23 @@ +import { MigrationInterface, QueryRunner } from 'typeorm'; + +export class AddIsCustomColumnToIndexMetadata1727699709905 + implements MigrationInterface +{ + name = 'AddIsCustomColumnToIndexMetadata1727699709905'; + + public async up(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE "metadata"."indexMetadata" + ADD COLUMN "isCustom" BOOLEAN + NOT NULL + DEFAULT FALSE; + `); + } + + public async down(queryRunner: QueryRunner): Promise<void> { + await queryRunner.query(` + ALTER TABLE "metadata"."indexMetadata" + DROP COLUMN "isCustom" + `); + } +} diff --git a/packages/twenty-server/src/database/typeorm/typeorm.service.ts b/packages/twenty-server/src/database/typeorm/typeorm.service.ts index 0e8b97094b58..46b546d65ee5 100644 --- a/packages/twenty-server/src/database/typeorm/typeorm.service.ts +++ b/packages/twenty-server/src/database/typeorm/typeorm.service.ts @@ -2,17 +2,17 @@ import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common'; import { DataSource } from 'typeorm'; -import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; -import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity'; import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity'; 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 { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity'; @Injectable() export class TypeORMService implements OnModuleInit, OnModuleDestroy { @@ -37,6 +37,7 @@ export class TypeORMService implements OnModuleInit, OnModuleDestroy { BillingSubscriptionItem, PostgresCredentials, ], + metadataTableName: '_typeorm_generated_columns_and_materialized_views', ssl: environmentService.get('PG_SSL_ALLOW_SELF_SIGNED') ? { rejectUnauthorized: false, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/factories/graphql-query-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/factories/graphql-query-resolver.factory.ts new file mode 100644 index 000000000000..e3ada8ac7cea --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/factories/graphql-query-resolver.factory.ts @@ -0,0 +1,54 @@ +import { Injectable } from '@nestjs/common'; +import { ModuleRef } from '@nestjs/core'; + +import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; +import { + ResolverArgs, + WorkspaceResolverBuilderMethodNames, +} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; + +import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service'; +import { GraphqlQueryDestroyManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service'; +import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service'; +import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service'; +import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service'; +import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service'; +import { GraphqlQuerySearchResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service'; +import { GraphqlQueryUpdateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service'; +import { GraphqlQueryUpdateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service'; + +@Injectable() +export class GraphqlQueryResolverFactory { + constructor(private moduleRef: ModuleRef) {} + + public getResolver( + operationName: WorkspaceResolverBuilderMethodNames, + ): ResolverService<ResolverArgs, any> { + switch (operationName) { + case 'findOne': + return this.moduleRef.get(GraphqlQueryFindOneResolverService); + case 'findMany': + return this.moduleRef.get(GraphqlQueryFindManyResolverService); + case 'findDuplicates': + return this.moduleRef.get(GraphqlQueryFindDuplicatesResolverService); + case 'search': + return this.moduleRef.get(GraphqlQuerySearchResolverService); + case 'createOne': + case 'createMany': + return this.moduleRef.get(GraphqlQueryCreateManyResolverService); + case 'destroyOne': + return this.moduleRef.get(GraphqlQueryDestroyOneResolverService); + case 'destroyMany': + return this.moduleRef.get(GraphqlQueryDestroyManyResolverService); + case 'updateOne': + case 'deleteOne': + return this.moduleRef.get(GraphqlQueryUpdateOneResolverService); + case 'updateMany': + case 'deleteMany': + case 'restoreMany': + return this.moduleRef.get(GraphqlQueryUpdateManyResolverService); + default: + throw new Error(`Unsupported operation: ${operationName}`); + } + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts index 093364db8479..21f9bdbdccb0 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser.ts @@ -25,7 +25,7 @@ export class GraphqlQueryFilterConditionParser { public parse( queryBuilder: SelectQueryBuilder<any>, objectNameSingular: string, - filter: RecordFilter, + filter: Partial<RecordFilter>, ): SelectQueryBuilder<any> { if (!filter || Object.keys(filter).length === 0) { return queryBuilder; diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts index fe37c4d445da..920fa01c56d6 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-field.parser.ts @@ -58,7 +58,6 @@ export class GraphqlQueryFilterFieldParser { } const { sql, params } = this.computeWhereConditionParts( - fieldMetadata, operator, objectNameSingular, key, @@ -73,7 +72,6 @@ export class GraphqlQueryFilterFieldParser { } private computeWhereConditionParts( - fieldMetadata: FieldMetadataInterface, operator: string, objectNameSingular: string, key: string, @@ -185,7 +183,6 @@ export class GraphqlQueryFilterFieldParser { ); const { sql, params } = this.computeWhereConditionParts( - fieldMetadata, operator, objectNameSingular, fullFieldName, diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts index 157187fd0bd2..0aa047fc31f7 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query.parser.ts @@ -9,7 +9,6 @@ import { RecordFilter, RecordOrderBy, } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { GraphqlQueryFilterConditionParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-filter/graphql-query-filter-condition.parser'; import { GraphqlQueryOrderFieldParser } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-parsers/graphql-query-order/graphql-query-order.parser'; @@ -17,6 +16,7 @@ import { GraphqlQuerySelectedFieldsParser } from 'src/engine/api/graphql/graphql import { FieldMetadataMap, ObjectMetadataMap, + ObjectMetadataMapItem, } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; export class GraphqlQueryParser { @@ -39,10 +39,10 @@ export class GraphqlQueryParser { ); } - applyFilterToBuilder( + public applyFilterToBuilder( queryBuilder: SelectQueryBuilder<any>, objectNameSingular: string, - recordFilter: RecordFilter, + recordFilter: Partial<RecordFilter>, ): SelectQueryBuilder<any> { return this.filterConditionParser.parse( queryBuilder, @@ -51,7 +51,7 @@ export class GraphqlQueryParser { ); } - applyDeletedAtToBuilder( + public applyDeletedAtToBuilder( queryBuilder: SelectQueryBuilder<any>, recordFilter: RecordFilter, ): SelectQueryBuilder<any> { @@ -62,9 +62,9 @@ export class GraphqlQueryParser { return queryBuilder; } - private checkForDeletedAtFilter( + private checkForDeletedAtFilter = ( filter: FindOptionsWhere<ObjectLiteral> | FindOptionsWhere<ObjectLiteral>[], - ): boolean { + ): boolean => { if (Array.isArray(filter)) { return filter.some((subFilter) => this.checkForDeletedAtFilter(subFilter), @@ -86,9 +86,9 @@ export class GraphqlQueryParser { } return false; - } + }; - applyOrderToBuilder( + public applyOrderToBuilder( queryBuilder: SelectQueryBuilder<any>, orderBy: RecordOrderBy, objectNameSingular: string, @@ -103,8 +103,8 @@ export class GraphqlQueryParser { return queryBuilder.orderBy(parsedOrderBys as OrderByCondition); } - parseSelectedFields( - parentObjectMetadata: ObjectMetadataInterface, + public parseSelectedFields( + parentObjectMetadata: ObjectMetadataMapItem, graphqlSelectedFields: Partial<Record<string, any>>, ): { select: Record<string, any>; relations: Record<string, any> } { const parentFields = diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts index 96f15862e392..642e11c81b1f 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/graphql-query-runner.module.ts @@ -1,12 +1,45 @@ import { Module } from '@nestjs/common'; +import { GraphqlQueryResolverFactory } from 'src/engine/api/graphql/graphql-query-runner/factories/graphql-query-resolver.factory'; import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service'; +import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service'; +import { GraphqlQueryDestroyManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service'; +import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service'; +import { GraphqlQueryFindDuplicatesResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service'; +import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service'; +import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service'; +import { GraphqlQuerySearchResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service'; +import { GraphqlQueryUpdateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service'; +import { GraphqlQueryUpdateOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service'; +import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service'; import { WorkspaceQueryHookModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.module'; import { WorkspaceQueryRunnerModule } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.module'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; + +const graphqlQueryResolvers = [ + GraphqlQueryCreateManyResolverService, + GraphqlQueryDestroyManyResolverService, + GraphqlQueryDestroyOneResolverService, + GraphqlQueryFindDuplicatesResolverService, + GraphqlQueryFindManyResolverService, + GraphqlQueryFindOneResolverService, + GraphqlQuerySearchResolverService, + GraphqlQueryUpdateManyResolverService, + GraphqlQueryUpdateOneResolverService, +]; @Module({ - imports: [WorkspaceQueryHookModule, WorkspaceQueryRunnerModule], - providers: [GraphqlQueryRunnerService], + imports: [ + WorkspaceQueryHookModule, + WorkspaceQueryRunnerModule, + FeatureFlagModule, + ], + providers: [ + GraphqlQueryRunnerService, + GraphqlQueryResolverFactory, + ApiEventEmitterService, + ...graphqlQueryResolvers, + ], exports: [GraphqlQueryRunnerService], }) export class GraphqlQueryRunnerModule {} 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 cd1337b458c7..d3d47daed171 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 @@ -6,266 +6,397 @@ import { RecordOrderBy, } 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 { IEdge } from 'src/engine/api/graphql/workspace-query-runner/interfaces/edge.interface'; import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { CreateManyResolverArgs, CreateOneResolverArgs, + DeleteManyResolverArgs, + DeleteOneResolverArgs, + DestroyManyResolverArgs, DestroyOneResolverArgs, + FindDuplicatesResolverArgs, FindManyResolverArgs, FindOneResolverArgs, + ResolverArgs, ResolverArgsType, + RestoreManyResolverArgs, + SearchResolverArgs, + UpdateManyResolverArgs, + UpdateOneResolverArgs, + WorkspaceResolverBuilderMethodNames, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; -import { GraphqlQueryCreateManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service'; -import { GraphqlQueryDestroyOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service'; -import { GraphqlQueryFindManyResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service'; -import { GraphqlQueryFindOneResolverService } from 'src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service'; +import { GraphqlQueryResolverFactory } from 'src/engine/api/graphql/graphql-query-runner/factories/graphql-query-resolver.factory'; +import { ApiEventEmitterService } from 'src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service'; import { QueryRunnerArgsFactory } from 'src/engine/api/graphql/workspace-query-runner/factories/query-runner-args.factory'; import { CallWebhookJobsJob, CallWebhookJobsJobData, CallWebhookJobsJobOperation, } from 'src/engine/api/graphql/workspace-query-runner/jobs/call-webhook-jobs.job'; -import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; import { WorkspaceQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-hook/workspace-query-hook.service'; -import { - WorkspaceQueryRunnerException, - WorkspaceQueryRunnerExceptionCode, -} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; -import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; -import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { LogExecutionTime } from 'src/engine/decorators/observability/log-execution-time.decorator'; -import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; -import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; +import { capitalize } from 'src/utils/capitalize'; @Injectable() export class GraphqlQueryRunnerService { constructor( - private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly workspaceQueryHookService: WorkspaceQueryHookService, private readonly queryRunnerArgsFactory: QueryRunnerArgsFactory, - private readonly workspaceEventEmitter: WorkspaceEventEmitter, @InjectMessageQueue(MessageQueue.webhookQueue) private readonly messageQueueService: MessageQueueService, + private readonly graphqlQueryResolverFactory: GraphqlQueryResolverFactory, + private readonly apiEventEmitterService: ApiEventEmitterService, ) {} + /** QUERIES */ + @LogExecutionTime() - async findOne< - ObjectRecord extends IRecord = IRecord, - Filter extends RecordFilter = RecordFilter, - >( + async findOne<ObjectRecord extends IRecord, Filter extends RecordFilter>( args: FindOneResolverArgs<Filter>, options: WorkspaceQueryRunnerOptions, - ): Promise<ObjectRecord | undefined> { - const graphqlQueryFindOneResolverService = - new GraphqlQueryFindOneResolverService(this.twentyORMGlobalManager); - - const { authContext, objectMetadataItem } = options; - - if (!args.filter || Object.keys(args.filter).length === 0) { - throw new WorkspaceQueryRunnerException( - 'Missing filter argument', - WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT, - ); - } - - const hookedArgs = - await this.workspaceQueryHookService.executePreQueryHooks( - authContext, - objectMetadataItem.nameSingular, - 'findOne', - args, - ); - - const computedArgs = (await this.queryRunnerArgsFactory.create( - hookedArgs, + ): Promise<ObjectRecord> { + return this.executeQuery<FindOneResolverArgs<Filter>, ObjectRecord>( + 'findOne', + args, options, - ResolverArgsType.FindOne, - )) as FindOneResolverArgs<Filter>; - - return graphqlQueryFindOneResolverService.findOne(computedArgs, options); + ); } @LogExecutionTime() async findMany< - ObjectRecord extends IRecord = IRecord, - Filter extends RecordFilter = RecordFilter, - OrderBy extends RecordOrderBy = RecordOrderBy, + ObjectRecord extends IRecord, + Filter extends RecordFilter, + OrderBy extends RecordOrderBy, >( args: FindManyResolverArgs<Filter, OrderBy>, options: WorkspaceQueryRunnerOptions, + ): Promise<IConnection<ObjectRecord, IEdge<ObjectRecord>>> { + return this.executeQuery< + FindManyResolverArgs<Filter, OrderBy>, + IConnection<ObjectRecord, IEdge<ObjectRecord>> + >('findMany', args, options); + } + + @LogExecutionTime() + async findDuplicates<ObjectRecord extends IRecord>( + args: FindDuplicatesResolverArgs<Partial<ObjectRecord>>, + options: WorkspaceQueryRunnerOptions, + ): Promise<IConnection<ObjectRecord>[]> { + return this.executeQuery< + FindDuplicatesResolverArgs<Partial<ObjectRecord>>, + IConnection<ObjectRecord>[] + >('findDuplicates', args, options); + } + + @LogExecutionTime() + async search<ObjectRecord extends IRecord = IRecord>( + args: SearchResolverArgs, + options: WorkspaceQueryRunnerOptions, ): Promise<IConnection<ObjectRecord>> { - const graphqlQueryFindManyResolverService = - new GraphqlQueryFindManyResolverService(this.twentyORMGlobalManager); + return this.executeQuery<SearchResolverArgs, IConnection<ObjectRecord>>( + 'search', + args, + options, + ); + } - const { authContext, objectMetadataItem } = options; + /** MUTATIONS */ - const hookedArgs = - await this.workspaceQueryHookService.executePreQueryHooks( - authContext, - objectMetadataItem.nameSingular, - 'findMany', - args, + @LogExecutionTime() + async createOne<ObjectRecord extends IRecord>( + args: CreateOneResolverArgs<Partial<ObjectRecord>>, + options: WorkspaceQueryRunnerOptions, + ): Promise<ObjectRecord> { + const results = await this.executeQuery< + CreateManyResolverArgs<Partial<ObjectRecord>>, + ObjectRecord[] + >('createMany', { data: [args.data], upsert: args.upsert }, options); + + // TODO: emitCreateEvents should be moved to the ORM layer + if (results) { + this.apiEventEmitterService.emitCreateEvents( + results, + options.authContext, + options.objectMetadataItem, ); + } - const computedArgs = (await this.queryRunnerArgsFactory.create( - hookedArgs, - options, - ResolverArgsType.FindMany, - )) as FindManyResolverArgs<Filter, OrderBy>; + return results[0]; + } - return graphqlQueryFindManyResolverService.findMany(computedArgs, options); + @LogExecutionTime() + async createMany<ObjectRecord extends IRecord>( + args: CreateManyResolverArgs<Partial<ObjectRecord>>, + options: WorkspaceQueryRunnerOptions, + ): Promise<ObjectRecord[]> { + const results = await this.executeQuery< + CreateManyResolverArgs<Partial<ObjectRecord>>, + ObjectRecord[] + >('createMany', args, options); + + if (results) { + this.apiEventEmitterService.emitCreateEvents( + results, + options.authContext, + options.objectMetadataItem, + ); + } + + return results; } @LogExecutionTime() - async createOne<ObjectRecord extends IRecord = IRecord>( - args: CreateOneResolverArgs<Partial<ObjectRecord>>, + public async updateOne<ObjectRecord extends IRecord>( + args: UpdateOneResolverArgs<Partial<ObjectRecord>>, options: WorkspaceQueryRunnerOptions, - ): Promise<ObjectRecord | undefined> { - const graphqlQueryCreateManyResolverService = - new GraphqlQueryCreateManyResolverService(this.twentyORMGlobalManager); + ): Promise<ObjectRecord> { + const existingRecord = await this.executeQuery< + FindOneResolverArgs, + ObjectRecord + >( + 'findOne', + { + filter: { id: { eq: args.id } }, + }, + options, + ); - const { authContext, objectMetadataItem } = options; + const result = await this.executeQuery< + UpdateOneResolverArgs<Partial<ObjectRecord>>, + ObjectRecord + >('updateOne', args, options); + + this.apiEventEmitterService.emitUpdateEvents( + [existingRecord], + [result], + Object.keys(args.data), + options.authContext, + options.objectMetadataItem, + ); - assertMutationNotOnRemoteObject(objectMetadataItem); + return result; + } - if (args.data.id) { - assertIsValidUuid(args.data.id); - } + @LogExecutionTime() + public async updateMany<ObjectRecord extends IRecord>( + args: UpdateManyResolverArgs<Partial<ObjectRecord>>, + options: WorkspaceQueryRunnerOptions, + ): Promise<ObjectRecord[]> { + const existingRecords = await this.executeQuery< + FindManyResolverArgs, + IConnection<ObjectRecord, IEdge<ObjectRecord>> + >( + 'findMany', + { + filter: args.filter, + }, + options, + ); - const createManyArgs = { - data: [args.data], - upsert: args.upsert, - } as CreateManyResolverArgs<ObjectRecord>; + const result = await this.executeQuery< + UpdateManyResolverArgs<Partial<ObjectRecord>>, + ObjectRecord[] + >('updateMany', args, options); + + this.apiEventEmitterService.emitUpdateEvents( + existingRecords.edges.map((edge) => edge.node), + result, + Object.keys(args.data), + options.authContext, + options.objectMetadataItem, + ); - const hookedArgs = - await this.workspaceQueryHookService.executePreQueryHooks( - authContext, - objectMetadataItem.nameSingular, - 'createMany', - createManyArgs, - ); + return result; + } - const computedArgs = (await this.queryRunnerArgsFactory.create( - hookedArgs, + @LogExecutionTime() + public async deleteOne<ObjectRecord extends IRecord & { deletedAt?: Date }>( + args: DeleteOneResolverArgs, + options: WorkspaceQueryRunnerOptions, + ): Promise<ObjectRecord> { + const result = await this.executeQuery< + UpdateOneResolverArgs<Partial<ObjectRecord>>, + ObjectRecord + >( + 'deleteOne', + { + id: args.id, + data: { deletedAt: new Date() } as Partial<ObjectRecord>, + }, options, - ResolverArgsType.CreateMany, - )) as CreateManyResolverArgs<ObjectRecord>; + ); - const results = (await graphqlQueryCreateManyResolverService.createMany( - computedArgs, - options, - )) as ObjectRecord[]; + this.apiEventEmitterService.emitDeletedEvents( + [result], + options.authContext, + options.objectMetadataItem, + ); - await this.triggerWebhooks<ObjectRecord>( - results, - CallWebhookJobsJobOperation.create, + return result; + } + + @LogExecutionTime() + public async deleteMany<ObjectRecord extends IRecord & { deletedAt?: Date }>( + args: DeleteManyResolverArgs, + options: WorkspaceQueryRunnerOptions, + ): Promise<ObjectRecord[]> { + const result = await this.executeQuery< + UpdateManyResolverArgs<Partial<ObjectRecord>>, + ObjectRecord[] + >( + 'deleteMany', + { + filter: args.filter, + + data: { deletedAt: new Date() } as Partial<ObjectRecord>, + }, options, ); - this.emitCreateEvents<ObjectRecord>( - results, - authContext, - objectMetadataItem, + this.apiEventEmitterService.emitDeletedEvents( + result, + options.authContext, + options.objectMetadataItem, ); - return results?.[0] as ObjectRecord; + return result; } @LogExecutionTime() - async createMany<ObjectRecord extends IRecord = IRecord>( - args: CreateManyResolverArgs<Partial<ObjectRecord>>, + async destroyOne<ObjectRecord extends IRecord>( + args: DestroyOneResolverArgs, options: WorkspaceQueryRunnerOptions, - ): Promise<ObjectRecord[] | undefined> { - const graphqlQueryCreateManyResolverService = - new GraphqlQueryCreateManyResolverService(this.twentyORMGlobalManager); + ): Promise<ObjectRecord> { + const result = await this.executeQuery< + DestroyOneResolverArgs, + ObjectRecord + >('destroyOne', args, options); + + this.apiEventEmitterService.emitDestroyEvents( + [result], + options.authContext, + options.objectMetadataItem, + ); + + return result; + } + + @LogExecutionTime() + async destroyMany<ObjectRecord extends IRecord>( + args: DestroyManyResolverArgs, + options: WorkspaceQueryRunnerOptions, + ): Promise<ObjectRecord[]> { + const result = await this.executeQuery< + DestroyManyResolverArgs, + ObjectRecord[] + >('destroyMany', args, options); + + this.apiEventEmitterService.emitDestroyEvents( + result, + options.authContext, + options.objectMetadataItem, + ); + return result; + } + + @LogExecutionTime() + public async restoreMany<ObjectRecord extends IRecord>( + args: RestoreManyResolverArgs, + options: WorkspaceQueryRunnerOptions, + ): Promise<ObjectRecord> { + const result = await this.executeQuery< + UpdateManyResolverArgs<Partial<ObjectRecord>>, + ObjectRecord + >( + 'restoreMany', + { + filter: args.filter, + data: { deletedAt: null } as Partial<ObjectRecord>, + }, + options, + ); + + return result; + } + + private async executeQuery<Input extends ResolverArgs, Response>( + operationName: WorkspaceResolverBuilderMethodNames, + args: Input, + options: WorkspaceQueryRunnerOptions, + ): Promise<Response> { const { authContext, objectMetadataItem } = options; - assertMutationNotOnRemoteObject(objectMetadataItem); + const resolver = + this.graphqlQueryResolverFactory.getResolver(operationName); - args.data.forEach((record) => { - if (record?.id) { - assertIsValidUuid(record.id); - } - }); + await resolver.validate(args, options); const hookedArgs = await this.workspaceQueryHookService.executePreQueryHooks( authContext, objectMetadataItem.nameSingular, - 'createMany', + operationName, args, ); - const computedArgs = (await this.queryRunnerArgsFactory.create( + const computedArgs = await this.queryRunnerArgsFactory.create( hookedArgs, options, - ResolverArgsType.CreateMany, - )) as CreateManyResolverArgs<ObjectRecord>; + ResolverArgsType[capitalize(operationName)], + ); - const results = (await graphqlQueryCreateManyResolverService.createMany( - computedArgs, - options, - )) as ObjectRecord[]; + const results = await resolver.resolve(computedArgs as Input, options); await this.workspaceQueryHookService.executePostQueryHooks( authContext, objectMetadataItem.nameSingular, - 'createMany', - results, + operationName, + Array.isArray(results) ? results : [results], ); - await this.triggerWebhooks<ObjectRecord>( - results, - CallWebhookJobsJobOperation.create, - options, - ); + const jobOperation = this.operationNameToJobOperation(operationName); - this.emitCreateEvents<ObjectRecord>( - results, - authContext, - objectMetadataItem, - ); + if (jobOperation) { + await this.triggerWebhooks(results, jobOperation, options); + } return results; } - private emitCreateEvents<BaseRecord extends IRecord = IRecord>( - records: BaseRecord[], - authContext: AuthContext, - objectMetadataItem: ObjectMetadataInterface, - ) { - this.workspaceEventEmitter.emit( - `${objectMetadataItem.nameSingular}.created`, - records.map( - (record) => - ({ - userId: authContext.user?.id, - recordId: record.id, - objectMetadata: objectMetadataItem, - properties: { - after: record, - }, - }) satisfies ObjectRecordCreateEvent<any>, - ), - authContext.workspace.id, - ); + private operationNameToJobOperation( + operationName: WorkspaceResolverBuilderMethodNames, + ): CallWebhookJobsJobOperation | undefined { + switch (operationName) { + case 'createOne': + case 'createMany': + return CallWebhookJobsJobOperation.create; + case 'updateOne': + case 'updateMany': + case 'restoreMany': + return CallWebhookJobsJobOperation.update; + case 'deleteOne': + case 'deleteMany': + return CallWebhookJobsJobOperation.delete; + case 'destroyOne': + return CallWebhookJobsJobOperation.destroy; + default: + return undefined; + } } - private async triggerWebhooks<Record>( - jobsData: Record[] | undefined, + private async triggerWebhooks<T>( + jobsData: T[] | undefined, operation: CallWebhookJobsJobOperation, options: WorkspaceQueryRunnerOptions, - ) { - if (!Array.isArray(jobsData)) { - return; - } + ): Promise<void> { + if (!jobsData || !Array.isArray(jobsData)) return; + jobsData.forEach((jobData) => { this.messageQueueService.add<CallWebhookJobsJobData>( CallWebhookJobsJob.name, @@ -279,15 +410,4 @@ export class GraphqlQueryRunnerService { ); }); } - - @LogExecutionTime() - async destroyOne<ObjectRecord extends IRecord = IRecord>( - args: DestroyOneResolverArgs, - options: WorkspaceQueryRunnerOptions, - ): Promise<ObjectRecord> { - const graphqlQueryDestroyOneResolverService = - new GraphqlQueryDestroyOneResolverService(this.twentyORMGlobalManager); - - return graphqlQueryDestroyOneResolverService.destroyOne(args, options); - } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts similarity index 78% rename from packages/twenty-server/src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper.ts rename to packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts index b9a81ef245dc..54220315345a 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper.ts @@ -20,32 +20,41 @@ import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspac import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; import { isPlainObject } from 'src/utils/is-plain-object'; -export class ObjectRecordsToGraphqlConnectionMapper { +export class ObjectRecordsToGraphqlConnectionHelper { private objectMetadataMap: ObjectMetadataMap; constructor(objectMetadataMap: ObjectMetadataMap) { this.objectMetadataMap = objectMetadataMap; } - public createConnection<ObjectRecord extends IRecord = IRecord>( - objectRecords: ObjectRecord[], - objectName: string, - take: number, - totalCount: number, - order: RecordOrderBy | undefined, - hasNextPage: boolean, - hasPreviousPage: boolean, + public createConnection<ObjectRecord extends IRecord = IRecord>({ + objectRecords, + objectName, + take, + totalCount, + order, + hasNextPage, + hasPreviousPage, depth = 0, - ): IConnection<ObjectRecord> { + }: { + objectRecords: ObjectRecord[]; + objectName: string; + take: number; + totalCount: number; + order?: RecordOrderBy; + hasNextPage: boolean; + hasPreviousPage: boolean; + depth?: number; + }): IConnection<ObjectRecord> { const edges = (objectRecords ?? []).map((objectRecord) => ({ - node: this.processRecord( + node: this.processRecord({ objectRecord, objectName, take, totalCount, order, depth, - ), + }), cursor: encodeCursor(objectRecord, order), })); @@ -61,14 +70,21 @@ export class ObjectRecordsToGraphqlConnectionMapper { }; } - public processRecord<T extends Record<string, any>>( - objectRecord: T, - objectName: string, - take: number, - totalCount: number, - order?: RecordOrderBy, + public processRecord<T extends Record<string, any>>({ + objectRecord, + objectName, + take, + totalCount, + order, depth = 0, - ): T { + }: { + objectRecord: T; + objectName: string; + take: number; + totalCount: number; + order?: RecordOrderBy; + depth?: number; + }): T { if (depth >= CONNECTION_MAX_DEPTH) { throw new GraphqlQueryRunnerException( `Maximum depth of ${CONNECTION_MAX_DEPTH} reached`, @@ -97,27 +113,31 @@ export class ObjectRecordsToGraphqlConnectionMapper { if (isRelationFieldMetadataType(fieldMetadata.type)) { if (Array.isArray(value)) { - processedObjectRecord[key] = this.createConnection( - value, - getRelationObjectMetadata(fieldMetadata, this.objectMetadataMap) - .nameSingular, + processedObjectRecord[key] = this.createConnection({ + objectRecords: value, + objectName: getRelationObjectMetadata( + fieldMetadata, + this.objectMetadataMap, + ).nameSingular, take, - value.length, + totalCount: value.length, order, - false, - false, - depth + 1, - ); + hasNextPage: false, + hasPreviousPage: false, + depth: depth + 1, + }); } else if (isPlainObject(value)) { - processedObjectRecord[key] = this.processRecord( - value, - getRelationObjectMetadata(fieldMetadata, this.objectMetadataMap) - .nameSingular, + processedObjectRecord[key] = this.processRecord({ + objectRecord: value, + objectName: getRelationObjectMetadata( + fieldMetadata, + this.objectMetadataMap, + ).nameSingular, take, totalCount, order, - depth + 1, - ); + depth: depth + 1, + }); } } else if (isCompositeFieldMetadataType(fieldMetadata.type)) { processedObjectRecord[key] = this.processCompositeField( diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts index f19c7cf06eb3..dd3e5abd4020 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper.ts @@ -4,6 +4,7 @@ import { FindOptionsRelations, In, ObjectLiteral, + Repository, } from 'typeorm'; import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; @@ -16,17 +17,38 @@ import { ObjectMetadataMap, ObjectMetadataMapItem, } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; -import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { deduceRelationDirection } from 'src/engine/utils/deduce-relation-direction.util'; export class ProcessNestedRelationsHelper { - private readonly twentyORMGlobalManager: TwentyORMGlobalManager; + constructor() {} - constructor(twentyORMGlobalManager: TwentyORMGlobalManager) { - this.twentyORMGlobalManager = twentyORMGlobalManager; + public async processNestedRelations<ObjectRecord extends IRecord = IRecord>( + objectMetadataMap: ObjectMetadataMap, + parentObjectMetadataItem: ObjectMetadataMapItem, + parentObjectRecords: ObjectRecord[], + relations: Record<string, FindOptionsRelations<ObjectLiteral>>, + limit: number, + authContext: any, + dataSource: DataSource, + ): Promise<void> { + const processRelationTasks = Object.entries(relations).map( + ([relationName, nestedRelations]) => + this.processRelation( + objectMetadataMap, + parentObjectMetadataItem, + parentObjectRecords, + relationName, + nestedRelations, + limit, + authContext, + dataSource, + ), + ); + + await Promise.all(processRelationTasks); } - private async processFromRelation<ObjectRecord extends IRecord = IRecord>( + private async processRelation<ObjectRecord extends IRecord = IRecord>( objectMetadataMap: ObjectMetadataMap, parentObjectMetadataItem: ObjectMetadataMapItem, parentObjectRecords: ObjectRecord[], @@ -35,49 +57,71 @@ export class ProcessNestedRelationsHelper { limit: number, authContext: any, dataSource: DataSource, - ) { + ): Promise<void> { const relationFieldMetadata = parentObjectMetadataItem.fields[relationName]; const relationMetadata = getRelationMetadata(relationFieldMetadata); - - const inverseRelationName = - objectMetadataMap[relationMetadata.toObjectMetadataId]?.fields[ - relationMetadata.toFieldMetadataId - ]?.name; - - const referenceObjectMetadata = getRelationObjectMetadata( + const relationDirection = deduceRelationDirection( relationFieldMetadata, - objectMetadataMap, + relationMetadata, ); - const referenceObjectMetadataName = referenceObjectMetadata.nameSingular; + const processor = + relationDirection === 'to' + ? this.processToRelation + : this.processFromRelation; - const relationRepository = await dataSource.getRepository( - referenceObjectMetadataName, + await processor.call( + this, + objectMetadataMap, + parentObjectMetadataItem, + parentObjectRecords, + relationName, + nestedRelations, + limit, + authContext, + dataSource, ); + } - const relationIds = parentObjectRecords.map((item) => item.id); - - const uniqueRelationIds = [...new Set(relationIds)]; - - const relationFindOptions: FindManyOptions = { - where: { - [`${inverseRelationName}Id`]: In(uniqueRelationIds), - }, - take: limit * parentObjectRecords.length, - }; + private async processFromRelation<ObjectRecord extends IRecord = IRecord>( + objectMetadataMap: ObjectMetadataMap, + parentObjectMetadataItem: ObjectMetadataMapItem, + parentObjectRecords: ObjectRecord[], + relationName: string, + nestedRelations: any, + limit: number, + authContext: any, + dataSource: DataSource, + ): Promise<void> { + const { inverseRelationName, referenceObjectMetadata } = + this.getRelationMetadata( + objectMetadataMap, + parentObjectMetadataItem, + relationName, + ); + const relationRepository = dataSource.getRepository( + referenceObjectMetadata.nameSingular, + ); - const relationResults = await relationRepository.find(relationFindOptions); + const relationIds = this.getUniqueIds(parentObjectRecords, 'id'); + const relationResults = await this.findRelations( + relationRepository, + inverseRelationName, + relationIds, + limit * parentObjectRecords.length, + ); - parentObjectRecords.forEach((item) => { - (item as any)[relationName] = relationResults.filter( - (rel) => rel[`${inverseRelationName}Id`] === item.id, - ); - }); + this.assignRelationResults( + parentObjectRecords, + relationResults, + relationName, + `${inverseRelationName}Id`, + ); if (Object.keys(nestedRelations).length > 0) { await this.processNestedRelations( objectMetadataMap, - objectMetadataMap[referenceObjectMetadataName], + objectMetadataMap[referenceObjectMetadata.nameSingular], relationResults as ObjectRecord[], nestedRelations as Record<string, FindOptionsRelations<ObjectLiteral>>, limit, @@ -96,48 +140,37 @@ export class ProcessNestedRelationsHelper { limit: number, authContext: any, dataSource: DataSource, - ) { - const relationFieldMetadata = parentObjectMetadataItem.fields[relationName]; - - const referenceObjectMetadata = getRelationObjectMetadata( - relationFieldMetadata, + ): Promise<void> { + const { referenceObjectMetadata } = this.getRelationMetadata( objectMetadataMap, + parentObjectMetadataItem, + relationName, ); - - const referenceObjectMetadataName = referenceObjectMetadata.nameSingular; - const relationRepository = dataSource.getRepository( - referenceObjectMetadataName, + referenceObjectMetadata.nameSingular, ); - const relationIds = parentObjectRecords.map( - (item) => item[`${relationName}Id`], + const relationIds = this.getUniqueIds( + parentObjectRecords, + `${relationName}Id`, + ); + const relationResults = await this.findRelations( + relationRepository, + 'id', + relationIds, + limit, ); - const uniqueRelationIds = [...new Set(relationIds)]; - - const relationFindOptions: FindManyOptions = { - where: { - id: In(uniqueRelationIds), - }, - take: limit, - }; - - const relationResults = await relationRepository.find(relationFindOptions); - - parentObjectRecords.forEach((item) => { - if (relationResults.length === 0) { - (item as any)[`${relationName}Id`] = null; - } - (item as any)[relationName] = relationResults.filter( - (rel) => rel.id === item[`${relationName}Id`], - )[0]; - }); + this.assignToRelationResults( + parentObjectRecords, + relationResults, + relationName, + ); if (Object.keys(nestedRelations).length > 0) { await this.processNestedRelations( objectMetadataMap, - objectMetadataMap[referenceObjectMetadataName], + objectMetadataMap[referenceObjectMetadata.nameSingular], relationResults as ObjectRecord[], nestedRelations as Record<string, FindOptionsRelations<ObjectLiteral>>, limit, @@ -147,48 +180,71 @@ export class ProcessNestedRelationsHelper { } } - public async processNestedRelations<ObjectRecord extends IRecord = IRecord>( + private getRelationMetadata( objectMetadataMap: ObjectMetadataMap, parentObjectMetadataItem: ObjectMetadataMapItem, - parentObjectRecords: ObjectRecord[], - relations: Record<string, FindOptionsRelations<ObjectLiteral>>, - limit: number, - authContext: any, - dataSource: DataSource, + relationName: string, ) { - for (const [relationName, nestedRelations] of Object.entries(relations)) { - const relationFieldMetadata = - parentObjectMetadataItem.fields[relationName]; - const relationMetadata = getRelationMetadata(relationFieldMetadata); - - const relationDirection = deduceRelationDirection( - relationFieldMetadata, - relationMetadata, + const relationFieldMetadata = parentObjectMetadataItem.fields[relationName]; + const relationMetadata = getRelationMetadata(relationFieldMetadata); + const referenceObjectMetadata = getRelationObjectMetadata( + relationFieldMetadata, + objectMetadataMap, + ); + const inverseRelationName = + objectMetadataMap[relationMetadata.toObjectMetadataId]?.fields[ + relationMetadata.toFieldMetadataId + ]?.name; + + return { inverseRelationName, referenceObjectMetadata }; + } + + private getUniqueIds(records: IRecord[], idField: string): any[] { + return [...new Set(records.map((item) => item[idField]))]; + } + + private async findRelations( + repository: Repository<any>, + field: string, + ids: any[], + limit: number, + ): Promise<any[]> { + if (ids.length === 0) { + return []; + } + const findOptions: FindManyOptions = { + where: { [field]: In(ids) }, + take: limit, + }; + + return repository.find(findOptions); + } + + private assignRelationResults( + parentRecords: IRecord[], + relationResults: any[], + relationName: string, + joinField: string, + ): void { + parentRecords.forEach((item) => { + (item as any)[relationName] = relationResults.filter( + (rel) => rel[joinField] === item.id, ); + }); + } - if (relationDirection === 'to') { - await this.processToRelation( - objectMetadataMap, - parentObjectMetadataItem, - parentObjectRecords, - relationName, - nestedRelations, - limit, - authContext, - dataSource, - ); - } else { - await this.processFromRelation( - objectMetadataMap, - parentObjectMetadataItem, - parentObjectRecords, - relationName, - nestedRelations, - limit, - authContext, - dataSource, - ); + private assignToRelationResults( + parentRecords: IRecord[], + relationResults: any[], + relationName: string, + ): void { + parentRecords.forEach((item) => { + if (relationResults.length === 0) { + (item as any)[`${relationName}Id`] = null; } - } + (item as any)[relationName] = + relationResults.find((rel) => rel.id === item[`${relationName}Id`]) ?? + null; + }); } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface.ts new file mode 100644 index 000000000000..f88691647425 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface.ts @@ -0,0 +1,12 @@ +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; + +export interface ResolverService<ResolverArgs, T> { + resolve: ( + args: ResolverArgs, + options: WorkspaceQueryRunnerOptions, + ) => Promise<T>; + validate: ( + args: ResolverArgs, + options: WorkspaceQueryRunnerOptions, + ) => Promise<void>; +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts index f66497a6812a..d5ee25e9be6d 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-create-many-resolver.service.ts @@ -1,51 +1,53 @@ +import { Injectable } from '@nestjs/common'; + import graphqlFields from 'graphql-fields'; import { In, InsertResult } from 'typeorm'; +import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { CreateManyResolverArgs } 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 { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper'; -import { getObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util'; -import { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { ObjectRecordsToGraphqlConnectionHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/object-records-to-graphql-connection.helper'; +import { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; +import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; -export class GraphqlQueryCreateManyResolverService { - private twentyORMGlobalManager: TwentyORMGlobalManager; - - constructor(twentyORMGlobalManager: TwentyORMGlobalManager) { - this.twentyORMGlobalManager = twentyORMGlobalManager; - } +@Injectable() +export class GraphqlQueryCreateManyResolverService + implements ResolverService<CreateManyResolverArgs, IRecord[]> +{ + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) {} - async createMany<ObjectRecord extends IRecord = IRecord>( + async resolve<ObjectRecord extends IRecord = IRecord>( args: CreateManyResolverArgs<Partial<ObjectRecord>>, options: WorkspaceQueryRunnerOptions, - ): Promise<ObjectRecord[] | undefined> { - const { authContext, objectMetadataItem, objectMetadataCollection, info } = + ): Promise<ObjectRecord[]> { + const { authContext, info, objectMetadataMap, objectMetadataMapItem } = options; - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + const dataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace( authContext.workspace.id, - objectMetadataItem.nameSingular, ); - - const objectMetadataMap = generateObjectMetadataMap( - objectMetadataCollection, - ); - const objectMetadata = getObjectMetadataOrThrow( - objectMetadataMap, - objectMetadataItem.nameSingular, + const repository = dataSource.getRepository( + objectMetadataMapItem.nameSingular, ); + const graphqlQueryParser = new GraphqlQueryParser( - objectMetadata.fields, + objectMetadataMapItem.fields, objectMetadataMap, ); const selectedFields = graphqlFields(info); - const { select, relations } = graphqlQueryParser.parseSelectedFields( - objectMetadataItem, + const { relations } = graphqlQueryParser.parseSelectedFields( + objectMetadataMapItem, selectedFields, ); @@ -56,24 +58,59 @@ export class GraphqlQueryCreateManyResolverService { skipUpdateIfNoValuesChanged: true, }); - const upsertedRecords = await repository.find({ - where: { + const queryBuilder = repository.createQueryBuilder( + objectMetadataMapItem.nameSingular, + ); + + const nonFormattedUpsertedRecords = (await queryBuilder + .where({ id: In(objectRecords.generatedMaps.map((record) => record.id)), - }, - select, - relations, - }); + }) + .take(QUERY_MAX_RECORDS) + .getMany()) as ObjectRecord[]; + + const upsertedRecords = formatResult( + nonFormattedUpsertedRecords, + objectMetadataMapItem, + objectMetadataMap, + ); + + const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); + + if (relations) { + await processNestedRelationsHelper.processNestedRelations( + objectMetadataMap, + objectMetadataMapItem, + upsertedRecords, + relations, + QUERY_MAX_RECORDS, + authContext, + dataSource, + ); + } const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap); + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); return upsertedRecords.map((record: ObjectRecord) => - typeORMObjectRecordsParser.processRecord( - record, - objectMetadataItem.nameSingular, - 1, - 1, - ), + typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: objectMetadataMapItem.nameSingular, + take: 1, + totalCount: 1, + }), ); } + + async validate<ObjectRecord extends IRecord>( + args: CreateManyResolverArgs<Partial<ObjectRecord>>, + options: WorkspaceQueryRunnerOptions, + ): Promise<void> { + assertMutationNotOnRemoteObject(options.objectMetadataItem); + args.data.forEach((record) => { + if (record?.id) { + assertIsValidUuid(record.id); + } + }); + } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts new file mode 100644 index 000000000000..04ceddf9ac9d --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-many-resolver.service.ts @@ -0,0 +1,108 @@ +import { Injectable } from '@nestjs/common'; + +import graphqlFields from 'graphql-fields'; + +import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; +import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; +import { DestroyManyResolverArgs } 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 { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; + +@Injectable() +export class GraphqlQueryDestroyManyResolverService + implements ResolverService<DestroyManyResolverArgs, IRecord[]> +{ + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) {} + + async resolve<ObjectRecord extends IRecord = IRecord>( + args: DestroyManyResolverArgs, + options: WorkspaceQueryRunnerOptions, + ): Promise<ObjectRecord[]> { + const { authContext, objectMetadataMapItem, objectMetadataMap, info } = + options; + const dataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace( + authContext.workspace.id, + ); + + const repository = dataSource.getRepository( + objectMetadataMapItem.nameSingular, + ); + + const graphqlQueryParser = new GraphqlQueryParser( + objectMetadataMapItem.fields, + objectMetadataMap, + ); + + const selectedFields = graphqlFields(info); + + const { relations } = graphqlQueryParser.parseSelectedFields( + objectMetadataMapItem, + selectedFields, + ); + + const queryBuilder = repository.createQueryBuilder( + objectMetadataMapItem.nameSingular, + ); + + const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( + queryBuilder, + objectMetadataMapItem.nameSingular, + args.filter, + ); + + const nonFormattedDeletedObjectRecords = await withFilterQueryBuilder + .delete() + .returning('*') + .execute(); + + const deletedRecords = formatResult( + nonFormattedDeletedObjectRecords.raw, + objectMetadataMapItem, + objectMetadataMap, + ); + + const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); + + if (relations) { + await processNestedRelationsHelper.processNestedRelations( + objectMetadataMap, + objectMetadataMapItem, + deletedRecords, + relations, + QUERY_MAX_RECORDS, + authContext, + dataSource, + ); + } + + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); + + return deletedRecords.map((record: ObjectRecord) => + typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: objectMetadataMapItem.nameSingular, + take: 1, + totalCount: 1, + }), + ); + } + + async validate( + args: DestroyManyResolverArgs, + _options: WorkspaceQueryRunnerOptions, + ): Promise<void> { + if (!args.filter) { + throw new Error('Filter is required'); + } + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts index 53dad3eddd0b..483222a49050 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-destroy-one-resolver.service.ts @@ -1,33 +1,118 @@ +import { Injectable } from '@nestjs/common'; + +import graphqlFields from 'graphql-fields'; + +import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { DestroyOneResolverArgs } 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 { + GraphqlQueryRunnerException, + GraphqlQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; +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 { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; -export class GraphqlQueryDestroyOneResolverService { - private twentyORMGlobalManager: TwentyORMGlobalManager; - - constructor(twentyORMGlobalManager: TwentyORMGlobalManager) { - this.twentyORMGlobalManager = twentyORMGlobalManager; - } +@Injectable() +export class GraphqlQueryDestroyOneResolverService + implements ResolverService<DestroyOneResolverArgs, IRecord> +{ + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) {} - async destroyOne<ObjectRecord extends IRecord = IRecord>( + async resolve<ObjectRecord extends IRecord = IRecord>( args: DestroyOneResolverArgs, options: WorkspaceQueryRunnerOptions, ): Promise<ObjectRecord> { - const { authContext, objectMetadataItem } = options; - const repository = - await this.twentyORMGlobalManager.getRepositoryForWorkspace( + const { authContext, objectMetadataMapItem, objectMetadataMap, info } = + options; + const dataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace( authContext.workspace.id, - objectMetadataItem.nameSingular, ); - const record = await repository.findOne({ - where: { id: args.id }, - }); + const repository = dataSource.getRepository( + objectMetadataMapItem.nameSingular, + ); + + const graphqlQueryParser = new GraphqlQueryParser( + objectMetadataMapItem.fields, + objectMetadataMap, + ); - await repository.delete(args.id); + const selectedFields = graphqlFields(info); + + const { relations } = graphqlQueryParser.parseSelectedFields( + objectMetadataMapItem, + selectedFields, + ); + + const queryBuilder = repository.createQueryBuilder( + objectMetadataMapItem.nameSingular, + ); + + const nonFormattedDeletedObjectRecords = await queryBuilder + .where({ + id: args.id, + }) + .take(1) + .delete() + .returning('*') + .execute(); + + if (!nonFormattedDeletedObjectRecords.affected) { + throw new GraphqlQueryRunnerException( + 'Record not found', + GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND, + ); + } - return record as ObjectRecord; + const recordBeforeDeletion = formatResult( + nonFormattedDeletedObjectRecords.raw, + objectMetadataMapItem, + objectMetadataMap, + )[0]; + + const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); + + if (relations) { + await processNestedRelationsHelper.processNestedRelations( + objectMetadataMap, + objectMetadataMapItem, + [recordBeforeDeletion], + relations, + QUERY_MAX_RECORDS, + authContext, + dataSource, + ); + } + + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); + + return typeORMObjectRecordsParser.processRecord({ + objectRecord: recordBeforeDeletion, + objectName: objectMetadataMapItem.nameSingular, + take: 1, + totalCount: 1, + }); + } + + async validate( + args: DestroyOneResolverArgs, + _options: WorkspaceQueryRunnerOptions, + ): Promise<void> { + if (!args.id) { + throw new GraphqlQueryRunnerException( + 'Missing id', + GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); + } } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts new file mode 100644 index 000000000000..d3bc72fa8220 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-duplicates-resolver.service.ts @@ -0,0 +1,214 @@ +import { Injectable } from '@nestjs/common'; + +import isEmpty from 'lodash.isempty'; +import { In } from 'typeorm'; + +import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; +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 { FindDuplicatesResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; + +import { + GraphqlQueryRunnerException, + GraphqlQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; +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 { settings } from 'src/engine/constants/settings'; +import { DUPLICATE_CRITERIA_COLLECTION } from 'src/engine/core-modules/duplicate/constants/duplicate-criteria.constants'; +import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; +import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; + +@Injectable() +export class GraphqlQueryFindDuplicatesResolverService + implements + ResolverService<FindDuplicatesResolverArgs, IConnection<IRecord>[]> +{ + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) {} + + async resolve<ObjectRecord extends IRecord = IRecord>( + args: FindDuplicatesResolverArgs<Partial<ObjectRecord>>, + options: WorkspaceQueryRunnerOptions, + ): Promise<IConnection<ObjectRecord>[]> { + const { authContext, objectMetadataMapItem, objectMetadataMap } = options; + + const dataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace( + authContext.workspace.id, + ); + const repository = dataSource.getRepository( + objectMetadataMapItem.nameSingular, + ); + const existingRecordsQueryBuilder = repository.createQueryBuilder( + objectMetadataMapItem.nameSingular, + ); + const duplicateRecordsQueryBuilder = repository.createQueryBuilder( + objectMetadataMapItem.nameSingular, + ); + + const graphqlQueryParser = new GraphqlQueryParser( + objectMetadataMap[objectMetadataMapItem.nameSingular].fields, + objectMetadataMap, + ); + + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); + + let objectRecords: Partial<ObjectRecord>[] = []; + + if (args.ids) { + const nonFormattedObjectRecords = (await existingRecordsQueryBuilder + .where({ id: In(args.ids) }) + .getMany()) as ObjectRecord[]; + + objectRecords = formatResult( + nonFormattedObjectRecords, + objectMetadataMapItem, + objectMetadataMap, + ); + } else if (args.data && !isEmpty(args.data)) { + objectRecords = formatData(args.data, objectMetadataMapItem); + } + + const duplicateConnections: IConnection<ObjectRecord>[] = await Promise.all( + objectRecords.map(async (record) => { + const duplicateConditions = this.buildDuplicateConditions( + objectMetadataMapItem, + [record], + record.id, + ); + + if (isEmpty(duplicateConditions)) { + return typeORMObjectRecordsParser.createConnection({ + objectRecords: [], + objectName: objectMetadataMapItem.nameSingular, + take: 0, + totalCount: 0, + order: [{ id: OrderByDirection.AscNullsFirst }], + hasNextPage: false, + hasPreviousPage: false, + }); + } + + const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( + duplicateRecordsQueryBuilder, + objectMetadataMapItem.nameSingular, + duplicateConditions, + ); + + const nonFormattedDuplicates = + (await withFilterQueryBuilder.getMany()) as ObjectRecord[]; + + const duplicates = formatResult( + nonFormattedDuplicates, + objectMetadataMapItem, + objectMetadataMap, + ); + + return typeORMObjectRecordsParser.createConnection({ + objectRecords: duplicates, + objectName: objectMetadataMapItem.nameSingular, + take: duplicates.length, + totalCount: duplicates.length, + order: [{ id: OrderByDirection.AscNullsFirst }], + hasNextPage: false, + hasPreviousPage: false, + }); + }), + ); + + return duplicateConnections; + } + + private buildDuplicateConditions( + objectMetadataMapItem: ObjectMetadataMapItem, + records?: Partial<IRecord>[] | undefined, + filteringByExistingRecordId?: string, + ): Partial<RecordFilter> { + if (!records || records.length === 0) { + return {}; + } + + const criteriaCollection = this.getApplicableDuplicateCriteriaCollection( + objectMetadataMapItem, + ); + + const conditions = records.flatMap((record) => { + const criteriaWithMatchingArgs = criteriaCollection.filter((criteria) => + criteria.columnNames.every((columnName) => { + const value = record[columnName] as string | undefined; + + return ( + value && value.length >= settings.minLengthOfStringForDuplicateCheck + ); + }), + ); + + return criteriaWithMatchingArgs.map((criteria) => { + const condition = {}; + + criteria.columnNames.forEach((columnName) => { + condition[columnName] = { eq: record[columnName] }; + }); + + return condition; + }); + }); + + const filter: Partial<RecordFilter> = {}; + + if (conditions && !isEmpty(conditions)) { + filter.or = conditions; + + if (filteringByExistingRecordId) { + filter.id = { neq: filteringByExistingRecordId }; + } + } + + return filter; + } + + private getApplicableDuplicateCriteriaCollection( + objectMetadataMapItem: ObjectMetadataMapItem, + ) { + return DUPLICATE_CRITERIA_COLLECTION.filter( + (duplicateCriteria) => + duplicateCriteria.objectName === objectMetadataMapItem.nameSingular, + ); + } + + async validate( + args: FindDuplicatesResolverArgs, + _options: WorkspaceQueryRunnerOptions, + ): Promise<void> { + if (!args.data && !args.ids) { + throw new GraphqlQueryRunnerException( + 'You have to provide either "data" or "ids" argument', + GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); + } + + if (args.data && args.ids) { + throw new GraphqlQueryRunnerException( + 'You cannot provide both "data" and "ids" arguments', + GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); + } + + if (!args.ids && isEmpty(args.data)) { + throw new GraphqlQueryRunnerException( + 'The "data" condition can not be empty when "ids" input not provided', + GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); + } + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts index 5caa30e4e51e..9411c5502103 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-many-resolver.service.ts @@ -1,6 +1,9 @@ +import { Injectable } from '@nestjs/common'; + import { isDefined } from 'class-validator'; import graphqlFields from 'graphql-fields'; +import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; import { Record as IRecord, OrderByDirection, @@ -17,26 +20,25 @@ import { GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; 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 { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; -import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper'; import { computeCursorArgFilter } from 'src/engine/api/graphql/graphql-query-runner/utils/compute-cursor-arg-filter'; -import { decodeCursor } from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; -import { getObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util'; import { - ObjectMetadataMapItem, - generateObjectMetadataMap, -} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; + getCursor, + getPaginationInfo, +} from 'src/engine/api/graphql/graphql-query-runner/utils/cursors.util'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; -export class GraphqlQueryFindManyResolverService { - private twentyORMGlobalManager: TwentyORMGlobalManager; - - constructor(twentyORMGlobalManager: TwentyORMGlobalManager) { - this.twentyORMGlobalManager = twentyORMGlobalManager; - } +@Injectable() +export class GraphqlQueryFindManyResolverService + implements ResolverService<FindManyResolverArgs, IConnection<IRecord>> +{ + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) {} - async findMany< + async resolve< ObjectRecord extends IRecord = IRecord, Filter extends RecordFilter = RecordFilter, OrderBy extends RecordOrderBy = RecordOrderBy, @@ -44,51 +46,41 @@ export class GraphqlQueryFindManyResolverService { args: FindManyResolverArgs<Filter, OrderBy>, options: WorkspaceQueryRunnerOptions, ): Promise<IConnection<ObjectRecord>> { - const { authContext, objectMetadataItem, info, objectMetadataCollection } = + const { authContext, objectMetadataMapItem, info, objectMetadataMap } = options; - this.validateArgsOrThrow(args); - const dataSource = await this.twentyORMGlobalManager.getDataSourceForWorkspace( authContext.workspace.id, ); const repository = dataSource.getRepository( - objectMetadataItem.nameSingular, + objectMetadataMapItem.nameSingular, ); const queryBuilder = repository.createQueryBuilder( - objectMetadataItem.nameSingular, + objectMetadataMapItem.nameSingular, ); const countQueryBuilder = repository.createQueryBuilder( - objectMetadataItem.nameSingular, - ); - - const objectMetadataMap = generateObjectMetadataMap( - objectMetadataCollection, + objectMetadataMapItem.nameSingular, ); - const objectMetadata = getObjectMetadataOrThrow( - objectMetadataMap, - objectMetadataItem.nameSingular, - ); const graphqlQueryParser = new GraphqlQueryParser( - objectMetadata.fields, + objectMetadataMapItem.fields, objectMetadataMap, ); const withFilterCountQueryBuilder = graphqlQueryParser.applyFilterToBuilder( countQueryBuilder, - objectMetadataItem.nameSingular, + objectMetadataMapItem.nameSingular, args.filter ?? ({} as Filter), ); const selectedFields = graphqlFields(info); const { relations } = graphqlQueryParser.parseSelectedFields( - objectMetadataItem, + objectMetadataMapItem, selectedFields, ); const isForwardPagination = !isDefined(args.before); @@ -105,7 +97,7 @@ export class GraphqlQueryFindManyResolverService { ? await withDeletedCountQueryBuilder.getCount() : 0; - const cursor = this.getCursor(args); + const cursor = getCursor(args); let appliedFilters = args.filter ?? ({} as Filter); @@ -118,7 +110,7 @@ export class GraphqlQueryFindManyResolverService { const cursorArgFilter = computeCursorArgFilter( cursor, orderByWithIdCondition, - objectMetadata.fields, + objectMetadataMapItem.fields, isForwardPagination, ); @@ -131,14 +123,14 @@ export class GraphqlQueryFindManyResolverService { const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( queryBuilder, - objectMetadataItem.nameSingular, + objectMetadataMapItem.nameSingular, appliedFilters, ); const withOrderByQueryBuilder = graphqlQueryParser.applyOrderToBuilder( withFilterQueryBuilder, orderByWithIdCondition, - objectMetadataItem.nameSingular, + objectMetadataMapItem.nameSingular, isForwardPagination, ); @@ -153,11 +145,11 @@ export class GraphqlQueryFindManyResolverService { const objectRecords = formatResult( nonFormattedObjectRecords, - objectMetadata, + objectMetadataMapItem, objectMetadataMap, ); - const { hasNextPage, hasPreviousPage } = this.getPaginationInfo( + const { hasNextPage, hasPreviousPage } = getPaginationInfo( objectRecords, limit, isForwardPagination, @@ -167,14 +159,12 @@ export class GraphqlQueryFindManyResolverService { objectRecords.pop(); } - const processNestedRelationsHelper = new ProcessNestedRelationsHelper( - this.twentyORMGlobalManager, - ); + const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); if (relations) { await processNestedRelationsHelper.processNestedRelations( objectMetadataMap, - objectMetadata, + objectMetadataMapItem, objectRecords, relations, limit, @@ -184,20 +174,25 @@ export class GraphqlQueryFindManyResolverService { } const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap); + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); - return typeORMObjectRecordsParser.createConnection( + const result = typeORMObjectRecordsParser.createConnection({ objectRecords, - objectMetadataItem.nameSingular, - limit, + objectName: objectMetadataMapItem.nameSingular, + take: limit, totalCount, - orderByWithIdCondition, + order: orderByWithIdCondition, hasNextPage, hasPreviousPage, - ); + }); + + return result; } - private validateArgsOrThrow(args: FindManyResolverArgs<any, any>) { + async validate<Filter extends RecordFilter>( + args: FindManyResolverArgs<Filter>, + _options: WorkspaceQueryRunnerOptions, + ): Promise<void> { if (args.first && args.last) { throw new GraphqlQueryRunnerException( 'Cannot provide both first and last', @@ -235,49 +230,4 @@ export class GraphqlQueryFindManyResolverService { ); } } - - private getCursor( - args: FindManyResolverArgs<any, any>, - ): Record<string, any> | undefined { - if (args.after) return decodeCursor(args.after); - if (args.before) return decodeCursor(args.before); - - return undefined; - } - - private addOrderByColumnsToSelect( - order: Record<string, any>, - select: Record<string, boolean>, - ) { - for (const column of Object.keys(order || {})) { - if (!select[column]) { - select[column] = true; - } - } - } - - private addForeingKeyColumnsToSelect( - relations: Record<string, any>, - select: Record<string, boolean>, - objectMetadata: ObjectMetadataMapItem, - ) { - for (const column of Object.keys(relations || {})) { - if (!select[`${column}Id`] && objectMetadata.fields[`${column}Id`]) { - select[`${column}Id`] = true; - } - } - } - - private getPaginationInfo( - objectRecords: any[], - limit: number, - isForwardPagination: boolean, - ) { - const hasMoreRecords = objectRecords.length > limit; - - return { - hasNextPage: isForwardPagination && hasMoreRecords, - hasPreviousPage: !isForwardPagination && hasMoreRecords, - }; - } } diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts index b9e86e420cc9..42c8daae8079 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-find-one-resolver.service.ts @@ -1,5 +1,8 @@ +import { Injectable } from '@nestjs/common'; + import graphqlFields from 'graphql-fields'; +import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; import { Record as IRecord, RecordFilter, @@ -13,28 +16,31 @@ import { GraphqlQueryRunnerExceptionCode, } from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; 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 { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; -import { ObjectRecordsToGraphqlConnectionMapper } from 'src/engine/api/graphql/graphql-query-runner/orm-mappers/object-records-to-graphql-connection.mapper'; -import { getObjectMetadataOrThrow } from 'src/engine/api/graphql/graphql-query-runner/utils/get-object-metadata-or-throw.util'; -import { generateObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; +import { + WorkspaceQueryRunnerException, + WorkspaceQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.exception'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; -export class GraphqlQueryFindOneResolverService { - private twentyORMGlobalManager: TwentyORMGlobalManager; - - constructor(twentyORMGlobalManager: TwentyORMGlobalManager) { - this.twentyORMGlobalManager = twentyORMGlobalManager; - } +@Injectable() +export class GraphqlQueryFindOneResolverService + implements ResolverService<FindOneResolverArgs, IRecord> +{ + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) {} - async findOne< + async resolve< ObjectRecord extends IRecord = IRecord, Filter extends RecordFilter = RecordFilter, >( args: FindOneResolverArgs<Filter>, options: WorkspaceQueryRunnerOptions, - ): Promise<ObjectRecord | undefined> { - const { authContext, objectMetadataItem, info, objectMetadataCollection } = + ): Promise<ObjectRecord> { + const { authContext, objectMetadataMapItem, info, objectMetadataMap } = options; const dataSource = @@ -43,37 +49,28 @@ export class GraphqlQueryFindOneResolverService { ); const repository = dataSource.getRepository( - objectMetadataItem.nameSingular, + objectMetadataMapItem.nameSingular, ); const queryBuilder = repository.createQueryBuilder( - objectMetadataItem.nameSingular, - ); - - const objectMetadataMap = generateObjectMetadataMap( - objectMetadataCollection, - ); - - const objectMetadata = getObjectMetadataOrThrow( - objectMetadataMap, - objectMetadataItem.nameSingular, + objectMetadataMapItem.nameSingular, ); const graphqlQueryParser = new GraphqlQueryParser( - objectMetadata.fields, + objectMetadataMapItem.fields, objectMetadataMap, ); const selectedFields = graphqlFields(info); const { relations } = graphqlQueryParser.parseSelectedFields( - objectMetadataItem, + objectMetadataMapItem, selectedFields, ); const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( queryBuilder, - objectMetadataItem.nameSingular, + objectMetadataMapItem.nameSingular, args.filter ?? ({} as Filter), ); @@ -86,12 +83,10 @@ export class GraphqlQueryFindOneResolverService { const objectRecord = formatResult( nonFormattedObjectRecord, - objectMetadata, + objectMetadataMapItem, objectMetadataMap, ); - const limit = QUERY_MAX_RECORDS; - if (!objectRecord) { throw new GraphqlQueryRunnerException( 'Record not found', @@ -99,32 +94,42 @@ export class GraphqlQueryFindOneResolverService { ); } - const processNestedRelationsHelper = new ProcessNestedRelationsHelper( - this.twentyORMGlobalManager, - ); + const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); const objectRecords = [objectRecord]; if (relations) { await processNestedRelationsHelper.processNestedRelations( objectMetadataMap, - objectMetadata, + objectMetadataMapItem, objectRecords, relations, - limit, + QUERY_MAX_RECORDS, authContext, dataSource, ); } const typeORMObjectRecordsParser = - new ObjectRecordsToGraphqlConnectionMapper(objectMetadataMap); - - return typeORMObjectRecordsParser.processRecord( - objectRecords[0], - objectMetadataItem.nameSingular, - 1, - 1, - ) as ObjectRecord; + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); + + return typeORMObjectRecordsParser.processRecord({ + objectRecord: objectRecords[0], + objectName: objectMetadataMapItem.nameSingular, + take: 1, + totalCount: 1, + }) as ObjectRecord; + } + + async validate<Filter extends RecordFilter>( + args: FindOneResolverArgs<Filter>, + _options: WorkspaceQueryRunnerOptions, + ): Promise<void> { + if (!args.filter || Object.keys(args.filter).length === 0) { + throw new WorkspaceQueryRunnerException( + 'Missing filter argument', + WorkspaceQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); + } } } 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 new file mode 100644 index 000000000000..e6f3273b9c09 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-search-resolver.service.ts @@ -0,0 +1,122 @@ +import { Injectable } from '@nestjs/common'; + +import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; +import { + Record as IRecord, + OrderByDirection, +} 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 { + GraphqlQueryRunnerException, + GraphqlQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; +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'; + +@Injectable() +export class GraphqlQuerySearchResolverService + implements ResolverService<SearchResolverArgs, IConnection<IRecord>> +{ + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly featureFlagService: FeatureFlagService, + ) {} + + async resolve<ObjectRecord extends IRecord = IRecord>( + args: SearchResolverArgs, + options: WorkspaceQueryRunnerOptions, + ): Promise<IConnection<ObjectRecord>> { + const { authContext, objectMetadataItem, objectMetadataMap } = options; + + const repository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace( + authContext.workspace.id, + objectMetadataItem.nameSingular, + ); + + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); + + if (!args.searchInput) { + return typeORMObjectRecordsParser.createConnection({ + objectRecords: [], + objectName: objectMetadataItem.nameSingular, + take: 0, + totalCount: 0, + order: [{ id: OrderByDirection.AscNullsFirst }], + hasNextPage: false, + hasPreviousPage: false, + }); + } + const searchTerms = this.formatSearchTerms(args.searchInput); + + const limit = args?.limit ?? QUERY_MAX_RECORDS; + + const resultsWithTsVector = (await repository + .createQueryBuilder() + .where(`"${SEARCH_VECTOR_FIELD.name}" @@ to_tsquery(:searchTerms)`, { + searchTerms, + }) + .orderBy( + `ts_rank("${SEARCH_VECTOR_FIELD.name}", to_tsquery(:searchTerms))`, + 'DESC', + ) + .setParameter('searchTerms', searchTerms) + .take(limit) + .getMany()) as ObjectRecord[]; + + const objectRecords = await repository.formatResult(resultsWithTsVector); + + const totalCount = await repository.count(); + const order = undefined; + + return typeORMObjectRecordsParser.createConnection({ + objectRecords: objectRecords ?? [], + objectName: objectMetadataItem.nameSingular, + take: limit, + totalCount, + order, + hasNextPage: false, + hasPreviousPage: false, + }); + } + + private formatSearchTerms(searchTerm: string) { + const words = searchTerm.trim().split(/\s+/); + const formattedWords = words.map((word) => { + const escapedWord = word.replace(/[\\:'&|!()]/g, '\\$&'); + + return `${escapedWord}:*`; + }); + + return formattedWords.join(' | '); + } + + async validate( + _args: SearchResolverArgs, + options: WorkspaceQueryRunnerOptions, + ): Promise<void> { + const featureFlagsForWorkspace = + await this.featureFlagService.getWorkspaceFeatureFlags( + options.authContext.workspace.id, + ); + + const isQueryRunnerTwentyORMEnabled = + featureFlagsForWorkspace.IS_QUERY_RUNNER_TWENTY_ORM_ENABLED; + + const isSearchEnabled = featureFlagsForWorkspace.IS_SEARCH_ENABLED; + + if (!isQueryRunnerTwentyORMEnabled || !isSearchEnabled) { + throw new GraphqlQueryRunnerException( + 'This endpoint is not available yet, please use findMany instead.', + GraphqlQueryRunnerExceptionCode.INVALID_QUERY_INPUT, + ); + } + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts new file mode 100644 index 000000000000..270e6fd8196f --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-many-resolver.service.ts @@ -0,0 +1,117 @@ +import { Injectable } from '@nestjs/common'; + +import graphqlFields from 'graphql-fields'; + +import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; +import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; +import { UpdateManyResolverArgs } 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 { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; +import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; +import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; + +@Injectable() +export class GraphqlQueryUpdateManyResolverService + implements ResolverService<UpdateManyResolverArgs, IRecord[]> +{ + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) {} + + async resolve<ObjectRecord extends IRecord = IRecord>( + args: UpdateManyResolverArgs<Partial<ObjectRecord>>, + options: WorkspaceQueryRunnerOptions, + ): Promise<ObjectRecord[]> { + const { authContext, objectMetadataMapItem, objectMetadataMap, info } = + options; + + const dataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace( + authContext.workspace.id, + ); + + const repository = dataSource.getRepository( + objectMetadataMapItem.nameSingular, + ); + + const graphqlQueryParser = new GraphqlQueryParser( + objectMetadataMapItem.fields, + objectMetadataMap, + ); + + const selectedFields = graphqlFields(info); + + const { relations } = graphqlQueryParser.parseSelectedFields( + objectMetadataMapItem, + selectedFields, + ); + + const queryBuilder = repository.createQueryBuilder( + objectMetadataMapItem.nameSingular, + ); + + const withFilterQueryBuilder = graphqlQueryParser.applyFilterToBuilder( + queryBuilder, + objectMetadataMapItem.nameSingular, + args.filter, + ); + + const data = formatData(args.data, objectMetadataMapItem); + + const nonFormattedUpdatedObjectRecords = await withFilterQueryBuilder + .update(data) + .returning('*') + .execute(); + + const updatedRecords = formatResult( + nonFormattedUpdatedObjectRecords.raw, + objectMetadataMapItem, + objectMetadataMap, + ); + + const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); + + if (relations) { + await processNestedRelationsHelper.processNestedRelations( + objectMetadataMap, + objectMetadataMapItem, + updatedRecords, + relations, + QUERY_MAX_RECORDS, + authContext, + dataSource, + ); + } + + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); + + return updatedRecords.map((record: ObjectRecord) => + typeORMObjectRecordsParser.processRecord({ + objectRecord: record, + objectName: objectMetadataMapItem.nameSingular, + take: 1, + totalCount: 1, + }), + ); + } + + async validate<ObjectRecord extends IRecord = IRecord>( + args: UpdateManyResolverArgs<Partial<ObjectRecord>>, + options: WorkspaceQueryRunnerOptions, + ): Promise<void> { + assertMutationNotOnRemoteObject(options.objectMetadataMapItem); + if (!args.filter) { + throw new Error('Filter is required'); + } + + args.filter.id?.in?.forEach((id: string) => assertIsValidUuid(id)); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts new file mode 100644 index 000000000000..8fe4396d2413 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/resolvers/graphql-query-update-one-resolver.service.ts @@ -0,0 +1,121 @@ +import { Injectable } from '@nestjs/common'; + +import graphqlFields from 'graphql-fields'; + +import { ResolverService } from 'src/engine/api/graphql/graphql-query-runner/interfaces/resolver-service.interface'; +import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; +import { UpdateOneResolverArgs } 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 { + GraphqlQueryRunnerException, + GraphqlQueryRunnerExceptionCode, +} from 'src/engine/api/graphql/graphql-query-runner/errors/graphql-query-runner.exception'; +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 { ProcessNestedRelationsHelper } from 'src/engine/api/graphql/graphql-query-runner/helpers/process-nested-relations.helper'; +import { assertIsValidUuid } from 'src/engine/api/graphql/workspace-query-runner/utils/assert-is-valid-uuid.util'; +import { assertMutationNotOnRemoteObject } from 'src/engine/metadata-modules/object-metadata/utils/assert-mutation-not-on-remote-object.util'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { formatData } from 'src/engine/twenty-orm/utils/format-data.util'; +import { formatResult } from 'src/engine/twenty-orm/utils/format-result.util'; + +@Injectable() +export class GraphqlQueryUpdateOneResolverService + implements ResolverService<UpdateOneResolverArgs, IRecord> +{ + constructor( + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + ) {} + + async resolve<ObjectRecord extends IRecord = IRecord>( + args: UpdateOneResolverArgs<Partial<ObjectRecord>>, + options: WorkspaceQueryRunnerOptions, + ): Promise<ObjectRecord> { + const { authContext, objectMetadataMapItem, objectMetadataMap, info } = + options; + + const dataSource = + await this.twentyORMGlobalManager.getDataSourceForWorkspace( + authContext.workspace.id, + ); + + const repository = dataSource.getRepository( + objectMetadataMapItem.nameSingular, + ); + + const graphqlQueryParser = new GraphqlQueryParser( + objectMetadataMapItem.fields, + objectMetadataMap, + ); + + const selectedFields = graphqlFields(info); + + const { relations } = graphqlQueryParser.parseSelectedFields( + objectMetadataMapItem, + selectedFields, + ); + + const queryBuilder = repository.createQueryBuilder( + objectMetadataMapItem.nameSingular, + ); + + const data = formatData(args.data, objectMetadataMapItem); + + const result = await queryBuilder + .update(data) + .where({ id: args.id }) + .returning('*') + .execute(); + + const nonFormattedUpdatedObjectRecords = result.raw; + + const updatedRecords = formatResult( + nonFormattedUpdatedObjectRecords, + objectMetadataMapItem, + objectMetadataMap, + ); + + if (updatedRecords.length === 0) { + throw new GraphqlQueryRunnerException( + 'Record not found', + GraphqlQueryRunnerExceptionCode.RECORD_NOT_FOUND, + ); + } + + const updatedRecord = updatedRecords[0] as ObjectRecord; + + const processNestedRelationsHelper = new ProcessNestedRelationsHelper(); + + if (relations) { + await processNestedRelationsHelper.processNestedRelations( + objectMetadataMap, + objectMetadataMapItem, + [updatedRecord], + relations, + QUERY_MAX_RECORDS, + authContext, + dataSource, + ); + } + + const typeORMObjectRecordsParser = + new ObjectRecordsToGraphqlConnectionHelper(objectMetadataMap); + + return typeORMObjectRecordsParser.processRecord<ObjectRecord>({ + objectRecord: updatedRecord, + objectName: objectMetadataMapItem.nameSingular, + take: 1, + totalCount: 1, + }); + } + + async validate<ObjectRecord extends IRecord = IRecord>( + args: UpdateOneResolverArgs<Partial<ObjectRecord>>, + options: WorkspaceQueryRunnerOptions, + ): Promise<void> { + assertMutationNotOnRemoteObject(options.objectMetadataMapItem); + assertIsValidUuid(args.id); + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service.ts new file mode 100644 index 000000000000..8cb2c9cc7a04 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/services/api-event-emitter.service.ts @@ -0,0 +1,137 @@ +import { Injectable } from '@nestjs/common'; + +import { Record as IRecord } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; + +import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; + +@Injectable() +export class ApiEventEmitterService { + constructor(private readonly workspaceEventEmitter: WorkspaceEventEmitter) {} + + public emitCreateEvents<T extends IRecord>( + records: T[], + authContext: AuthContext, + objectMetadataItem: ObjectMetadataInterface, + ): void { + this.workspaceEventEmitter.emit( + `${objectMetadataItem.nameSingular}.created`, + records.map((record) => ({ + userId: authContext.user?.id, + recordId: record.id, + objectMetadata: objectMetadataItem, + properties: { + before: null, + after: this.removeGraphQLAndNestedProperties(record), + }, + })), + authContext.workspace.id, + ); + } + + public emitUpdateEvents<T extends IRecord>( + existingRecords: T[], + records: T[], + updatedFields: string[], + authContext: AuthContext, + objectMetadataItem: ObjectMetadataInterface, + ): void { + const mappedExistingRecords = existingRecords.reduce( + (acc, { id, ...record }) => ({ + ...acc, + [id]: record, + }), + {}, + ); + + this.workspaceEventEmitter.emit( + `${objectMetadataItem.nameSingular}.updated`, + records.map((record) => { + return { + userId: authContext.user?.id, + recordId: record.id, + objectMetadata: objectMetadataItem, + properties: { + before: mappedExistingRecords[record.id] + ? this.removeGraphQLAndNestedProperties( + mappedExistingRecords[record.id], + ) + : undefined, + after: this.removeGraphQLAndNestedProperties(record), + updatedFields, + }, + }; + }), + authContext.workspace.id, + ); + } + + public emitDeletedEvents<T extends IRecord>( + records: T[], + authContext: AuthContext, + objectMetadataItem: ObjectMetadataInterface, + ): void { + this.workspaceEventEmitter.emit( + `${objectMetadataItem.nameSingular}.deleted`, + records.map((record) => { + return { + userId: authContext.user?.id, + recordId: record.id, + objectMetadata: objectMetadataItem, + properties: { + before: this.removeGraphQLAndNestedProperties(record), + after: null, + }, + }; + }), + authContext.workspace.id, + ); + } + + public emitDestroyEvents<T extends IRecord>( + records: T[], + authContext: AuthContext, + objectMetadataItem: ObjectMetadataInterface, + ): void { + this.workspaceEventEmitter.emit( + `${objectMetadataItem.nameSingular}.destroyed`, + records.map((record) => { + return { + userId: authContext.user?.id, + recordId: record.id, + objectMetadata: objectMetadataItem, + properties: { + before: this.removeGraphQLAndNestedProperties(record), + after: null, + }, + }; + }), + authContext.workspace.id, + ); + } + + private removeGraphQLAndNestedProperties<ObjectRecord extends IRecord>( + record: ObjectRecord, + ) { + if (!record) { + return {}; + } + + const sanitizedRecord = {}; + + for (const [key, value] of Object.entries(record)) { + if (value && typeof value === 'object' && value['edges']) { + continue; + } + + if (key === '__typename') { + continue; + } + + sanitizedRecord[key] = value; + } + + return sanitizedRecord; + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts index bf8eb52d0a57..bd27522ce1b2 100644 --- a/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts +++ b/packages/twenty-server/src/engine/api/graphql/graphql-query-runner/utils/cursors.util.ts @@ -2,6 +2,7 @@ import { Record as IRecord, RecordOrderBy, } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { FindManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { GraphqlQueryRunnerException, @@ -44,3 +45,25 @@ export const encodeCursor = <ObjectRecord extends IRecord = IRecord>( return Buffer.from(JSON.stringify(cursorData)).toString('base64'); }; + +export const getCursor = ( + args: FindManyResolverArgs<any, any>, +): Record<string, any> | undefined => { + if (args.after) return decodeCursor(args.after); + if (args.before) return decodeCursor(args.before); + + return undefined; +}; + +export const getPaginationInfo = ( + objectRecords: any[], + limit: number, + isForwardPagination: boolean, +) => { + const hasMoreRecords = objectRecords.length > limit; + + return { + hasNextPage: isForwardPagination && hasMoreRecords, + hasPreviousPage: !isForwardPagination && hasMoreRecords, + }; +}; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts index e10fab44e300..58c97cd2675f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-builder/factories/factories.ts @@ -2,18 +2,18 @@ import { ForeignDataWrapperServerQueryFactory } from 'src/engine/api/graphql/wor import { ArgsAliasFactory } from './args-alias.factory'; import { ArgsStringFactory } from './args-string.factory'; -import { RelationFieldAliasFactory } from './relation-field-alias.factory'; import { CreateManyQueryFactory } from './create-many-query.factory'; +import { DeleteManyQueryFactory } from './delete-many-query.factory'; import { DeleteOneQueryFactory } from './delete-one-query.factory'; import { FieldAliasFactory } from './field-alias.factory'; import { FieldsStringFactory } from './fields-string.factory'; +import { FindDuplicatesQueryFactory } from './find-duplicates-query.factory'; import { FindManyQueryFactory } from './find-many-query.factory'; import { FindOneQueryFactory } from './find-one-query.factory'; -import { UpdateOneQueryFactory } from './update-one-query.factory'; -import { UpdateManyQueryFactory } from './update-many-query.factory'; -import { DeleteManyQueryFactory } from './delete-many-query.factory'; -import { FindDuplicatesQueryFactory } from './find-duplicates-query.factory'; import { RecordPositionQueryFactory } from './record-position-query.factory'; +import { RelationFieldAliasFactory } from './relation-field-alias.factory'; +import { UpdateManyQueryFactory } from './update-many-query.factory'; +import { UpdateOneQueryFactory } from './update-one-query.factory'; export const workspaceQueryBuilderFactories = [ ArgsAliasFactory, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface.ts index 45883f99ddf3..d960c3d45a7f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface.ts @@ -4,6 +4,10 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metada import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { + ObjectMetadataMap, + ObjectMetadataMapItem, +} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; export interface WorkspaceQueryRunnerOptions { authContext: AuthContext; @@ -11,4 +15,6 @@ export interface WorkspaceQueryRunnerOptions { objectMetadataItem: ObjectMetadataInterface; fieldMetadataCollection: FieldMetadataInterface[]; objectMetadataCollection: ObjectMetadataInterface[]; + objectMetadataMap: ObjectMetadataMap; + objectMetadataMapItem: ObjectMetadataMapItem; } 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 d0bbc6872c06..a1b43eb22034 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 @@ -20,6 +20,7 @@ export enum CallWebhookJobsJobOperation { create = 'create', update = 'update', delete = 'delete', + destroy = 'destroy', } export type CallWebhookJobsJobData = { 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 bbaff49a4951..e03c83204e85 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 @@ -1,6 +1,7 @@ import { HttpService } from '@nestjs/axios'; import { Logger } from '@nestjs/common'; +import { AnalyticsService } from 'src/engine/core-modules/analytics/analytics.service'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; @@ -18,17 +19,41 @@ export type CallWebhookJobData = { @Processor(MessageQueue.webhookQueue) export class CallWebhookJob { private readonly logger = new Logger(CallWebhookJob.name); - - constructor(private readonly httpService: HttpService) {} + constructor( + private readonly httpService: HttpService, + private readonly analyticsService: AnalyticsService, + ) {} @Process(CallWebhookJob.name) async handle(data: CallWebhookJobData): Promise<void> { try { - await this.httpService.axiosRef.post(data.targetUrl, data); - this.logger.log( - `CallWebhookJob successfully called on targetUrl '${data.targetUrl}'`, + const response = await this.httpService.axiosRef.post( + data.targetUrl, + data, ); + const eventInput = { + action: 'webhook.response', + payload: { + status: response.status, + url: data.targetUrl, + webhookId: data.webhookId, + eventName: data.eventName, + }, + }; + + this.analyticsService.create(eventInput, 'webhook', data.workspaceId); } catch (err) { + const eventInput = { + action: 'webhook.response', + payload: { + status: err.response.status, + url: data.targetUrl, + webhookId: data.webhookId, + eventName: data.eventName, + }, + }; + + this.analyticsService.create(eventInput, 'webhook', data.workspaceId); this.logger.error( `Error calling webhook on targetUrl '${data.targetUrl}': ${err}`, ); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/workspace-query-runner-job.module.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/workspace-query-runner-job.module.ts index 8104cf075cf3..c7e93901b008 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/workspace-query-runner-job.module.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/jobs/workspace-query-runner-job.module.ts @@ -5,6 +5,7 @@ import { CallWebhookJobsJob } from 'src/engine/api/graphql/workspace-query-runne import { CallWebhookJob } from 'src/engine/api/graphql/workspace-query-runner/jobs/call-webhook.job'; import { RecordPositionBackfillJob } from 'src/engine/api/graphql/workspace-query-runner/jobs/record-position-backfill.job'; import { RecordPositionBackfillModule } from 'src/engine/api/graphql/workspace-query-runner/services/record-position-backfill-module'; +import { AnalyticsModule } from 'src/engine/core-modules/analytics/analytics.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; @@ -14,6 +15,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works DataSourceModule, RecordPositionBackfillModule, HttpModule, + AnalyticsModule, ], providers: [CallWebhookJobsJob, CallWebhookJob, RecordPositionBackfillJob], }) diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts index eb9ddbf06a6f..8eb8be61f774 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/listeners/entity-events-to-db.listener.ts @@ -49,6 +49,13 @@ export class EntityEventsToDbListener { return this.handle(payload); } + @OnEvent('*.destroyed') + async handleDestroy( + payload: WorkspaceEventBatch<ObjectRecordUpdateEvent<any>>, + ) { + return this.handle(payload); + } + private async handle(payload: WorkspaceEventBatch<ObjectRecordBaseEvent>) { const filteredEvents = payload.events.filter( (event) => event.objectMetadata?.isAuditLogged, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type.ts b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type.ts index f155475877e0..034c73bda552 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-query-runner/workspace-query-hook/types/workspace-query-hook.type.ts @@ -4,10 +4,12 @@ import { DeleteManyResolverArgs, DeleteOneResolverArgs, DestroyManyResolverArgs, + DestroyOneResolverArgs, FindDuplicatesResolverArgs, FindManyResolverArgs, FindOneResolverArgs, RestoreManyResolverArgs, + SearchResolverArgs, UpdateManyResolverArgs, UpdateOneResolverArgs, } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; @@ -39,4 +41,8 @@ export type WorkspacePreQueryHookPayload<T> = T extends 'createMany' ? RestoreManyResolverArgs : T extends 'destroyMany' ? DestroyManyResolverArgs - : never; + : T extends 'destroyOne' + ? DestroyOneResolverArgs + : T extends 'search' + ? SearchResolverArgs + : never; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts index 872ab7906fca..06a8d5507b2d 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-many-resolver.factory.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { CreateManyResolverArgs, @@ -32,12 +33,14 @@ export class CreateManyResolverFactory return async (_source, args, _context, info) => { try { - const options = { + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, objectMetadataItem: internalContext.objectMetadataItem, info, fieldMetadataCollection: internalContext.fieldMetadataCollection, objectMetadataCollection: internalContext.objectMetadataCollection, + objectMetadataMap: internalContext.objectMetadataMap, + objectMetadataMapItem: internalContext.objectMetadataMapItem, }; const isQueryRunnerTwentyORMEnabled = diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts index edf9206a1cde..5922d05550d6 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/create-one-resolver.factory.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { CreateOneResolverArgs, @@ -32,12 +33,14 @@ export class CreateOneResolverFactory return async (_source, args, _context, info) => { try { - const options = { + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, objectMetadataItem: internalContext.objectMetadataItem, info, fieldMetadataCollection: internalContext.fieldMetadataCollection, objectMetadataCollection: internalContext.objectMetadataCollection, + objectMetadataMap: internalContext.objectMetadataMap, + objectMetadataMapItem: internalContext.objectMetadataMapItem, }; const isQueryRunnerTwentyORMEnabled = @@ -50,13 +53,7 @@ export class CreateOneResolverFactory return await this.graphqlQueryRunnerService.createOne(args, options); } - return await this.workspaceQueryRunnerService.createOne(args, { - authContext: internalContext.authContext, - objectMetadataItem: internalContext.objectMetadataItem, - info, - fieldMetadataCollection: internalContext.fieldMetadataCollection, - objectMetadataCollection: internalContext.objectMetadataCollection, - }); + return await this.workspaceQueryRunnerService.createOne(args, options); } catch (error) { workspaceQueryRunnerGraphqlApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts index a8d36f3e4900..4a32ad5ea1c5 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-many-resolver.factory.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { DeleteManyResolverArgs, @@ -7,8 +8,11 @@ import { } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; +import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service'; import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; @Injectable() export class DeleteManyResolverFactory @@ -18,6 +22,8 @@ export class DeleteManyResolverFactory constructor( private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService, + private readonly featureFlagService: FeatureFlagService, + private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService, ) {} create( @@ -27,13 +33,27 @@ export class DeleteManyResolverFactory return async (_source, args, context, info) => { try { - return await this.workspaceQueryRunnerService.deleteMany(args, { + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, objectMetadataItem: internalContext.objectMetadataItem, info, fieldMetadataCollection: internalContext.fieldMetadataCollection, objectMetadataCollection: internalContext.objectMetadataCollection, - }); + objectMetadataMap: internalContext.objectMetadataMap, + objectMetadataMapItem: internalContext.objectMetadataMapItem, + }; + + const isQueryRunnerTwentyORMEnabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsQueryRunnerTwentyORMEnabled, + internalContext.authContext.workspace.id, + ); + + if (isQueryRunnerTwentyORMEnabled) { + return await this.graphqlQueryRunnerService.deleteMany(args, options); + } + + return await this.workspaceQueryRunnerService.deleteMany(args, options); } catch (error) { workspaceQueryRunnerGraphqlApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts index 93f249cdd6ac..d58ebe02fd56 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/delete-one-resolver.factory.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { DeleteOneResolverArgs, @@ -7,8 +8,11 @@ import { } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; +import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service'; import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; @Injectable() export class DeleteOneResolverFactory @@ -18,6 +22,8 @@ export class DeleteOneResolverFactory constructor( private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService, + private readonly featureFlagService: FeatureFlagService, + private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService, ) {} create( @@ -27,13 +33,27 @@ export class DeleteOneResolverFactory return async (_source, args, context, info) => { try { - return await this.workspaceQueryRunnerService.deleteOne(args, { + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, objectMetadataItem: internalContext.objectMetadataItem, info, fieldMetadataCollection: internalContext.fieldMetadataCollection, objectMetadataCollection: internalContext.objectMetadataCollection, - }); + objectMetadataMap: internalContext.objectMetadataMap, + objectMetadataMapItem: internalContext.objectMetadataMapItem, + }; + + const isQueryRunnerTwentyORMEnabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsQueryRunnerTwentyORMEnabled, + internalContext.authContext.workspace.id, + ); + + if (isQueryRunnerTwentyORMEnabled) { + return await this.graphqlQueryRunnerService.deleteOne(args, options); + } + + return await this.workspaceQueryRunnerService.deleteOne(args, options); } catch (error) { workspaceQueryRunnerGraphqlApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts index 4a064a406b9b..e90b93309c6f 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { DestroyManyResolverArgs, @@ -7,8 +8,11 @@ import { } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; +import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service'; import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; @Injectable() export class DestroyManyResolverFactory @@ -18,6 +22,8 @@ export class DestroyManyResolverFactory constructor( private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService, + private readonly featureFlagService: FeatureFlagService, + private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService, ) {} create( @@ -27,13 +33,33 @@ export class DestroyManyResolverFactory return async (_source, args, context, info) => { try { - return await this.workspaceQueryRunnerService.destroyMany(args, { + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, objectMetadataItem: internalContext.objectMetadataItem, info, fieldMetadataCollection: internalContext.fieldMetadataCollection, objectMetadataCollection: internalContext.objectMetadataCollection, - }); + objectMetadataMap: internalContext.objectMetadataMap, + objectMetadataMapItem: internalContext.objectMetadataMapItem, + }; + + const isQueryRunnerTwentyORMEnabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsQueryRunnerTwentyORMEnabled, + internalContext.authContext.workspace.id, + ); + + if (isQueryRunnerTwentyORMEnabled) { + return await this.graphqlQueryRunnerService.destroyMany( + args, + options, + ); + } + + return await this.workspaceQueryRunnerService.destroyMany( + args, + options, + ); } catch (error) { workspaceQueryRunnerGraphqlApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts index 4c204d6e8c0c..bb1e2aaaa9ba 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { DestroyOneResolverArgs, @@ -27,13 +28,17 @@ export class DestroyOneResolverFactory return async (_source, args, context, info) => { try { - return await this.graphQLQueryRunnerService.destroyOne(args, { + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, objectMetadataItem: internalContext.objectMetadataItem, info, fieldMetadataCollection: internalContext.fieldMetadataCollection, objectMetadataCollection: internalContext.objectMetadataCollection, - }); + objectMetadataMap: internalContext.objectMetadataMap, + objectMetadataMapItem: internalContext.objectMetadataMapItem, + }; + + return await this.graphQLQueryRunnerService.destroyOne(args, options); } catch (error) { workspaceQueryRunnerGraphqlApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/factories.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/factories.ts index 1724242e866d..b728ef8988e2 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/factories.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/factories.ts @@ -1,6 +1,7 @@ import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory'; import { DestroyOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory'; import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory'; +import { SearchResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/search-resolver-factory'; import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory'; import { CreateManyResolverFactory } from './create-many-resolver.factory'; @@ -25,6 +26,7 @@ export const workspaceResolverBuilderFactories = [ DestroyOneResolverFactory, DestroyManyResolverFactory, RestoreManyResolverFactory, + SearchResolverFactory, ]; export const workspaceResolverBuilderMethodNames = { @@ -32,6 +34,7 @@ export const workspaceResolverBuilderMethodNames = { FindManyResolverFactory.methodName, FindOneResolverFactory.methodName, FindDuplicatesResolverFactory.methodName, + SearchResolverFactory.methodName, ], mutations: [ CreateManyResolverFactory.methodName, diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts index 0a1494efb666..f8b57ad22cca 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-duplicates-resolver.factory.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { FindDuplicatesResolverArgs, @@ -7,8 +8,11 @@ import { } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; +import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service'; import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; @Injectable() export class FindDuplicatesResolverFactory @@ -18,6 +22,8 @@ export class FindDuplicatesResolverFactory constructor( private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService, + private readonly featureFlagService: FeatureFlagService, + private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService, ) {} create( @@ -27,13 +33,33 @@ export class FindDuplicatesResolverFactory return async (_source, args, context, info) => { try { - return await this.workspaceQueryRunnerService.findDuplicates(args, { + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, objectMetadataItem: internalContext.objectMetadataItem, info, fieldMetadataCollection: internalContext.fieldMetadataCollection, objectMetadataCollection: internalContext.objectMetadataCollection, - }); + objectMetadataMap: internalContext.objectMetadataMap, + objectMetadataMapItem: internalContext.objectMetadataMapItem, + }; + + const isQueryRunnerTwentyORMEnabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsQueryRunnerTwentyORMEnabled, + internalContext.authContext.workspace.id, + ); + + if (isQueryRunnerTwentyORMEnabled) { + return await this.graphqlQueryRunnerService.findDuplicates( + args, + options, + ); + } + + return await this.workspaceQueryRunnerService.findDuplicates( + args, + options, + ); } catch (error) { workspaceQueryRunnerGraphqlApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts index 2dd452a2976c..c695079e2f62 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-many-resolver.factory.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { FindManyResolverArgs, @@ -27,12 +28,14 @@ export class FindManyResolverFactory return async (_source, args, _context, info) => { try { - const options = { + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, objectMetadataItem: internalContext.objectMetadataItem, info, fieldMetadataCollection: internalContext.fieldMetadataCollection, objectMetadataCollection: internalContext.objectMetadataCollection, + objectMetadataMap: internalContext.objectMetadataMap, + objectMetadataMapItem: internalContext.objectMetadataMapItem, }; return await this.graphqlQueryRunnerService.findMany(args, options); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts index 3dbbc2330d06..00845e841710 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/find-one-resolver.factory.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { FindOneResolverArgs, @@ -27,12 +28,14 @@ export class FindOneResolverFactory return async (_source, args, _context, info) => { try { - const options = { + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, objectMetadataItem: internalContext.objectMetadataItem, info, fieldMetadataCollection: internalContext.fieldMetadataCollection, objectMetadataCollection: internalContext.objectMetadataCollection, + objectMetadataMap: internalContext.objectMetadataMap, + objectMetadataMapItem: internalContext.objectMetadataMapItem, }; return await this.graphqlQueryRunnerService.findOne(args, options); diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts index ceba95306aed..d92210040535 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { Resolver, @@ -7,8 +8,11 @@ import { } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; +import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service'; import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; @Injectable() export class RestoreManyResolverFactory @@ -18,6 +22,8 @@ export class RestoreManyResolverFactory constructor( private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService, + private readonly featureFlagService: FeatureFlagService, + private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService, ) {} create( @@ -27,13 +33,33 @@ export class RestoreManyResolverFactory return async (_source, args, context, info) => { try { - return await this.workspaceQueryRunnerService.restoreMany(args, { + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, objectMetadataItem: internalContext.objectMetadataItem, info, fieldMetadataCollection: internalContext.fieldMetadataCollection, objectMetadataCollection: internalContext.objectMetadataCollection, - }); + objectMetadataMap: internalContext.objectMetadataMap, + objectMetadataMapItem: internalContext.objectMetadataMapItem, + }; + + const isQueryRunnerTwentyORMEnabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsQueryRunnerTwentyORMEnabled, + internalContext.authContext.workspace.id, + ); + + if (isQueryRunnerTwentyORMEnabled) { + return await this.graphqlQueryRunnerService.restoreMany( + args, + options, + ); + } + + return await this.workspaceQueryRunnerService.restoreMany( + args, + options, + ); } catch (error) { workspaceQueryRunnerGraphqlApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/search-resolver-factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/search-resolver-factory.ts new file mode 100644 index 000000000000..9d559b656194 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/search-resolver-factory.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@nestjs/common'; + +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; +import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; +import { + Resolver, + SearchResolverArgs, +} from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; +import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; + +import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service'; +import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; + +@Injectable() +export class SearchResolverFactory + implements WorkspaceResolverBuilderFactoryInterface +{ + public static methodName = 'search' as const; + + constructor( + private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService, + ) {} + + create(context: WorkspaceSchemaBuilderContext): Resolver<SearchResolverArgs> { + const internalContext = context; + + return async (_source, args, _context, info) => { + try { + const options: WorkspaceQueryRunnerOptions = { + authContext: internalContext.authContext, + objectMetadataItem: internalContext.objectMetadataItem, + info, + fieldMetadataCollection: internalContext.fieldMetadataCollection, + objectMetadataCollection: internalContext.objectMetadataCollection, + objectMetadataMap: internalContext.objectMetadataMap, + objectMetadataMapItem: internalContext.objectMetadataMapItem, + }; + + return await this.graphqlQueryRunnerService.search(args, options); + } catch (error) { + workspaceQueryRunnerGraphqlApiExceptionHandler(error); + } + }; + } +} diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts index c2328d12ba05..11027e4cc4fd 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { Resolver, @@ -7,8 +8,11 @@ import { } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; +import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service'; import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; @Injectable() export class UpdateManyResolverFactory @@ -18,6 +22,8 @@ export class UpdateManyResolverFactory constructor( private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService, + private readonly featureFlagService: FeatureFlagService, + private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService, ) {} create( @@ -27,13 +33,27 @@ export class UpdateManyResolverFactory return async (_source, args, context, info) => { try { - return await this.workspaceQueryRunnerService.updateMany(args, { + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, objectMetadataItem: internalContext.objectMetadataItem, info, fieldMetadataCollection: internalContext.fieldMetadataCollection, objectMetadataCollection: internalContext.objectMetadataCollection, - }); + objectMetadataMap: internalContext.objectMetadataMap, + objectMetadataMapItem: internalContext.objectMetadataMapItem, + }; + + const isQueryRunnerTwentyORMEnabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsQueryRunnerTwentyORMEnabled, + internalContext.authContext.workspace.id, + ); + + if (isQueryRunnerTwentyORMEnabled) { + return await this.graphqlQueryRunnerService.updateMany(args, options); + } + + return await this.workspaceQueryRunnerService.updateMany(args, options); } catch (error) { workspaceQueryRunnerGraphqlApiExceptionHandler(error); } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts index c7a7dc6bacd6..13a2e4f714d1 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/factories/update-one-resolver.factory.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; +import { WorkspaceQueryRunnerOptions } from 'src/engine/api/graphql/workspace-query-runner/interfaces/query-runner-option.interface'; import { WorkspaceResolverBuilderFactoryInterface } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolver-builder-factory.interface'; import { Resolver, @@ -7,8 +8,11 @@ import { } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface'; import { WorkspaceSchemaBuilderContext } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface'; +import { GraphqlQueryRunnerService } from 'src/engine/api/graphql/graphql-query-runner/graphql-query-runner.service'; import { workspaceQueryRunnerGraphqlApiExceptionHandler } from 'src/engine/api/graphql/workspace-query-runner/utils/workspace-query-runner-graphql-api-exception-handler.util'; import { WorkspaceQueryRunnerService } from 'src/engine/api/graphql/workspace-query-runner/workspace-query-runner.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; @Injectable() export class UpdateOneResolverFactory @@ -18,6 +22,8 @@ export class UpdateOneResolverFactory constructor( private readonly workspaceQueryRunnerService: WorkspaceQueryRunnerService, + private readonly featureFlagService: FeatureFlagService, + private readonly graphqlQueryRunnerService: GraphqlQueryRunnerService, ) {} create( @@ -27,13 +33,27 @@ export class UpdateOneResolverFactory return async (_source, args, context, info) => { try { - return await this.workspaceQueryRunnerService.updateOne(args, { + const options: WorkspaceQueryRunnerOptions = { authContext: internalContext.authContext, objectMetadataItem: internalContext.objectMetadataItem, info, fieldMetadataCollection: internalContext.fieldMetadataCollection, objectMetadataCollection: internalContext.objectMetadataCollection, - }); + objectMetadataMap: internalContext.objectMetadataMap, + objectMetadataMapItem: internalContext.objectMetadataMapItem, + }; + + const isQueryRunnerTwentyORMEnabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsQueryRunnerTwentyORMEnabled, + internalContext.authContext.workspace.id, + ); + + if (isQueryRunnerTwentyORMEnabled) { + return await this.graphqlQueryRunnerService.updateOne(args, options); + } + + return await this.workspaceQueryRunnerService.updateOne(args, options); } catch (error) { workspaceQueryRunnerGraphqlApiExceptionHandler(error); } 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 4e2a0af85196..219b185c451e 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 @@ -22,6 +22,7 @@ export enum ResolverArgsType { DeleteMany = 'DeleteMany', RestoreMany = 'RestoreMany', DestroyMany = 'DestroyMany', + DestroyOne = 'DestroyOne', } export interface FindManyResolverArgs< @@ -47,6 +48,11 @@ export interface FindDuplicatesResolverArgs< data?: Data[]; } +export interface SearchResolverArgs { + searchInput?: string; + limit?: number; +} + export interface CreateOneResolverArgs< Data extends Partial<Record> = Partial<Record>, > { @@ -122,4 +128,5 @@ export type ResolverArgs = | UpdateManyResolverArgs | UpdateOneResolverArgs | DestroyManyResolverArgs - | RestoreManyResolverArgs; + | RestoreManyResolverArgs + | SearchResolverArgs; diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts index 6c06b85d9ff0..a652e3065c81 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-resolver-builder/workspace-resolver.factory.ts @@ -8,8 +8,10 @@ import { DeleteManyResolverFactory } from 'src/engine/api/graphql/workspace-reso import { DestroyManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-many-resolver.factory'; import { DestroyOneResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/destroy-one-resolver.factory'; import { RestoreManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/restore-many-resolver.factory'; +import { SearchResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/search-resolver-factory'; import { UpdateManyResolverFactory } from 'src/engine/api/graphql/workspace-resolver-builder/factories/update-many-resolver.factory'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { ObjectMetadataMap } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; import { getResolverName } from 'src/engine/utils/get-resolver-name.util'; import { CreateManyResolverFactory } from './factories/create-many-resolver.factory'; @@ -42,11 +44,13 @@ export class WorkspaceResolverFactory { private readonly deleteManyResolverFactory: DeleteManyResolverFactory, private readonly restoreManyResolverFactory: RestoreManyResolverFactory, private readonly destroyManyResolverFactory: DestroyManyResolverFactory, + private readonly searchResolverFactory: SearchResolverFactory, ) {} async create( authContext: AuthContext, objectMetadataCollection: ObjectMetadataInterface[], + objectMetadataMap: ObjectMetadataMap, workspaceResolverBuilderMethods: WorkspaceResolverBuilderMethods, ): Promise<IResolvers> { const factories = new Map< @@ -65,6 +69,7 @@ export class WorkspaceResolverFactory { ['deleteMany', this.deleteManyResolverFactory], ['restoreMany', this.restoreManyResolverFactory], ['destroyMany', this.destroyManyResolverFactory], + ['search', this.searchResolverFactory], ]); const resolvers: IResolvers = { Query: {}, @@ -91,7 +96,9 @@ export class WorkspaceResolverFactory { authContext, objectMetadataItem: objectMetadata, fieldMetadataCollection: objectMetadata.fields, - objectMetadataCollection: objectMetadataCollection, + objectMetadataCollection, + objectMetadataMap, + objectMetadataMapItem: objectMetadataMap[objectMetadata.nameSingular], }); } @@ -114,7 +121,9 @@ export class WorkspaceResolverFactory { authContext, objectMetadataItem: objectMetadata, fieldMetadataCollection: objectMetadata.fields, - objectMetadataCollection: objectMetadataCollection, + objectMetadataCollection, + objectMetadataMap, + objectMetadataMapItem: objectMetadataMap[objectMetadata.nameSingular], }); } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory.ts index 7db06ad369bf..67b3ee17683c 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory.ts @@ -1,14 +1,12 @@ import { Inject, Injectable, forwardRef } from '@nestjs/common'; -import { GraphQLInputFieldConfigMap, GraphQLInputObjectType } from 'graphql'; +import { GraphQLInputObjectType } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { TypeMapperService } from 'src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; -import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; +import { generateFields } from 'src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils'; import { pascalCase } from 'src/utils/pascal-case'; import { InputTypeFactory } from './input-type.factory'; @@ -55,7 +53,12 @@ export class InputTypeDefinitionFactory { }); return { - ...this.generateFields(objectMetadata, kind, options), + ...generateFields( + objectMetadata, + kind, + options, + this.inputTypeFactory, + ), and: { type: andOrType, }, @@ -73,7 +76,12 @@ export class InputTypeDefinitionFactory { * Other input types are generated with fields only */ default: - return this.generateFields(objectMetadata, kind, options); + return generateFields( + objectMetadata, + kind, + options, + this.inputTypeFactory, + ); } }, }); @@ -84,46 +92,4 @@ export class InputTypeDefinitionFactory { type: inputType, }; } - - private generateFields( - objectMetadata: ObjectMetadataInterface, - kind: InputTypeDefinitionKind, - options: WorkspaceBuildSchemaOptions, - ): GraphQLInputFieldConfigMap { - const fields: GraphQLInputFieldConfigMap = {}; - - for (const fieldMetadata of objectMetadata.fields) { - // Relation field types are generated during extension of object type definition - if (isRelationFieldMetadataType(fieldMetadata.type)) { - continue; - } - - const target = isCompositeFieldMetadataType(fieldMetadata.type) - ? fieldMetadata.type.toString() - : fieldMetadata.id; - - const isIdField = fieldMetadata.name === 'id'; - - const type = this.inputTypeFactory.create( - target, - fieldMetadata.type, - kind, - options, - { - nullable: fieldMetadata.isNullable, - defaultValue: fieldMetadata.defaultValue, - isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, - settings: fieldMetadata.settings, - isIdField, - }, - ); - - fields[fieldMetadata.name] = { - type, - description: fieldMetadata.description, - }; - } - - return fields; - } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory.ts index 1dbb46799b60..cf2efd3a733d 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory.ts @@ -1,14 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { GraphQLFieldConfigMap, GraphQLObjectType } from 'graphql'; +import { GraphQLObjectType } from 'graphql'; import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; +import { generateFields } from 'src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils'; import { pascalCase } from 'src/utils/pascal-case'; -import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; -import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { OutputTypeFactory } from './output-type.factory'; @@ -39,48 +37,13 @@ export class ObjectTypeDefinitionFactory { type: new GraphQLObjectType({ name: `${pascalCase(objectMetadata.nameSingular)}${kind.toString()}`, description: objectMetadata.description, - fields: this.generateFields(objectMetadata, kind, options), + fields: generateFields( + objectMetadata, + kind, + options, + this.outputTypeFactory, + ), }), }; } - - private generateFields( - objectMetadata: ObjectMetadataInterface, - kind: ObjectTypeDefinitionKind, - options: WorkspaceBuildSchemaOptions, - ): GraphQLFieldConfigMap<any, any> { - const fields: GraphQLFieldConfigMap<any, any> = {}; - - for (const fieldMetadata of objectMetadata.fields) { - // Relation field types are generated during extension of object type definition - if (isRelationFieldMetadataType(fieldMetadata.type)) { - continue; - } - - const target = isCompositeFieldMetadataType(fieldMetadata.type) - ? fieldMetadata.type.toString() - : fieldMetadata.id; - - const type = this.outputTypeFactory.create( - target, - fieldMetadata.type, - kind, - options, - { - nullable: fieldMetadata.isNullable, - isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, - settings: fieldMetadata.settings, - // Scalar type is already defined in the entity itself. - isIdField: false, - }, - ); - - fields[fieldMetadata.name] = { - type, - description: fieldMetadata.description, - }; - } - - return fields; - } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/root-type.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/root-type.factory.ts index c39111d3ff18..9f92a2e58d15 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/root-type.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/factories/root-type.factory.ts @@ -74,9 +74,7 @@ export class RootTypeFactory { const args = getResolverArgs(methodName); const objectType = this.typeDefinitionsStorage.getObjectTypeByKey( objectMetadata.id, - ['findMany', 'findDuplicates'].includes(methodName) - ? ObjectTypeDefinitionKind.Connection - : ObjectTypeDefinitionKind.Plain, + this.getObjectTypeDefinitionKindByMethodName(methodName), ); const argsType = this.argsFactory.create( { @@ -124,4 +122,17 @@ export class RootTypeFactory { return fieldConfigMap; } + + private getObjectTypeDefinitionKindByMethodName( + methodName: WorkspaceResolverBuilderMethodNames, + ): ObjectTypeDefinitionKind { + switch (methodName) { + case 'findMany': + case 'findDuplicates': + case 'search': + return ObjectTypeDefinitionKind.Connection; + default: + return ObjectTypeDefinitionKind.Plain; + } + } } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts index f5a6aec8b1a2..d0ab66983309 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-schema-builder-context.interface.ts @@ -2,10 +2,16 @@ import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metada import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { AuthContext } from 'src/engine/core-modules/auth/types/auth-context.type'; +import { + ObjectMetadataMap, + ObjectMetadataMapItem, +} from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; export interface WorkspaceSchemaBuilderContext { authContext: AuthContext; - objectMetadataItem: ObjectMetadataInterface; fieldMetadataCollection: FieldMetadataInterface[]; objectMetadataCollection: ObjectMetadataInterface[]; + objectMetadataItem: ObjectMetadataInterface; + objectMetadataMap: ObjectMetadataMap; + objectMetadataMapItem: ObjectMetadataMapItem; } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils.ts new file mode 100644 index 000000000000..d27f0eba9a76 --- /dev/null +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/utils/generate-fields.utils.ts @@ -0,0 +1,100 @@ +import { + GraphQLFieldConfigMap, + GraphQLInputFieldConfigMap, + GraphQLInputType, + GraphQLOutputType, +} from 'graphql'; + +import { WorkspaceBuildSchemaOptions } from 'src/engine/api/graphql/workspace-schema-builder/interfaces/workspace-build-schema-optionts.interface'; +import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; + +import { InputTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/input-type-definition.factory'; +import { ObjectTypeDefinitionKind } from 'src/engine/api/graphql/workspace-schema-builder/factories/object-type-definition.factory'; +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { isRelationFieldMetadataType } from 'src/engine/utils/is-relation-field-metadata-type.util'; + +type TypeFactory<T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind> = + { + create: ( + target: string, + fieldType: FieldMetadataType, + kind: T, + options: WorkspaceBuildSchemaOptions, + additionalOptions: { + nullable?: boolean; + defaultValue?: any; + isArray: boolean; + settings: any; + isIdField: boolean; + }, + ) => T extends InputTypeDefinitionKind + ? GraphQLInputType + : GraphQLOutputType; + }; + +export const generateFields = < + T extends InputTypeDefinitionKind | ObjectTypeDefinitionKind, +>( + objectMetadata: ObjectMetadataInterface, + kind: T, + options: WorkspaceBuildSchemaOptions, + typeFactory: TypeFactory<T>, +): T extends InputTypeDefinitionKind + ? GraphQLInputFieldConfigMap + : GraphQLFieldConfigMap<any, any> => { + const fields = {}; + + for (const fieldMetadata of objectMetadata.fields) { + if ( + isRelationFieldMetadataType(fieldMetadata.type) || + fieldMetadata.type === FieldMetadataType.TS_VECTOR + ) { + continue; + } + + const target = isCompositeFieldMetadataType(fieldMetadata.type) + ? fieldMetadata.type.toString() + : fieldMetadata.id; + + const typeFactoryOptions = isInputTypeDefinitionKind(kind) + ? { + nullable: fieldMetadata.isNullable, + defaultValue: fieldMetadata.defaultValue, + isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, + settings: fieldMetadata.settings, + isIdField: fieldMetadata.name === 'id', + } + : { + nullable: fieldMetadata.isNullable, + isArray: fieldMetadata.type === FieldMetadataType.MULTI_SELECT, + settings: fieldMetadata.settings, + // Scalar type is already defined in the entity itself. + isIdField: false, + }; + + const type = typeFactory.create( + target, + fieldMetadata.type, + kind, + options, + typeFactoryOptions, + ); + + fields[fieldMetadata.name] = { + type, + description: fieldMetadata.description, + }; + } + + return fields; +}; + +// Type guard +const isInputTypeDefinitionKind = ( + kind: InputTypeDefinitionKind | ObjectTypeDefinitionKind, +): kind is InputTypeDefinitionKind => { + return Object.values(InputTypeDefinitionKind).includes( + kind as InputTypeDefinitionKind, + ); +}; 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 c829e0e4088f..7e1755218730 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 @@ -137,6 +137,17 @@ export const getResolverArgs = ( isNullable: false, }, }; + case 'search': + return { + searchInput: { + type: GraphQLString, + isNullable: true, + }, + limit: { + type: GraphQLInt, + isNullable: true, + }, + }; default: throw new Error(`Unknown resolver type: ${type}`); } diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts index 336fa825f81c..32a44ad4d26e 100644 --- a/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts +++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema.factory.ts @@ -117,6 +117,7 @@ export class WorkspaceSchemaFactory { const autoGeneratedResolvers = await this.workspaceResolverFactory.create( authContext, objectMetadataCollection, + objectMetadataMap, workspaceResolverBuilderMethodNames, ); const scalarsResolvers = diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts index 4fec6c7ab443..625da7f23b07 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/utils/__tests__/map-field-metadata-to-graphql-query.utils.spec.ts @@ -31,6 +31,9 @@ describe('mapFieldMetadataToGraphqlQuery', () => { }); describe('should handle all field metadata types', () => { Object.values(FieldMetadataType).forEach((fieldMetadataType) => { + if (fieldMetadataType === FieldMetadataType.TS_VECTOR) { + return; + } it(`with field type ${fieldMetadataType}`, () => { const field = { type: fieldMetadataType, 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 1560c7f97ced..708472826d10 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 @@ -24,11 +24,10 @@ import { UserModule } from 'src/engine/core-modules/user/user.module'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; -import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; 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 { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { AuthResolver } from './auth.resolver'; @@ -47,9 +46,6 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; [Workspace, User, AppToken, FeatureFlagEntity], 'core', ), - ObjectMetadataRepositoryModule.forFeature([ - ConnectedAccountWorkspaceEntity, - ]), HttpModule, TokenModule, UserWorkspaceModule, @@ -57,6 +53,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; OnboardingModule, WorkspaceDataSourceModule, ConnectedAccountModule, + FeatureFlagModule, ], controllers: [ GoogleAuthController, diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts index 77c6b41ce1d0..08baa4ff5a51 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts @@ -8,18 +8,33 @@ import { import { GoogleAPIsOauthExchangeCodeForTokenStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy'; import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { TokenService } from 'src/engine/core-modules/auth/token/services/token.service'; @Injectable() export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard( 'google-apis', ) { - constructor(private readonly environmentService: EnvironmentService) { + constructor( + private readonly environmentService: EnvironmentService, + private readonly featureFlagService: FeatureFlagService, + private readonly tokenService: TokenService, + ) { super(); } async canActivate(context: ExecutionContext) { const request = context.switchToHttp().getRequest(); const state = JSON.parse(request.query.state); + const { workspaceId } = await this.tokenService.verifyTransientToken( + state.transientToken, + ); + const isGmailSendEmailScopeEnabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsGmailSendEmailScopeEnabled, + workspaceId, + ); if ( !this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED') && @@ -34,6 +49,7 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard( new GoogleAPIsOauthExchangeCodeForTokenStrategy( this.environmentService, {}, + isGmailSendEmailScopeEnabled, ); setRequestExtraParams(request, { diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts index 04d860d5ebc5..9b0e8f26062a 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts @@ -8,10 +8,17 @@ import { import { GoogleAPIsOauthRequestCodeStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-request-code.auth.strategy'; import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { TokenService } from 'src/engine/core-modules/auth/token/services/token.service'; @Injectable() export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') { - constructor(private readonly environmentService: EnvironmentService) { + constructor( + private readonly environmentService: EnvironmentService, + private readonly featureFlagService: FeatureFlagService, + private readonly tokenService: TokenService, + ) { super({ prompt: 'select_account', }); @@ -20,6 +27,15 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') { async canActivate(context: ExecutionContext) { const request = context.switchToHttp().getRequest(); + const { workspaceId } = await this.tokenService.verifyTransientToken( + request.query.transientToken, + ); + const isGmailSendEmailScopeEnabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsGmailSendEmailScopeEnabled, + workspaceId, + ); + if ( !this.environmentService.get('MESSAGING_PROVIDER_GMAIL_ENABLED') && !this.environmentService.get('CALENDAR_PROVIDER_GOOGLE_ENABLED') @@ -30,12 +46,17 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') { ); } - new GoogleAPIsOauthRequestCodeStrategy(this.environmentService, {}); + new GoogleAPIsOauthRequestCodeStrategy( + this.environmentService, + {}, + isGmailSendEmailScopeEnabled, + ); setRequestExtraParams(request, { transientToken: request.query.transientToken, redirectLocation: request.query.redirectLocation, calendarVisibility: request.query.calendarVisibility, messageVisibility: request.query.messageVisibility, + loginHint: request.query.loginHint, }); const activate = (await super.canActivate(context)) as boolean; diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts index b20072ed54f1..04c77d2d9c19 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts @@ -7,7 +7,6 @@ import { EnvironmentService } from 'src/engine/core-modules/environment/environm import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; -import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { CalendarEventListFetchJob, @@ -17,7 +16,6 @@ import { CalendarChannelVisibility, CalendarChannelWorkspaceEntity, } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; -import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository'; import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service'; import { ConnectedAccountProvider, @@ -35,6 +33,9 @@ import { MessagingMessageListFetchJobData, } from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +import { getGoogleApisOauthScopes } from 'src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; @Injectable() export class GoogleAPIsService { @@ -45,9 +46,8 @@ export class GoogleAPIsService { @InjectMessageQueue(MessageQueue.calendarQueue) private readonly calendarQueueService: MessageQueueService, private readonly environmentService: EnvironmentService, - @InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity) - private readonly connectedAccountRepository: ConnectedAccountRepository, private readonly accountsToReconnectService: AccountsToReconnectService, + private readonly featureFlagService: FeatureFlagService, ) {} async refreshGoogleRefreshToken(input: { @@ -71,14 +71,17 @@ export class GoogleAPIsService { 'CALENDAR_PROVIDER_GOOGLE_ENABLED', ); - const connectedAccounts = - await this.connectedAccountRepository.getAllByHandleAndWorkspaceMemberId( - handle, - workspaceMemberId, + const connectedAccountRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>( workspaceId, + 'connectedAccount', ); - const existingAccountId = connectedAccounts?.[0]?.id; + const connectedAccount = await connectedAccountRepository.findOne({ + where: { handle, accountOwnerId: workspaceMemberId }, + }); + + const existingAccountId = connectedAccount?.id; const newOrExistingConnectedAccountId = existingAccountId ?? v4(); const calendarChannelRepository = @@ -96,9 +99,16 @@ export class GoogleAPIsService { const workspaceDataSource = await this.twentyORMGlobalManager.getDataSourceForWorkspace(workspaceId); + const isGmailSendEmailScopeEnabled = + await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsGmailSendEmailScopeEnabled, + workspaceId, + ); + const scopes = getGoogleApisOauthScopes(isGmailSendEmailScopeEnabled); + await workspaceDataSource.transaction(async (manager: EntityManager) => { if (!existingAccountId) { - await this.connectedAccountRepository.create( + await connectedAccountRepository.save( { id: newOrExistingConnectedAccountId, handle, @@ -106,8 +116,9 @@ export class GoogleAPIsService { accessToken: input.accessToken, refreshToken: input.refreshToken, accountOwnerId: workspaceMemberId, + scopes, }, - workspaceId, + {}, manager, ); @@ -140,11 +151,15 @@ export class GoogleAPIsService { ); } } else { - await this.connectedAccountRepository.updateAccessTokenAndRefreshToken( - input.accessToken, - input.refreshToken, - newOrExistingConnectedAccountId, - workspaceId, + await connectedAccountRepository.update( + { + id: newOrExistingConnectedAccountId, + }, + { + accessToken: input.accessToken, + refreshToken: input.refreshToken, + scopes, + }, manager, ); diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-common.auth.strategy.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-common.auth.strategy.ts index 8636924735f9..addf4b6e78cd 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-common.auth.strategy.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-common.auth.strategy.ts @@ -4,6 +4,7 @@ import { PassportStrategy } from '@nestjs/passport'; import { Strategy } from 'passport-google-oauth20'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { getGoogleApisOauthScopes } from 'src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes'; export type GoogleAPIScopeConfig = { isCalendarEnabled?: boolean; @@ -18,14 +19,9 @@ export class GoogleAPIsOauthCommonStrategy extends PassportStrategy( constructor( environmentService: EnvironmentService, scopeConfig: GoogleAPIScopeConfig, + isGmailSendEmailScopeEnabled = false, ) { - const scopes = [ - 'email', - 'profile', - 'https://www.googleapis.com/auth/gmail.readonly', - 'https://www.googleapis.com/auth/calendar.events', - 'https://www.googleapis.com/auth/profile.emails.read', - ]; + const scopes = getGoogleApisOauthScopes(isGmailSendEmailScopeEnabled); super({ clientID: environmentService.get('AUTH_GOOGLE_CLIENT_ID'), diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy.ts index 244b1066d846..c8559bd141f2 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy.ts @@ -15,8 +15,9 @@ export class GoogleAPIsOauthExchangeCodeForTokenStrategy extends GoogleAPIsOauth constructor( environmentService: EnvironmentService, scopeConfig: GoogleAPIScopeConfig, + isGmailSendEmailScopeEnabled = false, ) { - super(environmentService, scopeConfig); + super(environmentService, scopeConfig, isGmailSendEmailScopeEnabled); } async validate( diff --git a/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-request-code.auth.strategy.ts b/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-request-code.auth.strategy.ts index f93642bc7ece..ee0782b9cd8b 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-request-code.auth.strategy.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/strategies/google-apis-oauth-request-code.auth.strategy.ts @@ -13,8 +13,9 @@ export class GoogleAPIsOauthRequestCodeStrategy extends GoogleAPIsOauthCommonStr constructor( environmentService: EnvironmentService, scopeConfig: GoogleAPIScopeConfig, + isGmailSendEmailScopeEnabled = false, ) { - super(environmentService, scopeConfig); + super(environmentService, scopeConfig, isGmailSendEmailScopeEnabled); } authenticate(req: any, options: any) { @@ -22,6 +23,7 @@ export class GoogleAPIsOauthRequestCodeStrategy extends GoogleAPIsOauthCommonStr ...options, accessType: 'offline', prompt: 'consent', + loginHint: req.params.loginHint, state: JSON.stringify({ transientToken: req.params.transientToken, redirectLocation: req.params.redirectLocation, diff --git a/packages/twenty-server/src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes.ts b/packages/twenty-server/src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes.ts new file mode 100644 index 000000000000..e532c3cdf405 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/auth/utils/get-google-apis-oauth-scopes.ts @@ -0,0 +1,17 @@ +export const getGoogleApisOauthScopes = ( + isGmailSendEmailScopeEnabled = false, +) => { + const scopes = [ + 'email', + 'profile', + 'https://www.googleapis.com/auth/gmail.readonly', + 'https://www.googleapis.com/auth/calendar.events', + 'https://www.googleapis.com/auth/profile.emails.read', + ]; + + if (isGmailSendEmailScopeEnabled) { + scopes.push('https://www.googleapis.com/auth/gmail.send'); + } + + return scopes; +}; diff --git a/packages/twenty-server/src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util.ts b/packages/twenty-server/src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util.ts index b6549ceff071..76743c04d0b4 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util.ts @@ -9,6 +9,7 @@ type GoogleAPIsRequestExtraParams = { redirectLocation?: string; calendarVisibility?: string; messageVisibility?: string; + loginHint?: string; }; export const setRequestExtraParams = ( @@ -20,6 +21,7 @@ export const setRequestExtraParams = ( redirectLocation, calendarVisibility, messageVisibility, + loginHint, } = params; if (!transientToken) { @@ -42,4 +44,7 @@ export const setRequestExtraParams = ( if (messageVisibility) { request.params.messageVisibility = messageVisibility; } + if (loginHint) { + request.params.loginHint = loginHint; + } }; diff --git a/packages/twenty-server/src/engine/core-modules/duplicate/duplicate.service.ts b/packages/twenty-server/src/engine/core-modules/duplicate/duplicate.service.ts index d7ed6f87bd85..48d021b5e689 100644 --- a/packages/twenty-server/src/engine/core-modules/duplicate/duplicate.service.ts +++ b/packages/twenty-server/src/engine/core-modules/duplicate/duplicate.service.ts @@ -1,15 +1,15 @@ import { Injectable } from '@nestjs/common'; -import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { Record as IRecord, Record, } from 'src/engine/api/graphql/workspace-query-builder/interfaces/record.interface'; +import { ObjectMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/object-metadata.interface'; import { settings } from 'src/engine/constants/settings'; +import { DUPLICATE_CRITERIA_COLLECTION } from 'src/engine/core-modules/duplicate/constants/duplicate-criteria.constants'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; -import { DUPLICATE_CRITERIA_COLLECTION } from 'src/engine/core-modules/duplicate/constants/duplicate-criteria.constants'; @Injectable() export class DuplicateService { @@ -94,80 +94,4 @@ export class DuplicateService { duplicateCriteria.objectName === objectMetadataItem.nameSingular, ); } - - /** - * TODO: Remove this code by September 1st, 2024 if it isn't used - * It was build to be used by the upsertMany function, but it was not used. - * It's a re-implementation of the methods to findDuplicates, but done - * at the SQL layer instead of doing it at the GraphQL layer - * - async findDuplicate( - data: Partial<Record>, - objectMetadata: ObjectMetadataInterface, - workspaceId: string, - ) { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - const { duplicateWhereClause, duplicateWhereParameters } = - this.buildDuplicateConditionForUpsert(objectMetadata, data); - - const results = await this.workspaceDataSourceService.executeRawQuery( - ` - SELECT - * - FROM - ${dataSourceSchema}."${computeObjectTargetTable( - objectMetadata, - )}" p - WHERE - ${duplicateWhereClause} - `, - duplicateWhereParameters, - workspaceId, - ); - - return results.length > 0 ? results[0] : null; - } - - private buildDuplicateConditionForUpsert( - objectMetadata: ObjectMetadataInterface, - data: Partial<Record>, - ) { - const criteriaCollection = this.getApplicableDuplicateCriteriaCollection( - objectMetadata, - ).filter( - (duplicateCriteria) => duplicateCriteria.useAsUniqueKeyForUpsert === true, - ); - - const whereClauses: string[] = []; - const whereParameters: any[] = []; - let parameterIndex = 1; - - criteriaCollection.forEach((c) => { - const clauseParts: string[] = []; - - c.columnNames.forEach((column) => { - const dataKey = Object.keys(data).find( - (key) => key.toLowerCase() === column.toLowerCase(), - ); - - if (dataKey) { - clauseParts.push(`p."${column}" = $${parameterIndex}`); - whereParameters.push(data[dataKey]); - parameterIndex++; - } - }); - if (clauseParts.length > 0) { - whereClauses.push(`(${clauseParts.join(' AND ')})`); - } - }); - - const duplicateWhereClause = whereClauses.join(' OR '); - const duplicateWhereParameters = whereParameters; - - return { duplicateWhereClause, duplicateWhereParameters }; - } - * - */ } 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 4e30e84bd83e..cb5b2fbe2822 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 @@ -391,7 +391,7 @@ export class EnvironmentVariables { @CastToBoolean() MESSAGING_PROVIDER_GMAIL_ENABLED = false; - MESSAGE_QUEUE_TYPE: string = MessageQueueDriverType.Sync; + MESSAGE_QUEUE_TYPE: string = MessageQueueDriverType.BullMQ; EMAIL_FROM_ADDRESS = 'noreply@yourdomain.com'; @@ -426,7 +426,7 @@ export class EnvironmentVariables { @CastToPositiveNumber() API_RATE_LIMITING_LIMIT = 500; - CACHE_STORAGE_TYPE: CacheStorageType = CacheStorageType.Memory; + CACHE_STORAGE_TYPE: CacheStorageType = CacheStorageType.Redis; @CastToPositiveNumber() CACHE_STORAGE_TTL: number = 3600 * 24 * 7; diff --git a/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record-destroy.event.ts b/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record-destroy.event.ts new file mode 100644 index 000000000000..f12b1e17547f --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/event-emitter/types/object-record-destroy.event.ts @@ -0,0 +1,7 @@ +import { ObjectRecordBaseEvent } from 'src/engine/core-modules/event-emitter/types/object-record.base.event'; + +export class ObjectRecordDestroyEvent<T> extends ObjectRecordBaseEvent { + properties: { + before: T; + }; +} 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 7bd085cc4136..836f6cd6d792 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,4 +10,8 @@ export enum FeatureFlagKey { IsMessageThreadSubscriberEnabled = 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED', IsQueryRunnerTwentyORMEnabled = 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED', IsWorkspaceFavoriteEnabled = 'IS_WORKSPACE_FAVORITE_ENABLED', + IsSearchEnabled = 'IS_SEARCH_ENABLED', + IsWorkspaceMigratedForSearch = 'IS_WORKSPACE_MIGRATED_FOR_SEARCH', + IsGmailSendEmailScopeEnabled = 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED', + IsAnalyticsV2Enabled = 'IS_ANALYTICS_V2_ENABLED', } diff --git a/packages/twenty-server/src/engine/core-modules/feature-flag/services/feature-flag.service.ts b/packages/twenty-server/src/engine/core-modules/feature-flag/services/feature-flag.service.ts index fc9c5038a2bd..cd2e4ca4d255 100644 --- a/packages/twenty-server/src/engine/core-modules/feature-flag/services/feature-flag.service.ts +++ b/packages/twenty-server/src/engine/core-modules/feature-flag/services/feature-flag.service.ts @@ -46,4 +46,17 @@ export class FeatureFlagService { return workspaceFeatureFlagsMap; } + + public async enableFeatureFlags( + keys: FeatureFlagKey[], + workspaceId: string, + ): Promise<void> { + await this.featureFlagRepository.upsert( + keys.map((key) => ({ workspaceId, key, value: true })), + { + conflictPaths: ['workspaceId', 'key'], + skipUpdateIfNoValuesChanged: true, + }, + ); + } } diff --git a/packages/twenty-server/src/engine/core-modules/file-storage/drivers/interfaces/storage-driver.interface.ts b/packages/twenty-server/src/engine/core-modules/file-storage/drivers/interfaces/storage-driver.interface.ts index 84cf20a21894..65a0076249c0 100644 --- a/packages/twenty-server/src/engine/core-modules/file-storage/drivers/interfaces/storage-driver.interface.ts +++ b/packages/twenty-server/src/engine/core-modules/file-storage/drivers/interfaces/storage-driver.interface.ts @@ -17,4 +17,8 @@ export interface StorageDriver { from: { folderPath: string; filename?: string }; to: { folderPath: string; filename?: string }; }): Promise<void>; + download(params: { + from: { folderPath: string; filename?: string }; + to: { folderPath: string; filename?: string }; + }): Promise<void>; } diff --git a/packages/twenty-server/src/engine/core-modules/file-storage/drivers/local.driver.ts b/packages/twenty-server/src/engine/core-modules/file-storage/drivers/local.driver.ts index 3bbcc5493646..104808b4e63f 100644 --- a/packages/twenty-server/src/engine/core-modules/file-storage/drivers/local.driver.ts +++ b/packages/twenty-server/src/engine/core-modules/file-storage/drivers/local.driver.ts @@ -21,10 +21,6 @@ export class LocalDriver implements StorageDriver { } async createFolder(path: string) { - if (existsSync(path)) { - return; - } - return fs.mkdir(path, { recursive: true }); } @@ -122,21 +118,24 @@ export class LocalDriver implements StorageDriver { } } - async copy(params: { - from: { folderPath: string; filename?: string }; - to: { folderPath: string; filename?: string }; - }): Promise<void> { + async copy( + params: { + from: { folderPath: string; filename?: string }; + to: { folderPath: string; filename?: string }; + }, + toInMemory = false, + ): Promise<void> { if (!params.from.filename && params.to.filename) { throw new Error('Cannot copy folder to file'); } const fromPath = join( - `${this.options.storagePath}/`, + this.options.storagePath, params.from.folderPath, params.from.filename || '', ); const toPath = join( - `${this.options.storagePath}/`, + toInMemory ? '' : this.options.storagePath, params.to.folderPath, params.to.filename || '', ); @@ -156,4 +155,11 @@ export class LocalDriver implements StorageDriver { throw error; } } + + async download(params: { + from: { folderPath: string; filename?: string }; + to: { folderPath: string; filename?: string }; + }): Promise<void> { + await this.copy(params, true); + } } diff --git a/packages/twenty-server/src/engine/core-modules/file-storage/drivers/s3.driver.ts b/packages/twenty-server/src/engine/core-modules/file-storage/drivers/s3.driver.ts index 765ded5b0068..766be0b13b80 100644 --- a/packages/twenty-server/src/engine/core-modules/file-storage/drivers/s3.driver.ts +++ b/packages/twenty-server/src/engine/core-modules/file-storage/drivers/s3.driver.ts @@ -1,4 +1,8 @@ import { Readable } from 'stream'; +import fs from 'fs'; +import { mkdir } from 'fs/promises'; +import { join } from 'path'; +import { pipeline } from 'stream/promises'; import { CopyObjectCommand, @@ -188,6 +192,23 @@ export class S3Driver implements StorageDriver { } } + extractFolderAndFilePaths(objectKey: string | undefined) { + if (!isDefined(objectKey)) { + return; + } + + const result = /(?<folder>.*)\/(?<file>.*)/.exec(objectKey); + + if (!isDefined(result) || !isDefined(result.groups)) { + return; + } + + const fromFolderPath = result.groups.folder; + const filename = result.groups.file; + + return { fromFolderPath, filename }; + } + async copy(params: { from: { folderPath: string; filename?: string }; to: { folderPath: string; filename?: string }; @@ -239,17 +260,18 @@ export class S3Driver implements StorageDriver { ); if (!listedObjects.Contents || listedObjects.Contents.length === 0) { - throw new Error('No objects found in the source folder.'); + throw new Error(`No objects found in the source folder ${fromKey}.`); } for (const object of listedObjects.Contents) { - const match = object.Key?.match(/(.*)\/(.*)/); + const folderAndFilePaths = this.extractFolderAndFilePaths(object.Key); - if (!isDefined(match)) { + if (!isDefined(folderAndFilePaths)) { continue; } - const fromFolderPath = match[1]; - const filename = match[2]; + + const { fromFolderPath, filename } = folderAndFilePaths; + const toFolderPath = fromFolderPath.replace( params.from.folderPath, params.to.folderPath, @@ -269,6 +291,85 @@ export class S3Driver implements StorageDriver { } } + async download(params: { + from: { folderPath: string; filename?: string }; + to: { folderPath: string; filename?: string }; + }): Promise<void> { + if (!params.from.filename && params.to.filename) { + throw new Error('Cannot copy folder to file'); + } + + if (isDefined(params.from.filename)) { + try { + const dir = params.to.folderPath; + + await mkdir(dir, { recursive: true }); + + const fileStream = await this.read({ + folderPath: params.from.folderPath, + filename: params.from.filename, + }); + + const toPath = join( + params.to.folderPath, + params.to.filename || params.from.filename, + ); + + await pipeline(fileStream, fs.createWriteStream(toPath)); + + return; + } catch (error) { + if (error.name === 'NotFound') { + throw new FileStorageException( + 'File not found', + FileStorageExceptionCode.FILE_NOT_FOUND, + ); + } + // For other errors, throw the original error + throw error; + } + } + + const listedObjects = await this.s3Client.send( + new ListObjectsV2Command({ + Bucket: this.bucketName, + Prefix: params.from.folderPath, + }), + ); + + if (!listedObjects.Contents || listedObjects.Contents.length === 0) { + throw new Error( + `No objects found in the source folder ${params.from.folderPath}.`, + ); + } + + for (const object of listedObjects.Contents) { + const folderAndFilePaths = this.extractFolderAndFilePaths(object.Key); + + if (!isDefined(folderAndFilePaths)) { + continue; + } + + const { fromFolderPath, filename } = folderAndFilePaths; + const toFolderPath = fromFolderPath.replace( + params.from.folderPath, + params.to.folderPath, + ); + + if (!isDefined(toFolderPath)) { + continue; + } + + await this.download({ + from: { + folderPath: fromFolderPath, + filename, + }, + to: { folderPath: toFolderPath, filename }, + }); + } + } + async checkBucketExists(args: HeadBucketCommandInput) { try { await this.s3Client.headBucket(args); diff --git a/packages/twenty-server/src/engine/core-modules/file-storage/file-storage.service.ts b/packages/twenty-server/src/engine/core-modules/file-storage/file-storage.service.ts index f23822b8a374..1b608e48ae4b 100644 --- a/packages/twenty-server/src/engine/core-modules/file-storage/file-storage.service.ts +++ b/packages/twenty-server/src/engine/core-modules/file-storage/file-storage.service.ts @@ -40,4 +40,11 @@ export class FileStorageService implements StorageDriver { }): Promise<void> { return this.driver.copy(params); } + + download(params: { + from: { folderPath: string; filename?: string }; + to: { folderPath: string; filename?: string }; + }): Promise<void> { + return this.driver.download(params); + } } diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts index 5c885c312dfc..5f10e3be0d75 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/__tests__/components.utils.spec.ts @@ -10,7 +10,8 @@ describe('computeSchemaComponents', () => { it('should test all non-deprecated field types', () => { expect(fields.map((field) => field.type)).toEqual( Object.keys(FieldMetadataType).filter( - (key) => key !== FieldMetadataType.LINK, + (key) => + key !== FieldMetadataType.LINK && key !== FieldMetadataType.TS_VECTOR, ), ); }); @@ -21,6 +22,7 @@ describe('computeSchemaComponents', () => { ] as ObjectMetadataEntity[]), ).toEqual({ ObjectName: { + description: undefined, type: 'object', properties: { fieldUuid: { @@ -195,6 +197,7 @@ describe('computeSchemaComponents', () => { 'API', 'IMPORT', 'MANUAL', + 'SYSTEM', ], }, }, @@ -203,6 +206,7 @@ describe('computeSchemaComponents', () => { required: ['fieldNumber'], }, 'ObjectName for Update': { + description: undefined, type: 'object', properties: { fieldUuid: { @@ -377,6 +381,7 @@ describe('computeSchemaComponents', () => { 'API', 'IMPORT', 'MANUAL', + 'SYSTEM', ], }, }, @@ -384,6 +389,7 @@ describe('computeSchemaComponents', () => { }, }, 'ObjectName for Response': { + description: undefined, type: 'object', properties: { fieldUuid: { @@ -558,6 +564,7 @@ describe('computeSchemaComponents', () => { 'API', 'IMPORT', 'MANUAL', + 'SYSTEM', ], }, workspaceMemberId: { diff --git a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts index 7e34e35a31fe..4b1910e1c08a 100644 --- a/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts +++ b/packages/twenty-server/src/engine/core-modules/open-api/utils/components.utils.ts @@ -109,7 +109,8 @@ const getSchemaComponentsProperties = ({ return item.fields.reduce((node, field) => { if ( !isFieldAvailable(field, forResponse) || - field.type === FieldMetadataType.RELATION + field.type === FieldMetadataType.RELATION || + field.type === FieldMetadataType.TS_VECTOR ) { return node; } diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/base-serverless.driver.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/base-serverless.driver.ts deleted file mode 100644 index e7abc6743302..000000000000 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/base-serverless.driver.ts +++ /dev/null @@ -1,25 +0,0 @@ -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 { SOURCE_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/source-file-name'; -import { compileTypescript } from 'src/engine/core-modules/serverless/drivers/utils/compile-typescript'; -import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; -import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; - -export class BaseServerlessDriver { - async getCompiledCode( - serverlessFunction: ServerlessFunctionEntity, - fileStorageService: FileStorageService, - ) { - const folderPath = getServerlessFolder({ - serverlessFunction, - version: 'draft', - }); - const fileStream = await fileStorageService.read({ - folderPath, - filename: SOURCE_FILE_NAME, - }); - const typescriptCode = await readFileContent(fileStream); - - return compileTypescript(typescriptCode); - } -} diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/.gitignore b/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/.gitignore new file mode 100644 index 000000000000..c5115e3f50b4 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/.gitignore @@ -0,0 +1 @@ +!base-typescript-project/**/.env diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/base-typescript-project/.env b/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/base-typescript-project/.env new file mode 100644 index 000000000000..f7ba4ce53275 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/base-typescript-project/.env @@ -0,0 +1,2 @@ +# Add your environment variables here. +# Access them in your serverless function code using process.env.VARIABLE diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/base-typescript-project/src/index.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/base-typescript-project/src/index.ts new file mode 100644 index 000000000000..da19597d5cfb --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/base-typescript-project/src/index.ts @@ -0,0 +1,7 @@ +export const handler = async ( + event: object, + context: object, +): Promise<object> => { + // Your code here + return {}; +}; diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/env-file-name.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/env-file-name.ts new file mode 100644 index 000000000000..4b409ce327af --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/env-file-name.ts @@ -0,0 +1 @@ +export const ENV_FILE_NAME = '.env'; diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/index-file-name.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/index-file-name.ts new file mode 100644 index 000000000000..d32b30a4927a --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/index-file-name.ts @@ -0,0 +1 @@ +export const INDEX_FILE_NAME = 'index.ts'; diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/outdir-folder.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/outdir-folder.ts new file mode 100644 index 000000000000..fed2d5ea331f --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/outdir-folder.ts @@ -0,0 +1 @@ +export const OUTDIR_FOLDER = 'dist'; diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/source-file-name.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/source-file-name.ts deleted file mode 100644 index 9126720f9e5d..000000000000 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/constants/source-file-name.ts +++ /dev/null @@ -1 +0,0 @@ -export const SOURCE_FILE_NAME = 'source.ts'; diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/lambda.driver.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/lambda.driver.ts index 9903309660d5..cf6315ca9a84 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/lambda.driver.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/lambda.driver.ts @@ -1,6 +1,7 @@ import * as fs from 'fs/promises'; import { join } from 'path'; +import dotenv from 'dotenv'; import { CreateFunctionCommand, DeleteFunctionCommand, @@ -18,6 +19,8 @@ import { waitUntilFunctionUpdatedV2, ListLayerVersionsCommandInput, ListLayerVersionsCommand, + UpdateFunctionConfigurationCommand, + UpdateFunctionConfigurationCommandInput, } from '@aws-sdk/client-lambda'; import { CreateFunctionCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/CreateFunctionCommand'; import { UpdateFunctionCodeCommandInput } from '@aws-sdk/client-lambda/dist-types/commands/UpdateFunctionCodeCommand'; @@ -36,7 +39,6 @@ import { NODE_LAYER_SUBFOLDER, } from 'src/engine/core-modules/serverless/drivers/utils/lambda-build-directory-manager'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; -import { BaseServerlessDriver } from 'src/engine/core-modules/serverless/drivers/base-serverless.driver'; import { createZipFile } from 'src/engine/core-modules/serverless/drivers/utils/create-zip-file'; import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto'; import { @@ -46,6 +48,11 @@ import { import { isDefined } from 'src/utils/is-defined'; import { COMMON_LAYER_NAME } from 'src/engine/core-modules/serverless/drivers/constants/common-layer-name'; import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies'; +import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; +import { SERVERLESS_TMPDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/serverless-tmpdir-folder'; +import { compileTypescript } from 'src/engine/core-modules/serverless/drivers/utils/compile-typescript'; +import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name'; +import { OUTDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/outdir-folder'; export interface LambdaDriverOptions extends LambdaClientConfig { fileStorageService: FileStorageService; @@ -53,16 +60,12 @@ export interface LambdaDriverOptions extends LambdaClientConfig { role: string; } -export class LambdaDriver - extends BaseServerlessDriver - implements ServerlessDriver -{ +export class LambdaDriver implements ServerlessDriver { private readonly lambdaClient: Lambda; private readonly lambdaRole: string; private readonly fileStorageService: FileStorageService; constructor(options: LambdaDriverOptions) { - super(); const { region, role, ...lambdaOptions } = options; this.lambdaClient = new Lambda({ ...lambdaOptions, region }); @@ -165,24 +168,50 @@ export class LambdaDriver } } - async build(serverlessFunction: ServerlessFunctionEntity) { - const javascriptCode = await this.getCompiledCode( + private getInMemoryServerlessFunctionFolderPath = ( + serverlessFunction: ServerlessFunctionEntity, + version: string, + ) => { + return join(SERVERLESS_TMPDIR_FOLDER, serverlessFunction.id, version); + }; + + async build(serverlessFunction: ServerlessFunctionEntity, version: string) { + const computedVersion = + version === 'latest' ? serverlessFunction.latestVersion : version; + + const inMemoryServerlessFunctionFolderPath = + this.getInMemoryServerlessFunctionFolderPath( + serverlessFunction, + computedVersion, + ); + + const folderPath = getServerlessFolder({ serverlessFunction, - this.fileStorageService, - ); + version, + }); - const lambdaBuildDirectoryManager = new LambdaBuildDirectoryManager(); + await this.fileStorageService.download({ + from: { folderPath }, + to: { folderPath: inMemoryServerlessFunctionFolderPath }, + }); - const { - sourceTemporaryDir, + compileTypescript(inMemoryServerlessFunctionFolderPath); + + const lambdaZipPath = join( + inMemoryServerlessFunctionFolderPath, + 'lambda.zip', + ); + + await createZipFile( + join(inMemoryServerlessFunctionFolderPath, OUTDIR_FOLDER), lambdaZipPath, - javascriptFilePath, - lambdaHandler, - } = await lambdaBuildDirectoryManager.init(); + ); - await fs.writeFile(javascriptFilePath, javascriptCode); + const envFileContent = await fs.readFile( + join(inMemoryServerlessFunctionFolderPath, ENV_FILE_NAME), + ); - await createZipFile(sourceTemporaryDir, lambdaZipPath); + const envVariables = dotenv.parse(envFileContent); const functionExists = await this.checkFunctionExists( serverlessFunction.id, @@ -198,8 +227,11 @@ export class LambdaDriver ZipFile: await fs.readFile(lambdaZipPath), }, FunctionName: serverlessFunction.id, - Handler: lambdaHandler, + Handler: 'src/index.handler', Layers: [layerArn], + Environment: { + Variables: envVariables, + }, Role: this.lambdaRole, Runtime: serverlessFunction.runtime, Description: 'Lambda function to run user script', @@ -210,23 +242,37 @@ export class LambdaDriver await this.lambdaClient.send(command); } else { - const params: UpdateFunctionCodeCommandInput = { + const updateCodeParams: UpdateFunctionCodeCommandInput = { ZipFile: await fs.readFile(lambdaZipPath), FunctionName: serverlessFunction.id, }; - const command = new UpdateFunctionCodeCommand(params); + const updateCodeCommand = new UpdateFunctionCodeCommand(updateCodeParams); - await this.lambdaClient.send(command); + await this.lambdaClient.send(updateCodeCommand); + + const updateConfigurationParams: UpdateFunctionConfigurationCommandInput = + { + Environment: { + Variables: envVariables, + }, + FunctionName: serverlessFunction.id, + }; + + const updateConfigurationCommand = new UpdateFunctionConfigurationCommand( + updateConfigurationParams, + ); + + await this.waitFunctionUpdates(serverlessFunction.id, 10); + + await this.lambdaClient.send(updateConfigurationCommand); } await this.waitFunctionUpdates(serverlessFunction.id, 10); - - await lambdaBuildDirectoryManager.clean(); } async publish(serverlessFunction: ServerlessFunctionEntity) { - await this.build(serverlessFunction); + await this.build(serverlessFunction, 'draft'); const params: PublishVersionCommandInput = { FunctionName: serverlessFunction.id, }; @@ -240,6 +286,20 @@ export class LambdaDriver throw new Error('New published version is undefined'); } + const draftFolderPath = getServerlessFolder({ + serverlessFunction: serverlessFunction, + version: 'draft', + }); + const newFolderPath = getServerlessFolder({ + serverlessFunction: serverlessFunction, + version: newVersion, + }); + + await this.fileStorageService.copy({ + from: { folderPath: draftFolderPath }, + to: { folderPath: newFolderPath }, + }); + return newVersion; } diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/local.driver.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/local.driver.ts index 00772cfd24a9..769c5c61524f 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/local.driver.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/local.driver.ts @@ -1,10 +1,9 @@ import { fork } from 'child_process'; -import { promises as fs, existsSync } from 'fs'; +import { promises as fs } from 'fs'; import { join } from 'path'; -import { v4 } from 'uuid'; +import dotenv from 'dotenv'; -import { FileStorageExceptionCode } from 'src/engine/core-modules/file-storage/interfaces/file-storage-exception'; import { ServerlessDriver, ServerlessExecuteError, @@ -12,35 +11,36 @@ import { } from 'src/engine/core-modules/serverless/drivers/interfaces/serverless-driver.interface'; 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 { BaseServerlessDriver } from 'src/engine/core-modules/serverless/drivers/base-serverless.driver'; -import { BUILD_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/build-file-name'; import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; import { ServerlessFunctionExecutionStatus } from 'src/engine/metadata-modules/serverless-function/dtos/serverless-function-execution-result.dto'; import { ServerlessFunctionEntity } from 'src/engine/metadata-modules/serverless-function/serverless-function.entity'; -import { - ServerlessFunctionException, - ServerlessFunctionExceptionCode, -} from 'src/engine/metadata-modules/serverless-function/serverless-function.exception'; import { COMMON_LAYER_NAME } from 'src/engine/core-modules/serverless/drivers/constants/common-layer-name'; import { copyAndBuildDependencies } from 'src/engine/core-modules/serverless/drivers/utils/copy-and-build-dependencies'; import { SERVERLESS_TMPDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/serverless-tmpdir-folder'; +import { compileTypescript } from 'src/engine/core-modules/serverless/drivers/utils/compile-typescript'; +import { OUTDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/outdir-folder'; +import { ENV_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/env-file-name'; + +const LISTENER_FILE_NAME = 'listener.js'; export interface LocalDriverOptions { fileStorageService: FileStorageService; } -export class LocalDriver - extends BaseServerlessDriver - implements ServerlessDriver -{ +export class LocalDriver implements ServerlessDriver { private readonly fileStorageService: FileStorageService; constructor(options: LocalDriverOptions) { - super(); this.fileStorageService = options.fileStorageService; } + private getInMemoryServerlessFunctionFolderPath = ( + serverlessFunction: ServerlessFunctionEntity, + version: string, + ) => { + return join(SERVERLESS_TMPDIR_FOLDER, serverlessFunction.id, version); + }; + private getInMemoryLayerFolderPath = (version: number) => { return join(SERVERLESS_TMPDIR_FOLDER, COMMON_LAYER_NAME, `${version}`); }; @@ -49,88 +49,54 @@ export class LocalDriver const inMemoryLastVersionLayerFolderPath = this.getInMemoryLayerFolderPath(version); - if (existsSync(inMemoryLastVersionLayerFolderPath)) { - return; + try { + await fs.access(inMemoryLastVersionLayerFolderPath); + } catch (e) { + await copyAndBuildDependencies(inMemoryLastVersionLayerFolderPath); } - - await copyAndBuildDependencies(inMemoryLastVersionLayerFolderPath); } async delete() {} - async build(serverlessFunction: ServerlessFunctionEntity) { + async build(serverlessFunction: ServerlessFunctionEntity, version: string) { + const computedVersion = + version === 'latest' ? serverlessFunction.latestVersion : version; + await this.createLayerIfNotExists(serverlessFunction.layerVersion); - const javascriptCode = await this.getCompiledCode( - serverlessFunction, - this.fileStorageService, - ); - const draftFolderPath = getServerlessFolder({ + const inMemoryServerlessFunctionFolderPath = + this.getInMemoryServerlessFunctionFolderPath( + serverlessFunction, + computedVersion, + ); + + const folderPath = getServerlessFolder({ serverlessFunction, - version: 'draft', + version, }); - await this.fileStorageService.write({ - file: javascriptCode, - name: BUILD_FILE_NAME, - mimeType: undefined, - folder: draftFolderPath, + await this.fileStorageService.download({ + from: { folderPath }, + to: { folderPath: inMemoryServerlessFunctionFolderPath }, }); - } - - async publish(serverlessFunction: ServerlessFunctionEntity) { - await this.build(serverlessFunction); - - return serverlessFunction.latestVersion - ? `${parseInt(serverlessFunction.latestVersion, 10) + 1}` - : '1'; - } - async execute( - serverlessFunction: ServerlessFunctionEntity, - payload: object, - version: string, - ): Promise<ServerlessExecuteResult> { - await this.createLayerIfNotExists(serverlessFunction.layerVersion); - - const startTime = Date.now(); - let fileContent = ''; - - try { - const fileStream = await this.fileStorageService.read({ - folderPath: getServerlessFolder({ - serverlessFunction, - version, - }), - filename: BUILD_FILE_NAME, - }); + compileTypescript(inMemoryServerlessFunctionFolderPath); - fileContent = await readFileContent(fileStream); - } catch (error) { - if (error.code === FileStorageExceptionCode.FILE_NOT_FOUND) { - throw new ServerlessFunctionException( - `Function Version '${version}' does not exist`, - ServerlessFunctionExceptionCode.SERVERLESS_FUNCTION_NOT_FOUND, - ); - } - throw error; - } - - const tmpFolderPath = join(SERVERLESS_TMPDIR_FOLDER, v4()); - - const tmpFilePath = join(tmpFolderPath, 'index.js'); - - await fs.symlink( - this.getInMemoryLayerFolderPath(serverlessFunction.layerVersion), - tmpFolderPath, - 'dir', + const envFileContent = await fs.readFile( + join(inMemoryServerlessFunctionFolderPath, ENV_FILE_NAME), ); - const modifiedContent = ` + const envVariables = dotenv.parse(envFileContent); + + const listener = ` + const index_1 = require("./src/index"); + + process.env = ${JSON.stringify(envVariables)} + process.on('message', async (message) => { const { event, context } = message; try { - const result = await handler(event, context); + const result = await index_1.handler(event, context); process.send(result); } catch (error) { process.send({ @@ -140,82 +106,158 @@ export class LocalDriver }); } }); - - ${fileContent} `; - await fs.writeFile(tmpFilePath, modifiedContent); + await fs.writeFile( + join( + inMemoryServerlessFunctionFolderPath, + OUTDIR_FOLDER, + LISTENER_FILE_NAME, + ), + listener, + ); + + try { + await fs.symlink( + join( + this.getInMemoryLayerFolderPath(serverlessFunction.layerVersion), + 'node_modules', + ), + join( + inMemoryServerlessFunctionFolderPath, + OUTDIR_FOLDER, + 'node_modules', + ), + 'dir', + ); + } catch (err) { + if (err.code !== 'EEXIST') { + throw err; + } + } + } + + async publish(serverlessFunction: ServerlessFunctionEntity) { + const newVersion = serverlessFunction.latestVersion + ? `${parseInt(serverlessFunction.latestVersion, 10) + 1}` + : '1'; + + const draftFolderPath = getServerlessFolder({ + serverlessFunction: serverlessFunction, + version: 'draft', + }); + const newFolderPath = getServerlessFolder({ + serverlessFunction: serverlessFunction, + version: newVersion, + }); + + await this.fileStorageService.copy({ + from: { folderPath: draftFolderPath }, + to: { folderPath: newFolderPath }, + }); + + await this.build(serverlessFunction, newVersion); + + return newVersion; + } + + async execute( + serverlessFunction: ServerlessFunctionEntity, + payload: object, + version: string, + ): Promise<ServerlessExecuteResult> { + const startTime = Date.now(); + const computedVersion = + version === 'latest' ? serverlessFunction.latestVersion : version; + + const listenerFile = join( + this.getInMemoryServerlessFunctionFolderPath( + serverlessFunction, + computedVersion, + ), + OUTDIR_FOLDER, + LISTENER_FILE_NAME, + ); - return await new Promise((resolve, reject) => { - const child = fork(tmpFilePath, { silent: true }); + try { + return await new Promise((resolve, reject) => { + const child = fork(listenerFile, { silent: true }); + + child.on('message', (message: object | ServerlessExecuteError) => { + const duration = Date.now() - startTime; + + if ('errorType' in message) { + resolve({ + data: null, + duration, + error: message, + status: ServerlessFunctionExecutionStatus.ERROR, + }); + } else { + resolve({ + data: message, + duration, + status: ServerlessFunctionExecutionStatus.SUCCESS, + }); + } + child.kill(); + }); + + child.stderr?.on('data', (data) => { + const stackTrace = data + .toString() + .split('\n') + .filter((line: string) => line.trim() !== ''); + const errorTrace = stackTrace.filter((line: string) => + line.includes('Error: '), + )?.[0]; + + let errorType = 'Unknown'; + let errorMessage = ''; - child.on('message', (message: object | ServerlessExecuteError) => { - const duration = Date.now() - startTime; + if (errorTrace) { + errorType = errorTrace.split(':')[0]; + errorMessage = errorTrace.split(': ')[1]; + } + const duration = Date.now() - startTime; - if ('errorType' in message) { resolve({ data: null, duration, - error: message, status: ServerlessFunctionExecutionStatus.ERROR, + error: { + errorType, + errorMessage, + stackTrace: stackTrace, + }, }); - } else { - resolve({ - data: message, - duration, - status: ServerlessFunctionExecutionStatus.SUCCESS, - }); - } - child.kill(); - fs.unlink(tmpFilePath).catch(console.error); - }); + child.kill(); + }); - child.stderr?.on('data', (data) => { - const stackTrace = data - .toString() - .split('\n') - .filter((line: string) => line.trim() !== ''); - const errorTrace = stackTrace.filter((line: string) => - line.includes('Error: '), - )?.[0]; - - let errorType = 'Unknown'; - let errorMessage = ''; - - if (errorTrace) { - errorType = errorTrace.split(':')[0]; - errorMessage = errorTrace.split(': ')[1]; - } - const duration = Date.now() - startTime; - - resolve({ - data: null, - duration, - status: ServerlessFunctionExecutionStatus.ERROR, - error: { - errorType, - errorMessage, - stackTrace: stackTrace, - }, + child.on('error', (error) => { + reject(error); + child.kill(); }); - child.kill(); - fs.unlink(tmpFilePath).catch(console.error); - }); - child.on('error', (error) => { - reject(error); - child.kill(); - fs.unlink(tmpFilePath).catch(console.error); - }); + child.on('exit', (code) => { + if (code && code !== 0) { + reject(new Error(`Child process exited with code ${code}`)); + } + }); - child.on('exit', (code) => { - if (code && code !== 0) { - reject(new Error(`Child process exited with code ${code}`)); - fs.unlink(tmpFilePath).catch(console.error); - } + child.send({ event: payload }); }); - - child.send({ event: payload }); - }); + } catch (error) { + return { + data: null, + duration: Date.now() - startTime, + error: { + errorType: 'UnhandledError', + errorMessage: error.message || 'Unknown error', + stackTrace: error.stack ? error.stack.split('\n') : [], + }, + status: ServerlessFunctionExecutionStatus.ERROR, + }; + } } } diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/compile-typescript.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/compile-typescript.ts index 7db82434a7f0..5e23b6a67a95 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/compile-typescript.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/compile-typescript.ts @@ -1,6 +1,11 @@ -import ts from 'typescript'; +import { join } from 'path'; -export const compileTypescript = (typescriptCode: string): string => { +import ts, { createProgram } from 'typescript'; + +import { OUTDIR_FOLDER } from 'src/engine/core-modules/serverless/drivers/constants/outdir-folder'; +import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name'; + +export const compileTypescript = (folderPath: string) => { const options: ts.CompilerOptions = { module: ts.ModuleKind.CommonJS, target: ts.ScriptTarget.ES2017, @@ -8,12 +13,9 @@ export const compileTypescript = (typescriptCode: string): string => { esModuleInterop: true, resolveJsonModule: true, allowSyntheticDefaultImports: true, + outDir: join(folderPath, OUTDIR_FOLDER, 'src'), types: ['node'], }; - const result = ts.transpileModule(typescriptCode, { - compilerOptions: options, - }); - - return result.outputText; + createProgram([join(folderPath, 'src', INDEX_FILE_NAME)], options).emit(); }; diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files.ts new file mode 100644 index 000000000000..3d3a8c2a8952 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/get-base-typescript-project-files.ts @@ -0,0 +1,39 @@ +import fs from 'fs/promises'; +import path, { join } from 'path'; + +import { ASSET_PATH } from 'src/constants/assets-path'; + +type File = { name: string; path: string; content: Buffer }; + +const getAllFiles = async ( + rootDir: string, + dir: string = rootDir, + files: File[] = [], +): Promise<File[]> => { + const dirEntries = await fs.readdir(dir, { withFileTypes: true }); + + for (const entry of dirEntries) { + const fullPath = path.join(dir, entry.name); + + if (entry.isDirectory()) { + return getAllFiles(rootDir, fullPath, files); + } else { + files.push({ + path: path.relative(rootDir, dir), + name: entry.name, + content: await fs.readFile(fullPath), + }); + } + } + + return files; +}; + +export const getBaseTypescriptProjectFiles = (async () => { + const baseTypescriptProjectPath = join( + ASSET_PATH, + `engine/core-modules/serverless/drivers/constants/base-typescript-project`, + ); + + return await getAllFiles(baseTypescriptProjectPath); +})(); diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/get-layer-dependencies-dir-name.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/get-layer-dependencies-dir-name.ts index 3c6ee4e8b26e..a576ea89e649 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/get-layer-dependencies-dir-name.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/get-layer-dependencies-dir-name.ts @@ -1,12 +1,17 @@ -import path from 'path'; +import path, { join } from 'path'; import { LAST_LAYER_VERSION } from 'src/engine/core-modules/serverless/drivers/layers/last-layer-version'; +import { ASSET_PATH } from 'src/constants/assets-path'; -// Can only be used in src/engine/integrations/serverless/drivers/utils folder export const getLayerDependenciesDirName = ( version: 'latest' | 'engine' | number, ): string => { const formattedVersion = version === 'latest' ? LAST_LAYER_VERSION : version; - return path.resolve(__dirname, `../layers/${formattedVersion}`); + const baseTypescriptProjectPath = join( + ASSET_PATH, + `engine/core-modules/serverless/drivers/layers/${formattedVersion}`, + ); + + return path.resolve(baseTypescriptProjectPath); }; diff --git a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/lambda-build-directory-manager.ts b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/lambda-build-directory-manager.ts index b67811f9742f..4ec30f7f4f45 100644 --- a/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/lambda-build-directory-manager.ts +++ b/packages/twenty-server/src/engine/core-modules/serverless/drivers/utils/lambda-build-directory-manager.ts @@ -10,14 +10,12 @@ export const NODE_LAYER_SUBFOLDER = 'nodejs'; const TEMPORARY_LAMBDA_FOLDER = 'lambda-build'; const TEMPORARY_LAMBDA_SOURCE_FOLDER = 'src'; const LAMBDA_ZIP_FILE_NAME = 'lambda.zip'; -const LAMBDA_ENTRY_FILE_NAME = 'index.js'; export class LambdaBuildDirectoryManager { private temporaryDir = join( SERVERLESS_TMPDIR_FOLDER, `${TEMPORARY_LAMBDA_FOLDER}-${v4()}`, ); - private lambdaHandler = `${LAMBDA_ENTRY_FILE_NAME.split('.')[0]}.handler`; async init() { const sourceTemporaryDir = join( @@ -25,15 +23,12 @@ export class LambdaBuildDirectoryManager { TEMPORARY_LAMBDA_SOURCE_FOLDER, ); const lambdaZipPath = join(this.temporaryDir, LAMBDA_ZIP_FILE_NAME); - const javascriptFilePath = join(sourceTemporaryDir, LAMBDA_ENTRY_FILE_NAME); await fs.mkdir(sourceTemporaryDir, { recursive: true }); return { sourceTemporaryDir, lambdaZipPath, - javascriptFilePath, - lambdaHandler: this.lambdaHandler, }; } diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts index 6a3d8b4400a6..0c53265e6302 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.spec.ts @@ -2,16 +2,17 @@ import { Test, TestingModule } from '@nestjs/testing'; import { getRepositoryToken } from '@nestjs/typeorm'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; +import { EmailService } from 'src/engine/core-modules/email/email.service'; +import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { User } from 'src/engine/core-modules/user/user.entity'; +import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service'; 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 { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; -import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service'; import { WorkspaceService } from './workspace.service'; @@ -66,6 +67,10 @@ describe('WorkspaceService', () => { provide: WorkspaceInvitationService, useValue: {}, }, + { + provide: FeatureFlagService, + useValue: {}, + }, ], }).compile(); diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts index 0be6fd02f97d..38befbf6e12d 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts @@ -1,6 +1,6 @@ import { BadRequestException } from '@nestjs/common'; -import { InjectRepository } from '@nestjs/typeorm'; import { ModuleRef } from '@nestjs/core'; +import { InjectRepository } from '@nestjs/typeorm'; import assert from 'assert'; @@ -8,6 +8,7 @@ import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { Repository } from 'typeorm'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; import { User } from 'src/engine/core-modules/user/user.entity'; @@ -17,6 +18,7 @@ import { WorkspaceActivationStatus, } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service'; +import { DEFAULT_FEATURE_FLAGS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags'; // eslint-disable-next-line @nx/workspace-inject-workspace-repository export class WorkspaceService extends TypeOrmQueryService<Workspace> { @@ -29,6 +31,7 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> { @InjectRepository(UserWorkspace, 'core') private readonly userWorkspaceRepository: Repository<UserWorkspace>, private readonly workspaceManagerService: WorkspaceManagerService, + private readonly featureFlagService: FeatureFlagService, private readonly billingSubscriptionService: BillingSubscriptionService, private moduleRef: ModuleRef, ) { @@ -69,6 +72,11 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> { activationStatus: WorkspaceActivationStatus.ONGOING_CREATION, }); + await this.featureFlagService.enableFeatureFlags( + DEFAULT_FEATURE_FLAGS, + user.defaultWorkspaceId, + ); + await this.workspaceManagerService.init(user.defaultWorkspaceId); await this.userWorkspaceService.createWorkspaceMember( user.defaultWorkspaceId, diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts index 040b945329d4..bd03d0e12c88 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts @@ -5,7 +5,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; -import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module'; @@ -13,12 +13,12 @@ import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-works import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; import { UserWorkspaceResolver } from 'src/engine/core-modules/user-workspace/user-workspace.resolver'; import { User } from 'src/engine/core-modules/user/user.entity'; +import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module'; import { WorkspaceWorkspaceMemberListener } from 'src/engine/core-modules/workspace/workspace-workspace-member.listener'; import { WorkspaceResolver } from 'src/engine/core-modules/workspace/workspace.resolver'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module'; -import { WorkspaceInvitationModule } from 'src/engine/core-modules/workspace-invitation/workspace-invitation.module'; import { workspaceAutoResolverOpts } from './workspace.auto-resolver-opts'; import { Workspace } from './workspace.entity'; @@ -35,11 +35,12 @@ import { WorkspaceService } from './services/workspace.service'; FileUploadModule, WorkspaceMetadataCacheModule, NestjsQueryTypeOrmModule.forFeature( - [User, Workspace, UserWorkspace, FeatureFlagEntity], + [User, Workspace, UserWorkspace], 'core', ), UserWorkspaceModule, WorkspaceManagerModule, + FeatureFlagModule, DataSourceModule, OnboardingModule, TypeORMModule, diff --git a/packages/twenty-server/src/engine/metadata-modules/constants/search-vector-field.constants.ts b/packages/twenty-server/src/engine/metadata-modules/constants/search-vector-field.constants.ts new file mode 100644 index 000000000000..498dca583cc2 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/constants/search-vector-field.constants.ts @@ -0,0 +1,5 @@ +export const SEARCH_VECTOR_FIELD = { + name: 'searchVector', + label: 'Search vector', + description: 'Field used for full-text search', +} as const; diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type.ts index 1efa0eeffff5..80f4689aef74 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type.ts @@ -12,6 +12,7 @@ export enum FieldActorSource { API = 'API', IMPORT = 'IMPORT', MANUAL = 'MANUAL', + SYSTEM = 'SYSTEM', } export const actorCompositeType: CompositeType = { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata-validation.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata-validation.service.ts new file mode 100644 index 000000000000..81ede4ec4faa --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata-validation.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@nestjs/common'; + +import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; + +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { + FieldMetadataException, + FieldMetadataExceptionCode, +} from 'src/engine/metadata-modules/field-metadata/field-metadata.exception'; + +@Injectable() +export class FieldMetadataValidationService< + T extends FieldMetadataType | 'default' = 'default', +> { + constructor() {} + + validateSettingsOrThrow({ + fieldType, + settings, + }: { + fieldType: FieldMetadataType; + settings: FieldMetadataSettings<T>; + }) { + switch (fieldType) { + case FieldMetadataType.NUMBER: + this.validateNumberSettings(settings); + break; + default: + break; + } + } + + private validateNumberSettings(settings: FieldMetadataSettings<T>) { + if ('decimals' in settings) { + const { decimals } = settings; + + if ( + decimals !== undefined && + (decimals < 0 || !Number.isInteger(decimals)) + ) { + throw new FieldMetadataException( + `Decimals value "${decimals}" must be a positive integer`, + FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + ); + } + } + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts index 3f5413aeb2b8..5035fd3ed760 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.entity.ts @@ -47,6 +47,7 @@ export enum FieldMetadataType { RICH_TEXT = 'RICH_TEXT', ACTOR = 'ACTOR', ARRAY = 'ARRAY', + TS_VECTOR = 'TS_VECTOR', } @Entity('fieldMetadata') diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts index 377575ba9bfd..b6cb8e8ed1d7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.module.ts @@ -12,6 +12,7 @@ import { ActorModule } from 'src/engine/core-modules/actor/actor.module'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { FieldMetadataDTO } from 'src/engine/metadata-modules/field-metadata/dtos/field-metadata.dto'; +import { FieldMetadataValidationService } from 'src/engine/metadata-modules/field-metadata/field-metadata-validation.service'; import { FieldMetadataResolver } from 'src/engine/metadata-modules/field-metadata/field-metadata.resolver'; import { FieldMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/field-metadata/interceptors/field-metadata-graphql-api-exception.interceptor'; import { IsFieldMetadataDefaultValue } from 'src/engine/metadata-modules/field-metadata/validators/is-field-metadata-default-value.validator'; @@ -44,7 +45,11 @@ import { UpdateFieldInput } from './dtos/update-field.input'; TypeORMModule, ActorModule, ], - services: [IsFieldMetadataDefaultValue, FieldMetadataService], + services: [ + IsFieldMetadataDefaultValue, + FieldMetadataService, + FieldMetadataValidationService, + ], resolvers: [ { EntityClass: FieldMetadataEntity, diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts index ac2c5b1d438d..ae56a6d3ab60 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/field-metadata.service.ts @@ -55,7 +55,9 @@ import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global. import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; import { WorkspaceMigrationRunnerService } from 'src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service'; import { ViewFieldWorkspaceEntity } from 'src/modules/view/standard-objects/view-field.workspace-entity'; +import { isDefined } from 'src/utils/is-defined'; +import { FieldMetadataValidationService } from './field-metadata-validation.service'; import { FieldMetadataEntity, FieldMetadataType, @@ -82,6 +84,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit private readonly typeORMService: TypeORMService, private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly fieldMetadataValidationService: FieldMetadataValidationService, ) { super(fieldMetadataRepository); } @@ -156,13 +159,7 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit ); } - this.validateFieldMetadataInput<CreateFieldInput>( - fieldMetadataInput, - objectMetadata, - ); - - console.time('createOne save'); - const createdFieldMetadata = await fieldMetadataRepository.save({ + const fieldMetadataForCreate = { id: v4(), createdAt: new Date(), updatedAt: new Date(), @@ -183,7 +180,18 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit : undefined, isActive: true, isCustom: true, - }); + }; + + this.validateFieldMetadata<CreateFieldInput>( + fieldMetadataForCreate.type, + fieldMetadataForCreate, + objectMetadata, + ); + + console.time('createOne save'); + const createdFieldMetadata = await fieldMetadataRepository.save( + fieldMetadataForCreate, + ); console.timeEnd('createOne save'); @@ -390,11 +398,6 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit } } - this.validateFieldMetadataInput<UpdateFieldInput>( - fieldMetadataInput, - objectMetadata, - ); - const updatableFieldInput = existingFieldMetadata.isCustom === false ? this.buildUpdatableStandardFieldInput( @@ -403,21 +406,21 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit ) : fieldMetadataInput; - // We're running field update under a transaction, so we can rollback if migration fails - await fieldMetadataRepository.update(id, { + const fieldMetadataForUpdate = { ...updatableFieldInput, - defaultValue: - // Todo: we handle default value for all field types. - ![ - FieldMetadataType.SELECT, - FieldMetadataType.MULTI_SELECT, - FieldMetadataType.BOOLEAN, - ].includes(existingFieldMetadata.type) - ? existingFieldMetadata.defaultValue - : updatableFieldInput.defaultValue !== null - ? updatableFieldInput.defaultValue - : null, - }); + defaultValue: isDefined(updatableFieldInput.defaultValue) + ? updatableFieldInput.defaultValue + : existingFieldMetadata.defaultValue, + }; + + this.validateFieldMetadata<UpdateFieldInput>( + existingFieldMetadata.type, + fieldMetadataForUpdate, + objectMetadata, + ); + + // We're running field update under a transaction, so we can rollback if migration fails + await fieldMetadataRepository.update(id, fieldMetadataForUpdate); const updatedFieldMetadata = await fieldMetadataRepository.findOne({ where: { id }, @@ -705,9 +708,11 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit } } - private validateFieldMetadataInput< - T extends UpdateFieldInput | CreateFieldInput, - >(fieldMetadataInput: T, objectMetadata: ObjectMetadataEntity): T { + private validateFieldMetadata<T extends UpdateFieldInput | CreateFieldInput>( + fieldMetadataType: FieldMetadataType, + fieldMetadataInput: T, + objectMetadata: ObjectMetadataEntity, + ): T { if (fieldMetadataInput.name) { try { validateFieldNameValidityOrThrow(fieldMetadataInput.name); @@ -737,6 +742,15 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit } } + if (fieldMetadataInput.isNullable === false) { + if (!isDefined(fieldMetadataInput.defaultValue)) { + throw new FieldMetadataException( + 'Default value is required for non nullable fields', + FieldMetadataExceptionCode.INVALID_FIELD_INPUT, + ); + } + } + if (fieldMetadataInput.options) { for (const option of fieldMetadataInput.options) { if (exceedsDatabaseIdentifierMaximumLength(option.value)) { @@ -748,6 +762,13 @@ export class FieldMetadataService extends TypeOrmQueryService<FieldMetadataEntit } } + if (fieldMetadataInput.settings) { + this.fieldMetadataValidationService.validateSettingsOrThrow({ + fieldType: fieldMetadataType, + settings: fieldMetadataInput.settings, + }); + } + return fieldMetadataInput; } } diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts index 7fa4f39c09f8..e0d40c0f7ccf 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface.ts @@ -12,6 +12,7 @@ type FieldMetadataDefaultSettings = { type FieldMetadataNumberSettings = { dataType: NumberDataType; + decimals?: number; }; type FieldMetadataDateSettings = { diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts index 7367a1dac737..53a7a67a6159 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface.ts @@ -22,4 +22,6 @@ export interface FieldMetadataInterface< fromRelationMetadata?: RelationMetadataEntity; toRelationMetadata?: RelationMetadataEntity; isCustom?: boolean; + generatedType?: 'STORED' | 'VIRTUAL'; + asExpression?: string; } diff --git a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts index 263448dab8e2..bc24729a7277 100644 --- a/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/field-metadata/utils/compute-column-name.util.ts @@ -11,6 +11,11 @@ import { pascalCase } from 'src/utils/pascal-case'; type ComputeColumnNameOptions = { isForeignKey?: boolean }; +export type FieldTypeAndNameMetadata = { + name: string; + type: FieldMetadataType; +}; + export function computeColumnName( fieldName: string, options?: ComputeColumnNameOptions, @@ -48,13 +53,16 @@ export function computeCompositeColumnName( export function computeCompositeColumnName< T extends FieldMetadataType | 'default', >( - fieldMetadata: FieldMetadataInterface<T>, + fieldMetadata: FieldTypeAndNameMetadata | FieldMetadataInterface<T>, compositeProperty: CompositeProperty, ): string; export function computeCompositeColumnName< T extends FieldMetadataType | 'default', >( - fieldMetadataOrFieldName: FieldMetadataInterface<T> | string, + fieldMetadataOrFieldName: + | FieldTypeAndNameMetadata + | FieldMetadataInterface<T> + | string, compositeProperty: CompositeProperty, ): string { const generateName = (name: string) => { diff --git a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts index 23f4e7bb8d7d..07471ac9b2da 100644 --- a/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/index-metadata/index-metadata.entity.ts @@ -13,6 +13,11 @@ import { import { IndexFieldMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +export enum IndexType { + BTREE = 'BTREE', + GIN = 'GIN', +} + @Entity('indexMetadata') export class IndexMetadataEntity { @PrimaryGeneratedColumn('uuid') @@ -48,4 +53,15 @@ export class IndexMetadataEntity { @UpdateDateColumn({ type: 'timestamptz' }) updatedAt: Date; + + @Column({ default: false }) + isCustom: boolean; + + @Column({ + type: 'enum', + enum: IndexType, + default: IndexType.BTREE, + nullable: false, + }) + indexType?: IndexType; } 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 97d9fdb9b3a7..f84572c33e8c 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 @@ -1,10 +1,14 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { isDefined } from 'class-validator'; import { Repository } from 'typeorm'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; +import { + IndexMetadataEntity, + IndexType, +} from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name'; 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'; @@ -28,6 +32,8 @@ export class IndexMetadataService { workspaceId: string, objectMetadata: ObjectMetadataEntity, fieldMetadataToIndex: Partial<FieldMetadataEntity>[], + isCustom: boolean, + indexType?: IndexType, ) { const tableName = computeObjectTargetTable(objectMetadata); @@ -53,6 +59,8 @@ export class IndexMetadataService { ), workspaceId, objectMetadataId: objectMetadata.id, + ...(isDefined(indexType) ? { indexType: indexType } : {}), + isCustom: isCustom, }); } catch (error) { throw new Error( @@ -74,6 +82,7 @@ export class IndexMetadataService { action: WorkspaceMigrationIndexActionType.CREATE, columns: columnNames, name: indexName, + type: indexType, }, ], } satisfies WorkspaceMigrationTableAction; diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts index 94a721dec53d..14d9d58c2200 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.module.ts @@ -10,9 +10,11 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; +import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module'; import { FieldMetadataEntity } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { IndexMetadataModule } from 'src/engine/metadata-modules/index-metadata/index-metadata.module'; import { BeforeUpdateOneObject } from 'src/engine/metadata-modules/object-metadata/hooks/before-update-one-object.hook'; import { ObjectMetadataGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/object-metadata/interceptors/object-metadata-graphql-api-exception.interceptor'; import { ObjectMetadataResolver } from 'src/engine/metadata-modules/object-metadata/object-metadata.resolver'; @@ -44,6 +46,8 @@ import { UpdateObjectPayload } from './dtos/update-object.input'; WorkspaceMigrationRunnerModule, WorkspaceMetadataVersionModule, RemoteTableRelationsModule, + IndexMetadataModule, + FeatureFlagModule, ], services: [ObjectMetadataService], resolvers: [ diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts index c0240c6c2f1b..ff08237d20fc 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/object-metadata.service.ts @@ -5,19 +5,30 @@ import console from 'console'; import { Query, QueryOptions } from '@ptc-org/nestjs-query-core'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; +import { isDefined } from 'class-validator'; import { FindManyOptions, FindOneOptions, In, Repository } from 'typeorm'; import { FieldMetadataSettings } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata-settings.interface'; +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { TypeORMService } from 'src/database/typeorm/typeorm.service'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +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 { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service'; import { FieldMetadataEntity, FieldMetadataType, } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; +import { + computeColumnName, + FieldTypeAndNameMetadata, +} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; +import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; +import { IndexMetadataService } from 'src/engine/metadata-modules/index-metadata/index-metadata.service'; import { DeleteOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/delete-object.input'; import { UpdateOneObjectInput } from 'src/engine/metadata-modules/object-metadata/dtos/update-object.input'; +import { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants'; import { ObjectMetadataException, ObjectMetadataExceptionCode, @@ -33,12 +44,15 @@ import { RelationToDelete } from 'src/engine/metadata-modules/relation-metadata/ import { RemoteTableRelationsService } from 'src/engine/metadata-modules/remote-server/remote-table/remote-table-relations/remote-table-relations.service'; import { mapUdtNameToFieldType } from 'src/engine/metadata-modules/remote-server/remote-table/utils/udt-name-mapper.util'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; +import { TsVectorColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory'; import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; import { WorkspaceMigrationColumnActionType, WorkspaceMigrationColumnDrop, + WorkspaceMigrationTableAction, WorkspaceMigrationTableActionType, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.service'; import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; @@ -58,6 +72,7 @@ import { createForeignKeyDeterministicUuid, createRelationDeterministicUuid, } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/create-deterministic-uuid.util'; +import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; import { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; @@ -79,12 +94,18 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt private readonly remoteTableRelationsService: RemoteTableRelationsService, + private readonly tsVectorColumnActionFactory: TsVectorColumnActionFactory, + private readonly dataSourceService: DataSourceService, private readonly typeORMService: TypeORMService, private readonly workspaceMigrationService: WorkspaceMigrationService, + + private readonly indexMetadataService: IndexMetadataService, + private readonly featureFlagService: FeatureFlagService, private readonly workspaceMigrationRunnerService: WorkspaceMigrationRunnerService, private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, + private readonly workspaceMigrationFactory: WorkspaceMigrationFactory, ) { super(objectMetadataRepository); } @@ -350,6 +371,18 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt objectMetadataInput, createdObjectMetadata, ); + + const isSearchEnabled = await this.featureFlagService.isFeatureEnabled( + FeatureFlagKey.IsSearchEnabled, + objectMetadataInput.workspaceId, + ); + + if (isSearchEnabled) { + await this.createSearchVectorField( + objectMetadataInput, + createdObjectMetadata, + ); + } } else { await this.remoteTableRelationsService.createForeignKeysMetadataAndMigrations( objectMetadataInput.workspaceId, @@ -533,9 +566,39 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt objectMetadataInput.primaryKeyFieldMetadataSettings, ); - return this.workspaceMigrationService.createCustomMigration( + await this.workspaceMigrationService.createCustomMigration( generateMigrationName(`create-${createdObjectMetadata.nameSingular}`), createdObjectMetadata.workspaceId, + [ + { + name: computeObjectTargetTable(createdObjectMetadata), + action: WorkspaceMigrationTableActionType.CREATE, + } satisfies WorkspaceMigrationTableAction, + ], + ); + + for (const fieldMetadata of createdObjectMetadata.fields) { + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName(`create-${fieldMetadata.name}`), + createdObjectMetadata.workspaceId, + [ + { + name: computeObjectTargetTable(createdObjectMetadata), + action: WorkspaceMigrationTableActionType.ALTER, + columns: this.workspaceMigrationFactory.createColumnActions( + WorkspaceMigrationColumnActionType.CREATE, + fieldMetadata, + ), + }, + ] satisfies WorkspaceMigrationTableAction[], + ); + } + + await this.workspaceMigrationService.createCustomMigration( + generateMigrationName( + `create-${createdObjectMetadata.nameSingular}-relations`, + ), + createdObjectMetadata.workspaceId, buildMigrationsForCustomObjectRelations( createdObjectMetadata, activityTargetObjectMetadata, @@ -548,6 +611,72 @@ export class ObjectMetadataService extends TypeOrmQueryService<ObjectMetadataEnt ); } + private async createSearchVectorField( + objectMetadataInput: CreateObjectInput, + createdObjectMetadata: ObjectMetadataEntity, + ) { + const searchVectorFieldMetadata = await this.fieldMetadataRepository.save({ + standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.searchVector, + objectMetadataId: createdObjectMetadata.id, + workspaceId: objectMetadataInput.workspaceId, + isCustom: false, + isActive: false, + isSystem: true, + type: FieldMetadataType.TS_VECTOR, + name: SEARCH_VECTOR_FIELD.name, + label: SEARCH_VECTOR_FIELD.label, + description: SEARCH_VECTOR_FIELD.description, + isNullable: true, + }); + + const searchableFieldForCustomObject = + createdObjectMetadata.labelIdentifierFieldMetadataId + ? createdObjectMetadata.fields.find( + (field) => + field.id === createdObjectMetadata.labelIdentifierFieldMetadataId, + ) + : createdObjectMetadata.fields.find( + (field) => field.name === DEFAULT_LABEL_IDENTIFIER_FIELD_NAME, + ); + + if (!isDefined(searchableFieldForCustomObject)) { + throw new Error('No searchable field found for custom object'); + } + + this.workspaceMigrationService.createCustomMigration( + generateMigrationName( + `update-${createdObjectMetadata.nameSingular}-add-searchVector`, + ), + createdObjectMetadata.workspaceId, + [ + { + name: computeTableName( + createdObjectMetadata.nameSingular, + createdObjectMetadata.isCustom, + ), + action: WorkspaceMigrationTableActionType.ALTER, + columns: this.tsVectorColumnActionFactory.handleCreateAction({ + ...searchVectorFieldMetadata, + defaultValue: undefined, + generatedType: 'STORED', + asExpression: getTsVectorColumnExpressionFromFields([ + searchableFieldForCustomObject as FieldTypeAndNameMetadata, + ]), + options: undefined, + } as FieldMetadataInterface<FieldMetadataType.TS_VECTOR>), + }, + ], + ); + + await this.indexMetadataService.createIndex( + objectMetadataInput.workspaceId, + createdObjectMetadata, + [searchVectorFieldMetadata], + false, + IndexType.GIN, + ); + } + private async createActivityTargetRelation( workspaceId: string, createdObjectMetadata: ObjectMetadataEntity, diff --git a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts index 35ccf55011d2..869c8d1bea32 100644 --- a/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/object-metadata/utils/build-migrations-for-custom-object-relations.util.ts @@ -18,10 +18,6 @@ export const buildMigrationsForCustomObjectRelations = ( noteTargetObjectMetadata: ObjectMetadataEntity, taskTargetObjectMetadata: ObjectMetadataEntity, ): WorkspaceMigrationTableAction[] => [ - { - name: computeObjectTargetTable(createdObjectMetadata), - action: WorkspaceMigrationTableActionType.CREATE, - } satisfies WorkspaceMigrationTableAction, // Add activity target relation { name: computeObjectTargetTable(activityTargetObjectMetadata), 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 affe7e1b7772..474c2f63f3c6 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 @@ -153,6 +153,7 @@ export class RelationMetadataService extends TypeOrmQueryService<RelationMetadat relationMetadataInput.workspaceId, toObjectMetadata, [foreignKeyFieldMetadata, deletedFieldMetadata], + false, ); await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/create-serverless-function-from-file.input.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/create-serverless-function-from-file.input.ts deleted file mode 100644 index 0e9f2885d676..000000000000 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/create-serverless-function-from-file.input.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Field, InputType } from '@nestjs/graphql'; - -import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; - -@InputType() -export class CreateServerlessFunctionFromFileInput { - @IsString() - @IsNotEmpty() - @Field() - name: string; - - @IsString() - @IsOptional() - @Field({ nullable: true }) - description?: string; -} diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input.ts index 63431ad881f7..327044b7bd2f 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/create-serverless-function.input.ts @@ -1,13 +1,16 @@ import { Field, InputType } from '@nestjs/graphql'; -import { IsNotEmpty, IsString } from 'class-validator'; - -import { CreateServerlessFunctionFromFileInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function-from-file.input'; +import { IsNotEmpty, IsOptional, IsString } from 'class-validator'; @InputType() -export class CreateServerlessFunctionInput extends CreateServerlessFunctionFromFileInput { +export class CreateServerlessFunctionInput { @IsString() @IsNotEmpty() @Field() - code: string; + name: string; + + @IsString() + @IsOptional() + @Field({ nullable: true }) + description?: 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 092d2fce762c..e24687cec848 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 @@ -50,11 +50,6 @@ export class ServerlessFunctionDTO { @Field({ nullable: true }) description: string; - @IsString() - @IsNotEmpty() - @Field() - sourceCodeHash: string; - @IsString() @IsNotEmpty() @Field() diff --git a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input.ts b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input.ts index 8c75bf25534c..60dec1d7d581 100644 --- a/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input.ts +++ b/packages/twenty-server/src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input.ts @@ -1,6 +1,7 @@ import { Field, InputType } from '@nestjs/graphql'; -import { IsNotEmpty, IsString, IsUUID } from 'class-validator'; +import { IsNotEmpty, IsObject, IsString, IsUUID } from 'class-validator'; +import graphqlTypeJson from 'graphql-type-json'; import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars'; @@ -21,7 +22,7 @@ export class UpdateServerlessFunctionInput { @Field({ nullable: true }) description?: string; - @IsString() - @Field() - code: string; + @Field(() => graphqlTypeJson) + @IsObject() + code: JSON; } 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 782362bfba94..58406efbda10 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,9 +29,6 @@ export class ServerlessFunctionEntity { @Column({ nullable: true }) latestVersion: string; - @Column({ nullable: false }) - sourceCodeHash: string; - @Column({ nullable: false, default: ServerlessFunctionRuntime.NODE18 }) runtime: ServerlessFunctionRuntime; 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 14f4ed48478d..05440c6c7aef 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 @@ -3,7 +3,6 @@ import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { InjectRepository } from '@nestjs/typeorm'; import graphqlTypeJson from 'graphql-type-json'; -import { FileUpload, GraphQLUpload } from 'graphql-upload'; import { Repository } from 'typeorm'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; @@ -11,7 +10,6 @@ import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature- 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 { CreateServerlessFunctionFromFileInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function-from-file.input'; 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 { ExecuteServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/execute-serverless-function.input'; @@ -63,7 +61,7 @@ export class ServerlessFunctionResolver { } } - @Query(() => String, { nullable: true }) + @Query(() => graphqlTypeJson, { nullable: true }) async getServerlessFunctionSourceCode( @Args('input') input: GetServerlessFunctionSourceCodeInput, @AuthWorkspace() { id: workspaceId }: Workspace, @@ -130,28 +128,6 @@ export class ServerlessFunctionResolver { name: input.name, description: input.description, }, - input.code, - workspaceId, - ); - } catch (error) { - serverlessFunctionGraphQLApiExceptionHandler(error); - } - } - - @Mutation(() => ServerlessFunctionDTO) - async createOneServerlessFunctionFromFile( - @Args({ name: 'file', type: () => GraphQLUpload }) - file: FileUpload, - @Args('input') - input: CreateServerlessFunctionFromFileInput, - @AuthWorkspace() { id: workspaceId }: Workspace, - ) { - try { - await this.checkFeatureFlag(workspaceId); - - return await this.serverlessFunctionService.createOneServerlessFunction( - input, - file, workspaceId, ); } catch (error) { 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 d7c591eb0c8c..191dc9edf414 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 @@ -1,9 +1,11 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { basename, dirname, join } from 'path'; + import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; -import { FileUpload } from 'graphql-upload'; import { Repository } from 'typeorm'; +import deepEqual from 'deep-equal'; 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'; @@ -12,10 +14,9 @@ import { ThrottlerService } from 'src/engine/core-modules/throttler/throttler.se 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 { SOURCE_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/source-file-name'; +import { INDEX_FILE_NAME } from 'src/engine/core-modules/serverless/drivers/constants/index-file-name'; import { ServerlessService } from 'src/engine/core-modules/serverless/serverless.service'; import { getServerlessFolder } from 'src/engine/core-modules/serverless/utils/serverless-get-folder.utils'; -import { CreateServerlessFunctionFromFileInput } from 'src/engine/metadata-modules/serverless-function/dtos/create-serverless-function-from-file.input'; import { UpdateServerlessFunctionInput } from 'src/engine/metadata-modules/serverless-function/dtos/update-serverless-function.input'; import { ServerlessFunctionEntity, @@ -25,10 +26,12 @@ import { ServerlessFunctionException, ServerlessFunctionExceptionCode, } from 'src/engine/metadata-modules/serverless-function/serverless-function.exception'; -import { serverlessFunctionCreateHash } from 'src/engine/metadata-modules/serverless-function/utils/serverless-function-create-hash.utils'; 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<ServerlessFunctionEntity> { @@ -47,7 +50,7 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun workspaceId: string, id: string, version: string, - ) { + ): Promise<{ [filePath: string]: string } | undefined> { const serverlessFunction = await this.serverlessFunctionRepository.findOne({ where: { id, @@ -68,12 +71,20 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun version, }); - const fileStream = await this.fileStorageService.read({ - folderPath, - filename: SOURCE_FILE_NAME, + const indexFileStream = await this.fileStorageService.read({ + folderPath: join(folderPath, 'src'), + filename: INDEX_FILE_NAME, + }); + + const envFileStream = await this.fileStorageService.read({ + folderPath: folderPath, + filename: ENV_FILE_NAME, }); - return await readFileContent(fileStream); + return { + '.env': await readFileContent(envFileStream), + 'src/index.ts': await readFileContent(indexFileStream), + }; } catch (error) { if (error.code === FileStorageExceptionCode.FILE_NOT_FOUND) { return; @@ -132,10 +143,7 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun 'draft', ); - if ( - serverlessFunctionCreateHash(latestCode || '') === - serverlessFunctionCreateHash(draftCode || '') - ) { + if (deepEqual(latestCode, draftCode)) { throw new Error( 'Cannot publish a new version when code has not changed', ); @@ -146,20 +154,6 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun existingServerlessFunction, ); - const draftFolderPath = getServerlessFolder({ - serverlessFunction: existingServerlessFunction, - version: 'draft', - }); - const newFolderPath = getServerlessFolder({ - serverlessFunction: existingServerlessFunction, - version: newVersion, - }); - - await this.fileStorageService.copy({ - from: { folderPath: draftFolderPath }, - to: { folderPath: newFolderPath }, - }); - await super.updateOne(existingServerlessFunction.id, { latestVersion: newVersion, }); @@ -213,9 +207,6 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun name: serverlessFunctionInput.name, description: serverlessFunctionInput.description, syncStatus: ServerlessFunctionSyncStatus.NOT_READY, - sourceCodeHash: serverlessFunctionCreateHash( - serverlessFunctionInput.code, - ), }); const fileFolder = getServerlessFolder({ @@ -223,12 +214,14 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun version: 'draft', }); - await this.fileStorageService.write({ - file: serverlessFunctionInput.code, - name: SOURCE_FILE_NAME, - mimeType: undefined, - folder: fileFolder, - }); + for (const key of Object.keys(serverlessFunctionInput.code)) { + await this.fileStorageService.write({ + file: serverlessFunctionInput.code[key], + name: basename(key), + mimeType: undefined, + folder: join(fileFolder, dirname(key)), + }); + } await this.serverlessService.build(existingServerlessFunction, 'draft'); await super.updateOne(existingServerlessFunction.id, { @@ -259,22 +252,12 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun } async createOneServerlessFunction( - serverlessFunctionInput: CreateServerlessFunctionFromFileInput, - code: FileUpload | string, + serverlessFunctionInput: CreateServerlessFunctionInput, workspaceId: string, ) { - let typescriptCode: string; - - if (typeof code === 'string') { - typescriptCode = code; - } else { - typescriptCode = await readFileContent(code.createReadStream()); - } - const createdServerlessFunction = await super.createOne({ ...serverlessFunctionInput, workspaceId, - sourceCodeHash: serverlessFunctionCreateHash(typescriptCode), layerVersion: LAST_LAYER_VERSION, }); @@ -283,12 +266,14 @@ export class ServerlessFunctionService extends TypeOrmQueryService<ServerlessFun version: 'draft', }); - await this.fileStorageService.write({ - file: typescriptCode, - name: SOURCE_FILE_NAME, - mimeType: undefined, - folder: draftFileFolder, - }); + for (const file of await getBaseTypescriptProjectFiles) { + await this.fileStorageService.write({ + file: file.content, + name: file.name, + mimeType: undefined, + folder: join(draftFileFolder, file.path), + }); + } await this.serverlessService.build(createdServerlessFunction, 'draft'); diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/metadata.constants.ts b/packages/twenty-server/src/engine/metadata-modules/utils/constants/identifier-max-char-length.constants.ts similarity index 100% rename from packages/twenty-server/src/engine/metadata-modules/utils/metadata.constants.ts rename to packages/twenty-server/src/engine/metadata-modules/utils/constants/identifier-max-char-length.constants.ts diff --git a/packages/twenty-server/src/engine/metadata-modules/utils/validate-database-identifier-length.utils.ts b/packages/twenty-server/src/engine/metadata-modules/utils/validate-database-identifier-length.utils.ts index dff01b4f33b3..b798049e5117 100644 --- a/packages/twenty-server/src/engine/metadata-modules/utils/validate-database-identifier-length.utils.ts +++ b/packages/twenty-server/src/engine/metadata-modules/utils/validate-database-identifier-length.utils.ts @@ -1,4 +1,4 @@ -import { IDENTIFIER_MAX_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/metadata.constants'; +import { IDENTIFIER_MAX_CHAR_LENGTH } from 'src/engine/metadata-modules/utils/constants/identifier-max-char-length.constants'; export const exceedsDatabaseIdentifierMaximumLength = (string: string) => { return string.length > IDENTIFIER_MAX_CHAR_LENGTH; diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts index 9af3e89d4b04..41c133621e7e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory.ts @@ -18,6 +18,7 @@ import { WorkspaceMigrationExceptionCode, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception'; +// TODO: could we export this to GraphQL ? export type CompositeFieldMetadataType = | FieldMetadataType.ADDRESS | FieldMetadataType.CURRENCY diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/factories.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/factories.ts index 5bda4bac116a..3dcbe1fac424 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/factories.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/factories.ts @@ -1,8 +1,10 @@ import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory'; import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory'; +import { TsVectorColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory'; export const workspaceColumnActionFactories = [ + TsVectorColumnActionFactory, BasicColumnActionFactory, EnumColumnActionFactory, CompositeColumnActionFactory, diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory.ts new file mode 100644 index 000000000000..dd72948f3264 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory.ts @@ -0,0 +1,52 @@ +import { Injectable, Logger } from '@nestjs/common'; + +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; +import { WorkspaceColumnActionOptions } from 'src/engine/metadata-modules/workspace-migration/interfaces/workspace-column-action-options.interface'; + +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { computeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; +import { ColumnActionAbstractFactory } from 'src/engine/metadata-modules/workspace-migration/factories/column-action-abstract.factory'; +import { fieldMetadataTypeToColumnType } from 'src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util'; +import { + WorkspaceMigrationColumnActionType, + WorkspaceMigrationColumnAlter, + WorkspaceMigrationColumnCreate, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { + WorkspaceMigrationException, + WorkspaceMigrationExceptionCode, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception'; + +export type TsVectorFieldMetadataType = FieldMetadataType.TS_VECTOR; + +@Injectable() +export class TsVectorColumnActionFactory extends ColumnActionAbstractFactory<TsVectorFieldMetadataType> { + protected readonly logger = new Logger(TsVectorColumnActionFactory.name); + + handleCreateAction( + fieldMetadata: FieldMetadataInterface<TsVectorFieldMetadataType>, + ): WorkspaceMigrationColumnCreate[] { + return [ + { + action: WorkspaceMigrationColumnActionType.CREATE, + columnName: computeColumnName(fieldMetadata), + columnType: fieldMetadataTypeToColumnType(fieldMetadata.type), + isNullable: fieldMetadata.isNullable ?? true, + defaultValue: undefined, + generatedType: fieldMetadata.generatedType, + asExpression: fieldMetadata.asExpression, + }, + ]; + } + + protected handleAlterAction( + _currentFieldMetadata: FieldMetadataInterface<TsVectorFieldMetadataType>, + _alteredFieldMetadata: FieldMetadataInterface<TsVectorFieldMetadataType>, + _options?: WorkspaceColumnActionOptions, + ): WorkspaceMigrationColumnAlter[] { + throw new WorkspaceMigrationException( + `TsVectorColumnActionFactory.handleAlterAction has not been implemented yet.`, + WorkspaceMigrationExceptionCode.INVALID_FIELD_METADATA, + ); + } +} diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util.ts index 9d3ff6276cf9..67955d0bb411 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/utils/field-metadata-type-to-column-type.util.ts @@ -38,6 +38,8 @@ export const fieldMetadataTypeToColumnType = <Type extends FieldMetadataType>( return 'enum'; case FieldMetadataType.RAW_JSON: return 'jsonb'; + case FieldMetadataType.TS_VECTOR: + return 'tsvector'; default: throw new WorkspaceMigrationException( `Cannot convert ${fieldMetadataType} to column type.`, diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts index 0c1177fb7921..e731f8cc0193 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.entity.ts @@ -5,6 +5,7 @@ import { PrimaryGeneratedColumn, } from 'typeorm'; +import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { RelationOnDeleteAction } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; export enum WorkspaceMigrationColumnActionType { @@ -30,12 +31,15 @@ export interface WorkspaceMigrationColumnDefinition { isArray?: boolean; isNullable: boolean; defaultValue: any; + generatedType?: 'STORED' | 'VIRTUAL'; + asExpression?: string; } export interface WorkspaceMigrationIndexAction { action: WorkspaceMigrationIndexActionType; name: string; columns: string[]; + type?: IndexType; } export interface WorkspaceMigrationColumnCreate diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts index 6f33cf0137e3..53aa564e4f82 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.factory.ts @@ -8,6 +8,7 @@ import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/fi import { BasicColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/basic-column-action.factory'; import { CompositeColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; import { EnumColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/enum-column-action.factory'; +import { TsVectorColumnActionFactory } from 'src/engine/metadata-modules/workspace-migration/factories/ts-vector-column-action.factory'; import { WorkspaceMigrationColumnAction, WorkspaceMigrationColumnActionType, @@ -30,6 +31,7 @@ export class WorkspaceMigrationFactory { constructor( private readonly basicColumnActionFactory: BasicColumnActionFactory, + private readonly tsVectorColumnActionFactory: TsVectorColumnActionFactory, private readonly enumColumnActionFactory: EnumColumnActionFactory, private readonly compositeColumnActionFactory: CompositeColumnActionFactory, ) { @@ -106,6 +108,10 @@ export class WorkspaceMigrationFactory { FieldMetadataType.PHONES, { factory: this.compositeColumnActionFactory }, ], + [ + FieldMetadataType.TS_VECTOR, + { factory: this.tsVectorColumnActionFactory }, + ], ]); } diff --git a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.module.ts b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.module.ts index 9e0d4072c476..e7ecd97b0601 100644 --- a/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/workspace-migration/workspace-migration.module.ts @@ -4,8 +4,8 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { workspaceColumnActionFactories } from 'src/engine/metadata-modules/workspace-migration/factories/factories'; import { WorkspaceMigrationFactory } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.factory'; -import { WorkspaceMigrationService } from './workspace-migration.service'; import { WorkspaceMigrationEntity } from './workspace-migration.entity'; +import { WorkspaceMigrationService } from './workspace-migration.service'; @Module({ imports: [TypeOrmModule.forFeature([WorkspaceMigrationEntity], 'metadata')], @@ -14,6 +14,10 @@ import { WorkspaceMigrationEntity } from './workspace-migration.entity'; WorkspaceMigrationFactory, WorkspaceMigrationService, ], - exports: [WorkspaceMigrationFactory, WorkspaceMigrationService], + exports: [ + ...workspaceColumnActionFactories, + WorkspaceMigrationFactory, + WorkspaceMigrationService, + ], }) export class WorkspaceMigrationModule {} diff --git a/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts b/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts index b2053e44c814..6f0bc8dffe5e 100644 --- a/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts +++ b/packages/twenty-server/src/engine/object-metadata-repository/metadata-to-repository.mapping.ts @@ -1,5 +1,4 @@ import { BlocklistRepository } from 'src/modules/blocklist/repositories/blocklist.repository'; -import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository'; import { AuditLogRepository } from 'src/modules/timeline/repositiories/audit-log.repository'; import { TimelineActivityRepository } from 'src/modules/timeline/repositiories/timeline-activity.repository'; import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository'; @@ -7,7 +6,6 @@ import { WorkspaceMemberRepository } from 'src/modules/workspace-member/reposito export const metadataToRepositoryMapping = { AuditLogWorkspaceEntity: AuditLogRepository, BlocklistWorkspaceEntity: BlocklistRepository, - ConnectedAccountWorkspaceEntity: ConnectedAccountRepository, TimelineActivityWorkspaceEntity: TimelineActivityRepository, WorkspaceMemberWorkspaceEntity: WorkspaceMemberRepository, }; diff --git a/packages/twenty-server/src/engine/twenty-orm/base.workspace-entity.ts b/packages/twenty-server/src/engine/twenty-orm/base.workspace-entity.ts index d9a19a689301..53355304c346 100644 --- a/packages/twenty-server/src/engine/twenty-orm/base.workspace-entity.ts +++ b/packages/twenty-server/src/engine/twenty-orm/base.workspace-entity.ts @@ -55,5 +55,5 @@ export abstract class BaseWorkspaceEntity { }, }) @WorkspaceIsNullable() - deletedAt?: string | null; + deletedAt: string | null; } diff --git a/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts b/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts index 0b9b5201d2b7..efbfed843108 100644 --- a/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts +++ b/packages/twenty-server/src/engine/twenty-orm/custom.workspace-entity.ts @@ -1,8 +1,11 @@ +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 { DEFAULT_LABEL_IDENTIFIER_FIELD_NAME } from 'src/engine/metadata-modules/object-metadata/object-metadata.constants'; import { RelationMetadataType, RelationOnDeleteAction, @@ -10,10 +13,12 @@ import { import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceCustomEntity } from 'src/engine/twenty-orm/decorators/workspace-custom-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; +import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.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 { CUSTOM_OBJECT_STANDARD_FIELD_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-field-ids'; +import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity'; import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; @@ -136,4 +141,22 @@ export class CustomWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceIsNullable() @WorkspaceIsSystem() timelineActivities: TimelineActivityWorkspaceEntity[]; + + @WorkspaceField({ + standardId: CUSTOM_OBJECT_STANDARD_FIELD_IDS.searchVector, + type: FieldMetadataType.TS_VECTOR, + label: SEARCH_VECTOR_FIELD.label, + description: SEARCH_VECTOR_FIELD.description, + generatedType: 'STORED', + asExpression: getTsVectorColumnExpressionFromFields([ + { + name: DEFAULT_LABEL_IDENTIFIER_FIELD_NAME, + type: FieldMetadataType.TEXT, + }, + ]), + }) + @WorkspaceIsNullable() + @WorkspaceIsSystem() + @WorkspaceIndex({ indexType: IndexType.GIN }) + [SEARCH_VECTOR_FIELD.name]: any; } diff --git a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts index c6dd8bfd7a4f..c0ffcc5df721 100644 --- a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts +++ b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-field.decorator.ts @@ -20,6 +20,8 @@ export interface WorkspaceFieldOptions< options?: FieldMetadataOptions<T>; settings?: FieldMetadataSettings<T>; isActive?: boolean; + generatedType?: 'STORED' | 'VIRTUAL'; + asExpression?: string; } export function WorkspaceField<T extends FieldMetadataType>( @@ -76,6 +78,8 @@ export function WorkspaceField<T extends FieldMetadataType>( gate, isDeprecated, isActive: options.isActive, + asExpression: options.asExpression, + generatedType: options.generatedType, }); }; } diff --git a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-index.decorator.ts b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-index.decorator.ts index bf2e43201c30..ef39e0cceff5 100644 --- a/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-index.decorator.ts +++ b/packages/twenty-server/src/engine/twenty-orm/decorators/workspace-index.decorator.ts @@ -1,18 +1,36 @@ +import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name'; import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; +import { getColumnsForIndex } from 'src/engine/twenty-orm/utils/get-default-columns-for-index.util'; import { convertClassNameToObjectMetadataName } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/convert-class-to-object-metadata-name.util'; +import { isDefined } from 'src/utils/is-defined'; import { TypedReflect } from 'src/utils/typed-reflect'; -export function WorkspaceIndex(): PropertyDecorator; -export function WorkspaceIndex(columns: string[]): ClassDecorator; +export type WorkspaceIndexMetadata = { + columns?: string[]; + indexType?: IndexType; +}; + +export function WorkspaceIndex( + metadata?: WorkspaceIndexMetadata, +): PropertyDecorator; +export function WorkspaceIndex( + metadata: WorkspaceIndexMetadata, +): ClassDecorator; export function WorkspaceIndex( - columns?: string[], + metadata?: WorkspaceIndexMetadata, ): PropertyDecorator | ClassDecorator { return (target: any, propertyKey: string | symbol) => { - if (propertyKey === undefined && columns === undefined) { + if (propertyKey === undefined && metadata === undefined) { throw new Error('Class level WorkspaceIndex should be used with columns'); } + if (propertyKey !== undefined && metadata?.columns !== undefined) { + throw new Error( + 'Property level WorkspaceIndex should not be used with columns', + ); + } + const gate = TypedReflect.getMetadata( 'workspace:gate-metadata-args', target, @@ -20,29 +38,46 @@ export function WorkspaceIndex( ); // TODO: handle composite field metadata types + if (isDefined(metadata?.columns)) { + const columns = metadata.columns; + + if (columns.length > 0) { + metadataArgsStorage.addIndexes({ + name: `IDX_${generateDeterministicIndexName([ + convertClassNameToObjectMetadataName(target.name), + ...columns, + ])}`, + columns, + target: target, + gate, + ...(isDefined(metadata?.indexType) + ? { type: metadata.indexType } + : {}), + }); + + return; + } + } + + if (isDefined(propertyKey)) { + const additionalDefaultColumnsForIndex = getColumnsForIndex( + metadata?.indexType, + ); + const columns = [ + propertyKey.toString(), + ...additionalDefaultColumnsForIndex, + ]; - if (Array.isArray(columns) && columns.length > 0) { metadataArgsStorage.addIndexes({ name: `IDX_${generateDeterministicIndexName([ - convertClassNameToObjectMetadataName(target.name), + convertClassNameToObjectMetadataName(target.constructor.name), ...columns, ])}`, columns, - target: target, + target: target.constructor, + ...(isDefined(metadata?.indexType) ? { type: metadata.indexType } : {}), gate, }); - - return; } - - metadataArgsStorage.addIndexes({ - name: `IDX_${generateDeterministicIndexName([ - convertClassNameToObjectMetadataName(target.constructor.name), - ...[propertyKey.toString(), 'deletedAt'], - ])}`, - columns: [propertyKey.toString(), 'deletedAt'], - target: target.constructor, - gate, - }); }; } diff --git a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface.ts b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface.ts index 09c339c936e5..46ad1132dadc 100644 --- a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface.ts +++ b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-field-metadata-args.interface.ts @@ -89,4 +89,14 @@ export interface WorkspaceFieldMetadataArgs { * Is active field. */ readonly isActive?: boolean; + + /** + * Is active field. + */ + readonly generatedType?: 'STORED' | 'VIRTUAL'; + + /** + * Is active field. + */ + readonly asExpression?: string; } diff --git a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-index-metadata-args.interface.ts b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-index-metadata-args.interface.ts index 9412e417c5f5..0ad260070947 100644 --- a/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-index-metadata-args.interface.ts +++ b/packages/twenty-server/src/engine/twenty-orm/interfaces/workspace-index-metadata-args.interface.ts @@ -1,5 +1,7 @@ import { Gate } from 'src/engine/twenty-orm/interfaces/gate.interface'; +import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; + export interface WorkspaceIndexMetadataArgs { /** * Class to which index is applied. @@ -17,6 +19,11 @@ export interface WorkspaceIndexMetadataArgs { */ columns: string[]; + /* + * Index type. Defaults to Btree. + */ + type?: IndexType; + /** * Field gate. */ diff --git a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts index ea3c3f9e84bc..bb4327cc8b1b 100644 --- a/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts +++ b/packages/twenty-server/src/engine/twenty-orm/repository/workspace.repository.ts @@ -664,7 +664,7 @@ export class WorkspaceRepository< return formatData(data, objectMetadata) as T; } - private async formatResult<T>( + async formatResult<T>( data: T, objectMetadata?: ObjectMetadataMapItem, ): Promise<T> { diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-default-columns-for-index.util.spec.ts b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-default-columns-for-index.util.spec.ts new file mode 100644 index 000000000000..fad8a9cd18be --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/utils/__tests__/get-default-columns-for-index.util.spec.ts @@ -0,0 +1,22 @@ +import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; +import { getColumnsForIndex } from 'src/engine/twenty-orm/utils/get-default-columns-for-index.util'; + +describe('getColumnsForIndex', () => { + it('should return ["deletedAt"] when indexType is undefined', () => { + const result = getColumnsForIndex(); + + expect(result).toEqual(['deletedAt']); + }); + + it('should return an empty array when indexType is IndexType.GIN', () => { + const result = getColumnsForIndex(IndexType.GIN); + + expect(result).toEqual([]); + }); + + it('should return ["deletedAt"] when indexType is IndexType.BTREE', () => { + const result = getColumnsForIndex(IndexType.BTREE); + + expect(result).toEqual(['deletedAt']); + }); +}); diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts index f6f31f32c9db..cd3851393678 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-data.util.ts @@ -1,9 +1,11 @@ -import { isPlainObject } from '@nestjs/common/utils/shared.utils'; +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; -import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; import { ObjectMetadataMapItem } from 'src/engine/metadata-modules/utils/generate-object-metadata-map.util'; -import { getCompositeFieldMetadataCollection } from 'src/engine/twenty-orm/utils/get-composite-field-metadata-collection'; +import { CompositeFieldMetadataType } from 'src/engine/metadata-modules/workspace-migration/factories/composite-column-action.factory'; +import { capitalize } from 'src/utils/capitalize'; export function formatData<T>( data: T, @@ -17,49 +19,70 @@ export function formatData<T>( return data.map((item) => formatData(item, objectMetadata)) as T; } - const compositeFieldMetadataCollection = - getCompositeFieldMetadataCollection(objectMetadata); - - const compositeFieldMetadataMap = new Map( - compositeFieldMetadataCollection.map((fieldMetadata) => [ - fieldMetadata.name, - fieldMetadata, - ]), - ); - const newData: object = {}; + const newData: Record<string, any> = {}; for (const [key, value] of Object.entries(data)) { - const fieldMetadata = compositeFieldMetadataMap.get(key); + const fieldMetadata = objectMetadata.fields[key]; if (!fieldMetadata) { - if (isPlainObject(value)) { - newData[key] = formatData(value, objectMetadata); - } else { - newData[key] = value; - } - continue; + throw new Error( + `Field metadata for field "${key}" is missing in object metadata`, + ); } - const compositeType = compositeTypeDefinitions.get(fieldMetadata.type); + if (isCompositeFieldMetadataType(fieldMetadata.type)) { + const formattedCompositeField = formatCompositeField( + value, + fieldMetadata, + ); - if (!compositeType) { - continue; + Object.assign(newData, formattedCompositeField); + } else { + newData[key] = formatFieldMetadataValue(value, fieldMetadata); } + } - for (const compositeProperty of compositeType.properties) { - const compositeKey = computeCompositeColumnName( - fieldMetadata.name, - compositeProperty, - ); - const value = data?.[key]?.[compositeProperty.name]; + return newData as T; +} - if (value === undefined || value === null) { - continue; - } +function formatCompositeField( + value: any, + fieldMetadata: FieldMetadataInterface, +): Record<string, any> { + const compositeType = compositeTypeDefinitions.get( + fieldMetadata.type as CompositeFieldMetadataType, + ); - newData[compositeKey] = data[key][compositeProperty.name]; + if (!compositeType) { + throw new Error( + `Composite type definition not found for type: ${fieldMetadata.type}`, + ); + } + + const formattedCompositeField: Record<string, any> = {}; + + for (const property of compositeType.properties) { + const subFieldKey = property.name; + const fullFieldName = `${fieldMetadata.name}${capitalize(subFieldKey)}`; + + if (value && value[subFieldKey] !== undefined) { + formattedCompositeField[fullFieldName] = formatFieldMetadataValue( + value[subFieldKey], + property as unknown as FieldMetadataInterface, + ); } } - return newData as T; + return formattedCompositeField; +} + +function formatFieldMetadataValue( + value: any, + fieldMetadata: FieldMetadataInterface, +) { + if (fieldMetadata.type === FieldMetadataType.RAW_JSON) { + return JSON.parse(value as string); + } + + return value; } diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts index 81949e0743a3..2b29ea908cd8 100644 --- a/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts +++ b/packages/twenty-server/src/engine/twenty-orm/utils/format-result.util.ts @@ -1,6 +1,9 @@ import { isPlainObject } from '@nestjs/common/utils/shared.utils'; +import { FieldMetadataInterface } from 'src/engine/metadata-modules/field-metadata/interfaces/field-metadata.interface'; + import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { computeCompositeColumnName } from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; import { RelationMetadataEntity } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; import { @@ -81,9 +84,15 @@ export function formatResult<T>( if (!compositePropertyArgs && !relationMetadata) { if (isPlainObject(value)) { newData[key] = formatResult(value, objectMetadata, objectMetadataMap); + } else if (objectMetadata.fields[key]) { + newData[key] = formatFieldMetadataValue( + value, + objectMetadata.fields[key], + ); } else { newData[key] = value; } + continue; } @@ -129,3 +138,18 @@ export function formatResult<T>( return newData as T; } + +function formatFieldMetadataValue( + value: any, + fieldMetadata: FieldMetadataInterface, +) { + if ( + typeof value === 'string' && + (fieldMetadata.type === FieldMetadataType.MULTI_SELECT || + fieldMetadata.type === FieldMetadataType.ARRAY) + ) { + return value.replace(/{|}/g, '').split(','); + } + + return value; +} diff --git a/packages/twenty-server/src/engine/twenty-orm/utils/get-default-columns-for-index.util.ts b/packages/twenty-server/src/engine/twenty-orm/utils/get-default-columns-for-index.util.ts new file mode 100644 index 000000000000..b97d086a6299 --- /dev/null +++ b/packages/twenty-server/src/engine/twenty-orm/utils/get-default-columns-for-index.util.ts @@ -0,0 +1,10 @@ +import { IndexType } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; + +export const getColumnsForIndex = (indexType?: IndexType) => { + switch (indexType) { + case IndexType.GIN: + return []; + default: + return ['deletedAt']; + } +}; diff --git a/packages/twenty-server/src/engine/utils/get-resolver-name.util.ts b/packages/twenty-server/src/engine/utils/get-resolver-name.util.ts index 77a30372ae2f..f274475e7008 100644 --- a/packages/twenty-server/src/engine/utils/get-resolver-name.util.ts +++ b/packages/twenty-server/src/engine/utils/get-resolver-name.util.ts @@ -33,6 +33,8 @@ export const getResolverName = ( return `delete${pascalCase(objectMetadata.namePlural)}`; case 'destroyMany': return `destroy${pascalCase(objectMetadata.namePlural)}`; + case 'search': + return `search${pascalCase(objectMetadata.namePlural)}`; default: throw new Error(`Unknown resolver type: ${type}`); } diff --git a/packages/twenty-server/src/engine/utils/should-seed-workspace-favorite.ts b/packages/twenty-server/src/engine/utils/should-seed-workspace-favorite.ts new file mode 100644 index 000000000000..9668a7a4eda4 --- /dev/null +++ b/packages/twenty-server/src/engine/utils/should-seed-workspace-favorite.ts @@ -0,0 +1,9 @@ +import { STANDARD_OBJECT_IDS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/standard-object-ids'; + +export const shouldSeedWorkspaceFavorite = ( + objectMetadataId, + objectMetadataMap, +): boolean => + objectMetadataId !== + objectMetadataMap[STANDARD_OBJECT_IDS.workflowVersion]?.id && + objectMetadataId !== objectMetadataMap[STANDARD_OBJECT_IDS.workflowRun]?.id; diff --git a/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/demo-objects-prefill-data.ts b/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/demo-objects-prefill-data.ts index 226e5557c82b..73b36137db03 100644 --- a/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/demo-objects-prefill-data.ts +++ b/packages/twenty-server/src/engine/workspace-manager/demo-objects-prefill-data/demo-objects-prefill-data.ts @@ -2,6 +2,7 @@ import { DataSource, EntityManager } from 'typeorm'; import { seedWorkspaceFavorites } from 'src/database/typeorm-seeds/workspace/favorites'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { shouldSeedWorkspaceFavorite } from 'src/engine/utils/should-seed-workspace-favorite'; import { companyPrefillDemoData } from 'src/engine/workspace-manager/demo-objects-prefill-data/company'; import { opportunityPrefillDemoData } from 'src/engine/workspace-manager/demo-objects-prefill-data/opportunity'; import { personPrefillDemoData } from 'src/engine/workspace-manager/demo-objects-prefill-data/person'; @@ -42,7 +43,7 @@ export const demoObjectsPrefillData = async ( await seedWorkspaceFavorites( viewDefinitionsWithId - .filter((view) => view.key === 'INDEX') + .filter((view) => view.key === 'INDEX' && shouldSeedWorkspaceFavorite(view.objectMetadataId, objectMetadataMap)) .map((view) => view.id), entityManager, schemaName, diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/company.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/company.ts index 2f3966e1043e..9c5591ada4bf 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/company.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/company.ts @@ -1,5 +1,7 @@ import { EntityManager } from 'typeorm'; +import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; + export const AIRBNB_ID = 'c776ee49-f608-4a77-8cc8-6fe96ae1e43f'; export const QONTO_ID = 'f45ee421-8a3e-4aa5-a1cf-7207cc6754e1'; export const STRIPE_ID = '1f70157c-4ea5-4d81-bc49-e1401abfbb94'; @@ -43,7 +45,7 @@ export const companyPrefillData = async ( addressAddressCountry: 'United States', employees: 5000, position: 1, - createdBySource: 'MANUAL', + createdBySource: FieldActorSource.SYSTEM, createdByWorkspaceMemberId: null, createdByName: 'System', }, @@ -59,7 +61,7 @@ export const companyPrefillData = async ( addressAddressCountry: 'France', employees: 800, position: 2, - createdBySource: 'MANUAL', + createdBySource: FieldActorSource.SYSTEM, createdByWorkspaceMemberId: null, createdByName: 'System', }, @@ -75,7 +77,7 @@ export const companyPrefillData = async ( addressAddressCountry: 'Ireland', employees: 8000, position: 3, - createdBySource: 'MANUAL', + createdBySource: FieldActorSource.SYSTEM, createdByWorkspaceMemberId: null, createdByName: 'System', }, @@ -91,7 +93,7 @@ export const companyPrefillData = async ( addressAddressCountry: 'United States', employees: 800, position: 4, - createdBySource: 'MANUAL', + createdBySource: FieldActorSource.SYSTEM, createdByWorkspaceMemberId: null, createdByName: 'System', }, @@ -107,7 +109,7 @@ export const companyPrefillData = async ( addressAddressCountry: 'United States', employees: 400, position: 5, - createdBySource: 'MANUAL', + createdBySource: FieldActorSource.SYSTEM, createdByWorkspaceMemberId: null, createdByName: 'System', }, diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/person.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/person.ts index ec07c61f15b2..8b2972c6dbff 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/person.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/person.ts @@ -1,5 +1,6 @@ import { EntityManager } from 'typeorm'; +import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { AIRBNB_ID, FIGMA_ID, @@ -40,7 +41,7 @@ export const personPrefillData = async ( avatarUrl: 'https://twentyhq.github.io/placeholder-images/people/image-3.png', position: 1, - createdBySource: 'MANUAL', + createdBySource: FieldActorSource.SYSTEM, createdByWorkspaceMemberId: null, createdByName: 'System', phonesPrimaryPhoneNumber: '1234567890', @@ -55,7 +56,7 @@ export const personPrefillData = async ( avatarUrl: 'https://twentyhq.github.io/placeholder-images/people/image-89.png', position: 2, - createdBySource: 'MANUAL', + createdBySource: FieldActorSource.SYSTEM, createdByWorkspaceMemberId: null, createdByName: 'System', phonesPrimaryPhoneNumber: '677118822', @@ -70,7 +71,7 @@ export const personPrefillData = async ( avatarUrl: 'https://twentyhq.github.io/placeholder-images/people/image-47.png', position: 3, - createdBySource: 'MANUAL', + createdBySource: FieldActorSource.SYSTEM, createdByWorkspaceMemberId: null, createdByName: 'System', phonesPrimaryPhoneNumber: '987625341', @@ -85,7 +86,7 @@ export const personPrefillData = async ( avatarUrl: 'https://twentyhq.github.io/placeholder-images/people/image-40.png', position: 4, - createdBySource: 'MANUAL', + createdBySource: FieldActorSource.SYSTEM, createdByWorkspaceMemberId: null, createdByName: 'System', phonesPrimaryPhoneNumber: '09882261', @@ -100,7 +101,7 @@ export const personPrefillData = async ( avatarUrl: 'https://twentyhq.github.io/placeholder-images/people/image-68.png', position: 5, - createdBySource: 'MANUAL', + createdBySource: FieldActorSource.SYSTEM, createdByWorkspaceMemberId: null, createdByName: 'System', phonesPrimaryPhoneNumber: '88226173', diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts index 7198fe4ef1e6..9584474f4377 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/standard-objects-prefill-data.ts @@ -2,6 +2,7 @@ import { DataSource, EntityManager } from 'typeorm'; import { seedWorkspaceFavorites } from 'src/database/typeorm-seeds/workspace/favorites'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { shouldSeedWorkspaceFavorite } from 'src/engine/utils/should-seed-workspace-favorite'; import { companyPrefillData } from 'src/engine/workspace-manager/standard-objects-prefill-data/company'; import { personPrefillData } from 'src/engine/workspace-manager/standard-objects-prefill-data/person'; import { viewPrefillData } from 'src/engine/workspace-manager/standard-objects-prefill-data/view'; @@ -45,7 +46,14 @@ export const standardObjectsPrefillData = async ( await seedWorkspaceFavorites( viewDefinitionsWithId - .filter((view) => view.key === 'INDEX') + .filter( + (view) => + view.key === 'INDEX' && + shouldSeedWorkspaceFavorite( + view.objectMetadataId, + objectMetadataMap, + ), + ) .map((view) => view.id), entityManager, schemaName, diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/view.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/view.ts index a4f230eaca32..3b7e47b80d90 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/view.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/view.ts @@ -9,6 +9,8 @@ import { opportunitiesByStageView } from 'src/engine/workspace-manager/standard- import { peopleAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/people-all.view'; import { tasksAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-all.view'; import { tasksByStatusView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-by-status.view'; +import { workflowRunsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-runs-all.view'; +import { workflowVersionsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-versions-all.view'; import { workflowsAllView } from 'src/engine/workspace-manager/standard-objects-prefill-data/views/workflows-all.view'; export const viewPrefillData = async ( @@ -18,14 +20,20 @@ export const viewPrefillData = async ( isWorkflowEnabled: boolean, ) => { const viewDefinitions = [ - await companiesAllView(objectMetadataMap), - await peopleAllView(objectMetadataMap), - await opportunitiesAllView(objectMetadataMap), - await opportunitiesByStageView(objectMetadataMap), - await notesAllView(objectMetadataMap), - await tasksAllView(objectMetadataMap), - await tasksByStatusView(objectMetadataMap), - ...(isWorkflowEnabled ? [await workflowsAllView(objectMetadataMap)] : []), + companiesAllView(objectMetadataMap), + peopleAllView(objectMetadataMap), + opportunitiesAllView(objectMetadataMap), + opportunitiesByStageView(objectMetadataMap), + notesAllView(objectMetadataMap), + tasksAllView(objectMetadataMap), + tasksByStatusView(objectMetadataMap), + ...(isWorkflowEnabled + ? [ + workflowsAllView(objectMetadataMap), + workflowVersionsAllView(objectMetadataMap), + workflowRunsAllView(objectMetadataMap), + ] + : []), ]; const viewDefinitionsWithId = viewDefinitions.map((viewDefinition) => ({ diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/companies-all.view.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/companies-all.view.ts index ff26eab35631..73aad80dbba6 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/companies-all.view.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/companies-all.view.ts @@ -5,7 +5,7 @@ import { } 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'; -export const companiesAllView = async ( +export const companiesAllView = ( objectMetadataMap: Record<string, ObjectMetadataEntity>, ) => { return { diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/notes-all.view.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/notes-all.view.ts index 97354aa1f2ff..fec9f63b0ed0 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/notes-all.view.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/notes-all.view.ts @@ -5,7 +5,7 @@ import { } 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'; -export const notesAllView = async ( +export const notesAllView = ( objectMetadataMap: Record<string, ObjectMetadataEntity>, ) => { return { diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/opportunities-all.view.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/opportunities-all.view.ts index 4dfad89ad4a7..b3a57bf4de61 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/opportunities-all.view.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/opportunities-all.view.ts @@ -2,7 +2,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat import { OPPORTUNITY_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'; -export const opportunitiesAllView = async ( +export const opportunitiesAllView = ( objectMetadataMap: Record<string, ObjectMetadataEntity>, ) => { return { diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/opportunity-by-stage.view.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/opportunity-by-stage.view.ts index c67a5945b814..82d3b479122c 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/opportunity-by-stage.view.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/opportunity-by-stage.view.ts @@ -2,7 +2,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat import { OPPORTUNITY_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'; -export const opportunitiesByStageView = async ( +export const opportunitiesByStageView = ( objectMetadataMap: Record<string, ObjectMetadataEntity>, ) => { return { diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/people-all.view.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/people-all.view.ts index 9fbddef9109b..53492e7b9dda 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/people-all.view.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/people-all.view.ts @@ -5,7 +5,7 @@ import { } 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'; -export const peopleAllView = async ( +export const peopleAllView = ( objectMetadataMap: Record<string, ObjectMetadataEntity>, ) => { return { diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-all.view.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-all.view.ts index 3d9a0ffadf71..29ed1ce476f8 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-all.view.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-all.view.ts @@ -5,7 +5,7 @@ import { } 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'; -export const tasksAllView = async ( +export const tasksAllView = ( objectMetadataMap: Record<string, ObjectMetadataEntity>, ) => { return { diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-by-status.view.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-by-status.view.ts index fe392c181305..304b3ed0112e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-by-status.view.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/tasks-by-status.view.ts @@ -5,7 +5,7 @@ import { } 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'; -export const tasksByStatusView = async ( +export const tasksByStatusView = ( objectMetadataMap: Record<string, ObjectMetadataEntity>, ) => { return { diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-runs-all.view.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-runs-all.view.ts new file mode 100644 index 000000000000..d7bc3775d9c3 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-runs-all.view.ts @@ -0,0 +1,56 @@ +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WORKFLOW_RUN_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'; + +export const workflowRunsAllView = ( + objectMetadataMap: Record<string, ObjectMetadataEntity>, +) => { + return { + name: 'All Workflow Runs', + objectMetadataId: objectMetadataMap[STANDARD_OBJECT_IDS.workflowRun].id, + type: 'table', + key: 'INDEX', + position: 0, + icon: 'IconHistory', + kanbanFieldMetadataId: '', + filters: [], + fields: [ + { + fieldMetadataId: + objectMetadataMap[STANDARD_OBJECT_IDS.workflowRun].fields[ + WORKFLOW_RUN_STANDARD_FIELD_IDS.name + ], + position: 0, + isVisible: true, + size: 210, + }, + { + fieldMetadataId: + objectMetadataMap[STANDARD_OBJECT_IDS.workflowRun].fields[ + WORKFLOW_RUN_STANDARD_FIELD_IDS.status + ], + position: 1, + isVisible: true, + size: 150, + }, + { + fieldMetadataId: + objectMetadataMap[STANDARD_OBJECT_IDS.workflowRun].fields[ + WORKFLOW_RUN_STANDARD_FIELD_IDS.startedAt + ], + position: 2, + isVisible: true, + size: 150, + }, + { + fieldMetadataId: + objectMetadataMap[STANDARD_OBJECT_IDS.workflowRun].fields[ + WORKFLOW_RUN_STANDARD_FIELD_IDS.endedAt + ], + position: 3, + isVisible: true, + size: 150, + }, + ], + }; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-versions-all.view.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-versions-all.view.ts new file mode 100644 index 000000000000..fff64745d579 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/workflow-versions-all.view.ts @@ -0,0 +1,56 @@ +import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; +import { WORKFLOW_VERSION_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'; + +export const workflowVersionsAllView = ( + objectMetadataMap: Record<string, ObjectMetadataEntity>, +) => { + return { + name: 'All Workflow Versions', + objectMetadataId: objectMetadataMap[STANDARD_OBJECT_IDS.workflowVersion].id, + type: 'table', + key: 'INDEX', + position: 0, + icon: 'IconVersions', + kanbanFieldMetadataId: '', + filters: [], + fields: [ + { + fieldMetadataId: + objectMetadataMap[STANDARD_OBJECT_IDS.workflowVersion].fields[ + WORKFLOW_VERSION_STANDARD_FIELD_IDS.name + ], + position: 0, + isVisible: true, + size: 210, + }, + { + fieldMetadataId: + objectMetadataMap[STANDARD_OBJECT_IDS.workflowVersion].fields[ + WORKFLOW_VERSION_STANDARD_FIELD_IDS.status + ], + position: 1, + isVisible: true, + size: 150, + }, + { + fieldMetadataId: + objectMetadataMap[STANDARD_OBJECT_IDS.workflowVersion].fields[ + WORKFLOW_VERSION_STANDARD_FIELD_IDS.trigger + ], + position: 2, + isVisible: true, + size: 150, + }, + { + fieldMetadataId: + objectMetadataMap[STANDARD_OBJECT_IDS.workflowVersion].fields[ + WORKFLOW_VERSION_STANDARD_FIELD_IDS.steps + ], + position: 3, + isVisible: true, + size: 150, + }, + ], + }; +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/workflows-all.view.ts b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/workflows-all.view.ts index 8eba4bdc4442..330d68523529 100644 --- a/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/workflows-all.view.ts +++ b/packages/twenty-server/src/engine/workspace-manager/standard-objects-prefill-data/views/workflows-all.view.ts @@ -2,7 +2,7 @@ import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadat import { WORKFLOW_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'; -export const workflowsAllView = async ( +export const workflowsAllView = ( objectMetadataMap: Record<string, ObjectMetadataEntity>, ) => { return { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory.ts index 0392041edc93..206e5097ac5a 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory.ts @@ -2,15 +2,15 @@ import { Injectable } from '@nestjs/common'; import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; +import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; 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 { WorkspaceMigrationEntity, WorkspaceMigrationIndexActionType, WorkspaceMigrationTableActionType, } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { computeObjectTargetTable } from 'src/engine/utils/compute-object-target-table.util'; -import { generateMigrationName } from 'src/engine/metadata-modules/workspace-migration/utils/generate-migration-name.util'; -import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; @Injectable() export class WorkspaceMigrationIndexFactory { @@ -94,6 +94,7 @@ export class WorkspaceMigrationIndexFactory { return fieldMetadata.name; }), + type: indexMetadata.indexType, })); workspaceMigrations.push({ diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/utils/custom-table-default-column.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/utils/custom-table-default-column.util.ts deleted file mode 100644 index 11a831f809d4..000000000000 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/utils/custom-table-default-column.util.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { TableColumnOptions } from 'typeorm'; - -import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; - -export const customTableDefaultColumns = ( - tableName: string, -): TableColumnOptions[] => [ - { - name: 'id', - type: 'uuid', - isPrimary: true, - default: 'public.uuid_generate_v4()', - }, - { - name: 'createdAt', - type: 'timestamptz', - default: 'now()', - }, - { - name: 'updatedAt', - type: 'timestamptz', - default: 'now()', - }, - { - name: 'deletedAt', - type: 'timestamptz', - isNullable: true, - }, - { - name: 'position', - type: 'float', - isNullable: true, - }, - { - name: 'name', - type: 'text', - isNullable: false, - default: "'Untitled'", - }, - { - name: 'createdBySource', - type: 'enum', - enumName: `${tableName}_createdBySource_enum`, - enum: Object.values(FieldActorSource), - isNullable: false, - default: `'${FieldActorSource.MANUAL}'`, - }, - { name: 'createdByWorkspaceMemberId', type: 'uuid', isNullable: true }, - { - name: 'createdByName', - type: 'text', - isNullable: false, - default: "''", - }, -]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/utils/table-default-column.util.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/utils/table-default-column.util.ts new file mode 100644 index 000000000000..8967eb83f0d9 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/utils/table-default-column.util.ts @@ -0,0 +1,10 @@ +import { TableColumnOptions } from 'typeorm'; + +export const tableDefaultColumns = (): TableColumnOptions[] => [ + { + name: 'id', + type: 'uuid', + isPrimary: true, + default: 'public.uuid_generate_v4()', + }, +]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts index 3fec17109c3f..db2072c8447b 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-migration-runner/workspace-migration-runner.service.ts @@ -26,9 +26,10 @@ import { WorkspaceMigrationService } from 'src/engine/metadata-modules/workspace import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; import { WorkspaceMigrationEnumService } from 'src/engine/workspace-manager/workspace-migration-runner/services/workspace-migration-enum.service'; import { convertOnDeleteActionToOnDelete } from 'src/engine/workspace-manager/workspace-migration-runner/utils/convert-on-delete-action-to-on-delete.util'; +import { tableDefaultColumns } from 'src/engine/workspace-manager/workspace-migration-runner/utils/table-default-column.util'; +import { isDefined } from 'src/utils/is-defined'; import { WorkspaceMigrationTypeService } from './services/workspace-migration-type.service'; -import { customTableDefaultColumns } from './utils/custom-table-default-column.util'; @Injectable() export class WorkspaceMigrationRunnerService { @@ -120,7 +121,12 @@ export class WorkspaceMigrationRunnerService { ) { switch (tableMigration.action) { case WorkspaceMigrationTableActionType.CREATE: - await this.createTable(queryRunner, schemaName, tableMigration.name); + await this.createTable( + queryRunner, + schemaName, + tableMigration.name, + tableMigration.columns, + ); break; case WorkspaceMigrationTableActionType.ALTER: { if (tableMigration.newName) { @@ -194,13 +200,21 @@ export class WorkspaceMigrationRunnerService { for (const index of indexes) { switch (index.action) { case WorkspaceMigrationIndexActionType.CREATE: - await queryRunner.createIndex( - `${schemaName}.${tableName}`, - new TableIndex({ - name: index.name, - columnNames: index.columns, - }), - ); + if (isDefined(index.type)) { + const quotedColumns = index.columns.map((column) => `"${column}"`); + + await queryRunner.query(` + CREATE INDEX "${index.name}" ON "${schemaName}"."${tableName}" USING ${index.type} (${quotedColumns.join(', ')}) + `); + } else { + await queryRunner.createIndex( + `${schemaName}.${tableName}`, + new TableIndex({ + name: index.name, + columnNames: index.columns, + }), + ); + } break; case WorkspaceMigrationIndexActionType.DROP: try { @@ -235,16 +249,26 @@ export class WorkspaceMigrationRunnerService { queryRunner: QueryRunner, schemaName: string, tableName: string, + columns?: WorkspaceMigrationColumnAction[], ) { await queryRunner.createTable( new Table({ name: tableName, schema: schemaName, - columns: customTableDefaultColumns(tableName), + columns: tableDefaultColumns(), }), true, ); + if (columns && columns.length > 0) { + await this.handleColumnChanges( + queryRunner, + schemaName, + tableName, + columns, + ); + } + // Enable totalCount for the table await queryRunner.query(` COMMENT ON TABLE "${schemaName}"."${tableName}" IS '@graphql({"totalCount": {"enabled": true}})'; @@ -380,6 +404,8 @@ export class WorkspaceMigrationRunnerService { enumName: enumName, isArray: migrationColumn.isArray, isNullable: migrationColumn.isNullable, + asExpression: migrationColumn.asExpression, + generatedType: migrationColumn.generatedType, }), ); } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field.comparator.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field.comparator.ts index 7e51a398f9be..d26c3aa8bd63 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field.comparator.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-field.comparator.ts @@ -24,6 +24,8 @@ const commonFieldPropertiesToIgnore = [ 'settings', 'joinColumn', 'gate', + 'asExpression', + 'generatedType', ]; const fieldPropertiesToStringify = ['defaultValue'] as const; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags.ts new file mode 100644 index 000000000000..88ec505dd432 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags.ts @@ -0,0 +1,6 @@ +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; + +export const DEFAULT_FEATURE_FLAGS = [ + FeatureFlagKey.IsSearchEnabled, + FeatureFlagKey.IsWorkspaceMigratedForSearch, +]; 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 8a19d49e4b2c..624504052ce4 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 @@ -63,6 +63,7 @@ export const CALENDAR_CHANNEL_EVENT_ASSOCIATION_STANDARD_FIELD_IDS = { calendarChannel: '20202020-93ee-4da4-8d58-0282c4a9cb7d', calendarEvent: '20202020-5aa5-437e-bb86-f42d457783e3', eventExternalId: '20202020-9ec8-48bb-b279-21d0734a75a1', + recurringEventExternalId: '20202020-c58f-4c69-9bf8-9518fa31aa50', }; export const CALENDAR_CHANNEL_STANDARD_FIELD_IDS = { @@ -78,6 +79,7 @@ export const CALENDAR_CHANNEL_STANDARD_FIELD_IDS = { syncStatus: '20202020-7116-41da-8b4b-035975c4eb6a', syncStage: '20202020-6246-42e6-b5cd-003bd921782c', syncStageStartedAt: '20202020-a934-46f1-a8e7-9568b1e3a53e', + syncedAt: '20202020-2ff5-4f70-953a-3d0d36357576', }; export const CALENDAR_EVENT_PARTICIPANT_STANDARD_FIELD_IDS = { @@ -103,7 +105,6 @@ export const CALENDAR_EVENT_STANDARD_FIELD_IDS = { iCalUID: '20202020-f24b-45f4-b6a3-d2f9fcb98714', conferenceSolution: '20202020-1c3f-4b5a-b526-5411a82179eb', conferenceLink: '20202020-35da-43ef-9ca0-e936e9dc237b', - recurringEventExternalId: '20202020-4b96-43d0-8156-4c7a9717635c', calendarChannelEventAssociations: '20202020-bdf8-4572-a2cc-ecbb6bcc3a02', calendarEventParticipants: '20202020-e07e-4ccb-88f5-6f3d00458eec', }; @@ -135,6 +136,7 @@ export const COMPANY_STANDARD_FIELD_IDS = { favorites: '20202020-4d1d-41ac-b13b-621631298d55', attachments: '20202020-c1b5-4120-b0f0-987ca401ed53', timelineActivities: '20202020-0414-4daf-9c0d-64fe7b27f89f', + searchVector: '85c71601-72f9-4b7b-b343-d46100b2c74d', }; export const CONNECTED_ACCOUNT_STANDARD_FIELD_IDS = { @@ -148,6 +150,7 @@ export const CONNECTED_ACCOUNT_STANDARD_FIELD_IDS = { messageChannels: '20202020-24f7-4362-8468-042204d1e445', calendarChannels: '20202020-af4a-47bb-99ec-51911c1d3977', handleAliases: '20202020-8a3d-46be-814f-6228af16c47b', + scopes: '20202020-8a3d-46be-814f-6228af16c47c', }; export const EVENT_STANDARD_FIELD_IDS = { @@ -300,6 +303,7 @@ export const OPPORTUNITY_STANDARD_FIELD_IDS = { noteTargets: '20202020-dd3f-42d5-a382-db58aabf43d3', attachments: '20202020-87c7-4118-83d6-2f4031005209', timelineActivities: '20202020-30e2-421f-96c7-19c69d1cf631', + searchVector: '428a0da5-4b2e-4ce3-b695-89a8b384e6e3', }; export const PERSON_STANDARD_FIELD_IDS = { @@ -310,7 +314,7 @@ export const PERSON_STANDARD_FIELD_IDS = { xLink: '20202020-8fc2-487c-b84a-55a99b145cfd', jobTitle: '20202020-b0d0-415a-bef9-640a26dacd9b', phone: '20202020-4564-4b8b-a09f-05445f2e0bce', - phones: '34becd3e-3c51-43fa-8b6e-af39e29368ab', + phones: '20202020-0638-448e-8825-439134618022', city: '20202020-5243-4ffb-afc5-2c675da41346', avatarUrl: '20202020-b8a6-40df-961c-373dc5d2ec21', position: '20202020-fcd5-4231-aff5-fff583eaa0b1', @@ -325,6 +329,7 @@ export const PERSON_STANDARD_FIELD_IDS = { messageParticipants: '20202020-498e-4c61-8158-fa04f0638334', calendarEventParticipants: '20202020-52ee-45e9-a702-b64b3753e3a9', timelineActivities: '20202020-a43e-4873-9c23-e522de906ce5', + searchVector: '57d1d7ad-fa10-44fc-82f3-ad0959ec2534', }; export const TASK_STANDARD_FIELD_IDS = { @@ -409,12 +414,15 @@ export const WORKFLOW_STANDARD_FIELD_IDS = { }; export const WORKFLOW_RUN_STANDARD_FIELD_IDS = { + name: '20202020-b840-4253-aef9-4e5013694587', workflowVersion: '20202020-2f52-4ba8-8dc4-d0d6adb9578d', workflow: '20202020-8c57-4e7f-84f5-f373f68e1b82', startedAt: '20202020-a234-4e2d-bd15-85bcea6bb183', endedAt: '20202020-e1c1-4b6b-bbbd-b2beaf2e159e', status: '20202020-6b3e-4f9c-8c2b-2e5b8e6d6f3b', + position: '20202020-7802-4c40-ae89-1f506fe3365c', createdBy: '20202020-6007-401a-8aa5-e6f38581a6f3', + output: '20202020-7be4-4db2-8ac6-3ff0d740843d', }; export const WORKFLOW_VERSION_STANDARD_FIELD_IDS = { @@ -422,6 +430,7 @@ export const WORKFLOW_VERSION_STANDARD_FIELD_IDS = { workflow: '20202020-afa3-46c3-91b0-0631ca6aa1c8', trigger: '20202020-4eae-43e7-86e0-212b41a30b48', status: '20202020-5a34-440e-8a25-39d8c3d1d4cf', + position: '20202020-791d-4950-ab28-0e704767ae1c', runs: '20202020-1d08-46df-901a-85045f18099a', steps: '20202020-5988-4a64-b94a-1f9b7b989039', }; @@ -462,4 +471,5 @@ export const CUSTOM_OBJECT_STANDARD_FIELD_IDS = { favorites: '20202020-a4a7-4686-b296-1c6c3482ee21', attachments: '20202020-8d59-46ca-b7b2-73d167712134', timelineActivities: '20202020-f1ef-4ba4-8f33-1a4577afa477', + searchVector: '70e56537-18ef-4811-b1c7-0a444006b815', }; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts index 9cbe04fd173f..28c6ca74435e 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-field.factory.ts @@ -166,6 +166,8 @@ export class StandardFieldFactory { isCustom: workspaceFieldMetadataArgs.isDeprecated ? true : false, isSystem: workspaceFieldMetadataArgs.isSystem ?? false, isActive: workspaceFieldMetadataArgs.isActive ?? true, + asExpression: workspaceFieldMetadataArgs.asExpression, + generatedType: workspaceFieldMetadataArgs.generatedType, }, ]; } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory.ts index 17bd00215c07..f3d89d0312c6 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory.ts @@ -5,9 +5,12 @@ import { PartialIndexMetadata } from 'src/engine/workspace-manager/workspace-syn import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; +import { generateDeterministicIndexName } from 'src/engine/metadata-modules/index-metadata/utils/generate-deterministic-index-name'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; +import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity'; import { metadataArgsStorage } from 'src/engine/twenty-orm/storage/metadata-args.storage'; +import { computeTableName } from 'src/engine/utils/compute-table-name.util'; import { isGatedAndNotEnabled } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/is-gate-and-not-enabled.util'; @Injectable() @@ -15,23 +18,37 @@ export class StandardIndexFactory { create( standardObjectMetadataDefinitions: (typeof BaseWorkspaceEntity)[], context: WorkspaceSyncContext, - originalObjectMetadataMap: Record<string, ObjectMetadataEntity>, + originalStandardObjectMetadataMap: Record<string, ObjectMetadataEntity>, + originalCustomObjectMetadataMap: Record<string, ObjectMetadataEntity>, workspaceFeatureFlagsMap: FeatureFlagMap, ): Partial<IndexMetadataEntity>[] { - return standardObjectMetadataDefinitions.flatMap((standardObjectMetadata) => - this.createIndexMetadata( - standardObjectMetadata, + const standardIndexOnStandardObjects = + standardObjectMetadataDefinitions.flatMap((standardObjectMetadata) => + this.createStandardIndexMetadataForStandardObject( + standardObjectMetadata, + context, + originalStandardObjectMetadataMap, + workspaceFeatureFlagsMap, + ), + ); + + const standardIndexesOnCustomObjects = + this.createStandardIndexMetadataForCustomObject( context, - originalObjectMetadataMap, + originalCustomObjectMetadataMap, workspaceFeatureFlagsMap, - ), - ); + ); + + return [ + standardIndexOnStandardObjects, + standardIndexesOnCustomObjects, + ].flat(); } - private createIndexMetadata( + private createStandardIndexMetadataForStandardObject( target: typeof BaseWorkspaceEntity, context: WorkspaceSyncContext, - originalObjectMetadataMap: Record<string, ObjectMetadataEntity>, + originalStandardObjectMetadataMap: Record<string, ObjectMetadataEntity>, workspaceFeatureFlagsMap: FeatureFlagMap, ): Partial<IndexMetadataEntity>[] { const workspaceEntity = metadataArgsStorage.filterEntities(target); @@ -58,7 +75,7 @@ export class StandardIndexFactory { return workspaceIndexMetadataArgsCollection.map( (workspaceIndexMetadataArgs) => { const objectMetadata = - originalObjectMetadataMap[workspaceEntity.nameSingular]; + originalStandardObjectMetadataMap[workspaceEntity.nameSingular]; if (!objectMetadata) { throw new Error( @@ -71,10 +88,55 @@ export class StandardIndexFactory { objectMetadataId: objectMetadata.id, name: workspaceIndexMetadataArgs.name, columns: workspaceIndexMetadataArgs.columns, + isCustom: false, + indexType: workspaceIndexMetadataArgs.type, }; return indexMetadata; }, ); } + + private createStandardIndexMetadataForCustomObject( + context: WorkspaceSyncContext, + originalCustomObjectMetadataMap: Record<string, ObjectMetadataEntity>, + workspaceFeatureFlagsMap: FeatureFlagMap, + ): Partial<IndexMetadataEntity>[] { + const target = CustomWorkspaceEntity; + const workspaceEntity = metadataArgsStorage.filterExtendedEntities(target); + + if (!workspaceEntity) { + throw new Error( + `Object metadata decorator not found, can't parse ${target.name}`, + ); + } + + const workspaceIndexMetadataArgsCollection = metadataArgsStorage + .filterIndexes(target) + .filter((workspaceIndexMetadataArgs) => { + return !isGatedAndNotEnabled( + workspaceIndexMetadataArgs.gate, + workspaceFeatureFlagsMap, + ); + }); + + return Object.entries(originalCustomObjectMetadataMap).flatMap( + ([customObjectName, customObjectMetadata]) => { + return workspaceIndexMetadataArgsCollection.map( + (workspaceIndexMetadataArgs) => { + const indexMetadata: PartialIndexMetadata = { + workspaceId: context.workspaceId, + objectMetadataId: customObjectMetadata.id, + name: `IDX_${generateDeterministicIndexName([computeTableName(customObjectName, true), ...workspaceIndexMetadataArgs.columns])}`, + columns: workspaceIndexMetadataArgs.columns, + isCustom: false, + indexType: workspaceIndexMetadataArgs.type, + }; + + return indexMetadata; + }, + ); + }, + ); + } } diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts index 81722ad20710..4f17d0ee3dca 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/interfaces/partial-field-metadata.interface.ts @@ -16,6 +16,8 @@ export type PartialFieldMetadata = Omit< workspaceId: string; objectMetadataId?: string; isActive?: boolean; + asExpression?: string; + generatedType?: 'STORED' | 'VIRTUAL'; }; export type PartialComputedFieldMetadata = { diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service.ts index d82e2bf69c69..4e10f7ea2b81 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-field-metadata.service.ts @@ -10,6 +10,7 @@ import { } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface'; import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; import { CustomWorkspaceEntity } from 'src/engine/twenty-orm/custom.workspace-entity'; @@ -143,13 +144,27 @@ export class WorkspaceSyncFieldMetadataService { ] of standardObjectStandardFieldMetadataMap) { const originalObjectMetadata = originalObjectMetadataMap[standardObjectId]; - const computedStandardFieldMetadataCollection = computeStandardFields( + + let computedStandardFieldMetadataCollection = computeStandardFields( standardFieldMetadataCollection, originalObjectMetadata, // We need to provide this for generated relations with custom objects customObjectMetadataCollection, ); + let originalObjectMetadataFields = originalObjectMetadata.fields; + + if (!workspaceFeatureFlagsMap.IS_SEARCH_ENABLED) { + computedStandardFieldMetadataCollection = + computedStandardFieldMetadataCollection.filter( + (field) => field.type !== FieldMetadataType.TS_VECTOR, + ); + + originalObjectMetadataFields = originalObjectMetadataFields.filter( + (field) => field.type !== FieldMetadataType.TS_VECTOR, + ); + } + const fieldComparatorResults = this.workspaceFieldComparator.compare( originalObjectMetadata.id, originalObjectMetadata.fields, @@ -177,11 +192,24 @@ export class WorkspaceSyncFieldMetadataService { // Loop over all custom objects from the DB and compare their fields with standard fields for (const customObjectMetadata of customObjectMetadataCollection) { // Also, maybe it's better to refactor a bit and move generation part into a separate module ? - const standardFieldMetadataCollection = computeStandardFields( + let standardFieldMetadataCollection = computeStandardFields( customObjectStandardFieldMetadataCollection, customObjectMetadata, ); + let customObjectMetadataFields = customObjectMetadata.fields; + + if (!workspaceFeatureFlagsMap.IS_SEARCH_ENABLED) { + standardFieldMetadataCollection = + standardFieldMetadataCollection.filter( + (field) => field.type !== FieldMetadataType.TS_VECTOR, + ); + + customObjectMetadataFields = customObjectMetadataFields.filter( + (field) => field.type !== FieldMetadataType.TS_VECTOR, + ); + } + /** * COMPARE FIELD METADATA */ diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service.ts index 4f7a30ef9d25..a5357d1608b9 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/services/workspace-sync-index-metadata.service.ts @@ -1,22 +1,25 @@ import { Injectable, Logger } from '@nestjs/common'; -import { EntityManager } from 'typeorm'; +import { Any, EntityManager } from 'typeorm'; import { FeatureFlagMap } from 'src/engine/core-modules/feature-flag/interfaces/feature-flag-map.interface'; -import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; -import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface'; import { WorkspaceMigrationBuilderAction } from 'src/engine/workspace-manager/workspace-migration-builder/interfaces/workspace-migration-builder-action.interface'; +import { ComparatorAction } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/comparator.interface'; +import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; -import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; -import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage'; -import { StandardIndexFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory'; +import { + IndexMetadataEntity, + IndexType, +} from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; import { ObjectMetadataEntity } from 'src/engine/metadata-modules/object-metadata/object-metadata.entity'; -import { mapObjectMetadataByUniqueIdentifier } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/sync-metadata.util'; -import { IndexMetadataEntity } from 'src/engine/metadata-modules/index-metadata/index-metadata.entity'; -import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; +import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; +import { WorkspaceMigrationIndexFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory'; import { WorkspaceIndexComparator } from 'src/engine/workspace-manager/workspace-sync-metadata/comparators/workspace-index.comparator'; +import { StandardIndexFactory } from 'src/engine/workspace-manager/workspace-sync-metadata/factories/standard-index.factory'; import { WorkspaceMetadataUpdaterService } from 'src/engine/workspace-manager/workspace-sync-metadata/services/workspace-metadata-updater.service'; -import { WorkspaceMigrationIndexFactory } from 'src/engine/workspace-manager/workspace-migration-builder/factories/workspace-migration-index.factory'; +import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; +import { WorkspaceSyncStorage } from 'src/engine/workspace-manager/workspace-sync-metadata/storage/workspace-sync.storage'; +import { mapObjectMetadataByUniqueIdentifier } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/sync-metadata.util'; @Injectable() export class WorkspaceSyncIndexMetadataService { @@ -47,35 +50,60 @@ export class WorkspaceSyncIndexMetadataService { workspaceId: context.workspaceId, // We're only interested in standard fields fields: { isCustom: false }, - isCustom: false, }, relations: ['dataSource', 'fields', 'indexes'], }); // Create map of object metadata & field metadata by unique identifier - const originalObjectMetadataMap = mapObjectMetadataByUniqueIdentifier( - originalObjectMetadataCollection, - // Relation are based on the singular name + const originalStandardObjectMetadataMap = + mapObjectMetadataByUniqueIdentifier( + originalObjectMetadataCollection.filter( + (objectMetadata) => !objectMetadata.isCustom, + ), + // Relation are based on the singular name + (objectMetadata) => objectMetadata.nameSingular, + ); + + const originalCustomObjectMetadataMap = mapObjectMetadataByUniqueIdentifier( + originalObjectMetadataCollection.filter( + (objectMetadata) => objectMetadata.isCustom, + ), (objectMetadata) => objectMetadata.nameSingular, ); const indexMetadataRepository = manager.getRepository(IndexMetadataEntity); - const originalIndexMetadataCollection = await indexMetadataRepository.find({ + let originalIndexMetadataCollection = await indexMetadataRepository.find({ where: { workspaceId: context.workspaceId, + objectMetadataId: Any( + Object.values(originalObjectMetadataCollection).map( + (object) => object.id, + ), + ), + isCustom: false, }, relations: ['indexFieldMetadatas.fieldMetadata'], }); // Generate index metadata from models - const standardIndexMetadataCollection = this.standardIndexFactory.create( + let standardIndexMetadataCollection = this.standardIndexFactory.create( standardObjectMetadataDefinitions, context, - originalObjectMetadataMap, + originalStandardObjectMetadataMap, + originalCustomObjectMetadataMap, workspaceFeatureFlagsMap, ); + if (!workspaceFeatureFlagsMap.IS_SEARCH_ENABLED) { + originalIndexMetadataCollection = originalIndexMetadataCollection.filter( + (index) => index.indexType !== IndexType.GIN, + ); + + standardIndexMetadataCollection = standardIndexMetadataCollection.filter( + (index) => index.indexType !== IndexType.GIN, + ); + } const indexComparatorResults = this.workspaceIndexComparator.compare( originalIndexMetadataCollection, standardIndexMetadataCollection, diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts index d510e4f2d2e7..914ee491e448 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/standard-objects/index.ts @@ -54,8 +54,6 @@ export const standardObjectMetadataDefinitions = [ CompanyWorkspaceEntity, ConnectedAccountWorkspaceEntity, FavoriteWorkspaceEntity, - OpportunityWorkspaceEntity, - PersonWorkspaceEntity, TimelineActivityWorkspaceEntity, ViewFieldWorkspaceEntity, ViewFilterWorkspaceEntity, @@ -79,10 +77,4 @@ export const standardObjectMetadataDefinitions = [ PersonWorkspaceEntity, TaskWorkspaceEntity, TaskTargetWorkspaceEntity, - TimelineActivityWorkspaceEntity, - ViewFieldWorkspaceEntity, - ViewFilterWorkspaceEntity, - ViewSortWorkspaceEntity, - ViewWorkspaceEntity, - WebhookWorkspaceEntity, ]; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts new file mode 100644 index 000000000000..8c7594b49db1 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/__tests__/get-ts-vectors-column-expression.utils.spec.ts @@ -0,0 +1,103 @@ +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; + +const nameTextField = { name: 'name', type: FieldMetadataType.TEXT }; +const nameFullNameField = { + name: 'name', + type: FieldMetadataType.FULL_NAME, +}; +const jobTitleTextField = { name: 'jobTitle', type: FieldMetadataType.TEXT }; +const emailsEmailsField = { name: 'emails', type: FieldMetadataType.EMAILS }; + +jest.mock( + 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util', + () => ({ + computeColumnName: jest.fn((name) => { + if (name === 'name') { + return 'name'; + } + if (name === 'jobTitle') { + return 'jobTitle'; + } + if (name === 'emailsPrimaryEmail') { + return 'emailsPrimaryEmail'; + } + if (name === 'emailsAdditionalEmails') { + return 'emailsAdditionalEmails'; + } + if (name === 'nameFirstName') { + return 'nameFirstName'; + } + if (name === 'nameLastName') { + return 'nameLastName'; + } + }), + computeCompositeColumnName: jest.fn((field, property) => { + if ( + field.name === emailsEmailsField.name && + property.name === 'primaryEmail' + ) { + return 'emailsPrimaryEmail'; + } + if ( + field.name === emailsEmailsField.name && + property.name === 'additionalEmails' + ) { + return 'emailsAdditionalEmails'; + } + if ( + field.name === nameFullNameField.name && + property.name === 'firstName' + ) { + return 'nameFirstName'; + } + if ( + field.name === nameFullNameField.name && + property.name === 'lastName' + ) { + return 'nameLastName'; + } + }), + }), +); + +describe('getTsVectorColumnExpressionFromFields', () => { + it('should generate correct expression for simple text field', () => { + const fields = [nameTextField]; + const result = getTsVectorColumnExpressionFromFields(fields); + + expect(result).toContain("to_tsvector('simple', COALESCE(\"name\", ''))"); + }); + + it('should handle multiple fields', () => { + const fields = [nameFullNameField, jobTitleTextField, emailsEmailsField]; + const result = getTsVectorColumnExpressionFromFields(fields); + const expected = ` + CASE + WHEN "deletedAt" IS NULL THEN + to_tsvector('simple', COALESCE("nameFirstName", '') || ' ' || COALESCE("nameLastName", '') || ' ' || COALESCE("jobTitle", '') || ' ' || + COALESCE( + replace( + "emailsPrimaryEmail", + '@', + ' ' + ), + '' + ) + ) + ELSE NULL + END + `.trim(); + + expect(result.trim()).toBe(expected); + }); + + it('should include CASE statement for handling deletedAt', () => { + const fields = [nameTextField]; + const result = getTsVectorColumnExpressionFromFields(fields); + + expect(result).toContain('CASE'); + expect(result).toContain('WHEN "deletedAt" IS NULL THEN'); + expect(result).toContain('ELSE NULL'); + }); +}); 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 new file mode 100644 index 000000000000..816c7b64d288 --- /dev/null +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util.ts @@ -0,0 +1,88 @@ +import { compositeTypeDefinitions } from 'src/engine/metadata-modules/field-metadata/composite-types'; +import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; +import { + computeColumnName, + computeCompositeColumnName, +} from 'src/engine/metadata-modules/field-metadata/utils/compute-column-name.util'; +import { isCompositeFieldMetadataType } from 'src/engine/metadata-modules/field-metadata/utils/is-composite-field-metadata-type.util'; +import { + WorkspaceMigrationException, + WorkspaceMigrationExceptionCode, +} from 'src/engine/metadata-modules/workspace-migration/workspace-migration.exception'; + +type FieldTypeAndNameMetadata = { + name: string; + type: FieldMetadataType; +}; + +export const getTsVectorColumnExpressionFromFields = ( + fieldsUsedForSearch: FieldTypeAndNameMetadata[], +): string => { + const columnExpressions = fieldsUsedForSearch.flatMap( + getColumnExpressionsFromField, + ); + const concatenatedExpression = columnExpressions.join(" || ' ' || "); + + const tsVectorExpression = `to_tsvector('simple', ${concatenatedExpression})`; + + return ` + CASE + WHEN "deletedAt" IS NULL THEN + ${tsVectorExpression} + ELSE NULL + END + `; +}; + +const getColumnExpressionsFromField = ( + fieldMetadataTypeAndName: FieldTypeAndNameMetadata, +): string[] => { + if (isCompositeFieldMetadataType(fieldMetadataTypeAndName.type)) { + const compositeType = compositeTypeDefinitions.get( + fieldMetadataTypeAndName.type, + ); + + if (!compositeType) { + throw new WorkspaceMigrationException( + `Composite type not found for field metadata type: ${fieldMetadataTypeAndName.type}`, + WorkspaceMigrationExceptionCode.INVALID_FIELD_METADATA, + ); + } + + return compositeType.properties + .filter((property) => property.type === FieldMetadataType.TEXT) + .map((property) => { + const columnName = computeCompositeColumnName( + fieldMetadataTypeAndName, + property, + ); + + return getColumnExpression(columnName, fieldMetadataTypeAndName.type); + }); + } + const columnName = computeColumnName(fieldMetadataTypeAndName.name); + + return [getColumnExpression(columnName, fieldMetadataTypeAndName.type)]; +}; + +const getColumnExpression = ( + columnName: string, + fieldType: FieldMetadataType, +): string => { + const quotedColumnName = `"${columnName}"`; + + if (fieldType === FieldMetadataType.EMAILS) { + return ` + COALESCE( + replace( + ${quotedColumnName}, + '@', + ' ' + ), + '' + ) + `; + } else { + return `COALESCE(${quotedColumnName}, '')`; + } +}; diff --git a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts index 6e035b99c07f..c090d413aa18 100644 --- a/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts +++ b/packages/twenty-server/src/engine/workspace-manager/workspace-sync-metadata/workspace-sync-metadata.service.ts @@ -1,10 +1,12 @@ import { Injectable, Logger } from '@nestjs/common'; -import { InjectDataSource } from '@nestjs/typeorm'; +import { InjectDataSource, InjectRepository } from '@nestjs/typeorm'; -import { DataSource, QueryFailedError } from 'typeorm'; +import { DataSource, QueryFailedError, Repository } from 'typeorm'; import { WorkspaceSyncContext } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/workspace-sync-context.interface'; +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 { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { WorkspaceMetadataVersionService } from 'src/engine/metadata-modules/workspace-metadata-version/services/workspace-metadata-version.service'; import { WorkspaceMigrationEntity } from 'src/engine/metadata-modules/workspace-migration/workspace-migration.entity'; @@ -35,6 +37,8 @@ export class WorkspaceSyncMetadataService { private readonly workspaceSyncIndexMetadataService: WorkspaceSyncIndexMetadataService, private readonly workspaceSyncObjectMetadataIdentifiersService: WorkspaceSyncObjectMetadataIdentifiersService, private readonly workspaceMetadataVersionService: WorkspaceMetadataVersionService, + @InjectRepository(FeatureFlagEntity, 'core') + private readonly featureFlagRepository: Repository<FeatureFlagEntity>, ) {} /** @@ -149,6 +153,13 @@ export class WorkspaceSyncMetadataService { await this.workspaceMigrationRunnerService.executeMigrationFromPendingMigrations( context.workspaceId, ); + + if (workspaceFeatureFlagsMap.IS_SEARCH_ENABLED) { + await this.featureFlagService.enableFeatureFlags( + [FeatureFlagKey.IsWorkspaceMigratedForSearch], + context.workspaceId, + ); + } } catch (error) { this.logger.error('Sync of standard objects failed with:', error); diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-cleaner/listeners/calendar-event-cleaner-connected-account.listener.ts b/packages/twenty-server/src/modules/calendar/calendar-event-cleaner/listeners/calendar-event-cleaner-connected-account.listener.ts index 387dc0743c68..1406d442d02c 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-cleaner/listeners/calendar-event-cleaner-connected-account.listener.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-cleaner/listeners/calendar-event-cleaner-connected-account.listener.ts @@ -19,8 +19,8 @@ export class CalendarEventCleanerConnectedAccountListener { private readonly calendarQueueService: MessageQueueService, ) {} - @OnEvent('connectedAccount.deleted') - async handleDeletedEvent( + @OnEvent('connectedAccount.destroyed') + async handleDestroyedEvent( payload: WorkspaceEventBatch< ObjectRecordDeleteEvent<ConnectedAccountWorkspaceEntity> >, diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts index 0e472d465e9c..0385ae884422 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/calendar-event-import-manager.module.ts @@ -25,13 +25,11 @@ import { CalendarCommonModule } from 'src/modules/calendar/common/calendar-commo import { CalendarChannelSyncStatusService } from 'src/modules/calendar/common/services/calendar-channel-sync-status.service'; import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module'; import { RefreshAccessTokenManagerModule } from 'src/modules/connected-account/refresh-access-token-manager/refresh-access-token-manager.module'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @Module({ imports: [ ObjectMetadataRepositoryModule.forFeature([ - ConnectedAccountWorkspaceEntity, BlocklistWorkspaceEntity, WorkspaceMemberWorkspaceEntity, ]), diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job.ts index 406e717021ac..1e28927e25b8 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/jobs/calendar-event-list-fetch.job.ts @@ -3,15 +3,12 @@ import { Scope } from '@nestjs/common'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { CalendarEventsImportService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-events-import.service'; import { CalendarChannelSyncStage, CalendarChannelWorkspaceEntity, } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; -import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { isThrottled } from 'src/modules/connected-account/utils/is-throttled'; export type CalendarEventsImportJobData = { @@ -27,8 +24,6 @@ export class CalendarEventListFetchJob { constructor( private readonly twentyORMManager: TwentyORMManager, private readonly calendarEventsImportService: CalendarEventsImportService, - @InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity) - private readonly connectedAccountRepository: ConnectedAccountRepository, ) {} @Process(CalendarEventListFetchJob.name) @@ -47,6 +42,7 @@ export class CalendarEventListFetchJob { id: calendarChannelId, isSyncEnabled: true, }, + relations: ['connectedAccount'], }); if (!calendarChannel) { @@ -62,12 +58,6 @@ export class CalendarEventListFetchJob { return; } - const connectedAccount = - await this.connectedAccountRepository.getConnectedAccountOrThrow( - workspaceId, - calendarChannel.connectedAccountId, - ); - switch (calendarChannel.syncStage) { case CalendarChannelSyncStage.FULL_CALENDAR_EVENT_LIST_FETCH_PENDING: await calendarChannelRepository.update(calendarChannelId, { @@ -77,7 +67,7 @@ export class CalendarEventListFetchJob { await this.calendarEventsImportService.processCalendarEventsImport( calendarChannel, - connectedAccount, + calendarChannel.connectedAccount, workspaceId, ); break; @@ -85,7 +75,7 @@ export class CalendarEventListFetchJob { case CalendarChannelSyncStage.PARTIAL_CALENDAR_EVENT_LIST_FETCH_PENDING: await this.calendarEventsImportService.processCalendarEventsImport( calendarChannel, - connectedAccount, + calendarChannel.connectedAccount, workspaceId, ); break; diff --git a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts index 8c71c73f1fc8..7552722e02cc 100644 --- a/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts +++ b/packages/twenty-server/src/modules/calendar/calendar-event-import-manager/services/calendar-save-events.service.ts @@ -7,12 +7,10 @@ import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queu import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; -import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { injectIdsInCalendarEvents } from 'src/modules/calendar/calendar-event-import-manager/utils/inject-ids-in-calendar-events.util'; import { CalendarEventParticipantService } from 'src/modules/calendar/calendar-event-participant-manager/services/calendar-event-participant.service'; import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity'; import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; -import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity'; import { CalendarEventWithParticipants } from 'src/modules/calendar/common/types/calendar-event'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @@ -28,7 +26,6 @@ export class CalendarSaveEventsService { private readonly calendarEventParticipantService: CalendarEventParticipantService, @InjectMessageQueue(MessageQueue.contactCreationQueue) private readonly messageQueueService: MessageQueueService, - private readonly workspaceEventEmitter: WorkspaceEventEmitter, ) {} public async saveCalendarEventsAndEnqueueContactCreationJob( @@ -103,6 +100,7 @@ export class CalendarSaveEventsService { calendarEventId: calendarEvent.id, eventExternalId: calendarEvent.externalId, calendarChannelId: calendarChannel.id, + recurringEventExternalId: calendarEvent.recurringEventExternalId, })); const participantsToSave = eventsToSave.flatMap( @@ -113,16 +111,57 @@ export class CalendarSaveEventsService { (event) => event.participants, ); - const savedCalendarEventParticipantsToEmit: CalendarEventParticipantWorkspaceEntity[] = - []; - const workspaceDataSource = await this.twentyORMManager.getDatasource(); await workspaceDataSource?.transaction(async (transactionManager) => { - await calendarEventRepository.save(eventsToSave, {}, transactionManager); + await calendarEventRepository.save( + eventsToSave.map( + (calendarEvent) => + ({ + id: calendarEvent.id, + iCalUID: calendarEvent.iCalUID, + title: calendarEvent.title, + description: calendarEvent.description, + startsAt: calendarEvent.startsAt, + endsAt: calendarEvent.endsAt, + location: calendarEvent.location, + isFullDay: calendarEvent.isFullDay, + isCanceled: calendarEvent.isCanceled, + conferenceSolution: calendarEvent.conferenceSolution, + conferenceLink: { + primaryLinkLabel: calendarEvent.conferenceLinkLabel, + primaryLinkUrl: calendarEvent.conferenceLinkUrl, + }, + externalCreatedAt: calendarEvent.externalCreatedAt, + externalUpdatedAt: calendarEvent.externalUpdatedAt, + }) satisfies DeepPartial<CalendarEventWorkspaceEntity>, + ), + {}, + transactionManager, + ); await calendarEventRepository.save( - eventsToUpdate, + eventsToUpdate.map( + (calendarEvent) => + ({ + id: calendarEvent.id, + iCalUID: calendarEvent.iCalUID, + title: calendarEvent.title, + description: calendarEvent.description, + startsAt: calendarEvent.startsAt, + endsAt: calendarEvent.endsAt, + location: calendarEvent.location, + isFullDay: calendarEvent.isFullDay, + isCanceled: calendarEvent.isCanceled, + conferenceSolution: calendarEvent.conferenceSolution, + conferenceLink: { + primaryLinkLabel: calendarEvent.conferenceLinkLabel, + primaryLinkUrl: calendarEvent.conferenceLinkUrl, + }, + externalCreatedAt: calendarEvent.externalCreatedAt, + externalUpdatedAt: calendarEvent.externalUpdatedAt, + }) satisfies DeepPartial<CalendarEventWorkspaceEntity>, + ), {}, transactionManager, ); diff --git a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-event.service.ts b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-event.service.ts index 041c3a648290..2e8810d1f7b1 100644 --- a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-event.service.ts +++ b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-event.service.ts @@ -1,12 +1,12 @@ import { ForbiddenException, Injectable } from '@nestjs/common'; import groupBy from 'lodash.groupby'; +import { Any } from 'typeorm'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { CalendarChannelEventAssociationWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity'; import { CalendarChannelVisibility } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; -import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @@ -15,8 +15,6 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta export class CanAccessCalendarEventService { constructor( private readonly twentyORMManager: TwentyORMManager, - @InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity) - private readonly connectedAccountRepository: ConnectedAccountRepository, @InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity) private readonly workspaceMemberService: WorkspaceMemberRepository, ) {} @@ -46,20 +44,20 @@ export class CanAccessCalendarEventService { const currentWorkspaceMember = await this.workspaceMemberService.getByIdOrFail(userId, workspaceId); - const calendarChannelsConnectedAccounts = - await this.connectedAccountRepository.getByIds( - calendarChannels.map((channel) => channel.connectedAccountId), - workspaceId, + const connectedAccountRepository = + await this.twentyORMManager.getRepository<ConnectedAccountWorkspaceEntity>( + 'connectedAccount', ); - const calendarChannelsWorkspaceMemberIds = - calendarChannelsConnectedAccounts.map( - (connectedAccount) => connectedAccount.accountOwnerId, - ); + const connectedAccounts = await connectedAccountRepository.find({ + select: ['id'], + where: { + calendarChannels: Any(calendarChannels.map((channel) => channel.id)), + accountOwnerId: currentWorkspaceMember.id, + }, + }); - if ( - calendarChannelsWorkspaceMemberIds.includes(currentWorkspaceMember.id) - ) { + if (connectedAccounts.length > 0) { return; } diff --git a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-query-hook.module.ts b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-query-hook.module.ts index ff3d178c4a21..2c5a26b18c4f 100644 --- a/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-query-hook.module.ts +++ b/packages/twenty-server/src/modules/calendar/common/query-hooks/calendar-query-hook.module.ts @@ -4,15 +4,11 @@ import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repos import { CalendarEventFindManyPreQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook'; import { CalendarEventFindOnePreQueryHook } from 'src/modules/calendar/common/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook'; import { CanAccessCalendarEventService } from 'src/modules/calendar/common/query-hooks/calendar-event/services/can-access-calendar-event.service'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; @Module({ imports: [ - ObjectMetadataRepositoryModule.forFeature([ - ConnectedAccountWorkspaceEntity, - WorkspaceMemberWorkspaceEntity, - ]), + ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]), ], providers: [ CanAccessCalendarEventService, diff --git a/packages/twenty-server/src/modules/calendar/common/services/calendar-channel-sync-status.service.ts b/packages/twenty-server/src/modules/calendar/common/services/calendar-channel-sync-status.service.ts index 4940c1db01eb..c0fe5278f898 100644 --- a/packages/twenty-server/src/modules/calendar/common/services/calendar-channel-sync-status.service.ts +++ b/packages/twenty-server/src/modules/calendar/common/services/calendar-channel-sync-status.service.ts @@ -171,6 +171,7 @@ export class CalendarChannelSyncStatusService { syncStatus: CalendarChannelSyncStatus.ACTIVE, throttleFailureCount: 0, syncStageStartedAt: null, + syncedAt: new Date().toISOString(), }); await this.schedulePartialCalendarEventListFetch(calendarChannelIds); diff --git a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity.ts b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity.ts index f1475da3b012..ee74a697eed8 100644 --- a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity.ts +++ b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel-event-association.workspace-entity.ts @@ -1,16 +1,16 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { CALENDAR_CHANNEL_EVENT_ASSOCIATION_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 { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; -import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; -import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator'; -import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; -import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; +import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; +import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; +import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator'; +import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator'; +import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; +import { CALENDAR_CHANNEL_EVENT_ASSOCIATION_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 { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity'; import { CalendarEventWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event.workspace-entity'; @@ -35,6 +35,16 @@ export class CalendarChannelEventAssociationWorkspaceEntity extends BaseWorkspac }) eventExternalId: string; + @WorkspaceField({ + standardId: + CALENDAR_CHANNEL_EVENT_ASSOCIATION_STANDARD_FIELD_IDS.recurringEventExternalId, + type: FieldMetadataType.TEXT, + label: 'Recurring Event ID', + description: 'Recurring Event ID', + icon: 'IconHistory', + }) + recurringEventExternalId: string; + @WorkspaceRelation({ standardId: CALENDAR_CHANNEL_EVENT_ASSOCIATION_STANDARD_FIELD_IDS.calendarChannel, diff --git a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity.ts b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity.ts index b6bb2b803af7..fca6a0b01369 100644 --- a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity.ts +++ b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity.ts @@ -270,6 +270,16 @@ export class CalendarChannelWorkspaceEntity extends BaseWorkspaceEntity { }) syncCursor: string; + @WorkspaceField({ + standardId: CALENDAR_CHANNEL_STANDARD_FIELD_IDS.syncedAt, + type: FieldMetadataType.DATE_TIME, + label: 'Last sync date', + description: 'Last sync date', + icon: 'IconHistory', + }) + @WorkspaceIsNullable() + syncedAt: string | null; + @WorkspaceField({ standardId: CALENDAR_CHANNEL_STANDARD_FIELD_IDS.syncStageStartedAt, type: FieldMetadataType.DATE_TIME, diff --git a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts index 55ede657ae96..4e4bbbf8615b 100644 --- a/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts +++ b/packages/twenty-server/src/modules/calendar/common/standard-objects/calendar-event.workspace-entity.ts @@ -145,15 +145,6 @@ export class CalendarEventWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceIsNullable() conferenceLink: LinksMetadata; - @WorkspaceField({ - standardId: CALENDAR_EVENT_STANDARD_FIELD_IDS.recurringEventExternalId, - type: FieldMetadataType.TEXT, - label: 'Recurring Event ID', - description: 'Recurring Event ID', - icon: 'IconHistory', - }) - recurringEventExternalId: string; - @WorkspaceRelation({ standardId: CALENDAR_EVENT_STANDARD_FIELD_IDS.calendarChannelEventAssociations, diff --git a/packages/twenty-server/src/modules/calendar/common/types/calendar-event.ts b/packages/twenty-server/src/modules/calendar/common/types/calendar-event.ts index 00f4c82ac5c5..50517314f4c3 100644 --- a/packages/twenty-server/src/modules/calendar/common/types/calendar-event.ts +++ b/packages/twenty-server/src/modules/calendar/common/types/calendar-event.ts @@ -5,6 +5,7 @@ export type CalendarEvent = Omit< CalendarEventWorkspaceEntity, | 'createdAt' | 'updatedAt' + | 'deletedAt' | 'calendarChannelEventAssociations' | 'calendarEventParticipants' | 'conferenceLink' @@ -19,6 +20,7 @@ export type CalendarEventParticipant = Omit< | 'id' | 'createdAt' | 'updatedAt' + | 'deletedAt' | 'personId' | 'workspaceMemberId' | 'person' @@ -34,6 +36,7 @@ export type CalendarEventParticipantWithCalendarEventId = export type CalendarEventWithParticipants = CalendarEvent & { externalId: string; + recurringEventExternalId?: string; participants: CalendarEventParticipant[]; status: string; }; @@ -41,6 +44,7 @@ export type CalendarEventWithParticipants = CalendarEvent & { export type CalendarEventWithParticipantsAndCalendarEventId = CalendarEvent & { id: string; externalId: string; + recurringEventExternalId?: string; participants: CalendarEventParticipantWithCalendarEventId[]; status: string; }; diff --git a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts index 32e774d4ccd5..df129f13471b 100644 --- a/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts +++ b/packages/twenty-server/src/modules/company/standard-objects/company.workspace-entity.ts @@ -1,5 +1,6 @@ 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, @@ -8,6 +9,7 @@ import { AddressMetadata } from 'src/engine/metadata-modules/field-metadata/comp import { CurrencyMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.composite-type'; import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.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, @@ -15,6 +17,7 @@ import { import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; +import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator'; import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.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'; @@ -22,6 +25,7 @@ import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace- import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; import { COMPANY_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 { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity'; import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; import { FavoriteWorkspaceEntity } from 'src/modules/favorite/standard-objects/favorite.workspace-entity'; @@ -32,6 +36,9 @@ import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/tas import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; +const NAME_FIELD_NAME = 'name'; +const DOMAIN_NAME_FIELD_NAME = 'domainName'; + @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.company, namePlural: 'companies', @@ -49,7 +56,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { description: 'The company name', icon: 'IconBuildingSkyscraper', }) - name: string; + [NAME_FIELD_NAME]: string; @WorkspaceField({ standardId: COMPANY_STANDARD_FIELD_IDS.domainName, @@ -59,7 +66,7 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { 'The company website URL. We use this url to fetch the company icon', icon: 'IconLink', }) - domainName?: LinksMetadata; + [DOMAIN_NAME_FIELD_NAME]?: LinksMetadata; @WorkspaceField({ standardId: COMPANY_STANDARD_FIELD_IDS.employees, @@ -273,4 +280,21 @@ export class CompanyWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceIsDeprecated() @WorkspaceIsNullable() addressOld: string; + + @WorkspaceField({ + standardId: COMPANY_STANDARD_FIELD_IDS.searchVector, + type: FieldMetadataType.TS_VECTOR, + label: SEARCH_VECTOR_FIELD.label, + description: SEARCH_VECTOR_FIELD.description, + icon: 'IconUser', + generatedType: 'STORED', + asExpression: getTsVectorColumnExpressionFromFields([ + { name: NAME_FIELD_NAME, type: FieldMetadataType.TEXT }, + { name: DOMAIN_NAME_FIELD_NAME, type: FieldMetadataType.LINKS }, + ]), + }) + @WorkspaceIsNullable() + @WorkspaceIsSystem() + @WorkspaceIndex({ indexType: IndexType.GIN }) + [SEARCH_VECTOR_FIELD.name]: any; } diff --git a/packages/twenty-server/src/modules/connected-account/email-alias-manager/email-alias-manager.module.ts b/packages/twenty-server/src/modules/connected-account/email-alias-manager/email-alias-manager.module.ts index e1678e3d7ab6..a634ff0feb2d 100644 --- a/packages/twenty-server/src/modules/connected-account/email-alias-manager/email-alias-manager.module.ts +++ b/packages/twenty-server/src/modules/connected-account/email-alias-manager/email-alias-manager.module.ts @@ -1,18 +1,11 @@ import { Module } from '@nestjs/common'; -import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/google-email-alias-manager.service'; import { EmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/services/email-alias-manager.service'; import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @Module({ - imports: [ - ObjectMetadataRepositoryModule.forFeature([ - ConnectedAccountWorkspaceEntity, - ]), - OAuth2ClientManagerModule, - ], + imports: [OAuth2ClientManagerModule], providers: [EmailAliasManagerService, GoogleEmailAliasManagerService], exports: [EmailAliasManagerService], }) diff --git a/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.ts b/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.ts index 50a855ba31a7..e4ec7e083960 100644 --- a/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.ts +++ b/packages/twenty-server/src/modules/connected-account/email-alias-manager/services/email-alias-manager.service.ts @@ -1,21 +1,18 @@ import { Injectable } from '@nestjs/common'; -import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; +import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { GoogleEmailAliasManagerService } from 'src/modules/connected-account/email-alias-manager/drivers/google/google-email-alias-manager.service'; -import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @Injectable() export class EmailAliasManagerService { constructor( - @InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity) - private readonly connectedAccountRepository: ConnectedAccountRepository, private readonly googleEmailAliasManagerService: GoogleEmailAliasManagerService, + private readonly twentyORMManager: TwentyORMManager, ) {} public async refreshHandleAliases( connectedAccount: ConnectedAccountWorkspaceEntity, - workspaceId: string, ) { let handleAliases: string[]; @@ -32,10 +29,16 @@ export class EmailAliasManagerService { ); } - await this.connectedAccountRepository.updateHandleAliases( - handleAliases, - connectedAccount.id, - workspaceId, + const connectedAccountRepository = + await this.twentyORMManager.getRepository<ConnectedAccountWorkspaceEntity>( + 'connectedAccount', + ); + + await connectedAccountRepository.update( + { id: connectedAccount.id }, + { + handleAliases: handleAliases.join(','), // TODO: modify handleAliases to be of fieldmetadatatype array + }, ); } } diff --git a/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts b/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts index da8bededdb03..62ee28d1bede 100644 --- a/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts +++ b/packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts @@ -15,8 +15,8 @@ export class ConnectedAccountListener { private readonly accountsToReconnectService: AccountsToReconnectService, ) {} - @OnEvent('connectedAccount.deleted') - async handleDeletedEvent( + @OnEvent('connectedAccount.destroyed') + async handleDestroyedEvent( payload: WorkspaceEventBatch< ObjectRecordDeleteEvent<ConnectedAccountWorkspaceEntity> >, diff --git a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts index c04999bea385..f49db465d04b 100644 --- a/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts +++ b/packages/twenty-server/src/modules/connected-account/query-hooks/connected-account-delete-one.pre-query.hook.ts @@ -8,7 +8,7 @@ import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { WorkspaceEventEmitter } from 'src/engine/workspace-event-emitter/workspace-event-emitter'; import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; -@WorkspaceQueryHook(`connectedAccount.deleteOne`) +@WorkspaceQueryHook(`connectedAccount.destroyOne`) export class ConnectedAccountDeleteOnePreQueryHook implements WorkspaceQueryHookInstance { @@ -34,7 +34,7 @@ export class ConnectedAccountDeleteOnePreQueryHook }); this.workspaceEventEmitter.emit( - 'messageChannel.deleted', + 'messageChannel.destroyed', messageChannels.map( (messageChannel) => ({ diff --git a/packages/twenty-server/src/modules/connected-account/refresh-access-token-manager/drivers/google/google-api-refresh-access-token.module.ts b/packages/twenty-server/src/modules/connected-account/refresh-access-token-manager/drivers/google/google-api-refresh-access-token.module.ts index 14529ba0b745..5c8308ec7053 100644 --- a/packages/twenty-server/src/modules/connected-account/refresh-access-token-manager/drivers/google/google-api-refresh-access-token.module.ts +++ b/packages/twenty-server/src/modules/connected-account/refresh-access-token-manager/drivers/google/google-api-refresh-access-token.module.ts @@ -1,17 +1,10 @@ import { Module } from '@nestjs/common'; -import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-access-token-manager/drivers/google/services/google-api-refresh-access-token.service'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; @Module({ - imports: [ - ObjectMetadataRepositoryModule.forFeature([ - ConnectedAccountWorkspaceEntity, - ]), - MessagingCommonModule, - ], + imports: [MessagingCommonModule], providers: [GoogleAPIRefreshAccessTokenService], exports: [GoogleAPIRefreshAccessTokenService], }) diff --git a/packages/twenty-server/src/modules/connected-account/refresh-access-token-manager/services/refresh-access-token.service.ts b/packages/twenty-server/src/modules/connected-account/refresh-access-token-manager/services/refresh-access-token.service.ts index 3326cd4ba8a1..853401356842 100644 --- a/packages/twenty-server/src/modules/connected-account/refresh-access-token-manager/services/refresh-access-token.service.ts +++ b/packages/twenty-server/src/modules/connected-account/refresh-access-token-manager/services/refresh-access-token.service.ts @@ -1,20 +1,18 @@ import { Injectable } from '@nestjs/common'; -import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; +import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; import { GoogleAPIRefreshAccessTokenService } from 'src/modules/connected-account/refresh-access-token-manager/drivers/google/services/google-api-refresh-access-token.service'; import { RefreshAccessTokenException, RefreshAccessTokenExceptionCode, } from 'src/modules/connected-account/refresh-access-token-manager/exceptions/refresh-access-token.exception'; -import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; @Injectable() export class RefreshAccessTokenService { constructor( private readonly googleAPIRefreshAccessTokenService: GoogleAPIRefreshAccessTokenService, - @InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity) - private readonly connectedAccountRepository: ConnectedAccountRepository, + private readonly twentyORMManager: TwentyORMManager, ) {} async refreshAndSaveAccessToken( @@ -44,10 +42,16 @@ export class RefreshAccessTokenService { ); } - await this.connectedAccountRepository.updateAccessToken( - accessToken, - connectedAccount.id, - workspaceId, + const connectedAccountRepository = + await this.twentyORMManager.getRepository<ConnectedAccountWorkspaceEntity>( + 'connectedAccount', + ); + + await connectedAccountRepository.update( + { id: connectedAccount.id }, + { + accessToken, + }, ); return accessToken; diff --git a/packages/twenty-server/src/modules/connected-account/repositories/connected-account.repository.ts b/packages/twenty-server/src/modules/connected-account/repositories/connected-account.repository.ts deleted file mode 100644 index 7be9d5d654fa..000000000000 --- a/packages/twenty-server/src/modules/connected-account/repositories/connected-account.repository.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { Injectable, NotFoundException } from '@nestjs/common'; - -import { EntityManager } from 'typeorm'; - -import { WorkspaceDataSourceService } from 'src/engine/workspace-datasource/workspace-datasource.service'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; - -@Injectable() -export class ConnectedAccountRepository { - constructor( - private readonly workspaceDataSourceService: WorkspaceDataSourceService, - ) {} - - public async getAll( - workspaceId: string, - transactionManager?: EntityManager, - ): Promise<ConnectedAccountWorkspaceEntity[]> { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - return await this.workspaceDataSourceService.executeRawQuery( - `SELECT * FROM ${dataSourceSchema}."connectedAccount" WHERE "provider" = 'google'`, - [], - workspaceId, - transactionManager, - ); - } - - public async getByIds( - connectedAccountIds: string[], - workspaceId: string, - transactionManager?: EntityManager, - ): Promise<ConnectedAccountWorkspaceEntity[]> { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - return await this.workspaceDataSourceService.executeRawQuery( - `SELECT * FROM ${dataSourceSchema}."connectedAccount" WHERE "id" = ANY($1)`, - [connectedAccountIds], - workspaceId, - transactionManager, - ); - } - - public async getAllByWorkspaceMemberId( - workspaceMemberId: string, - workspaceId: string, - transactionManager?: EntityManager, - ): Promise<ConnectedAccountWorkspaceEntity[] | undefined> { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - const connectedAccounts = - await this.workspaceDataSourceService.executeRawQuery( - `SELECT * FROM ${dataSourceSchema}."connectedAccount" WHERE "accountOwnerId" = $1`, - [workspaceMemberId], - workspaceId, - transactionManager, - ); - - return connectedAccounts; - } - - public async getAllByHandleAndWorkspaceMemberId( - handle: string, - workspaceMemberId: string, - workspaceId: string, - transactionManager?: EntityManager, - ): Promise<ConnectedAccountWorkspaceEntity[] | undefined> { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - const connectedAccounts = - await this.workspaceDataSourceService.executeRawQuery( - `SELECT * FROM ${dataSourceSchema}."connectedAccount" WHERE "handle" = $1 AND "accountOwnerId" = $2 LIMIT 1`, - [handle, workspaceMemberId], - workspaceId, - transactionManager, - ); - - return connectedAccounts; - } - - public async create( - connectedAccount: Pick< - ConnectedAccountWorkspaceEntity, - | 'id' - | 'handle' - | 'provider' - | 'accessToken' - | 'refreshToken' - | 'accountOwnerId' - >, - workspaceId: string, - transactionManager?: EntityManager, - ): Promise<ConnectedAccountWorkspaceEntity> { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - return await this.workspaceDataSourceService.executeRawQuery( - `INSERT INTO ${dataSourceSchema}."connectedAccount" ("id", "handle", "provider", "accessToken", "refreshToken", "accountOwnerId") VALUES ($1, $2, $3, $4, $5, $6)`, - [ - connectedAccount.id, - connectedAccount.handle, - connectedAccount.provider, - connectedAccount.accessToken, - connectedAccount.refreshToken, - connectedAccount.accountOwnerId, - ], - workspaceId, - transactionManager, - ); - } - - public async updateAccessTokenAndRefreshToken( - accessToken: string, - refreshToken: string, - connectedAccountId: string, - workspaceId: string, - transactionManager?: EntityManager, - ) { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - await this.workspaceDataSourceService.executeRawQuery( - `UPDATE ${dataSourceSchema}."connectedAccount" SET "accessToken" = $1, "refreshToken" = $2, "authFailedAt" = NULL WHERE "id" = $3`, - [accessToken, refreshToken, connectedAccountId], - workspaceId, - transactionManager, - ); - } - - public async getById( - connectedAccountId: string, - workspaceId: string, - transactionManager?: EntityManager, - ): Promise<ConnectedAccountWorkspaceEntity | undefined> { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - const connectedAccounts = - await this.workspaceDataSourceService.executeRawQuery( - `SELECT * FROM ${dataSourceSchema}."connectedAccount" WHERE "id" = $1 LIMIT 1`, - [connectedAccountId], - workspaceId, - transactionManager, - ); - - return connectedAccounts[0]; - } - - public async getByIdOrFail( - connectedAccountId: string, - workspaceId: string, - transactionManager?: EntityManager, - ): Promise<ConnectedAccountWorkspaceEntity> { - const connectedAccount = await this.getById( - connectedAccountId, - workspaceId, - transactionManager, - ); - - if (!connectedAccount) { - throw new NotFoundException( - `Connected account with id ${connectedAccountId} not found in workspace ${workspaceId}`, - ); - } - - return connectedAccount; - } - - public async updateAccessToken( - accessToken: string, - connectedAccountId: string, - workspaceId: string, - transactionManager?: EntityManager, - ) { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - await this.workspaceDataSourceService.executeRawQuery( - `UPDATE ${dataSourceSchema}."connectedAccount" SET "accessToken" = $1, "authFailedAt" = NULL WHERE "id" = $2`, - [accessToken, connectedAccountId], - workspaceId, - transactionManager, - ); - } - - public async updateAuthFailedAt( - connectedAccountId: string, - workspaceId: string, - transactionManager?: EntityManager, - ) { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - await this.workspaceDataSourceService.executeRawQuery( - `UPDATE ${dataSourceSchema}."connectedAccount" SET "authFailedAt" = NOW() WHERE "id" = $1`, - [connectedAccountId], - workspaceId, - transactionManager, - ); - } - - public async getConnectedAccountOrThrow( - workspaceId: string, - connectedAccountId: string, - ): Promise<ConnectedAccountWorkspaceEntity> { - const connectedAccount = await this.getById( - connectedAccountId, - workspaceId, - ); - - if (!connectedAccount) { - throw new Error( - `Connected account ${connectedAccountId} not found in workspace ${workspaceId}`, - ); - } - - return connectedAccount; - } - - public async updateHandleAliases( - handleAliases: string[], - connectedAccountId: string, - workspaceId: string, - transactionManager?: EntityManager, - ) { - const dataSourceSchema = - this.workspaceDataSourceService.getSchemaName(workspaceId); - - await this.workspaceDataSourceService.executeRawQuery( - `UPDATE ${dataSourceSchema}."connectedAccount" SET "handleAliases" = $1 WHERE "id" = $2`, - // TODO: modify handleAliases to be of fieldmetadatatype array - [handleAliases.join(','), connectedAccountId], - workspaceId, - transactionManager, - ); - } -} diff --git a/packages/twenty-server/src/modules/connected-account/standard-objects/connected-account.workspace-entity.ts b/packages/twenty-server/src/modules/connected-account/standard-objects/connected-account.workspace-entity.ts index f09bb74ea697..12363ed088b8 100644 --- a/packages/twenty-server/src/modules/connected-account/standard-objects/connected-account.workspace-entity.ts +++ b/packages/twenty-server/src/modules/connected-account/standard-objects/connected-account.workspace-entity.ts @@ -99,6 +99,16 @@ export class ConnectedAccountWorkspaceEntity extends BaseWorkspaceEntity { }) handleAliases: string; + @WorkspaceField({ + standardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.scopes, + type: FieldMetadataType.ARRAY, + label: 'Scopes', + description: 'Scopes', + icon: 'IconSettings', + }) + @WorkspaceIsNullable() + scopes: string[] | null; + @WorkspaceRelation({ standardId: CONNECTED_ACCOUNT_STANDARD_FIELD_IDS.accountOwner, type: RelationMetadataType.MANY_TO_ONE, diff --git a/packages/twenty-server/src/modules/mail-sender/exceptions/mail-sender.exception.ts b/packages/twenty-server/src/modules/mail-sender/exceptions/mail-sender.exception.ts new file mode 100644 index 000000000000..01e1b2ed932f --- /dev/null +++ b/packages/twenty-server/src/modules/mail-sender/exceptions/mail-sender.exception.ts @@ -0,0 +1,13 @@ +import { CustomException } from 'src/utils/custom-exception'; + +export class MailSenderException extends CustomException { + code: MailSenderExceptionCode; + constructor(message: string, code: MailSenderExceptionCode) { + super(message, code); + } +} + +export enum MailSenderExceptionCode { + PROVIDER_NOT_SUPPORTED = 'PROVIDER_NOT_SUPPORTED', + CONNECTED_ACCOUNT_NOT_FOUND = 'CONNECTED_ACCOUNT_NOT_FOUND', +} diff --git a/packages/twenty-server/src/modules/mail-sender/workflow-actions/send-email.workflow-action.ts b/packages/twenty-server/src/modules/mail-sender/workflow-actions/send-email.workflow-action.ts index 3a17c77cf92d..026b4537d898 100644 --- a/packages/twenty-server/src/modules/mail-sender/workflow-actions/send-email.workflow-action.ts +++ b/packages/twenty-server/src/modules/mail-sender/workflow-actions/send-email.workflow-action.ts @@ -4,13 +4,24 @@ import { z } from 'zod'; import Handlebars from 'handlebars'; import { JSDOM } from 'jsdom'; import DOMPurify from 'dompurify'; -import { WorkflowActionEmail } from 'twenty-emails'; -import { render } from '@react-email/components'; import { WorkflowActionResult } from 'src/modules/workflow/workflow-executor/types/workflow-action-result.type'; import { WorkflowSendEmailStep } from 'src/modules/workflow/workflow-executor/types/workflow-action.type'; import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; +import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; +import { + WorkflowStepExecutorException, + WorkflowStepExecutorExceptionCode, +} from 'src/modules/workflow/workflow-executor/exceptions/workflow-step-executor.exception'; +import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; +import { + MailSenderException, + MailSenderExceptionCode, +} from 'src/modules/mail-sender/exceptions/mail-sender.exception'; +import { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider'; +import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager'; +import { isDefined } from 'src/utils/is-defined'; @Injectable() export class SendEmailWorkflowAction { @@ -18,8 +29,48 @@ export class SendEmailWorkflowAction { constructor( private readonly environmentService: EnvironmentService, private readonly emailService: EmailService, + private readonly gmailClientProvider: GmailClientProvider, + private readonly scopedWorkspaceContextFactory: ScopedWorkspaceContextFactory, + private readonly twentyORMGlobalManager: TwentyORMGlobalManager, ) {} + private async getEmailClient(step: WorkflowSendEmailStep) { + const { workspaceId } = this.scopedWorkspaceContextFactory.create(); + + if (!workspaceId) { + throw new WorkflowStepExecutorException( + 'Scoped workspace not found', + WorkflowStepExecutorExceptionCode.SCOPED_WORKSPACE_NOT_FOUND, + ); + } + + const connectedAccountRepository = + await this.twentyORMGlobalManager.getRepositoryForWorkspace<ConnectedAccountWorkspaceEntity>( + workspaceId, + 'connectedAccount', + ); + const connectedAccount = await connectedAccountRepository.findOneBy({ + id: step.settings.connectedAccountId, + }); + + if (!isDefined(connectedAccount)) { + throw new MailSenderException( + `Connected Account '${step.settings.connectedAccountId}' not found`, + MailSenderExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND, + ); + } + + switch (connectedAccount.provider) { + case 'google': + return await this.gmailClientProvider.getGmailClient(connectedAccount); + default: + throw new MailSenderException( + `Provider ${connectedAccount.provider} is not supported`, + MailSenderExceptionCode.PROVIDER_NOT_SUPPORTED, + ); + } + } + async execute({ step, payload, @@ -30,6 +81,8 @@ export class SendEmailWorkflowAction { [key: string]: string; }; }): Promise<WorkflowActionResult> { + const emailProvider = await this.getEmailClient(step); + try { const emailSchema = z.string().trim().email('Invalid email'); @@ -41,34 +94,34 @@ export class SendEmailWorkflowAction { return { result: { success: false } }; } - const mainText = Handlebars.compile(step.settings.template)(payload); + const body = Handlebars.compile(step.settings.body)(payload); + const subject = Handlebars.compile(step.settings.subject)(payload); const window = new JSDOM('').window; const purify = DOMPurify(window); - const safeHTML = purify.sanitize(mainText || ''); + const safeBody = purify.sanitize(body || ''); + const safeSubject = purify.sanitize(subject || ''); - const email = WorkflowActionEmail({ - dangerousHTML: safeHTML, - title: step.settings.title, - callToAction: step.settings.callToAction, - }); - const html = render(email, { - pretty: true, - }); - const text = render(email, { - plainText: true, - }); + const message = [ + `To: ${payload.email}`, + `Subject: ${safeSubject || ''}`, + 'MIME-Version: 1.0', + 'Content-Type: text/plain; charset="UTF-8"', + '', + safeBody, + ].join('\n'); + + const encodedMessage = Buffer.from(message).toString('base64'); - await this.emailService.send({ - from: `${this.environmentService.get( - 'EMAIL_FROM_NAME', - )} <${this.environmentService.get('EMAIL_FROM_ADDRESS')}>`, - to: payload.email, - subject: step.settings.subject || '', - text, - html, + await emailProvider.users.messages.send({ + userId: 'me', + requestBody: { + raw: encodedMessage, + }, }); + this.logger.log(`Email sent successfully`); + return { result: { success: true } }; } catch (error) { return { error }; diff --git a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/can-access-message-thread.service.ts b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/can-access-message-thread.service.ts index 44141f3dde92..0e30225843ed 100644 --- a/packages/twenty-server/src/modules/messaging/common/query-hooks/message/can-access-message-thread.service.ts +++ b/packages/twenty-server/src/modules/messaging/common/query-hooks/message/can-access-message-thread.service.ts @@ -5,17 +5,13 @@ import { Any } from 'typeorm'; import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; -import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; import { WorkspaceMemberRepository } from 'src/modules/workspace-member/repositories/workspace-member.repository'; import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity'; -import { isDefined } from 'src/utils/is-defined'; export class CanAccessMessageThreadService { constructor( - @InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity) - private readonly connectedAccountRepository: ConnectedAccountRepository, @InjectObjectMetadataRepository(WorkspaceMemberWorkspaceEntity) private readonly workspaceMemberRepository: WorkspaceMemberRepository, private readonly twentyORMManager: TwentyORMManager, @@ -31,6 +27,7 @@ export class CanAccessMessageThreadService { 'messageChannel', ); const messageChannels = await messageChannelRepository.find({ + select: ['id', 'visibility'], where: { id: Any( messageChannelMessageAssociations.map( @@ -52,20 +49,20 @@ export class CanAccessMessageThreadService { const currentWorkspaceMember = await this.workspaceMemberRepository.getByIdOrFail(userId, workspaceId); - const messageChannelsConnectedAccounts = - await this.connectedAccountRepository.getByIds( - messageChannels - .map((channel) => channel.connectedAccountId) - .filter(isDefined), - workspaceId, + const connectedAccountRepository = + await this.twentyORMManager.getRepository<ConnectedAccountWorkspaceEntity>( + 'connectedAccount', ); - const messageChannelsWorkspaceMemberIds = - messageChannelsConnectedAccounts.map( - (connectedAccount) => connectedAccount.accountOwnerId, - ); + const connectedAccounts = await connectedAccountRepository.find({ + select: ['id'], + where: { + messageChannels: Any(messageChannels.map((channel) => channel.id)), + accountOwnerId: currentWorkspaceMember.id, + }, + }); - if (messageChannelsWorkspaceMemberIds.includes(currentWorkspaceMember.id)) { + if (connectedAccounts.length > 0) { return; } diff --git a/packages/twenty-server/src/modules/messaging/common/query-hooks/messaging-query-hook.module.ts b/packages/twenty-server/src/modules/messaging/common/query-hooks/messaging-query-hook.module.ts index 9c84640768af..1222865f6962 100644 --- a/packages/twenty-server/src/modules/messaging/common/query-hooks/messaging-query-hook.module.ts +++ b/packages/twenty-server/src/modules/messaging/common/query-hooks/messaging-query-hook.module.ts @@ -1,7 +1,6 @@ import { Module } from '@nestjs/common'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { CanAccessMessageThreadService } from 'src/modules/messaging/common/query-hooks/message/can-access-message-thread.service'; import { MessageFindManyPreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-many.pre-query.hook'; import { MessageFindOnePreQueryHook } from 'src/modules/messaging/common/query-hooks/message/message-find-one.pre-query-hook'; @@ -9,10 +8,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta @Module({ imports: [ - ObjectMetadataRepositoryModule.forFeature([ - ConnectedAccountWorkspaceEntity, - WorkspaceMemberWorkspaceEntity, - ]), + ObjectMetadataRepositoryModule.forFeature([WorkspaceMemberWorkspaceEntity]), ], providers: [ CanAccessMessageThreadService, diff --git a/packages/twenty-server/src/modules/messaging/common/services/message-channel-sync-status.service.ts b/packages/twenty-server/src/modules/messaging/common/services/message-channel-sync-status.service.ts index 19e6746ce1a3..970290c35841 100644 --- a/packages/twenty-server/src/modules/messaging/common/services/message-channel-sync-status.service.ts +++ b/packages/twenty-server/src/modules/messaging/common/services/message-channel-sync-status.service.ts @@ -146,6 +146,7 @@ export class MessageChannelSyncStatusService { syncStage: MessageChannelSyncStage.PARTIAL_MESSAGE_LIST_FETCH_PENDING, throttleFailureCount: 0, syncStageStartedAt: null, + syncedAt: new Date().toISOString(), }); } @@ -161,6 +162,7 @@ export class MessageChannelSyncStatusService { await messageChannelRepository.update(messageChannelIds, { syncStage: MessageChannelSyncStage.MESSAGES_IMPORT_ONGOING, + syncStageStartedAt: new Date().toISOString(), }); } diff --git a/packages/twenty-server/src/modules/messaging/message-cleaner/listeners/messaging-message-cleaner-connected-account.listener.ts b/packages/twenty-server/src/modules/messaging/message-cleaner/listeners/messaging-message-cleaner-connected-account.listener.ts index 8e837cce6a59..c5c3a033dedc 100644 --- a/packages/twenty-server/src/modules/messaging/message-cleaner/listeners/messaging-message-cleaner-connected-account.listener.ts +++ b/packages/twenty-server/src/modules/messaging/message-cleaner/listeners/messaging-message-cleaner-connected-account.listener.ts @@ -19,8 +19,8 @@ export class MessagingMessageCleanerConnectedAccountListener { private readonly messageQueueService: MessageQueueService, ) {} - @OnEvent('connectedAccount.deleted') - async handleDeletedEvent( + @OnEvent('connectedAccount.destroyed') + async handleDestroyedEvent( payload: WorkspaceEventBatch< ObjectRecordDeleteEvent<ConnectedAccountWorkspaceEntity> >, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module.ts index 3fc555a87c96..c6ee8b72cff5 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module.ts @@ -2,15 +2,14 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { EnvironmentModule } from 'src/engine/core-modules/environment/environment.module'; import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; -import { EnvironmentModule } from 'src/engine/core-modules/environment/environment.module'; import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module'; import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module'; import { BlocklistWorkspaceEntity } from 'src/modules/blocklist/standard-objects/blocklist.workspace-entity'; import { EmailAliasManagerModule } from 'src/modules/connected-account/email-alias-manager/email-alias-manager.module'; import { OAuth2ClientManagerModule } from 'src/modules/connected-account/oauth2-client-manager/oauth2-client-manager.module'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { MessagingCommonModule } from 'src/modules/messaging/common/messaging-common.module'; import { GmailClientProvider } from 'src/modules/messaging/message-import-manager/drivers/gmail/providers/gmail-client.provider'; import { GmailFetchByBatchService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-fetch-by-batch.service'; @@ -26,10 +25,7 @@ import { MessageParticipantManagerModule } from 'src/modules/messaging/message-p baseURL: 'https://www.googleapis.com/batch/gmail/v1', }), EnvironmentModule, - ObjectMetadataRepositoryModule.forFeature([ - ConnectedAccountWorkspaceEntity, - BlocklistWorkspaceEntity, - ]), + ObjectMetadataRepositoryModule.forFeature([BlocklistWorkspaceEntity]), MessagingCommonModule, TypeOrmModule.forFeature([FeatureFlagEntity], 'core'), OAuth2ClientManagerModule, @@ -46,6 +42,10 @@ import { MessageParticipantManagerModule } from 'src/modules/messaging/message-p GmailGetMessageListService, GmailHandleErrorService, ], - exports: [GmailGetMessagesService, GmailGetMessageListService], + exports: [ + GmailGetMessagesService, + GmailGetMessageListService, + GmailClientProvider, + ], }) export class MessagingGmailDriverModule {} diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts index cc66a1ff5dbc..67761ec5d780 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job.ts @@ -3,16 +3,12 @@ import { Logger, Scope } from '@nestjs/common'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; -import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { isThrottled } from 'src/modules/connected-account/utils/is-throttled'; import { MessageChannelSyncStage, MessageChannelWorkspaceEntity, } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; -import { MessageImportExceptionHandlerService } from 'src/modules/messaging/message-import-manager/services/message-import-exception-handler.service'; import { MessagingFullMessageListFetchService } from 'src/modules/messaging/message-import-manager/services/messaging-full-message-list-fetch.service'; import { MessagingPartialMessageListFetchService } from 'src/modules/messaging/message-import-manager/services/messaging-partial-message-list-fetch.service'; import { MessagingTelemetryService } from 'src/modules/messaging/monitoring/services/messaging-telemetry.service'; @@ -32,11 +28,8 @@ export class MessagingMessageListFetchJob { constructor( private readonly messagingFullMessageListFetchService: MessagingFullMessageListFetchService, private readonly messagingPartialMessageListFetchService: MessagingPartialMessageListFetchService, - @InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity) - private readonly connectedAccountRepository: ConnectedAccountRepository, private readonly messagingTelemetryService: MessagingTelemetryService, private readonly twentyORMManager: TwentyORMManager, - private readonly messageImportErrorHandlerService: MessageImportExceptionHandlerService, ) {} @Process(MessagingMessageListFetchJob.name) @@ -60,6 +53,7 @@ export class MessagingMessageListFetchJob { where: { id: messageChannelId, }, + relations: ['connectedAccount'], }); if (!messageChannel) { @@ -72,16 +66,6 @@ export class MessagingMessageListFetchJob { return; } - const connectedAccount = - await this.connectedAccountRepository.getByIdOrFail( - messageChannel.connectedAccountId, - workspaceId, - ); - - if (!messageChannel?.isSyncEnabled) { - return; - } - if ( isThrottled( messageChannel.syncStageStartedAt, @@ -100,20 +84,20 @@ export class MessagingMessageListFetchJob { await this.messagingTelemetryService.track({ eventName: 'partial_message_list_fetch.started', workspaceId, - connectedAccountId: connectedAccount.id, + connectedAccountId: messageChannel.connectedAccount.id, messageChannelId: messageChannel.id, }); await this.messagingPartialMessageListFetchService.processMessageListFetch( messageChannel, - connectedAccount, + messageChannel.connectedAccount, workspaceId, ); await this.messagingTelemetryService.track({ eventName: 'partial_message_list_fetch.completed', workspaceId, - connectedAccountId: connectedAccount.id, + connectedAccountId: messageChannel.connectedAccount.id, messageChannelId: messageChannel.id, }); @@ -121,26 +105,26 @@ export class MessagingMessageListFetchJob { case MessageChannelSyncStage.FULL_MESSAGE_LIST_FETCH_PENDING: this.logger.log( - `Fetching full message list for workspace ${workspaceId} and account ${connectedAccount.id}`, + `Fetching full message list for workspace ${workspaceId} and account ${messageChannel.connectedAccount.id}`, ); await this.messagingTelemetryService.track({ eventName: 'full_message_list_fetch.started', workspaceId, - connectedAccountId: connectedAccount.id, + connectedAccountId: messageChannel.connectedAccount.id, messageChannelId: messageChannel.id, }); await this.messagingFullMessageListFetchService.processMessageListFetch( messageChannel, - connectedAccount, + messageChannel.connectedAccount, workspaceId, ); await this.messagingTelemetryService.track({ eventName: 'full_message_list_fetch.completed', workspaceId, - connectedAccountId: connectedAccount.id, + connectedAccountId: messageChannel.connectedAccount.id, messageChannelId: messageChannel.id, }); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts index a22e1b26a769..f062400d19bb 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/jobs/messaging-messages-import.job.ts @@ -3,16 +3,12 @@ import { Scope } from '@nestjs/common'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; -import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository'; -import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { isThrottled } from 'src/modules/connected-account/utils/is-throttled'; import { MessageChannelSyncStage, MessageChannelWorkspaceEntity, } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity'; -import { MessageImportExceptionHandlerService } from 'src/modules/messaging/message-import-manager/services/message-import-exception-handler.service'; import { MessagingMessagesImportService } from 'src/modules/messaging/message-import-manager/services/messaging-messages-import.service'; import { MessagingTelemetryService } from 'src/modules/messaging/monitoring/services/messaging-telemetry.service'; @@ -27,12 +23,9 @@ export type MessagingMessagesImportJobData = { }) export class MessagingMessagesImportJob { constructor( - @InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity) - private readonly connectedAccountRepository: ConnectedAccountRepository, private readonly messagingMessagesImportService: MessagingMessagesImportService, private readonly messagingTelemetryService: MessagingTelemetryService, private readonly twentyORMManager: TwentyORMManager, - private readonly messageImportErrorHandlerService: MessageImportExceptionHandlerService, ) {} @Process(MessagingMessagesImportJob.name) @@ -56,6 +49,7 @@ export class MessagingMessagesImportJob { where: { id: messageChannelId, }, + relations: ['connectedAccount'], }); if (!messageChannel) { @@ -68,12 +62,6 @@ export class MessagingMessagesImportJob { return; } - const connectedAccount = - await this.connectedAccountRepository.getConnectedAccountOrThrow( - workspaceId, - messageChannel.connectedAccountId, - ); - if (!messageChannel?.isSyncEnabled) { return; } @@ -96,7 +84,7 @@ export class MessagingMessagesImportJob { await this.messagingMessagesImportService.processMessageBatchImport( messageChannel, - connectedAccount, + messageChannel.connectedAccount, workspaceId, ); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/listeners/messaging-import-manager-message-channel.listener.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/listeners/messaging-import-manager-message-channel.listener.ts index 513e2672adf4..80802c3fb2c4 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/listeners/messaging-import-manager-message-channel.listener.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/listeners/messaging-import-manager-message-channel.listener.ts @@ -19,8 +19,8 @@ export class MessagingMessageImportManagerMessageChannelListener { private readonly messageQueueService: MessageQueueService, ) {} - @OnEvent('messageChannel.deleted') - async handleDeletedEvent( + @OnEvent('messageChannel.destroyed') + async handleDestroyedEvent( payload: WorkspaceEventBatch< ObjectRecordDeleteEvent<MessageChannelWorkspaceEntity> >, diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-messages-import.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-messages-import.service.ts index 6017441003ae..09c59c55758b 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-messages-import.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-messages-import.service.ts @@ -110,7 +110,6 @@ export class MessagingMessagesImportService { await this.emailAliasManagerService.refreshHandleAliases( connectedAccount, - workspaceId, ); messageIdsToFetch = await this.cacheStorage.setPop( diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-partial-message-list-fetch.service.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-partial-message-list-fetch.service.ts index bdf13895045a..b80ebc9c86ad 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-partial-message-list-fetch.service.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/services/messaging-partial-message-list-fetch.service.ts @@ -54,7 +54,6 @@ export class MessagingPartialMessageListFetchService { }, { throttleFailureCount: 0, - syncStageStartedAt: null, }, ); diff --git a/packages/twenty-server/src/modules/messaging/message-import-manager/types/message.ts b/packages/twenty-server/src/modules/messaging/message-import-manager/types/message.ts index 4ec483869a8d..b665e54898a8 100644 --- a/packages/twenty-server/src/modules/messaging/message-import-manager/types/message.ts +++ b/packages/twenty-server/src/modules/messaging/message-import-manager/types/message.ts @@ -6,6 +6,7 @@ export type Message = Omit< MessageWorkspaceEntity, | 'createdAt' | 'updatedAt' + | 'deletedAt' | 'messageChannelMessageAssociations' | 'messageParticipants' | 'messageThread' @@ -25,6 +26,7 @@ export type MessageParticipant = Omit< | 'id' | 'createdAt' | 'updatedAt' + | 'deletedAt' | 'personId' | 'workspaceMemberId' | 'person' diff --git a/packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job.ts b/packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job.ts index 8529ebaef6f4..ee556d672d96 100644 --- a/packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job.ts +++ b/packages/twenty-server/src/modules/messaging/message-participant-manager/jobs/messaging-create-company-and-contact-after-sync.job.ts @@ -6,9 +6,7 @@ import { Process } from 'src/engine/core-modules/message-queue/decorators/proces import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { FieldActorSource } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; -import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; -import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository'; import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity'; import { CreateCompanyAndContactService } from 'src/modules/contact-creation-manager/services/create-company-and-contact.service'; import { MessageDirection } from 'src/modules/messaging/common/enums/message-direction.enum'; @@ -30,8 +28,6 @@ export class MessagingCreateCompanyAndContactAfterSyncJob { ); constructor( private readonly createCompanyAndContactService: CreateCompanyAndContactService, - @InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity) - private readonly connectedAccountRepository: ConnectedAccountRepository, private readonly twentyORMManager: TwentyORMManager, ) {} @@ -63,10 +59,16 @@ export class MessagingCreateCompanyAndContactAfterSyncJob { return; } - const connectedAccount = await this.connectedAccountRepository.getById( - connectedAccountId, - workspaceId, - ); + const connectedAccountRepository = + await this.twentyORMManager.getRepository<ConnectedAccountWorkspaceEntity>( + 'connectedAccount', + ); + + const connectedAccount = await connectedAccountRepository.findOne({ + where: { + id: connectedAccountId, + }, + }); if (!connectedAccount) { throw new Error( diff --git a/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts b/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts index 142b000b9a0d..72e71098ea50 100644 --- a/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts +++ b/packages/twenty-server/src/modules/opportunity/standard-objects/opportunity.workspace-entity.ts @@ -1,11 +1,13 @@ 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 { CurrencyMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/currency.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, @@ -22,6 +24,7 @@ import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace- import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; import { OPPORTUNITY_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 { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity'; import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; import { CompanyWorkspaceEntity } from 'src/modules/company/standard-objects/company.workspace-entity'; @@ -31,6 +34,8 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity'; import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; +const NAME_FIELD_NAME = 'name'; + @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.opportunity, namePlural: 'opportunities', @@ -232,4 +237,20 @@ export class OpportunityWorkspaceEntity extends BaseWorkspaceEntity { }) @WorkspaceIsDeprecated() probability: string; + + @WorkspaceField({ + standardId: OPPORTUNITY_STANDARD_FIELD_IDS.searchVector, + type: FieldMetadataType.TS_VECTOR, + label: SEARCH_VECTOR_FIELD.label, + description: SEARCH_VECTOR_FIELD.description, + icon: 'IconUser', + generatedType: 'STORED', + asExpression: getTsVectorColumnExpressionFromFields([ + { name: NAME_FIELD_NAME, type: FieldMetadataType.TEXT }, + ]), + }) + @WorkspaceIsNullable() + @WorkspaceIsSystem() + @WorkspaceIndex({ indexType: IndexType.GIN }) + [SEARCH_VECTOR_FIELD.name]: any; } diff --git a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts index e162bb82b7b9..d2cccd1d9edd 100644 --- a/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts +++ b/packages/twenty-server/src/modules/person/standard-objects/person.workspace-entity.ts @@ -1,5 +1,6 @@ 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, @@ -9,6 +10,7 @@ import { FullNameMetadata } from 'src/engine/metadata-modules/field-metadata/com import { LinksMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/links.composite-type'; import { PhonesMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/phones.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, @@ -16,6 +18,7 @@ import { import { BaseWorkspaceEntity } from 'src/engine/twenty-orm/base.workspace-entity'; import { WorkspaceEntity } from 'src/engine/twenty-orm/decorators/workspace-entity.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; +import { WorkspaceIndex } from 'src/engine/twenty-orm/decorators/workspace-index.decorator'; import { WorkspaceIsDeprecated } from 'src/engine/twenty-orm/decorators/workspace-is-deprecated.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'; @@ -23,6 +26,7 @@ import { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace- import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; import { PERSON_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 { getTsVectorColumnExpressionFromFields } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/get-ts-vector-column-expression.util'; import { ActivityTargetWorkspaceEntity } from 'src/modules/activity/standard-objects/activity-target.workspace-entity'; import { AttachmentWorkspaceEntity } from 'src/modules/attachment/standard-objects/attachment.workspace-entity'; import { CalendarEventParticipantWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-event-participant.workspace-entity'; @@ -34,6 +38,10 @@ import { OpportunityWorkspaceEntity } from 'src/modules/opportunity/standard-obj import { TaskTargetWorkspaceEntity } from 'src/modules/task/standard-objects/task-target.workspace-entity'; import { TimelineActivityWorkspaceEntity } from 'src/modules/timeline/standard-objects/timeline-activity.workspace-entity'; +const NAME_FIELD_NAME = 'name'; +const EMAILS_FIELD_NAME = 'emails'; +const JOB_TITLE_FIELD_NAME = 'jobTitle'; + @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.person, namePlural: 'people', @@ -53,7 +61,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { icon: 'IconUser', }) @WorkspaceIsNullable() - name: FullNameMetadata | null; + [NAME_FIELD_NAME]: FullNameMetadata | null; @WorkspaceField({ standardId: PERSON_STANDARD_FIELD_IDS.email, @@ -72,7 +80,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { description: 'Contact’s Emails', icon: 'IconMail', }) - emails: EmailsMetadata; + [EMAILS_FIELD_NAME]: EmailsMetadata; @WorkspaceField({ standardId: PERSON_STANDARD_FIELD_IDS.linkedinLink, @@ -101,7 +109,7 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { description: 'Contact’s job title', icon: 'IconBriefcase', }) - jobTitle: string; + [JOB_TITLE_FIELD_NAME]: string; @WorkspaceField({ standardId: PERSON_STANDARD_FIELD_IDS.phone, @@ -290,4 +298,22 @@ export class PersonWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceIsNullable() @WorkspaceIsSystem() timelineActivities: Relation<TimelineActivityWorkspaceEntity[]>; + + @WorkspaceField({ + standardId: PERSON_STANDARD_FIELD_IDS.searchVector, + type: FieldMetadataType.TS_VECTOR, + label: SEARCH_VECTOR_FIELD.label, + description: SEARCH_VECTOR_FIELD.description, + icon: 'IconUser', + generatedType: 'STORED', + asExpression: getTsVectorColumnExpressionFromFields([ + { name: NAME_FIELD_NAME, type: FieldMetadataType.FULL_NAME }, + { name: EMAILS_FIELD_NAME, type: FieldMetadataType.EMAILS }, + { name: JOB_TITLE_FIELD_NAME, type: FieldMetadataType.TEXT }, + ]), + }) + @WorkspaceIsNullable() + @WorkspaceIsSystem() + @WorkspaceIndex({ indexType: IndexType.GIN }) + [SEARCH_VECTOR_FIELD.name]: any; } diff --git a/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts b/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts index 149171e6eb7f..3e45fee95d60 100644 --- a/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts +++ b/packages/twenty-server/src/modules/view/standard-objects/view-filter.workspace-entity.ts @@ -1,18 +1,18 @@ import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface'; import { FieldMetadataType } from 'src/engine/metadata-modules/field-metadata/field-metadata.entity'; -import { VIEW_FILTER_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 { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; +import { RelationMetadataType } 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 { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.decorator'; -import { WorkspaceIsSystem } from 'src/engine/twenty-orm/decorators/workspace-is-system.decorator'; import { WorkspaceField } from 'src/engine/twenty-orm/decorators/workspace-field.decorator'; -import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; -import { RelationMetadataType } from 'src/engine/metadata-modules/relation-metadata/relation-metadata.entity'; +import { WorkspaceIsNotAuditLogged } from 'src/engine/twenty-orm/decorators/workspace-is-not-audit-logged.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 { WorkspaceJoinColumn } from 'src/engine/twenty-orm/decorators/workspace-join-column.decorator'; +import { WorkspaceRelation } from 'src/engine/twenty-orm/decorators/workspace-relation.decorator'; +import { VIEW_FILTER_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 { ViewWorkspaceEntity } from 'src/modules/view/standard-objects/view.workspace-entity'; @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.viewFilter, diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts index c38cf4b5a485..347536f57a82 100644 --- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-run.workspace-entity.ts @@ -27,18 +27,39 @@ export enum WorkflowRunStatus { FAILED = 'FAILED', } +export type WorkflowRunOutput = { + steps: { + id: string; + name: string; + type: string; + attemptCount: number; + result: object | undefined; + error: string | undefined; + }[]; +}; + @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.workflowRun, namePlural: 'workflowRuns', - labelSingular: 'workflowRun', - labelPlural: 'WorkflowRuns', + labelSingular: 'Workflow Run', + labelPlural: 'Workflow Runs', description: 'A workflow run', + labelIdentifierStandardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.name, + icon: 'IconHistory', }) @WorkspaceGate({ featureFlag: FeatureFlagKey.IsWorkflowEnabled, }) -@WorkspaceIsSystem() export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity { + @WorkspaceField({ + standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.name, + type: FieldMetadataType.TEXT, + label: 'Name', + description: 'Name of the workflow run', + icon: 'IconText', + }) + name: string; + @WorkspaceField({ standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.startedAt, type: FieldMetadataType.DATE_TIME, @@ -64,7 +85,7 @@ export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity { type: FieldMetadataType.SELECT, label: 'Workflow run status', description: 'Workflow run status', - icon: 'IconHistory', + icon: 'IconStatusChange', options: [ { value: WorkflowRunStatus.NOT_STARTED, @@ -108,6 +129,26 @@ export class WorkflowRunWorkspaceEntity extends BaseWorkspaceEntity { }) createdBy: ActorMetadata; + @WorkspaceField({ + standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.output, + type: FieldMetadataType.RAW_JSON, + label: 'Output', + description: 'Json object to provide output of the workflow run', + }) + @WorkspaceIsNullable() + output: WorkflowRunOutput | null; + + @WorkspaceField({ + standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.position, + type: FieldMetadataType.POSITION, + label: 'Position', + description: 'Workflow run position', + icon: 'IconHierarchy2', + }) + @WorkspaceIsSystem() + @WorkspaceIsNullable() + position: number | null; + // Relations @WorkspaceRelation({ standardId: WORKFLOW_RUN_STANDARD_FIELD_IDS.workflowVersion, diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts index 80545fe7897d..5ec2b4ade7da 100644 --- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow-version.workspace-entity.ts @@ -61,8 +61,8 @@ const WorkflowVersionStatusOptions = [ @WorkspaceEntity({ standardId: STANDARD_OBJECT_IDS.workflowVersion, namePlural: 'workflowVersions', - labelSingular: 'WorkflowVersion', - labelPlural: 'WorkflowVersions', + labelSingular: 'Workflow Version', + labelPlural: 'Workflow Versions', description: 'A workflow version', icon: 'IconVersions', labelIdentifierStandardId: WORKFLOW_VERSION_STANDARD_FIELD_IDS.name, @@ -70,7 +70,6 @@ const WorkflowVersionStatusOptions = [ @WorkspaceGate({ featureFlag: FeatureFlagKey.IsWorkflowEnabled, }) -@WorkspaceIsSystem() export class WorkflowVersionWorkspaceEntity extends BaseWorkspaceEntity { @WorkspaceField({ standardId: WORKFLOW_VERSION_STANDARD_FIELD_IDS.name, @@ -86,6 +85,7 @@ export class WorkflowVersionWorkspaceEntity extends BaseWorkspaceEntity { type: FieldMetadataType.RAW_JSON, label: 'Version trigger', description: 'Json object to provide trigger', + icon: 'IconSettingsAutomation', }) @WorkspaceIsNullable() trigger: WorkflowTrigger | null; @@ -95,6 +95,7 @@ export class WorkflowVersionWorkspaceEntity extends BaseWorkspaceEntity { type: FieldMetadataType.RAW_JSON, label: 'Version steps', description: 'Json object to provide steps', + icon: 'IconSettingsAutomation', }) @WorkspaceIsNullable() steps: WorkflowStep[] | null; @@ -104,11 +105,23 @@ export class WorkflowVersionWorkspaceEntity extends BaseWorkspaceEntity { type: FieldMetadataType.SELECT, label: 'Version status', description: 'The workflow version status', + icon: 'IconStatusChange', options: WorkflowVersionStatusOptions, defaultValue: "'DRAFT'", }) status: WorkflowVersionStatus; + @WorkspaceField({ + standardId: WORKFLOW_VERSION_STANDARD_FIELD_IDS.position, + type: FieldMetadataType.POSITION, + label: 'Position', + description: 'Workflow version position', + icon: 'IconHierarchy2', + }) + @WorkspaceIsSystem() + @WorkspaceIsNullable() + position: number | null; + // Relations @WorkspaceRelation({ standardId: WORKFLOW_VERSION_STANDARD_FIELD_IDS.workflow, diff --git a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts index 98dd40417e35..167f449740ff 100644 --- a/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts +++ b/packages/twenty-server/src/modules/workflow/common/standard-objects/workflow.workspace-entity.ts @@ -84,6 +84,7 @@ export class WorkflowWorkspaceEntity extends BaseWorkspaceEntity { type: FieldMetadataType.MULTI_SELECT, label: 'Statuses', description: 'The current statuses of the workflow versions', + icon: 'IconStatusChange', options: WorkflowStatusOptions, }) @WorkspaceIsNullable() diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-step-settings.type.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-step-settings.type.ts index 99b334d0f5f5..bb8f8351faab 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-step-settings.type.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/types/workflow-step-settings.type.ts @@ -14,11 +14,7 @@ export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & { }; export type WorkflowSendEmailStepSettings = BaseWorkflowStepSettings & { + connectedAccountId: string; subject?: string; - template?: string; - title?: string; - callToAction?: { - value: string; - href: string; - }; + body?: string; }; diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.module.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.module.ts index 0f68b2917c89..24ae66fd7f11 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.module.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workflow-executor.module.ts @@ -7,9 +7,14 @@ import { CodeWorkflowAction } from 'src/modules/serverless/workflow-actions/code import { SendEmailWorkflowAction } from 'src/modules/mail-sender/workflow-actions/send-email.workflow-action'; import { ServerlessFunctionModule } from 'src/engine/metadata-modules/serverless-function/serverless-function.module'; import { ScopedWorkspaceContextFactory } from 'src/engine/twenty-orm/factories/scoped-workspace-context.factory'; +import { MessagingGmailDriverModule } from 'src/modules/messaging/message-import-manager/drivers/gmail/messaging-gmail-driver.module'; @Module({ - imports: [WorkflowCommonModule, ServerlessFunctionModule], + imports: [ + WorkflowCommonModule, + ServerlessFunctionModule, + MessagingGmailDriverModule, + ], providers: [ WorkflowExecutorWorkspaceService, ScopedWorkspaceContextFactory, diff --git a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts index 593543047e06..c50684f876c6 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service.ts @@ -1,17 +1,17 @@ import { Injectable } from '@nestjs/common'; -import { WorkflowStep } from 'src/modules/workflow/workflow-executor/types/workflow-action.type'; import { - WorkflowExecutorException, - WorkflowExecutorExceptionCode, -} from 'src/modules/workflow/workflow-executor/exceptions/workflow-executor.exception'; + WorkflowRunOutput, + WorkflowRunStatus, +} from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; import { WorkflowActionFactory } from 'src/modules/workflow/workflow-executor/factories/workflow-action.factory'; +import { WorkflowStep } from 'src/modules/workflow/workflow-executor/types/workflow-action.type'; const MAX_RETRIES_ON_FAILURE = 3; -export type WorkflowExecutionOutput = { - result?: object; - error?: object; +export type WorkflowExecutorOutput = { + steps: WorkflowRunOutput['steps']; + status: WorkflowRunStatus; }; @Injectable() @@ -22,17 +22,17 @@ export class WorkflowExecutorWorkspaceService { currentStepIndex, steps, payload, + output, attemptCount = 1, }: { currentStepIndex: number; steps: WorkflowStep[]; + output: WorkflowExecutorOutput; payload?: object; attemptCount?: number; - }): Promise<WorkflowExecutionOutput> { + }): Promise<WorkflowExecutorOutput> { if (currentStepIndex >= steps.length) { - return { - result: payload, - }; + return { ...output, status: WorkflowRunStatus.COMPLETED }; } const step = steps[currentStepIndex]; @@ -44,19 +44,47 @@ export class WorkflowExecutorWorkspaceService { payload, }); + const baseStepOutput = { + id: step.id, + name: step.name, + type: step.type, + attemptCount, + }; + + const updatedOutput = { + ...output, + steps: [ + ...output.steps, + { + ...baseStepOutput, + result: result.result, + error: result.error?.errorMessage, + }, + ], + }; + if (result.result) { return await this.execute({ currentStepIndex: currentStepIndex + 1, steps, payload: result.result, + output: updatedOutput, }); } if (!result.error) { - throw new WorkflowExecutorException( - 'Execution result error, no data or error', - WorkflowExecutorExceptionCode.WORKFLOW_FAILED, - ); + return { + ...output, + steps: [ + ...output.steps, + { + ...baseStepOutput, + result: undefined, + error: 'Execution result error, no data or error', + }, + ], + status: WorkflowRunStatus.FAILED, + }; } if (step.settings.errorHandlingOptions.continueOnFailure.value) { @@ -64,6 +92,7 @@ export class WorkflowExecutorWorkspaceService { currentStepIndex: currentStepIndex + 1, steps, payload, + output: updatedOutput, }); } @@ -75,13 +104,11 @@ export class WorkflowExecutorWorkspaceService { currentStepIndex, steps, payload, + output: updatedOutput, attemptCount: attemptCount + 1, }); } - throw new WorkflowExecutorException( - `Workflow failed: ${result.error}`, - WorkflowExecutorExceptionCode.WORKFLOW_FAILED, - ); + return { ...updatedOutput, status: WorkflowRunStatus.FAILED }; } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts index 0ca2a3107a6e..5a79462a355c 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/jobs/run-workflow.job.ts @@ -3,8 +3,8 @@ import { Scope } from '@nestjs/common'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; -import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { WorkflowRunStatus } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; +import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { WorkflowExecutorWorkspaceService } from 'src/modules/workflow/workflow-executor/workspace-services/workflow-executor.workspace-service'; import { WorkflowRunWorkspaceService } from 'src/modules/workflow/workflow-runner/workspace-services/workflow-run.workspace-service'; @@ -36,24 +36,23 @@ export class RunWorkflowJob { workflowVersionId, ); - try { + const { steps, status } = await this.workflowExecutorWorkspaceService.execute({ currentStepIndex: 0, steps: workflowVersion.steps || [], payload, + output: { + steps: [], + status: WorkflowRunStatus.RUNNING, + }, }); - await this.workflowRunWorkspaceService.endWorkflowRun( - workflowRunId, - WorkflowRunStatus.COMPLETED, - ); - } catch (error) { - await this.workflowRunWorkspaceService.endWorkflowRun( - workflowRunId, - WorkflowRunStatus.FAILED, - ); - - throw error; - } + await this.workflowRunWorkspaceService.endWorkflowRun( + workflowRunId, + status, + { + steps, + }, + ); } } diff --git a/packages/twenty-server/src/modules/workflow/workflow-runner/workspace-services/workflow-run.workspace-service.ts b/packages/twenty-server/src/modules/workflow/workflow-runner/workspace-services/workflow-run.workspace-service.ts index 00162c595f96..2f3aca25543c 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-runner/workspace-services/workflow-run.workspace-service.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-runner/workspace-services/workflow-run.workspace-service.ts @@ -2,11 +2,12 @@ import { Injectable } from '@nestjs/common'; import { ActorMetadata } from 'src/engine/metadata-modules/field-metadata/composite-types/actor.composite-type'; import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager'; -import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { + WorkflowRunOutput, WorkflowRunStatus, WorkflowRunWorkspaceEntity, } from 'src/modules/workflow/common/standard-objects/workflow-run.workspace-entity'; +import { WorkflowCommonWorkspaceService } from 'src/modules/workflow/common/workspace-services/workflow-common.workspace-service'; import { WorkflowRunException, WorkflowRunExceptionCode, @@ -70,7 +71,11 @@ export class WorkflowRunWorkspaceService { }); } - async endWorkflowRun(workflowRunId: string, status: WorkflowRunStatus) { + async endWorkflowRun( + workflowRunId: string, + status: WorkflowRunStatus, + output: WorkflowRunOutput, + ) { const workflowRunRepository = await this.twentyORMManager.getRepository<WorkflowRunWorkspaceEntity>( 'workflowRun', @@ -96,6 +101,7 @@ export class WorkflowRunWorkspaceService { return workflowRunRepository.update(workflowRunToUpdate.id, { status, + output, endedAt: new Date().toISOString(), }); } diff --git a/packages/twenty-server/src/modules/workflow/workflow-trigger/database-event-trigger/listeners/database-event-trigger.listener.ts b/packages/twenty-server/src/modules/workflow/workflow-trigger/database-event-trigger/listeners/database-event-trigger.listener.ts index e5b62d59afc6..5ea3f82d4781 100644 --- a/packages/twenty-server/src/modules/workflow/workflow-trigger/database-event-trigger/listeners/database-event-trigger.listener.ts +++ b/packages/twenty-server/src/modules/workflow/workflow-trigger/database-event-trigger/listeners/database-event-trigger.listener.ts @@ -1,11 +1,12 @@ import { Injectable, Logger } from '@nestjs/common'; import { OnEvent } from '@nestjs/event-emitter'; -import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; -import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { ObjectRecordCreateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-create.event'; import { ObjectRecordDeleteEvent } from 'src/engine/core-modules/event-emitter/types/object-record-delete.event'; +import { ObjectRecordDestroyEvent } from 'src/engine/core-modules/event-emitter/types/object-record-destroy.event'; import { ObjectRecordUpdateEvent } from 'src/engine/core-modules/event-emitter/types/object-record-update.event'; +import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; +import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; @@ -49,11 +50,19 @@ export class DatabaseEventTriggerListener { await this.handleEvent(payload); } + @OnEvent('*.destroyed') + async handleObjectRecordDestroyEvent( + payload: WorkspaceEventBatch<ObjectRecordDestroyEvent<any>>, + ) { + await this.handleEvent(payload); + } + private async handleEvent( payload: WorkspaceEventBatch< | ObjectRecordCreateEvent<any> | ObjectRecordUpdateEvent<any> | ObjectRecordDeleteEvent<any> + | ObjectRecordDestroyEvent<any> >, ) { const workspaceId = payload.workspaceId; diff --git a/packages/twenty-server/test/people.integration-spec.ts b/packages/twenty-server/test/people.integration-spec.ts index fd568bd5ba40..28b981e22dbe 100644 --- a/packages/twenty-server/test/people.integration-spec.ts +++ b/packages/twenty-server/test/people.integration-spec.ts @@ -27,7 +27,7 @@ describe('peopleResolver (integration)', () => { whatsapp { primaryPhoneNumber } - workPrefereance + workPreference performanceRating } } @@ -69,7 +69,7 @@ describe('peopleResolver (integration)', () => { expect(people).toHaveProperty('companyId'); expect(people).toHaveProperty('intro'); expect(people).toHaveProperty('whatsapp'); - expect(people).toHaveProperty('workPrefereance'); + expect(people).toHaveProperty('workPreference'); expect(people).toHaveProperty('performanceRating'); } }); diff --git a/packages/twenty-server/test/serverless-functions.integration-spec.ts b/packages/twenty-server/test/serverless-functions.integration-spec.ts deleted file mode 100644 index b4b87ff7caed..000000000000 --- a/packages/twenty-server/test/serverless-functions.integration-spec.ts +++ /dev/null @@ -1,61 +0,0 @@ -import request from 'supertest'; - -const client = request(`http://localhost:${APP_PORT}`); - -describe('serverlessFunctionsResolver (integration)', () => { - it('should find many serverlessFunctions', () => { - const queryData = { - query: ` - query serverlessFunctions { - serverlessFunctions { - edges { - node { - id - name - description - sourceCodeHash - 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('sourceCodeHash'); - 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/utils/setup-test.ts b/packages/twenty-server/test/utils/setup-test.ts index aac860fc79ee..bc206be689a6 100644 --- a/packages/twenty-server/test/utils/setup-test.ts +++ b/packages/twenty-server/test/utils/setup-test.ts @@ -1,5 +1,5 @@ -import 'tsconfig-paths/register'; import { JestConfigWithTsJest } from 'ts-jest'; +import 'tsconfig-paths/register'; import { createApp } from './create-app'; diff --git a/packages/twenty-ui/package.json b/packages/twenty-ui/package.json index 132d5b99edaf..ecf63eb4cbdf 100644 --- a/packages/twenty-ui/package.json +++ b/packages/twenty-ui/package.json @@ -1,6 +1,6 @@ { "name": "twenty-ui", - "version": "0.24.2", + "version": "0.31.0", "type": "module", "main": "./src/index.ts", "exports": { diff --git a/packages/twenty-ui/src/display/chip/components/Chip.tsx b/packages/twenty-ui/src/display/chip/components/Chip.tsx index 02d7e2e61309..48795fd42551 100644 --- a/packages/twenty-ui/src/display/chip/components/Chip.tsx +++ b/packages/twenty-ui/src/display/chip/components/Chip.tsx @@ -111,6 +111,10 @@ const StyledContainer = withTheme(styled.div< border-radius: ${({ theme, variant }) => variant === ChipVariant.Rounded ? '50px' : theme.border.radius.sm}; + + & > svg { + flex-shrink: 0; + } `); export const Chip = ({ @@ -123,6 +127,7 @@ export const Chip = ({ rightComponent, accent = ChipAccent.TextPrimary, onClick, + className, }: ChipProps) => { return ( <StyledContainer @@ -133,6 +138,7 @@ export const Chip = ({ size={size} variant={variant} onClick={onClick} + className={className} > {leftComponent} <OverflowingTextWithTooltip diff --git a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts index 2daafb4ec61f..c2e19f37fe70 100644 --- a/packages/twenty-ui/src/display/icon/components/TablerIcons.ts +++ b/packages/twenty-ui/src/display/icon/components/TablerIcons.ts @@ -57,19 +57,56 @@ export { IconCreativeCommonsSa, IconCreditCard, IconCsv, + IconCurrencyAfghani, + IconCurrencyBahraini, IconCurrencyBaht, + IconCurrencyDinar, IconCurrencyDirham, IconCurrencyDollar, + IconCurrencyDollarAustralian, + IconCurrencyDollarBrunei, + IconCurrencyDollarCanadian, + IconCurrencyDollarGuyanese, + IconCurrencyDollarSingapore, + IconCurrencyDong, + IconCurrencyDram, IconCurrencyEuro, + IconCurrencyFlorin, + IconCurrencyForint, IconCurrencyFrank, + IconCurrencyGuarani, + IconCurrencyHryvnia, + IconCurrencyIranianRial, + IconCurrencyKip, IconCurrencyKroneCzech, + IconCurrencyKroneDanish, IconCurrencyKroneSwedish, + IconCurrencyLari, + IconCurrencyLeu, + IconCurrencyLira, + IconCurrencyLyd, + IconCurrencyManat, + IconCurrencyNaira, + IconCurrencyPaanga, + IconCurrencyPeso, IconCurrencyPound, + IconCurrencyQuetzal, IconCurrencyReal, + IconCurrencyRenminbi, IconCurrencyRiyal, + IconCurrencyRubel, + IconCurrencyRufiyaa, + IconCurrencyRupee, + IconCurrencyRupeeNepalese, + IconCurrencyTaka, + IconCurrencyTenge, + IconCurrencyTugrik, + IconCurrencySom, + IconCurrencyShekel, IconCurrencyWon, IconCurrencyYen, IconCurrencyYuan, + IconCurrencyZloty, IconDatabase, IconDeviceFloppy, IconDoorEnter, @@ -139,10 +176,11 @@ export { IconPilcrow, IconPlayerPlay, IconPlayerStop, - IconPower, + IconPlaylistAdd, IconPlaystationSquare, IconPlug, IconPlus, + IconPower, IconPresentation, IconProgressCheck, IconPuzzle, @@ -154,6 +192,7 @@ export { IconReload, IconRepeat, IconRestore, + IconRobot, IconRocket, IconRotate, IconRotate2, @@ -173,6 +212,7 @@ export { IconTestPipe, IconTextSize, IconTimelineEvent, + IconTool, IconTrash, IconUnlink, IconUpload, @@ -183,7 +223,6 @@ export { IconWand, IconWorld, IconX, - IconPlaylistAdd, } from '@tabler/icons-react'; export type { TablerIconsProps } from '@tabler/icons-react'; diff --git a/packages/twenty-ui/src/display/icon/hooks/useIcons.ts b/packages/twenty-ui/src/display/icon/hooks/useIcons.ts index c07d0a38ac82..981fae839cbc 100644 --- a/packages/twenty-ui/src/display/icon/hooks/useIcons.ts +++ b/packages/twenty-ui/src/display/icon/hooks/useIcons.ts @@ -12,7 +12,10 @@ export const useIcons = () => { }; const getIcon = (iconKey?: string | null) => { - if (!iconKey) return defaultIcon; + if (!iconKey) { + return defaultIcon; + } + return icons[iconKey] ?? defaultIcon; }; diff --git a/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx b/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx index c73cd0fda992..7f8df1450282 100644 --- a/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx +++ b/packages/twenty-ui/src/display/tooltip/OverflowingTextWithTooltip.tsx @@ -1,6 +1,6 @@ +import { styled } from '@linaria/react'; import { useRef, useState } from 'react'; import { createPortal } from 'react-dom'; -import { styled } from '@linaria/react'; import { THEME_COMMON } from '@ui/theme'; diff --git a/packages/twenty-website/package.json b/packages/twenty-website/package.json index 3219867baf33..9db997579234 100644 --- a/packages/twenty-website/package.json +++ b/packages/twenty-website/package.json @@ -1,6 +1,6 @@ { "name": "twenty-website", - "version": "0.24.2", + "version": "0.31.0", "private": true, "scripts": { "nx": "NX_DEFAULT_PROJECT=twenty-website node ../../node_modules/nx/bin/nx.js", @@ -14,6 +14,8 @@ "database:generate:pg": "npx drizzle-kit generate:pg --config=src/database/drizzle-posgres.config.ts" }, "dependencies": { + "@docsearch/react": "^3.6.2", + "gray-matter": "^4.0.3", "next-runtime-env": "^3.2.2", "postgres": "^3.4.3" } diff --git a/packages/twenty-website/public/images/readme/Github Read-me banner.png b/packages/twenty-website/public/images/readme/Github Read-me banner.png new file mode 100644 index 000000000000..2b87a59ccc44 Binary files /dev/null and b/packages/twenty-website/public/images/readme/Github Read-me banner.png differ diff --git a/packages/twenty-website/public/images/releases/0.31/0.31-advanced-settings.png b/packages/twenty-website/public/images/releases/0.31/0.31-advanced-settings.png new file mode 100644 index 000000000000..d6f094b5b9cd Binary files /dev/null and b/packages/twenty-website/public/images/releases/0.31/0.31-advanced-settings.png differ diff --git a/packages/twenty-website/public/images/releases/0.31/0.31-search.png b/packages/twenty-website/public/images/releases/0.31/0.31-search.png new file mode 100644 index 000000000000..2d1d6882a9f0 Binary files /dev/null and b/packages/twenty-website/public/images/releases/0.31/0.31-search.png differ diff --git a/packages/twenty-website/src/app/_components/ui/layout/articles/ArticleContent.tsx b/packages/twenty-website/src/app/_components/ui/layout/articles/ArticleContent.tsx index eb87fcc7600b..c337be0bd1c6 100644 --- a/packages/twenty-website/src/app/_components/ui/layout/articles/ArticleContent.tsx +++ b/packages/twenty-website/src/app/_components/ui/layout/articles/ArticleContent.tsx @@ -1,7 +1,7 @@ 'use client'; -import { ReactNode } from 'react'; import styled from '@emotion/styled'; +import { ReactNode } from 'react'; import { Theme } from '@/app/_components/ui/theme/theme'; import { wrapHeadingsWithAnchor } from '@/shared-utils/wrapHeadingsWithAnchor'; @@ -17,6 +17,21 @@ const StyledContent = styled.div` max-width: 100%; line-height: 1.8; color: black; + padding: 4px; + border-radius: 4px; + background: #1414140a; + } + + pre { + background: #1414140a; + padding: 4px; + border-radius: 4px; + + code { + padding: 0; + border-radius: 0; + background: none; + } } p { diff --git a/packages/twenty-website/src/app/layout.css b/packages/twenty-website/src/app/layout.css index 1869c0e7b04b..acff6bd6012c 100644 --- a/packages/twenty-website/src/app/layout.css +++ b/packages/twenty-website/src/app/layout.css @@ -124,9 +124,3 @@ strong, font-weight: 500; text-decoration: none; } - -code { - background: #1414140a; - padding: 4px; - border-radius: 4px; - } \ No newline at end of file diff --git a/packages/twenty-website/src/content/developers/frontend-development/frontend-commands.mdx b/packages/twenty-website/src/content/developers/frontend-development/frontend-commands.mdx index 56dc4e718a37..eb1b02c2dec8 100644 --- a/packages/twenty-website/src/content/developers/frontend-development/frontend-commands.mdx +++ b/packages/twenty-website/src/content/developers/frontend-development/frontend-commands.mdx @@ -68,7 +68,7 @@ To avoid unnecessary [re-renders](/contributor/frontend/best-practices#managing- [Recoil](https://recoiljs.org/docs/introduction/core-concepts) handles state management. -See [best practices](/contributor/frontend/best-practices#state-management) for more information on state management. +See [best practices](/developers/section/frontend-development/best-practices-front#state-management) for more information on state management. ## Testing @@ -78,4 +78,4 @@ Jest is mainly for testing utility functions, and not components themselves. Storybook is for testing the behavior of isolated components, as well as displaying the design system. -<ArticleEditContent></ArticleEditContent> \ No newline at end of file +<ArticleEditContent></ArticleEditContent> diff --git a/packages/twenty-website/src/content/developers/local-setup.mdx b/packages/twenty-website/src/content/developers/local-setup.mdx index 874edf3c2e32..69cf9cb630e7 100644 --- a/packages/twenty-website/src/content/developers/local-setup.mdx +++ b/packages/twenty-website/src/content/developers/local-setup.mdx @@ -122,6 +122,8 @@ You can access the database at [localhost:5432](localhost:5432), with user `twen ``` </ArticleTab> <ArticleTab> + All the following steps are to be run in the WSL terminal (within your virtual machine) + <b>Option 1 :</b> To provision your database locally: ```bash make postgres-on-linux @@ -138,19 +140,35 @@ You can access the database at [localhost:5432](localhost:5432), with user `twen </ArticleTab> </ArticleTabs> +## Step 4: Set up a Redis Database (cache) +Twenty requires a redis cache to provide the best performances -## Step 4: Setup environment variables +<ArticleTabs label1="Linux" label2="Mac OS" label3="Windows (WSL)"> + <ArticleTab> + Use the following link to install Redis on your Linux machine: [Redis Installation](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/) + </ArticleTab> + <ArticleTab> + To provision your database locally with `brew`: + ```bash + brew install redis + ``` + </ArticleTab> + <ArticleTab> + Use the following link to install Redis on your Linux virtual machine: [Redis Installation](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/install-redis-on-linux/) + </ArticleTab> +</ArticleTabs> + +## Step 5: Setup environment variables Use environment variables or `.env` files to configure your project. Copy the `.env.example` files in `/front` and `/server`: ```bash cp ./packages/twenty-front/.env.example ./packages/twenty-front/.env - cp ./packages/twenty-server/.env.example ./packages/twenty-server/.env ``` -## Step 5: Installing dependencies +## Step 6: Installing dependencies <ArticleWarning> @@ -161,13 +179,29 @@ Use `nvm` to install the correct `node` version. The `.nvmrc` ensures all contri To build Twenty server and seed some data into your database, run the following commands: ```bash nvm install # installs recommended node version - nvm use # use recommended node version - yarn ``` -## Step 6: Running the project +## Step 7: Running the project + +Start your redis server: +<ArticleTabs label1="Linux" label2="Mac OS" label3="Windows (WSL)"> + <ArticleTab> + Depending on your Linux distribution, Redis server might be started automatically. + If not, check the [Redis installation guide](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) for your distro. + </ArticleTab> + <ArticleTab> + ```bash + brew services start redis + ``` + </ArticleTab> + <ArticleTab> + Depending on your Linux distribution, Redis server might be started automatically. + If not, check the [Redis installation guide](https://redis.io/docs/latest/operate/oss_and_stack/install/install-redis/) for your distro. + </ArticleTab> +</ArticleTabs> + Setup your database with the following command: ```bash @@ -177,7 +211,6 @@ npx nx database:reset twenty-server Start the server and the frontend: ```bash npx nx start twenty-server - npx nx start twenty-front ``` @@ -186,8 +219,9 @@ Alternatively, you can start both applications at once: npx nx start ``` -Twenty's server will be up and running at [http://localhost:3000/graphql](http://localhost:3000/graphql). -Twenty's frontend will be running at [http://localhost:3001](http://localhost:3001). Just login using the seeded demo account: `tim@apple.dev` (password: `Applecar2025`) to start using Twenty. +Twenty's server will be up and running at [http://localhost:3000](http://localhost:3000). The GraphQL API can be accessed at [http://localhost:3000/graphql](http://localhost:3000/graphql), and the REST API can be reached at [http://localhost:3000/rest](http://localhost:3000/rest). + +Twenty's frontend will be running at [http://localhost:3001](http://localhost:3001). Just log in using the seeded demo account: `tim@apple.dev` (password: `Applecar2025`) to start using Twenty. ## Troubleshooting 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 d5219e511fba..6eb28b045504 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 @@ -41,7 +41,7 @@ yarn command:prod cron:calendar:calendar-event-list-fetch ['FRONT_BASE_URL', 'http://localhost:3001', 'Url to the hosted frontend'], ['SERVER_URL', 'http://localhost:3000', 'Url to the hosted server'], ['PORT', '3000', 'Port'], - ['CACHE_STORAGE_TYPE', 'memory', 'Cache type (memory, redis...)'], + ['CACHE_STORAGE_TYPE', 'redis', 'Cache type (memory, redis...)'], ['CACHE_STORAGE_TTL', '3600 * 24 * 7', 'Cache TTL in seconds'] ]}></ArticleTable> @@ -162,7 +162,7 @@ yarn command:prod cron:calendar:calendar-event-list-fetch ### Message Queue <ArticleTable options={[ - ['MESSAGE_QUEUE_TYPE', 'pg-boss', "Queue driver: 'pg-boss' or 'bull-mq'"], + ['MESSAGE_QUEUE_TYPE', 'bull-mq', "Queue driver: 'pg-boss' or 'bull-mq'"], ]}></ArticleTable> ### Logging 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 d22c3384f0dd..c260b6a04178 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 @@ -22,6 +22,8 @@ Migrating a CRM is a bit trickier than migrating a traditional software, because ## v0.21.0 to v0.22.0 +Upgrade your Twenty instance to use v0.22.0 image + Run the following commands: ``` @@ -36,6 +38,8 @@ The `yarn command:prod upgrade-0.22` command will apply specific data transforma ## v0.22.0 to v0.23.0 +Upgrade your Twenty instance to use v0.23.0 image + Run the following commands: ``` @@ -48,6 +52,8 @@ The `yarn command:prod upgrade-0.23` takes care of the data migration, including ## v0.23.0 to v0.24.0 +Upgrade your Twenty instance to use v0.24.0 image + Run the following commands: ``` @@ -58,4 +64,39 @@ yarn command:prod upgrade-0.24 The `yarn database:migrate:prod` command will apply the migrations to the database structure (core and metadata schemas) The `yarn command:prod upgrade-0.24` takes care of the data migration of all workspaces. +# v0.24.0 to v0.30.0 + +Upgrade your Twenty instance to use v0.30.0 image + +**Breaking change**: +To enhance performances, Twenty now requires redis cache to be configured. We have updated our [docker-compose.yml](https://raw.githubusercontent.com/twentyhq/twenty/main/packages/twenty-docker/docker-compose.yml) to reflect this. +Make sure to update your configuration and to update your environment variables accordingly: +``` +REDIS_HOST={your-redis-host} +REDIS_PORT={your-redis-port} +CACHE_STORAGE_TYPE=redis +``` + +**Schema and data migration**: +``` +yarn database:migrate:prod +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. + +# v0.30.0 to v0.31.0 + +Upgrade your Twenty instance to use v0.31.0 image + +**Schema and data migration**: +``` +yarn database:migrate:prod +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. + <ArticleEditContent></ArticleEditContent> diff --git a/packages/twenty-website/src/content/releases/0.31.0.mdx b/packages/twenty-website/src/content/releases/0.31.0.mdx new file mode 100644 index 000000000000..f8ffb6c5f67f --- /dev/null +++ b/packages/twenty-website/src/content/releases/0.31.0.mdx @@ -0,0 +1,16 @@ +--- +release: 0.31.0 +Date: October 7th 2024 +--- + +# Advanced Settings + +To maintain the simplicity of Twenty, we are introducing "Advanced Settings." This option consolidates all settings intended for advanced use cases, often preferred by developers, such as API and function settings or security settings. + +![](/images/releases/0.31/0.31-advanced-settings.png) + +# More powerful search + +We have significantly enhanced our search performance, making it feel instantaneous when searching for records such as people, companies, or tasks. + +![](/images/releases/0.31/0.31-search.png) \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 293917a2211f..a2066d9d3725 100644 --- a/yarn.lock +++ b/yarn.lock @@ -147,13 +147,6 @@ __metadata: languageName: node linkType: hard -"@algolia/events@npm:^4.0.1": - version: 4.0.1 - resolution: "@algolia/events@npm:4.0.1" - checksum: 10c0/f398d815c6ed21ac08f6caadf1e9155add74ac05d99430191c3b1f1335fd91deaf468c6b304e6225c9885d3d44c06037c53def101e33d9c22daff175b2a65ca9 - languageName: node - linkType: hard - "@algolia/logger-common@npm:4.24.0": version: 4.24.0 resolution: "@algolia/logger-common@npm:4.24.0" @@ -1607,7 +1600,7 @@ __metadata: languageName: node linkType: hard -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.0, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.24.7, @babel/code-frame@npm:^7.8.3": +"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.10.4, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.16.7, @babel/code-frame@npm:^7.24.7": version: 7.24.7 resolution: "@babel/code-frame@npm:7.24.7" dependencies: @@ -1624,7 +1617,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.14.0, @babel/core@npm:^7.14.5, @babel/core@npm:^7.18.9, @babel/core@npm:^7.20.12, @babel/core@npm:^7.21.3, @babel/core@npm:^7.22.5, @babel/core@npm:^7.23.0, @babel/core@npm:^7.23.2, @babel/core@npm:^7.23.3, @babel/core@npm:^7.23.5, @babel/core@npm:^7.23.9, @babel/core@npm:^7.24.5, @babel/core@npm:^7.7.5": +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.13.16, @babel/core@npm:^7.14.0, @babel/core@npm:^7.14.5, @babel/core@npm:^7.18.9, @babel/core@npm:^7.20.12, @babel/core@npm:^7.21.3, @babel/core@npm:^7.22.5, @babel/core@npm:^7.23.0, @babel/core@npm:^7.23.2, @babel/core@npm:^7.23.5, @babel/core@npm:^7.23.9, @babel/core@npm:^7.24.5, @babel/core@npm:^7.7.5": version: 7.25.2 resolution: "@babel/core@npm:7.25.2" dependencies: @@ -1647,7 +1640,7 @@ __metadata: languageName: node linkType: hard -"@babel/generator@npm:^7.14.0, @babel/generator@npm:^7.18.13, @babel/generator@npm:^7.22.5, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.23.3, @babel/generator@npm:^7.23.5, @babel/generator@npm:^7.25.0, @babel/generator@npm:^7.7.2": +"@babel/generator@npm:^7.14.0, @babel/generator@npm:^7.18.13, @babel/generator@npm:^7.22.5, @babel/generator@npm:^7.23.0, @babel/generator@npm:^7.23.5, @babel/generator@npm:^7.25.0, @babel/generator@npm:^7.7.2": version: 7.25.0 resolution: "@babel/generator@npm:7.25.0" dependencies: @@ -2878,7 +2871,7 @@ __metadata: languageName: node linkType: hard -"@babel/plugin-transform-runtime@npm:^7.22.9, @babel/plugin-transform-runtime@npm:^7.23.2": +"@babel/plugin-transform-runtime@npm:^7.23.2": version: 7.24.7 resolution: "@babel/plugin-transform-runtime@npm:7.24.7" dependencies: @@ -3012,7 +3005,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-env@npm:^7.20.2, @babel/preset-env@npm:^7.22.9, @babel/preset-env@npm:^7.23.2": +"@babel/preset-env@npm:^7.20.2, @babel/preset-env@npm:^7.23.2": version: 7.25.3 resolution: "@babel/preset-env@npm:7.25.3" dependencies: @@ -3131,7 +3124,7 @@ __metadata: languageName: node linkType: hard -"@babel/preset-react@npm:^7.14.5, @babel/preset-react@npm:^7.18.6, @babel/preset-react@npm:^7.22.5": +"@babel/preset-react@npm:^7.14.5, @babel/preset-react@npm:^7.18.6": version: 7.24.7 resolution: "@babel/preset-react@npm:7.24.7" dependencies: @@ -3184,7 +3177,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime-corejs3@npm:^7.22.6, @babel/runtime-corejs3@npm:^7.24.4": +"@babel/runtime-corejs3@npm:^7.24.4": version: 7.25.0 resolution: "@babel/runtime-corejs3@npm:7.25.0" dependencies: @@ -3194,7 +3187,7 @@ __metadata: languageName: node linkType: hard -"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.10.3, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.6, @babel/runtime@npm:^7.23.8, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.2, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2": +"@babel/runtime@npm:^7.0.0, @babel/runtime@npm:^7.1.2, @babel/runtime@npm:^7.12.1, @babel/runtime@npm:^7.12.13, @babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.13.10, @babel/runtime@npm:^7.15.4, @babel/runtime@npm:^7.17.8, @babel/runtime@npm:^7.18.3, @babel/runtime@npm:^7.20.13, @babel/runtime@npm:^7.21.0, @babel/runtime@npm:^7.22.6, @babel/runtime@npm:^7.23.8, @babel/runtime@npm:^7.24.1, @babel/runtime@npm:^7.5.5, @babel/runtime@npm:^7.6.2, @babel/runtime@npm:^7.7.2, @babel/runtime@npm:^7.8.4, @babel/runtime@npm:^7.9.2": version: 7.25.0 resolution: "@babel/runtime@npm:7.25.0" dependencies: @@ -3214,7 +3207,7 @@ __metadata: languageName: node linkType: hard -"@babel/traverse@npm:^7.14.0, @babel/traverse@npm:^7.16.0, @babel/traverse@npm:^7.16.8, @babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.22.8, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.23.5, @babel/traverse@npm:^7.24.7, @babel/traverse@npm:^7.24.8, @babel/traverse@npm:^7.25.0, @babel/traverse@npm:^7.25.1, @babel/traverse@npm:^7.25.2, @babel/traverse@npm:^7.25.3": +"@babel/traverse@npm:^7.14.0, @babel/traverse@npm:^7.16.0, @babel/traverse@npm:^7.16.8, @babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.23.2, @babel/traverse@npm:^7.23.5, @babel/traverse@npm:^7.24.7, @babel/traverse@npm:^7.24.8, @babel/traverse@npm:^7.25.0, @babel/traverse@npm:^7.25.1, @babel/traverse@npm:^7.25.2, @babel/traverse@npm:^7.25.3": version: 7.25.3 resolution: "@babel/traverse@npm:7.25.3" dependencies: @@ -3600,27 +3593,27 @@ __metadata: languageName: node linkType: hard -"@discoveryjs/json-ext@npm:0.5.7, @discoveryjs/json-ext@npm:^0.5.3": +"@discoveryjs/json-ext@npm:^0.5.3": version: 0.5.7 resolution: "@discoveryjs/json-ext@npm:0.5.7" checksum: 10c0/e10f1b02b78e4812646ddf289b7d9f2cb567d336c363b266bd50cd223cf3de7c2c74018d91cd2613041568397ef3a4a2b500aba588c6e5bd78c38374ba68f38c languageName: node linkType: hard -"@docsearch/css@npm:3.6.1": - version: 3.6.1 - resolution: "@docsearch/css@npm:3.6.1" - checksum: 10c0/546b7b725044d006fe5fd2061763fbd1f944d9db21c7b86adb2d11e7bd5eee41b102f1ecccb001bb1603ef7503282cc9ad204482db62e4bc0b038c46a9cd9e6d +"@docsearch/css@npm:3.6.2": + version: 3.6.2 + resolution: "@docsearch/css@npm:3.6.2" + checksum: 10c0/f9f8af55814a8a8dfbac78972cff2c264d4e5508de61d893dbc07544c8e1dcb044803ba150c56f4d245f8f5f88d84fa7f6226038b813850bd602f4bf48123793 languageName: node linkType: hard -"@docsearch/react@npm:^3.5.2": - version: 3.6.1 - resolution: "@docsearch/react@npm:3.6.1" +"@docsearch/react@npm:^3.6.2": + version: 3.6.2 + resolution: "@docsearch/react@npm:3.6.2" dependencies: "@algolia/autocomplete-core": "npm:1.9.3" "@algolia/autocomplete-preset-algolia": "npm:1.9.3" - "@docsearch/css": "npm:3.6.1" + "@docsearch/css": "npm:3.6.2" algoliasearch: "npm:^4.19.1" peerDependencies: "@types/react": ">= 16.8.0 < 19.0.0" @@ -3636,533 +3629,7 @@ __metadata: optional: true search-insights: optional: true - checksum: 10c0/890d46ed1f971a6af9f64377c9e510e4b39324bfedcc143c7bd35ba883f8fdac3dc844b0a0000059fd3dec16a0443e7f723d65c468ca7bafd03be546caf38479 - languageName: node - linkType: hard - -"@docusaurus/core@npm:3.4.0, @docusaurus/core@npm:^3.1.0": - version: 3.4.0 - resolution: "@docusaurus/core@npm:3.4.0" - dependencies: - "@babel/core": "npm:^7.23.3" - "@babel/generator": "npm:^7.23.3" - "@babel/plugin-syntax-dynamic-import": "npm:^7.8.3" - "@babel/plugin-transform-runtime": "npm:^7.22.9" - "@babel/preset-env": "npm:^7.22.9" - "@babel/preset-react": "npm:^7.22.5" - "@babel/preset-typescript": "npm:^7.22.5" - "@babel/runtime": "npm:^7.22.6" - "@babel/runtime-corejs3": "npm:^7.22.6" - "@babel/traverse": "npm:^7.22.8" - "@docusaurus/cssnano-preset": "npm:3.4.0" - "@docusaurus/logger": "npm:3.4.0" - "@docusaurus/mdx-loader": "npm:3.4.0" - "@docusaurus/utils": "npm:3.4.0" - "@docusaurus/utils-common": "npm:3.4.0" - "@docusaurus/utils-validation": "npm:3.4.0" - autoprefixer: "npm:^10.4.14" - babel-loader: "npm:^9.1.3" - babel-plugin-dynamic-import-node: "npm:^2.3.3" - boxen: "npm:^6.2.1" - chalk: "npm:^4.1.2" - chokidar: "npm:^3.5.3" - clean-css: "npm:^5.3.2" - cli-table3: "npm:^0.6.3" - combine-promises: "npm:^1.1.0" - commander: "npm:^5.1.0" - copy-webpack-plugin: "npm:^11.0.0" - core-js: "npm:^3.31.1" - css-loader: "npm:^6.8.1" - css-minimizer-webpack-plugin: "npm:^5.0.1" - cssnano: "npm:^6.1.2" - del: "npm:^6.1.1" - detect-port: "npm:^1.5.1" - escape-html: "npm:^1.0.3" - eta: "npm:^2.2.0" - eval: "npm:^0.1.8" - file-loader: "npm:^6.2.0" - fs-extra: "npm:^11.1.1" - html-minifier-terser: "npm:^7.2.0" - html-tags: "npm:^3.3.1" - html-webpack-plugin: "npm:^5.5.3" - leven: "npm:^3.1.0" - lodash: "npm:^4.17.21" - mini-css-extract-plugin: "npm:^2.7.6" - p-map: "npm:^4.0.0" - postcss: "npm:^8.4.26" - postcss-loader: "npm:^7.3.3" - prompts: "npm:^2.4.2" - react-dev-utils: "npm:^12.0.1" - react-helmet-async: "npm:^1.3.0" - react-loadable: "npm:@docusaurus/react-loadable@6.0.0" - react-loadable-ssr-addon-v5-slorber: "npm:^1.0.1" - react-router: "npm:^5.3.4" - react-router-config: "npm:^5.1.1" - react-router-dom: "npm:^5.3.4" - rtl-detect: "npm:^1.0.4" - semver: "npm:^7.5.4" - serve-handler: "npm:^6.1.5" - shelljs: "npm:^0.8.5" - terser-webpack-plugin: "npm:^5.3.9" - tslib: "npm:^2.6.0" - update-notifier: "npm:^6.0.2" - url-loader: "npm:^4.1.1" - webpack: "npm:^5.88.1" - webpack-bundle-analyzer: "npm:^4.9.0" - webpack-dev-server: "npm:^4.15.1" - webpack-merge: "npm:^5.9.0" - webpackbar: "npm:^5.0.2" - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - bin: - docusaurus: bin/docusaurus.mjs - checksum: 10c0/28a9d2c4c893930e7fa7bbf5df2aed79a5cdc618c9f40d5b867846cb78ee371a52af41a59c6adf677cd480cb4350dfad4866de4b06928b4169c295c601472867 - languageName: node - linkType: hard - -"@docusaurus/cssnano-preset@npm:3.4.0": - version: 3.4.0 - resolution: "@docusaurus/cssnano-preset@npm:3.4.0" - dependencies: - cssnano-preset-advanced: "npm:^6.1.2" - postcss: "npm:^8.4.38" - postcss-sort-media-queries: "npm:^5.2.0" - tslib: "npm:^2.6.0" - checksum: 10c0/bcbdfb9d34d8b26bf89c8e414f5fc775bae5c12a0c280064a8aaf30c7260ffb760dee5ce86171f87cf4dcdeddb39a815ebfc6bdfd5ec14fb26c5cb340c51af55 - languageName: node - linkType: hard - -"@docusaurus/logger@npm:3.4.0": - version: 3.4.0 - resolution: "@docusaurus/logger@npm:3.4.0" - dependencies: - chalk: "npm:^4.1.2" - tslib: "npm:^2.6.0" - checksum: 10c0/0759eee9bc01cf0c16da70ccd0cd3363e649f00bb6d04595bf436a4d40235b6f78d6d18f8a5499244693f067a708e3fb3126c122c01b1c0fa3665198d24a80a2 - languageName: node - linkType: hard - -"@docusaurus/mdx-loader@npm:3.4.0": - version: 3.4.0 - resolution: "@docusaurus/mdx-loader@npm:3.4.0" - dependencies: - "@docusaurus/logger": "npm:3.4.0" - "@docusaurus/utils": "npm:3.4.0" - "@docusaurus/utils-validation": "npm:3.4.0" - "@mdx-js/mdx": "npm:^3.0.0" - "@slorber/remark-comment": "npm:^1.0.0" - escape-html: "npm:^1.0.3" - estree-util-value-to-estree: "npm:^3.0.1" - file-loader: "npm:^6.2.0" - fs-extra: "npm:^11.1.1" - image-size: "npm:^1.0.2" - mdast-util-mdx: "npm:^3.0.0" - mdast-util-to-string: "npm:^4.0.0" - rehype-raw: "npm:^7.0.0" - remark-directive: "npm:^3.0.0" - remark-emoji: "npm:^4.0.0" - remark-frontmatter: "npm:^5.0.0" - remark-gfm: "npm:^4.0.0" - stringify-object: "npm:^3.3.0" - tslib: "npm:^2.6.0" - unified: "npm:^11.0.3" - unist-util-visit: "npm:^5.0.0" - url-loader: "npm:^4.1.1" - vfile: "npm:^6.0.1" - webpack: "npm:^5.88.1" - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - checksum: 10c0/3a3295e01571ceefdc74abbfdc5125b6acd2c7bfe13cac0c6c79f61c9fc16e1244594748c92ceb01baae648d4aedd594fa1b513aca66f7a74244d347c91820b0 - languageName: node - linkType: hard - -"@docusaurus/module-type-aliases@npm:3.4.0, @docusaurus/module-type-aliases@npm:^3.1.0": - version: 3.4.0 - resolution: "@docusaurus/module-type-aliases@npm:3.4.0" - dependencies: - "@docusaurus/types": "npm:3.4.0" - "@types/history": "npm:^4.7.11" - "@types/react": "npm:*" - "@types/react-router-config": "npm:*" - "@types/react-router-dom": "npm:*" - react-helmet-async: "npm:*" - react-loadable: "npm:@docusaurus/react-loadable@6.0.0" - peerDependencies: - react: "*" - react-dom: "*" - checksum: 10c0/37645717442eaf2d62dcb972db544f5231392f1dbeb7499d725cef50b4c2762d7a95facff8a759f9127814861c6ccb859f69661f1634b7bf8c27be13f9d3e626 - languageName: node - linkType: hard - -"@docusaurus/plugin-content-blog@npm:3.4.0": - version: 3.4.0 - resolution: "@docusaurus/plugin-content-blog@npm:3.4.0" - dependencies: - "@docusaurus/core": "npm:3.4.0" - "@docusaurus/logger": "npm:3.4.0" - "@docusaurus/mdx-loader": "npm:3.4.0" - "@docusaurus/types": "npm:3.4.0" - "@docusaurus/utils": "npm:3.4.0" - "@docusaurus/utils-common": "npm:3.4.0" - "@docusaurus/utils-validation": "npm:3.4.0" - cheerio: "npm:^1.0.0-rc.12" - feed: "npm:^4.2.2" - fs-extra: "npm:^11.1.1" - lodash: "npm:^4.17.21" - reading-time: "npm:^1.5.0" - srcset: "npm:^4.0.0" - tslib: "npm:^2.6.0" - unist-util-visit: "npm:^5.0.0" - utility-types: "npm:^3.10.0" - webpack: "npm:^5.88.1" - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - checksum: 10c0/016804ee40bd8c9e2a097eb76e618a00cbedd48967df8da2670961086dc61ce44d7a3bc959050c82b469b2efa07b0f3faedb11caf7a9983c181bab30b9f2054e - languageName: node - linkType: hard - -"@docusaurus/plugin-content-docs@npm:3.4.0": - version: 3.4.0 - resolution: "@docusaurus/plugin-content-docs@npm:3.4.0" - dependencies: - "@docusaurus/core": "npm:3.4.0" - "@docusaurus/logger": "npm:3.4.0" - "@docusaurus/mdx-loader": "npm:3.4.0" - "@docusaurus/module-type-aliases": "npm:3.4.0" - "@docusaurus/types": "npm:3.4.0" - "@docusaurus/utils": "npm:3.4.0" - "@docusaurus/utils-common": "npm:3.4.0" - "@docusaurus/utils-validation": "npm:3.4.0" - "@types/react-router-config": "npm:^5.0.7" - combine-promises: "npm:^1.1.0" - fs-extra: "npm:^11.1.1" - js-yaml: "npm:^4.1.0" - lodash: "npm:^4.17.21" - tslib: "npm:^2.6.0" - utility-types: "npm:^3.10.0" - webpack: "npm:^5.88.1" - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - checksum: 10c0/dc12d09c7ecd9f18bf48ee16432f01a974880f251a6c68b0be6cc6876edd2f25561cf4b0829a34bc9daa9ec5d44c8797a0b096dc7480346fb502482734ea728c - languageName: node - linkType: hard - -"@docusaurus/plugin-content-pages@npm:3.4.0": - version: 3.4.0 - resolution: "@docusaurus/plugin-content-pages@npm:3.4.0" - dependencies: - "@docusaurus/core": "npm:3.4.0" - "@docusaurus/mdx-loader": "npm:3.4.0" - "@docusaurus/types": "npm:3.4.0" - "@docusaurus/utils": "npm:3.4.0" - "@docusaurus/utils-validation": "npm:3.4.0" - fs-extra: "npm:^11.1.1" - tslib: "npm:^2.6.0" - webpack: "npm:^5.88.1" - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - checksum: 10c0/2d741a710ebb7b5687f4635d1f67ef1297e3db400093c84152688aeb95d9a6728b56be8080f0fcee095800e55e3fa72cf358782ef76cf61230c171443f21f480 - languageName: node - linkType: hard - -"@docusaurus/plugin-debug@npm:3.4.0": - version: 3.4.0 - resolution: "@docusaurus/plugin-debug@npm:3.4.0" - dependencies: - "@docusaurus/core": "npm:3.4.0" - "@docusaurus/types": "npm:3.4.0" - "@docusaurus/utils": "npm:3.4.0" - fs-extra: "npm:^11.1.1" - react-json-view-lite: "npm:^1.2.0" - tslib: "npm:^2.6.0" - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - checksum: 10c0/2cda3492e3da55d81a26dc2e03c1bb96ab4243985a0b9b1c47589ed320a5917e535fd0b88669cad2c42fa50e41344ac8323dd5fa408b7e07c5ebd0aa5cee4117 - languageName: node - linkType: hard - -"@docusaurus/plugin-google-analytics@npm:3.4.0": - version: 3.4.0 - resolution: "@docusaurus/plugin-google-analytics@npm:3.4.0" - dependencies: - "@docusaurus/core": "npm:3.4.0" - "@docusaurus/types": "npm:3.4.0" - "@docusaurus/utils-validation": "npm:3.4.0" - tslib: "npm:^2.6.0" - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - checksum: 10c0/6b2c1785c202b527ec68777d1cf5fbe5211a02793e86f6821e4b403a1f0e4359b2a4be28e837be641ac7e436eb26f0e961854124c74b8ad8d9bba01f302ac908 - languageName: node - linkType: hard - -"@docusaurus/plugin-google-gtag@npm:3.4.0": - version: 3.4.0 - resolution: "@docusaurus/plugin-google-gtag@npm:3.4.0" - dependencies: - "@docusaurus/core": "npm:3.4.0" - "@docusaurus/types": "npm:3.4.0" - "@docusaurus/utils-validation": "npm:3.4.0" - "@types/gtag.js": "npm:^0.0.12" - tslib: "npm:^2.6.0" - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - checksum: 10c0/1ab2bad4dd47c3aab8393b241efbdad7c5f8b2248970cf9f5e04537de279ffe0d92bf428f8690abf8b522142433e8ea7eb61fbf98a70d606b4e45b95663cb563 - languageName: node - linkType: hard - -"@docusaurus/plugin-google-tag-manager@npm:3.4.0": - version: 3.4.0 - resolution: "@docusaurus/plugin-google-tag-manager@npm:3.4.0" - dependencies: - "@docusaurus/core": "npm:3.4.0" - "@docusaurus/types": "npm:3.4.0" - "@docusaurus/utils-validation": "npm:3.4.0" - tslib: "npm:^2.6.0" - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - checksum: 10c0/91242ee1d7f7f755de512501cd297d6c1e4546b706310e78cfac83e5e96ce45f37434aca1052567365ba904559f40bd7a713e6271535d3ade8781db012d5d730 - languageName: node - linkType: hard - -"@docusaurus/plugin-sitemap@npm:3.4.0": - version: 3.4.0 - resolution: "@docusaurus/plugin-sitemap@npm:3.4.0" - dependencies: - "@docusaurus/core": "npm:3.4.0" - "@docusaurus/logger": "npm:3.4.0" - "@docusaurus/types": "npm:3.4.0" - "@docusaurus/utils": "npm:3.4.0" - "@docusaurus/utils-common": "npm:3.4.0" - "@docusaurus/utils-validation": "npm:3.4.0" - fs-extra: "npm:^11.1.1" - sitemap: "npm:^7.1.1" - tslib: "npm:^2.6.0" - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - checksum: 10c0/498754e04a29d1715d8c4c91a1c30526294de5714f14dff31563339ad0966dc7e2ffca5cd5ffd059268be364fb1eb4f6ccc3f397f69f7537c9f655a29f56bd9f - languageName: node - linkType: hard - -"@docusaurus/preset-classic@npm:^3.1.0": - version: 3.4.0 - resolution: "@docusaurus/preset-classic@npm:3.4.0" - dependencies: - "@docusaurus/core": "npm:3.4.0" - "@docusaurus/plugin-content-blog": "npm:3.4.0" - "@docusaurus/plugin-content-docs": "npm:3.4.0" - "@docusaurus/plugin-content-pages": "npm:3.4.0" - "@docusaurus/plugin-debug": "npm:3.4.0" - "@docusaurus/plugin-google-analytics": "npm:3.4.0" - "@docusaurus/plugin-google-gtag": "npm:3.4.0" - "@docusaurus/plugin-google-tag-manager": "npm:3.4.0" - "@docusaurus/plugin-sitemap": "npm:3.4.0" - "@docusaurus/theme-classic": "npm:3.4.0" - "@docusaurus/theme-common": "npm:3.4.0" - "@docusaurus/theme-search-algolia": "npm:3.4.0" - "@docusaurus/types": "npm:3.4.0" - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - checksum: 10c0/faa95f52b459b91903e35e0a90ccc6fea040657eacd64c6cdd9ac93f8d981b17adbf978ab97b1e4bdb7621f10febb2633c94c598f45feb5106aeed62f6485a91 - languageName: node - linkType: hard - -"@docusaurus/theme-classic@npm:3.4.0": - version: 3.4.0 - resolution: "@docusaurus/theme-classic@npm:3.4.0" - dependencies: - "@docusaurus/core": "npm:3.4.0" - "@docusaurus/mdx-loader": "npm:3.4.0" - "@docusaurus/module-type-aliases": "npm:3.4.0" - "@docusaurus/plugin-content-blog": "npm:3.4.0" - "@docusaurus/plugin-content-docs": "npm:3.4.0" - "@docusaurus/plugin-content-pages": "npm:3.4.0" - "@docusaurus/theme-common": "npm:3.4.0" - "@docusaurus/theme-translations": "npm:3.4.0" - "@docusaurus/types": "npm:3.4.0" - "@docusaurus/utils": "npm:3.4.0" - "@docusaurus/utils-common": "npm:3.4.0" - "@docusaurus/utils-validation": "npm:3.4.0" - "@mdx-js/react": "npm:^3.0.0" - clsx: "npm:^2.0.0" - copy-text-to-clipboard: "npm:^3.2.0" - infima: "npm:0.2.0-alpha.43" - lodash: "npm:^4.17.21" - nprogress: "npm:^0.2.0" - postcss: "npm:^8.4.26" - prism-react-renderer: "npm:^2.3.0" - prismjs: "npm:^1.29.0" - react-router-dom: "npm:^5.3.4" - rtlcss: "npm:^4.1.0" - tslib: "npm:^2.6.0" - utility-types: "npm:^3.10.0" - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - checksum: 10c0/9b8cfd8c6350f3bfe7d23480b860e3f18ec1be2c0cc563e6b5bf8ba9e4b7be75d01d1601362f8615a33635cd2bf76deb48628e367372899dc87f9eadab8e7b0f - languageName: node - linkType: hard - -"@docusaurus/theme-common@npm:3.4.0": - version: 3.4.0 - resolution: "@docusaurus/theme-common@npm:3.4.0" - dependencies: - "@docusaurus/mdx-loader": "npm:3.4.0" - "@docusaurus/module-type-aliases": "npm:3.4.0" - "@docusaurus/plugin-content-blog": "npm:3.4.0" - "@docusaurus/plugin-content-docs": "npm:3.4.0" - "@docusaurus/plugin-content-pages": "npm:3.4.0" - "@docusaurus/utils": "npm:3.4.0" - "@docusaurus/utils-common": "npm:3.4.0" - "@types/history": "npm:^4.7.11" - "@types/react": "npm:*" - "@types/react-router-config": "npm:*" - clsx: "npm:^2.0.0" - parse-numeric-range: "npm:^1.3.0" - prism-react-renderer: "npm:^2.3.0" - tslib: "npm:^2.6.0" - utility-types: "npm:^3.10.0" - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - checksum: 10c0/a52ca6849674e216f1584e3d9ba13fafa70deb12192534c213473fb702ba656c686c09de8400f2041e7224c79097832149d3a3d0d434ec8d346bb67f8ef73847 - languageName: node - linkType: hard - -"@docusaurus/theme-search-algolia@npm:3.4.0": - version: 3.4.0 - resolution: "@docusaurus/theme-search-algolia@npm:3.4.0" - dependencies: - "@docsearch/react": "npm:^3.5.2" - "@docusaurus/core": "npm:3.4.0" - "@docusaurus/logger": "npm:3.4.0" - "@docusaurus/plugin-content-docs": "npm:3.4.0" - "@docusaurus/theme-common": "npm:3.4.0" - "@docusaurus/theme-translations": "npm:3.4.0" - "@docusaurus/utils": "npm:3.4.0" - "@docusaurus/utils-validation": "npm:3.4.0" - algoliasearch: "npm:^4.18.0" - algoliasearch-helper: "npm:^3.13.3" - clsx: "npm:^2.0.0" - eta: "npm:^2.2.0" - fs-extra: "npm:^11.1.1" - lodash: "npm:^4.17.21" - tslib: "npm:^2.6.0" - utility-types: "npm:^3.10.0" - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - checksum: 10c0/cdc04522ecd64dad67ca2a276bc1938df40a72a68819db14e36fe770a393fcf2448491534e657ac684c8dfcbb8af8f45a202b5e5464cb26f5098927a2526228a - languageName: node - linkType: hard - -"@docusaurus/theme-translations@npm:3.4.0": - version: 3.4.0 - resolution: "@docusaurus/theme-translations@npm:3.4.0" - dependencies: - fs-extra: "npm:^11.1.1" - tslib: "npm:^2.6.0" - checksum: 10c0/e32ce684d2c9269534ab6f1a71086ae2520e68cd5c42ecb0222da7d7b8a60cd96dc23dbcd0f0856b5439b71efd6552f321c66f17d218967c6b02b46589181e2a - languageName: node - linkType: hard - -"@docusaurus/tsconfig@npm:3.1.0": - version: 3.1.0 - resolution: "@docusaurus/tsconfig@npm:3.1.0" - checksum: 10c0/bb2bfdc16aaa37a92a13ecb00d3e041e578c906618c563955b174e7a4ef602863e8d13debf0e2b60db6de118ad684275be5e73c036e695a3027fab0d16800f85 - languageName: node - linkType: hard - -"@docusaurus/types@npm:3.4.0": - version: 3.4.0 - resolution: "@docusaurus/types@npm:3.4.0" - dependencies: - "@mdx-js/mdx": "npm:^3.0.0" - "@types/history": "npm:^4.7.11" - "@types/react": "npm:*" - commander: "npm:^5.1.0" - joi: "npm:^17.9.2" - react-helmet-async: "npm:^1.3.0" - utility-types: "npm:^3.10.0" - webpack: "npm:^5.88.1" - webpack-merge: "npm:^5.9.0" - peerDependencies: - react: ^18.0.0 - react-dom: ^18.0.0 - checksum: 10c0/c86b95dfbf02db6faa9bb4d6c552d54f2e57924a95937cff6f1884e0ef66f7bbaf84e645fffa229f2571fea6ee469d3dd15abff20f81f7dc886ad38c4c79cbdb - languageName: node - linkType: hard - -"@docusaurus/utils-common@npm:3.4.0": - version: 3.4.0 - resolution: "@docusaurus/utils-common@npm:3.4.0" - dependencies: - tslib: "npm:^2.6.0" - peerDependencies: - "@docusaurus/types": "*" - peerDependenciesMeta: - "@docusaurus/types": - optional: true - checksum: 10c0/df8e27a88621f5984624ac8c15061dd3eb2f33d3c3f5e08381d3aa316347a62884b2b5f051f54050516ceea1d4656012893be09ca81eae56809f4eb723924f56 - languageName: node - linkType: hard - -"@docusaurus/utils-validation@npm:3.4.0": - version: 3.4.0 - resolution: "@docusaurus/utils-validation@npm:3.4.0" - dependencies: - "@docusaurus/logger": "npm:3.4.0" - "@docusaurus/utils": "npm:3.4.0" - "@docusaurus/utils-common": "npm:3.4.0" - fs-extra: "npm:^11.2.0" - joi: "npm:^17.9.2" - js-yaml: "npm:^4.1.0" - lodash: "npm:^4.17.21" - tslib: "npm:^2.6.0" - checksum: 10c0/5a4c13bd41f1c5132b33c09f29f788fb76c3a9b0c4326e8bb2661041ab8c9cabd682f5d3f6203fae49e28bc975217b99e485dcc23065afb16498978774b37ee6 - languageName: node - linkType: hard - -"@docusaurus/utils@npm:3.4.0": - version: 3.4.0 - resolution: "@docusaurus/utils@npm:3.4.0" - dependencies: - "@docusaurus/logger": "npm:3.4.0" - "@docusaurus/utils-common": "npm:3.4.0" - "@svgr/webpack": "npm:^8.1.0" - escape-string-regexp: "npm:^4.0.0" - file-loader: "npm:^6.2.0" - fs-extra: "npm:^11.1.1" - github-slugger: "npm:^1.5.0" - globby: "npm:^11.1.0" - gray-matter: "npm:^4.0.3" - jiti: "npm:^1.20.0" - js-yaml: "npm:^4.1.0" - lodash: "npm:^4.17.21" - micromatch: "npm:^4.0.5" - prompts: "npm:^2.4.2" - resolve-pathname: "npm:^3.0.0" - shelljs: "npm:^0.8.5" - tslib: "npm:^2.6.0" - url-loader: "npm:^4.1.1" - utility-types: "npm:^3.10.0" - webpack: "npm:^5.88.1" - peerDependencies: - "@docusaurus/types": "*" - peerDependenciesMeta: - "@docusaurus/types": - optional: true - checksum: 10c0/b80444985e97c1c9586a2b2669438b02e3a122d382b38633f0a7317365b5d3ad05beb882328ada4b09bb3de7e18d29f89d2a4e02fadf4acebdc5dd768b2265b9 + checksum: 10c0/8fcf47de8786d097005912347fe566577361193026d58b610d5540ef26fd3bf1b30bfe986e23357fd1ee5b97f0a5deb102de3bda79c069536e49a9f3d4b0fc76 languageName: node linkType: hard @@ -6976,13 +6443,6 @@ __metadata: languageName: node linkType: hard -"@leichtgewicht/ip-codec@npm:^2.0.1": - version: 2.0.5 - resolution: "@leichtgewicht/ip-codec@npm:2.0.5" - checksum: 10c0/14a0112bd59615eef9e3446fea018045720cd3da85a98f801a685a818b0d96ef2a1f7227e8d271def546b2e2a0fe91ef915ba9dc912ab7967d2317b1a051d66b - languageName: node - linkType: hard - "@lezer/common@npm:^1.0.0, @lezer/common@npm:^1.0.2, @lezer/common@npm:^1.1.0, @lezer/common@npm:^1.2.0": version: 1.2.1 resolution: "@lezer/common@npm:1.2.1" @@ -7164,37 +6624,6 @@ __metadata: languageName: node linkType: hard -"@mdx-js/mdx@npm:^3.0.0": - version: 3.0.1 - resolution: "@mdx-js/mdx@npm:3.0.1" - dependencies: - "@types/estree": "npm:^1.0.0" - "@types/estree-jsx": "npm:^1.0.0" - "@types/hast": "npm:^3.0.0" - "@types/mdx": "npm:^2.0.0" - collapse-white-space: "npm:^2.0.0" - devlop: "npm:^1.0.0" - estree-util-build-jsx: "npm:^3.0.0" - estree-util-is-identifier-name: "npm:^3.0.0" - estree-util-to-js: "npm:^2.0.0" - estree-walker: "npm:^3.0.0" - hast-util-to-estree: "npm:^3.0.0" - hast-util-to-jsx-runtime: "npm:^2.0.0" - markdown-extensions: "npm:^2.0.0" - periscopic: "npm:^3.0.0" - remark-mdx: "npm:^3.0.0" - remark-parse: "npm:^11.0.0" - remark-rehype: "npm:^11.0.0" - source-map: "npm:^0.7.0" - unified: "npm:^11.0.0" - unist-util-position-from-estree: "npm:^2.0.0" - unist-util-stringify-position: "npm:^4.0.0" - unist-util-visit: "npm:^5.0.0" - vfile: "npm:^6.0.0" - checksum: 10c0/8cd7084f1242209bbeef81f69ea670ffffa0656dda2893bbd46b1b2b26078a57f9d993f8f82ad8ba16bc969189235140007185276d7673471827331521eae2e0 - languageName: node - linkType: hard - "@mdx-js/react@npm:^2.1.5, @mdx-js/react@npm:^2.2.1": version: 2.3.0 resolution: "@mdx-js/react@npm:2.3.0" @@ -8106,106 +7535,173 @@ __metadata: languageName: node linkType: hard -"@nivo/calendar@npm:^0.84.0": - version: 0.84.0 - resolution: "@nivo/calendar@npm:0.84.0" +"@nivo/annotations@npm:0.87.0": + version: 0.87.0 + resolution: "@nivo/annotations@npm:0.87.0" dependencies: - "@nivo/core": "npm:0.84.0" - "@nivo/legends": "npm:0.84.0" - "@nivo/tooltip": "npm:0.84.0" - "@types/d3-scale": "npm:^3.2.3" + "@nivo/colors": "npm:0.87.0" + "@nivo/core": "npm:0.87.0" + "@react-spring/web": "npm:9.4.5 || ^9.7.2" + lodash: "npm:^4.17.21" + peerDependencies: + react: ">= 16.14.0 < 19.0.0" + checksum: 10c0/603599c5e697b987ccd360fc835fd4152b43e9facf5542369669faad9352e56a093c6b9dff8c3b4114d25267144478c89ad834fe4929b0ec223c7600795b276b + languageName: node + linkType: hard + +"@nivo/axes@npm:0.87.0": + version: 0.87.0 + resolution: "@nivo/axes@npm:0.87.0" + dependencies: + "@nivo/core": "npm:0.87.0" + "@nivo/scales": "npm:0.87.0" + "@react-spring/web": "npm:9.4.5 || ^9.7.2" + "@types/d3-format": "npm:^1.4.1" + "@types/d3-time-format": "npm:^2.3.1" + d3-format: "npm:^1.4.4" + d3-time-format: "npm:^3.0.0" + peerDependencies: + react: ">= 16.14.0 < 19.0.0" + checksum: 10c0/7af5f6ea3101ab957b171713276ff2acb1dee608fe96876a3afac8ede5327856ddfcd3046d084fbe417ef032080caa87736187d3b77d2f6f79f65c7d70fa5bb2 + languageName: node + linkType: hard + +"@nivo/calendar@npm:^0.87.0": + version: 0.87.0 + resolution: "@nivo/calendar@npm:0.87.0" + dependencies: + "@nivo/core": "npm:0.87.0" + "@nivo/legends": "npm:0.87.0" + "@nivo/tooltip": "npm:0.87.0" + "@types/d3-scale": "npm:^4.0.8" "@types/d3-time": "npm:^1.0.10" "@types/d3-time-format": "npm:^3.0.0" - d3-scale: "npm:^3.2.3" + d3-scale: "npm:^4.0.2" d3-time: "npm:^1.0.10" d3-time-format: "npm:^3.0.0" lodash: "npm:^4.17.21" peerDependencies: react: ">= 16.14.0 < 19.0.0" - checksum: 10c0/7c83753a6f6b830df6823eaefcaf22883aca0b2ba630085075a39ef9cdd35313ca153416d88037ebf4cc31a356bb356f26e581dda770c38d7402c97dad182572 + checksum: 10c0/5cb0f4ceb45109695f1b86e4215ef464b7c33449c50d45a436f71a30029bcfca6b91956d6be9862d17cd0d8bf4dd80e2ce281cb85ace83ee07159a0e9f603242 languageName: node linkType: hard -"@nivo/colors@npm:0.84.0": - version: 0.84.0 - resolution: "@nivo/colors@npm:0.84.0" +"@nivo/colors@npm:0.87.0": + version: 0.87.0 + resolution: "@nivo/colors@npm:0.87.0" dependencies: - "@nivo/core": "npm:0.84.0" - "@types/d3-color": "npm:^2.0.0" - "@types/d3-scale": "npm:^3.2.3" - "@types/d3-scale-chromatic": "npm:^2.0.0" + "@nivo/core": "npm:0.87.0" + "@types/d3-color": "npm:^3.0.0" + "@types/d3-scale": "npm:^4.0.8" + "@types/d3-scale-chromatic": "npm:^3.0.0" "@types/prop-types": "npm:^15.7.2" d3-color: "npm:^3.1.0" - d3-scale: "npm:^3.2.3" - d3-scale-chromatic: "npm:^2.0.0" + d3-scale: "npm:^4.0.2" + d3-scale-chromatic: "npm:^3.0.0" lodash: "npm:^4.17.21" prop-types: "npm:^15.7.2" peerDependencies: react: ">= 16.14.0 < 19.0.0" - checksum: 10c0/036d9128bd451711c66bd377d80470fa9aa4a763fc2e89ac379fbde4661b3899096d1a8926e64a0edf56fa403d75e68eaead02bd5d4649883bb5c375796a63cc + checksum: 10c0/872f4e2d8392f89633531250e0474a365e0456dff146d2e8ba8576226effd1eda7e721ce1dd15a792b05d06caa28a11510a5ca8e55fb84aedb8ba3d964f357f5 languageName: node linkType: hard -"@nivo/core@npm:0.84.0, @nivo/core@npm:^0.84.0": - version: 0.84.0 - resolution: "@nivo/core@npm:0.84.0" +"@nivo/core@npm:0.87.0, @nivo/core@npm:^0.87.0": + version: 0.87.0 + resolution: "@nivo/core@npm:0.87.0" dependencies: - "@nivo/recompose": "npm:0.84.0" - "@nivo/tooltip": "npm:0.84.0" + "@nivo/tooltip": "npm:0.87.0" "@react-spring/web": "npm:9.4.5 || ^9.7.2" - "@types/d3-shape": "npm:^2.0.0" + "@types/d3-shape": "npm:^3.1.6" d3-color: "npm:^3.1.0" d3-format: "npm:^1.4.4" d3-interpolate: "npm:^3.0.1" - d3-scale: "npm:^3.2.3" + d3-scale: "npm:^4.0.2" d3-scale-chromatic: "npm:^3.0.0" - d3-shape: "npm:^1.3.5" + d3-shape: "npm:^3.2.0" d3-time-format: "npm:^3.0.0" lodash: "npm:^4.17.21" + prop-types: "npm:^15.7.2" peerDependencies: - prop-types: ">= 15.5.10 < 16.0.0" react: ">= 16.14.0 < 19.0.0" - checksum: 10c0/c92cca26ca7b33ae29c04a4da429843bc0a9c7b7a49be1b8d98b01af74384719a5f7e35d31e42a37f2191e0359d8d44e8e911acedae5371a02c4f7abdfb40fa3 + checksum: 10c0/75bb5e48cf57c8e31fbd9b9f33121fb4bffb98d47a967e5135527f8fde51537b68da3d894dc7231bf3c969523985eac6a5fc6d50827b958d51e6907a6391ed84 languageName: node linkType: hard -"@nivo/legends@npm:0.84.0": - version: 0.84.0 - resolution: "@nivo/legends@npm:0.84.0" +"@nivo/legends@npm:0.87.0": + version: 0.87.0 + resolution: "@nivo/legends@npm:0.87.0" dependencies: - "@nivo/colors": "npm:0.84.0" - "@nivo/core": "npm:0.84.0" - "@types/d3-scale": "npm:^3.2.3" - "@types/prop-types": "npm:^15.7.2" - d3-scale: "npm:^3.2.3" - prop-types: "npm:^15.7.2" + "@nivo/colors": "npm:0.87.0" + "@nivo/core": "npm:0.87.0" + "@types/d3-scale": "npm:^4.0.8" + d3-scale: "npm:^4.0.2" peerDependencies: react: ">= 16.14.0 < 19.0.0" - checksum: 10c0/25f64a068910a411b5073a30e74c5da0eb8e91d00d39619d62472726686349536bca95de8eb7e3c7e4a29872f652bdcd272f68f315ec54248c1cfdb15e3c37b6 + checksum: 10c0/1501bf698cefa2695d1124c38da5fc7712a5ce9911683c6694cbea1a481b9daef8ca7b0fc49eb85530c50347c571ea5b686caff3d6a81174b66bc38318bd317d languageName: node linkType: hard -"@nivo/recompose@npm:0.84.0": - version: 0.84.0 - resolution: "@nivo/recompose@npm:0.84.0" +"@nivo/line@npm:^0.87.0": + version: 0.87.0 + resolution: "@nivo/line@npm:0.87.0" dependencies: - "@types/prop-types": "npm:^15.7.2" - "@types/react-lifecycles-compat": "npm:^3.0.1" - prop-types: "npm:^15.7.2" - react-lifecycles-compat: "npm:^3.0.4" + "@nivo/annotations": "npm:0.87.0" + "@nivo/axes": "npm:0.87.0" + "@nivo/colors": "npm:0.87.0" + "@nivo/core": "npm:0.87.0" + "@nivo/legends": "npm:0.87.0" + "@nivo/scales": "npm:0.87.0" + "@nivo/tooltip": "npm:0.87.0" + "@nivo/voronoi": "npm:0.87.0" + "@react-spring/web": "npm:9.4.5 || ^9.7.2" + d3-shape: "npm:^3.2.0" peerDependencies: react: ">= 16.14.0 < 19.0.0" - checksum: 10c0/5a182544d445bf277cabb82e4139681231ab9659275e35e8e15f79b31ae96ebb223b19b8326c101c419bc82c26a4f26eb9980ff21937d84f971ce5833fe17d30 + checksum: 10c0/8ca8e4fbfe988a9c59c20f3c20caa391589f19af94d460e22925b9659e9058b82fddd4039c76e8242c6fd2676c15c6c8202041a9d3115397bdc9fe322ea7a06d + languageName: node + linkType: hard + +"@nivo/scales@npm:0.87.0": + version: 0.87.0 + resolution: "@nivo/scales@npm:0.87.0" + dependencies: + "@types/d3-scale": "npm:^4.0.8" + "@types/d3-time": "npm:^1.1.1" + "@types/d3-time-format": "npm:^3.0.0" + d3-scale: "npm:^4.0.2" + d3-time: "npm:^1.0.11" + d3-time-format: "npm:^3.0.0" + lodash: "npm:^4.17.21" + checksum: 10c0/7df01cd72fecbd82791c8aeb99439aa23ff54229ad9044e111fc02d51d0a2f7e8d67a75ed39854d829a7ab274580ca2e252931be5062eff3d08c48ab1580c7bd languageName: node linkType: hard -"@nivo/tooltip@npm:0.84.0": - version: 0.84.0 - resolution: "@nivo/tooltip@npm:0.84.0" +"@nivo/tooltip@npm:0.87.0": + version: 0.87.0 + resolution: "@nivo/tooltip@npm:0.87.0" dependencies: - "@nivo/core": "npm:0.84.0" + "@nivo/core": "npm:0.87.0" "@react-spring/web": "npm:9.4.5 || ^9.7.2" - checksum: 10c0/c9b52157951359c573b9bbe8e976d73f217694d89f2df470272a22d89b428fcab8d4e2c1e030104de5c2cf4fc06c0895ba7f4e56d93be2f42c25a836e1503038 + peerDependencies: + react: ">= 16.14.0 < 19.0.0" + checksum: 10c0/ac6b1b0bb0a09017c0e5055432e4c5ee771301615db3ee3d34abb55c900f765af78830eb2b18c8d94ff49ddeaa19895e907c793fb431943ade11cc2d7369b7e4 + languageName: node + linkType: hard + +"@nivo/voronoi@npm:0.87.0": + version: 0.87.0 + resolution: "@nivo/voronoi@npm:0.87.0" + dependencies: + "@nivo/core": "npm:0.87.0" + "@nivo/tooltip": "npm:0.87.0" + "@types/d3-delaunay": "npm:^6.0.4" + "@types/d3-scale": "npm:^4.0.8" + d3-delaunay: "npm:^6.0.4" + d3-scale: "npm:^4.0.2" + peerDependencies: + react: ">= 16.14.0 < 19.0.0" + checksum: 10c0/02bc6bb36ef1211bfdc440c135766f041dfba47b0d6c1e3b2748f7cde20ccec106621e559640a611b36b3e9d6a0e8d0492cace9143efbbf7aeb5499741eecf33 languageName: node linkType: hard @@ -9941,33 +9437,6 @@ __metadata: languageName: node linkType: hard -"@pnpm/config.env-replace@npm:^1.1.0": - version: 1.1.0 - resolution: "@pnpm/config.env-replace@npm:1.1.0" - checksum: 10c0/4cfc4a5c49ab3d0c6a1f196cfd4146374768b0243d441c7de8fa7bd28eaab6290f514b98490472cc65dbd080d34369447b3e9302585e1d5c099befd7c8b5e55f - languageName: node - linkType: hard - -"@pnpm/network.ca-file@npm:^1.0.1": - version: 1.0.2 - resolution: "@pnpm/network.ca-file@npm:1.0.2" - dependencies: - graceful-fs: "npm:4.2.10" - checksum: 10c0/95f6e0e38d047aca3283550719155ce7304ac00d98911e4ab026daedaf640a63bd83e3d13e17c623fa41ac72f3801382ba21260bcce431c14fbbc06430ecb776 - languageName: node - linkType: hard - -"@pnpm/npm-conf@npm:^2.1.0": - version: 2.3.0 - resolution: "@pnpm/npm-conf@npm:2.3.0" - dependencies: - "@pnpm/config.env-replace": "npm:^1.1.0" - "@pnpm/network.ca-file": "npm:^1.0.1" - config-chain: "npm:^1.1.11" - checksum: 10c0/605e986805b5bc46bde3d17cdc5a58f9da7da28ac331b83acde055eddefa8ca0e027844d8a97d337b8179ee6964db985214cec1206b76c29d0fcd5496c60abf2 - languageName: node - linkType: hard - "@polka/url@npm:^1.0.0-next.24": version: 1.0.0-next.25 resolution: "@polka/url@npm:1.0.0-next.25" @@ -12437,20 +11906,13 @@ __metadata: languageName: node linkType: hard -"@sindresorhus/is@npm:^4.0.0, @sindresorhus/is@npm:^4.6.0": +"@sindresorhus/is@npm:^4.0.0": version: 4.6.0 resolution: "@sindresorhus/is@npm:4.6.0" checksum: 10c0/33b6fb1d0834ec8dd7689ddc0e2781c2bfd8b9c4e4bacbcb14111e0ae00621f2c264b8a7d36541799d74888b5dccdf422a891a5cb5a709ace26325eedc81e22e languageName: node linkType: hard -"@sindresorhus/is@npm:^5.2.0": - version: 5.6.0 - resolution: "@sindresorhus/is@npm:5.6.0" - checksum: 10c0/66727344d0c92edde5760b5fd1f8092b717f2298a162a5f7f29e4953e001479927402d9d387e245fb9dc7d3b37c72e335e93ed5875edfc5203c53be8ecba1b52 - languageName: node - linkType: hard - "@sinonjs/commons@npm:^3.0.0": version: 3.0.1 resolution: "@sinonjs/commons@npm:3.0.1" @@ -12469,17 +11931,6 @@ __metadata: languageName: node linkType: hard -"@slorber/remark-comment@npm:^1.0.0": - version: 1.0.0 - resolution: "@slorber/remark-comment@npm:1.0.0" - dependencies: - micromark-factory-space: "npm:^1.0.0" - micromark-util-character: "npm:^1.1.0" - micromark-util-symbol: "npm:^1.0.1" - checksum: 10c0/b8da9d8f560740959c421d3ce5be43952eace1c95cb65402d9473a15e66463346a37fb5f121a6b22a83af51e8845b0b4ff3c321f14ce31bd58fb126acf6c8ed9 - languageName: node - linkType: hard - "@smithy/abort-controller@npm:^3.1.1": version: 3.1.1 resolution: "@smithy/abort-controller@npm:3.1.1" @@ -14857,7 +14308,7 @@ __metadata: languageName: node linkType: hard -"@svgr/webpack@npm:^8.0.1, @svgr/webpack@npm:^8.1.0": +"@svgr/webpack@npm:^8.0.1": version: 8.1.0 resolution: "@svgr/webpack@npm:8.1.0" dependencies: @@ -15244,15 +14695,6 @@ __metadata: languageName: node linkType: hard -"@szmarczak/http-timer@npm:^5.0.1": - version: 5.0.1 - resolution: "@szmarczak/http-timer@npm:5.0.1" - dependencies: - defer-to-connect: "npm:^2.0.1" - checksum: 10c0/4629d2fbb2ea67c2e9dc03af235c0991c79ebdddcbc19aed5d5732fb29ce01c13331e9b1a491584b9069bd6ecde6581dcbf871f11b7eefdebbab34de6cf2197e - languageName: node - linkType: hard - "@tabler/icons-react@npm:^2.44.0": version: 2.47.0 resolution: "@tabler/icons-react@npm:2.47.0" @@ -15807,15 +15249,6 @@ __metadata: languageName: node linkType: hard -"@types/bonjour@npm:^3.5.9": - version: 3.5.13 - resolution: "@types/bonjour@npm:3.5.13" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/eebedbca185ac3c39dd5992ef18d9e2a9f99e7f3c2f52f5561f90e9ed482c5d224c7962db95362712f580ed5713264e777a98d8f0bd8747f4eadf62937baed16 - languageName: node - linkType: hard - "@types/bytes@npm:^3.1.1": version: 3.1.4 resolution: "@types/bytes@npm:3.1.4" @@ -15870,16 +15303,6 @@ __metadata: languageName: node linkType: hard -"@types/connect-history-api-fallback@npm:^1.3.5": - version: 1.5.4 - resolution: "@types/connect-history-api-fallback@npm:1.5.4" - dependencies: - "@types/express-serve-static-core": "npm:*" - "@types/node": "npm:*" - checksum: 10c0/1b4035b627dcd714b05a22557f942e24a57ca48e7377dde0d2f86313fe685bc0a6566512a73257a55b5665b96c3041fb29228ac93331d8133011716215de8244 - languageName: node - linkType: hard - "@types/connect@npm:*": version: 3.4.38 resolution: "@types/connect@npm:3.4.38" @@ -15979,20 +15402,13 @@ __metadata: languageName: node linkType: hard -"@types/d3-color@npm:*": +"@types/d3-color@npm:*, @types/d3-color@npm:^3.0.0": version: 3.1.3 resolution: "@types/d3-color@npm:3.1.3" checksum: 10c0/65eb0487de606eb5ad81735a9a5b3142d30bc5ea801ed9b14b77cb14c9b909f718c059f13af341264ee189acf171508053342142bdf99338667cea26a2d8d6ae languageName: node linkType: hard -"@types/d3-color@npm:^2.0.0": - version: 2.0.6 - resolution: "@types/d3-color@npm:2.0.6" - checksum: 10c0/3d4b064d304fce21e9dccea3b8e11d11b7f1393df9bf577ea8b26fe16e0ea4b4ee4710c4fc4147c95c2db96512a23f80345dc22ebbb8d9c6dc473c4b709af47d - languageName: node - linkType: hard - "@types/d3-contour@npm:*": version: 3.0.6 resolution: "@types/d3-contour@npm:3.0.6" @@ -16003,7 +15419,7 @@ __metadata: languageName: node linkType: hard -"@types/d3-delaunay@npm:*": +"@types/d3-delaunay@npm:*, @types/d3-delaunay@npm:^6.0.4": version: 6.0.4 resolution: "@types/d3-delaunay@npm:6.0.4" checksum: 10c0/d154a8864f08c4ea23ecb9bdabcef1c406a25baa8895f0cb08a0ed2799de0d360e597552532ce7086ff0cdffa8f3563f9109d18f0191459d32bb620a36939123 @@ -16063,6 +15479,13 @@ __metadata: languageName: node linkType: hard +"@types/d3-format@npm:^1.4.1": + version: 1.4.5 + resolution: "@types/d3-format@npm:1.4.5" + checksum: 10c0/d4dbfff22afdf1ad60db7115e877b891864fac380537534dbacf9b5f87cdcd0a418e8d83d4947c59ed8715befa7d018aecd8445f05ae3a5b0796dd495508c082 + languageName: node + linkType: hard + "@types/d3-geo@npm:*": version: 3.1.0 resolution: "@types/d3-geo@npm:3.1.0" @@ -16095,13 +15518,6 @@ __metadata: languageName: node linkType: hard -"@types/d3-path@npm:^2": - version: 2.0.4 - resolution: "@types/d3-path@npm:2.0.4" - checksum: 10c0/82214a9644cfffe0c1f9a7aab00e3912aaba89115c60d94ecf716d282eac71671761962a9e911a8ebc457777e3db42f80c355b61010e5e27218f6aed32128d39 - languageName: node - linkType: hard - "@types/d3-polygon@npm:*": version: 3.0.2 resolution: "@types/d3-polygon@npm:3.0.2" @@ -16123,21 +15539,14 @@ __metadata: languageName: node linkType: hard -"@types/d3-scale-chromatic@npm:*": +"@types/d3-scale-chromatic@npm:*, @types/d3-scale-chromatic@npm:^3.0.0": version: 3.0.3 resolution: "@types/d3-scale-chromatic@npm:3.0.3" checksum: 10c0/2f48c6f370edba485b57b73573884ded71914222a4580140ff87ee96e1d55ccd05b1d457f726e234a31269b803270ac95d5554229ab6c43c7e4a9894e20dd490 languageName: node linkType: hard -"@types/d3-scale-chromatic@npm:^2.0.0": - version: 2.0.4 - resolution: "@types/d3-scale-chromatic@npm:2.0.4" - checksum: 10c0/d545ea57b4c2fb539d60fce090bc2d265df48047702b8762c7decca1557edf9f761722a5e47d4a65bbf9c7271421a4f6088dde5ee700f94ba8f798c8b0ca3af6 - languageName: node - linkType: hard - -"@types/d3-scale@npm:*": +"@types/d3-scale@npm:*, @types/d3-scale@npm:^4.0.8": version: 4.0.8 resolution: "@types/d3-scale@npm:4.0.8" dependencies: @@ -16146,15 +15555,6 @@ __metadata: languageName: node linkType: hard -"@types/d3-scale@npm:^3.2.3": - version: 3.3.5 - resolution: "@types/d3-scale@npm:3.3.5" - dependencies: - "@types/d3-time": "npm:^2" - checksum: 10c0/2689ab13092e3fded22cdd1b888afd91aa60190be40c8eddc12b2d42de59b00917778340f90317c68c5ffc3a1bee68f5ca155434cd466bc7804f400f3f9e7529 - languageName: node - linkType: hard - "@types/d3-selection@npm:*, @types/d3-selection@npm:^3.0.10, @types/d3-selection@npm:^3.0.3": version: 3.0.10 resolution: "@types/d3-selection@npm:3.0.10" @@ -16162,7 +15562,7 @@ __metadata: languageName: node linkType: hard -"@types/d3-shape@npm:*": +"@types/d3-shape@npm:*, @types/d3-shape@npm:^3.1.6": version: 3.1.6 resolution: "@types/d3-shape@npm:3.1.6" dependencies: @@ -16171,15 +15571,6 @@ __metadata: languageName: node linkType: hard -"@types/d3-shape@npm:^2.0.0": - version: 2.1.7 - resolution: "@types/d3-shape@npm:2.1.7" - dependencies: - "@types/d3-path": "npm:^2" - checksum: 10c0/2433f073b20a1f0180406a83e070a8d862101e637c1f6be8fbe814065d6627848b84b2bd33251752f5b469cd8e02217d21c43a8454ea1b56d7a0f493fa1a75a0 - languageName: node - linkType: hard - "@types/d3-time-format@npm:*": version: 4.0.3 resolution: "@types/d3-time-format@npm:4.0.3" @@ -16187,6 +15578,13 @@ __metadata: languageName: node linkType: hard +"@types/d3-time-format@npm:^2.3.1": + version: 2.3.4 + resolution: "@types/d3-time-format@npm:2.3.4" + checksum: 10c0/37b447f7338ab99d1591c7c2e55dde3b35916904132040046de4ad68a5691580bc29f23d04d6ce262454bc2713f1fbeaac912b5b44efcd8b733adc30b08ce28a + languageName: node + linkType: hard + "@types/d3-time-format@npm:^3.0.0": version: 3.0.4 resolution: "@types/d3-time-format@npm:3.0.4" @@ -16201,20 +15599,13 @@ __metadata: languageName: node linkType: hard -"@types/d3-time@npm:^1.0.10": +"@types/d3-time@npm:^1.0.10, @types/d3-time@npm:^1.1.1": version: 1.1.4 resolution: "@types/d3-time@npm:1.1.4" checksum: 10c0/d1dafa4605c10739de216bdf3dfe9c3953e583e849dc5586216525897c96bbbae8972c50e9c11a4c54e700c089914cf7a9764e9806d316a84838ecf9e5c52722 languageName: node linkType: hard -"@types/d3-time@npm:^2": - version: 2.1.4 - resolution: "@types/d3-time@npm:2.1.4" - checksum: 10c0/b597bfa51a163d4231e953d6903b06fd6341d0f11a28222a79fafaddb46155d7f458a67c814de53df84926a47dd535897228a475679d228576b0cda87351e534 - languageName: node - linkType: hard - "@types/d3-timer@npm:*": version: 3.0.2 resolution: "@types/d3-timer@npm:3.0.2" @@ -16385,7 +15776,7 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:*, @types/estree@npm:1.0.5, @types/estree@npm:^1.0.0, @types/estree@npm:^1.0.5": +"@types/estree@npm:*, @types/estree@npm:1.0.5, @types/estree@npm:^1.0.0": version: 1.0.5 resolution: "@types/estree@npm:1.0.5" checksum: 10c0/b3b0e334288ddb407c7b3357ca67dbee75ee22db242ca7c56fe27db4e1a31989cb8af48a84dd401deb787fe10cc6b2ab1ee82dc4783be87ededbe3d53c79c70d @@ -16406,26 +15797,26 @@ __metadata: languageName: node linkType: hard -"@types/express-serve-static-core@npm:*, @types/express-serve-static-core@npm:^4.17.18, @types/express-serve-static-core@npm:^4.17.30, @types/express-serve-static-core@npm:^4.17.33": - version: 4.19.5 - resolution: "@types/express-serve-static-core@npm:4.19.5" +"@types/express-serve-static-core@npm:4.17.31": + version: 4.17.31 + resolution: "@types/express-serve-static-core@npm:4.17.31" dependencies: "@types/node": "npm:*" "@types/qs": "npm:*" "@types/range-parser": "npm:*" - "@types/send": "npm:*" - checksum: 10c0/ba8d8d976ab797b2602c60e728802ff0c98a00f13d420d82770f3661b67fa36ea9d3be0b94f2ddd632afe1fbc6e41620008b01db7e4fabdd71a2beb5539b0725 + checksum: 10c0/c24f28f77413e16e1eea765c530ee8dc4797379a44323e9788f92fabb29c2c31beab17c4e64dec8eb8166f8d2abd40e45bd8bc876e55de271a5688b603ae1162 languageName: node linkType: hard -"@types/express-serve-static-core@npm:4.17.31": - version: 4.17.31 - resolution: "@types/express-serve-static-core@npm:4.17.31" +"@types/express-serve-static-core@npm:^4.17.18, @types/express-serve-static-core@npm:^4.17.30, @types/express-serve-static-core@npm:^4.17.33": + version: 4.19.5 + resolution: "@types/express-serve-static-core@npm:4.19.5" dependencies: "@types/node": "npm:*" "@types/qs": "npm:*" "@types/range-parser": "npm:*" - checksum: 10c0/c24f28f77413e16e1eea765c530ee8dc4797379a44323e9788f92fabb29c2c31beab17c4e64dec8eb8166f8d2abd40e45bd8bc876e55de271a5688b603ae1162 + "@types/send": "npm:*" + checksum: 10c0/ba8d8d976ab797b2602c60e728802ff0c98a00f13d420d82770f3661b67fa36ea9d3be0b94f2ddd632afe1fbc6e41620008b01db7e4fabdd71a2beb5539b0725 languageName: node linkType: hard @@ -16544,13 +15935,6 @@ __metadata: languageName: node linkType: hard -"@types/gtag.js@npm:^0.0.12": - version: 0.0.12 - resolution: "@types/gtag.js@npm:0.0.12" - checksum: 10c0/fee8f4c6e627301b89ab616c9e219bd53fa6ea1ffd1d0a8021e21363f0bdb2cf7eb1a5bcda0c6f1502186379bc7784ec29c932e21634f4e07f9e7a8c56887400 - languageName: node - linkType: hard - "@types/har-format@npm:*, @types/har-format@npm:^1.2.10": version: 1.2.15 resolution: "@types/har-format@npm:1.2.15" @@ -16576,13 +15960,6 @@ __metadata: languageName: node linkType: hard -"@types/history@npm:^4.7.11": - version: 4.7.11 - resolution: "@types/history@npm:4.7.11" - checksum: 10c0/3facf37c2493d1f92b2e93a22cac7ea70b06351c2ab9aaceaa3c56aa6099fb63516f6c4ec1616deb5c56b4093c026a043ea2d3373e6c0644d55710364d02c934 - languageName: node - linkType: hard - "@types/hoist-non-react-statics@npm:^3.3.1": version: 3.3.5 resolution: "@types/hoist-non-react-statics@npm:3.3.5" @@ -16593,13 +15970,6 @@ __metadata: languageName: node linkType: hard -"@types/html-minifier-terser@npm:^6.0.0": - version: 6.1.0 - resolution: "@types/html-minifier-terser@npm:6.1.0" - checksum: 10c0/a62fb8588e2f3818d82a2d7b953ad60a4a52fd767ae04671de1c16f5788bd72f1ed3a6109ed63fd190c06a37d919e3c39d8adbc1793a005def76c15a3f5f5dab - languageName: node - linkType: hard - "@types/http-assert@npm:*": version: 1.5.5 resolution: "@types/http-assert@npm:1.5.5" @@ -16607,7 +15977,7 @@ __metadata: languageName: node linkType: hard -"@types/http-cache-semantics@npm:*, @types/http-cache-semantics@npm:^4.0.2": +"@types/http-cache-semantics@npm:*": version: 4.0.4 resolution: "@types/http-cache-semantics@npm:4.0.4" checksum: 10c0/51b72568b4b2863e0fe8d6ce8aad72a784b7510d72dc866215642da51d84945a9459fa89f49ec48f1e9a1752e6a78e85a4cda0ded06b1c73e727610c925f9ce6 @@ -16621,15 +15991,6 @@ __metadata: languageName: node linkType: hard -"@types/http-proxy@npm:^1.17.8": - version: 1.17.15 - resolution: "@types/http-proxy@npm:1.17.15" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/e2bf2fcdf23c88141b8d2c85ed5e5418b62ef78285884a2b5a717af55f4d9062136aa475489d10292093343df58fb81975f34bebd6b9df322288fd9821cbee07 - languageName: node - linkType: hard - "@types/istanbul-lib-coverage@npm:*, @types/istanbul-lib-coverage@npm:^2.0.0, @types/istanbul-lib-coverage@npm:^2.0.1, @types/istanbul-lib-coverage@npm:^2.0.4": version: 2.0.6 resolution: "@types/istanbul-lib-coverage@npm:2.0.6" @@ -17020,15 +16381,6 @@ __metadata: languageName: node linkType: hard -"@types/mdast@npm:^4.0.0, @types/mdast@npm:^4.0.2": - version: 4.0.4 - resolution: "@types/mdast@npm:4.0.4" - dependencies: - "@types/unist": "npm:*" - checksum: 10c0/84f403dbe582ee508fd9c7643ac781ad8597fcbfc9ccb8d4715a2c92e4545e5772cbd0dbdf18eda65789386d81b009967fdef01b24faf6640f817287f54d9c82 - languageName: node - linkType: hard - "@types/mdurl@npm:^1.0.0": version: 1.0.5 resolution: "@types/mdurl@npm:1.0.5" @@ -17113,15 +16465,6 @@ __metadata: languageName: node linkType: hard -"@types/node-forge@npm:^1.3.0": - version: 1.3.11 - resolution: "@types/node-forge@npm:1.3.11" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/3d7d23ca0ba38ac0cf74028393bd70f31169ab9aba43f21deb787840170d307d662644bac07287495effe2812ddd7ac8a14dbd43f16c2936bbb06312e96fc3b9 - languageName: node - linkType: hard - "@types/node@npm:*, @types/node@npm:>=8.1.0, @types/node@npm:^22.1.0": version: 22.1.0 resolution: "@types/node@npm:22.1.0" @@ -17154,13 +16497,6 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:^17.0.5": - version: 17.0.45 - resolution: "@types/node@npm:17.0.45" - checksum: 10c0/0db377133d709b33a47892581a21a41cd7958f22723a3cc6c71d55ac018121382de42fbfc7970d5ae3e7819dbe5f40e1c6a5174aedf7e7964e9cb8fa72b580b0 - languageName: node - linkType: hard - "@types/node@npm:^18.0.0, @types/node@npm:^18.11.18": version: 18.19.43 resolution: "@types/node@npm:18.19.43" @@ -17310,6 +16646,13 @@ __metadata: languageName: node linkType: hard +"@types/pluralize@npm:^0.0.33": + version: 0.0.33 + resolution: "@types/pluralize@npm:0.0.33" + checksum: 10c0/24899caf85b79dd291a6b6e9b9f3b67b452b18d578d0ac0d531a705bf5ee0361d9386ea1f8532c64de9e22c6e9606c5497787bb5e31bd299c487980436c59785 + languageName: node + linkType: hard + "@types/pretty-hrtime@npm:^1.0.0": version: 1.0.3 resolution: "@types/pretty-hrtime@npm:1.0.3" @@ -17374,47 +16717,6 @@ __metadata: languageName: node linkType: hard -"@types/react-lifecycles-compat@npm:^3.0.1": - version: 3.0.4 - resolution: "@types/react-lifecycles-compat@npm:3.0.4" - dependencies: - "@types/react": "npm:*" - checksum: 10c0/3c33fcd7d52d44031b21cf8a6ae9c0f208fe3b972ee4f03fcbe4509d2c50da474bfdd3330f5a09046b7fd63a1f7f23b194bc8d774823c1981cc13929744b90d2 - languageName: node - linkType: hard - -"@types/react-router-config@npm:*, @types/react-router-config@npm:^5.0.7": - version: 5.0.11 - resolution: "@types/react-router-config@npm:5.0.11" - dependencies: - "@types/history": "npm:^4.7.11" - "@types/react": "npm:*" - "@types/react-router": "npm:^5.1.0" - checksum: 10c0/3fa4daf8c14689a05f34e289fc53c4a892e97f35715455c507a8048d9875b19cd3d3142934ca973effed6a6c38f33539b6e173cd254f67e2021ecd5458d551c8 - languageName: node - linkType: hard - -"@types/react-router-dom@npm:*": - version: 5.3.3 - resolution: "@types/react-router-dom@npm:5.3.3" - dependencies: - "@types/history": "npm:^4.7.11" - "@types/react": "npm:*" - "@types/react-router": "npm:*" - checksum: 10c0/a9231a16afb9ed5142678147eafec9d48582809295754fb60946e29fcd3757a4c7a3180fa94b45763e4c7f6e3f02379e2fcb8dd986db479dcab40eff5fc62a91 - languageName: node - linkType: hard - -"@types/react-router@npm:*, @types/react-router@npm:^5.1.0": - version: 5.1.20 - resolution: "@types/react-router@npm:5.1.20" - dependencies: - "@types/history": "npm:^4.7.11" - "@types/react": "npm:*" - checksum: 10c0/1f7eee61981d2f807fa01a34a0ef98ebc0774023832b6611a69c7f28fdff01de5a38cabf399f32e376bf8099dcb7afaf724775bea9d38870224492bea4cb5737 - languageName: node - linkType: hard - "@types/react@npm:*, @types/react@npm:>=16, @types/react@npm:^18.2.39": version: 18.3.3 resolution: "@types/react@npm:18.3.3" @@ -17459,15 +16761,6 @@ __metadata: languageName: node linkType: hard -"@types/sax@npm:^1.2.1": - version: 1.2.7 - resolution: "@types/sax@npm:1.2.7" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/d077a761a0753b079bf8279b3993948030ca86ed9125437b9b29c1de40db9b2deb7fddc369f014b58861d450e8b8cc75f163aa29dc8cea81952efbfd859168cf - languageName: node - linkType: hard - "@types/scheduler@npm:^0.16": version: 0.16.8 resolution: "@types/scheduler@npm:0.16.8" @@ -17499,16 +16792,7 @@ __metadata: languageName: node linkType: hard -"@types/serve-index@npm:^1.9.1": - version: 1.9.4 - resolution: "@types/serve-index@npm:1.9.4" - dependencies: - "@types/express": "npm:*" - checksum: 10c0/94c1b9e8f1ea36a229e098e1643d5665d9371f8c2658521718e259130a237c447059b903bac0dcc96ee2c15fd63f49aa647099b7d0d437a67a6946527a837438 - languageName: node - linkType: hard - -"@types/serve-static@npm:*, @types/serve-static@npm:^1.13.10": +"@types/serve-static@npm:*": version: 1.15.7 resolution: "@types/serve-static@npm:1.15.7" dependencies: @@ -17526,15 +16810,6 @@ __metadata: languageName: node linkType: hard -"@types/sockjs@npm:^0.3.33": - version: 0.3.36 - resolution: "@types/sockjs@npm:0.3.36" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/b20b7820ee813f22de4f2ce98bdd12c68c930e016a8912b1ed967595ac0d8a4cbbff44f4d486dd97f77f5927e7b5725bdac7472c9ec5b27f53a5a13179f0612f - languageName: node - linkType: hard - "@types/stack-utils@npm:^2.0.0": version: 2.0.3 resolution: "@types/stack-utils@npm:2.0.3" @@ -17686,7 +16961,7 @@ __metadata: languageName: node linkType: hard -"@types/ws@npm:^8.0.0, @types/ws@npm:^8.5.5": +"@types/ws@npm:^8.0.0": version: 8.5.12 resolution: "@types/ws@npm:8.5.12" dependencies: @@ -18001,7 +17276,7 @@ __metadata: languageName: node linkType: hard -"@ungap/structured-clone@npm:^1.0.0, @ungap/structured-clone@npm:^1.2.0": +"@ungap/structured-clone@npm:^1.2.0": version: 1.2.0 resolution: "@ungap/structured-clone@npm:1.2.0" checksum: 10c0/8209c937cb39119f44eb63cf90c0b73e7c754209a6411c707be08e50e29ee81356dca1a848a405c8bdeebfe2f5e4f831ad310ae1689eeef65e7445c090c6657d @@ -18231,7 +17506,7 @@ __metadata: languageName: node linkType: hard -"@webassemblyjs/ast@npm:1.12.1, @webassemblyjs/ast@npm:^1.11.5, @webassemblyjs/ast@npm:^1.12.1": +"@webassemblyjs/ast@npm:1.12.1, @webassemblyjs/ast@npm:^1.11.5": version: 1.12.1 resolution: "@webassemblyjs/ast@npm:1.12.1" dependencies: @@ -18317,7 +17592,7 @@ __metadata: languageName: node linkType: hard -"@webassemblyjs/wasm-edit@npm:^1.11.5, @webassemblyjs/wasm-edit@npm:^1.12.1": +"@webassemblyjs/wasm-edit@npm:^1.11.5": version: 1.12.1 resolution: "@webassemblyjs/wasm-edit@npm:1.12.1" dependencies: @@ -18358,7 +17633,7 @@ __metadata: languageName: node linkType: hard -"@webassemblyjs/wasm-parser@npm:1.12.1, @webassemblyjs/wasm-parser@npm:^1.11.5, @webassemblyjs/wasm-parser@npm:^1.12.1": +"@webassemblyjs/wasm-parser@npm:1.12.1, @webassemblyjs/wasm-parser@npm:^1.11.5": version: 1.12.1 resolution: "@webassemblyjs/wasm-parser@npm:1.12.1" dependencies: @@ -18725,7 +18000,7 @@ __metadata: languageName: node linkType: hard -"accepts@npm:^1.3.5, accepts@npm:~1.3.4, accepts@npm:~1.3.5, accepts@npm:~1.3.8": +"accepts@npm:^1.3.5, accepts@npm:~1.3.5, accepts@npm:~1.3.8": version: 1.3.8 resolution: "accepts@npm:1.3.8" dependencies: @@ -18790,7 +18065,7 @@ __metadata: languageName: node linkType: hard -"acorn-walk@npm:^8.0.0, acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1, acorn-walk@npm:^8.2.0, acorn-walk@npm:^8.3.2": +"acorn-walk@npm:^8.0.2, acorn-walk@npm:^8.1.1, acorn-walk@npm:^8.2.0, acorn-walk@npm:^8.3.2": version: 8.3.3 resolution: "acorn-walk@npm:8.3.3" dependencies: @@ -18808,7 +18083,7 @@ __metadata: languageName: node linkType: hard -"acorn@npm:^8.0.0, acorn@npm:^8.0.4, acorn@npm:^8.1.0, acorn@npm:^8.11.0, acorn@npm:^8.11.3, acorn@npm:^8.12.1, acorn@npm:^8.4.1, acorn@npm:^8.5.0, acorn@npm:^8.7.1, acorn@npm:^8.8.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": +"acorn@npm:^8.0.0, acorn@npm:^8.1.0, acorn@npm:^8.11.0, acorn@npm:^8.11.3, acorn@npm:^8.12.1, acorn@npm:^8.4.1, acorn@npm:^8.5.0, acorn@npm:^8.7.1, acorn@npm:^8.8.1, acorn@npm:^8.8.2, acorn@npm:^8.9.0": version: 8.12.1 resolution: "acorn@npm:8.12.1" bin: @@ -18824,7 +18099,7 @@ __metadata: languageName: node linkType: hard -"address@npm:^1.0.1, address@npm:^1.1.2": +"address@npm:^1.0.1": version: 1.2.2 resolution: "address@npm:1.2.2" checksum: 10c0/1c8056b77fb124456997b78ed682ecc19d2fd7ea8bd5850a2aa8c3e3134c913847c57bcae418622efd32ba858fa1e242a40a251ac31da0515664fc0ac03a047d @@ -18896,7 +18171,7 @@ __metadata: languageName: node linkType: hard -"ajv-formats@npm:2.1.1, ajv-formats@npm:^2.1.1": +"ajv-formats@npm:2.1.1": version: 2.1.1 resolution: "ajv-formats@npm:2.1.1" dependencies: @@ -18910,7 +18185,7 @@ __metadata: languageName: node linkType: hard -"ajv-keywords@npm:^3.4.1, ajv-keywords@npm:^3.5.2": +"ajv-keywords@npm:^3.5.2": version: 3.5.2 resolution: "ajv-keywords@npm:3.5.2" peerDependencies: @@ -18919,17 +18194,6 @@ __metadata: languageName: node linkType: hard -"ajv-keywords@npm:^5.1.0": - version: 5.1.0 - resolution: "ajv-keywords@npm:5.1.0" - dependencies: - fast-deep-equal: "npm:^3.1.3" - peerDependencies: - ajv: ^8.8.2 - checksum: 10c0/18bec51f0171b83123ba1d8883c126e60c6f420cef885250898bf77a8d3e65e3bfb9e8564f497e30bdbe762a83e0d144a36931328616a973ee669dc74d4a9590 - languageName: node - linkType: hard - "ajv@npm:8.12.0": version: 8.12.0 resolution: "ajv@npm:8.12.0" @@ -18942,7 +18206,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^6.12.2, ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5, ajv@npm:~6.12.6": +"ajv@npm:^6.12.3, ajv@npm:^6.12.4, ajv@npm:^6.12.5, ajv@npm:~6.12.6": version: 6.12.6 resolution: "ajv@npm:6.12.6" dependencies: @@ -18954,7 +18218,7 @@ __metadata: languageName: node linkType: hard -"ajv@npm:^8.0.0, ajv@npm:^8.9.0": +"ajv@npm:^8.0.0": version: 8.17.1 resolution: "ajv@npm:8.17.1" dependencies: @@ -18966,18 +18230,7 @@ __metadata: languageName: node linkType: hard -"algoliasearch-helper@npm:^3.13.3": - version: 3.22.3 - resolution: "algoliasearch-helper@npm:3.22.3" - dependencies: - "@algolia/events": "npm:^4.0.1" - peerDependencies: - algoliasearch: ">= 3.1 < 6" - checksum: 10c0/c522eedd6cef022cd5c23ad3ec24691ce555ea1401cdd8c1cd650070b083dbd10bb6e859436d3a22659cc7a3ec9c056accbc6c02f957e1e316c2f5b3ec387f92 - languageName: node - linkType: hard - -"algoliasearch@npm:^4.18.0, algoliasearch@npm:^4.19.1": +"algoliasearch@npm:^4.19.1": version: 4.24.0 resolution: "algoliasearch@npm:4.24.0" dependencies: @@ -19007,7 +18260,7 @@ __metadata: languageName: node linkType: hard -"ansi-align@npm:^3.0.0, ansi-align@npm:^3.0.1": +"ansi-align@npm:^3.0.0": version: 3.0.1 resolution: "ansi-align@npm:3.0.1" dependencies: @@ -19055,15 +18308,6 @@ __metadata: languageName: node linkType: hard -"ansi-html-community@npm:^0.0.8": - version: 0.0.8 - resolution: "ansi-html-community@npm:0.0.8" - bin: - ansi-html: bin/ansi-html - checksum: 10c0/45d3a6f0b4f10b04fdd44bef62972e2470bfd917bf00439471fa7473d92d7cbe31369c73db863cc45dda115cb42527f39e232e9256115534b8ee5806b0caeed4 - languageName: node - linkType: hard - "ansi-regex@npm:^2.0.0": version: 2.1.1 resolution: "ansi-regex@npm:2.1.1" @@ -19467,13 +18711,6 @@ __metadata: languageName: node linkType: hard -"arg@npm:^5.0.0": - version: 5.0.2 - resolution: "arg@npm:5.0.2" - checksum: 10c0/ccaf86f4e05d342af6666c569f844bec426595c567d32a8289715087825c2ca7edd8a3d204e4d2fb2aa4602e09a57d0c13ea8c9eea75aac3dbb4af5514e6800e - languageName: node - linkType: hard - "argparse@npm:^1.0.7, argparse@npm:~1.0.9": version: 1.0.10 resolution: "argparse@npm:1.0.10" @@ -19733,7 +18970,7 @@ __metadata: languageName: node linkType: hard -"assert@npm:^2.0.0, assert@npm:^2.1.0": +"assert@npm:^2.1.0": version: 2.1.0 resolution: "assert@npm:2.1.0" dependencies: @@ -19861,24 +19098,6 @@ __metadata: languageName: node linkType: hard -"autoprefixer@npm:^10.4.14, autoprefixer@npm:^10.4.19": - version: 10.4.20 - resolution: "autoprefixer@npm:10.4.20" - dependencies: - browserslist: "npm:^4.23.3" - caniuse-lite: "npm:^1.0.30001646" - fraction.js: "npm:^4.3.7" - normalize-range: "npm:^0.1.2" - picocolors: "npm:^1.0.1" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.1.0 - bin: - autoprefixer: bin/autoprefixer - checksum: 10c0/e1f00978a26e7c5b54ab12036d8c13833fad7222828fc90914771b1263f51b28c7ddb5803049de4e77696cbd02bb25cfc3634e80533025bb26c26aacdf938940 - languageName: node - linkType: hard - "available-typed-arrays@npm:^1.0.7": version: 1.0.7 resolution: "available-typed-arrays@npm:1.0.7" @@ -20176,19 +19395,6 @@ __metadata: languageName: node linkType: hard -"babel-loader@npm:^9.1.3": - version: 9.1.3 - resolution: "babel-loader@npm:9.1.3" - dependencies: - find-cache-dir: "npm:^4.0.0" - schema-utils: "npm:^4.0.0" - peerDependencies: - "@babel/core": ^7.12.0 - webpack: ">=5" - checksum: 10c0/e3fc3c9e02bd908b37e8e8cd4f3d7280cf6ac45e33fc203aedbb615135a0fecc33bf92573b71a166a827af029d302c0b060354985cd91d510320bd70a2f949eb - languageName: node - linkType: hard - "babel-merge@npm:^3.0.0": version: 3.0.0 resolution: "babel-merge@npm:3.0.0" @@ -20232,15 +19438,6 @@ __metadata: languageName: node linkType: hard -"babel-plugin-dynamic-import-node@npm:^2.3.3": - version: 2.3.3 - resolution: "babel-plugin-dynamic-import-node@npm:2.3.3" - dependencies: - object.assign: "npm:^4.1.0" - checksum: 10c0/1bd80df981e1fc1aff0cd4e390cf27aaa34f95f7620cd14dff07ba3bad56d168c098233a7d2deb2c9b1dc13643e596a6b94fc608a3412ee3c56e74a25cd2167e - languageName: node - linkType: hard - "babel-plugin-istanbul@npm:^6.1.1": version: 6.1.1 resolution: "babel-plugin-istanbul@npm:6.1.1" @@ -21106,13 +20303,6 @@ __metadata: languageName: node linkType: hard -"batch@npm:0.6.1": - version: 0.6.1 - resolution: "batch@npm:0.6.1" - checksum: 10c0/925a13897b4db80d4211082fe287bcf96d297af38e26448c857cee3e095c9792e3b8f26b37d268812e7f38a589f694609de8534a018b1937d7dc9f84e6b387c5 - languageName: node - linkType: hard - "bcrypt-pbkdf@npm:^1.0.0": version: 1.0.2 resolution: "bcrypt-pbkdf@npm:1.0.2" @@ -21320,16 +20510,6 @@ __metadata: languageName: node linkType: hard -"bonjour-service@npm:^1.0.11": - version: 1.2.1 - resolution: "bonjour-service@npm:1.2.1" - dependencies: - fast-deep-equal: "npm:^3.1.3" - multicast-dns: "npm:^7.2.5" - checksum: 10c0/953cbfc27fc9e36e6f988012993ab2244817d82426603e0390d4715639031396c932b6657b1aa4ec30dbb5fa903d6b2c7f1be3af7a8ba24165c93e987c849730 - languageName: node - linkType: hard - "boolbase@npm:^1.0.0": version: 1.0.0 resolution: "boolbase@npm:1.0.0" @@ -21360,38 +20540,6 @@ __metadata: languageName: node linkType: hard -"boxen@npm:^6.2.1": - version: 6.2.1 - resolution: "boxen@npm:6.2.1" - dependencies: - ansi-align: "npm:^3.0.1" - camelcase: "npm:^6.2.0" - chalk: "npm:^4.1.2" - cli-boxes: "npm:^3.0.0" - string-width: "npm:^5.0.1" - type-fest: "npm:^2.5.0" - widest-line: "npm:^4.0.1" - wrap-ansi: "npm:^8.0.1" - checksum: 10c0/2a50d059c950a50d9f3c873093702747740814ce8819225c4f8cbe92024c9f5a9219d2b7128f5cfa17c022644d929bbbc88b9591de67249c6ebe07f7486bdcfd - languageName: node - linkType: hard - -"boxen@npm:^7.0.0": - version: 7.1.1 - resolution: "boxen@npm:7.1.1" - dependencies: - ansi-align: "npm:^3.0.1" - camelcase: "npm:^7.0.1" - chalk: "npm:^5.2.0" - cli-boxes: "npm:^3.0.0" - string-width: "npm:^5.1.2" - type-fest: "npm:^2.13.0" - widest-line: "npm:^4.0.1" - wrap-ansi: "npm:^8.1.0" - checksum: 10c0/3a9891dc98ac40d582c9879e8165628258e2c70420c919e70fff0a53ccc7b42825e73cda6298199b2fbc1f41f5d5b93b492490ad2ae27623bed3897ddb4267f8 - languageName: node - linkType: hard - "bplist-parser@npm:^0.2.0": version: 0.2.0 resolution: "bplist-parser@npm:0.2.0" @@ -21558,7 +20706,7 @@ __metadata: languageName: node linkType: hard -"browserify-zlib@npm:^0.2.0, browserify-zlib@npm:~0.2.0": +"browserify-zlib@npm:~0.2.0": version: 0.2.0 resolution: "browserify-zlib@npm:0.2.0" dependencies: @@ -21625,7 +20773,7 @@ __metadata: languageName: node linkType: hard -"browserslist@npm:^4.0.0, browserslist@npm:^4.14.5, browserslist@npm:^4.18.1, browserslist@npm:^4.21.10, browserslist@npm:^4.23.0, browserslist@npm:^4.23.1, browserslist@npm:^4.23.3": +"browserslist@npm:^4.14.5, browserslist@npm:^4.23.1, browserslist@npm:^4.23.3": version: 4.23.3 resolution: "browserslist@npm:4.23.3" dependencies: @@ -21937,28 +21085,6 @@ __metadata: languageName: node linkType: hard -"cacheable-lookup@npm:^7.0.0": - version: 7.0.0 - resolution: "cacheable-lookup@npm:7.0.0" - checksum: 10c0/63a9c144c5b45cb5549251e3ea774c04d63063b29e469f7584171d059d3a88f650f47869a974e2d07de62116463d742c287a81a625e791539d987115cb081635 - languageName: node - linkType: hard - -"cacheable-request@npm:^10.2.8": - version: 10.2.14 - resolution: "cacheable-request@npm:10.2.14" - dependencies: - "@types/http-cache-semantics": "npm:^4.0.2" - get-stream: "npm:^6.0.1" - http-cache-semantics: "npm:^4.1.1" - keyv: "npm:^4.5.3" - mimic-response: "npm:^4.0.0" - normalize-url: "npm:^8.0.0" - responselike: "npm:^3.0.0" - checksum: 10c0/41b6658db369f20c03128227ecd219ca7ac52a9d24fc0f499cc9aa5d40c097b48b73553504cebd137024d957c0ddb5b67cf3ac1439b136667f3586257763f88d - languageName: node - linkType: hard - "cacheable-request@npm:^6.0.0": version: 6.1.0 resolution: "cacheable-request@npm:6.1.0" @@ -22075,19 +21201,7 @@ __metadata: languageName: node linkType: hard -"caniuse-api@npm:^3.0.0": - version: 3.0.0 - resolution: "caniuse-api@npm:3.0.0" - dependencies: - browserslist: "npm:^4.0.0" - caniuse-lite: "npm:^1.0.0" - lodash.memoize: "npm:^4.1.2" - lodash.uniq: "npm:^4.5.0" - checksum: 10c0/60f9e85a3331e6d761b1b03eec71ca38ef7d74146bece34694853033292156b815696573ed734b65583acf493e88163618eda915c6c826d46a024c71a9572b4c - languageName: node - linkType: hard - -"caniuse-lite@npm:^1.0.0, caniuse-lite@npm:^1.0.30001406, caniuse-lite@npm:^1.0.30001579, caniuse-lite@npm:^1.0.30001646": +"caniuse-lite@npm:^1.0.30001406, caniuse-lite@npm:^1.0.30001579, caniuse-lite@npm:^1.0.30001646": version: 1.0.30001651 resolution: "caniuse-lite@npm:1.0.30001651" checksum: 10c0/7821278952a6dbd17358e5d08083d258f092e2a530f5bc1840657cb140fbbc5ec44293bc888258c44a18a9570cde149ed05819ac8320b9710cf22f699891e6ad @@ -22197,7 +21311,7 @@ __metadata: languageName: node linkType: hard -"chalk@npm:^5.0.1, chalk@npm:^5.2.0, chalk@npm:^5.3.0": +"chalk@npm:^5.2.0, chalk@npm:^5.3.0": version: 5.3.0 resolution: "chalk@npm:5.3.0" checksum: 10c0/8297d436b2c0f95801103ff2ef67268d362021b8210daf8ddbe349695333eb3610a71122172ff3b0272f1ef2cf7cc2c41fdaa4715f52e49ffe04c56340feed09 @@ -22367,7 +21481,7 @@ __metadata: languageName: node linkType: hard -"cheerio@npm:^1.0.0-rc.10, cheerio@npm:^1.0.0-rc.12": +"cheerio@npm:^1.0.0-rc.10": version: 1.0.0-rc.12 resolution: "cheerio@npm:1.0.0-rc.12" dependencies: @@ -22401,7 +21515,7 @@ __metadata: languageName: node linkType: hard -"chokidar@npm:3.6.0, chokidar@npm:^3.4.2, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3, chokidar@npm:^3.6.0": +"chokidar@npm:3.6.0, chokidar@npm:^3.5.1, chokidar@npm:^3.5.3, chokidar@npm:^3.6.0": version: 3.6.0 resolution: "chokidar@npm:3.6.0" dependencies: @@ -22549,15 +21663,6 @@ __metadata: languageName: node linkType: hard -"clean-css@npm:^5.2.2, clean-css@npm:^5.3.2, clean-css@npm:~5.3.2": - version: 5.3.3 - resolution: "clean-css@npm:5.3.3" - dependencies: - source-map: "npm:~0.6.0" - checksum: 10c0/381de7523e23f3762eb180e327dcc0cedafaf8cb1cd8c26b7cc1fc56e0829a92e734729c4f955394d65ed72fb62f82d8baf78af34b33b8a7d41ebad2accdd6fb - languageName: node - linkType: hard - "clean-regexp@npm:^1.0.0": version: 1.0.0 resolution: "clean-regexp@npm:1.0.0" @@ -22597,13 +21702,6 @@ __metadata: languageName: node linkType: hard -"cli-boxes@npm:^3.0.0": - version: 3.0.0 - resolution: "cli-boxes@npm:3.0.0" - checksum: 10c0/4db3e8fbfaf1aac4fb3a6cbe5a2d3fa048bee741a45371b906439b9ffc821c6e626b0f108bdcd3ddf126a4a319409aedcf39a0730573ff050fdd7b6731e99fb9 - languageName: node - linkType: hard - "cli-color@npm:^2.0.0": version: 2.0.4 resolution: "cli-color@npm:2.0.4" @@ -22951,13 +22049,6 @@ __metadata: languageName: node linkType: hard -"collapse-white-space@npm:^2.0.0": - version: 2.1.0 - resolution: "collapse-white-space@npm:2.1.0" - checksum: 10c0/b2e2800f4ab261e62eb27a1fbe853378296e3a726d6695117ed033e82d61fb6abeae4ffc1465d5454499e237005de9cfc52c9562dc7ca4ac759b9a222ef14453 - languageName: node - linkType: hard - "collect-v8-coverage@npm:^1.0.0": version: 1.0.2 resolution: "collect-v8-coverage@npm:1.0.2" @@ -23026,14 +22117,7 @@ __metadata: languageName: node linkType: hard -"colord@npm:^2.9.3": - version: 2.9.3 - resolution: "colord@npm:2.9.3" - checksum: 10c0/9699e956894d8996b28c686afe8988720785f476f59335c80ce852ded76ab3ebe252703aec53d9bef54f6219aea6b960fb3d9a8300058a1d0c0d4026460cd110 - languageName: node - linkType: hard - -"colorette@npm:^2.0.10, colorette@npm:^2.0.16, colorette@npm:^2.0.20": +"colorette@npm:^2.0.16, colorette@npm:^2.0.20": version: 2.0.20 resolution: "colorette@npm:2.0.20" checksum: 10c0/e94116ff33b0ff56f3b83b9ace895e5bf87c2a7a47b3401b8c3f3226e050d5ef76cf4072fb3325f9dc24d1698f9b730baf4e05eeaf861d74a1883073f4c98a40 @@ -23064,13 +22148,6 @@ __metadata: languageName: node linkType: hard -"combine-promises@npm:^1.1.0": - version: 1.2.0 - resolution: "combine-promises@npm:1.2.0" - checksum: 10c0/906ebf056006eff93c11548df0415053b6756145dae1f5a89579e743cb15fceeb0604555791321db4fba5072aa39bb4de6547e9cdf14589fe949b33d1613422c - languageName: node - linkType: hard - "combine-source-map@npm:^0.8.0, combine-source-map@npm:~0.8.0": version: 0.8.0 resolution: "combine-source-map@npm:0.8.0" @@ -23196,13 +22273,6 @@ __metadata: languageName: node linkType: hard -"common-path-prefix@npm:^3.0.0": - version: 3.0.0 - resolution: "common-path-prefix@npm:3.0.0" - checksum: 10c0/c4a74294e1b1570f4a8ab435285d185a03976c323caa16359053e749db4fde44e3e6586c29cd051100335e11895767cbbd27ea389108e327d62f38daf4548fdb - languageName: node - linkType: hard - "common-tags@npm:1.8.2": version: 1.8.2 resolution: "common-tags@npm:1.8.2" @@ -23360,7 +22430,7 @@ __metadata: languageName: node linkType: hard -"config-chain@npm:^1.1.11, config-chain@npm:^1.1.13": +"config-chain@npm:^1.1.13": version: 1.1.13 resolution: "config-chain@npm:1.1.13" dependencies: @@ -23384,19 +22454,6 @@ __metadata: languageName: node linkType: hard -"configstore@npm:^6.0.0": - version: 6.0.0 - resolution: "configstore@npm:6.0.0" - dependencies: - dot-prop: "npm:^6.0.1" - graceful-fs: "npm:^4.2.6" - unique-string: "npm:^3.0.0" - write-file-atomic: "npm:^3.0.3" - xdg-basedir: "npm:^5.0.1" - checksum: 10c0/6681a96038ab3e0397cbdf55e6e1624ac3dfa3afe955e219f683df060188a418bda043c9114a59a337e7aec9562b0a0c838ed7db24289e6d0c266bc8313b9580 - languageName: node - linkType: hard - "confusing-browser-globals@npm:^1.0.9": version: 1.0.11 resolution: "confusing-browser-globals@npm:1.0.11" @@ -23404,13 +22461,6 @@ __metadata: languageName: node linkType: hard -"connect-history-api-fallback@npm:^2.0.0": - version: 2.0.0 - resolution: "connect-history-api-fallback@npm:2.0.0" - checksum: 10c0/90fa8b16ab76e9531646cc70b010b1dbd078153730c510d3142f6cf07479ae8a812c5a3c0e40a28528dd1681a62395d0cfdef67da9e914c4772ac85d69a3ed87 - languageName: node - linkType: hard - "connect-injector@npm:^0.4.4": version: 0.4.4 resolution: "connect-injector@npm:0.4.4" @@ -23423,7 +22473,7 @@ __metadata: languageName: node linkType: hard -"consola@npm:^2.15.0, consola@npm:^2.15.3": +"consola@npm:^2.15.0": version: 2.15.3 resolution: "consola@npm:2.15.3" checksum: 10c0/34a337e6b4a1349ee4d7b4c568484344418da8fdb829d7d71bfefcd724f608f273987633b6eef465e8de510929907a092e13cb7a28a5d3acb3be446fcc79fd5e @@ -23437,7 +22487,7 @@ __metadata: languageName: node linkType: hard -"console-browserify@npm:^1.1.0, console-browserify@npm:^1.2.0": +"console-browserify@npm:^1.1.0": version: 1.2.0 resolution: "console-browserify@npm:1.2.0" checksum: 10c0/89b99a53b7d6cee54e1e64fa6b1f7ac24b844b4019c5d39db298637e55c1f4ffa5c165457ad984864de1379df2c8e1886cbbdac85d9dbb6876a9f26c3106f226 @@ -23462,20 +22512,13 @@ __metadata: languageName: node linkType: hard -"constants-browserify@npm:^1.0.0, constants-browserify@npm:~1.0.0": +"constants-browserify@npm:~1.0.0": version: 1.0.0 resolution: "constants-browserify@npm:1.0.0" checksum: 10c0/ab49b1d59a433ed77c964d90d19e08b2f77213fb823da4729c0baead55e3c597f8f97ebccfdfc47bd896d43854a117d114c849a6f659d9986420e97da0f83ac5 languageName: node linkType: hard -"content-disposition@npm:0.5.2": - version: 0.5.2 - resolution: "content-disposition@npm:0.5.2" - checksum: 10c0/49eebaa0da1f9609b192e99d7fec31d1178cb57baa9d01f5b63b29787ac31e9d18b5a1033e854c68c9b6cce790e700a6f7fa60e43f95e2e416404e114a8f2f49 - languageName: node - linkType: hard - "content-disposition@npm:0.5.4, content-disposition@npm:^0.5.4": version: 0.5.4 resolution: "content-disposition@npm:0.5.4" @@ -23550,13 +22593,6 @@ __metadata: languageName: node linkType: hard -"copy-text-to-clipboard@npm:^3.2.0": - version: 3.2.0 - resolution: "copy-text-to-clipboard@npm:3.2.0" - checksum: 10c0/d60fdadc59d526e19d56ad23cec2b292d33c771a5091621bd322d138804edd3c10eb2367d46ec71b39f5f7f7116a2910b332281aeb36a5b679199d746a8a5381 - languageName: node - linkType: hard - "copy-to-clipboard@npm:^3.2.0, copy-to-clipboard@npm:^3.3.1": version: 3.3.3 resolution: "copy-to-clipboard@npm:3.3.3" @@ -23566,22 +22602,6 @@ __metadata: languageName: node linkType: hard -"copy-webpack-plugin@npm:^11.0.0": - version: 11.0.0 - resolution: "copy-webpack-plugin@npm:11.0.0" - dependencies: - fast-glob: "npm:^3.2.11" - glob-parent: "npm:^6.0.1" - globby: "npm:^13.1.1" - normalize-path: "npm:^3.0.0" - schema-utils: "npm:^4.0.0" - serialize-javascript: "npm:^6.0.0" - peerDependencies: - webpack: ^5.1.0 - checksum: 10c0/a667dd226b26f148584a35fb705f5af926d872584912cf9fd203c14f2b3a68f473a1f5cf768ec1dd5da23820823b850e5d50458b685c468e4a224b25c12a15b4 - languageName: node - linkType: hard - "core-js-compat@npm:^3.34.0, core-js-compat@npm:^3.37.1, core-js-compat@npm:^3.38.0": version: 3.38.0 resolution: "core-js-compat@npm:3.38.0" @@ -23605,7 +22625,7 @@ __metadata: languageName: node linkType: hard -"core-js@npm:^3.31.1, core-js@npm:^3.8.2": +"core-js@npm:^3.8.2": version: 3.38.0 resolution: "core-js@npm:3.38.0" checksum: 10c0/3218ae19bfe0c6560663012cbd3e7f3dc1b36d50fc71e8c365f3b119185e8a35ac4e8bb9698ae510b3c201ef93f40bdc29f9215716ccf31aca28f77969bb4ed0 @@ -23655,7 +22675,7 @@ __metadata: languageName: node linkType: hard -"cosmiconfig@npm:8.3.6, cosmiconfig@npm:^8.0.0, cosmiconfig@npm:^8.1.3, cosmiconfig@npm:^8.2.0, cosmiconfig@npm:^8.3.5": +"cosmiconfig@npm:8.3.6, cosmiconfig@npm:^8.0.0, cosmiconfig@npm:^8.1.3, cosmiconfig@npm:^8.2.0": version: 8.3.6 resolution: "cosmiconfig@npm:8.3.6" dependencies: @@ -23887,7 +22907,7 @@ __metadata: languageName: node linkType: hard -"crypto-browserify@npm:^3.0.0, crypto-browserify@npm:^3.12.0": +"crypto-browserify@npm:^3.0.0": version: 3.12.0 resolution: "crypto-browserify@npm:3.12.0" dependencies: @@ -23913,15 +22933,6 @@ __metadata: languageName: node linkType: hard -"crypto-random-string@npm:^4.0.0": - version: 4.0.0 - resolution: "crypto-random-string@npm:4.0.0" - dependencies: - type-fest: "npm:^1.0.1" - checksum: 10c0/16e11a3c8140398f5408b7fded35a961b9423c5dac39a60cbbd08bd3f0e07d7de130e87262adea7db03ec1a7a4b7551054e0db07ee5408b012bac5400cfc07a5 - languageName: node - linkType: hard - "css-box-model@npm:^1.2.1": version: 1.2.1 resolution: "css-box-model@npm:1.2.1" @@ -23931,15 +22942,6 @@ __metadata: languageName: node linkType: hard -"css-declaration-sorter@npm:^7.2.0": - version: 7.2.0 - resolution: "css-declaration-sorter@npm:7.2.0" - peerDependencies: - postcss: ^8.0.9 - checksum: 10c0/d8516be94f8f2daa233ef021688b965c08161624cbf830a4d7ee1099429437c0ee124d35c91b1c659cfd891a68e8888aa941726dab12279bc114aaed60a94606 - languageName: node - linkType: hard - "css-in-js-utils@npm:^3.1.0": version: 3.1.0 resolution: "css-in-js-utils@npm:3.1.0" @@ -23949,9 +22951,9 @@ __metadata: languageName: node linkType: hard -"css-loader@npm:^6.8.1": - version: 6.11.0 - resolution: "css-loader@npm:6.11.0" +"css-loader@npm:^7.1.2": + version: 7.1.2 + resolution: "css-loader@npm:7.1.2" dependencies: icss-utils: "npm:^5.1.0" postcss: "npm:^8.4.33" @@ -23963,13 +22965,13 @@ __metadata: semver: "npm:^7.5.4" peerDependencies: "@rspack/core": 0.x || 1.x - webpack: ^5.0.0 + webpack: ^5.27.0 peerDependenciesMeta: "@rspack/core": optional: true webpack: optional: true - checksum: 10c0/bb52434138085fed06a33e2ffbdae9ee9014ad23bf60f59d6b7ee67f28f26c6b1764024d3030bd19fd884d6ee6ee2224eaed64ad19eb18fbbb23d148d353a965 + checksum: 10c0/edec9ed71e3c416c9c6ad41c138834c94baf7629de3b97a3337ae8cec4a45e05c57bdb7c4b4d267229fc04b8970d0d1c0734ded8dcd0ac8c7c286b36facdbbf0 languageName: node linkType: hard @@ -23980,48 +22982,6 @@ __metadata: languageName: node linkType: hard -"css-minimizer-webpack-plugin@npm:^5.0.1": - version: 5.0.1 - resolution: "css-minimizer-webpack-plugin@npm:5.0.1" - dependencies: - "@jridgewell/trace-mapping": "npm:^0.3.18" - cssnano: "npm:^6.0.1" - jest-worker: "npm:^29.4.3" - postcss: "npm:^8.4.24" - schema-utils: "npm:^4.0.1" - serialize-javascript: "npm:^6.0.1" - peerDependencies: - webpack: ^5.0.0 - peerDependenciesMeta: - "@parcel/css": - optional: true - "@swc/css": - optional: true - clean-css: - optional: true - csso: - optional: true - esbuild: - optional: true - lightningcss: - optional: true - checksum: 10c0/1792259e18f7c5ee25b6bbf60b38b64201747add83d1f751c8c654159b46ebacd0d1103d35f17d97197033e21e02d2ba4a4e9aa14c9c0d067b7c7653c721814e - languageName: node - linkType: hard - -"css-select@npm:^4.1.3": - version: 4.3.0 - resolution: "css-select@npm:4.3.0" - dependencies: - boolbase: "npm:^1.0.0" - css-what: "npm:^6.0.1" - domhandler: "npm:^4.3.1" - domutils: "npm:^2.8.0" - nth-check: "npm:^2.0.1" - checksum: 10c0/a489d8e5628e61063d5a8fe0fa1cc7ae2478cb334a388a354e91cf2908154be97eac9fa7ed4dffe87a3e06cf6fcaa6016553115335c4fd3377e13dac7bd5a8e1 - languageName: node - linkType: hard - "css-select@npm:^5.1.0": version: 5.1.0 resolution: "css-select@npm:5.1.0" @@ -24072,7 +23032,7 @@ __metadata: languageName: node linkType: hard -"css-what@npm:^6.0.1, css-what@npm:^6.1.0": +"css-what@npm:^6.1.0": version: 6.1.0 resolution: "css-what@npm:6.1.0" checksum: 10c0/a09f5a6b14ba8dcf57ae9a59474722e80f20406c53a61e9aedb0eedc693b135113ffe2983f4efc4b5065ae639442e9ae88df24941ef159c218b231011d733746 @@ -24102,84 +23062,6 @@ __metadata: languageName: node linkType: hard -"cssnano-preset-advanced@npm:^6.1.2": - version: 6.1.2 - resolution: "cssnano-preset-advanced@npm:6.1.2" - dependencies: - autoprefixer: "npm:^10.4.19" - browserslist: "npm:^4.23.0" - cssnano-preset-default: "npm:^6.1.2" - postcss-discard-unused: "npm:^6.0.5" - postcss-merge-idents: "npm:^6.0.3" - postcss-reduce-idents: "npm:^6.0.3" - postcss-zindex: "npm:^6.0.2" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/22d3ddab258e6b31e7e2e7c48712f023b60fadb2813929752dace0326e28cd250830b5420a33f81b01df52d2460cb5f999fff5907f58508809efe1a8a739a707 - languageName: node - linkType: hard - -"cssnano-preset-default@npm:^6.1.2": - version: 6.1.2 - resolution: "cssnano-preset-default@npm:6.1.2" - dependencies: - browserslist: "npm:^4.23.0" - css-declaration-sorter: "npm:^7.2.0" - cssnano-utils: "npm:^4.0.2" - postcss-calc: "npm:^9.0.1" - postcss-colormin: "npm:^6.1.0" - postcss-convert-values: "npm:^6.1.0" - postcss-discard-comments: "npm:^6.0.2" - postcss-discard-duplicates: "npm:^6.0.3" - postcss-discard-empty: "npm:^6.0.3" - postcss-discard-overridden: "npm:^6.0.2" - postcss-merge-longhand: "npm:^6.0.5" - postcss-merge-rules: "npm:^6.1.1" - postcss-minify-font-values: "npm:^6.1.0" - postcss-minify-gradients: "npm:^6.0.3" - postcss-minify-params: "npm:^6.1.0" - postcss-minify-selectors: "npm:^6.0.4" - postcss-normalize-charset: "npm:^6.0.2" - postcss-normalize-display-values: "npm:^6.0.2" - postcss-normalize-positions: "npm:^6.0.2" - postcss-normalize-repeat-style: "npm:^6.0.2" - postcss-normalize-string: "npm:^6.0.2" - postcss-normalize-timing-functions: "npm:^6.0.2" - postcss-normalize-unicode: "npm:^6.1.0" - postcss-normalize-url: "npm:^6.0.2" - postcss-normalize-whitespace: "npm:^6.0.2" - postcss-ordered-values: "npm:^6.0.2" - postcss-reduce-initial: "npm:^6.1.0" - postcss-reduce-transforms: "npm:^6.0.2" - postcss-svgo: "npm:^6.0.3" - postcss-unique-selectors: "npm:^6.0.4" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/af99021f936763850f5f35dc9e6a9dfb0da30856dea36e0420b011da2a447099471db2a5f3d1f5f52c0489da186caf9a439d8f048a80f82617077efb018333fa - languageName: node - linkType: hard - -"cssnano-utils@npm:^4.0.2": - version: 4.0.2 - resolution: "cssnano-utils@npm:4.0.2" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/260b8c8ffa48b908aa77ef129f9b8648ecd92aed405b20e7fe6b8370779dd603530344fc9d96683d53533246e48b36ac9d2aa5a476b4f81c547bbad86d187f35 - languageName: node - linkType: hard - -"cssnano@npm:^6.0.1, cssnano@npm:^6.1.2": - version: 6.1.2 - resolution: "cssnano@npm:6.1.2" - dependencies: - cssnano-preset-default: "npm:^6.1.2" - lilconfig: "npm:^3.1.1" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/4df0dc0389b34b38acb09b7cfb07267b0eda95349c6d5e9b7666acc7200bb33359650869a60168e9d878298b05f4ad2c7f070815c90551720a3f4e1037f79691 - languageName: node - linkType: hard - "csso@npm:^5.0.5": version: 5.0.5 resolution: "csso@npm:5.0.5" @@ -24238,7 +23120,7 @@ __metadata: languageName: node linkType: hard -"d3-array@npm:2, d3-array@npm:^2.3.0": +"d3-array@npm:2": version: 2.12.1 resolution: "d3-array@npm:2.12.1" dependencies: @@ -24247,10 +23129,12 @@ __metadata: languageName: node linkType: hard -"d3-color@npm:1 - 2": - version: 2.0.0 - resolution: "d3-color@npm:2.0.0" - checksum: 10c0/5aa58dfb78e3db764373a904eabb643dc024ff6071128a41e86faafa100e0e17a796e06ac3f2662e9937242bb75b8286788629773d76936f11c17bd5fe5e15cd +"d3-array@npm:2 - 3, d3-array@npm:2.10.0 - 3": + version: 3.2.4 + resolution: "d3-array@npm:3.2.4" + dependencies: + internmap: "npm:1 - 2" + checksum: 10c0/08b95e91130f98c1375db0e0af718f4371ccacef7d5d257727fe74f79a24383e79aba280b9ffae655483ffbbad4fd1dec4ade0119d88c4749f388641c8bf8c50 languageName: node linkType: hard @@ -24261,6 +23145,15 @@ __metadata: languageName: node linkType: hard +"d3-delaunay@npm:^6.0.4": + version: 6.0.4 + resolution: "d3-delaunay@npm:6.0.4" + dependencies: + delaunator: "npm:5" + checksum: 10c0/57c3aecd2525664b07c4c292aa11cf49b2752c0cf3f5257f752999399fe3c592de2d418644d79df1f255471eec8057a9cc0c3062ed7128cb3348c45f69597754 + languageName: node + linkType: hard + "d3-dispatch@npm:1 - 3": version: 3.0.1 resolution: "d3-dispatch@npm:3.0.1" @@ -24285,10 +23178,10 @@ __metadata: languageName: node linkType: hard -"d3-format@npm:1 - 2": - version: 2.0.0 - resolution: "d3-format@npm:2.0.0" - checksum: 10c0/c869af459e20767dc3d9cbb2946ba79cc266ae4fb35d11c50c63fc89ea4ed168c702c7e3db94d503b3618de9609bf3bf2d855ef53e21109ddd7eb9c8f3fcf8a1 +"d3-format@npm:1 - 3": + version: 3.1.0 + resolution: "d3-format@npm:3.1.0" + checksum: 10c0/049f5c0871ebce9859fc5e2f07f336b3c5bfff52a2540e0bac7e703fce567cd9346f4ad1079dd18d6f1e0eaa0599941c1810898926f10ac21a31fd0a34b4aa75 languageName: node linkType: hard @@ -24299,16 +23192,7 @@ __metadata: languageName: node linkType: hard -"d3-interpolate@npm:1 - 2, d3-interpolate@npm:1.2.0 - 2": - version: 2.0.1 - resolution: "d3-interpolate@npm:2.0.1" - dependencies: - d3-color: "npm:1 - 2" - checksum: 10c0/2a5725b0c9c7fef3e8878cf75ad67be851b1472de3dda1f694c441786a1a32e198ddfaa6880d6b280401c1af5b844b61ccdd63d85d1607c1e6bb3a3f0bf532ea - languageName: node - linkType: hard - -"d3-interpolate@npm:1 - 3, d3-interpolate@npm:^3.0.1": +"d3-interpolate@npm:1 - 3, d3-interpolate@npm:1.2.0 - 3, d3-interpolate@npm:^3.0.1": version: 3.0.1 resolution: "d3-interpolate@npm:3.0.1" dependencies: @@ -24317,20 +23201,10 @@ __metadata: languageName: node linkType: hard -"d3-path@npm:1": - version: 1.0.9 - resolution: "d3-path@npm:1.0.9" - checksum: 10c0/e35e84df5abc18091f585725b8235e1fa97efc287571585427d3a3597301e6c506dea56b11dfb3c06ca5858b3eb7f02c1bf4f6a716aa9eade01c41b92d497eb5 - languageName: node - linkType: hard - -"d3-scale-chromatic@npm:^2.0.0": - version: 2.0.0 - resolution: "d3-scale-chromatic@npm:2.0.0" - dependencies: - d3-color: "npm:1 - 2" - d3-interpolate: "npm:1 - 2" - checksum: 10c0/93cafe497b00046b1d4e237a8bb8981fbb35ba03070f420bd913872f6e9d2c9628ed8bb8c84c6a6ffe16029359fa74b646c5c5129732ef4186ab059a77da3021 +"d3-path@npm:^3.1.0": + version: 3.1.0 + resolution: "d3-path@npm:3.1.0" + checksum: 10c0/dc1d58ec87fa8319bd240cf7689995111a124b141428354e9637aa83059eb12e681f77187e0ada5dedfce346f7e3d1f903467ceb41b379bfd01cd8e31721f5da languageName: node linkType: hard @@ -24344,16 +23218,16 @@ __metadata: languageName: node linkType: hard -"d3-scale@npm:^3.2.3": - version: 3.3.0 - resolution: "d3-scale@npm:3.3.0" +"d3-scale@npm:^4.0.2": + version: 4.0.2 + resolution: "d3-scale@npm:4.0.2" dependencies: - d3-array: "npm:^2.3.0" - d3-format: "npm:1 - 2" - d3-interpolate: "npm:1.2.0 - 2" - d3-time: "npm:^2.1.1" - d3-time-format: "npm:2 - 3" - checksum: 10c0/cb63c271ec9c5b632c245c63e0d0716b32adcc468247972c552f5be62fb34a17f71e4ac29fd8976704369f4b958bc6789c61a49427efe2160ae979d7843569dc + d3-array: "npm:2.10.0 - 3" + d3-format: "npm:1 - 3" + d3-interpolate: "npm:1.2.0 - 3" + d3-time: "npm:2.1.1 - 3" + d3-time-format: "npm:2 - 4" + checksum: 10c0/65d9ad8c2641aec30ed5673a7410feb187a224d6ca8d1a520d68a7d6eac9d04caedbff4713d1e8545be33eb7fec5739983a7ab1d22d4e5ad35368c6729d362f1 languageName: node linkType: hard @@ -24364,16 +23238,25 @@ __metadata: languageName: node linkType: hard -"d3-shape@npm:^1.3.5": - version: 1.3.7 - resolution: "d3-shape@npm:1.3.7" +"d3-shape@npm:^3.2.0": + version: 3.2.0 + resolution: "d3-shape@npm:3.2.0" + dependencies: + d3-path: "npm:^3.1.0" + checksum: 10c0/f1c9d1f09926daaf6f6193ae3b4c4b5521e81da7d8902d24b38694517c7f527ce3c9a77a9d3a5722ad1e3ff355860b014557b450023d66a944eabf8cfde37132 + languageName: node + linkType: hard + +"d3-time-format@npm:2 - 4": + version: 4.1.0 + resolution: "d3-time-format@npm:4.1.0" dependencies: - d3-path: "npm:1" - checksum: 10c0/548057ce59959815decb449f15632b08e2a1bdce208f9a37b5f98ec7629dda986c2356bc7582308405ce68aedae7d47b324df41507404df42afaf352907577ae + d3-time: "npm:1 - 3" + checksum: 10c0/735e00fb25a7fd5d418fac350018713ae394eefddb0d745fab12bbff0517f9cdb5f807c7bbe87bb6eeb06249662f8ea84fec075f7d0cd68609735b2ceb29d206 languageName: node linkType: hard -"d3-time-format@npm:2 - 3, d3-time-format@npm:^3.0.0": +"d3-time-format@npm:^3.0.0": version: 3.0.0 resolution: "d3-time-format@npm:3.0.0" dependencies: @@ -24382,7 +23265,7 @@ __metadata: languageName: node linkType: hard -"d3-time@npm:1 - 2, d3-time@npm:^2.1.1": +"d3-time@npm:1 - 2": version: 2.1.1 resolution: "d3-time@npm:2.1.1" dependencies: @@ -24391,7 +23274,16 @@ __metadata: languageName: node linkType: hard -"d3-time@npm:^1.0.10": +"d3-time@npm:1 - 3, d3-time@npm:2.1.1 - 3": + version: 3.1.0 + resolution: "d3-time@npm:3.1.0" + dependencies: + d3-array: "npm:2 - 3" + checksum: 10c0/a984f77e1aaeaa182679b46fbf57eceb6ebdb5f67d7578d6f68ef933f8eeb63737c0949991618a8d29472dbf43736c7d7f17c452b2770f8c1271191cba724ca1 + languageName: node + linkType: hard + +"d3-time@npm:^1.0.10, d3-time@npm:^1.0.11": version: 1.1.0 resolution: "d3-time@npm:1.1.0" checksum: 10c0/69ab137adff5b22d0fa148ea514a207bd9cd7d2c042ccf34a268f2ef73720b404f0be6e7b56c95650c53caf52080b5254e2a27f0a676f41d1dd22ef8872c8335 @@ -24660,7 +23552,7 @@ __metadata: languageName: node linkType: hard -"debounce@npm:^1.2.0, debounce@npm:^1.2.1": +"debounce@npm:^1.2.0": version: 1.2.1 resolution: "debounce@npm:1.2.1" checksum: 10c0/6c9320aa0973fc42050814621a7a8a78146c1975799b5b3cc1becf1f77ba9a5aa583987884230da0842a03f385def452fad5d60db97c3d1c8b824e38a8edf500 @@ -24674,7 +23566,7 @@ __metadata: languageName: node linkType: hard -"debug@npm:2.6.9, debug@npm:^2.0.0, debug@npm:^2.6.0, debug@npm:^2.6.8, debug@npm:^2.6.9": +"debug@npm:2.6.9, debug@npm:^2.0.0, debug@npm:^2.6.8, debug@npm:^2.6.9": version: 2.6.9 resolution: "debug@npm:2.6.9" dependencies: @@ -24863,15 +23755,6 @@ __metadata: languageName: node linkType: hard -"default-gateway@npm:^6.0.3": - version: 6.0.3 - resolution: "default-gateway@npm:6.0.3" - dependencies: - execa: "npm:^5.0.0" - checksum: 10c0/5184f9e6e105d24fb44ade9e8741efa54bb75e84625c1ea78c4ef8b81dff09ca52d6dbdd1185cf0dc655bb6b282a64fffaf7ed2dd561b8d9ad6f322b1f039aba - languageName: node - linkType: hard - "default-require-extensions@npm:^3.0.0": version: 3.0.1 resolution: "default-require-extensions@npm:3.0.1" @@ -24897,7 +23780,7 @@ __metadata: languageName: node linkType: hard -"defer-to-connect@npm:^2.0.0, defer-to-connect@npm:^2.0.1": +"defer-to-connect@npm:^2.0.0": version: 2.0.1 resolution: "defer-to-connect@npm:2.0.1" checksum: 10c0/625ce28e1b5ad10cf77057b9a6a727bf84780c17660f6644dab61dd34c23de3001f03cedc401f7d30a4ed9965c2e8a7336e220a329146f2cf85d4eddea429782 @@ -24947,7 +23830,7 @@ __metadata: languageName: node linkType: hard -"del@npm:^6.0.0, del@npm:^6.1.1": +"del@npm:^6.0.0": version: 6.1.1 resolution: "del@npm:6.1.1" dependencies: @@ -24963,6 +23846,15 @@ __metadata: languageName: node linkType: hard +"delaunator@npm:5": + version: 5.0.1 + resolution: "delaunator@npm:5.0.1" + dependencies: + robust-predicates: "npm:^3.0.2" + checksum: 10c0/3d7ea4d964731c5849af33fec0a271bc6753487b331fd7d43ccb17d77834706e1c383e6ab8fda0032da955e7576d1083b9603cdaf9cbdfd6b3ebd1fb8bb675a5 + languageName: node + linkType: hard + "delay@npm:^5.0.0": version: 5.0.0 resolution: "delay@npm:5.0.0" @@ -25119,19 +24011,6 @@ __metadata: languageName: node linkType: hard -"detect-port-alt@npm:^1.1.6": - version: 1.1.6 - resolution: "detect-port-alt@npm:1.1.6" - dependencies: - address: "npm:^1.0.1" - debug: "npm:^2.6.0" - bin: - detect: ./bin/detect-port - detect-port: ./bin/detect-port - checksum: 10c0/7269e6aef7b782d98c77505c07a7a0f5e2ee98a9607dc791035fc0192fc58aa03cc833fae605e10eaf239a2a5a55cd938e0bb141dea764ac6180ca082fd62b23 - languageName: node - linkType: hard - "detect-port@npm:^1.3.0, detect-port@npm:^1.5.1": version: 1.6.1 resolution: "detect-port@npm:1.6.1" @@ -25158,15 +24037,6 @@ __metadata: languageName: node linkType: hard -"devlop@npm:^1.0.0, devlop@npm:^1.1.0": - version: 1.1.0 - resolution: "devlop@npm:1.1.0" - dependencies: - dequal: "npm:^2.0.0" - checksum: 10c0/e0928ab8f94c59417a2b8389c45c55ce0a02d9ac7fd74ef62d01ba48060129e1d594501b77de01f3eeafc7cb00773819b0df74d96251cf20b31c5b3071f45c0e - languageName: node - linkType: hard - "dezalgo@npm:^1.0.0, dezalgo@npm:^1.0.4": version: 1.0.4 resolution: "dezalgo@npm:1.0.4" @@ -25252,15 +24122,6 @@ __metadata: languageName: node linkType: hard -"dns-packet@npm:^5.2.2": - version: 5.6.1 - resolution: "dns-packet@npm:5.6.1" - dependencies: - "@leichtgewicht/ip-codec": "npm:^2.0.1" - checksum: 10c0/8948d3d03063fb68e04a1e386875f8c3bcc398fc375f535f2b438fad8f41bf1afa6f5e70893ba44f4ae884c089247e0a31045722fa6ff0f01d228da103f1811d - languageName: node - linkType: hard - "doc-path@npm:4.1.1": version: 4.1.1 resolution: "doc-path@npm:4.1.1" @@ -25286,19 +24147,6 @@ __metadata: languageName: node linkType: hard -"docusaurus-node-polyfills@npm:^1.0.0": - version: 1.0.0 - resolution: "docusaurus-node-polyfills@npm:1.0.0" - dependencies: - node-polyfill-webpack-plugin: "npm:^1.1.2" - os-browserify: "npm:^0.3.0" - process: "npm:^0.11.10" - peerDependencies: - webpack: ">=5" - checksum: 10c0/f25f5a18d79b192252fada137b337195f4b1ed5f97959b17061276fa36147871239c00c1d3c260b0822b391617fa63daed73133565ceecee9992a340db8cdeb1 - languageName: node - linkType: hard - "dom-accessibility-api@npm:^0.5.9": version: 0.5.16 resolution: "dom-accessibility-api@npm:0.5.16" @@ -25313,15 +24161,6 @@ __metadata: languageName: node linkType: hard -"dom-converter@npm:^0.2.0": - version: 0.2.0 - resolution: "dom-converter@npm:0.2.0" - dependencies: - utila: "npm:~0.4" - checksum: 10c0/e96aa63bd8c6ee3cd9ce19c3aecfc2c42e50a460e8087114794d4f5ecf3a4f052b34ea3bf2d73b5d80b4da619073b49905e6d7d788ceb7814ca4c29be5354a11 - languageName: node - linkType: hard - "dom-helpers@npm:^3.3.1": version: 3.4.0 resolution: "dom-helpers@npm:3.4.0" @@ -25341,17 +24180,6 @@ __metadata: languageName: node linkType: hard -"dom-serializer@npm:^1.0.1": - version: 1.4.1 - resolution: "dom-serializer@npm:1.4.1" - dependencies: - domelementtype: "npm:^2.0.1" - domhandler: "npm:^4.2.0" - entities: "npm:^2.0.0" - checksum: 10c0/67d775fa1ea3de52035c98168ddcd59418356943b5eccb80e3c8b3da53adb8e37edb2cc2f885802b7b1765bf5022aec21dfc32910d7f9e6de4c3148f095ab5e0 - languageName: node - linkType: hard - "dom-serializer@npm:^2.0.0": version: 2.0.0 resolution: "dom-serializer@npm:2.0.0" @@ -25370,13 +24198,6 @@ __metadata: languageName: node linkType: hard -"domain-browser@npm:^4.19.0": - version: 4.23.0 - resolution: "domain-browser@npm:4.23.0" - checksum: 10c0/dfcc6ba070a2c968a4d922e7d99ef440d1076812af0d983404aadf64729f746bb4a0ad2c5e73ccd5d9cf41bc79037f2a1e4a915bdf33d07e0d77f487b635b5b2 - languageName: node - linkType: hard - "domelementtype@npm:1, domelementtype@npm:^1.3.1": version: 1.3.1 resolution: "domelementtype@npm:1.3.1" @@ -25384,7 +24205,7 @@ __metadata: languageName: node linkType: hard -"domelementtype@npm:^2.0.1, domelementtype@npm:^2.2.0, domelementtype@npm:^2.3.0": +"domelementtype@npm:^2.0.1, domelementtype@npm:^2.3.0": version: 2.3.0 resolution: "domelementtype@npm:2.3.0" checksum: 10c0/686f5a9ef0fff078c1412c05db73a0dce096190036f33e400a07e2a4518e9f56b1e324f5c576a0a747ef0e75b5d985c040b0d51945ce780c0dd3c625a18cd8c9 @@ -25409,15 +24230,6 @@ __metadata: languageName: node linkType: hard -"domhandler@npm:^4.0.0, domhandler@npm:^4.2.0, domhandler@npm:^4.3.1": - version: 4.3.1 - resolution: "domhandler@npm:4.3.1" - dependencies: - domelementtype: "npm:^2.2.0" - checksum: 10c0/5c199c7468cb052a8b5ab80b13528f0db3d794c64fc050ba793b574e158e67c93f8336e87fd81e9d5ee43b0e04aea4d8b93ed7be4899cb726a1601b3ba18538b - languageName: node - linkType: hard - "domhandler@npm:^5.0.2, domhandler@npm:^5.0.3": version: 5.0.3 resolution: "domhandler@npm:5.0.3" @@ -25444,17 +24256,6 @@ __metadata: languageName: node linkType: hard -"domutils@npm:^2.5.2, domutils@npm:^2.8.0": - version: 2.8.0 - resolution: "domutils@npm:2.8.0" - dependencies: - dom-serializer: "npm:^1.0.1" - domelementtype: "npm:^2.2.0" - domhandler: "npm:^4.2.0" - checksum: 10c0/d58e2ae01922f0dd55894e61d18119924d88091837887bf1438f2327f32c65eb76426bd9384f81e7d6dcfb048e0f83c19b222ad7101176ad68cdc9c695b563db - languageName: node - linkType: hard - "domutils@npm:^3.0.1": version: 3.1.0 resolution: "domutils@npm:3.1.0" @@ -25485,15 +24286,6 @@ __metadata: languageName: node linkType: hard -"dot-prop@npm:^6.0.1": - version: 6.0.1 - resolution: "dot-prop@npm:6.0.1" - dependencies: - is-obj: "npm:^2.0.0" - checksum: 10c0/30e51ec6408978a6951b21e7bc4938aad01a86f2fdf779efe52330205c6bb8a8ea12f35925c2029d6dc9d1df22f916f32f828ce1e9b259b1371c580541c22b5a - languageName: node - linkType: hard - "dotenv-cli@npm:^7.2.1": version: 7.4.2 resolution: "dotenv-cli@npm:7.4.2" @@ -25677,7 +24469,7 @@ __metadata: languageName: node linkType: hard -"duplexer@npm:^0.1.1, duplexer@npm:^0.1.2": +"duplexer@npm:^0.1.1": version: 0.1.2 resolution: "duplexer@npm:0.1.2" checksum: 10c0/c57bcd4bdf7e623abab2df43a7b5b23d18152154529d166c1e0da6bee341d84c432d157d7e97b32fecb1bf3a8b8857dd85ed81a915789f550637ed25b8e64fc2 @@ -25804,13 +24596,6 @@ __metadata: languageName: node linkType: hard -"emojilib@npm:^2.4.0": - version: 2.4.0 - resolution: "emojilib@npm:2.4.0" - checksum: 10c0/6e66ba8921175842193f974e18af448bb6adb0cf7aeea75e08b9d4ea8e9baba0e4a5347b46ed901491dcaba277485891c33a8d70b0560ca5cc9672a94c21ab8f - languageName: node - linkType: hard - "emojis-list@npm:^3.0.0": version: 3.0.0 resolution: "emojis-list@npm:3.0.0" @@ -25818,13 +24603,6 @@ __metadata: languageName: node linkType: hard -"emoticon@npm:^4.0.1": - version: 4.1.0 - resolution: "emoticon@npm:4.1.0" - checksum: 10c0/b3bc0a9b370445ac1e980ccba7baea614b4648199cc6fa0a51696a6d2393733e8f985edc4f1af381a1903f625789483dd155de427ec9fa2ea415fac116adc06d - languageName: node - linkType: hard - "encodeurl@npm:~1.0.2": version: 1.0.2 resolution: "encodeurl@npm:1.0.2" @@ -25850,7 +24628,7 @@ __metadata: languageName: node linkType: hard -"enhanced-resolve@npm:^5.0.0, enhanced-resolve@npm:^5.12.0, enhanced-resolve@npm:^5.14.0, enhanced-resolve@npm:^5.15.0, enhanced-resolve@npm:^5.17.0, enhanced-resolve@npm:^5.7.0": +"enhanced-resolve@npm:^5.0.0, enhanced-resolve@npm:^5.12.0, enhanced-resolve@npm:^5.14.0, enhanced-resolve@npm:^5.15.0, enhanced-resolve@npm:^5.7.0": version: 5.17.1 resolution: "enhanced-resolve@npm:5.17.1" dependencies: @@ -26545,14 +25323,7 @@ __metadata: languageName: node linkType: hard -"escape-goat@npm:^4.0.0": - version: 4.0.0 - resolution: "escape-goat@npm:4.0.0" - checksum: 10c0/9d2a8314e2370f2dd9436d177f6b3b1773525df8f895c8f3e1acb716f5fd6b10b336cb1cd9862d4709b36eb207dbe33664838deca9c6d55b8371be4eebb972f6 - 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 @@ -27037,15 +25808,6 @@ __metadata: languageName: node linkType: hard -"estree-util-attach-comments@npm:^3.0.0": - version: 3.0.0 - resolution: "estree-util-attach-comments@npm:3.0.0" - dependencies: - "@types/estree": "npm:^1.0.0" - checksum: 10c0/ee69bb5c45e2ad074725b90ed181c1c934b29d81bce4b0c7761431e83c4c6ab1b223a6a3d6a4fbeb92128bc5d5ee201d5dd36cf1770aa5e16a40b0cf36e8a1f1 - languageName: node - linkType: hard - "estree-util-build-jsx@npm:^2.0.0": version: 2.2.2 resolution: "estree-util-build-jsx@npm:2.2.2" @@ -27057,18 +25819,6 @@ __metadata: languageName: node linkType: hard -"estree-util-build-jsx@npm:^3.0.0": - version: 3.0.1 - resolution: "estree-util-build-jsx@npm:3.0.1" - dependencies: - "@types/estree-jsx": "npm:^1.0.0" - devlop: "npm:^1.0.0" - estree-util-is-identifier-name: "npm:^3.0.0" - estree-walker: "npm:^3.0.0" - checksum: 10c0/274c119817b8e7caa14a9778f1e497fea56cdd2b01df1a1ed037f843178992d3afe85e0d364d485e1e2e239255763553d1b647b15e4a7ba50851bcb43dc6bf80 - languageName: node - linkType: hard - "estree-util-is-identifier-name@npm:^2.0.0": version: 2.1.0 resolution: "estree-util-is-identifier-name@npm:2.1.0" @@ -27076,13 +25826,6 @@ __metadata: languageName: node linkType: hard -"estree-util-is-identifier-name@npm:^3.0.0": - version: 3.0.0 - resolution: "estree-util-is-identifier-name@npm:3.0.0" - checksum: 10c0/d1881c6ed14bd588ebd508fc90bf2a541811dbb9ca04dec2f39d27dcaa635f85b5ed9bbbe7fc6fb1ddfca68744a5f7c70456b4b7108b6c4c52780631cc787c5b - languageName: node - linkType: hard - "estree-util-to-js@npm:^1.1.0": version: 1.2.0 resolution: "estree-util-to-js@npm:1.2.0" @@ -27094,26 +25837,6 @@ __metadata: languageName: node linkType: hard -"estree-util-to-js@npm:^2.0.0": - version: 2.0.0 - resolution: "estree-util-to-js@npm:2.0.0" - dependencies: - "@types/estree-jsx": "npm:^1.0.0" - astring: "npm:^1.8.0" - source-map: "npm:^0.7.0" - checksum: 10c0/ac88cb831401ef99e365f92f4af903755d56ae1ce0e0f0fb8ff66e678141f3d529194f0fb15f6c78cd7554c16fda36854df851d58f9e05cfab15bddf7a97cea0 - languageName: node - linkType: hard - -"estree-util-value-to-estree@npm:^3.0.1": - version: 3.1.2 - resolution: "estree-util-value-to-estree@npm:3.1.2" - dependencies: - "@types/estree": "npm:^1.0.0" - checksum: 10c0/fb0fa42f44488eeb2357b60dc3fd5581422b0a36144fd90639fd3963c7396f225e7d7efeee0144b0a7293ea00e4ec9647b8302d057d48f894e8d5775c3c72eb7 - languageName: node - linkType: hard - "estree-util-visit@npm:^1.0.0": version: 1.2.1 resolution: "estree-util-visit@npm:1.2.1" @@ -27124,16 +25847,6 @@ __metadata: languageName: node linkType: hard -"estree-util-visit@npm:^2.0.0": - version: 2.0.0 - resolution: "estree-util-visit@npm:2.0.0" - dependencies: - "@types/estree-jsx": "npm:^1.0.0" - "@types/unist": "npm:^3.0.0" - checksum: 10c0/acda8b03cc8f890d79c7c7361f6c95331ba84b7ccc0c32b49f447fc30206b20002b37ffdfc97b6ad16e6fe065c63ecbae1622492e2b6b4775c15966606217f39 - languageName: node - linkType: hard - "estree-walker@npm:^0.6.1": version: 0.6.1 resolution: "estree-walker@npm:0.6.1" @@ -27164,13 +25877,6 @@ __metadata: languageName: node linkType: hard -"eta@npm:^2.2.0": - version: 2.2.0 - resolution: "eta@npm:2.2.0" - checksum: 10c0/643b54d9539d2761bf6c5f4f48df1a5ea2d46c7f5a5fdc47a7d4802a8aa2b6262d4d61f724452e226c18cf82db02d48e65293fcc548f26a3f9d75a5ba7c3b859 - languageName: node - linkType: hard - "etag@npm:~1.8.1": version: 1.8.1 resolution: "etag@npm:1.8.1" @@ -27178,16 +25884,6 @@ __metadata: languageName: node linkType: hard -"eval@npm:^0.1.8": - version: 0.1.8 - resolution: "eval@npm:0.1.8" - dependencies: - "@types/node": "npm:*" - require-like: "npm:>= 0.1.1" - checksum: 10c0/258e700bff09e3ce3344273d5b6691b8ec5b043538d84f738f14d8b0aded33d64c00c15b380de725b1401b15f428ab35a9e7ca19a7d25f162c4f877c71586be9 - languageName: node - linkType: hard - "event-emitter@npm:^0.3.5": version: 0.3.5 resolution: "event-emitter@npm:0.3.5" @@ -27632,7 +26328,7 @@ __metadata: languageName: node linkType: hard -"fast-glob@npm:3.3.2, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.5, fast-glob@npm:^3.2.7, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.0, fast-glob@npm:^3.3.1, fast-glob@npm:^3.3.2": +"fast-glob@npm:3.3.2, fast-glob@npm:^3.2.11, fast-glob@npm:^3.2.5, fast-glob@npm:^3.2.7, fast-glob@npm:^3.2.9, fast-glob@npm:^3.3.1, fast-glob@npm:^3.3.2": version: 3.3.2 resolution: "fast-glob@npm:3.3.2" dependencies: @@ -27703,7 +26399,7 @@ __metadata: languageName: node linkType: hard -"fast-url-parser@npm:1.1.3, fast-url-parser@npm:^1.1.3": +"fast-url-parser@npm:^1.1.3": version: 1.1.3 resolution: "fast-url-parser@npm:1.1.3" dependencies: @@ -27755,24 +26451,6 @@ __metadata: languageName: node linkType: hard -"fault@npm:^2.0.0": - version: 2.0.1 - resolution: "fault@npm:2.0.1" - dependencies: - format: "npm:^0.2.0" - checksum: 10c0/b80fbf1019b9ce8b08ee09ce86e02b028563e13a32ac3be34e42bfac00a97b96d8dee6d31e26578ffc16224eb6729e01ff1f97ddfeee00494f4f56c0aeed4bdd - languageName: node - linkType: hard - -"faye-websocket@npm:^0.11.3": - version: 0.11.4 - resolution: "faye-websocket@npm:0.11.4" - dependencies: - websocket-driver: "npm:>=0.5.1" - checksum: 10c0/c6052a0bb322778ce9f89af92890f6f4ce00d5ec92418a35e5f4c6864a4fe736fec0bcebd47eac7c0f0e979b01530746b1c85c83cb04bae789271abf19737420 - languageName: node - linkType: hard - "fb-watchman@npm:^2.0.0": version: 2.0.2 resolution: "fb-watchman@npm:2.0.2" @@ -27813,15 +26491,6 @@ __metadata: languageName: node linkType: hard -"feed@npm:^4.2.2": - version: 4.2.2 - resolution: "feed@npm:4.2.2" - dependencies: - xml-js: "npm:^1.6.11" - checksum: 10c0/c0849bde569da94493224525db00614fd1855a5d7c2e990f6e8637bd0298e85c3d329efe476cba77e711e438c3fb48af60cd5ef0c409da5bcd1f479790b0a372 - languageName: node - linkType: hard - "fetch-retry@npm:^5.0.2": version: 5.0.6 resolution: "fetch-retry@npm:5.0.6" @@ -27965,13 +26634,6 @@ __metadata: languageName: node linkType: hard -"filesize@npm:^8.0.6": - version: 8.0.7 - resolution: "filesize@npm:8.0.7" - checksum: 10c0/82072d94816484df5365d4d5acbb2327a65dc49704c64e403e8c40d8acb7364de1cf1e65cb512c77a15d353870f73e4fed46dad5c6153d0618d9ce7a64d09cfc - languageName: node - linkType: hard - "fill-range@npm:^7.1.1": version: 7.1.1 resolution: "fill-range@npm:7.1.1" @@ -27988,13 +26650,6 @@ __metadata: languageName: node linkType: hard -"filter-obj@npm:^2.0.2": - version: 2.0.2 - resolution: "filter-obj@npm:2.0.2" - checksum: 10c0/65899fb1151e16d3289c23e7d6c2a9276592de1e16ab8e14c29d225768273ac48a92d3be4182496a16d89a046cf24ebcbecef7fdac8c27c3c29feafc4fb9fdb3 - languageName: node - linkType: hard - "finalhandler@npm:1.2.0": version: 1.2.0 resolution: "finalhandler@npm:1.2.0" @@ -28032,16 +26687,6 @@ __metadata: languageName: node linkType: hard -"find-cache-dir@npm:^4.0.0": - version: 4.0.0 - resolution: "find-cache-dir@npm:4.0.0" - dependencies: - common-path-prefix: "npm:^3.0.0" - pkg-dir: "npm:^7.0.0" - checksum: 10c0/0faa7956974726c8769671de696d24c643ca1e5b8f7a2401283caa9e07a5da093293e0a0f4bd18c920ec981d2ef945c7f5b946cde268dfc9077d833ad0293cff - languageName: node - linkType: hard - "find-file-up@npm:^0.1.2": version: 0.1.3 resolution: "find-file-up@npm:0.1.3" @@ -28110,16 +26755,6 @@ __metadata: languageName: node linkType: hard -"find-up@npm:^6.3.0": - version: 6.3.0 - resolution: "find-up@npm:6.3.0" - dependencies: - locate-path: "npm:^7.1.0" - path-exists: "npm:^5.0.0" - checksum: 10c0/07e0314362d316b2b13f7f11ea4692d5191e718ca3f7264110127520f3347996349bf9e16805abae3e196805814bc66ef4bff2b8904dc4a6476085fc9b0eba07 - languageName: node - linkType: hard - "find-versions@npm:^5.0.0": version: 5.1.0 resolution: "find-versions@npm:5.1.0" @@ -28297,37 +26932,6 @@ __metadata: languageName: node linkType: hard -"fork-ts-checker-webpack-plugin@npm:^6.5.0": - version: 6.5.3 - resolution: "fork-ts-checker-webpack-plugin@npm:6.5.3" - dependencies: - "@babel/code-frame": "npm:^7.8.3" - "@types/json-schema": "npm:^7.0.5" - chalk: "npm:^4.1.0" - chokidar: "npm:^3.4.2" - cosmiconfig: "npm:^6.0.0" - deepmerge: "npm:^4.2.2" - fs-extra: "npm:^9.0.0" - glob: "npm:^7.1.6" - memfs: "npm:^3.1.2" - minimatch: "npm:^3.0.4" - schema-utils: "npm:2.7.0" - semver: "npm:^7.3.2" - tapable: "npm:^1.0.0" - peerDependencies: - eslint: ">= 6" - typescript: ">= 2.7" - vue-template-compiler: "*" - webpack: ">= 4" - peerDependenciesMeta: - eslint: - optional: true - vue-template-compiler: - optional: true - checksum: 10c0/0885ea75474de011d4068ca3e2d3ca6e4cd318f5cfa018e28ff8fef23ef3a1f1c130160ef192d3e5d31ef7b6fe9f8fb1d920eab5e9e449fb30ce5cc96647245c - languageName: node - linkType: hard - "form-data-encoder@npm:1.7.2": version: 1.7.2 resolution: "form-data-encoder@npm:1.7.2" @@ -28335,13 +26939,6 @@ __metadata: languageName: node linkType: hard -"form-data-encoder@npm:^2.1.2": - version: 2.1.4 - resolution: "form-data-encoder@npm:2.1.4" - checksum: 10c0/4c06ae2b79ad693a59938dc49ebd020ecb58e4584860a90a230f80a68b026483b022ba5e4143cff06ae5ac8fd446a0b500fabc87bbac3d1f62f2757f8dabcaf7 - languageName: node - linkType: hard - "form-data@npm:4.0.0, form-data@npm:^4.0.0": version: 4.0.0 resolution: "form-data@npm:4.0.0" @@ -28400,13 +26997,6 @@ __metadata: languageName: node linkType: hard -"fraction.js@npm:^4.3.7": - version: 4.3.7 - resolution: "fraction.js@npm:4.3.7" - checksum: 10c0/df291391beea9ab4c263487ffd9d17fed162dbb736982dee1379b2a8cc94e4e24e46ed508c6d278aded9080ba51872f1bc5f3a5fd8d7c74e5f105b508ac28711 - languageName: node - linkType: hard - "framer-motion@npm:^10.12.17": version: 10.18.0 resolution: "framer-motion@npm:10.18.0" @@ -28522,7 +27112,7 @@ __metadata: languageName: node linkType: hard -"fs-extra@npm:^11.1.0, fs-extra@npm:^11.1.1, fs-extra@npm:^11.2.0": +"fs-extra@npm:^11.1.0, fs-extra@npm:^11.2.0": version: 11.2.0 resolution: "fs-extra@npm:11.2.0" dependencies: @@ -28849,7 +27439,7 @@ __metadata: languageName: node linkType: hard -"get-stream@npm:^6.0.0, get-stream@npm:^6.0.1": +"get-stream@npm:^6.0.0": version: 6.0.1 resolution: "get-stream@npm:6.0.1" checksum: 10c0/49825d57d3fd6964228e6200a58169464b8e8970489b3acdc24906c782fb7f01f9f56f8e6653c4a50713771d6658f7cfe051e5eb8c12e334138c9c918b296341 @@ -28928,7 +27518,7 @@ __metadata: languageName: node linkType: hard -"github-slugger@npm:^1.0.0, github-slugger@npm:^1.3.0, github-slugger@npm:^1.5.0": +"github-slugger@npm:^1.0.0, github-slugger@npm:^1.3.0": version: 1.5.0 resolution: "github-slugger@npm:1.5.0" checksum: 10c0/116f99732925f939cbfd6f2e57db1aa7e111a460db0d103e3b3f2fce6909d44311663d4542350706cad806345b9892358cc3b153674f88eeae77f43380b3bfca @@ -28960,7 +27550,7 @@ __metadata: languageName: node linkType: hard -"glob-parent@npm:^6.0.1, glob-parent@npm:^6.0.2": +"glob-parent@npm:^6.0.2": version: 6.0.2 resolution: "glob-parent@npm:6.0.2" dependencies: @@ -29090,15 +27680,6 @@ __metadata: languageName: node linkType: hard -"global-modules@npm:^2.0.0": - version: 2.0.0 - resolution: "global-modules@npm:2.0.0" - dependencies: - global-prefix: "npm:^3.0.0" - checksum: 10c0/43b770fe24aa6028f4b9770ea583a47f39750be15cf6e2578f851e4ccc9e4fa674b8541928c0b09c21461ca0763f0d36e4068cec86c914b07fd6e388e66ba5b9 - languageName: node - linkType: hard - "global-prefix@npm:^0.1.4": version: 0.1.5 resolution: "global-prefix@npm:0.1.5" @@ -29111,17 +27692,6 @@ __metadata: languageName: node linkType: hard -"global-prefix@npm:^3.0.0": - version: 3.0.0 - resolution: "global-prefix@npm:3.0.0" - dependencies: - ini: "npm:^1.3.5" - kind-of: "npm:^6.0.2" - which: "npm:^1.3.1" - checksum: 10c0/510f489fb68d1cc7060f276541709a0ee6d41356ef852de48f7906c648ac223082a1cc8fce86725ca6c0e032bcdc1189ae77b4744a624b29c34a9d0ece498269 - languageName: node - linkType: hard - "globals@npm:^11.1.0": version: 11.12.0 resolution: "globals@npm:11.12.0" @@ -29155,7 +27725,7 @@ __metadata: languageName: node linkType: hard -"globby@npm:^11.0.1, globby@npm:^11.0.2, globby@npm:^11.0.3, globby@npm:^11.0.4, globby@npm:^11.1.0": +"globby@npm:^11.0.1, globby@npm:^11.0.2, globby@npm:^11.0.3, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" dependencies: @@ -29169,19 +27739,6 @@ __metadata: languageName: node linkType: hard -"globby@npm:^13.1.1": - version: 13.2.2 - resolution: "globby@npm:13.2.2" - dependencies: - dir-glob: "npm:^3.0.1" - fast-glob: "npm:^3.3.0" - ignore: "npm:^5.2.4" - merge2: "npm:^1.4.1" - slash: "npm:^4.0.0" - checksum: 10c0/a8d7cc7cbe5e1b2d0f81d467bbc5bc2eac35f74eaded3a6c85fc26d7acc8e6de22d396159db8a2fc340b8a342e74cac58de8f4aee74146d3d146921a76062664 - languageName: node - linkType: hard - "globrex@npm:^0.1.2": version: 0.1.2 resolution: "globrex@npm:0.1.2" @@ -29269,25 +27826,6 @@ __metadata: languageName: node linkType: hard -"got@npm:^12.1.0": - version: 12.6.1 - resolution: "got@npm:12.6.1" - dependencies: - "@sindresorhus/is": "npm:^5.2.0" - "@szmarczak/http-timer": "npm:^5.0.1" - cacheable-lookup: "npm:^7.0.0" - cacheable-request: "npm:^10.2.8" - decompress-response: "npm:^6.0.0" - form-data-encoder: "npm:^2.1.2" - get-stream: "npm:^6.0.1" - http2-wrapper: "npm:^2.1.10" - lowercase-keys: "npm:^3.0.0" - p-cancelable: "npm:^3.0.0" - responselike: "npm:^3.0.0" - checksum: 10c0/2fe97fcbd7a9ffc7c2d0ecf59aca0a0562e73a7749cadada9770eeb18efbdca3086262625fb65590594edc220a1eca58fab0d26b0c93c2f9a008234da71ca66b - languageName: node - linkType: hard - "got@npm:^9.6.0": version: 9.6.0 resolution: "got@npm:9.6.0" @@ -29307,13 +27845,6 @@ __metadata: languageName: node linkType: hard -"graceful-fs@npm:4.2.10": - version: 4.2.10 - resolution: "graceful-fs@npm:4.2.10" - checksum: 10c0/4223a833e38e1d0d2aea630c2433cfb94ddc07dfc11d511dbd6be1d16688c5be848acc31f9a5d0d0ddbfb56d2ee5a6ae0278aceeb0ca6a13f27e06b9956fb952 - languageName: node - linkType: hard - "graceful-fs@npm:^4.1.11, graceful-fs@npm:^4.1.15, graceful-fs@npm:^4.1.2, graceful-fs@npm:^4.1.5, graceful-fs@npm:^4.1.6, graceful-fs@npm:^4.2.0, graceful-fs@npm:^4.2.11, graceful-fs@npm:^4.2.2, graceful-fs@npm:^4.2.3, graceful-fs@npm:^4.2.4, graceful-fs@npm:^4.2.6, graceful-fs@npm:^4.2.9": version: 4.2.11 resolution: "graceful-fs@npm:4.2.11" @@ -29631,15 +28162,6 @@ __metadata: languageName: node linkType: hard -"gzip-size@npm:^6.0.0": - version: 6.0.0 - resolution: "gzip-size@npm:6.0.0" - dependencies: - duplexer: "npm:^0.1.2" - checksum: 10c0/4ccb924626c82125897a997d1c84f2377846a6ef57fbee38f7c0e6b41387fba4d00422274440747b58008b5d60114bac2349c2908e9aba55188345281af40a3f - languageName: node - linkType: hard - "hamt_plus@npm:1.0.2": version: 1.0.2 resolution: "hamt_plus@npm:1.0.2" @@ -29647,13 +28169,6 @@ __metadata: languageName: node linkType: hard -"handle-thing@npm:^2.0.0": - version: 2.0.1 - resolution: "handle-thing@npm:2.0.1" - checksum: 10c0/7ae34ba286a3434f1993ebd1cc9c9e6b6d8ea672182db28b1afc0a7119229552fa7031e3e5f3cd32a76430ece4e94b7da6f12af2eb39d6239a7693e4bd63a998 - languageName: node - linkType: hard - "handlebars@npm:^4.7.7, handlebars@npm:^4.7.8": version: 4.7.8 resolution: "handlebars@npm:4.7.8" @@ -29810,13 +28325,6 @@ __metadata: languageName: node linkType: hard -"has-yarn@npm:^3.0.0": - version: 3.0.0 - resolution: "has-yarn@npm:3.0.0" - checksum: 10c0/38c76618cb764e4a98ea114a3938e0bed6ceafb6bacab2ffb32e7c7d1e18b5e09cd03387d507ee87072388e1f20b1f80947fee62c41fc450edfbbdc02a665787 - languageName: node - linkType: hard - "has@npm:^1.0.0": version: 1.0.4 resolution: "has@npm:1.0.4" @@ -29932,22 +28440,6 @@ __metadata: languageName: node linkType: hard -"hast-util-from-parse5@npm:^8.0.0": - version: 8.0.1 - resolution: "hast-util-from-parse5@npm:8.0.1" - dependencies: - "@types/hast": "npm:^3.0.0" - "@types/unist": "npm:^3.0.0" - devlop: "npm:^1.0.0" - hastscript: "npm:^8.0.0" - property-information: "npm:^6.0.0" - vfile: "npm:^6.0.0" - vfile-location: "npm:^5.0.0" - web-namespaces: "npm:^2.0.0" - checksum: 10c0/4a30bb885cff1f0e023c429ae3ece73fe4b03386f07234bf23f5555ca087c2573ff4e551035b417ed7615bde559f394cdaf1db2b91c3b7f0575f3563cd238969 - languageName: node - linkType: hard - "hast-util-has-property@npm:^2.0.0": version: 2.0.1 resolution: "hast-util-has-property@npm:2.0.1" @@ -30021,15 +28513,6 @@ __metadata: languageName: node linkType: hard -"hast-util-parse-selector@npm:^4.0.0": - version: 4.0.0 - resolution: "hast-util-parse-selector@npm:4.0.0" - dependencies: - "@types/hast": "npm:^3.0.0" - checksum: 10c0/5e98168cb44470dc274aabf1a28317e4feb09b1eaf7a48bbaa8c1de1b43a89cd195cb1284e535698e658e3ec26ad91bc5e52c9563c36feb75abbc68aaf68fb9f - languageName: node - linkType: hard - "hast-util-phrasing@npm:^2.0.0": version: 2.0.2 resolution: "hast-util-phrasing@npm:2.0.2" @@ -30095,27 +28578,6 @@ __metadata: languageName: node linkType: hard -"hast-util-raw@npm:^9.0.0": - version: 9.0.4 - resolution: "hast-util-raw@npm:9.0.4" - dependencies: - "@types/hast": "npm:^3.0.0" - "@types/unist": "npm:^3.0.0" - "@ungap/structured-clone": "npm:^1.0.0" - hast-util-from-parse5: "npm:^8.0.0" - hast-util-to-parse5: "npm:^8.0.0" - html-void-elements: "npm:^3.0.0" - mdast-util-to-hast: "npm:^13.0.0" - parse5: "npm:^7.0.0" - unist-util-position: "npm:^5.0.0" - unist-util-visit: "npm:^5.0.0" - vfile: "npm:^6.0.0" - web-namespaces: "npm:^2.0.0" - zwitch: "npm:^2.0.0" - checksum: 10c0/03d0fe7ba8bd75c9ce81f829650b19b78917bbe31db70d36bf6f136842496c3474e3bb1841f2d30dafe1f6b561a89a524185492b9a93d40b131000743c0d7998 - languageName: node - linkType: hard - "hast-util-sanitize@npm:^4.0.0": version: 4.1.0 resolution: "hast-util-sanitize@npm:4.1.0" @@ -30148,30 +28610,6 @@ __metadata: languageName: node linkType: hard -"hast-util-to-estree@npm:^3.0.0": - version: 3.1.0 - resolution: "hast-util-to-estree@npm:3.1.0" - dependencies: - "@types/estree": "npm:^1.0.0" - "@types/estree-jsx": "npm:^1.0.0" - "@types/hast": "npm:^3.0.0" - comma-separated-tokens: "npm:^2.0.0" - devlop: "npm:^1.0.0" - estree-util-attach-comments: "npm:^3.0.0" - estree-util-is-identifier-name: "npm:^3.0.0" - hast-util-whitespace: "npm:^3.0.0" - mdast-util-mdx-expression: "npm:^2.0.0" - mdast-util-mdx-jsx: "npm:^3.0.0" - mdast-util-mdxjs-esm: "npm:^2.0.0" - property-information: "npm:^6.0.0" - space-separated-tokens: "npm:^2.0.0" - style-to-object: "npm:^0.4.0" - unist-util-position: "npm:^5.0.0" - zwitch: "npm:^2.0.0" - checksum: 10c0/9003a8bac26a4580d5fc9f2a271d17330dd653266425e9f5539feecd2f7538868d6630a18f70698b8b804bf14c306418a3f4ab3119bb4692aca78b0c08b1291e - languageName: node - linkType: hard - "hast-util-to-html@npm:^8.0.0": version: 8.0.4 resolution: "hast-util-to-html@npm:8.0.4" @@ -30191,29 +28629,6 @@ __metadata: languageName: node linkType: hard -"hast-util-to-jsx-runtime@npm:^2.0.0": - version: 2.3.0 - resolution: "hast-util-to-jsx-runtime@npm:2.3.0" - dependencies: - "@types/estree": "npm:^1.0.0" - "@types/hast": "npm:^3.0.0" - "@types/unist": "npm:^3.0.0" - comma-separated-tokens: "npm:^2.0.0" - devlop: "npm:^1.0.0" - estree-util-is-identifier-name: "npm:^3.0.0" - hast-util-whitespace: "npm:^3.0.0" - mdast-util-mdx-expression: "npm:^2.0.0" - mdast-util-mdx-jsx: "npm:^3.0.0" - mdast-util-mdxjs-esm: "npm:^2.0.0" - property-information: "npm:^6.0.0" - space-separated-tokens: "npm:^2.0.0" - style-to-object: "npm:^1.0.0" - unist-util-position: "npm:^5.0.0" - vfile-message: "npm:^4.0.0" - checksum: 10c0/df7a36dcc792df7667a54438f044b721753d5e09692606d23bf7336bf4651670111fe7728eebbf9f0e4f96ab3346a05bb23037fa1b1d115482b3bc5bde8b6912 - languageName: node - linkType: hard - "hast-util-to-mdast@npm:^8.3.0": version: 8.4.1 resolution: "hast-util-to-mdast@npm:8.4.1" @@ -30251,21 +28666,6 @@ __metadata: languageName: node linkType: hard -"hast-util-to-parse5@npm:^8.0.0": - version: 8.0.0 - resolution: "hast-util-to-parse5@npm:8.0.0" - dependencies: - "@types/hast": "npm:^3.0.0" - comma-separated-tokens: "npm:^2.0.0" - devlop: "npm:^1.0.0" - property-information: "npm:^6.0.0" - space-separated-tokens: "npm:^2.0.0" - web-namespaces: "npm:^2.0.0" - zwitch: "npm:^2.0.0" - checksum: 10c0/3c0c7fba026e0c4be4675daf7277f9ff22ae6da801435f1b7104f7740de5422576f1c025023c7b3df1d0a161e13a04c6ab8f98ada96eb50adb287b537849a2bd - languageName: node - linkType: hard - "hast-util-to-string@npm:^3.0.0": version: 3.0.0 resolution: "hast-util-to-string@npm:3.0.0" @@ -30316,19 +28716,6 @@ __metadata: languageName: node linkType: hard -"hastscript@npm:^8.0.0": - version: 8.0.0 - resolution: "hastscript@npm:8.0.0" - dependencies: - "@types/hast": "npm:^3.0.0" - comma-separated-tokens: "npm:^2.0.0" - hast-util-parse-selector: "npm:^4.0.0" - property-information: "npm:^6.0.0" - space-separated-tokens: "npm:^2.0.0" - checksum: 10c0/f0b54bbdd710854b71c0f044612db0fe1b5e4d74fa2001633dc8c535c26033269f04f536f9fd5b03f234de1111808f9e230e9d19493bf919432bb24d541719e0 - languageName: node - linkType: hard - "he@npm:^1.2.0": version: 1.2.0 resolution: "he@npm:1.2.0" @@ -30484,18 +28871,6 @@ __metadata: languageName: node linkType: hard -"hpack.js@npm:^2.1.6": - version: 2.1.6 - resolution: "hpack.js@npm:2.1.6" - dependencies: - inherits: "npm:^2.0.1" - obuf: "npm:^1.0.0" - readable-stream: "npm:^2.0.1" - wbuf: "npm:^1.1.0" - checksum: 10c0/55b9e824430bab82a19d079cb6e33042d7d0640325678c9917fcc020c61d8a08ca671b6c942c7f0aae9bb6e4b67ffb50734a72f9e21d66407c3138c1983b70f0 - languageName: node - linkType: hard - "html-element-attributes@npm:^1.0.0": version: 1.3.1 resolution: "html-element-attributes@npm:1.3.1" @@ -30512,55 +28887,14 @@ __metadata: languageName: node linkType: hard -"html-entities@npm:^2.3.2": - version: 2.5.2 - resolution: "html-entities@npm:2.5.2" - checksum: 10c0/f20ffb4326606245c439c231de40a7c560607f639bf40ffbfb36b4c70729fd95d7964209045f1a4e62fe17f2364cef3d6e49b02ea09016f207fde51c2211e481 - languageName: node - linkType: hard - -"html-escaper@npm:^2.0.0, html-escaper@npm:^2.0.2": +"html-escaper@npm:^2.0.0": version: 2.0.2 resolution: "html-escaper@npm:2.0.2" checksum: 10c0/208e8a12de1a6569edbb14544f4567e6ce8ecc30b9394fcaa4e7bb1e60c12a7c9a1ed27e31290817157e8626f3a4f29e76c8747030822eb84a6abb15c255f0a0 languageName: node linkType: hard -"html-minifier-terser@npm:^6.0.2": - version: 6.1.0 - resolution: "html-minifier-terser@npm:6.1.0" - dependencies: - camel-case: "npm:^4.1.2" - clean-css: "npm:^5.2.2" - commander: "npm:^8.3.0" - he: "npm:^1.2.0" - param-case: "npm:^3.0.4" - relateurl: "npm:^0.2.7" - terser: "npm:^5.10.0" - bin: - html-minifier-terser: cli.js - checksum: 10c0/1aa4e4f01cf7149e3ac5ea84fb7a1adab86da40d38d77a6fff42852b5ee3daccb78b615df97264e3a6a5c33e57f0c77f471d607ca1e1debd1dab9b58286f4b5a - languageName: node - linkType: hard - -"html-minifier-terser@npm:^7.2.0": - version: 7.2.0 - resolution: "html-minifier-terser@npm:7.2.0" - dependencies: - camel-case: "npm:^4.1.2" - clean-css: "npm:~5.3.2" - commander: "npm:^10.0.0" - entities: "npm:^4.4.0" - param-case: "npm:^3.0.4" - relateurl: "npm:^0.2.7" - terser: "npm:^5.15.1" - bin: - html-minifier-terser: cli.js - checksum: 10c0/ffc97c17299d9ec30e17269781b816ea2fc411a9206fc9e768be8f2decb1ea1470892809babb23bb4e3ab1f64d606d97e1803bf526ae3af71edc0fd3070b94b9 - languageName: node - linkType: hard - -"html-tags@npm:^3.1.0, html-tags@npm:^3.3.1": +"html-tags@npm:^3.1.0": version: 3.3.1 resolution: "html-tags@npm:3.3.1" checksum: 10c0/680165e12baa51bad7397452d247dbcc5a5c29dac0e6754b1187eee3bf26f514bc1907a431dd2f7eb56207611ae595ee76a0acc8eaa0d931e72c791dd6463d79 @@ -30587,34 +28921,6 @@ __metadata: languageName: node linkType: hard -"html-void-elements@npm:^3.0.0": - version: 3.0.0 - resolution: "html-void-elements@npm:3.0.0" - checksum: 10c0/a8b9ec5db23b7c8053876dad73a0336183e6162bf6d2677376d8b38d654fdc59ba74fdd12f8812688f7db6fad451210c91b300e472afc0909224e0a44c8610d2 - languageName: node - linkType: hard - -"html-webpack-plugin@npm:^5.5.3": - version: 5.6.0 - resolution: "html-webpack-plugin@npm:5.6.0" - dependencies: - "@types/html-minifier-terser": "npm:^6.0.0" - html-minifier-terser: "npm:^6.0.2" - lodash: "npm:^4.17.21" - pretty-error: "npm:^4.0.0" - tapable: "npm:^2.0.0" - peerDependencies: - "@rspack/core": 0.x || 1.x - webpack: ^5.20.0 - peerDependenciesMeta: - "@rspack/core": - optional: true - webpack: - optional: true - checksum: 10c0/50d1a0f90d512463ea8d798985d91a7ccc9d5e461713dedb240125b2ff0671f58135dd9355f7969af341ff4725e73b2defbc0984cfdce930887a48506d970002 - languageName: node - linkType: hard - "html-whitespace-sensitive-tag-names@npm:^3.0.0": version: 3.0.0 resolution: "html-whitespace-sensitive-tag-names@npm:3.0.0" @@ -30643,18 +28949,6 @@ __metadata: languageName: node linkType: hard -"htmlparser2@npm:^6.1.0": - version: 6.1.0 - resolution: "htmlparser2@npm:6.1.0" - dependencies: - domelementtype: "npm:^2.0.1" - domhandler: "npm:^4.0.0" - domutils: "npm:^2.5.2" - entities: "npm:^2.0.0" - checksum: 10c0/3058499c95634f04dc66be8c2e0927cd86799413b2d6989d8ae542ca4dbf5fa948695d02c27d573acf44843af977aec6d9a7bdd0f6faa6b2d99e2a729b2a31b6 - languageName: node - linkType: hard - "htmlparser2@npm:^8.0.1, htmlparser2@npm:^8.0.2": version: 8.0.2 resolution: "htmlparser2@npm:8.0.2" @@ -30674,13 +28968,6 @@ __metadata: languageName: node linkType: hard -"http-deceiver@npm:^1.2.7": - version: 1.2.7 - resolution: "http-deceiver@npm:1.2.7" - checksum: 10c0/8bb9b716f5fc55f54a451da7f49b9c695c3e45498a789634daec26b61e4add7c85613a4a9e53726c39d09de7a163891ecd6eb5809adb64500a840fd86fe81d03 - languageName: node - linkType: hard - "http-errors@npm:2.0.0": version: 2.0.0 resolution: "http-errors@npm:2.0.0" @@ -30707,25 +28994,6 @@ __metadata: languageName: node linkType: hard -"http-errors@npm:~1.6.2": - version: 1.6.3 - resolution: "http-errors@npm:1.6.3" - dependencies: - depd: "npm:~1.1.2" - inherits: "npm:2.0.3" - setprototypeof: "npm:1.1.0" - statuses: "npm:>= 1.4.0 < 2" - checksum: 10c0/17ec4046ee974477778bfdd525936c254b872054703ec2caa4d6f099566b8adade636ae6aeeacb39302c5cd6e28fb407ebd937f500f5010d0b6850750414ff78 - languageName: node - linkType: hard - -"http-parser-js@npm:>=0.5.1": - version: 0.5.8 - resolution: "http-parser-js@npm:0.5.8" - checksum: 10c0/4ed89f812c44f84c4ae5d43dd3a0c47942b875b63be0ed2ccecbe6b0018af867d806495fc6e12474aff868721163699c49246585bddea4f0ecc6d2b02e19faf1 - languageName: node - linkType: hard - "http-proxy-agent@npm:^4.0.1": version: 4.0.1 resolution: "http-proxy-agent@npm:4.0.1" @@ -30768,24 +29036,6 @@ __metadata: languageName: node linkType: hard -"http-proxy-middleware@npm:^2.0.3": - version: 2.0.6 - resolution: "http-proxy-middleware@npm:2.0.6" - dependencies: - "@types/http-proxy": "npm:^1.17.8" - http-proxy: "npm:^1.18.1" - is-glob: "npm:^4.0.1" - is-plain-obj: "npm:^3.0.0" - micromatch: "npm:^4.0.2" - peerDependencies: - "@types/express": ^4.17.13 - peerDependenciesMeta: - "@types/express": - optional: true - checksum: 10c0/25a0e550dd1900ee5048a692e0e9b2b6339d06d487a705d90c47e359e9c6561d648cd7862d001d090e651c9efffa1b6e5160fcf1f299b5fa4935f76e9754eb11 - languageName: node - linkType: hard - "http-proxy@npm:^1.18.1": version: 1.18.1 resolution: "http-proxy@npm:1.18.1" @@ -30848,16 +29098,6 @@ __metadata: languageName: node linkType: hard -"http2-wrapper@npm:^2.1.10": - version: 2.2.1 - resolution: "http2-wrapper@npm:2.2.1" - dependencies: - quick-lru: "npm:^5.1.1" - resolve-alpn: "npm:^1.2.0" - checksum: 10c0/7207201d3c6e53e72e510c9b8912e4f3e468d3ecc0cf3bf52682f2aac9cd99358b896d1da4467380adc151cf97c412bedc59dc13dae90c523f42053a7449eedb - languageName: node - linkType: hard - "https-browserify@npm:^1.0.0": version: 1.0.0 resolution: "https-browserify@npm:1.0.0" @@ -31057,17 +29297,6 @@ __metadata: languageName: node linkType: hard -"image-size@npm:^1.0.2": - version: 1.1.1 - resolution: "image-size@npm:1.1.1" - dependencies: - queue: "npm:6.0.2" - bin: - image-size: bin/image-size.js - checksum: 10c0/2660470096d12be82195f7e80fe03274689fbd14184afb78eaf66ade7cd06352518325814f88af4bde4b26647889fe49e573129f6e7ba8f5ff5b85cc7f559000 - languageName: node - linkType: hard - "imask@npm:^7.6.1": version: 7.6.1 resolution: "imask@npm:7.6.1" @@ -31084,13 +29313,6 @@ __metadata: languageName: node linkType: hard -"immer@npm:^9.0.7": - version: 9.0.21 - resolution: "immer@npm:9.0.21" - checksum: 10c0/03ea3ed5d4d72e8bd428df4a38ad7e483ea8308e9a113d3b42e0ea2cc0cc38340eb0a6aca69592abbbf047c685dbda04e3d34bf2ff438ab57339ed0a34cc0a05 - languageName: node - linkType: hard - "immutable@npm:~3.7.6": version: 3.7.6 resolution: "immutable@npm:3.7.6" @@ -31134,7 +29356,7 @@ __metadata: languageName: node linkType: hard -"import-lazy@npm:^4.0.0, import-lazy@npm:~4.0.0": +"import-lazy@npm:~4.0.0": version: 4.0.0 resolution: "import-lazy@npm:4.0.0" checksum: 10c0/a3520313e2c31f25c0b06aa66d167f329832b68a4f957d7c9daf6e0fa41822b6e84948191648b9b9d8ca82f94740cdf15eecf2401a5b42cd1c33fd84f2225cca @@ -31181,13 +29403,6 @@ __metadata: languageName: node linkType: hard -"infima@npm:0.2.0-alpha.43": - version: 0.2.0-alpha.43 - resolution: "infima@npm:0.2.0-alpha.43" - checksum: 10c0/d248958713a97e1c9f73ace27ceff726ba86a9b534efb0ebdec3e72b785d8edb36db922e38ce09bbeb98a17b657e61357f22edc3a58f02ad51b7ae2ebd96e4e4 - languageName: node - linkType: hard - "inflight@npm:^1.0.4": version: 1.0.6 resolution: "inflight@npm:1.0.6" @@ -31242,13 +29457,6 @@ __metadata: languageName: node linkType: hard -"inline-style-parser@npm:0.2.3": - version: 0.2.3 - resolution: "inline-style-parser@npm:0.2.3" - checksum: 10c0/21b46d39a39c8aeaa738346650469388e8a412dd276ab75aa3d85b1883311e89c86a1fdbb8c2f1958f4c979bae74067f6ba0385455b125faf4fa77e1dbb94799 - languageName: node - linkType: hard - "inline-style-prefixer@npm:^7.0.1": version: 7.0.1 resolution: "inline-style-prefixer@npm:7.0.1" @@ -31390,6 +29598,13 @@ __metadata: languageName: node linkType: hard +"internmap@npm:1 - 2": + version: 2.0.3 + resolution: "internmap@npm:2.0.3" + checksum: 10c0/8cedd57f07bbc22501516fbfc70447f0c6812871d471096fad9ea603516eacc2137b633633daf432c029712df0baefd793686388ddf5737e3ea15074b877f7ed + languageName: node + linkType: hard + "internmap@npm:^1.0.0": version: 1.0.1 resolution: "internmap@npm:1.0.1" @@ -31461,13 +29676,6 @@ __metadata: languageName: node linkType: hard -"ipaddr.js@npm:^2.0.1": - version: 2.2.0 - resolution: "ipaddr.js@npm:2.2.0" - checksum: 10c0/e4ee875dc1bd92ac9d27e06cfd87cdb63ca786ff9fd7718f1d4f7a8ef27db6e5d516128f52d2c560408cbb75796ac2f83ead669e73507c86282d45f84c5abbb6 - languageName: node - linkType: hard - "is-absolute-url@npm:^3.0.0": version: 3.0.3 resolution: "is-absolute-url@npm:3.0.3" @@ -31631,17 +29839,6 @@ __metadata: languageName: node linkType: hard -"is-ci@npm:^3.0.1": - version: 3.0.1 - resolution: "is-ci@npm:3.0.1" - dependencies: - ci-info: "npm:^3.2.0" - bin: - is-ci: bin.js - checksum: 10c0/0e81caa62f4520d4088a5bef6d6337d773828a88610346c4b1119fb50c842587ed8bef1e5d9a656835a599e7209405b5761ddf2339668f2d0f4e889a92fe6051 - languageName: node - linkType: hard - "is-core-module@npm:^2.1.0, is-core-module@npm:^2.11.0, is-core-module@npm:^2.13.0, is-core-module@npm:^2.13.1, is-core-module@npm:^2.8.1": version: 2.15.0 resolution: "is-core-module@npm:2.15.0" @@ -31887,13 +30084,6 @@ __metadata: languageName: node linkType: hard -"is-npm@npm:^6.0.0": - version: 6.0.0 - resolution: "is-npm@npm:6.0.0" - checksum: 10c0/1f064c66325cba6e494783bee4e635caa2655aad7f853a0e045d086e0bb7d83d2d6cdf1745dc9a7c7c93dacbf816fbee1f8d9179b02d5d01674d4f92541dc0d9 - languageName: node - linkType: hard - "is-number-object@npm:^1.0.4": version: 1.0.7 resolution: "is-number-object@npm:1.0.7" @@ -31952,13 +30142,6 @@ __metadata: languageName: node linkType: hard -"is-plain-obj@npm:^3.0.0": - version: 3.0.0 - resolution: "is-plain-obj@npm:3.0.0" - checksum: 10c0/8e6483bfb051d42ec9c704c0ede051a821c6b6f9a6c7a3e3b55aa855e00981b0580c8f3b1f5e2e62649b39179b1abfee35d6f8086d999bfaa32c1908d29b07bc - languageName: node - linkType: hard - "is-plain-obj@npm:^4.0.0": version: 4.1.0 resolution: "is-plain-obj@npm:4.1.0" @@ -32038,13 +30221,6 @@ __metadata: languageName: node linkType: hard -"is-root@npm:^2.1.0": - version: 2.1.0 - resolution: "is-root@npm:2.1.0" - checksum: 10c0/83d3f5b052c3f28fbdbdf0d564bdd34fa14933f5694c78704f85cd1871255bc017fbe3fe2bc2fff2d227c6be5927ad2149b135c0a7c0060e7ac4e610d81a4f01 - languageName: node - linkType: hard - "is-scoped@npm:^2.1.0": version: 2.1.0 resolution: "is-scoped@npm:2.1.0" @@ -32241,13 +30417,6 @@ __metadata: languageName: node linkType: hard -"is-yarn-global@npm:^0.4.0": - version: 0.4.1 - resolution: "is-yarn-global@npm:0.4.1" - checksum: 10c0/8ff66f33454614f8e913ad91cc4de0d88d519a46c1ed41b3f589da79504ed0fcfa304064fe3096dda9360c5f35aa210cb8e978fd36798f3118cb66a4de64d365 - languageName: node - linkType: hard - "isarray@npm:0.0.1": version: 0.0.1 resolution: "isarray@npm:0.0.1" @@ -33075,7 +31244,7 @@ __metadata: languageName: node linkType: hard -"jest-worker@npm:^29.4.3, jest-worker@npm:^29.7.0": +"jest-worker@npm:^29.7.0": version: 29.7.0 resolution: "jest-worker@npm:29.7.0" dependencies: @@ -33115,7 +31284,7 @@ __metadata: languageName: node linkType: hard -"jiti@npm:^1.17.1, jiti@npm:^1.20.0": +"jiti@npm:^1.17.1": version: 1.21.6 resolution: "jiti@npm:1.21.6" bin: @@ -33131,7 +31300,7 @@ __metadata: languageName: node linkType: hard -"joi@npm:^17.11.0, joi@npm:^17.9.2": +"joi@npm:^17.11.0": version: 17.13.3 resolution: "joi@npm:17.13.3" dependencies: @@ -34269,25 +32438,6 @@ __metadata: languageName: node linkType: hard -"latest-version@npm:^7.0.0": - version: 7.0.0 - resolution: "latest-version@npm:7.0.0" - dependencies: - package-json: "npm:^8.1.0" - checksum: 10c0/68045f5e419e005c12e595ae19687dd88317dd0108b83a8773197876622c7e9d164fe43aacca4f434b2cba105c92848b89277f658eabc5d50e81fb743bbcddb1 - languageName: node - linkType: hard - -"launch-editor@npm:^2.6.0": - version: 2.8.1 - resolution: "launch-editor@npm:2.8.1" - dependencies: - picocolors: "npm:^1.0.0" - shell-quote: "npm:^1.8.1" - checksum: 10c0/e18fcda6617a995306602871c7a71ddcfdd82d88a57508ae970be86bfb6685f131cf9ddb8896df4e8e4cde6d0e2d14318d2b41314eaae6abf03ca205948daa27 - languageName: node - linkType: hard - "lazy-universal-dotenv@npm:^4.0.0": version: 4.0.0 resolution: "lazy-universal-dotenv@npm:4.0.0" @@ -34461,13 +32611,6 @@ __metadata: languageName: node linkType: hard -"loader-utils@npm:^3.2.0": - version: 3.3.1 - resolution: "loader-utils@npm:3.3.1" - checksum: 10c0/f2af4eb185ac5bf7e56e1337b666f90744e9f443861ac521b48f093fb9e8347f191c8960b4388a3365147d218913bc23421234e7788db69f385bacfefa0b4758 - languageName: node - linkType: hard - "local-pkg@npm:^0.5.0": version: 0.5.0 resolution: "local-pkg@npm:0.5.0" @@ -34506,15 +32649,6 @@ __metadata: languageName: node linkType: hard -"locate-path@npm:^7.1.0": - version: 7.2.0 - resolution: "locate-path@npm:7.2.0" - dependencies: - p-locate: "npm:^6.0.0" - checksum: 10c0/139e8a7fe11cfbd7f20db03923cacfa5db9e14fa14887ea121345597472b4a63c1a42a8a5187defeeff6acf98fd568da7382aa39682d38f0af27433953a97751 - languageName: node - linkType: hard - "lodash-es@npm:^4.17.21": version: 4.17.21 resolution: "lodash-es@npm:4.17.21" @@ -34939,13 +33073,6 @@ __metadata: languageName: node linkType: hard -"lowercase-keys@npm:^3.0.0": - version: 3.0.0 - resolution: "lowercase-keys@npm:3.0.0" - checksum: 10c0/ef62b9fa5690ab0a6e4ef40c94efce68e3ed124f583cc3be38b26ff871da0178a28b9a84ce0c209653bb25ca135520ab87fea7cd411a54ac4899cb2f30501430 - languageName: node - linkType: hard - "lru-cache@npm:7.10.1 - 7.13.1": version: 7.13.1 resolution: "lru-cache@npm:7.13.1" @@ -35253,13 +33380,6 @@ __metadata: languageName: node linkType: hard -"markdown-extensions@npm:^2.0.0": - version: 2.0.0 - resolution: "markdown-extensions@npm:2.0.0" - checksum: 10c0/406139da2aa0d5ebad86195c8e8c02412f873c452b4c087ae7bc767af37956141be449998223bb379eea179b5fd38dfa610602b6f29c22ddab5d51e627a7e41d - languageName: node - linkType: hard - "markdown-it@npm:^14.0.0, markdown-it@npm:^14.1.0": version: 14.1.0 resolution: "markdown-it@npm:14.1.0" @@ -35376,22 +33496,6 @@ __metadata: languageName: node linkType: hard -"mdast-util-directive@npm:^3.0.0": - version: 3.0.0 - resolution: "mdast-util-directive@npm:3.0.0" - dependencies: - "@types/mdast": "npm:^4.0.0" - "@types/unist": "npm:^3.0.0" - devlop: "npm:^1.0.0" - mdast-util-from-markdown: "npm:^2.0.0" - mdast-util-to-markdown: "npm:^2.0.0" - parse-entities: "npm:^4.0.0" - stringify-entities: "npm:^4.0.0" - unist-util-visit-parents: "npm:^6.0.0" - checksum: 10c0/4a71b27f5f0c4ead5293a12d4118d4d832951ac0efdeba4af2dd78f5679f9cabee80feb3619f219a33674c12df3780def1bd3150d7298aaf0ef734f0dfbab999 - languageName: node - linkType: hard - "mdast-util-find-and-replace@npm:^1.1.0": version: 1.1.1 resolution: "mdast-util-find-and-replace@npm:1.1.1" @@ -35415,18 +33519,6 @@ __metadata: languageName: node linkType: hard -"mdast-util-find-and-replace@npm:^3.0.0, mdast-util-find-and-replace@npm:^3.0.1": - version: 3.0.1 - resolution: "mdast-util-find-and-replace@npm:3.0.1" - dependencies: - "@types/mdast": "npm:^4.0.0" - escape-string-regexp: "npm:^5.0.0" - unist-util-is: "npm:^6.0.0" - unist-util-visit-parents: "npm:^6.0.0" - checksum: 10c0/1faca98c4ee10a919f23b8cc6d818e5bb6953216a71dfd35f51066ed5d51ef86e5063b43dcfdc6061cd946e016a9f0d44a1dccadd58452cf4ed14e39377f00cb - languageName: node - linkType: hard - "mdast-util-from-markdown@npm:^0.8.0": version: 0.8.5 resolution: "mdast-util-from-markdown@npm:0.8.5" @@ -35460,26 +33552,6 @@ __metadata: languageName: node linkType: hard -"mdast-util-from-markdown@npm:^2.0.0": - version: 2.0.1 - resolution: "mdast-util-from-markdown@npm:2.0.1" - dependencies: - "@types/mdast": "npm:^4.0.0" - "@types/unist": "npm:^3.0.0" - decode-named-character-reference: "npm:^1.0.0" - devlop: "npm:^1.0.0" - mdast-util-to-string: "npm:^4.0.0" - micromark: "npm:^4.0.0" - micromark-util-decode-numeric-character-reference: "npm:^2.0.0" - micromark-util-decode-string: "npm:^2.0.0" - micromark-util-normalize-identifier: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - unist-util-stringify-position: "npm:^4.0.0" - checksum: 10c0/496596bc6419200ff6258531a0ebcaee576a5c169695f5aa296a79a85f2a221bb9247d565827c709a7c2acfb56ae3c3754bf483d86206617bd299a9658c8121c - languageName: node - linkType: hard - "mdast-util-frontmatter@npm:^0.2.0": version: 0.2.0 resolution: "mdast-util-frontmatter@npm:0.2.0" @@ -35489,20 +33561,6 @@ __metadata: languageName: node linkType: hard -"mdast-util-frontmatter@npm:^2.0.0": - version: 2.0.1 - resolution: "mdast-util-frontmatter@npm:2.0.1" - dependencies: - "@types/mdast": "npm:^4.0.0" - devlop: "npm:^1.0.0" - escape-string-regexp: "npm:^5.0.0" - mdast-util-from-markdown: "npm:^2.0.0" - mdast-util-to-markdown: "npm:^2.0.0" - micromark-extension-frontmatter: "npm:^2.0.0" - checksum: 10c0/d9b0b70dd9c574cc0220d4e05dd8e9d86ac972a6a5af9e0c49c839b31cb750d4313445cfbbdf9264a7fbe3f8c8d920b45358b8500f4286e6b9dc830095b25b9a - languageName: node - linkType: hard - "mdast-util-gfm-autolink-literal@npm:^0.1.0": version: 0.1.3 resolution: "mdast-util-gfm-autolink-literal@npm:0.1.3" @@ -35526,19 +33584,6 @@ __metadata: languageName: node linkType: hard -"mdast-util-gfm-autolink-literal@npm:^2.0.0": - version: 2.0.0 - resolution: "mdast-util-gfm-autolink-literal@npm:2.0.0" - dependencies: - "@types/mdast": "npm:^4.0.0" - ccount: "npm:^2.0.0" - devlop: "npm:^1.0.0" - mdast-util-find-and-replace: "npm:^3.0.0" - micromark-util-character: "npm:^2.0.0" - checksum: 10c0/821ef91db108f05b321c54fdf4436df9d6badb33e18f714d8d52c0e70f988f5b6b118cdd4d607b4cb3bef1718304ce7e9fb25fa580622c3d20d68c1489c64875 - languageName: node - linkType: hard - "mdast-util-gfm-footnote@npm:^1.0.0": version: 1.0.2 resolution: "mdast-util-gfm-footnote@npm:1.0.2" @@ -35550,19 +33595,6 @@ __metadata: languageName: node linkType: hard -"mdast-util-gfm-footnote@npm:^2.0.0": - version: 2.0.0 - resolution: "mdast-util-gfm-footnote@npm:2.0.0" - dependencies: - "@types/mdast": "npm:^4.0.0" - devlop: "npm:^1.1.0" - mdast-util-from-markdown: "npm:^2.0.0" - mdast-util-to-markdown: "npm:^2.0.0" - micromark-util-normalize-identifier: "npm:^2.0.0" - checksum: 10c0/c673b22bea24740235e74cfd66765b41a2fa540334f7043fa934b94938b06b7d3c93f2d3b33671910c5492b922c0cc98be833be3b04cfed540e0679650a6d2de - languageName: node - linkType: hard - "mdast-util-gfm-strikethrough@npm:^0.2.0": version: 0.2.3 resolution: "mdast-util-gfm-strikethrough@npm:0.2.3" @@ -35582,17 +33614,6 @@ __metadata: languageName: node linkType: hard -"mdast-util-gfm-strikethrough@npm:^2.0.0": - version: 2.0.0 - resolution: "mdast-util-gfm-strikethrough@npm:2.0.0" - dependencies: - "@types/mdast": "npm:^4.0.0" - mdast-util-from-markdown: "npm:^2.0.0" - mdast-util-to-markdown: "npm:^2.0.0" - checksum: 10c0/b053e93d62c7545019bd914271ea9e5667ad3b3b57d16dbf68e56fea39a7e19b4a345e781312714eb3d43fdd069ff7ee22a3ca7f6149dfa774554f19ce3ac056 - languageName: node - linkType: hard - "mdast-util-gfm-table@npm:^0.1.0": version: 0.1.6 resolution: "mdast-util-gfm-table@npm:0.1.6" @@ -35615,19 +33636,6 @@ __metadata: languageName: node linkType: hard -"mdast-util-gfm-table@npm:^2.0.0": - version: 2.0.0 - resolution: "mdast-util-gfm-table@npm:2.0.0" - dependencies: - "@types/mdast": "npm:^4.0.0" - devlop: "npm:^1.0.0" - markdown-table: "npm:^3.0.0" - mdast-util-from-markdown: "npm:^2.0.0" - mdast-util-to-markdown: "npm:^2.0.0" - checksum: 10c0/128af47c503a53bd1c79f20642561e54a510ad5e2db1e418d28fefaf1294ab839e6c838e341aef5d7e404f9170b9ca3d1d89605f234efafde93ee51174a6e31e - languageName: node - linkType: hard - "mdast-util-gfm-task-list-item@npm:^0.1.0": version: 0.1.6 resolution: "mdast-util-gfm-task-list-item@npm:0.1.6" @@ -35647,18 +33655,6 @@ __metadata: languageName: node linkType: hard -"mdast-util-gfm-task-list-item@npm:^2.0.0": - version: 2.0.0 - resolution: "mdast-util-gfm-task-list-item@npm:2.0.0" - dependencies: - "@types/mdast": "npm:^4.0.0" - devlop: "npm:^1.0.0" - mdast-util-from-markdown: "npm:^2.0.0" - mdast-util-to-markdown: "npm:^2.0.0" - checksum: 10c0/258d725288482b636c0a376c296431390c14b4f29588675297cb6580a8598ed311fc73ebc312acfca12cc8546f07a3a285a53a3b082712e2cbf5c190d677d834 - languageName: node - linkType: hard - "mdast-util-gfm@npm:^0.1.0": version: 0.1.2 resolution: "mdast-util-gfm@npm:0.1.2" @@ -35687,21 +33683,6 @@ __metadata: languageName: node linkType: hard -"mdast-util-gfm@npm:^3.0.0": - version: 3.0.0 - resolution: "mdast-util-gfm@npm:3.0.0" - dependencies: - mdast-util-from-markdown: "npm:^2.0.0" - mdast-util-gfm-autolink-literal: "npm:^2.0.0" - mdast-util-gfm-footnote: "npm:^2.0.0" - mdast-util-gfm-strikethrough: "npm:^2.0.0" - mdast-util-gfm-table: "npm:^2.0.0" - mdast-util-gfm-task-list-item: "npm:^2.0.0" - mdast-util-to-markdown: "npm:^2.0.0" - checksum: 10c0/91596fe9bf3e4a0c546d0c57f88106c17956d9afbe88ceb08308e4da2388aff64489d649ddad599caecfdf755fc3ae4c9b82c219b85281bc0586b67599881fca - languageName: node - linkType: hard - "mdast-util-mdx-expression@npm:^1.0.0": version: 1.3.2 resolution: "mdast-util-mdx-expression@npm:1.3.2" @@ -35715,20 +33696,6 @@ __metadata: languageName: node linkType: hard -"mdast-util-mdx-expression@npm:^2.0.0": - version: 2.0.0 - resolution: "mdast-util-mdx-expression@npm:2.0.0" - dependencies: - "@types/estree-jsx": "npm:^1.0.0" - "@types/hast": "npm:^3.0.0" - "@types/mdast": "npm:^4.0.0" - devlop: "npm:^1.0.0" - mdast-util-from-markdown: "npm:^2.0.0" - mdast-util-to-markdown: "npm:^2.0.0" - checksum: 10c0/512848cbc44b9dc7cffc1bb3f95f7e67f0d6562870e56a67d25647f475d411e136b915ba417c8069fb36eac1839d0209fb05fb323d377f35626a82fcb0879363 - languageName: node - linkType: hard - "mdast-util-mdx-jsx@npm:^2.0.0": version: 2.1.4 resolution: "mdast-util-mdx-jsx@npm:2.1.4" @@ -35749,27 +33716,6 @@ __metadata: languageName: node linkType: hard -"mdast-util-mdx-jsx@npm:^3.0.0": - version: 3.1.2 - resolution: "mdast-util-mdx-jsx@npm:3.1.2" - dependencies: - "@types/estree-jsx": "npm:^1.0.0" - "@types/hast": "npm:^3.0.0" - "@types/mdast": "npm:^4.0.0" - "@types/unist": "npm:^3.0.0" - ccount: "npm:^2.0.0" - devlop: "npm:^1.1.0" - mdast-util-from-markdown: "npm:^2.0.0" - mdast-util-to-markdown: "npm:^2.0.0" - parse-entities: "npm:^4.0.0" - stringify-entities: "npm:^4.0.0" - unist-util-remove-position: "npm:^5.0.0" - unist-util-stringify-position: "npm:^4.0.0" - vfile-message: "npm:^4.0.0" - checksum: 10c0/855b60c3db9bde2fe142bd366597f7bd5892fc288428ba054e26ffcffc07bfe5648c0792d614ba6e08b1eab9784ffc3c1267cf29dfc6db92b419d68b5bcd487d - languageName: node - linkType: hard - "mdast-util-mdx@npm:^2.0.0": version: 2.0.1 resolution: "mdast-util-mdx@npm:2.0.1" @@ -35783,19 +33729,6 @@ __metadata: languageName: node linkType: hard -"mdast-util-mdx@npm:^3.0.0": - version: 3.0.0 - resolution: "mdast-util-mdx@npm:3.0.0" - dependencies: - mdast-util-from-markdown: "npm:^2.0.0" - mdast-util-mdx-expression: "npm:^2.0.0" - mdast-util-mdx-jsx: "npm:^3.0.0" - mdast-util-mdxjs-esm: "npm:^2.0.0" - mdast-util-to-markdown: "npm:^2.0.0" - checksum: 10c0/4faea13f77d6bc9aa64ee41a5e4779110b73444a17fda363df6ebe880ecfa58b321155b71f8801c3faa6d70d6222a32a00cbd6dbf5fad8db417f4688bc9c74e1 - languageName: node - linkType: hard - "mdast-util-mdxjs-esm@npm:^1.0.0": version: 1.3.1 resolution: "mdast-util-mdxjs-esm@npm:1.3.1" @@ -35809,20 +33742,6 @@ __metadata: languageName: node linkType: hard -"mdast-util-mdxjs-esm@npm:^2.0.0": - version: 2.0.1 - resolution: "mdast-util-mdxjs-esm@npm:2.0.1" - dependencies: - "@types/estree-jsx": "npm:^1.0.0" - "@types/hast": "npm:^3.0.0" - "@types/mdast": "npm:^4.0.0" - devlop: "npm:^1.0.0" - mdast-util-from-markdown: "npm:^2.0.0" - mdast-util-to-markdown: "npm:^2.0.0" - checksum: 10c0/5bda92fc154141705af2b804a534d891f28dac6273186edf1a4c5e3f045d5b01dbcac7400d27aaf91b7e76e8dce007c7b2fdf136c11ea78206ad00bdf9db46bc - languageName: node - linkType: hard - "mdast-util-phrasing@npm:^3.0.0": version: 3.0.1 resolution: "mdast-util-phrasing@npm:3.0.1" @@ -35833,16 +33752,6 @@ __metadata: languageName: node linkType: hard -"mdast-util-phrasing@npm:^4.0.0": - version: 4.1.0 - resolution: "mdast-util-phrasing@npm:4.1.0" - dependencies: - "@types/mdast": "npm:^4.0.0" - unist-util-is: "npm:^6.0.0" - checksum: 10c0/bf6c31d51349aa3d74603d5e5a312f59f3f65662ed16c58017169a5fb0f84ca98578f626c5ee9e4aa3e0a81c996db8717096705521bddb4a0185f98c12c9b42f - languageName: node - linkType: hard - "mdast-util-to-hast@npm:^11.1.1": version: 11.3.0 resolution: "mdast-util-to-hast@npm:11.3.0" @@ -35876,23 +33785,6 @@ __metadata: languageName: node linkType: hard -"mdast-util-to-hast@npm:^13.0.0": - version: 13.2.0 - resolution: "mdast-util-to-hast@npm:13.2.0" - dependencies: - "@types/hast": "npm:^3.0.0" - "@types/mdast": "npm:^4.0.0" - "@ungap/structured-clone": "npm:^1.0.0" - devlop: "npm:^1.0.0" - micromark-util-sanitize-uri: "npm:^2.0.0" - trim-lines: "npm:^3.0.0" - unist-util-position: "npm:^5.0.0" - unist-util-visit: "npm:^5.0.0" - vfile: "npm:^6.0.0" - checksum: 10c0/9ee58def9287df8350cbb6f83ced90f9c088d72d4153780ad37854f87144cadc6f27b20347073b285173b1649b0723ddf0b9c78158608a804dcacb6bda6e1816 - languageName: node - linkType: hard - "mdast-util-to-markdown@npm:^0.6.0, mdast-util-to-markdown@npm:^0.6.1, mdast-util-to-markdown@npm:~0.6.0": version: 0.6.5 resolution: "mdast-util-to-markdown@npm:0.6.5" @@ -35923,22 +33815,6 @@ __metadata: languageName: node linkType: hard -"mdast-util-to-markdown@npm:^2.0.0": - version: 2.1.0 - resolution: "mdast-util-to-markdown@npm:2.1.0" - dependencies: - "@types/mdast": "npm:^4.0.0" - "@types/unist": "npm:^3.0.0" - longest-streak: "npm:^3.0.0" - mdast-util-phrasing: "npm:^4.0.0" - mdast-util-to-string: "npm:^4.0.0" - micromark-util-decode-string: "npm:^2.0.0" - unist-util-visit: "npm:^5.0.0" - zwitch: "npm:^2.0.0" - checksum: 10c0/8bd37a9627a438ef6418d6642661904d0cc03c5c732b8b018a8e238ef5cc82fe8aef1940b19c6f563245e58b9659f35e527209bd3fe145f3c723ba14d18fc3e6 - languageName: node - linkType: hard - "mdast-util-to-string@npm:^1.0.0": version: 1.1.0 resolution: "mdast-util-to-string@npm:1.1.0" @@ -35962,15 +33838,6 @@ __metadata: languageName: node linkType: hard -"mdast-util-to-string@npm:^4.0.0": - version: 4.0.0 - resolution: "mdast-util-to-string@npm:4.0.0" - dependencies: - "@types/mdast": "npm:^4.0.0" - checksum: 10c0/2d3c1af29bf3fe9c20f552ee9685af308002488f3b04b12fa66652c9718f66f41a32f8362aa2d770c3ff464c034860b41715902ada2306bb0a055146cef064d7 - languageName: node - linkType: hard - "mdn-data@npm:2.0.14": version: 2.0.14 resolution: "mdn-data@npm:2.0.14" @@ -36057,7 +33924,7 @@ __metadata: languageName: node linkType: hard -"memfs@npm:^3.1.2, memfs@npm:^3.4.1, memfs@npm:^3.4.3": +"memfs@npm:^3.4.1": version: 3.5.3 resolution: "memfs@npm:3.5.3" dependencies: @@ -36185,45 +34052,6 @@ __metadata: languageName: node linkType: hard -"micromark-core-commonmark@npm:^2.0.0": - version: 2.0.1 - resolution: "micromark-core-commonmark@npm:2.0.1" - dependencies: - decode-named-character-reference: "npm:^1.0.0" - devlop: "npm:^1.0.0" - micromark-factory-destination: "npm:^2.0.0" - micromark-factory-label: "npm:^2.0.0" - micromark-factory-space: "npm:^2.0.0" - micromark-factory-title: "npm:^2.0.0" - micromark-factory-whitespace: "npm:^2.0.0" - micromark-util-character: "npm:^2.0.0" - micromark-util-chunked: "npm:^2.0.0" - micromark-util-classify-character: "npm:^2.0.0" - micromark-util-html-tag-name: "npm:^2.0.0" - micromark-util-normalize-identifier: "npm:^2.0.0" - micromark-util-resolve-all: "npm:^2.0.0" - micromark-util-subtokenize: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/a0b280b1b6132f600518e72cb29a4dd1b2175b85f5ed5b25d2c5695e42b876b045971370daacbcfc6b4ce8cf7acbf78dd3a0284528fb422b450144f4b3bebe19 - languageName: node - linkType: hard - -"micromark-extension-directive@npm:^3.0.0": - version: 3.0.1 - resolution: "micromark-extension-directive@npm:3.0.1" - dependencies: - devlop: "npm:^1.0.0" - micromark-factory-space: "npm:^2.0.0" - micromark-factory-whitespace: "npm:^2.0.0" - micromark-util-character: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - parse-entities: "npm:^4.0.0" - checksum: 10c0/9d226fba0ce18f326d2b28cf2b981c78f6c0c7c2f85e810bf4b12a788dfa4b694386589b081da165227da573ff547238f39c5258d09954b055f167bba1af4983 - languageName: node - linkType: hard - "micromark-extension-frontmatter@npm:^0.2.0": version: 0.2.2 resolution: "micromark-extension-frontmatter@npm:0.2.2" @@ -36233,18 +34061,6 @@ __metadata: languageName: node linkType: hard -"micromark-extension-frontmatter@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-extension-frontmatter@npm:2.0.0" - dependencies: - fault: "npm:^2.0.0" - micromark-util-character: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/7d0d876e598917a67146d29f536d6fbbf9d1b2401a77e2f64a3f80f934a63ff26fa94b01759c9185c24b2a91e4e6abf908fa7aa246f00a7778a6b37a17464300 - languageName: node - linkType: hard - "micromark-extension-gfm-autolink-literal@npm:^1.0.0": version: 1.0.5 resolution: "micromark-extension-gfm-autolink-literal@npm:1.0.5" @@ -36257,18 +34073,6 @@ __metadata: languageName: node linkType: hard -"micromark-extension-gfm-autolink-literal@npm:^2.0.0": - version: 2.1.0 - resolution: "micromark-extension-gfm-autolink-literal@npm:2.1.0" - dependencies: - micromark-util-character: "npm:^2.0.0" - micromark-util-sanitize-uri: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/84e6fbb84ea7c161dfa179665dc90d51116de4c28f3e958260c0423e5a745372b7dcbc87d3cde98213b532e6812f847eef5ae561c9397d7f7da1e59872ef3efe - languageName: node - linkType: hard - "micromark-extension-gfm-autolink-literal@npm:~0.5.0": version: 0.5.7 resolution: "micromark-extension-gfm-autolink-literal@npm:0.5.7" @@ -36294,22 +34098,6 @@ __metadata: languageName: node linkType: hard -"micromark-extension-gfm-footnote@npm:^2.0.0": - version: 2.1.0 - resolution: "micromark-extension-gfm-footnote@npm:2.1.0" - dependencies: - devlop: "npm:^1.0.0" - micromark-core-commonmark: "npm:^2.0.0" - micromark-factory-space: "npm:^2.0.0" - micromark-util-character: "npm:^2.0.0" - micromark-util-normalize-identifier: "npm:^2.0.0" - micromark-util-sanitize-uri: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/d172e4218968b7371b9321af5cde8c77423f73b233b2b0fcf3ff6fd6f61d2e0d52c49123a9b7910612478bf1f0d5e88c75a3990dd68f70f3933fe812b9f77edc - languageName: node - linkType: hard - "micromark-extension-gfm-strikethrough@npm:^1.0.0": version: 1.0.7 resolution: "micromark-extension-gfm-strikethrough@npm:1.0.7" @@ -36324,20 +34112,6 @@ __metadata: languageName: node linkType: hard -"micromark-extension-gfm-strikethrough@npm:^2.0.0": - version: 2.1.0 - resolution: "micromark-extension-gfm-strikethrough@npm:2.1.0" - dependencies: - devlop: "npm:^1.0.0" - micromark-util-chunked: "npm:^2.0.0" - micromark-util-classify-character: "npm:^2.0.0" - micromark-util-resolve-all: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/ef4f248b865bdda71303b494671b7487808a340b25552b11ca6814dff3fcfaab9be8d294643060bbdb50f79313e4a686ab18b99cbe4d3ee8a4170fcd134234fb - languageName: node - linkType: hard - "micromark-extension-gfm-strikethrough@npm:~0.6.5": version: 0.6.5 resolution: "micromark-extension-gfm-strikethrough@npm:0.6.5" @@ -36360,19 +34134,6 @@ __metadata: languageName: node linkType: hard -"micromark-extension-gfm-table@npm:^2.0.0": - version: 2.1.0 - resolution: "micromark-extension-gfm-table@npm:2.1.0" - dependencies: - devlop: "npm:^1.0.0" - micromark-factory-space: "npm:^2.0.0" - micromark-util-character: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/c1b564ab68576406046d825b9574f5b4dbedbb5c44bede49b5babc4db92f015d9057dd79d8e0530f2fecc8970a695c40ac2e5e1d4435ccf3ef161038d0d1463b - languageName: node - linkType: hard - "micromark-extension-gfm-table@npm:~0.4.0": version: 0.4.3 resolution: "micromark-extension-gfm-table@npm:0.4.3" @@ -36391,15 +34152,6 @@ __metadata: languageName: node linkType: hard -"micromark-extension-gfm-tagfilter@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-extension-gfm-tagfilter@npm:2.0.0" - dependencies: - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/995558843fff137ae4e46aecb878d8a4691cdf23527dcf1e2f0157d66786be9f7bea0109c52a8ef70e68e3f930af811828ba912239438e31a9cfb9981f44d34d - languageName: node - linkType: hard - "micromark-extension-gfm-tagfilter@npm:~0.3.0": version: 0.3.0 resolution: "micromark-extension-gfm-tagfilter@npm:0.3.0" @@ -36420,19 +34172,6 @@ __metadata: languageName: node linkType: hard -"micromark-extension-gfm-task-list-item@npm:^2.0.0": - version: 2.1.0 - resolution: "micromark-extension-gfm-task-list-item@npm:2.1.0" - dependencies: - devlop: "npm:^1.0.0" - micromark-factory-space: "npm:^2.0.0" - micromark-util-character: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/78aa537d929e9309f076ba41e5edc99f78d6decd754b6734519ccbbfca8abd52e1c62df68d41a6ae64d2a3fc1646cea955893c79680b0b4385ced4c52296181f - languageName: node - linkType: hard - "micromark-extension-gfm-task-list-item@npm:~0.3.0": version: 0.3.3 resolution: "micromark-extension-gfm-task-list-item@npm:0.3.3" @@ -36472,22 +34211,6 @@ __metadata: languageName: node linkType: hard -"micromark-extension-gfm@npm:^3.0.0": - version: 3.0.0 - resolution: "micromark-extension-gfm@npm:3.0.0" - dependencies: - micromark-extension-gfm-autolink-literal: "npm:^2.0.0" - micromark-extension-gfm-footnote: "npm:^2.0.0" - micromark-extension-gfm-strikethrough: "npm:^2.0.0" - micromark-extension-gfm-table: "npm:^2.0.0" - micromark-extension-gfm-tagfilter: "npm:^2.0.0" - micromark-extension-gfm-task-list-item: "npm:^2.0.0" - micromark-util-combine-extensions: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/970e28df6ebdd7c7249f52a0dda56e0566fbfa9ae56c8eeeb2445d77b6b89d44096880cd57a1c01e7821b1f4e31009109fbaca4e89731bff7b83b8519690e5d9 - languageName: node - linkType: hard - "micromark-extension-mdx-expression@npm:^1.0.0": version: 1.0.8 resolution: "micromark-extension-mdx-expression@npm:1.0.8" @@ -36504,22 +34227,6 @@ __metadata: languageName: node linkType: hard -"micromark-extension-mdx-expression@npm:^3.0.0": - version: 3.0.0 - resolution: "micromark-extension-mdx-expression@npm:3.0.0" - dependencies: - "@types/estree": "npm:^1.0.0" - devlop: "npm:^1.0.0" - micromark-factory-mdx-expression: "npm:^2.0.0" - micromark-factory-space: "npm:^2.0.0" - micromark-util-character: "npm:^2.0.0" - micromark-util-events-to-acorn: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/fa799c594d8ff9ecbbd28e226959c4928590cfcddb60a926d9d859d00fc7acd25684b6f78dbe6a7f0830879a402b4a3628efd40bb9df1f5846e6d2b7332715f7 - languageName: node - linkType: hard - "micromark-extension-mdx-jsx@npm:^1.0.0": version: 1.0.5 resolution: "micromark-extension-mdx-jsx@npm:1.0.5" @@ -36538,24 +34245,6 @@ __metadata: languageName: node linkType: hard -"micromark-extension-mdx-jsx@npm:^3.0.0": - version: 3.0.0 - resolution: "micromark-extension-mdx-jsx@npm:3.0.0" - dependencies: - "@types/acorn": "npm:^4.0.0" - "@types/estree": "npm:^1.0.0" - devlop: "npm:^1.0.0" - estree-util-is-identifier-name: "npm:^3.0.0" - micromark-factory-mdx-expression: "npm:^2.0.0" - micromark-factory-space: "npm:^2.0.0" - micromark-util-character: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - vfile-message: "npm:^4.0.0" - checksum: 10c0/18a81c8def7f3a2088dc435bba19e649c19f679464b1a01e2c680f9518820e70fb0974b8403c790aee8f44205833a280b56ba157fe5a5b2903b476c5de5ba353 - languageName: node - linkType: hard - "micromark-extension-mdx-md@npm:^1.0.0": version: 1.0.1 resolution: "micromark-extension-mdx-md@npm:1.0.1" @@ -36565,15 +34254,6 @@ __metadata: languageName: node linkType: hard -"micromark-extension-mdx-md@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-extension-mdx-md@npm:2.0.0" - dependencies: - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/bae91c61273de0e5ba80a980c03470e6cd9d7924aa936f46fbda15d780704d9386e945b99eda200e087b96254fbb4271a9545d5ce02676cd6ae67886a8bf82df - languageName: node - linkType: hard - "micromark-extension-mdxjs-esm@npm:^1.0.0": version: 1.0.5 resolution: "micromark-extension-mdxjs-esm@npm:1.0.5" @@ -36591,23 +34271,6 @@ __metadata: languageName: node linkType: hard -"micromark-extension-mdxjs-esm@npm:^3.0.0": - version: 3.0.0 - resolution: "micromark-extension-mdxjs-esm@npm:3.0.0" - dependencies: - "@types/estree": "npm:^1.0.0" - devlop: "npm:^1.0.0" - micromark-core-commonmark: "npm:^2.0.0" - micromark-util-character: "npm:^2.0.0" - micromark-util-events-to-acorn: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - unist-util-position-from-estree: "npm:^2.0.0" - vfile-message: "npm:^4.0.0" - checksum: 10c0/13e3f726495a960650cdedcba39198ace5bdc953ccb12c14d71fc9ed9bb88e40cc3ba9231e973f6984da3b3573e7ddb23ce409f7c16f52a8d57b608bf46c748d - languageName: node - linkType: hard - "micromark-extension-mdxjs@npm:^1.0.0": version: 1.0.1 resolution: "micromark-extension-mdxjs@npm:1.0.1" @@ -36624,22 +34287,6 @@ __metadata: languageName: node linkType: hard -"micromark-extension-mdxjs@npm:^3.0.0": - version: 3.0.0 - resolution: "micromark-extension-mdxjs@npm:3.0.0" - dependencies: - acorn: "npm:^8.0.0" - acorn-jsx: "npm:^5.0.0" - micromark-extension-mdx-expression: "npm:^3.0.0" - micromark-extension-mdx-jsx: "npm:^3.0.0" - micromark-extension-mdx-md: "npm:^2.0.0" - micromark-extension-mdxjs-esm: "npm:^3.0.0" - micromark-util-combine-extensions: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/fd84f036ddad0aabbc12e7f1b3e9dcfe31573bbc413c5ae903779ef0366d7a4c08193547e7ba75718c9f45654e45f52e575cfc2f23a5f89205a8a70d9a506aea - languageName: node - linkType: hard - "micromark-factory-destination@npm:^1.0.0": version: 1.1.0 resolution: "micromark-factory-destination@npm:1.1.0" @@ -36651,17 +34298,6 @@ __metadata: languageName: node linkType: hard -"micromark-factory-destination@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-factory-destination@npm:2.0.0" - dependencies: - micromark-util-character: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/b73492f687d41a6a379159c2f3acbf813042346bcea523d9041d0cc6124e6715f0779dbb2a0b3422719e9764c3b09f9707880aa159557e3cb4aeb03b9d274915 - languageName: node - linkType: hard - "micromark-factory-label@npm:^1.0.0": version: 1.1.0 resolution: "micromark-factory-label@npm:1.1.0" @@ -36674,18 +34310,6 @@ __metadata: languageName: node linkType: hard -"micromark-factory-label@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-factory-label@npm:2.0.0" - dependencies: - devlop: "npm:^1.0.0" - micromark-util-character: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/8ffad00487a7891941b1d1f51d53a33c7a659dcf48617edb7a4008dad7aff67ec316baa16d55ca98ae3d75ce1d81628dbf72fedc7c6f108f740dec0d5d21c8ee - languageName: node - linkType: hard - "micromark-factory-mdx-expression@npm:^1.0.0": version: 1.0.9 resolution: "micromark-factory-mdx-expression@npm:1.0.9" @@ -36702,22 +34326,6 @@ __metadata: languageName: node linkType: hard -"micromark-factory-mdx-expression@npm:^2.0.0": - version: 2.0.1 - resolution: "micromark-factory-mdx-expression@npm:2.0.1" - dependencies: - "@types/estree": "npm:^1.0.0" - devlop: "npm:^1.0.0" - micromark-util-character: "npm:^2.0.0" - micromark-util-events-to-acorn: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - unist-util-position-from-estree: "npm:^2.0.0" - vfile-message: "npm:^4.0.0" - checksum: 10c0/d9cf475a73a7fbfa09aba0d057e033d57e45b7adff78692be9efb4405c4a1717ece4594a632f92a4302e4f8f2ae96355785b616e3f5b2fe8599ec24cfdeee12d - languageName: node - linkType: hard - "micromark-factory-space@npm:^1.0.0": version: 1.1.0 resolution: "micromark-factory-space@npm:1.1.0" @@ -36728,16 +34336,6 @@ __metadata: languageName: node linkType: hard -"micromark-factory-space@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-factory-space@npm:2.0.0" - dependencies: - micromark-util-character: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/103ca954dade963d4ff1d2f27d397833fe855ddc72590205022832ef68b775acdea67949000cee221708e376530b1de78c745267b0bf8366740840783eb37122 - languageName: node - linkType: hard - "micromark-factory-title@npm:^1.0.0": version: 1.1.0 resolution: "micromark-factory-title@npm:1.1.0" @@ -36750,18 +34348,6 @@ __metadata: languageName: node linkType: hard -"micromark-factory-title@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-factory-title@npm:2.0.0" - dependencies: - micromark-factory-space: "npm:^2.0.0" - micromark-util-character: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/2b2188e7a011b1b001faf8c860286d246d5c3485ef8819270c60a5808f4c7613e49d4e481dbdff62600ef7acdba0f5100be2d125cbd2a15e236c26b3668a8ebd - languageName: node - linkType: hard - "micromark-factory-whitespace@npm:^1.0.0": version: 1.1.0 resolution: "micromark-factory-whitespace@npm:1.1.0" @@ -36774,19 +34360,7 @@ __metadata: languageName: node linkType: hard -"micromark-factory-whitespace@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-factory-whitespace@npm:2.0.0" - dependencies: - micromark-factory-space: "npm:^2.0.0" - micromark-util-character: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/4e91baab0cc71873095134bd0e225d01d9786cde352701402d71b72d317973954754e8f9f1849901f165530e6421202209f4d97c460a27bb0808ec5a3fc3148c - languageName: node - linkType: hard - -"micromark-util-character@npm:^1.0.0, micromark-util-character@npm:^1.1.0": +"micromark-util-character@npm:^1.0.0": version: 1.2.0 resolution: "micromark-util-character@npm:1.2.0" dependencies: @@ -36796,16 +34370,6 @@ __metadata: languageName: node linkType: hard -"micromark-util-character@npm:^2.0.0": - version: 2.1.0 - resolution: "micromark-util-character@npm:2.1.0" - dependencies: - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/fc37a76aaa5a5138191ba2bef1ac50c36b3bcb476522e98b1a42304ab4ec76f5b036a746ddf795d3de3e7004b2c09f21dd1bad42d161f39b8cfc0acd067e6373 - languageName: node - linkType: hard - "micromark-util-chunked@npm:^1.0.0": version: 1.1.0 resolution: "micromark-util-chunked@npm:1.1.0" @@ -36815,15 +34379,6 @@ __metadata: languageName: node linkType: hard -"micromark-util-chunked@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-chunked@npm:2.0.0" - dependencies: - micromark-util-symbol: "npm:^2.0.0" - checksum: 10c0/043b5f2abc8c13a1e2e4c378ead191d1a47ed9e0cd6d0fa5a0a430b2df9e17ada9d5de5a20688a000bbc5932507e746144acec60a9589d9a79fa60918e029203 - languageName: node - linkType: hard - "micromark-util-classify-character@npm:^1.0.0": version: 1.1.0 resolution: "micromark-util-classify-character@npm:1.1.0" @@ -36835,17 +34390,6 @@ __metadata: languageName: node linkType: hard -"micromark-util-classify-character@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-classify-character@npm:2.0.0" - dependencies: - micromark-util-character: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/2bf5fa5050faa9b69f6c7e51dbaaf02329ab70fabad8229984381b356afbbf69db90f4617bec36d814a7d285fb7cad8e3c4e38d1daf4387dc9e240aa7f9a292a - languageName: node - linkType: hard - "micromark-util-combine-extensions@npm:^1.0.0": version: 1.1.0 resolution: "micromark-util-combine-extensions@npm:1.1.0" @@ -36856,16 +34400,6 @@ __metadata: languageName: node linkType: hard -"micromark-util-combine-extensions@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-combine-extensions@npm:2.0.0" - dependencies: - micromark-util-chunked: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/cd4c8d1a85255527facb419ff3b3cc3d7b7f27005c5ef5fa7ef2c4d0e57a9129534fc292a188ec2d467c2c458642d369c5f894bc8a9e142aed6696cc7989d3ea - languageName: node - linkType: hard - "micromark-util-decode-numeric-character-reference@npm:^1.0.0": version: 1.1.0 resolution: "micromark-util-decode-numeric-character-reference@npm:1.1.0" @@ -36875,15 +34409,6 @@ __metadata: languageName: node linkType: hard -"micromark-util-decode-numeric-character-reference@npm:^2.0.0": - version: 2.0.1 - resolution: "micromark-util-decode-numeric-character-reference@npm:2.0.1" - dependencies: - micromark-util-symbol: "npm:^2.0.0" - checksum: 10c0/3f6d684ee8f317c67806e19b3e761956256cb936a2e0533aad6d49ac5604c6536b2041769c6febdd387ab7175b7b7e551851bf2c1f78da943e7a3671ca7635ac - languageName: node - linkType: hard - "micromark-util-decode-string@npm:^1.0.0": version: 1.1.0 resolution: "micromark-util-decode-string@npm:1.1.0" @@ -36896,18 +34421,6 @@ __metadata: languageName: node linkType: hard -"micromark-util-decode-string@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-decode-string@npm:2.0.0" - dependencies: - decode-named-character-reference: "npm:^1.0.0" - micromark-util-character: "npm:^2.0.0" - micromark-util-decode-numeric-character-reference: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - checksum: 10c0/f5413bebb21bdb686cfa1bcfa7e9c93093a523d1b42443ead303b062d2d680a94e5e8424549f57b8ba9d786a758e5a26a97f56068991bbdbca5d1885b3aa7227 - languageName: node - linkType: hard - "micromark-util-encode@npm:^1.0.0": version: 1.1.0 resolution: "micromark-util-encode@npm:1.1.0" @@ -36915,13 +34428,6 @@ __metadata: languageName: node linkType: hard -"micromark-util-encode@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-encode@npm:2.0.0" - checksum: 10c0/ebdaafff23100bbf4c74e63b4b1612a9ddf94cd7211d6a076bc6fb0bc32c1b48d6fb615aa0953e607c62c97d849f97f1042260d3eb135259d63d372f401bbbb2 - languageName: node - linkType: hard - "micromark-util-events-to-acorn@npm:^1.0.0": version: 1.2.3 resolution: "micromark-util-events-to-acorn@npm:1.2.3" @@ -36938,22 +34444,6 @@ __metadata: languageName: node linkType: hard -"micromark-util-events-to-acorn@npm:^2.0.0": - version: 2.0.2 - resolution: "micromark-util-events-to-acorn@npm:2.0.2" - dependencies: - "@types/acorn": "npm:^4.0.0" - "@types/estree": "npm:^1.0.0" - "@types/unist": "npm:^3.0.0" - devlop: "npm:^1.0.0" - estree-util-visit: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - vfile-message: "npm:^4.0.0" - checksum: 10c0/2bd2660a49efddb625e6adcabdc3384ae4c50c7a04270737270f4aab53d09e8253e6d2607cd947c4c77f8a9900278915babb240e61fd143dc5bab51d9fd50709 - languageName: node - linkType: hard - "micromark-util-html-tag-name@npm:^1.0.0": version: 1.2.0 resolution: "micromark-util-html-tag-name@npm:1.2.0" @@ -36961,13 +34451,6 @@ __metadata: languageName: node linkType: hard -"micromark-util-html-tag-name@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-html-tag-name@npm:2.0.0" - checksum: 10c0/988aa26367449bd345b627ae32cf605076daabe2dc1db71b578a8a511a47123e14af466bcd6dcbdacec60142f07bc2723ec5f7a0eed0f5319ce83b5e04825429 - languageName: node - linkType: hard - "micromark-util-normalize-identifier@npm:^1.0.0": version: 1.1.0 resolution: "micromark-util-normalize-identifier@npm:1.1.0" @@ -36977,15 +34460,6 @@ __metadata: languageName: node linkType: hard -"micromark-util-normalize-identifier@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-normalize-identifier@npm:2.0.0" - dependencies: - micromark-util-symbol: "npm:^2.0.0" - checksum: 10c0/93bf8789b8449538f22cf82ac9b196363a5f3b2f26efd98aef87c4c1b1f8c05be3ef6391ff38316ff9b03c1a6fd077342567598019ddd12b9bd923dacc556333 - languageName: node - linkType: hard - "micromark-util-resolve-all@npm:^1.0.0": version: 1.1.0 resolution: "micromark-util-resolve-all@npm:1.1.0" @@ -36995,15 +34469,6 @@ __metadata: languageName: node linkType: hard -"micromark-util-resolve-all@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-resolve-all@npm:2.0.0" - dependencies: - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/3b912e88453dcefe728a9080c8934a75ac4732056d6576ceecbcaf97f42c5d6fa2df66db8abdc8427eb167c5ffddefe26713728cfe500bc0e314ed260d6e2746 - languageName: node - linkType: hard - "micromark-util-sanitize-uri@npm:^1.0.0, micromark-util-sanitize-uri@npm:^1.1.0": version: 1.2.0 resolution: "micromark-util-sanitize-uri@npm:1.2.0" @@ -37015,17 +34480,6 @@ __metadata: languageName: node linkType: hard -"micromark-util-sanitize-uri@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-sanitize-uri@npm:2.0.0" - dependencies: - micromark-util-character: "npm:^2.0.0" - micromark-util-encode: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - checksum: 10c0/74763ca1c927dd520d3ab8fd9856a19740acf76fc091f0a1f5d4e99c8cd5f1b81c5a0be3efb564941a071fb6d85fd951103f2760eb6cff77b5ab3abe08341309 - languageName: node - linkType: hard - "micromark-util-subtokenize@npm:^1.0.0": version: 1.1.0 resolution: "micromark-util-subtokenize@npm:1.1.0" @@ -37038,32 +34492,13 @@ __metadata: languageName: node linkType: hard -"micromark-util-subtokenize@npm:^2.0.0": - version: 2.0.1 - resolution: "micromark-util-subtokenize@npm:2.0.1" - dependencies: - devlop: "npm:^1.0.0" - micromark-util-chunked: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/000cefde827db129f4ed92b8fbdeb4866c5f9c93068c0115485564b0426abcb9058080aa257df9035e12ca7fa92259d66623ea750b9eb3bcdd8325d3fb6fc237 - languageName: node - linkType: hard - -"micromark-util-symbol@npm:^1.0.0, micromark-util-symbol@npm:^1.0.1": +"micromark-util-symbol@npm:^1.0.0": version: 1.1.0 resolution: "micromark-util-symbol@npm:1.1.0" checksum: 10c0/10ceaed33a90e6bfd3a5d57053dbb53f437d4809cc11430b5a09479c0ba601577059be9286df4a7eae6e350a60a2575dc9fa9d9872b5b8d058c875e075c33803 languageName: node linkType: hard -"micromark-util-symbol@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-symbol@npm:2.0.0" - checksum: 10c0/4e76186c185ce4cefb9cea8584213d9ffacd77099d1da30c0beb09fa21f46f66f6de4c84c781d7e34ff763fe3a06b530e132fa9004882afab9e825238d0aa8b3 - languageName: node - linkType: hard - "micromark-util-types@npm:^1.0.0, micromark-util-types@npm:^1.0.1": version: 1.1.0 resolution: "micromark-util-types@npm:1.1.0" @@ -37071,13 +34506,6 @@ __metadata: languageName: node linkType: hard -"micromark-util-types@npm:^2.0.0": - version: 2.0.0 - resolution: "micromark-util-types@npm:2.0.0" - checksum: 10c0/d74e913b9b61268e0d6939f4209e3abe9dada640d1ee782419b04fd153711112cfaaa3c4d5f37225c9aee1e23c3bb91a1f5223e1e33ba92d33e83956a53e61de - languageName: node - linkType: hard - "micromark@npm:^2.11.3, micromark@npm:~2.11.0, micromark@npm:~2.11.3": version: 2.11.4 resolution: "micromark@npm:2.11.4" @@ -37113,31 +34541,6 @@ __metadata: languageName: node linkType: hard -"micromark@npm:^4.0.0": - version: 4.0.0 - resolution: "micromark@npm:4.0.0" - dependencies: - "@types/debug": "npm:^4.0.0" - debug: "npm:^4.0.0" - decode-named-character-reference: "npm:^1.0.0" - devlop: "npm:^1.0.0" - micromark-core-commonmark: "npm:^2.0.0" - micromark-factory-space: "npm:^2.0.0" - micromark-util-character: "npm:^2.0.0" - micromark-util-chunked: "npm:^2.0.0" - micromark-util-combine-extensions: "npm:^2.0.0" - micromark-util-decode-numeric-character-reference: "npm:^2.0.0" - micromark-util-encode: "npm:^2.0.0" - micromark-util-normalize-identifier: "npm:^2.0.0" - micromark-util-resolve-all: "npm:^2.0.0" - micromark-util-sanitize-uri: "npm:^2.0.0" - micromark-util-subtokenize: "npm:^2.0.0" - micromark-util-symbol: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - checksum: 10c0/7e91c8d19ff27bc52964100853f1b3b32bb5b2ece57470a34ba1b2f09f4e2a183d90106c4ae585c9f2046969ee088576fed79b2f7061cba60d16652ccc2c64fd - languageName: node - linkType: hard - "micromatch@npm:^4.0.0, micromatch@npm:^4.0.2, micromatch@npm:^4.0.4, micromatch@npm:^4.0.5": version: 4.0.7 resolution: "micromatch@npm:4.0.7" @@ -37181,13 +34584,6 @@ __metadata: languageName: node linkType: hard -"mime-db@npm:~1.33.0": - version: 1.33.0 - resolution: "mime-db@npm:1.33.0" - checksum: 10c0/79172ce5468c8503b49dddfdddc18d3f5fe2599f9b5fe1bc321a8cbee14c96730fc6db22f907b23701b05b2936f865795f62ec3a78a7f3c8cb2450bb68c6763e - languageName: node - linkType: hard - "mime-format@npm:2.0.1": version: 2.0.1 resolution: "mime-format@npm:2.0.1" @@ -37197,16 +34593,7 @@ __metadata: languageName: node linkType: hard -"mime-types@npm:2.1.18": - version: 2.1.18 - resolution: "mime-types@npm:2.1.18" - dependencies: - mime-db: "npm:~1.33.0" - checksum: 10c0/a96a8d12f4bb98bc7bfac6a8ccbd045f40368fc1030d9366050c3613825d3715d1c1f393e10a75a885d2cdc1a26cd6d5e11f3a2a0d5c4d361f00242139430a0f - languageName: node - linkType: hard - -"mime-types@npm:2.1.35, mime-types@npm:^2.1.12, mime-types@npm:^2.1.25, mime-types@npm:^2.1.27, mime-types@npm:^2.1.31, mime-types@npm:~2.1.17, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": +"mime-types@npm:2.1.35, mime-types@npm:^2.1.12, mime-types@npm:^2.1.25, mime-types@npm:^2.1.27, mime-types@npm:~2.1.19, mime-types@npm:~2.1.24, mime-types@npm:~2.1.34": version: 2.1.35 resolution: "mime-types@npm:2.1.35" dependencies: @@ -37270,13 +34657,6 @@ __metadata: languageName: node linkType: hard -"mimic-response@npm:^4.0.0": - version: 4.0.0 - resolution: "mimic-response@npm:4.0.0" - checksum: 10c0/761d788d2668ae9292c489605ffd4fad220f442fbae6832adce5ebad086d691e906a6d5240c290293c7a11e99fbdbbef04abbbed498bf8699a4ee0f31315e3fb - languageName: node - linkType: hard - "min-indent@npm:^1.0.0, min-indent@npm:^1.0.1": version: 1.0.1 resolution: "min-indent@npm:1.0.1" @@ -37284,18 +34664,6 @@ __metadata: languageName: node linkType: hard -"mini-css-extract-plugin@npm:^2.7.6": - version: 2.9.0 - resolution: "mini-css-extract-plugin@npm:2.9.0" - dependencies: - schema-utils: "npm:^4.0.0" - tapable: "npm:^2.2.1" - peerDependencies: - webpack: ^5.0.0 - checksum: 10c0/46e20747ea250420db8a82801b9779299ce3cd5ec4d6dd75e00904c39cc80f0f01decaa534b8cb9658d7d3b656b919cb2cc84b1ba7e2394d2d6548578a5c2901 - languageName: node - linkType: hard - "minimalistic-assert@npm:^1.0.0, minimalistic-assert@npm:^1.0.1": version: 1.0.1 resolution: "minimalistic-assert@npm:1.0.1" @@ -37310,15 +34678,6 @@ __metadata: languageName: node linkType: hard -"minimatch@npm:3.1.2, minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": - version: 3.1.2 - resolution: "minimatch@npm:3.1.2" - dependencies: - brace-expansion: "npm:^1.1.7" - checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311 - languageName: node - linkType: hard - "minimatch@npm:4.2.3": version: 4.2.3 resolution: "minimatch@npm:4.2.3" @@ -37346,6 +34705,15 @@ __metadata: languageName: node linkType: hard +"minimatch@npm:^3.0.2, minimatch@npm:^3.0.4, minimatch@npm:^3.0.5, minimatch@npm:^3.1.1, minimatch@npm:^3.1.2": + version: 3.1.2 + resolution: "minimatch@npm:3.1.2" + dependencies: + brace-expansion: "npm:^1.1.7" + checksum: 10c0/0262810a8fc2e72cca45d6fd86bd349eee435eb95ac6aa45c9ea2180e7ee875ef44c32b55b5973ceabe95ea12682f6e3725cbb63d7a2d1da3ae1163c8b210311 + languageName: node + linkType: hard + "minimatch@npm:^5.0.1, minimatch@npm:^5.1.0": version: 5.1.6 resolution: "minimatch@npm:5.1.6" @@ -37812,18 +35180,6 @@ __metadata: languageName: node linkType: hard -"multicast-dns@npm:^7.2.5": - version: 7.2.5 - resolution: "multicast-dns@npm:7.2.5" - dependencies: - dns-packet: "npm:^5.2.2" - thunky: "npm:^1.0.2" - bin: - multicast-dns: cli.js - checksum: 10c0/5120171d4bdb1577764c5afa96e413353bff530d1b37081cb29cccc747f989eb1baf40574fe8e27060fc1aef72b59c042f72b9b208413de33bcf411343c69057 - languageName: node - linkType: hard - "multimatch@npm:^5.0.0": version: 5.0.0 resolution: "multimatch@npm:5.0.0" @@ -38237,18 +35593,6 @@ __metadata: languageName: node linkType: hard -"node-emoji@npm:^2.1.0": - version: 2.1.3 - resolution: "node-emoji@npm:2.1.3" - dependencies: - "@sindresorhus/is": "npm:^4.6.0" - char-regex: "npm:^1.0.2" - emojilib: "npm:^2.4.0" - skin-tone: "npm:^2.0.0" - checksum: 10c0/e688333373563aa8308df16111eee2b5837b53a51fb63bf8b7fbea2896327c5d24c9984eb0c8ca6ac155d4d9c194dcf1840d271033c1b588c7c45a3b65339ef7 - languageName: node - linkType: hard - "node-fetch-native@npm:^1.6.3": version: 1.6.4 resolution: "node-fetch-native@npm:1.6.4" @@ -38284,7 +35628,7 @@ __metadata: languageName: node linkType: hard -"node-forge@npm:^1, node-forge@npm:^1.3.1": +"node-forge@npm:^1.3.1": version: 1.3.1 resolution: "node-forge@npm:1.3.1" checksum: 10c0/e882819b251a4321f9fc1d67c85d1501d3004b4ee889af822fd07f64de3d1a8e272ff00b689570af0465d65d6bf5074df9c76e900e0aff23e60b847f2a46fbe8 @@ -38390,40 +35734,6 @@ __metadata: languageName: node linkType: hard -"node-polyfill-webpack-plugin@npm:^1.1.2": - version: 1.1.4 - resolution: "node-polyfill-webpack-plugin@npm:1.1.4" - dependencies: - assert: "npm:^2.0.0" - browserify-zlib: "npm:^0.2.0" - buffer: "npm:^6.0.3" - console-browserify: "npm:^1.2.0" - constants-browserify: "npm:^1.0.0" - crypto-browserify: "npm:^3.12.0" - domain-browser: "npm:^4.19.0" - events: "npm:^3.3.0" - filter-obj: "npm:^2.0.2" - https-browserify: "npm:^1.0.0" - os-browserify: "npm:^0.3.0" - path-browserify: "npm:^1.0.1" - process: "npm:^0.11.10" - punycode: "npm:^2.1.1" - querystring-es3: "npm:^0.2.1" - readable-stream: "npm:^3.6.0" - stream-browserify: "npm:^3.0.0" - stream-http: "npm:^3.2.0" - string_decoder: "npm:^1.3.0" - timers-browserify: "npm:^2.0.12" - tty-browserify: "npm:^0.0.1" - url: "npm:^0.11.0" - util: "npm:^0.12.4" - vm-browserify: "npm:^1.1.2" - peerDependencies: - webpack: ">=5" - checksum: 10c0/7536ede8c9254aa16e97bc16a81b736931d5d1b8345267dac46b5ea1bf276ee95d45e8e8fbce7b7d9eb0d7acbc5be881342096d52d9f6a8236c637299dbe6ad7 - languageName: node - linkType: hard - "node-preload@npm:^0.2.1": version: 0.2.1 resolution: "node-preload@npm:0.2.1" @@ -38520,13 +35830,6 @@ __metadata: languageName: node linkType: hard -"normalize-range@npm:^0.1.2": - version: 0.1.2 - resolution: "normalize-range@npm:0.1.2" - checksum: 10c0/bf39b73a63e0a42ad1a48c2bd1bda5a07ede64a7e2567307a407674e595bcff0fa0d57e8e5f1e7fa5e91000797c7615e13613227aaaa4d6d6e87f5bd5cc95de6 - languageName: node - linkType: hard - "normalize-url@npm:^4.1.0": version: 4.5.1 resolution: "normalize-url@npm:4.5.1" @@ -38541,13 +35844,6 @@ __metadata: languageName: node linkType: hard -"normalize-url@npm:^8.0.0": - version: 8.0.1 - resolution: "normalize-url@npm:8.0.1" - checksum: 10c0/eb439231c4b84430f187530e6fdac605c5048ef4ec556447a10c00a91fc69b52d8d8298d9d608e68d3e0f7dc2d812d3455edf425e0f215993667c3183bcab1ef - languageName: node - linkType: hard - "npm-bundled@npm:^1.1.1": version: 1.1.2 resolution: "npm-bundled@npm:1.1.2" @@ -38772,13 +36068,6 @@ __metadata: languageName: node linkType: hard -"nprogress@npm:^0.2.0": - version: 0.2.0 - resolution: "nprogress@npm:0.2.0" - checksum: 10c0/eab9a923a1ad1eed71a455ecfbc358442dd9bcd71b9fa3fa1c67eddf5159360b182c218f76fca320c97541a1b45e19ced04e6dcb044a662244c5419f8ae9e821 - languageName: node - linkType: hard - "nth-check@npm:^2.0.0, nth-check@npm:^2.0.1": version: 2.1.1 resolution: "nth-check@npm:2.1.1" @@ -39012,7 +36301,7 @@ __metadata: languageName: node linkType: hard -"object.assign@npm:^4.1.0, object.assign@npm:^4.1.4, object.assign@npm:^4.1.5": +"object.assign@npm:^4.1.4, object.assign@npm:^4.1.5": version: 4.1.5 resolution: "object.assign@npm:4.1.5" dependencies: @@ -39085,7 +36374,7 @@ __metadata: languageName: node linkType: hard -"obuf@npm:^1.0.0, obuf@npm:^1.1.2, obuf@npm:~1.1.2": +"obuf@npm:~1.1.2": version: 1.1.2 resolution: "obuf@npm:1.1.2" checksum: 10c0/520aaac7ea701618eacf000fc96ae458e20e13b0569845800fc582f81b386731ab22d55354b4915d58171db00e79cfcd09c1638c02f89577ef092b38c65b7d81 @@ -39152,7 +36441,7 @@ __metadata: languageName: node linkType: hard -"open@npm:^8.0.4, open@npm:^8.0.9, open@npm:^8.4.0": +"open@npm:^8.0.4, open@npm:^8.4.0": version: 8.4.2 resolution: "open@npm:8.4.2" dependencies: @@ -39201,7 +36490,7 @@ __metadata: languageName: node linkType: hard -"opener@npm:^1.5.1, opener@npm:^1.5.2": +"opener@npm:^1.5.1": version: 1.5.2 resolution: "opener@npm:1.5.2" bin: @@ -39293,7 +36582,7 @@ __metadata: languageName: node linkType: hard -"os-browserify@npm:^0.3.0, os-browserify@npm:~0.3.0": +"os-browserify@npm:~0.3.0": version: 0.3.0 resolution: "os-browserify@npm:0.3.0" checksum: 10c0/6ff32cb1efe2bc6930ad0fd4c50e30c38010aee909eba8d65be60af55efd6cbb48f0287e3649b4e3f3a63dce5a667b23c187c4293a75e557f0d5489d735bcf52 @@ -39385,13 +36674,6 @@ __metadata: languageName: node linkType: hard -"p-cancelable@npm:^3.0.0": - version: 3.0.0 - resolution: "p-cancelable@npm:3.0.0" - checksum: 10c0/948fd4f8e87b956d9afc2c6c7392de9113dac817cb1cecf4143f7a3d4c57ab5673614a80be3aba91ceec5e4b69fd8c869852d7e8048bc3d9273c4c36ce14b9aa - languageName: node - linkType: hard - "p-finally@npm:^1.0.0": version: 1.0.0 resolution: "p-finally@npm:1.0.0" @@ -39417,15 +36699,6 @@ __metadata: languageName: node linkType: hard -"p-limit@npm:^4.0.0": - version: 4.0.0 - resolution: "p-limit@npm:4.0.0" - dependencies: - yocto-queue: "npm:^1.0.0" - checksum: 10c0/a56af34a77f8df2ff61ddfb29431044557fcbcb7642d5a3233143ebba805fc7306ac1d448de724352861cb99de934bc9ab74f0d16fe6a5460bdbdf938de875ad - languageName: node - linkType: hard - "p-limit@npm:^5.0.0": version: 5.0.0 resolution: "p-limit@npm:5.0.0" @@ -39462,15 +36735,6 @@ __metadata: languageName: node linkType: hard -"p-locate@npm:^6.0.0": - version: 6.0.0 - resolution: "p-locate@npm:6.0.0" - dependencies: - p-limit: "npm:^4.0.0" - checksum: 10c0/d72fa2f41adce59c198270aa4d3c832536c87a1806e0f69dffb7c1a7ca998fb053915ca833d90f166a8c082d3859eabfed95f01698a3214c20df6bb8de046312 - languageName: node - linkType: hard - "p-map@npm:^3.0.0": version: 3.0.0 resolution: "p-map@npm:3.0.0" @@ -39499,7 +36763,7 @@ __metadata: languageName: node linkType: hard -"p-retry@npm:4, p-retry@npm:^4.5.0": +"p-retry@npm:4": version: 4.6.2 resolution: "p-retry@npm:4.6.2" dependencies: @@ -39556,18 +36820,6 @@ __metadata: languageName: node linkType: hard -"package-json@npm:^8.1.0": - version: 8.1.1 - resolution: "package-json@npm:8.1.1" - dependencies: - got: "npm:^12.1.0" - registry-auth-token: "npm:^5.0.1" - registry-url: "npm:^6.0.0" - semver: "npm:^7.3.7" - checksum: 10c0/83b057878bca229033aefad4ef51569b484e63a65831ddf164dc31f0486817e17ffcb58c819c7af3ef3396042297096b3ffc04e107fd66f8f48756f6d2071c8f - languageName: node - linkType: hard - "pacote@npm:^11.1.11, pacote@npm:^11.2.6, pacote@npm:^11.3.5": version: 11.3.5 resolution: "pacote@npm:11.3.5" @@ -39791,13 +37043,6 @@ __metadata: languageName: node linkType: hard -"parse-numeric-range@npm:^1.3.0": - version: 1.3.0 - resolution: "parse-numeric-range@npm:1.3.0" - checksum: 10c0/53465afaa92111e86697281b684aa4574427360889cc23a1c215488c06b72441febdbf09f47ab0bef9a0c701e059629f3eebd2fe6fb241a254ad7a7a642aebe8 - languageName: node - linkType: hard - "parse-passwd@npm:^1.0.0": version: 1.0.0 resolution: "parse-passwd@npm:1.0.0" @@ -39857,7 +37102,7 @@ __metadata: languageName: node linkType: hard -"parseurl@npm:^1.3.3, parseurl@npm:~1.3.2, parseurl@npm:~1.3.3": +"parseurl@npm:^1.3.3, parseurl@npm:~1.3.3": version: 1.3.3 resolution: "parseurl@npm:1.3.3" checksum: 10c0/90dd4760d6f6174adb9f20cf0965ae12e23879b5f5464f38e92fce8073354341e4b3b76fa3d878351efe7d01e617121955284cfd002ab087fba1a0726ec0b4f5 @@ -40008,13 +37253,6 @@ __metadata: languageName: node linkType: hard -"path-exists@npm:^5.0.0": - version: 5.0.0 - resolution: "path-exists@npm:5.0.0" - checksum: 10c0/b170f3060b31604cde93eefdb7392b89d832dfbc1bed717c9718cbe0f230c1669b7e75f87e19901da2250b84d092989a0f9e44d2ef41deb09aa3ad28e691a40a - languageName: node - linkType: hard - "path-is-absolute@npm:^1.0.0, path-is-absolute@npm:^1.0.1": version: 1.0.1 resolution: "path-is-absolute@npm:1.0.1" @@ -40022,13 +37260,6 @@ __metadata: languageName: node linkType: hard -"path-is-inside@npm:1.0.2": - version: 1.0.2 - resolution: "path-is-inside@npm:1.0.2" - checksum: 10c0/7fdd4b41672c70461cce734fc222b33e7b447fa489c7c4377c95e7e6852d83d69741f307d88ec0cc3b385b41cb4accc6efac3c7c511cd18512e95424f5fa980c - languageName: node - linkType: hard - "path-key@npm:^2.0.0": version: 2.0.1 resolution: "path-key@npm:2.0.1" @@ -40104,13 +37335,6 @@ __metadata: languageName: node linkType: hard -"path-to-regexp@npm:2.2.1": - version: 2.2.1 - resolution: "path-to-regexp@npm:2.2.1" - checksum: 10c0/f4b51090a73dad5ce0720f13ce8528ac77914bc927d72cc4ba05ab32770ad3a8d2e431962734b688b9ed863d4098d858da6ff4746037e4e24259cbd3b2c32b79 - languageName: node - linkType: hard - "path-to-regexp@npm:3.2.0": version: 3.2.0 resolution: "path-to-regexp@npm:3.2.0" @@ -40364,6 +37588,13 @@ __metadata: languageName: node linkType: hard +"picocolors@npm:^1.1.0": + version: 1.1.0 + resolution: "picocolors@npm:1.1.0" + checksum: 10c0/86946f6032148801ef09c051c6fb13b5cf942eaf147e30ea79edb91dd32d700934edebe782a1078ff859fb2b816792e97ef4dab03d7f0b804f6b01a0df35e023 + languageName: node + linkType: hard + "picomatch@npm:3.0.1": version: 3.0.1 resolution: "picomatch@npm:3.0.1" @@ -40459,15 +37690,6 @@ __metadata: languageName: node linkType: hard -"pkg-dir@npm:^7.0.0": - version: 7.0.0 - resolution: "pkg-dir@npm:7.0.0" - dependencies: - find-up: "npm:^6.3.0" - checksum: 10c0/1afb23d2efb1ec9d8b2c4a0c37bf146822ad2774f074cb05b853be5dca1b40815c5960dd126df30ab8908349262a266f31b771e877235870a3b8fd313beebec5 - languageName: node - linkType: hard - "pkg-types@npm:^1.0.3, pkg-types@npm:^1.1.1": version: 1.1.3 resolution: "pkg-types@npm:1.1.3" @@ -40479,15 +37701,6 @@ __metadata: languageName: node linkType: hard -"pkg-up@npm:^3.1.0": - version: 3.1.0 - resolution: "pkg-up@npm:3.1.0" - dependencies: - find-up: "npm:^3.0.0" - checksum: 10c0/ecb60e1f8e1f611c0bdf1a0b6a474d6dfb51185567dc6f29cdef37c8d480ecba5362e006606bb290519bbb6f49526c403fabea93c3090c20368d98bb90c999ab - languageName: node - linkType: hard - "planer@npm:^1.2.0": version: 1.2.0 resolution: "planer@npm:1.2.0" @@ -40577,91 +37790,6 @@ __metadata: languageName: node linkType: hard -"postcss-calc@npm:^9.0.1": - version: 9.0.1 - resolution: "postcss-calc@npm:9.0.1" - dependencies: - postcss-selector-parser: "npm:^6.0.11" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.2.2 - checksum: 10c0/e0df07337162dbcaac5d6e030c7fd289e21da8766a9daca5d6b2b3c8094bb524ae5d74c70048ea7fe5fe4960ce048c60ac97922d917c3bbff34f58e9d2b0eb0e - languageName: node - linkType: hard - -"postcss-colormin@npm:^6.1.0": - version: 6.1.0 - resolution: "postcss-colormin@npm:6.1.0" - dependencies: - browserslist: "npm:^4.23.0" - caniuse-api: "npm:^3.0.0" - colord: "npm:^2.9.3" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/0802963fa0d8f2fe408b2e088117670f5303c69a58c135f0ecf0e5ceff69e95e87111b22c4e29c9adb2f69aa8d3bc175f4e8e8708eeb99c9ffc36c17064de427 - languageName: node - linkType: hard - -"postcss-convert-values@npm:^6.1.0": - version: 6.1.0 - resolution: "postcss-convert-values@npm:6.1.0" - dependencies: - browserslist: "npm:^4.23.0" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/a80066965cb58fe8fcaf79f306b32c83fc678e1f0678e43f4db3e9fee06eed6db92cf30631ad348a17492769d44757400493c91a33ee865ee8dedea9234a11f5 - languageName: node - linkType: hard - -"postcss-discard-comments@npm:^6.0.2": - version: 6.0.2 - resolution: "postcss-discard-comments@npm:6.0.2" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/338a1fcba7e2314d956e5e5b9bd1e12e6541991bf85ac72aed6e229a029bf60edb31f11576b677623576169aa7d9c75e1be259ac7b50d0b735b841b5518f9da9 - languageName: node - linkType: hard - -"postcss-discard-duplicates@npm:^6.0.3": - version: 6.0.3 - resolution: "postcss-discard-duplicates@npm:6.0.3" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/24d2f00e54668f2837eb38a64b1751d7a4a73b2752f9749e61eb728f1fae837984bc2b339f7f5207aff5f66f72551253489114b59b9ba21782072677a81d7d1b - languageName: node - linkType: hard - -"postcss-discard-empty@npm:^6.0.3": - version: 6.0.3 - resolution: "postcss-discard-empty@npm:6.0.3" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/1af08bb29f18eda41edf3602b257d89a4cf0a16f79fc773cfebd4a37251f8dbd9b77ac18efe55d0677d000b43a8adf2ef9328d31961c810e9433a38494a1fa65 - languageName: node - linkType: hard - -"postcss-discard-overridden@npm:^6.0.2": - version: 6.0.2 - resolution: "postcss-discard-overridden@npm:6.0.2" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/fda70ef3cd4cb508369c5bbbae44d7760c40ec9f2e65df1cd1b6e0314317fb1d25ae7f64987ca84e66889c1e9d1862487a6ce391c159dfe04d536597bfc5030d - languageName: node - linkType: hard - -"postcss-discard-unused@npm:^6.0.5": - version: 6.0.5 - resolution: "postcss-discard-unused@npm:6.0.5" - dependencies: - postcss-selector-parser: "npm:^6.0.16" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/fca82f17395a7fcc78eab4e03dfb05958beb240c10cacb3836b832c6ea99f5259980c70890a9b7d8b67adf8071b61f3fcf1b432c7a116397aaf67909366da5cc - languageName: node - linkType: hard - "postcss-load-config@npm:^6.0.1": version: 6.0.1 resolution: "postcss-load-config@npm:6.0.1" @@ -40685,106 +37813,6 @@ __metadata: languageName: node linkType: hard -"postcss-loader@npm:^7.3.3": - version: 7.3.4 - resolution: "postcss-loader@npm:7.3.4" - dependencies: - cosmiconfig: "npm:^8.3.5" - jiti: "npm:^1.20.0" - semver: "npm:^7.5.4" - peerDependencies: - postcss: ^7.0.0 || ^8.0.1 - webpack: ^5.0.0 - checksum: 10c0/1bf7614aeea9ad1f8ee6be3a5451576c059391688ea67f825aedc2674056369597faeae4e4a81fe10843884c9904a71403d9a54197e1f560e8fbb9e61f2a2680 - languageName: node - linkType: hard - -"postcss-merge-idents@npm:^6.0.3": - version: 6.0.3 - resolution: "postcss-merge-idents@npm:6.0.3" - dependencies: - cssnano-utils: "npm:^4.0.2" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/fdb51d971df33218bd5fdd9619e5a4d854e23affcea51f96bf4391260cb8d0bec937854582fa9a19bde1fa1b2a43fa5a2f179da23a3adeb8e8d292a4749a8ed7 - languageName: node - linkType: hard - -"postcss-merge-longhand@npm:^6.0.5": - version: 6.0.5 - resolution: "postcss-merge-longhand@npm:6.0.5" - dependencies: - postcss-value-parser: "npm:^4.2.0" - stylehacks: "npm:^6.1.1" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/5a223a7f698c05ab42e9997108a7ff27ea1e0c33a11a353d65a04fc89c3b5b750b9e749550d76b6406329117a055adfc79dde7fee48dca5c8e167a2854ae3fea - languageName: node - linkType: hard - -"postcss-merge-rules@npm:^6.1.1": - version: 6.1.1 - resolution: "postcss-merge-rules@npm:6.1.1" - dependencies: - browserslist: "npm:^4.23.0" - caniuse-api: "npm:^3.0.0" - cssnano-utils: "npm:^4.0.2" - postcss-selector-parser: "npm:^6.0.16" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/6d8952dbb19b1e59bf5affe0871fa1be6515103466857cff5af879d6cf619659f8642ec7a931cabb7cdbd393d8c1e91748bf70bee70fa3edea010d4e25786d04 - languageName: node - linkType: hard - -"postcss-minify-font-values@npm:^6.1.0": - version: 6.1.0 - resolution: "postcss-minify-font-values@npm:6.1.0" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/0d6567170c22a7db42096b5eac298f041614890fbe01759a9fa5ccda432f2bb09efd399d92c11bf6675ae13ccd259db4602fad3c358317dee421df5f7ab0a003 - languageName: node - linkType: hard - -"postcss-minify-gradients@npm:^6.0.3": - version: 6.0.3 - resolution: "postcss-minify-gradients@npm:6.0.3" - dependencies: - colord: "npm:^2.9.3" - cssnano-utils: "npm:^4.0.2" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/7fcbcec94fe5455b89fe1b424a451198e60e0407c894bbacdc062d9fdef2f8571b483b5c3bb17f22d2f1249431251b2de22e1e4e8b0614d10624f8ee6e71afd2 - languageName: node - linkType: hard - -"postcss-minify-params@npm:^6.1.0": - version: 6.1.0 - resolution: "postcss-minify-params@npm:6.1.0" - dependencies: - browserslist: "npm:^4.23.0" - cssnano-utils: "npm:^4.0.2" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/e5c38c3e5fb42e2ca165764f983716e57d854a63a477f7389ccc94cd2ab8123707006613bd7f29acc6eafd296fff513aa6d869c98ac52590f886d641cb21a59e - languageName: node - linkType: hard - -"postcss-minify-selectors@npm:^6.0.4": - version: 6.0.4 - resolution: "postcss-minify-selectors@npm:6.0.4" - dependencies: - postcss-selector-parser: "npm:^6.0.16" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/695ec2e1e3a7812b0cabe1105d0ed491760be3d8e9433914fb5af1fc30a84e6dc24089cd31b7e300de620b8e7adf806526c1acf8dd14077a7d1d2820c60a327c - languageName: node - linkType: hard - "postcss-modules-extract-imports@npm:^3.1.0": version: 3.1.0 resolution: "postcss-modules-extract-imports@npm:3.1.0" @@ -40829,191 +37857,13 @@ __metadata: languageName: node linkType: hard -"postcss-normalize-charset@npm:^6.0.2": - version: 6.0.2 - resolution: "postcss-normalize-charset@npm:6.0.2" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/af32a3b4cf94163d728b8aa935b2494c9f69fbc96a33b35f67ae15dbdef7fcc8732569df97cbaaf20ca6c0103c39adad0cfce2ba07ffed283796787f6c36f410 - languageName: node - linkType: hard - -"postcss-normalize-display-values@npm:^6.0.2": - version: 6.0.2 - resolution: "postcss-normalize-display-values@npm:6.0.2" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/782761850c7e697fdb6c3ff53076de716a71b60f9e835efb2f7ef238de347c88b5d55f0d43cf5c608e1ee58de65360e3d9fccd5f20774bba08ded7c87d8a5651 - languageName: node - linkType: hard - -"postcss-normalize-positions@npm:^6.0.2": - version: 6.0.2 - resolution: "postcss-normalize-positions@npm:6.0.2" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/9fdd42a47226bbda5f68774f3c4c3a90eb4fa708aef5a997c6a52fe6cac06585c9774038fe3bc1aa86a203c29223b8d8db6ebe7580c1aa293154f2b48db0b038 - languageName: node - linkType: hard - -"postcss-normalize-repeat-style@npm:^6.0.2": - version: 6.0.2 - resolution: "postcss-normalize-repeat-style@npm:6.0.2" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/9133ccbdf1286920c1cd0d01c1c5fa0bd3251b717f2f3e47d691dcc44978ac1dc419d20d9ae5428bd48ee542059e66b823ba699356f5968ccced5606c7c7ca34 - languageName: node - linkType: hard - -"postcss-normalize-string@npm:^6.0.2": - version: 6.0.2 - resolution: "postcss-normalize-string@npm:6.0.2" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/fecc2d52c4029b24fecf2ca2fb45df5dbdf9f35012194ad4ea80bc7be3252cdcb21a0976400902320595aa6178f2cc625cc804c6b6740aef6efa42105973a205 - languageName: node - linkType: hard - -"postcss-normalize-timing-functions@npm:^6.0.2": - version: 6.0.2 - resolution: "postcss-normalize-timing-functions@npm:6.0.2" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/a22af0b3374704e59ae70bbbcc66b7029137e284f04e30a2ad548818d1540d6c1ed748dd8f689b9b6df5c1064085a00ad07b6f7e25ffaad49d4e661b616cdeae - languageName: node - linkType: hard - -"postcss-normalize-unicode@npm:^6.1.0": - version: 6.1.0 - resolution: "postcss-normalize-unicode@npm:6.1.0" - dependencies: - browserslist: "npm:^4.23.0" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/ff5746670d94dd97b49a0955c3c71ff516fb4f54bbae257f877d179bacc44a62e50a0fd6e7ddf959f2ca35c335de4266b0c275d880bb57ad7827189339ab1582 - languageName: node - linkType: hard - -"postcss-normalize-url@npm:^6.0.2": - version: 6.0.2 - resolution: "postcss-normalize-url@npm:6.0.2" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/4718f1c0657788d2c560b340ee8e0a4eb3eb053eba6fbbf489e9a6e739b4c5f9ce1957f54bd03497c50a1f39962bf6ab9ff6ba4976b69dd160f6afd1670d69b7 - languageName: node - linkType: hard - -"postcss-normalize-whitespace@npm:^6.0.2": - version: 6.0.2 - resolution: "postcss-normalize-whitespace@npm:6.0.2" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/d5275a88e29a894aeb83a2a833e816d2456dbf3f39961628df596ce205dcc4895186a023812ff691945e0804241ccc53e520d16591b5812288474b474bbaf652 - languageName: node - linkType: hard - -"postcss-ordered-values@npm:^6.0.2": - version: 6.0.2 - resolution: "postcss-ordered-values@npm:6.0.2" - dependencies: - cssnano-utils: "npm:^4.0.2" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/aece23a289228aa804217a85f8da198d22b9123f02ca1310b81834af380d6fbe115e4300683599b4a2ab7f1c6a1dbd6789724c47c38e2b0a3774f2ea4b4f0963 - languageName: node - linkType: hard - -"postcss-reduce-idents@npm:^6.0.3": - version: 6.0.3 - resolution: "postcss-reduce-idents@npm:6.0.3" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/d9f9209e52ebb3d1d7feefc0be24fc74792e064e0fdec99554f050c6b882c61073d5d40986c545061b30e5ead881615e92c965dc765d8d83b2dec10d6a664e1f - languageName: node - linkType: hard - -"postcss-reduce-initial@npm:^6.1.0": - version: 6.1.0 - resolution: "postcss-reduce-initial@npm:6.1.0" - dependencies: - browserslist: "npm:^4.23.0" - caniuse-api: "npm:^3.0.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/a8f28cf51ce9a1b9423cce1a01c1d7cbee90125930ec36435a0073e73aef402d90affe2fd3600c964b679cf738869fda447b95a9acce74414e9d67d5c6ba8646 - languageName: node - linkType: hard - -"postcss-reduce-transforms@npm:^6.0.2": - version: 6.0.2 - resolution: "postcss-reduce-transforms@npm:6.0.2" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/755ef27b3d083f586ac831f0c611a66e76f504d27e2100dc7674f6b86afad597901b4520cb889fe58ca70e852aa7fd0c0acb69a63d39dfe6a95860b472394e7c - languageName: node - linkType: hard - -"postcss-selector-parser@npm:^6.0.11, postcss-selector-parser@npm:^6.0.16, postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4": - version: 6.1.1 - resolution: "postcss-selector-parser@npm:6.1.1" +"postcss-selector-parser@npm:^6.0.2, postcss-selector-parser@npm:^6.0.4": + version: 6.1.2 + resolution: "postcss-selector-parser@npm:6.1.2" dependencies: cssesc: "npm:^3.0.0" util-deprecate: "npm:^1.0.2" - checksum: 10c0/5608765e033fee35d448e1f607ffbaa750eb86901824a8bc4a911ea8bc137cb82f29239330787427c5d3695afd90d8721e190f211dbbf733e25033d8b3100763 - languageName: node - linkType: hard - -"postcss-sort-media-queries@npm:^5.2.0": - version: 5.2.0 - resolution: "postcss-sort-media-queries@npm:5.2.0" - dependencies: - sort-css-media-queries: "npm:2.2.0" - peerDependencies: - postcss: ^8.4.23 - checksum: 10c0/5e7f265a21999bdbf6592f7e15b3e889dd93bc9b15fe048958e8f85603ac276e69ef50305e8b41b10f4eea68917c9c25c7956fa9c3ba7f8577c1149416d35c4e - languageName: node - linkType: hard - -"postcss-svgo@npm:^6.0.3": - version: 6.0.3 - resolution: "postcss-svgo@npm:6.0.3" - dependencies: - postcss-value-parser: "npm:^4.2.0" - svgo: "npm:^3.2.0" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/994b15a88cbb411f32cfa98957faa5623c76f2d75fede51f5f47238f06b367ebe59c204fecbdaf21ccb9e727239a4b290087e04c502392658a0c881ddfbd61f2 - languageName: node - linkType: hard - -"postcss-unique-selectors@npm:^6.0.4": - version: 6.0.4 - resolution: "postcss-unique-selectors@npm:6.0.4" - dependencies: - postcss-selector-parser: "npm:^6.0.16" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/bfb99d8a7c675c93f2e65c9d9d563477bfd46fdce9e2727d42d57982b31ccbaaf944e8034bfbefe48b3119e77fba7eb1b181c19b91cb3a5448058fa66a7c9ae9 + checksum: 10c0/523196a6bd8cf660bdf537ad95abd79e546d54180f9afb165a4ab3e651ac705d0f8b8ce6b3164fb9e3279ce482c5f751a69eb2d3a1e8eb0fd5e82294fb3ef13e languageName: node linkType: hard @@ -41024,15 +37874,6 @@ __metadata: languageName: node linkType: hard -"postcss-zindex@npm:^6.0.2": - version: 6.0.2 - resolution: "postcss-zindex@npm:6.0.2" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/346291703e1f2dd954144d2bb251713dad6ae10e8aa05c3873dee2fc7a30d72da7866bec060abd932b9b839bc1495f73d813dde5312750a69d7ad33c435ce7ea - languageName: node - linkType: hard - "postcss@npm:8.4.31": version: 8.4.31 resolution: "postcss@npm:8.4.31" @@ -41044,7 +37885,18 @@ __metadata: languageName: node linkType: hard -"postcss@npm:^8.4.21, postcss@npm:^8.4.24, postcss@npm:^8.4.26, postcss@npm:^8.4.33, postcss@npm:^8.4.38, postcss@npm:^8.4.40": +"postcss@npm:^8.4.33": + version: 8.4.47 + resolution: "postcss@npm:8.4.47" + dependencies: + nanoid: "npm:^3.3.7" + picocolors: "npm:^1.1.0" + source-map-js: "npm:^1.2.1" + checksum: 10c0/929f68b5081b7202709456532cee2a145c1843d391508c5a09de2517e8c4791638f71dd63b1898dba6712f8839d7a6da046c72a5e44c162e908f5911f57b5f44 + languageName: node + linkType: hard + +"postcss@npm:^8.4.40": version: 8.4.41 resolution: "postcss@npm:8.4.41" dependencies: @@ -41246,16 +38098,6 @@ __metadata: languageName: node linkType: hard -"pretty-error@npm:^4.0.0": - version: 4.0.0 - resolution: "pretty-error@npm:4.0.0" - dependencies: - lodash: "npm:^4.17.20" - renderkid: "npm:^3.0.0" - checksum: 10c0/dc292c087e2857b2e7592784ab31e37a40f3fa918caa11eba51f9fb2853e1d4d6e820b219917e35f5721d833cfd20fdf4f26ae931a90fd1ad0cae2125c345138 - languageName: node - linkType: hard - "pretty-format@npm:^27.0.2": version: 27.5.1 resolution: "pretty-format@npm:27.5.1" @@ -41297,13 +38139,6 @@ __metadata: languageName: node linkType: hard -"pretty-time@npm:^1.1.0": - version: 1.1.0 - resolution: "pretty-time@npm:1.1.0" - checksum: 10c0/ba9d7af19cd43838fb2b147654990949575e400dc2cc24bf71ec4a6c4033a38ba8172b1014b597680c6d4d3c075e94648b2c13a7206c5f0c90b711c7388726f3 - languageName: node - linkType: hard - "pretty@npm:2.0.0": version: 2.0.0 resolution: "pretty@npm:2.0.0" @@ -41336,7 +38171,7 @@ __metadata: languageName: node linkType: hard -"prism-react-renderer@npm:^2.1.0, prism-react-renderer@npm:^2.3.0": +"prism-react-renderer@npm:^2.1.0": version: 2.3.1 resolution: "prism-react-renderer@npm:2.3.1" dependencies: @@ -41348,7 +38183,7 @@ __metadata: languageName: node linkType: hard -"prismjs@npm:^1.23.0, prismjs@npm:^1.29.0": +"prismjs@npm:^1.23.0": version: 1.29.0 resolution: "prismjs@npm:1.29.0" checksum: 10c0/d906c4c4d01b446db549b4f57f72d5d7e6ccaca04ecc670fb85cea4d4b1acc1283e945a9cbc3d81819084a699b382f970e02f9d1378e14af9808d366d9ed7ec6 @@ -41467,7 +38302,7 @@ __metadata: languageName: node linkType: hard -"prompts@npm:^2.0.1, prompts@npm:^2.4.0, prompts@npm:^2.4.1, prompts@npm:^2.4.2": +"prompts@npm:^2.0.1, prompts@npm:^2.4.0, prompts@npm:^2.4.1": version: 2.4.2 resolution: "prompts@npm:2.4.2" dependencies: @@ -41808,15 +38643,6 @@ __metadata: languageName: node linkType: hard -"pupa@npm:^3.1.0": - version: 3.1.0 - resolution: "pupa@npm:3.1.0" - dependencies: - escape-goat: "npm:^4.0.0" - checksum: 10c0/02afa6e4547a733484206aaa8f8eb3fbfb12d3dd17d7ca4fa1ea390a7da2cb8f381e38868bbf68009c4d372f8f6059f553171b6a712d8f2802c7cd43d513f06c - languageName: node - linkType: hard - "puppeteer-core@npm:^2.1.1": version: 2.1.1 resolution: "puppeteer-core@npm:2.1.1" @@ -41902,7 +38728,7 @@ __metadata: languageName: node linkType: hard -"querystring-es3@npm:^0.2.1, querystring-es3@npm:~0.2.0": +"querystring-es3@npm:~0.2.0": version: 0.2.1 resolution: "querystring-es3@npm:0.2.1" checksum: 10c0/476938c1adb45c141f024fccd2ffd919a3746e79ed444d00e670aad68532977b793889648980e7ca7ff5ffc7bfece623118d0fbadcaf217495eeb7059ae51580 @@ -41930,15 +38756,6 @@ __metadata: languageName: node linkType: hard -"queue@npm:6.0.2": - version: 6.0.2 - resolution: "queue@npm:6.0.2" - dependencies: - inherits: "npm:~2.0.3" - checksum: 10c0/cf987476cc72e7d3aaabe23ccefaab1cd757a2b5e0c8d80b67c9575a6b5e1198807ffd4f0948a3f118b149d1111d810ee773473530b77a5c606673cac2c9c996 - languageName: node - linkType: hard - "quick-lru@npm:^5.1.1": version: 5.1.1 resolution: "quick-lru@npm:5.1.1" @@ -41979,14 +38796,7 @@ __metadata: languageName: node linkType: hard -"range-parser@npm:1.2.0": - version: 1.2.0 - resolution: "range-parser@npm:1.2.0" - checksum: 10c0/c7aef4f6588eb974c475649c157f197d07437d8c6c8ff7e36280a141463fb5ab7a45918417334ebd7b665c6b8321cf31c763f7631dd5f5db9372249261b8b02a - languageName: node - linkType: hard - -"range-parser@npm:^1.2.1, range-parser@npm:~1.2.1": +"range-parser@npm:~1.2.1": version: 1.2.1 resolution: "range-parser@npm:1.2.1" checksum: 10c0/96c032ac2475c8027b7a4e9fe22dc0dfe0f6d90b85e496e0f016fbdb99d6d066de0112e680805075bd989905e2123b3b3d002765149294dce0c1f7f01fcc2ea0 @@ -42092,38 +38902,6 @@ __metadata: languageName: node linkType: hard -"react-dev-utils@npm:^12.0.1": - version: 12.0.1 - resolution: "react-dev-utils@npm:12.0.1" - dependencies: - "@babel/code-frame": "npm:^7.16.0" - address: "npm:^1.1.2" - browserslist: "npm:^4.18.1" - chalk: "npm:^4.1.2" - cross-spawn: "npm:^7.0.3" - detect-port-alt: "npm:^1.1.6" - escape-string-regexp: "npm:^4.0.0" - filesize: "npm:^8.0.6" - find-up: "npm:^5.0.0" - fork-ts-checker-webpack-plugin: "npm:^6.5.0" - global-modules: "npm:^2.0.0" - globby: "npm:^11.0.4" - gzip-size: "npm:^6.0.0" - immer: "npm:^9.0.7" - is-root: "npm:^2.1.0" - loader-utils: "npm:^3.2.0" - open: "npm:^8.4.0" - pkg-up: "npm:^3.1.0" - prompts: "npm:^2.4.2" - react-error-overlay: "npm:^6.0.11" - recursive-readdir: "npm:^2.2.2" - shell-quote: "npm:^1.7.3" - strip-ansi: "npm:^6.0.1" - text-table: "npm:^0.2.0" - checksum: 10c0/94bc4ee5014290ca47a025e53ab2205c5dc0299670724d46a0b1bacbdd48904827b5ae410842d0a3a92481509097ae032e4a9dc7ca70db437c726eaba6411e82 - languageName: node - linkType: hard - "react-devtools-inline@npm:4.4.0": version: 4.4.0 resolution: "react-devtools-inline@npm:4.4.0" @@ -42222,33 +39000,13 @@ __metadata: languageName: node linkType: hard -"react-error-overlay@npm:^6.0.11": - version: 6.0.11 - resolution: "react-error-overlay@npm:6.0.11" - checksum: 10c0/8fc93942976e0c704274aec87dbc8e21f62a2cc78d1c93f9bcfff9f7494b00c60f7a2f0bd48d832bcd3190627c0255a1df907373f61f820371373a65ec4b2d64 - languageName: node - linkType: hard - -"react-fast-compare@npm:^3.2.0, react-fast-compare@npm:^3.2.2": +"react-fast-compare@npm:^3.2.0": version: 3.2.2 resolution: "react-fast-compare@npm:3.2.2" checksum: 10c0/0bbd2f3eb41ab2ff7380daaa55105db698d965c396df73e6874831dbafec8c4b5b08ba36ff09df01526caa3c61595247e3269558c284e37646241cba2b90a367 languageName: node linkType: hard -"react-helmet-async@npm:*": - version: 2.0.5 - resolution: "react-helmet-async@npm:2.0.5" - dependencies: - invariant: "npm:^2.2.4" - react-fast-compare: "npm:^3.2.2" - shallowequal: "npm:^1.1.0" - peerDependencies: - react: ^16.6.0 || ^17.0.0 || ^18.0.0 - checksum: 10c0/f390ea8bf13c2681850e5f8eb5b73d8613f407c245a5fd23e9db9b2cc14a3700dd1ce992d3966632886d1d613083294c2aeee009193f49dfa7d145d9f13ea2b0 - languageName: node - linkType: hard - "react-helmet-async@npm:^1.3.0": version: 1.3.0 resolution: "react-helmet-async@npm:1.3.0" @@ -42364,45 +39122,6 @@ __metadata: languageName: node linkType: hard -"react-json-view-lite@npm:^1.2.0": - version: 1.4.0 - resolution: "react-json-view-lite@npm:1.4.0" - peerDependencies: - react: ^16.13.1 || ^17.0.0 || ^18.0.0 - checksum: 10c0/80dd21b14f9dcd93b2f473084aaa934594834a98ae2ed5725c98fae34486226d2eaa69a0bc4233f89b7bab4825e2d393efd6f7d39d59aa37a5bb44a61785f7e5 - languageName: node - linkType: hard - -"react-lifecycles-compat@npm:^3.0.4": - version: 3.0.4 - resolution: "react-lifecycles-compat@npm:3.0.4" - checksum: 10c0/1d0df3c85af79df720524780f00c064d53a9dd1899d785eddb7264b378026979acbddb58a4b7e06e7d0d12aa1494fd5754562ee55d32907b15601068dae82c27 - languageName: node - linkType: hard - -"react-loadable-ssr-addon-v5-slorber@npm:^1.0.1": - version: 1.0.1 - resolution: "react-loadable-ssr-addon-v5-slorber@npm:1.0.1" - dependencies: - "@babel/runtime": "npm:^7.10.3" - peerDependencies: - react-loadable: "*" - webpack: ">=4.41.1 || 5.x" - checksum: 10c0/7b0645f66adec56646f985ba8094c66a1c0a4627d96ad80eea32431d773ef1f79aa47d3247a8f21db3b064a0c6091653c5b5d3483b7046722eb64e55bffe635c - languageName: node - linkType: hard - -"react-loadable@npm:@docusaurus/react-loadable@6.0.0": - version: 6.0.0 - resolution: "@docusaurus/react-loadable@npm:6.0.0" - dependencies: - "@types/react": "npm:*" - peerDependencies: - react: "*" - checksum: 10c0/6b145d1a8d2e7342ceef58dd154aa990322f72a6cb98955ab8ce8e3f0dc7f0c5d00f9c2e4efa8d356c5effed72a130b5588857332b11faba0398f5429b484b04 - languageName: node - linkType: hard - "react-loading-skeleton@npm:^3.3.1": version: 3.4.0 resolution: "react-loading-skeleton@npm:3.4.0" @@ -42612,19 +39331,7 @@ __metadata: languageName: node linkType: hard -"react-router-config@npm:^5.1.1": - version: 5.1.1 - resolution: "react-router-config@npm:5.1.1" - dependencies: - "@babel/runtime": "npm:^7.1.2" - peerDependencies: - react: ">=15" - react-router: ">=5" - checksum: 10c0/1f8f4e55ca68b7b012293e663eb0ee4d670a3df929b78928f713ef98cd9d62c7f5c30a098d6668e64bbb11c7d6bb24e9e6b9c985a8b82465a1858dc7ba663f2b - languageName: node - linkType: hard - -"react-router-dom@npm:^5.2.0, react-router-dom@npm:^5.3.4": +"react-router-dom@npm:^5.2.0": version: 5.3.4 resolution: "react-router-dom@npm:5.3.4" dependencies: @@ -42666,7 +39373,7 @@ __metadata: languageName: node linkType: hard -"react-router@npm:5.3.4, react-router@npm:^5.3.4": +"react-router@npm:5.3.4": version: 5.3.4 resolution: "react-router@npm:5.3.4" dependencies: @@ -42907,7 +39614,7 @@ __metadata: languageName: node linkType: hard -"readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.1, readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:^2.0.6, readable-stream@npm:^2.2.2, readable-stream@npm:^2.3.5, readable-stream@npm:^2.3.8, readable-stream@npm:~2.3.6": +"readable-stream@npm:^2.0.0, readable-stream@npm:^2.0.2, readable-stream@npm:^2.0.5, readable-stream@npm:^2.0.6, readable-stream@npm:^2.2.2, readable-stream@npm:^2.3.5, readable-stream@npm:^2.3.8, readable-stream@npm:~2.3.6": version: 2.3.8 resolution: "readable-stream@npm:2.3.8" dependencies: @@ -42974,13 +39681,6 @@ __metadata: languageName: node linkType: hard -"reading-time@npm:^1.5.0": - version: 1.5.0 - resolution: "reading-time@npm:1.5.0" - checksum: 10c0/0f730852fd4fb99e5f78c5b0cf36ab8c3fa15db96f87d9563843f6fd07a47864273ade539ebb184b785b728cde81a70283aa2d9b80cba5ca03b81868be03cabc - languageName: node - linkType: hard - "readline-sync@npm:^1.4.9": version: 1.4.10 resolution: "readline-sync@npm:1.4.10" @@ -43026,15 +39726,6 @@ __metadata: languageName: node linkType: hard -"recursive-readdir@npm:^2.2.2": - version: 2.2.3 - resolution: "recursive-readdir@npm:2.2.3" - dependencies: - minimatch: "npm:^3.0.5" - checksum: 10c0/d0238f137b03af9cd645e1e0b40ae78b6cda13846e3ca57f626fcb58a66c79ae018a10e926b13b3a460f1285acc946a4e512ea8daa2e35df4b76a105709930d1 - languageName: node - linkType: hard - "redent@npm:^3.0.0": version: 3.0.0 resolution: "redent@npm:3.0.0" @@ -43241,15 +39932,6 @@ __metadata: languageName: node linkType: hard -"registry-auth-token@npm:^5.0.1": - version: 5.0.2 - resolution: "registry-auth-token@npm:5.0.2" - dependencies: - "@pnpm/npm-conf": "npm:^2.1.0" - checksum: 10c0/20fc2225681cc54ae7304b31ebad5a708063b1949593f02dfe5fb402bc1fc28890cecec6497ea396ba86d6cca8a8480715926dfef8cf1f2f11e6f6cc0a1b4bde - languageName: node - linkType: hard - "registry-url@npm:^5.0.0": version: 5.1.0 resolution: "registry-url@npm:5.1.0" @@ -43259,15 +39941,6 @@ __metadata: languageName: node linkType: hard -"registry-url@npm:^6.0.0": - version: 6.0.1 - resolution: "registry-url@npm:6.0.1" - dependencies: - rc: "npm:1.2.8" - checksum: 10c0/66e2221c8113fc35ee9d23fe58cb516fc8d556a189fb8d6f1011a02efccc846c4c9b5075b4027b99a5d5c9ad1345ac37f297bea3c0ca30d607ec8084bf561b90 - languageName: node - linkType: hard - "regjsgen@npm:^0.2.0": version: 0.2.0 resolution: "regjsgen@npm:0.2.0" @@ -43378,17 +40051,6 @@ __metadata: languageName: node linkType: hard -"rehype-raw@npm:^7.0.0": - version: 7.0.0 - resolution: "rehype-raw@npm:7.0.0" - dependencies: - "@types/hast": "npm:^3.0.0" - hast-util-raw: "npm:^9.0.0" - vfile: "npm:^6.0.0" - checksum: 10c0/1435b4b6640a5bc3abe3b2133885c4dbff5ef2190ef9cfe09d6a63f74dd7d7ffd0cede70603278560ccf1acbfb9da9faae4b68065a28bc5aa88ad18e40f32d52 - languageName: node - linkType: hard - "rehype-remark@npm:^9.1.2": version: 9.1.2 resolution: "rehype-remark@npm:9.1.2" @@ -43425,13 +40087,6 @@ __metadata: languageName: node linkType: hard -"relateurl@npm:^0.2.7": - version: 0.2.7 - resolution: "relateurl@npm:0.2.7" - checksum: 10c0/c248b4e3b32474f116a804b537fa6343d731b80056fb506dffd91e737eef4cac6be47a65aae39b522b0db9d0b1011d1a12e288d82a109ecd94a5299d82f6573a - languageName: node - linkType: hard - "relay-runtime@npm:12.0.0": version: 12.0.0 resolution: "relay-runtime@npm:12.0.0" @@ -43465,31 +40120,6 @@ __metadata: languageName: node linkType: hard -"remark-directive@npm:^3.0.0": - version: 3.0.0 - resolution: "remark-directive@npm:3.0.0" - dependencies: - "@types/mdast": "npm:^4.0.0" - mdast-util-directive: "npm:^3.0.0" - micromark-extension-directive: "npm:^3.0.0" - unified: "npm:^11.0.0" - checksum: 10c0/eeec4d70501c5bce55b2528fa0c8f1e2a5c713c9f72a7d4678dd3868c425620ec409a719bb2656663296bc476c63f5d7bcacd5a9059146bfc89d40e4ce13a7f6 - languageName: node - linkType: hard - -"remark-emoji@npm:^4.0.0": - version: 4.0.1 - resolution: "remark-emoji@npm:4.0.1" - dependencies: - "@types/mdast": "npm:^4.0.2" - emoticon: "npm:^4.0.1" - mdast-util-find-and-replace: "npm:^3.0.1" - node-emoji: "npm:^2.1.0" - unified: "npm:^11.0.4" - checksum: 10c0/27f88892215f3efe8f25c43f226a82d70144a1ae5906d36f6e09390b893b2d5524d5949bd8ca6a02be0e3cb5cba908b35c4221f4e07f34e93d13d6ff9347dbb8 - languageName: node - linkType: hard - "remark-external-links@npm:^8.0.0": version: 8.0.0 resolution: "remark-external-links@npm:8.0.0" @@ -43513,18 +40143,6 @@ __metadata: languageName: node linkType: hard -"remark-frontmatter@npm:^5.0.0": - version: 5.0.0 - resolution: "remark-frontmatter@npm:5.0.0" - dependencies: - "@types/mdast": "npm:^4.0.0" - mdast-util-frontmatter: "npm:^2.0.0" - micromark-extension-frontmatter: "npm:^2.0.0" - unified: "npm:^11.0.0" - checksum: 10c0/102325d5edbcf30eaf74de8a0a6e03096cc2370dfef19080fd2dd208f368fbb2323388751ac9931a1aa38a4f2828fa4bad6c52dc5249dcadcd34861693b52bf9 - languageName: node - linkType: hard - "remark-gfm@npm:^1.0.0": version: 1.0.0 resolution: "remark-gfm@npm:1.0.0" @@ -43547,20 +40165,6 @@ __metadata: languageName: node linkType: hard -"remark-gfm@npm:^4.0.0": - version: 4.0.0 - resolution: "remark-gfm@npm:4.0.0" - dependencies: - "@types/mdast": "npm:^4.0.0" - mdast-util-gfm: "npm:^3.0.0" - micromark-extension-gfm: "npm:^3.0.0" - remark-parse: "npm:^11.0.0" - remark-stringify: "npm:^11.0.0" - unified: "npm:^11.0.0" - checksum: 10c0/db0aa85ab718d475c2596e27c95be9255d3b0fc730a4eda9af076b919f7dd812f7be3ac020611a8dbe5253fd29671d7b12750b56e529fdc32dfebad6dbf77403 - languageName: node - linkType: hard - "remark-mdx@npm:^2.0.0": version: 2.3.0 resolution: "remark-mdx@npm:2.3.0" @@ -43571,16 +40175,6 @@ __metadata: languageName: node linkType: hard -"remark-mdx@npm:^3.0.0": - version: 3.0.1 - resolution: "remark-mdx@npm:3.0.1" - dependencies: - mdast-util-mdx: "npm:^3.0.0" - micromark-extension-mdxjs: "npm:^3.0.0" - checksum: 10c0/9e16cd5ff3b30620bd25351a2dd1701627fa5555785b35ee5fe07bd1e6793a9c825cc1f6af9e54a44351f74879f8b5ea2bce8e5a21379aeab58935e76a4d69ce - languageName: node - linkType: hard - "remark-parse@npm:^10.0.0, remark-parse@npm:^10.0.1": version: 10.0.2 resolution: "remark-parse@npm:10.0.2" @@ -43592,18 +40186,6 @@ __metadata: languageName: node linkType: hard -"remark-parse@npm:^11.0.0": - version: 11.0.0 - resolution: "remark-parse@npm:11.0.0" - dependencies: - "@types/mdast": "npm:^4.0.0" - mdast-util-from-markdown: "npm:^2.0.0" - micromark-util-types: "npm:^2.0.0" - unified: "npm:^11.0.0" - checksum: 10c0/6eed15ddb8680eca93e04fcb2d1b8db65a743dcc0023f5007265dda558b09db595a087f622062ccad2630953cd5cddc1055ce491d25a81f3317c858348a8dd38 - languageName: node - linkType: hard - "remark-parse@npm:^9.0.0": version: 9.0.0 resolution: "remark-parse@npm:9.0.0" @@ -43625,19 +40207,6 @@ __metadata: languageName: node linkType: hard -"remark-rehype@npm:^11.0.0": - version: 11.1.0 - resolution: "remark-rehype@npm:11.1.0" - dependencies: - "@types/hast": "npm:^3.0.0" - "@types/mdast": "npm:^4.0.0" - mdast-util-to-hast: "npm:^13.0.0" - unified: "npm:^11.0.0" - vfile: "npm:^6.0.0" - checksum: 10c0/7a9534847ea70e78cf09227a4302af7e491f625fd092351a1b1ee27a2de0a369ac4acf069682e8a8ec0a55847b3e83f0be76b2028aa90e98e69e21420b9794c3 - languageName: node - linkType: hard - "remark-slug@npm:^6.0.0": version: 6.1.0 resolution: "remark-slug@npm:6.1.0" @@ -43660,17 +40229,6 @@ __metadata: languageName: node linkType: hard -"remark-stringify@npm:^11.0.0": - version: 11.0.0 - resolution: "remark-stringify@npm:11.0.0" - dependencies: - "@types/mdast": "npm:^4.0.0" - mdast-util-to-markdown: "npm:^2.0.0" - unified: "npm:^11.0.0" - checksum: 10c0/0cdb37ce1217578f6f847c7ec9f50cbab35df5b9e3903d543e74b405404e67c07defcb23cd260a567b41b769400f6de03c2c3d9cd6ae7a6707d5c8d89ead489f - languageName: node - linkType: hard - "remark-stringify@npm:^9.0.1": version: 9.0.1 resolution: "remark-stringify@npm:9.0.1" @@ -43708,19 +40266,6 @@ __metadata: languageName: node linkType: hard -"renderkid@npm:^3.0.0": - version: 3.0.0 - resolution: "renderkid@npm:3.0.0" - dependencies: - css-select: "npm:^4.1.3" - dom-converter: "npm:^0.2.0" - htmlparser2: "npm:^6.1.0" - lodash: "npm:^4.17.21" - strip-ansi: "npm:^6.0.1" - checksum: 10c0/24a9fae4cc50e731d059742d1b3eec163dc9e3872b12010d120c3fcbd622765d9cda41f79a1bbb4bf63c1d3442f18a08f6e1642cb5d7ebf092a0ce3f7a3bd143 - languageName: node - linkType: hard - "repeat-string@npm:^1.0.0, repeat-string@npm:^1.6.1": version: 1.6.1 resolution: "repeat-string@npm:1.6.1" @@ -43797,13 +40342,6 @@ __metadata: languageName: node linkType: hard -"require-like@npm:>= 0.1.1": - version: 0.1.2 - resolution: "require-like@npm:0.1.2" - checksum: 10c0/9035ff6c4000a56ede6fc51dd5c56541fafa5a7dddc9b1c3a5f9148d95ee21c603c9bf5c6e37b19fc7de13d9294260842d8590b2ffd6c7c773e78603d1af8050 - languageName: node - linkType: hard - "require-main-filename@npm:^2.0.0": version: 2.0.0 resolution: "require-main-filename@npm:2.0.0" @@ -43832,7 +40370,7 @@ __metadata: languageName: node linkType: hard -"resolve-alpn@npm:^1.0.0, resolve-alpn@npm:^1.2.0": +"resolve-alpn@npm:^1.0.0": version: 1.2.1 resolution: "resolve-alpn@npm:1.2.1" checksum: 10c0/b70b29c1843bc39781ef946c8cd4482e6d425976599c0f9c138cec8209e4e0736161bf39319b01676a847000085dfdaf63583c6fb4427bf751a10635bd2aa0c4 @@ -43997,15 +40535,6 @@ __metadata: languageName: node linkType: hard -"responselike@npm:^3.0.0": - version: 3.0.0 - resolution: "responselike@npm:3.0.0" - dependencies: - lowercase-keys: "npm:^3.0.0" - checksum: 10c0/8af27153f7e47aa2c07a5f2d538cb1e5872995f0e9ff77def858ecce5c3fe677d42b824a62cde502e56d275ab832b0a8bd350d5cd6b467ac0425214ac12ae658 - languageName: node - linkType: hard - "restore-cursor@npm:^3.1.0": version: 3.1.0 resolution: "restore-cursor@npm:3.1.0" @@ -44109,6 +40638,13 @@ __metadata: languageName: node linkType: hard +"robust-predicates@npm:^3.0.2": + version: 3.0.2 + resolution: "robust-predicates@npm:3.0.2" + checksum: 10c0/4ecd53649f1c2d49529c85518f2fa69ffb2f7a4453f7fd19c042421c7b4d76c3efb48bc1c740c8f7049346d7cb58cf08ee0c9adaae595cc23564d360adb1fde4 + languageName: node + linkType: hard + "rollup-plugin-inject@npm:^3.0.0": version: 3.0.2 resolution: "rollup-plugin-inject@npm:3.0.2" @@ -44252,27 +40788,6 @@ __metadata: languageName: node linkType: hard -"rtl-detect@npm:^1.0.4": - version: 1.1.2 - resolution: "rtl-detect@npm:1.1.2" - checksum: 10c0/1b92888aafca1593314f837e83fdf02eb208faae3e713ab87c176804728efd3b1980d53b64f65f1fa593348087e852c5cd729b7b9372950f6e9b7be489afc0ca - languageName: node - linkType: hard - -"rtlcss@npm:^4.1.0": - version: 4.2.0 - resolution: "rtlcss@npm:4.2.0" - dependencies: - escalade: "npm:^3.1.1" - picocolors: "npm:^1.0.0" - postcss: "npm:^8.4.21" - strip-json-comments: "npm:^3.1.1" - bin: - rtlcss: bin/rtlcss.js - checksum: 10c0/8d1512c36f426bc4f133bc14ab06f11f3f7880a88491ddab81733551465f72adace688653f13fbb6d343961c08503ede5b204bf224e8adf8941a045d5756f537 - languageName: node - linkType: hard - "run-async@npm:^2.0.0, run-async@npm:^2.4.0": version: 2.4.1 resolution: "run-async@npm:2.4.1" @@ -44333,7 +40848,7 @@ __metadata: languageName: node linkType: hard -"safe-buffer@npm:5.2.1, safe-buffer@npm:>=5.1.0, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": +"safe-buffer@npm:5.2.1, safe-buffer@npm:^5.0.1, safe-buffer@npm:^5.1.0, safe-buffer@npm:^5.1.1, safe-buffer@npm:^5.1.2, safe-buffer@npm:^5.2.0, safe-buffer@npm:^5.2.1, safe-buffer@npm:~5.2.0": version: 5.2.1 resolution: "safe-buffer@npm:5.2.1" checksum: 10c0/6501914237c0a86e9675d4e51d89ca3c21ffd6a31642efeba25ad65720bce6921c9e7e974e5be91a786b25aa058b5303285d3c15dbabf983a919f5f630d349f3 @@ -44365,13 +40880,6 @@ __metadata: languageName: node linkType: hard -"sax@npm:^1.2.4": - 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" @@ -44390,17 +40898,6 @@ __metadata: languageName: node linkType: hard -"schema-utils@npm:2.7.0": - version: 2.7.0 - resolution: "schema-utils@npm:2.7.0" - dependencies: - "@types/json-schema": "npm:^7.0.4" - ajv: "npm:^6.12.2" - ajv-keywords: "npm:^3.4.1" - checksum: 10c0/723c3c856a0313a89aa81c5fb2c93d4b11225f5cdd442665fddd55d3c285ae72e079f5286a3a9a1a973affe888f6c33554a2cf47b79b24cd8de2f1f756a6fb1b - languageName: node - linkType: hard - "schema-utils@npm:^2.7.0": version: 2.7.1 resolution: "schema-utils@npm:2.7.1" @@ -44423,18 +40920,6 @@ __metadata: languageName: node linkType: hard -"schema-utils@npm:^4.0.0, schema-utils@npm:^4.0.1": - version: 4.2.0 - resolution: "schema-utils@npm:4.2.0" - dependencies: - "@types/json-schema": "npm:^7.0.9" - ajv: "npm:^8.9.0" - ajv-formats: "npm:^2.1.1" - ajv-keywords: "npm:^5.1.0" - checksum: 10c0/8dab7e7800316387fd8569870b4b668cfcecf95ac551e369ea799bbcbfb63fb0365366d4b59f64822c9f7904d8c5afcfaf5a6124a4b08783e558cd25f299a6b4 - languageName: node - linkType: hard - "scoped-regex@npm:^2.0.0": version: 2.1.0 resolution: "scoped-regex@npm:2.1.0" @@ -44489,23 +40974,6 @@ __metadata: languageName: node linkType: hard -"select-hose@npm:^2.0.0": - version: 2.0.0 - resolution: "select-hose@npm:2.0.0" - checksum: 10c0/01cc52edd29feddaf379efb4328aededa633f0ac43c64b11a8abd075ff34f05b0d280882c4fbcbdf1a0658202c9cd2ea8d5985174dcf9a2dac7e3a4996fa9b67 - languageName: node - linkType: hard - -"selfsigned@npm:^2.1.1": - version: 2.4.1 - resolution: "selfsigned@npm:2.4.1" - dependencies: - "@types/node-forge": "npm:^1.3.0" - node-forge: "npm:^1" - checksum: 10c0/521829ec36ea042f7e9963bf1da2ed040a815cf774422544b112ec53b7edc0bc50a0f8cc2ae7aa6cc19afa967c641fd96a15de0fc650c68651e41277d2e1df09 - languageName: node - linkType: hard - "semver-diff@npm:^3.1.1": version: 3.1.1 resolution: "semver-diff@npm:3.1.1" @@ -44515,15 +40983,6 @@ __metadata: languageName: node linkType: hard -"semver-diff@npm:^4.0.0": - version: 4.0.0 - resolution: "semver-diff@npm:4.0.0" - dependencies: - semver: "npm:^7.3.5" - checksum: 10c0/3ed1bb22f39b4b6e98785bb066e821eabb9445d3b23e092866c50e7df8b9bd3eda617b242f81db4159586e0e39b0deb908dd160a24f783bd6f52095b22cd68ea - languageName: node - linkType: hard - "semver-regex@npm:^4.0.5": version: 4.0.5 resolution: "semver-regex@npm:4.0.5" @@ -44630,7 +41089,7 @@ __metadata: languageName: node linkType: hard -"serialize-javascript@npm:^6.0.0, serialize-javascript@npm:^6.0.1": +"serialize-javascript@npm:^6.0.1": version: 6.0.2 resolution: "serialize-javascript@npm:6.0.2" dependencies: @@ -44639,37 +41098,6 @@ __metadata: languageName: node linkType: hard -"serve-handler@npm:^6.1.5": - version: 6.1.5 - resolution: "serve-handler@npm:6.1.5" - dependencies: - bytes: "npm:3.0.0" - content-disposition: "npm:0.5.2" - fast-url-parser: "npm:1.1.3" - mime-types: "npm:2.1.18" - minimatch: "npm:3.1.2" - path-is-inside: "npm:1.0.2" - path-to-regexp: "npm:2.2.1" - range-parser: "npm:1.2.0" - checksum: 10c0/6fd393ae37a0305107e634ca545322b00605322189fe70d8f1a4a90a101c4e354768c610efe5a7ef1af3820cec5c33d97467c88151f35a3cb41d8ff2075ef802 - languageName: node - linkType: hard - -"serve-index@npm:^1.9.1": - version: 1.9.1 - resolution: "serve-index@npm:1.9.1" - dependencies: - accepts: "npm:~1.3.4" - batch: "npm:0.6.1" - debug: "npm:2.6.9" - escape-html: "npm:~1.0.3" - http-errors: "npm:~1.6.2" - mime-types: "npm:~2.1.17" - parseurl: "npm:~1.3.2" - checksum: 10c0/a666471a24196f74371edf2c3c7bcdd82adbac52f600804508754b5296c3567588bf694258b19e0cb23a567acfa20d9721bfdaed3286007b81f9741ada8a3a9c - languageName: node - linkType: hard - "serve-static@npm:1.15.0": version: 1.15.0 resolution: "serve-static@npm:1.15.0" @@ -44739,20 +41167,13 @@ __metadata: languageName: node linkType: hard -"setimmediate@npm:^1.0.4, setimmediate@npm:^1.0.5": +"setimmediate@npm:^1.0.5": version: 1.0.5 resolution: "setimmediate@npm:1.0.5" checksum: 10c0/5bae81bfdbfbd0ce992893286d49c9693c82b1bcc00dcaaf3a09c8f428fdeacf4190c013598b81875dfac2b08a572422db7df779a99332d0fce186d15a3e4d49 languageName: node linkType: hard -"setprototypeof@npm:1.1.0": - version: 1.1.0 - resolution: "setprototypeof@npm:1.1.0" - checksum: 10c0/a77b20876689c6a89c3b42f0c3596a9cae02f90fc902570cbd97198e9e8240382086c9303ad043e88cee10f61eae19f1004e51d885395a1e9bf49f9ebed12872 - languageName: node - linkType: hard - "setprototypeof@npm:1.2.0": version: 1.2.0 resolution: "setprototypeof@npm:1.2.0" @@ -44962,7 +41383,7 @@ __metadata: languageName: node linkType: hard -"sirv@npm:^2.0.3, sirv@npm:^2.0.4": +"sirv@npm:^2.0.4": version: 2.0.4 resolution: "sirv@npm:2.0.4" dependencies: @@ -44980,29 +41401,6 @@ __metadata: languageName: node linkType: hard -"sitemap@npm:^7.1.1": - version: 7.1.2 - resolution: "sitemap@npm:7.1.2" - dependencies: - "@types/node": "npm:^17.0.5" - "@types/sax": "npm:^1.2.1" - arg: "npm:^5.0.0" - sax: "npm:^1.2.4" - bin: - sitemap: dist/cli.js - checksum: 10c0/01dd1268c0d4b89f8ef082bcb9ef18d0182d00d1622e9c54743474918169491e5360538f9a01a769262e0fe23d6e3822a90680eff0f076cf87b68d459014a34c - languageName: node - linkType: hard - -"skin-tone@npm:^2.0.0": - version: 2.0.0 - resolution: "skin-tone@npm:2.0.0" - dependencies: - unicode-emoji-modifier-base: "npm:^1.0.0" - checksum: 10c0/82d4c2527864f9cbd6cb7f3c4abb31e2224752234d5013b881d3e34e9ab543545b05206df5a17d14b515459fcb265ce409f9cfe443903176b0360cd20e4e4ba5 - languageName: node - linkType: hard - "slash@npm:3.0.0, slash@npm:^3.0.0": version: 3.0.0 resolution: "slash@npm:3.0.0" @@ -45024,13 +41422,6 @@ __metadata: languageName: node linkType: hard -"slash@npm:^4.0.0": - version: 4.0.0 - resolution: "slash@npm:4.0.0" - checksum: 10c0/b522ca75d80d107fd30d29df0549a7b2537c83c4c4ecd12cd7d4ea6c8aaca2ab17ada002e7a1d78a9d736a0261509f26ea5b489082ee443a3a810586ef8eff18 - languageName: node - linkType: hard - "slash@npm:^5.0.0": version: 5.1.0 resolution: "slash@npm:5.1.0" @@ -45077,17 +41468,6 @@ __metadata: languageName: node linkType: hard -"sockjs@npm:^0.3.24": - version: 0.3.24 - resolution: "sockjs@npm:0.3.24" - dependencies: - faye-websocket: "npm:^0.11.3" - uuid: "npm:^8.3.2" - websocket-driver: "npm:^0.7.4" - checksum: 10c0/aa102c7d921bf430215754511c81ea7248f2dcdf268fbdb18e4d8183493a86b8793b164c636c52f474a886f747447c962741df2373888823271efdb9d2594f33 - languageName: node - linkType: hard - "socks-proxy-agent@npm:^6.0.0": version: 6.2.1 resolution: "socks-proxy-agent@npm:6.2.1" @@ -45131,13 +41511,6 @@ __metadata: languageName: node linkType: hard -"sort-css-media-queries@npm:2.2.0": - version: 2.2.0 - resolution: "sort-css-media-queries@npm:2.2.0" - checksum: 10c0/7478308c7ca93409f959ab993d41de2f0515ed5f51b671908ecb777aae0d63be97b454d59d80e14ee4874884618a2e825d4ae7ccb225779276904dd175f4e766 - languageName: node - linkType: hard - "sort-keys-length@npm:^1.0.0": version: 1.0.1 resolution: "sort-keys-length@npm:1.0.1" @@ -45172,6 +41545,13 @@ __metadata: languageName: node linkType: hard +"source-map-js@npm:^1.2.1": + version: 1.2.1 + resolution: "source-map-js@npm:1.2.1" + checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf + languageName: node + linkType: hard + "source-map-support@npm:0.5.13": version: 0.5.13 resolution: "source-map-support@npm:0.5.13" @@ -45241,7 +41621,7 @@ __metadata: languageName: node linkType: hard -"source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.0, source-map@npm:~0.6.1": +"source-map@npm:^0.6.0, source-map@npm:^0.6.1, source-map@npm:~0.6.1": version: 0.6.1 resolution: "source-map@npm:0.6.1" checksum: 10c0/ab55398007c5e5532957cb0beee2368529618ac0ab372d789806f5718123cc4367d57de3904b4e6a4170eb5a0b0f41373066d02ca0735a0c4d75c7d328d3e011 @@ -45336,33 +41716,6 @@ __metadata: languageName: node linkType: hard -"spdy-transport@npm:^3.0.0": - version: 3.0.0 - resolution: "spdy-transport@npm:3.0.0" - dependencies: - debug: "npm:^4.1.0" - detect-node: "npm:^2.0.4" - hpack.js: "npm:^2.1.6" - obuf: "npm:^1.1.2" - readable-stream: "npm:^3.0.6" - wbuf: "npm:^1.7.3" - checksum: 10c0/eaf7440fa90724fffc813c386d4a8a7427d967d6e46d7c51d8f8a533d1a6911b9823ea9218703debbae755337e85f110185d7a00ae22ec5c847077b908ce71bb - languageName: node - linkType: hard - -"spdy@npm:^4.0.2": - version: 4.0.2 - resolution: "spdy@npm:4.0.2" - dependencies: - debug: "npm:^4.1.0" - handle-thing: "npm:^2.0.0" - http-deceiver: "npm:^1.2.7" - select-hose: "npm:^2.0.0" - spdy-transport: "npm:^3.0.0" - checksum: 10c0/983509c0be9d06fd00bb9dff713c5b5d35d3ffd720db869acdd5ad7aa6fc0e02c2318b58f75328957d8ff772acdf1f7d19382b6047df342044ff3e2d6805ccdf - languageName: node - linkType: hard - "split-on-first@npm:^1.0.0": version: 1.1.0 resolution: "split-on-first@npm:1.1.0" @@ -45400,13 +41753,6 @@ __metadata: languageName: node linkType: hard -"srcset@npm:^4.0.0": - version: 4.0.0 - resolution: "srcset@npm:4.0.0" - checksum: 10c0/0685c3bd2423b33831734fb71560cd8784f024895e70ee2ac2c392e30047c27ffd9481e001950fb0503f4906bc3fe963145935604edad77944d09c9800990660 - languageName: node - linkType: hard - "sshpk@npm:^1.7.0": version: 1.18.0 resolution: "sshpk@npm:1.18.0" @@ -45541,14 +41887,14 @@ __metadata: languageName: node linkType: hard -"statuses@npm:>= 1.4.0 < 2, statuses@npm:>= 1.5.0 < 2": +"statuses@npm:>= 1.5.0 < 2": version: 1.5.0 resolution: "statuses@npm:1.5.0" checksum: 10c0/e433900956357b3efd79b1c547da4d291799ac836960c016d10a98f6a810b1b5c0dcc13b5a7aa609a58239b5190e1ea176ad9221c2157d2fd1c747393e6b2940 languageName: node linkType: hard -"std-env@npm:^3.0.1, std-env@npm:^3.5.0": +"std-env@npm:^3.5.0": version: 3.7.0 resolution: "std-env@npm:3.7.0" checksum: 10c0/60edf2d130a4feb7002974af3d5a5f3343558d1ccf8d9b9934d225c638606884db4a20d2fe6440a09605bca282af6b042ae8070a10490c0800d69e82e478f41e @@ -45674,7 +42020,7 @@ __metadata: languageName: node linkType: hard -"stream-http@npm:^3.0.0, stream-http@npm:^3.2.0": +"stream-http@npm:^3.0.0": version: 3.2.0 resolution: "stream-http@npm:3.2.0" dependencies: @@ -45931,7 +42277,7 @@ __metadata: languageName: node linkType: hard -"stringify-object@npm:3.3.0, stringify-object@npm:^3.3.0": +"stringify-object@npm:3.3.0": version: 3.3.0 resolution: "stringify-object@npm:3.3.0" dependencies: @@ -46153,7 +42499,7 @@ __metadata: languageName: node linkType: hard -"style-to-object@npm:^0.4.0, style-to-object@npm:^0.4.1": +"style-to-object@npm:^0.4.1": version: 0.4.4 resolution: "style-to-object@npm:0.4.4" dependencies: @@ -46162,15 +42508,6 @@ __metadata: languageName: node linkType: hard -"style-to-object@npm:^1.0.0": - version: 1.0.6 - resolution: "style-to-object@npm:1.0.6" - dependencies: - inline-style-parser: "npm:0.2.3" - checksum: 10c0/be5e8e3f0e35c0338de4112b9d861db576a52ebbd97f2501f1fb2c900d05c8fc42c5114407fa3a7f8b39301146cd8ca03a661bf52212394125a9629d5b771aba - languageName: node - linkType: hard - "style-value-types@npm:5.0.0": version: 5.0.0 resolution: "style-value-types@npm:5.0.0" @@ -46197,18 +42534,6 @@ __metadata: languageName: node linkType: hard -"stylehacks@npm:^6.1.1": - version: 6.1.1 - resolution: "stylehacks@npm:6.1.1" - dependencies: - browserslist: "npm:^4.23.0" - postcss-selector-parser: "npm:^6.0.16" - peerDependencies: - postcss: ^8.4.31 - checksum: 10c0/2dd2bccfd8311ff71492e63a7b8b86c3d7b1fff55d4ba5a2357aff97743e633d351cdc2f5ae3c0057637d00dab4ef5fc5b218a1b370e4585a41df22b5a5128be - languageName: node - linkType: hard - "stylis@npm:4.2.0": version: 4.2.0 resolution: "stylis@npm:4.2.0" @@ -46370,7 +42695,7 @@ __metadata: languageName: node linkType: hard -"svgo@npm:^3.0.2, svgo@npm:^3.2.0": +"svgo@npm:^3.0.2": version: 3.3.2 resolution: "svgo@npm:3.3.2" dependencies: @@ -46450,14 +42775,7 @@ __metadata: languageName: node linkType: hard -"tapable@npm:^1.0.0": - version: 1.1.3 - resolution: "tapable@npm:1.1.3" - checksum: 10c0/c9f0265e55e45821ec672b9b9ee8a35d95bf3ea6b352199f8606a2799018e89cfe4433c554d424b31fc67c4be26b05d4f36dc3c607def416fdb2514cd63dba50 - languageName: node - linkType: hard - -"tapable@npm:^2.0.0, tapable@npm:^2.1.1, tapable@npm:^2.2.0, tapable@npm:^2.2.1": +"tapable@npm:^2.1.1, tapable@npm:^2.2.0, tapable@npm:^2.2.1": version: 2.2.1 resolution: "tapable@npm:2.2.1" checksum: 10c0/bc40e6efe1e554d075469cedaba69a30eeb373552aaf41caeaaa45bf56ffacc2674261b106245bd566b35d8f3329b52d838e851ee0a852120acae26e622925c9 @@ -46569,7 +42887,7 @@ __metadata: languageName: node linkType: hard -"terser-webpack-plugin@npm:^5.3.10, terser-webpack-plugin@npm:^5.3.7, terser-webpack-plugin@npm:^5.3.9": +"terser-webpack-plugin@npm:^5.3.7": version: 5.3.10 resolution: "terser-webpack-plugin@npm:5.3.10" dependencies: @@ -46591,7 +42909,7 @@ __metadata: languageName: node linkType: hard -"terser@npm:^5.10.0, terser@npm:^5.15.1, terser@npm:^5.26.0": +"terser@npm:^5.26.0": version: 5.31.5 resolution: "terser@npm:5.31.5" dependencies: @@ -46690,13 +43008,6 @@ __metadata: languageName: node linkType: hard -"thunky@npm:^1.0.2": - version: 1.1.0 - resolution: "thunky@npm:1.1.0" - checksum: 10c0/369764f39de1ce1de2ba2fa922db4a3f92e9c7f33bcc9a713241bc1f4a5238b484c17e0d36d1d533c625efb00e9e82c3e45f80b47586945557b45abb890156d2 - languageName: node - linkType: hard - "timers-browserify@npm:^1.0.1": version: 1.4.2 resolution: "timers-browserify@npm:1.4.2" @@ -46706,15 +43017,6 @@ __metadata: languageName: node linkType: hard -"timers-browserify@npm:^2.0.12": - version: 2.0.12 - resolution: "timers-browserify@npm:2.0.12" - dependencies: - setimmediate: "npm:^1.0.4" - checksum: 10c0/98e84db1a685bc8827c117a8bc62aac811ad56a995d07938fc7ed8cdc5bf3777bfe2d4e5da868847194e771aac3749a20f6cdd22091300fe889a76fe214a4641 - languageName: node - linkType: hard - "timers-ext@npm:^0.1.7": version: 0.1.8 resolution: "timers-ext@npm:0.1.8" @@ -47297,7 +43599,7 @@ __metadata: languageName: node linkType: hard -"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.5.2, tslib@npm:^2.6.0, tslib@npm:^2.6.1, tslib@npm:^2.6.2, tslib@npm:^2.6.3, tslib@npm:~2.6.0": +"tslib@npm:^2.0.0, tslib@npm:^2.0.1, tslib@npm:^2.0.3, tslib@npm:^2.1.0, tslib@npm:^2.2.0, tslib@npm:^2.3.0, tslib@npm:^2.3.1, tslib@npm:^2.4.0, tslib@npm:^2.5.0, tslib@npm:^2.5.2, tslib@npm:^2.6.1, tslib@npm:^2.6.2, tslib@npm:^2.6.3, tslib@npm:~2.6.0": version: 2.6.3 resolution: "tslib@npm:2.6.3" checksum: 10c0/2598aef53d9dbe711af75522464b2104724d6467b26a60f2bdac8297d2b5f1f6b86a71f61717384aa8fd897240467aaa7bcc36a0700a0faf751293d1331db39a @@ -47393,7 +43695,7 @@ __metadata: languageName: node linkType: hard -"tty-browserify@npm:0.0.1, tty-browserify@npm:^0.0.1": +"tty-browserify@npm:0.0.1": version: 0.0.1 resolution: "tty-browserify@npm:0.0.1" checksum: 10c0/5e34883388eb5f556234dae75b08e069b9e62de12bd6d87687f7817f5569430a6dfef550b51dbc961715ae0cd0eb5a059e6e3fc34dc127ea164aa0f9b5bb033d @@ -47458,6 +43760,9 @@ __metadata: version: 0.0.0-use.local resolution: "twenty-front@workspace:packages/twenty-front" dependencies: + "@nivo/calendar": "npm:^0.87.0" + "@nivo/core": "npm:^0.87.0" + "@nivo/line": "npm:^0.87.0" "@xyflow/react": "npm:^12.0.4" transliteration: "npm:^2.3.5" languageName: unknown @@ -47536,6 +43841,8 @@ __metadata: version: 0.0.0-use.local resolution: "twenty-website@workspace:packages/twenty-website" dependencies: + "@docsearch/react": "npm:^3.6.2" + gray-matter: "npm:^4.0.3" next-runtime-env: "npm:^3.2.2" postgres: "npm:^3.4.3" languageName: unknown @@ -47570,10 +43877,6 @@ __metadata: "@codesandbox/sandpack-react": "npm:^2.13.5" "@crxjs/vite-plugin": "npm:^1.0.14" "@dagrejs/dagre": "npm:^1.1.2" - "@docusaurus/core": "npm:^3.1.0" - "@docusaurus/module-type-aliases": "npm:^3.1.0" - "@docusaurus/preset-classic": "npm:^3.1.0" - "@docusaurus/tsconfig": "npm:3.1.0" "@emotion/react": "npm:^11.11.1" "@emotion/styled": "npm:^11.11.0" "@envelop/on-resolve": "npm:^4.1.0" @@ -47608,8 +43911,6 @@ __metadata: "@nestjs/testing": "npm:^9.0.0" "@nestjs/typeorm": "npm:^10.0.0" "@next/eslint-plugin-next": "npm:^14.1.4" - "@nivo/calendar": "npm:^0.84.0" - "@nivo/core": "npm:^0.84.0" "@nx/eslint": "npm:18.3.3" "@nx/eslint-plugin": "npm:18.3.3" "@nx/jest": "npm:18.3.3" @@ -47693,6 +43994,7 @@ __metadata: "@types/passport-google-oauth20": "npm:^2.0.11" "@types/passport-jwt": "npm:^3.0.8" "@types/passport-microsoft": "npm:^1.0.3" + "@types/pluralize": "npm:^0.0.33" "@types/react": "npm:^18.2.39" "@types/react-datepicker": "npm:^6.2.0" "@types/react-dom": "npm:^18.2.15" @@ -47724,6 +44026,7 @@ __metadata: concurrently: "npm:^8.2.2" cross-env: "npm:^7.0.3" cross-var: "npm:^1.1.0" + css-loader: "npm:^7.1.2" danger: "npm:^11.3.0" danger-plugin-todos: "npm:^1.3.1" dataloader: "npm:^2.2.2" @@ -47731,7 +44034,6 @@ __metadata: date-fns-tz: "npm:^2.0.0" debounce: "npm:^2.0.0" deep-equal: "npm:^2.2.2" - docusaurus-node-polyfills: "npm:^1.0.0" dompurify: "npm:^3.0.11" dotenv-cli: "npm:^7.2.1" drizzle-kit: "npm:^0.20.14" @@ -48298,13 +44600,6 @@ __metadata: languageName: node linkType: hard -"unicode-emoji-modifier-base@npm:^1.0.0": - version: 1.0.0 - resolution: "unicode-emoji-modifier-base@npm:1.0.0" - checksum: 10c0/b37623fcf0162186debd20f116483e035a2d5b905b932a2c472459d9143d446ebcbefb2a494e2fe4fa7434355396e2a95ec3fc1f0c29a3bc8f2c827220e79c66 - languageName: node - linkType: hard - "unicode-match-property-ecmascript@npm:^2.0.0": version: 2.0.0 resolution: "unicode-match-property-ecmascript@npm:2.0.0" @@ -48344,21 +44639,6 @@ __metadata: languageName: node linkType: hard -"unified@npm:^11.0.0, unified@npm:^11.0.3, unified@npm:^11.0.4": - version: 11.0.5 - resolution: "unified@npm:11.0.5" - dependencies: - "@types/unist": "npm:^3.0.0" - bail: "npm:^2.0.0" - devlop: "npm:^1.0.0" - extend: "npm:^3.0.0" - is-plain-obj: "npm:^4.0.0" - trough: "npm:^2.0.0" - vfile: "npm:^6.0.0" - checksum: 10c0/53c8e685f56d11d9d458a43e0e74328a4d6386af51c8ac37a3dcabec74ce5026da21250590d4aff6733ccd7dc203116aae2b0769abc18cdf9639a54ae528dfc9 - languageName: node - linkType: hard - "unified@npm:^9.2.1": version: 9.2.2 resolution: "unified@npm:9.2.2" @@ -48445,15 +44725,6 @@ __metadata: languageName: node linkType: hard -"unique-string@npm:^3.0.0": - version: 3.0.0 - resolution: "unique-string@npm:3.0.0" - dependencies: - crypto-random-string: "npm:^4.0.0" - checksum: 10c0/b35ea034b161b2a573666ec16c93076b4b6106b8b16c2415808d747ab3a0566b5db0c4be231d4b11cfbc16d7fd915c9d8a45884bff0e2db11b799775b2e1e017 - languageName: node - linkType: hard - "unist-builder@npm:^3.0.0": version: 3.0.1 resolution: "unist-builder@npm:3.0.1" @@ -48554,15 +44825,6 @@ __metadata: languageName: node linkType: hard -"unist-util-position-from-estree@npm:^2.0.0": - version: 2.0.0 - resolution: "unist-util-position-from-estree@npm:2.0.0" - dependencies: - "@types/unist": "npm:^3.0.0" - checksum: 10c0/39127bf5f0594e0a76d9241dec4f7aa26323517120ce1edd5ed91c8c1b9df7d6fb18af556e4b6250f1c7368825720ed892e2b6923be5cdc08a9bb16536dc37b3 - languageName: node - linkType: hard - "unist-util-position@npm:^4.0.0": version: 4.0.4 resolution: "unist-util-position@npm:4.0.4" @@ -48572,15 +44834,6 @@ __metadata: languageName: node linkType: hard -"unist-util-position@npm:^5.0.0": - version: 5.0.0 - resolution: "unist-util-position@npm:5.0.0" - dependencies: - "@types/unist": "npm:^3.0.0" - checksum: 10c0/dde3b31e314c98f12b4dc6402f9722b2bf35e96a4f2d463233dd90d7cde2d4928074a7a11eff0a5eb1f4e200f27fc1557e0a64a7e8e4da6558542f251b1b7400 - languageName: node - linkType: hard - "unist-util-remove-position@npm:^4.0.0": version: 4.0.2 resolution: "unist-util-remove-position@npm:4.0.2" @@ -48591,16 +44844,6 @@ __metadata: languageName: node linkType: hard -"unist-util-remove-position@npm:^5.0.0": - version: 5.0.0 - resolution: "unist-util-remove-position@npm:5.0.0" - dependencies: - "@types/unist": "npm:^3.0.0" - unist-util-visit: "npm:^5.0.0" - checksum: 10c0/e8c76da4399446b3da2d1c84a97c607b37d03d1d92561e14838cbe4fdcb485bfc06c06cfadbb808ccb72105a80643976d0660d1fe222ca372203075be9d71105 - languageName: node - linkType: hard - "unist-util-select@npm:^4.0.0, unist-util-select@npm:^4.0.1": version: 4.0.3 resolution: "unist-util-select@npm:4.0.3" @@ -48631,15 +44874,6 @@ __metadata: languageName: node linkType: hard -"unist-util-stringify-position@npm:^4.0.0": - version: 4.0.0 - resolution: "unist-util-stringify-position@npm:4.0.0" - dependencies: - "@types/unist": "npm:^3.0.0" - checksum: 10c0/dfe1dbe79ba31f589108cb35e523f14029b6675d741a79dea7e5f3d098785045d556d5650ec6a8338af11e9e78d2a30df12b1ee86529cded1098da3f17ee999e - languageName: node - linkType: hard - "unist-util-visit-parents@npm:^3.0.0": version: 3.1.1 resolution: "unist-util-visit-parents@npm:3.1.1" @@ -48846,28 +45080,6 @@ __metadata: languageName: node linkType: hard -"update-notifier@npm:^6.0.2": - version: 6.0.2 - resolution: "update-notifier@npm:6.0.2" - dependencies: - boxen: "npm:^7.0.0" - chalk: "npm:^5.0.1" - configstore: "npm:^6.0.0" - has-yarn: "npm:^3.0.0" - import-lazy: "npm:^4.0.0" - is-ci: "npm:^3.0.1" - is-installed-globally: "npm:^0.4.0" - is-npm: "npm:^6.0.0" - is-yarn-global: "npm:^0.4.0" - latest-version: "npm:^7.0.0" - pupa: "npm:^3.1.0" - semver: "npm:^7.3.7" - semver-diff: "npm:^4.0.0" - xdg-basedir: "npm:^5.1.0" - checksum: 10c0/ad3980073312df904133a6e6c554a7f9d0832ed6275e55f5a546313fe77a0f20f23a7b1b4aeb409e20a78afb06f4d3b2b28b332d9cfb55745b5d1ea155810bcc - languageName: node - linkType: hard - "upper-case-first@npm:^2.0.2": version: 2.0.2 resolution: "upper-case-first@npm:2.0.2" @@ -48909,23 +45121,6 @@ __metadata: languageName: node linkType: hard -"url-loader@npm:^4.1.1": - version: 4.1.1 - resolution: "url-loader@npm:4.1.1" - dependencies: - loader-utils: "npm:^2.0.0" - mime-types: "npm:^2.1.27" - schema-utils: "npm:^3.0.0" - peerDependencies: - file-loader: "*" - webpack: ^4.0.0 || ^5.0.0 - peerDependenciesMeta: - file-loader: - optional: true - checksum: 10c0/71b6300e02ce26c70625eae1a2297c0737635038c62691bb3007ac33e85c0130efc74bfb444baf5c6b3bad5953491159d31d66498967d1417865d0c7e7cd1a64 - languageName: node - linkType: hard - "url-parse-lax@npm:^3.0.0": version: 3.0.0 resolution: "url-parse-lax@npm:3.0.0" @@ -48952,7 +45147,7 @@ __metadata: languageName: node linkType: hard -"url@npm:^0.11.0, url@npm:~0.11.0": +"url@npm:~0.11.0": version: 0.11.4 resolution: "url@npm:0.11.4" dependencies: @@ -49129,13 +45324,6 @@ __metadata: languageName: node linkType: hard -"utila@npm:~0.4": - version: 0.4.0 - resolution: "utila@npm:0.4.0" - checksum: 10c0/2791604e09ca4f77ae314df83e80d1805f867eb5c7e13e7413caee01273c278cf2c9a3670d8d25c889a877f7b149d892fe61b0181a81654b425e9622ab23d42e - languageName: node - linkType: hard - "utility-types@npm:^3.10.0": version: 3.11.0 resolution: "utility-types@npm:3.11.0" @@ -49349,16 +45537,6 @@ __metadata: languageName: node linkType: hard -"vfile-location@npm:^5.0.0": - version: 5.0.3 - resolution: "vfile-location@npm:5.0.3" - dependencies: - "@types/unist": "npm:^3.0.0" - vfile: "npm:^6.0.0" - checksum: 10c0/1711f67802a5bc175ea69750d59863343ed43d1b1bb25c0a9063e4c70595e673e53e2ed5cdbb6dcdc370059b31605144d95e8c061b9361bcc2b036b8f63a4966 - languageName: node - linkType: hard - "vfile-matter@npm:^3.0.1": version: 3.0.1 resolution: "vfile-matter@npm:3.0.1" @@ -49390,16 +45568,6 @@ __metadata: languageName: node linkType: hard -"vfile-message@npm:^4.0.0": - version: 4.0.2 - resolution: "vfile-message@npm:4.0.2" - dependencies: - "@types/unist": "npm:^3.0.0" - unist-util-stringify-position: "npm:^4.0.0" - checksum: 10c0/07671d239a075f888b78f318bc1d54de02799db4e9dce322474e67c35d75ac4a5ac0aaf37b18801d91c9f8152974ea39678aa72d7198758b07f3ba04fb7d7514 - languageName: node - linkType: hard - "vfile@npm:^4.0.0": version: 4.2.1 resolution: "vfile@npm:4.2.1" @@ -49424,17 +45592,6 @@ __metadata: languageName: node linkType: hard -"vfile@npm:^6.0.0, vfile@npm:^6.0.1": - version: 6.0.2 - resolution: "vfile@npm:6.0.2" - dependencies: - "@types/unist": "npm:^3.0.0" - unist-util-stringify-position: "npm:^4.0.0" - vfile-message: "npm:^4.0.0" - checksum: 10c0/96b7e060b332ff1b05462053bd9b0f39062c00c5eabb78fc75603cc808d5f77c4379857fffca3e30a28e0aad2d51c065dfcd4a43fbe15b1fc9c2aaa9ac1be8e1 - languageName: node - linkType: hard - "vinyl-file@npm:^3.0.0": version: 3.0.0 resolution: "vinyl-file@npm:3.0.0" @@ -49682,7 +45839,7 @@ __metadata: languageName: node linkType: hard -"vm-browserify@npm:^1.0.0, vm-browserify@npm:^1.1.2": +"vm-browserify@npm:^1.0.0": version: 1.1.2 resolution: "vm-browserify@npm:1.1.2" checksum: 10c0/0cc1af6e0d880deb58bc974921320c187f9e0a94f25570fca6b1bd64e798ce454ab87dfd797551b1b0cc1849307421aae0193cedf5f06bdb5680476780ee344b @@ -49860,7 +46017,7 @@ __metadata: languageName: node linkType: hard -"watchpack@npm:^2.2.0, watchpack@npm:^2.4.0, watchpack@npm:^2.4.1": +"watchpack@npm:^2.2.0, watchpack@npm:^2.4.0": version: 2.4.1 resolution: "watchpack@npm:2.4.1" dependencies: @@ -49870,15 +46027,6 @@ __metadata: languageName: node linkType: hard -"wbuf@npm:^1.1.0, wbuf@npm:^1.7.3": - version: 1.7.3 - resolution: "wbuf@npm:1.7.3" - dependencies: - minimalistic-assert: "npm:^1.0.0" - checksum: 10c0/56edcc5ef2b3d30913ba8f1f5cccc364d180670b24d5f3f8849c1e6fb514e5c7e3a87548ae61227a82859eba6269c11393ae24ce12a2ea1ecb9b465718ddced7 - languageName: node - linkType: hard - "wcwidth@npm:^1.0.0, wcwidth@npm:^1.0.1": version: 1.0.1 resolution: "wcwidth@npm:1.0.1" @@ -49943,101 +46091,6 @@ __metadata: languageName: node linkType: hard -"webpack-bundle-analyzer@npm:^4.9.0": - version: 4.10.2 - resolution: "webpack-bundle-analyzer@npm:4.10.2" - dependencies: - "@discoveryjs/json-ext": "npm:0.5.7" - acorn: "npm:^8.0.4" - acorn-walk: "npm:^8.0.0" - commander: "npm:^7.2.0" - debounce: "npm:^1.2.1" - escape-string-regexp: "npm:^4.0.0" - gzip-size: "npm:^6.0.0" - html-escaper: "npm:^2.0.2" - opener: "npm:^1.5.2" - picocolors: "npm:^1.0.0" - sirv: "npm:^2.0.3" - ws: "npm:^7.3.1" - bin: - webpack-bundle-analyzer: lib/bin/analyzer.js - checksum: 10c0/00603040e244ead15b2d92981f0559fa14216381349412a30070a7358eb3994cd61a8221d34a3b3fb8202dc3d1c5ee1fbbe94c5c52da536e5b410aa1cf279a48 - languageName: node - linkType: hard - -"webpack-dev-middleware@npm:^5.3.4": - version: 5.3.4 - resolution: "webpack-dev-middleware@npm:5.3.4" - dependencies: - colorette: "npm:^2.0.10" - memfs: "npm:^3.4.3" - mime-types: "npm:^2.1.31" - range-parser: "npm:^1.2.1" - schema-utils: "npm:^4.0.0" - peerDependencies: - webpack: ^4.0.0 || ^5.0.0 - checksum: 10c0/257df7d6bc5494d1d3cb66bba70fbdf5a6e0423e39b6420f7631aeb52435afbfbff8410a62146dcdf3d2f945c62e03193aae2ac1194a2f7d5a2523b9d194e9e1 - languageName: node - linkType: hard - -"webpack-dev-server@npm:^4.15.1": - version: 4.15.2 - resolution: "webpack-dev-server@npm:4.15.2" - dependencies: - "@types/bonjour": "npm:^3.5.9" - "@types/connect-history-api-fallback": "npm:^1.3.5" - "@types/express": "npm:^4.17.13" - "@types/serve-index": "npm:^1.9.1" - "@types/serve-static": "npm:^1.13.10" - "@types/sockjs": "npm:^0.3.33" - "@types/ws": "npm:^8.5.5" - ansi-html-community: "npm:^0.0.8" - bonjour-service: "npm:^1.0.11" - chokidar: "npm:^3.5.3" - colorette: "npm:^2.0.10" - compression: "npm:^1.7.4" - connect-history-api-fallback: "npm:^2.0.0" - default-gateway: "npm:^6.0.3" - express: "npm:^4.17.3" - graceful-fs: "npm:^4.2.6" - html-entities: "npm:^2.3.2" - http-proxy-middleware: "npm:^2.0.3" - ipaddr.js: "npm:^2.0.1" - launch-editor: "npm:^2.6.0" - open: "npm:^8.0.9" - p-retry: "npm:^4.5.0" - rimraf: "npm:^3.0.2" - schema-utils: "npm:^4.0.0" - selfsigned: "npm:^2.1.1" - serve-index: "npm:^1.9.1" - sockjs: "npm:^0.3.24" - spdy: "npm:^4.0.2" - webpack-dev-middleware: "npm:^5.3.4" - ws: "npm:^8.13.0" - peerDependencies: - webpack: ^4.37.0 || ^5.0.0 - peerDependenciesMeta: - webpack: - optional: true - webpack-cli: - optional: true - bin: - webpack-dev-server: bin/webpack-dev-server.js - checksum: 10c0/625bd5b79360afcf98782c8b1fd710b180bb0e96d96b989defff550c546890010ceea82ffbecb2a0a23f7f018bc72f2dee7b3070f7b448fb0110df6657fb2904 - languageName: node - linkType: hard - -"webpack-merge@npm:^5.9.0": - version: 5.10.0 - resolution: "webpack-merge@npm:5.10.0" - dependencies: - clone-deep: "npm:^4.0.1" - flat: "npm:^5.0.2" - wildcard: "npm:^2.0.0" - checksum: 10c0/b607c84cabaf74689f965420051a55a08722d897bdd6c29cb0b2263b451c090f962d41ecf8c9bf56b0ab3de56e65476ace0a8ecda4f4a4663684243d90e0512b - languageName: node - linkType: hard - "webpack-node-externals@npm:3.0.0": version: 3.0.0 resolution: "webpack-node-externals@npm:3.0.0" @@ -50133,75 +46186,6 @@ __metadata: languageName: node linkType: hard -"webpack@npm:^5.88.1": - version: 5.93.0 - resolution: "webpack@npm:5.93.0" - dependencies: - "@types/eslint-scope": "npm:^3.7.3" - "@types/estree": "npm:^1.0.5" - "@webassemblyjs/ast": "npm:^1.12.1" - "@webassemblyjs/wasm-edit": "npm:^1.12.1" - "@webassemblyjs/wasm-parser": "npm:^1.12.1" - acorn: "npm:^8.7.1" - acorn-import-attributes: "npm:^1.9.5" - browserslist: "npm:^4.21.10" - chrome-trace-event: "npm:^1.0.2" - enhanced-resolve: "npm:^5.17.0" - es-module-lexer: "npm:^1.2.1" - eslint-scope: "npm:5.1.1" - events: "npm:^3.2.0" - glob-to-regexp: "npm:^0.4.1" - graceful-fs: "npm:^4.2.11" - json-parse-even-better-errors: "npm:^2.3.1" - loader-runner: "npm:^4.2.0" - mime-types: "npm:^2.1.27" - neo-async: "npm:^2.6.2" - schema-utils: "npm:^3.2.0" - tapable: "npm:^2.1.1" - terser-webpack-plugin: "npm:^5.3.10" - watchpack: "npm:^2.4.1" - webpack-sources: "npm:^3.2.3" - peerDependenciesMeta: - webpack-cli: - optional: true - bin: - webpack: bin/webpack.js - checksum: 10c0/f0c72f1325ff57a4cc461bb978e6e1296f2a7d45c9765965271aa686ccdd448512956f4d7fdcf8c164d073af046c5a0aba17ce85ea98e33e5e2bfbfe13aa5808 - languageName: node - linkType: hard - -"webpackbar@npm:^5.0.2": - version: 5.0.2 - resolution: "webpackbar@npm:5.0.2" - dependencies: - chalk: "npm:^4.1.0" - consola: "npm:^2.15.3" - pretty-time: "npm:^1.1.0" - std-env: "npm:^3.0.1" - peerDependencies: - webpack: 3 || 4 || 5 - checksum: 10c0/336568a6ed1c1ad743c8d20a5cab5875a7ebe1e96181f49ae0a1a897f1a59d1661d837574a25d8ba9dfa4f2f705bd46ca0cd037ff60286ff70fb8d9db2b0c123 - languageName: node - linkType: hard - -"websocket-driver@npm:>=0.5.1, websocket-driver@npm:^0.7.4": - version: 0.7.4 - resolution: "websocket-driver@npm:0.7.4" - dependencies: - http-parser-js: "npm:>=0.5.1" - safe-buffer: "npm:>=5.1.0" - websocket-extensions: "npm:>=0.1.1" - checksum: 10c0/5f09547912b27bdc57bac17b7b6527d8993aa4ac8a2d10588bb74aebaf785fdcf64fea034aae0c359b7adff2044dd66f3d03866e4685571f81b13e548f9021f1 - languageName: node - linkType: hard - -"websocket-extensions@npm:>=0.1.1": - version: 0.1.4 - resolution: "websocket-extensions@npm:0.1.4" - checksum: 10c0/bbc8c233388a0eb8a40786ee2e30d35935cacbfe26ab188b3e020987e85d519c2009fe07cfc37b7f718b85afdba7e54654c9153e6697301f72561bfe429177e0 - languageName: node - linkType: hard - "whatwg-encoding@npm:^2.0.0": version: 2.0.0 resolution: "whatwg-encoding@npm:2.0.0" @@ -50341,7 +46325,7 @@ __metadata: languageName: node linkType: hard -"which@npm:^1.2.12, which@npm:^1.2.9, which@npm:^1.3.1": +"which@npm:^1.2.12, which@npm:^1.2.9": version: 1.3.1 resolution: "which@npm:1.3.1" dependencies: @@ -50415,22 +46399,6 @@ __metadata: languageName: node linkType: hard -"widest-line@npm:^4.0.1": - version: 4.0.1 - resolution: "widest-line@npm:4.0.1" - dependencies: - string-width: "npm:^5.0.1" - checksum: 10c0/7da9525ba45eaf3e4ed1a20f3dcb9b85bd9443962450694dae950f4bdd752839747bbc14713522b0b93080007de8e8af677a61a8c2114aa553ad52bde72d0f9c - languageName: node - linkType: hard - -"wildcard@npm:^2.0.0": - version: 2.0.1 - resolution: "wildcard@npm:2.0.1" - checksum: 10c0/08f70cd97dd9a20aea280847a1fe8148e17cae7d231640e41eb26d2388697cbe65b67fd9e68715251c39b080c5ae4f76d71a9a69fa101d897273efdfb1b58bf7 - languageName: node - linkType: hard - "windows-release@npm:^4.0.0": version: 4.0.0 resolution: "windows-release@npm:4.0.0" @@ -50483,7 +46451,7 @@ __metadata: languageName: node linkType: hard -"wrap-ansi@npm:^8.0.1, wrap-ansi@npm:^8.1.0": +"wrap-ansi@npm:^8.1.0": version: 8.1.0 resolution: "wrap-ansi@npm:8.1.0" dependencies: @@ -50564,7 +46532,7 @@ __metadata: languageName: node linkType: hard -"ws@npm:^5.2.0 || ^6.0.0 || ^7.0.0, ws@npm:^7.3.1": +"ws@npm:^5.2.0 || ^6.0.0 || ^7.0.0": version: 7.5.10 resolution: "ws@npm:7.5.10" peerDependencies: @@ -50617,13 +46585,6 @@ __metadata: languageName: node linkType: hard -"xdg-basedir@npm:^5.0.1, xdg-basedir@npm:^5.1.0": - version: 5.1.0 - resolution: "xdg-basedir@npm:5.1.0" - checksum: 10c0/c88efabc71ffd996ba9ad8923a8cc1c7c020a03e2c59f0ffa72e06be9e724ad2a0fccef488757bc6ed3d8849d753dd25082d1035d95cb179e79eae4d034d0b80 - languageName: node - linkType: hard - "xlsx-ugnis@npm:^0.19.3": version: 0.19.3 resolution: "xlsx-ugnis@npm:0.19.3" @@ -50642,17 +46603,6 @@ __metadata: languageName: node linkType: hard -"xml-js@npm:^1.6.11": - version: 1.6.11 - resolution: "xml-js@npm:1.6.11" - dependencies: - sax: "npm:^1.2.4" - bin: - xml-js: ./bin/cli.js - checksum: 10c0/c83631057f10bf90ea785cee434a8a1a0030c7314fe737ad9bf568a281083b565b28b14c9e9ba82f11fc9dc582a3a907904956af60beb725be1c9ad4b030bc5a - languageName: node - linkType: hard - "xml-name-validator@npm:^4.0.0": version: 4.0.0 resolution: "xml-name-validator@npm:4.0.0"