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 @@
+
+
+
+
+
@@ -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 && (
-
- )}
- {callToAction && (
-
- )}
-
- );
-};
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 (
- <>
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- >
- );
-};
-
-const createRouter = (
- isBillingEnabled?: boolean,
- isCRMMigrationEnabled?: boolean,
- isServerlessFunctionSettingsEnabled?: boolean,
-) =>
- createBrowserRouter(
- createRoutesFromElements(
- }
- // To switch state to `loading` temporarily to enable us
- // to set scroll position before the page is rendered
- loader={async () => Promise.resolve(null)}
- >
- }>
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- } />
- }
- />
- >} />
- } />
- } />
- } />
-
- }
- />
- } />
-
- }>
- } />
-
- ,
- ),
- );
-
-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 (
-
- );
-};
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 = {
- title: 'App/App',
- component: App,
+const meta: Meta = {
+ title: 'App/AppRouter',
+ component: AppRouter,
decorators: [
(Story) => {
return (
@@ -41,7 +42,7 @@ const meta: Meta = {
};
export default meta;
-export type Story = StoryObj;
+export type Story = StoryObj;
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;
};
-export type CreateServerlessFunctionFromFileInput = {
- description?: InputMaybe;
- name: Scalars['String']['input'];
-};
-
export type CreateServerlessFunctionInput = {
- code: Scalars['String']['input'];
description?: InputMaybe;
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;
@@ -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;
};
@@ -652,6 +658,7 @@ export type MutationSignUpArgs = {
email: Scalars['String']['input'];
password: Scalars['String']['input'];
workspaceInviteHash?: InputMaybe;
+ workspacePersonalInviteToken?: InputMaybe;
};
@@ -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;
findOneRemoteServerById: RemoteServer;
findWorkspaceFromInviteHash: Workspace;
+ findWorkspaceInvitations: Array;
getAISQLQuery: AisqlQueryResult;
getAvailablePackages: Scalars['JSON']['output'];
getPostgresCredentials?: Maybe;
getProductPrices: ProductPricesEntity;
- getServerlessFunctionSourceCode?: Maybe;
+ getServerlessFunctionSourceCode?: Maybe;
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;
+ result: Array;
/** Boolean that confirms query was dispatched */
success: Scalars['Boolean']['output'];
};
@@ -1068,7 +1078,6 @@ export type ServerlessFunction = {
latestVersion?: Maybe;
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;
/** 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;
export const RemoteTableFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteTableFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteTable"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"schemaPendingUpdates"}}]}}]} as unknown as DocumentNode;
-export const ServerlessFunctionFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"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;
+export const ServerlessFunctionFieldsFragmentDoc = {"kind":"Document","definitions":[{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode;
export const CreateServerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"createServer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateRemoteServerInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneRemoteServer"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"RemoteServerFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteServerFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServer"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperId"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperOptions"}},{"kind":"Field","name":{"kind":"Name","value":"foreignDataWrapperType"}},{"kind":"Field","name":{"kind":"Name","value":"userMappingOptions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"user"}}]}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}},{"kind":"Field","name":{"kind":"Name","value":"label"}}]}}]} as unknown as DocumentNode;
export const DeleteServerDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"deleteServer"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteServerIdInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneRemoteServer"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode;
export const SyncRemoteTableDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"syncRemoteTable"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteTableInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"syncRemoteTable"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"RemoteTableFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"RemoteTableFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"RemoteTable"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"schema"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"schemaPendingUpdates"}}]}}]} as unknown as DocumentNode;
@@ -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;
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;
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;
-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;
+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;
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;
-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;
+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;
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;
export const DeleteOneObjectMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneObjectMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneObject"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}}]}}]}}]} as unknown as DocumentNode;
-export const DeleteOneFieldMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneFieldMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneField"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode;
+export const DeleteOneFieldMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneFieldMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneField"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isNullable"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"settings"}}]}}]}}]} as unknown as DocumentNode;
export const DeleteOneRelationMetadataItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneRelationMetadataItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneRelation"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"idToDelete"}}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode;
-export const ObjectMetadataItemsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"ObjectMetadataItems"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"objectFilter"}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"fieldFilter"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"fieldFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"objects"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"1000"}}]}},{"kind":"Argument","name":{"kind":"Name","value":"filter"},"value":{"kind":"Variable","name":{"kind":"Name","value":"objectFilter"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"dataSourceId"}},{"kind":"Field","name":{"kind":"Name","value":"nameSingular"}},{"kind":"Field","name":{"kind":"Name","value":"namePlural"}},{"kind":"Field","name":{"kind":"Name","value":"labelSingular"}},{"kind":"Field","name":{"kind":"Name","value":"labelPlural"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"icon"}},{"kind":"Field","name":{"kind":"Name","value":"isCustom"}},{"kind":"Field","name":{"kind":"Name","value":"isRemote"}},{"kind":"Field","name":{"kind":"Name","value":"isActive"}},{"kind":"Field","name":{"kind":"Name","value":"isSystem"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}},{"kind":"Field","name":{"kind":"Name","value":"labelIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"imageIdentifierFieldMetadataId"}},{"kind":"Field","name":{"kind":"Name","value":"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;
-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;
-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;
+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;
+export const CreateOneServerlessFunctionItemDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"CreateOneServerlessFunctionItem"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"CreateServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"createOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode;
+export const DeleteOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"DeleteServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode;
export const ExecuteOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"ExecuteOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"ExecuteServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"executeOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"duration"}},{"kind":"Field","name":{"kind":"Name","value":"status"}},{"kind":"Field","name":{"kind":"Name","value":"error"}}]}}]}}]} as unknown as DocumentNode;
-export const PublishOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"PublishOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PublishServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"publishServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"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;
-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;
+export const PublishOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"PublishOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PublishServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"publishServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode;
+export const UpdateOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateServerlessFunctionInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateOneServerlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode;
export const FindManyAvailablePackagesDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindManyAvailablePackages"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAvailablePackages"}}]}}]} as unknown as DocumentNode;
-export const GetManyServerlessFunctionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetManyServerlessFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverlessFunctions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"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;
-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;
+export const GetManyServerlessFunctionsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetManyServerlessFunctions"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverlessFunctions"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"paging"},"value":{"kind":"ObjectValue","fields":[{"kind":"ObjectField","name":{"kind":"Name","value":"first"},"value":{"kind":"IntValue","value":"100"}}]}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"edges"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"node"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode;
+export const GetOneServerlessFunctionDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetOneServerlessFunction"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"serverlessFunction"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ServerlessFunctionFields"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ServerlessFunctionFields"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ServerlessFunction"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"runtime"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"latestVersion"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode;
export const FindOneServerlessFunctionSourceCodeDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindOneServerlessFunctionSourceCode"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"GetServerlessFunctionSourceCodeInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getServerlessFunctionSourceCode"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}]}]}}]} as unknown as DocumentNode;
\ No newline at end of file
diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx
index 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(
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- ,
-);
+root.render();
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}
>
-
+
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 && }
+ {title && (
+
+ )}
{Array.from({ length }).map((_, index) => (
-
+
))}
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}
>
-
+
);
@@ -73,7 +77,7 @@ const StyledSkeletonAddLoader = () => {
highlightColor={BACKGROUND_LIGHT.transparent.lighter}
borderRadius={4}
>
-
+
);
};
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 {children};
+};
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) => void;
+ disabled?: boolean;
+}>) => {
+ const handleClick = (event: React.MouseEvent) => {
+ if (disabled !== true) {
+ onClick?.(event);
+ }
+ };
+
+ return (
+
+ {children}
+
+ );
+};
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}
>
-
+
{withSubSections &&
skeletonItems.map(({ id }, index) => (
-
+
-
-
- {index === 1 && }
+
+
+ {index === 1 && (
+
+ )}
))}
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(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 (
- handleThreadClick(event)}
- divider={divider}
- visibility={visibility}
+ disabled={isDisabled}
>
@@ -201,6 +180,6 @@ export const EmailThreadPreview = ({
{formatToHumanReadableDate(thread.lastMessageReceivedAt)}
-
+
);
};
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 && (
-
- {timelineThreads?.map((thread: TimelineThread, index: number) => (
-
+
+ {timelineThreads?.map((thread: TimelineThread) => (
+
))}
-
+
)}
({
- __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 }) => (
-
- {children}
-
- ),
+ 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}
/>
) : (
-
+
{attachments.map((attachment) => (
))}
-
+
)}
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 (
-
+
{isEditing ? (
@@ -129,12 +131,14 @@ export const AttachmentRow = ({ attachment }: { attachment: Attachment }) => {
onKeyDown={handleOnKeyDown}
/>
) : (
-
- {attachment.name}
-
+
+
+
+
+
)}
@@ -154,7 +158,7 @@ export const AttachmentRow = ({ attachment }: { attachment: Attachment }) => {
onRename={handleRename}
/>
-
+
);
};
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 }) => (
-
-
-
- {children}
-
-
-
-);
+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 }) => (
-
-
- {children}
-
-
-);
+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 = {
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 (
-
+
);
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 (
@@ -103,7 +104,7 @@ export const TaskGroups = ({
title={status}
tasks={tasksByStatus}
button={
- showAddButton && (
+ (status === 'TODO' || !hasTodoStatus) && (
)
}
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}
-
+
{tasks.map((task) => (
))}
-
+
)}
>
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 (
- {
openActivityRightDrawer(task.id);
}}
@@ -130,6 +114,14 @@ export const TaskRow = ({ task }: { task: Task }) => {
+ {task.dueAt && (
+
+
+ {beautifyExactDate(task.dueAt)}
+
+ )}
{TaskTargetsContextProvider && (
{
/>
)}
-
-
- {task.dueAt && beautifyExactDate(task.dueAt)}
-
-
+
);
};
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 }) => (
-
-
- {children}
-
-
-);
+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 }) => (
- {children}
-);
-
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 = {
title: 'Modules/TimelineActivities/Rows/MainObject/EventRowMainObjectUpdated',
@@ -35,7 +35,9 @@ const meta: Meta = {
},
},
} 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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
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 (
+
+ );
+};
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 (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+};
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) => (
-
+ }>
} />
} />
@@ -345,12 +345,12 @@ export const SettingsRoutes = ({
element={}
/>
}
+ path={SettingsPath.ObjectNewFieldSelect}
+ element={}
/>
}
+ path={SettingsPath.ObjectNewFieldConfigure}
+ element={}
/>
{
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) => (
+
+ ));
+};
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(
+ }
+ // To switch state to `loading` temporarily to enable us
+ // to set scroll position before the page is rendered
+ loader={async () => Promise.resolve(null)}
+ >
+ }>
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ } />
+ }
+ />
+ >} />
+ } />
+ } />
+ } />
+
+ }
+ />
+ } />
+
+ }>
+ } />
+
+ ,
+ ),
+ );
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 (
-
-
- {message}
-
- );
-};
+// const StyledContainer = styled.div`
+// align-items: center;
+// display: flex;
+// flex-direction: column;
+// height: 100vh;
+// justify-content: center;
+// `;
+
+// const AppInaccessible = ({ message }: { message: string }) => {
+// return (
+//
+//
+// {message}
+//
+// );
+// };
export const ChromeExtensionSidecarProvider: React.FC<
React.PropsWithChildren
> = ({ children }) => {
- const isLoadingTokensFromExtension = useRecoilValue(
- isLoadingTokensFromExtensionState,
- );
- const chromeExtensionId = useRecoilValue(chromeExtensionIdState);
-
- if (!isInFrame()) return <>{children}>;
-
- if (!isDefined(chromeExtensionId))
- return (
-
- );
-
- if (isDefined(isLoadingTokensFromExtension) && !isLoadingTokensFromExtension)
- return (
-
- );
-
- return isLoadingTokensFromExtension && <>{children}>;
+ return <>{children}>;
+
+ // TODO: this is conflictting with storybook tests
+ // if (!isInFrame()) return <>{children}>;
+
+ // if (!isDefined(chromeExtensionId))
+ // return (
+ //
+ // );
+
+ // if (isDefined(isLoadingTokensFromExtension) && !isLoadingTokensFromExtension)
+ // return (
+ //
+ // );
+
+ // 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({
- 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({
+ 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({
+ skip: !isCommandMenuOpened || !isSearchEnabled,
+ objectNameSingular: CoreObjectNameSingular.Person,
+ limit: 3,
+ searchInput: commandMenuSearch ?? undefined,
+ });
- const { records: companies } = useFindManyRecords({
- skip: !isCommandMenuOpened,
+ const people = isSearchEnabled ? peopleFromSearch : peopleFromFindMany;
+
+ const { records: companiesFromSearch } = useSearchRecords({
+ skip: !isCommandMenuOpened || !isSearchEnabled,
+ objectNameSingular: CoreObjectNameSingular.Company,
+ limit: 3,
+ searchInput: commandMenuSearch ?? undefined,
+ });
+
+ const { records: companiesFromFindMany } = useFindManyRecords({
+ 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({
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({
+ 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 = {
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 = {
},
]);
openCommandMenu();
- }, [addToCommandMenu, setToInitialCommandMenu, openCommandMenu]);
+ }, [
+ addToCommandMenu,
+ setObjectsInCommandMenu,
+ openCommandMenu,
+ objectMetadataItems,
+ ]);
return ;
},
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({
+ 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({
+ 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}
>
-
+
-
-
+
+
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(PrefetchKey.AllViews);
- const loading = useIsPrefetchLoading();
-
- const { workspaceFavorites } = useFavorites();
-
- const workspaceFavoriteIds = new Set(
- workspaceFavorites.map((favorite) => favorite.recordId),
- );
- const favoriteViewObjectMetadataIds = views.reduce((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 ;
}
return (
({
- 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 }) => (
-
-
-
- {children}
-
-
-
-);
+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: ,
- footer: ,
+ footer: ,
}
: {
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 = () => {
)}
+ {isWorkspaceFavoriteEnabled && }
+
{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(PrefetchKey.AllViews);
+
+ const { workspaceFavorites } = useFavorites();
+
+ const workspaceFavoriteIds = new Set(
+ workspaceFavorites.map((favorite) => favorite.recordId),
+ );
+
+ const favoriteViewObjectMetadataIds = new Set(
+ views.reduce((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(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 ;
+ }
+
+ return (
+ shouldDisplayObjectInOpenedSection && (
+
+ )
+ );
+};
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}
>
-
-
-
+
+
+
);
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 }) => (
-
-
- {children}
-
-
-);
+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 }) => (
-
- {children}
-
-);
+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 }) => (
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 }) => (
-
- {children}
-
-);
+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.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,
+): 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
+ | 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
+ | 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 = ({
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 }) => (
-
-
- {children}
-
-
-);
+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 }) => (
-
-
- {children}
-
-
-);
+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 }) => (
-
-
- {children}
-
-
-);
+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 }) => (
-
-
- {children}
-
-
-);
+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 }) => (
-
-
-
- {children}
-
-
-
- );
-
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 }) => (
-
-
-
- {children}
-
-
-
-);
+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 }) => (
-
-
-
- {children}
-
-
-
-);
-
+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 }) => (
-
-
-
- {children}
-
-
-
-);
+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 }) => (
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 }) => (
-
-
-
- {children}
-
-
-
-);
+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 (
-
+
-
- {children}
-
+ {children}
-
+
);
};
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 }) => (
-
-
- {children}
-
-
-);
+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 = ({
onCompleted,
cursorFilter,
}: UseFindManyRecordsParams) => {
- const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { objectMetadataItem } = useObjectMetadataItem({
objectNameSingular,
});
@@ -66,7 +63,7 @@ export const useFindManyRecords = ({
const { data, loading, error, fetchMore } =
useQuery(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 = ({
+ 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(
+ 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 (
}
dropdownComponents={}
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 (
- <>
+
{!filterDefinitionUsedInDropdown ? (
- ) : isObjectFilterDropdownOperandSelectUnfolded ? (
-
- ) : isEmptyOperand ? (
-
) : (
- selectedOperandInDropdown && (
- <>
-
-
- {[
- 'TEXT',
- 'EMAIL',
- 'EMAILS',
- 'PHONE',
- 'FULL_NAME',
- 'LINK',
- 'LINKS',
- 'ADDRESS',
- 'ACTOR',
- 'ARRAY',
- 'PHONES',
- ].includes(filterDefinitionUsedInDropdown.type) && (
-
- )}
- {['NUMBER', 'CURRENCY'].includes(
- filterDefinitionUsedInDropdown.type,
- ) && }
- {filterDefinitionUsedInDropdown.type === 'RATING' && (
-
- )}
- {['DATE_TIME', 'DATE'].includes(
- filterDefinitionUsedInDropdown.type,
- ) && }
- {filterDefinitionUsedInDropdown.type === 'RELATION' && (
- <>
-
-
-
- >
- )}
- {filterDefinitionUsedInDropdown.type === 'SELECT' && (
- <>
-
-
-
- >
- )}
- >
- )
+ <>
+
+ {isObjectFilterDropdownOperandSelectUnfolded && (
+
+
+
+ )}
+ {isConfigurable && selectedOperandInDropdown && (
+ <>
+ {[
+ 'TEXT',
+ 'EMAIL',
+ 'EMAILS',
+ 'PHONE',
+ 'FULL_NAME',
+ 'LINK',
+ 'LINKS',
+ 'ADDRESS',
+ 'ACTOR',
+ 'ARRAY',
+ 'PHONES',
+ ].includes(filterDefinitionUsedInDropdown.type) &&
+ !isActorSourceCompositeFilter(
+ filterDefinitionUsedInDropdown,
+ ) && }
+ {['NUMBER', 'CURRENCY'].includes(
+ filterDefinitionUsedInDropdown.type,
+ ) && }
+ {filterDefinitionUsedInDropdown.type === 'RATING' && (
+
+ )}
+ {['DATE_TIME', 'DATE'].includes(
+ filterDefinitionUsedInDropdown.type,
+ ) && }
+ {filterDefinitionUsedInDropdown.type === 'RELATION' && (
+ <>
+
+
+ >
+ )}
+ {isActorSourceCompositeFilter(filterDefinitionUsedInDropdown) && (
+ <>
+
+
+ >
+ )}
+ {filterDefinitionUsedInDropdown.type === 'SELECT' && (
+ <>
+
+
+ >
+ )}
+ >
+ )}
+ >
)}
- >
+
);
};
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(
- 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 (
);
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(null);
+
+ const [firstLevelFilterDefinition, setFirstLevelFilterDefinition] =
+ useState(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 (
<>
- ) =>
- setSearchText(event.target.value)
- }
- />
-
-
- {sortedAvailableFilterDefinitions.map(
- (availableFilterDefinition, index) => (
-
-
-
- ),
- )}
-
-
+ {shouldShowFirstLevelMenu ? (
+ <>
+ ) =>
+ setObjectFilterDropdownSearchInput(event.target.value)
+ }
+ />
+
+
+ {[...availableFilterDefinitions]
+ .sort((a, b) => a.label.localeCompare(b.label))
+ .filter((item) =>
+ item.label
+ .toLocaleLowerCase()
+ .includes(
+ objectFilterDropdownSearchInput.toLocaleLowerCase(),
+ ),
+ )
+ .map((availableFilterDefinition, index) => (
+
+
+ ))}
+
+
+ >
+ ) : (
+
+ )}
>
);
};
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 (
+ <>
+
+ {getFilterableFieldTypeLabel(fieldType)}
+
+ ) =>
+ setSearchText(event.target.value)
+ }
+ />
+
+
+ >
+ );
+};
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 (
{
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 (
-
);
};
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 (
+
+ !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 | Record => {
+ 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 ? (
-
- {SORT_DIRECTIONS.map((sortOrder, index) => (
- {
- setSelectedSortDirection(sortOrder);
- setIsSortDirectionMenuUnfolded(false);
- }}
- text={sortOrder === 'asc' ? 'Ascending' : 'Descending'}
- />
- ))}
-
- ) : (
- <>
- setIsSortDirectionMenuUnfolded(true)}
- >
- {selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'}
-
-
- setObjectSortDropdownSearchInput(event.target.value)
- }
- />
+ {isSortDirectionMenuUnfolded && (
+
- {[...availableSortDefinitions]
- .sort((a, b) => a.label.localeCompare(b.label))
- .filter((item) =>
- item.label
- .toLocaleLowerCase()
- .includes(
- objectSortDropdownSearchInput.toLocaleLowerCase(),
- ),
- )
- .map((availableSortDefinition, index) => (
- {
- setObjectSortDropdownSearchInput('');
- handleAddSort(availableSortDefinition);
- }}
- LeftIcon={getIcon(availableSortDefinition.iconName)}
- text={availableSortDefinition.label}
- />
- ))}
+ {SORT_DIRECTIONS.map((sortOrder, index) => (
+ {
+ setSelectedSortDirection(sortOrder);
+ setIsSortDirectionMenuUnfolded(false);
+ }}
+ text={sortOrder === 'asc' ? 'Ascending' : 'Descending'}
+ />
+ ))}
- >
+
)}
+
+ setIsSortDirectionMenuUnfolded(true)}
+ >
+ {selectedSortDirection === 'asc' ? 'Ascending' : 'Descending'}
+
+
+ setObjectSortDropdownSearchInput(event.target.value)
+ }
+ />
+
+ {[...availableSortDefinitions]
+ .sort((a, b) => a.label.localeCompare(b.label))
+ .filter((item) =>
+ item.label
+ .toLocaleLowerCase()
+ .includes(
+ objectSortDropdownSearchInput.toLocaleLowerCase(),
+ ),
+ )
+ .map((availableSortDefinition, index) => (
+ {
+ setObjectSortDropdownSearchInput('');
+ handleAddSort(availableSortDefinition);
+ }}
+ LeftIcon={getIcon(availableSortDefinition.iconName)}
+ text={availableSortDefinition.label}
+ />
+ ))}
+
+
>
}
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 = ({
);
- 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 && (
{
e.stopPropagation();
- setIsCardInCompactMode(false);
+ setIsCardExpanded((prev) => !prev);
}}
/>
@@ -314,7 +311,7 @@ export const RecordBoardCard = ({
@@ -342,13 +339,14 @@ export const RecordBoardCard = ({
metadata: fieldDefinition.metadata,
type: fieldDefinition.type,
}),
+ settings: fieldDefinition.settings,
},
useUpdateRecord: useUpdateOneRecordHook,
hotkeyScope: InlineCellHotkeyScope.InlineCell,
}}
>
{inView ? (
-
+
) : (
)}
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 = ({
>
-
+
@@ -51,8 +55,14 @@ export const RecordBoardColumnCardContainerSkeletonLoader = ({
skeletonItems.map(({ id }) => (
-
-
+
+
))}
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 (
{objectMetadataItem.nameSingular ===
- CoreObjectNameSingular.Opportunity ? (
-
+ CoreObjectNameSingular.Opportunity &&
+ !isOpportunitiesCompanyFieldDisabled ? (
+
) : (
)}
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 = () => {
handleNewButtonClick('first', isOpportunity)}
/>
)}
@@ -165,23 +161,26 @@ export const RecordBoardColumnHeader = () => {
stageId={columnDefinition.id}
/>
)}
- {newRecord?.isCreating && newRecord.position === 'first' && (
- handleCreateSuccess('first')}
- position="first"
- />
- )}
- {isCreatingCard && (
-
- )}
+ {newRecord?.isCreating &&
+ newRecord.position === 'first' &&
+ (newRecord.isOpportunity ? (
+ handleCreateSuccess('first', columnDefinition.id)}
+ onEntitySelected={(company) =>
+ company && handleEntitySelect('first', company)
+ }
+ relationObjectNameSingular={CoreObjectNameSingular.Company}
+ relationPickerScopeId="relation-picker"
+ selectedRelationRecordIds={[]}
+ />
+ ) : (
+ 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 (
handleNewButtonClick('last')}>
+ handleNewButtonClick('last', false)}>
New
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 ? (
handleCreateSuccess('last', columnId, false)}
+ onEntitySelected={(company) =>
+ company ? handleEntitySelect('last', company) : null
+ }
relationObjectNameSingular={CoreObjectNameSingular.Company}
relationPickerScopeId="relation-picker"
selectedRelationRecordIds={[]}
/>
) : (
-
+ handleNewButtonClick('last', true)}>
New
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 = (
+ recoilVal: RecoilState,
+ 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 = {
@@ -24,7 +22,16 @@ export const textfieldDefinition: FieldDefinition = {
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 = {
},
};
-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) =>
({ children }: { children: ReactNode }) => {
@@ -91,7 +96,7 @@ const getWrapper =
};
return (
-
+
- {children}
+ {children}
-
+
);
};
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 (
-
+
{
useUpdateRecord: useUpdateOneRecordMutation,
}}
>
- {children}
+ {children}
-
+
);
};
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 ;
+ const { fieldValue, fieldDefinition } = useNumberFieldDisplay();
+ return (
+
+ );
};
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;
+type Story = StoryObj;
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;
-
-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;
+type Story = StoryObj;
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 (
{
onCancel={onCancel}
placeholder="Email"
fieldMetadataType={FieldMetadataType.Emails}
+ validateInput={validateInput}
renderItem={({
value: email,
index,
@@ -46,7 +58,7 @@ export const EmailsFieldInput = ({ onCancel }: EmailsFieldInputProps) => {
{
});
};
+ const isPrimaryLink = (index: number) => index === 0 && links?.length > 1;
+
return (
{
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) => {
= {
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 = ({
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 = ({
};
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 = ({
placeholder={placeholder}
value={inputValue}
hotkeyScope={hotkeyScope}
+ hasError={!errorData.isValid}
renderInput={
renderInput
? (props) =>
@@ -170,7 +190,7 @@ export const MultiItemFieldInput = ({
})
: undefined
}
- onChange={(event) => setInputValue(event.target.value)}
+ onChange={(event) => handleOnChange(event.target.value)}
onEnter={handleSubmitInput}
rightComponent={
{
});
};
+ const isPrimaryPhone = (index: number) => index === 0 && phones?.length > 1;
+
return (
{
= {
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 | Json[] | null;
+export type FieldRichTextValue = Record | 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 = {
fieldDefinition: Pick, 'type'>;
@@ -32,7 +33,9 @@ export const computeDraftValueFromFieldValue = ({
}
return {
- amount: fieldValue?.amountMicros ? fieldValue.amountMicros / 1000000 : '',
+ amount: isUndefinedOrNull(fieldValue?.amountMicros)
+ ? ''
+ : (fieldValue.amountMicros / 1000000).toString(),
currencyCode: fieldValue?.currencyCode ?? '',
} as unknown as FieldInputDraftValue;
}
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,
+ 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 & {
- definition: {
- type: Filter['definition']['type'];
- };
-};
-
-const applyEmptyFilters = (
- operand: ViewFilterOperand,
- correspondingField: Pick,
- 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[],
): 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();
- 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={
- {isOpportunity && isSelectingCompany ? (
-
- ) : (
-
- {columnIds.map((columnId) => (
-
- ))}
-
- )}
+
+ {columnIds.map((columnId) => (
+
+ ))}
+
}
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 }) => (
-
-
-
-
- {children}
-
-
-
-
-);
+const WrapperWithResponse = getJestMetadataAndApolloMocksWrapper({
+ apolloMocks: mocks,
+});
const graphqlEmptyResponse = [
{
@@ -197,21 +145,9 @@ const graphqlEmptyResponse = [
},
];
-const WrapperWithEmptyResponse = ({ children }: { children: ReactNode }) => (
-
-
-
-
- {children}
-
-
-
-
-);
+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: (
`
@@ -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}
>
-
+
);
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 }) => (
-
-
+
+
))}
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 (
-
-
-
-
+
+
+ {!isNewViewableRecordLoading && (
+
+ )}
+
+
+
);
};
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({
+ 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(
+ const [recordFromStore] = useRecoilState(
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) ? (
-
-
-
- }
- avatarType={recordIdentifier?.avatarType ?? 'rounded'}
- onUploadPicture={
- objectNameSingular === 'person' ? onUploadPicture : undefined
- }
- />
- ) : (
- <>>
- );
+ useUpdateRecord: useUpdateOneObjectRecordMutation,
+ hotkeyScope: InlineCellHotkeyScope.InlineCell,
+ isCentered: !isMobile,
+ }}
+ >
+
+
+ }
+ avatarType={recordIdentifier?.avatarType ?? 'rounded'}
+ onUploadPicture={
+ objectNameSingular === 'person' ? onUploadPicture : undefined
+ }
+ />
+ ) : (
+
+ );
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(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 = {
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}
>
-
+
);
};
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 && (
-
- )}
+
+ {isDefined(onCreate) && (
+ <>
+
+
- >
- )}
-
+
+ >
+ )}
>
);
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 (
{
- 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();
}}
>
- {recordsInDropdown?.map((record) => {
+ {itemsInDropdown?.map((item) => {
return (
{
resetSelectedItem();
- handleRecordSelectChange(record, newCheckedValue);
+ handleItemSelectChange(item, newCheckedValue);
}}
avatar={
-
}
- text={record.name}
/>
);
})}
{showNoResult && }
- {loadingRecords && }
+ {loadingItems && }
);
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 & {
+ 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 }) => (
-
-
-
- {children}
-
-
-
-);
+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>,
+ [FieldMetadataType.Emails]: {
+ primaryEmailLabel: 'Email',
+ } satisfies Partial>,
+ [FieldMetadataType.Phones]: {
+ primaryPhoneCountryCodeLabel: 'Phone country code',
+ primaryPhoneNumberLabel: 'Phone number',
+ } satisfies Partial>,
[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 ;
+ return ;
}
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 ;
+ return ;
}
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 = {
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 = {
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 = {
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
/>
-
- {isFunctionSettingsEnabled && (
-
- )}
{
/>
)}
+
+ {isAdvancedModeEnabled && (
+
+
+
+
+
+
+
+
+ {isFunctionSettingsEnabled && (
+
+ )}
+
+
+
+ )}
+
theme.spacing(8, 8, 2)};
+`;
+
+export const SettingsSkeletonLoader = () => {
+ const theme = useTheme();
+ return (
+
+
+ {' '}
+
+ }
+ />
+
+
+
+
+
+
+
+
+ );
+};
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 (
@@ -81,16 +110,21 @@ export const SettingsDataModelNewFieldBreadcrumbDropDown = ({
dropdownComponents={
- handleClick(false)}
- selected={!isConfigureStep}
- />
- handleClick(true)}
- selected={isConfigureStep}
- />
+
+ handleClick('select')}
+ selected={!isConfigureStep}
+ />
+
+
+ handleClick('configure')}
+ selected={isConfigureStep}
+ disabled={!isDefined(fieldType)}
+ />
+
}
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 = SettingsFieldTypeConfig & {
+ subFields: (keyof T)[];
+ filterableSubFields: (keyof T)[];
+ labelBySubField: Record;
+ exampleValue: T;
+};
+
+type SettingsCompositeFieldTypeConfigArray = Record<
+ CompositeFieldType,
+ SettingsCompositeFieldTypeConfig
+>;
+
+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,
+ [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,
+ [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,
+ [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,
+ [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,
+ [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,
+ [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,
+ [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,
+} 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 = {
+ label: string;
+ Icon: IconComponent;
+ exampleValue?: T;
+ category: SettingsFieldTypeCategoryType;
+};
+
+type SettingsNonCompositeFieldTypeConfigArray = Record<
+ SettingsNonCompositeFieldType,
+ SettingsFieldTypeConfig
+>;
+
+// 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,
+ [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,
+ [FieldMetadataType.Numeric]: {
+ label: 'Numeric',
+ Icon: IllustrationIconNumbers,
+ exampleValue: 2000,
+ category: 'Basic',
+ } as const satisfies SettingsFieldTypeConfig,
+ [FieldMetadataType.Number]: {
+ label: 'Number',
+ Icon: IllustrationIconNumbers,
+ exampleValue: 2000,
+ category: 'Basic',
+ } as const satisfies SettingsFieldTypeConfig,
+ [FieldMetadataType.Boolean]: {
+ label: 'True/False',
+ Icon: IllustrationIconToggle,
+ exampleValue: true,
+ category: 'Basic',
+ } as const satisfies SettingsFieldTypeConfig,
+ [FieldMetadataType.DateTime]: {
+ label: 'Date and Time',
+ Icon: IllustrationIconCalendarTime,
+ exampleValue: DEFAULT_DATE_VALUE.toISOString(),
+ category: 'Basic',
+ } as const satisfies SettingsFieldTypeConfig,
+ [FieldMetadataType.Date]: {
+ label: 'Date',
+ Icon: IllustrationIconCalendarEvent,
+ exampleValue: DEFAULT_DATE_VALUE.toISOString(),
+ category: 'Basic',
+ } as const satisfies SettingsFieldTypeConfig,
+ [FieldMetadataType.Select]: {
+ label: 'Select',
+ Icon: IllustrationIconTag,
+ category: 'Basic',
+ } as const satisfies SettingsFieldTypeConfig,
+ [FieldMetadataType.MultiSelect]: {
+ label: 'Multi-select',
+ Icon: IllustrationIconTags,
+ category: 'Basic',
+ } as const satisfies SettingsFieldTypeConfig,
+ [FieldMetadataType.Relation]: {
+ label: 'Relation',
+ Icon: IllustrationIconOneToMany,
+ category: 'Relation',
+ } as const satisfies SettingsFieldTypeConfig>,
+ [FieldMetadataType.Email]: {
+ label: 'Email',
+ Icon: IllustrationIconMail,
+ category: 'Basic',
+ } as const satisfies SettingsFieldTypeConfig,
+ [FieldMetadataType.Phone]: {
+ label: 'Phone',
+ Icon: IllustrationIconPhone,
+ exampleValue: '+1234-567-890',
+ category: 'Basic',
+ } as const satisfies SettingsFieldTypeConfig,
+ [FieldMetadataType.Rating]: {
+ label: 'Rating',
+ Icon: IllustrationIconStar,
+ exampleValue: 'RATING_3',
+ category: 'Basic',
+ } as const satisfies SettingsFieldTypeConfig,
+ [FieldMetadataType.RawJson]: {
+ label: 'JSON',
+ Icon: IllustrationIconJson,
+ exampleValue: { key: 'value' },
+ category: 'Basic',
+ } as const satisfies SettingsFieldTypeConfig,
+ [FieldMetadataType.RichText]: {
+ label: 'Rich Text',
+ Icon: IllustrationIconSetting,
+ exampleValue: { key: 'value' },
+ category: 'Basic',
+ } as const satisfies SettingsFieldTypeConfig,
+ [FieldMetadataType.Array]: {
+ label: 'Array',
+ Icon: IllustrationIconArray,
+ category: 'Basic',
+ exampleValue: ['value1', 'value2'],
+ } as const satisfies SettingsFieldTypeConfig,
+ };
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();
+ 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 (
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: Pick<
+ FieldMetadataItem,
+ 'icon' | 'label' | 'type' | 'isCustom'
+ > &
Partial>;
} & Pick;
@@ -163,6 +174,16 @@ export const SettingsDataModelFieldSettingsFormCard = ({
);
}
+ if (fieldMetadataItem.type === FieldMetadataType.Number) {
+ return (
+
+ );
+ }
+
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();
- const [searchQuery, setSearchQuery] = useState('');
- const fieldTypeConfigs = Object.entries(
- 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 (
- (
-
-
- {SETTINGS_FIELD_TYPE_CATEGORIES.map((category) => (
-
-
-
- {fieldTypeConfigs
- .filter(([, config]) => config.category === category)
- .map(([key, config]) => (
-
- {
- onChange(key as SettingsSupportedFieldType);
- resetDefaultValueField(
- key as SettingsSupportedFieldType,
- );
- onFieldTypeSelect();
- }}
- Icon={
-
- }
- title={config.label}
- />
-
- ))}
-
-
- ))}
-
- )}
- />
- );
-};
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();
+ const [searchQuery, setSearchQuery] = useState('');
+ const fieldTypeConfigs = Object.entries>(
+ 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 (
+ <>
+ {' '}
+
+ (
+
+ {SETTINGS_FIELD_TYPE_CATEGORIES.map((category) => (
+
+
+
+ {fieldTypeConfigs
+ .filter(([, config]) => config.category === category)
+ .map(([key, config]) => (
+
+ {
+ setValue('type', key as SettingsFieldType);
+ resetDefaultValueField(key as SettingsFieldType);
+ }}
+ >
+
+ }
+ title={config.label}
+ />
+
+
+ ))}
+
+
+ ))}
+
+ )}
+ />
+ >
+ );
+};
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 = {
@@ -25,11 +25,15 @@ type Story = StoryObj;
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;
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 = {
- title:
- 'Modules/Settings/DataModel/Fields/Forms/SettingsDataModelFieldTypeSelect',
- component: SettingsDataModelFieldTypeSelect,
- decorators: [FormProviderDecorator, ComponentDecorator],
- parameters: {
- container: { width: 512 },
- msw: graphqlMocks,
- },
-};
-
-export default meta;
-type Story = StoryObj;
-
-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 (
+ <>
+ Number of decimals
+
+
+ Example: {exampleValue}
+
+
+
+ handleTextInputChange(value)}
+ disabled={disabled}
+ />
+
+
+
+
+ >
+ );
+};
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();
+
+ return (
+
+ {
+ const count = value?.decimals ?? 0;
+
+ return (
+ onChange({ decimals: value })}
+ disabled={disabled}
+ >
+ );
+ }}
+ />
+
+ );
+};
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;
+
+const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
+ display: grid;
+ flex: 1 1 100%;
+`;
+
+export const SettingsDataModelFieldNumberSettingsFormCard = ({
+ disabled,
+ fieldMetadataItem,
+ objectMetadataItem,
+}: SettingsDataModelFieldNumberSettingsFormCardProps) => {
+ return (
+
+ }
+ form={
+
+ }
+ />
+ );
+};
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;
+ 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();
+ const {
+ control,
+ watch: watchFormValue,
+ setValue,
+ } = useFormContext();
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 (
-
+
{
+ 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 &
Partial>;
@@ -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();
const { findObjectMetadataItemById } = useFilteredObjectMetadataItems();
-
+ const isMobile = useIsMobile();
const {
initialRelationObjectMetadataItem,
initialRelationType,
@@ -69,7 +78,7 @@ export const SettingsDataModelFieldRelationSettingsFormCard = ({
return (
+
}
/>
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;
+ 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 = ({
{
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}
/>
{
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 = {
title:
'Modules/Settings/DataModel/Fields/Preview/SettingsDataModelFieldPreviewCard',
@@ -38,7 +47,7 @@ type Story = StoryObj;
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 }) => (
-
-
-
- {children}
-
-
-
+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 = (
- 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 ? (
+
+
+
+ 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}
+ />
+
+
+ ) : (
+ <>>
+ )}
+ >
+ );
+};
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({
+ 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(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 = ({
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 ? (
-
-
- Name
- Runtime
-
-
-
- {serverlessFunctions.map(
- (serverlessFunction: ServerlessFunction) => (
-
- ),
- )}
-
-
+
+
+
+ Name
+ Runtime
+
+
+
+ {serverlessFunctions.map(
+ (serverlessFunction: ServerlessFunction) => (
+
+ ),
+ )}
+
+
+
) : (
)}
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 = (
);
- const TAB_LIST_COMPONENT_ID = 'serverless-function-editor';
-
const HeaderTabList = (
{
+ return { id: file.path, title: file.path.split('/').at(-1) || '' };
+ })}
/>
);
- const Header = (
-
- );
const navigate = useNavigate();
useHotkeyScopeOnMount(
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionEditorTab,
@@ -95,18 +95,25 @@ export const SettingsServerlessFunctionCodeEditorTab = ({
},
SettingsServerlessFunctionHotkeyScope.ServerlessFunctionEditorTab,
);
+
return (
-
+ {activeTabId && (
+ onChange(activeTabId, newCodeValue)}
+ setIsCodeValid={setIsCodeValid}
+ />
+ )}
);
};
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 = (
- ,
- ]}
- />
- );
-
- const OutputHeader = (
- ]}
- rightNodes={[]}
- />
- );
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.'
/>
-
-
+
+ ,
+ ]}
+ />
+
+
+
+ ]}
+ rightNodes={[]}
+ />
+
+
);
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({
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 = (
props: SpreadsheetImportProps,
) => {
+ const mergedProps = {
+ ...defaultSpreadsheetImportProps,
+ ...props,
+ } as SpreadsheetImportProps;
+
return (
-
-
+
+
);
};
-
-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}
>
-
+
);
};
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 & {
@@ -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 (
{
? 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 {0};
@@ -46,7 +49,7 @@ export const CurrencyDisplay = ({ currencyValue }: CurrencyDisplayProps) => {
/>{' '}
>
)}
- {amountToDisplay !== 0 ? formatAmount(amountToDisplay) : ''}
+ {amountToDisplay !== null ? formatAmount(amountToDisplay) : ''}
);
};
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) => (
- {value && formatNumber(Number(value))}
+export const NumberDisplay = ({ value, decimals }: NumberDisplayProps) => (
+
+ {value && formatNumber(Number(value), decimals)}
+
);
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
/>
);
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