diff --git a/.github/workflows/ci-front.yaml b/.github/workflows/ci-front.yaml
index b61506709f93..506a6dce0e5e 100644
--- a/.github/workflows/ci-front.yaml
+++ b/.github/workflows/ci-front.yaml
@@ -43,7 +43,8 @@ jobs:
- name: Front / Build storybook
run: npx nx storybook:build twenty-front
front-sb-test:
- runs-on: ci-8-cores
+ runs-on: shipfox-8vcpu-ubuntu-2204
+ timeout-minutes: 60
needs: front-sb-build
strategy:
matrix:
@@ -68,7 +69,8 @@ jobs:
- name: Run storybook tests
run: npx nx storybook:serve-and-test:static twenty-front --configuration=${{ matrix.storybook_scope }}
front-sb-test-performance:
- runs-on: ci-8-cores
+ runs-on: shipfox-8vcpu-ubuntu-2204
+ timeout-minutes: 60
env:
REACT_APP_SERVER_BASE_URL: http://localhost:3000
NX_REJECT_UNKNOWN_LOCAL_CACHE: 0
diff --git a/Makefile b/Makefile
index 764dbd1531d4..af42f19d9fb0 100644
--- a/Makefile
+++ b/Makefile
@@ -1,26 +1,12 @@
-docker-dev-build:
- make -C packages/twenty-docker dev-build
-
-docker-dev-up:
- make -C packages/twenty-docker dev-up
-
-docker-dev-start:
- make -C packages/twenty-docker dev-start
-
-docker-dev-stop:
- make -C packages/twenty-docker dev-stop
-
-docker-dev-sh:
- make -C packages/twenty-docker dev-sh
-
postgres-on-docker:
- make -C packages/twenty-postgres provision-on-docker
-
-postgres-on-macos-arm:
- make -C packages/twenty-postgres provision-on-macos-arm
-
-postgres-on-macos-intel:
- make -C packages/twenty-postgres provision-on-macos-intel
-
-postgres-on-linux:
- make -C packages/twenty-postgres provision-on-linux
+ docker run \
+ --name twenty_postgres \
+ -e POSTGRES_USER=postgres \
+ -e POSTGRES_PASSWORD=postgres \
+ -e POSTGRES_DB=default \
+ -v twenty_db_data:/var/lib/postgresql/data \
+ -p 5432:5432 \
+ twentycrm/twenty-postgres:latest
+
+redis-on-docker:
+ docker run -d --name twenty_redis -p 6379:6379 redis/redis-stack-server:latest
\ 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
index d4f5a4d0ab99..9f1f55ae767e 100644
--- a/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md
+++ b/oss-gg/twenty-design-challenges/1-design-promotional-poster-20-share.md
@@ -23,4 +23,9 @@ Your turn 馃憞
禄 12-October-2024 by [Ionfinisher](https://oss.gg/Ionfinisher) poster Link: [poster](https://x.com/ion_finisher/status/1845168965963628802)
禄 14-October-2024 by [AliYar-Khan](https://oss.gg/AliYar-Khan) poster Link: [poster](https://x.com/Mr_Programmer14/status/1845888855183884352)
+
+禄 16-October-2024 by [Harsh BHat](https://oss.gg/harshsbhat) poster Link: [poster](https://x.com/HarshBhatX/status/1846233330435477531)
+
+禄 17-October-2024 by [Atharva Deshmukh](https://oss.gg/Atharva-3000) poster Link: [poster](https://x.com/0x_atharva/status/1846915861191577697)
+
---
diff --git a/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md b/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md
index ceee0fa8e4da..d67c49b64154 100644
--- a/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md
+++ b/oss-gg/twenty-design-challenges/2-design-new-logo-twenty.md
@@ -24,5 +24,9 @@ Your turn 馃憞
禄 13-October-2024 by [Ionfinisher](https://oss.gg/Ionfinisher) Logo Link: [logo](https://drive.google.com/file/d/1l9vE8CIjW9KfdioI5WKzxrdmvO8LR4j7/view?usp=drive_link) 禄 tweet Link: [tweet](https://x.com/ion_finisher/status/1845466470429442163)
+禄 16-October-2024 by [harshsbhat](https://oss.gg/harshsbhat) Logo Link: [logo](https://drive.google.com/file/d/1jmqwNvlSyWSY1-pCG63TAtDvCoVa8xg-/view?usp=sharing) 禄 tweet Link: [tweet](https://x.com/HarshBhatX/status/1846234658712772977)
+
+禄 17-October-2024 by [shlok-py](https://oss.gg/shlok-py) Logo Link: [logo](https://drive.google.com/file/d/1BakHRLJul6DcNbLyeOXgJO9Ap4DpUxO9/view?usp=sharing) 禄 tweet Link: [tweet](https://x.com/koirala_shlok/status/1846910669658247201)
+
---
diff --git a/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md b/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md
index 168e311b9a32..a0e1421bfd99 100644
--- a/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md
+++ b/oss-gg/twenty-side-quest/1-quote-tweet-20-oss-gg-launch.md
@@ -43,3 +43,6 @@ Your turn 馃憞
禄 13-October-2024 by Yash Parmar
禄 Link to Tweet: https://x.com/yashp3020/status/1845720834716959009
+
+禄 16-October-2024 by Harsh Bhat
+禄 Link to Tweet: https://x.com/HarshBhatX/status/1846252536241508392
diff --git a/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md b/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md
index 508210ae5a6e..07d2e067c708 100644
--- a/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md
+++ b/oss-gg/twenty-side-quest/2-tweet-about-fav-twenty-feature.md
@@ -25,4 +25,7 @@ Your turn 馃憞
禄 13-October-2024 by Ali Yar Khan
禄 Link to Tweet: https://x.com/Mr_Programmer14/status/1845530448245711197
+
+禄 16-October-2024 by Harsh Bhat
+禄 Link to Tweet: https://x.com/HarshBhatX/status/1846075312691413066
---
diff --git a/oss-gg/twenty-side-quest/4-meme-magic.md b/oss-gg/twenty-side-quest/4-meme-magic.md
index b5cb3263d218..041c73ef081e 100644
--- a/oss-gg/twenty-side-quest/4-meme-magic.md
+++ b/oss-gg/twenty-side-quest/4-meme-magic.md
@@ -31,4 +31,7 @@ Your turn 馃憞
禄 14-October-2024 by Yash Parmar
禄 Link to Tweet: [https://x.com/yashp3020/status/1845108226527994222](https://x.com/yashp3020/status/1845720142702842093)
+
+禄 16-October-2024 by Harsh Bhat
+禄 Link to Tweet: https://x.com/HarshBhatX/status/1844698253104709899
---
diff --git a/oss-gg/twenty-side-quest/5-gif-magic.md b/oss-gg/twenty-side-quest/5-gif-magic.md
index 20467fef4784..320ffa9015db 100644
--- a/oss-gg/twenty-side-quest/5-gif-magic.md
+++ b/oss-gg/twenty-side-quest/5-gif-magic.md
@@ -29,5 +29,9 @@ Your turn 馃憞
禄 13-October-2024 by Nabhag Motivaras
禄 Link to gif: https://giphy.com/gifs/twenty-twentycrm-opensourcecrm-wCcsmnJuzzzGrfuf9B
+禄 15-October-2024 by Ali Yar Khan
+禄 Link to gif: https://giphy.com/gifs/Q3f7T107wSsMJlT7aj
+禄 16-October-2024 by Harsh Bhat
+禄 Link to gif: https://giphy.com/gifs/oss-twentycrm-mgoYSDrjIalUL7XJzm
---
diff --git a/package.json b/package.json
index a4dc90df92ac..f86d4c1b9d08 100644
--- a/package.json
+++ b/package.json
@@ -347,7 +347,7 @@
"version": "0.2.1",
"nx": {},
"scripts": {
- "start": "npx nx run-many -t start -p twenty-server twenty-front"
+ "start": "npx nx run-many -t start worker -p twenty-server twenty-front"
},
"workspaces": {
"packages": [
diff --git a/packages/twenty-docker/.env.example b/packages/twenty-docker/.env.example
index 59d8d03f93a7..c1a7a9d3bae8 100644
--- a/packages/twenty-docker/.env.example
+++ b/packages/twenty-docker/.env.example
@@ -3,10 +3,9 @@ TAG=latest
# POSTGRES_ADMIN_PASSWORD=replace_me_with_a_strong_password
PG_DATABASE_HOST=db:5432
+REDIS_URL=redis://redis:6379
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
diff --git a/packages/twenty-docker/docker-compose.yml b/packages/twenty-docker/docker-compose.yml
index b2efc1a168e4..8800f4f3f3b9 100644
--- a/packages/twenty-docker/docker-compose.yml
+++ b/packages/twenty-docker/docker-compose.yml
@@ -25,8 +25,7 @@ services:
PG_DATABASE_URL: postgres://twenty:twenty@${PG_DATABASE_HOST}/default
SERVER_URL: ${SERVER_URL}
FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL}
- REDIS_PORT: ${REDIS_PORT:-6379}
- REDIS_HOST: ${REDIS_HOST:-redis}
+ REDIS_URL: ${REDIS_URL:-redis://localhost:6379}
ENABLE_DB_MIGRATIONS: "true"
@@ -59,8 +58,7 @@ services:
PG_DATABASE_URL: postgres://twenty:twenty@${PG_DATABASE_HOST}/default
SERVER_URL: ${SERVER_URL}
FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL}
- REDIS_PORT: ${REDIS_PORT:-6379}
- REDIS_HOST: ${REDIS_HOST:-redis}
+ REDIS_URL: ${REDIS_URL:-redis://localhost:6379}
ENABLE_DB_MIGRATIONS: "false" # it already runs on the server
diff --git a/packages/twenty-docker/k8s/manifests/deployment-server.yaml b/packages/twenty-docker/k8s/manifests/deployment-server.yaml
index b1229d649bbb..99e5c60132ed 100644
--- a/packages/twenty-docker/k8s/manifests/deployment-server.yaml
+++ b/packages/twenty-docker/k8s/manifests/deployment-server.yaml
@@ -41,10 +41,8 @@ spec:
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: "REDIS_URL"
+ value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379"
- name: ENABLE_DB_MIGRATIONS
value: "true"
- name: SIGN_IN_PREFILLED
diff --git a/packages/twenty-docker/k8s/manifests/deployment-worker.yaml b/packages/twenty-docker/k8s/manifests/deployment-worker.yaml
index b3a7e07a19aa..92d0322e5930 100644
--- a/packages/twenty-docker/k8s/manifests/deployment-worker.yaml
+++ b/packages/twenty-docker/k8s/manifests/deployment-worker.yaml
@@ -40,10 +40,8 @@ spec:
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: "REDIS_URL"
+ value: "redis://twentycrm-redis.twentycrm.svc.cluster.local:6379"
- name: ACCESS_TOKEN_SECRET
valueFrom:
secretKeyRef:
diff --git a/packages/twenty-docker/k8s/terraform/deployment-server.tf b/packages/twenty-docker/k8s/terraform/deployment-server.tf
index 1868b17624da..0f643f5c6d80 100644
--- a/packages/twenty-docker/k8s/terraform/deployment-server.tf
+++ b/packages/twenty-docker/k8s/terraform/deployment-server.tf
@@ -61,12 +61,8 @@ resource "kubernetes_deployment" "twentycrm_server" {
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
+ name = "REDIS_URL"
+ value = "redis://${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local:6379"
}
env {
name = "ENABLE_DB_MIGRATIONS"
diff --git a/packages/twenty-docker/k8s/terraform/deployment-worker.tf b/packages/twenty-docker/k8s/terraform/deployment-worker.tf
index 78e5ea6dcc1d..163f02c4977e 100644
--- a/packages/twenty-docker/k8s/terraform/deployment-worker.tf
+++ b/packages/twenty-docker/k8s/terraform/deployment-worker.tf
@@ -59,13 +59,8 @@ resource "kubernetes_deployment" "twentycrm_worker" {
}
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
+ name = "REDIS_URL"
+ value = "redis://${kubernetes_service.twentycrm_redis.metadata.0.name}.${kubernetes_namespace.twentycrm.metadata.0.name}.svc.cluster.local:6379"
}
env {
diff --git a/packages/twenty-docker/twenty-postgres-spilo/Dockerfile b/packages/twenty-docker/twenty-postgres-spilo/Dockerfile
index 21e107c477bb..a87a8a97ec12 100644
--- a/packages/twenty-docker/twenty-postgres-spilo/Dockerfile
+++ b/packages/twenty-docker/twenty-postgres-spilo/Dockerfile
@@ -1,6 +1,5 @@
ARG POSTGRES_VERSION=15
ARG SPILO_VERSION=3.2-p1
-ARG PG_GRAPHQL_VERSION=1.5.6
ARG WRAPPERS_VERSION=0.2.0
# Build the mysql_fdw extension
@@ -38,10 +37,9 @@ WORKDIR /build/openssl
RUN ./config && make && make install
-# Extend the Spilo image with the pg_graphql and mysql_fdw extensions
+# Extend the Spilo image with the mysql_fdw extensions
FROM ghcr.io/zalando/spilo-${POSTGRES_VERSION}:${SPILO_VERSION}
ARG POSTGRES_VERSION
-ARG PG_GRAPHQL_VERSION
ARG WRAPPERS_VERSION
ARG TARGETARCH
@@ -63,14 +61,6 @@ RUN curl -L "https://github.com/supabase/wrappers/releases/download/v${WRAPPERS_
COPY --from=build-libssl /usr/local/lib/libssl* /usr/local/lib/libcrypto* /usr/lib/
COPY --from=build-libssl /usr/local/lib/engines-1.1 /usr/lib/engines-1.1
-# Copy pg_graphql
-COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${POSTGRES_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql--${PG_GRAPHQL_VERSION}.sql \
- /usr/share/postgresql/${POSTGRES_VERSION}/extension
-COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${POSTGRES_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql.control \
- /usr/share/postgresql/${POSTGRES_VERSION}/extension
-COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${POSTGRES_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql.so \
- /usr/lib/postgresql/${POSTGRES_VERSION}/lib/pg_graphql.so
-
# Copy mysql_fdw
COPY --from=build-mysql_fdw /mysql_fdw/mysql_fdw.so \
/usr/lib/postgresql/${POSTGRES_VERSION}/lib/mysql_fdw.so
diff --git a/packages/twenty-docker/twenty-postgres/Dockerfile b/packages/twenty-docker/twenty-postgres/Dockerfile
index 5647a6cd35ac..9c9b96398e66 100644
--- a/packages/twenty-docker/twenty-postgres/Dockerfile
+++ b/packages/twenty-docker/twenty-postgres/Dockerfile
@@ -3,7 +3,6 @@ ARG IMAGE_TAG='15.5.0-debian-11-r15'
FROM bitnami/postgresql:${IMAGE_TAG}
ARG PG_MAIN_VERSION=15
-ARG PG_GRAPHQL_VERSION=1.5.6
ARG WRAPPERS_VERSION=0.2.0
ARG TARGETARCH
@@ -26,14 +25,6 @@ RUN set -eux; \
RUN apt update && apt install build-essential git curl default-libmysqlclient-dev -y
-# Install precompiled pg_graphql extensions
-COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${PG_MAIN_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql--${PG_GRAPHQL_VERSION}.sql \
- /opt/bitnami/postgresql/share/extension/
-COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${PG_MAIN_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql.control \
- /opt/bitnami/postgresql/share/extension/
-COPY ./packages/twenty-postgres/linux/${TARGETARCH}/${PG_MAIN_VERSION}/pg_graphql/${PG_GRAPHQL_VERSION}/pg_graphql.so \
- /opt/bitnami/postgresql/lib/
-
# Install precompiled supabase wrappers extensions
RUN curl -L "https://github.com/supabase/wrappers/releases/download/v${WRAPPERS_VERSION}/wrappers-v${WRAPPERS_VERSION}-pg${PG_MAIN_VERSION}-${TARGETARCH}-linux-gnu.deb" -o wrappers.deb
RUN dpkg --install wrappers.deb
diff --git a/packages/twenty-docker/twenty-website/Dockerfile b/packages/twenty-docker/twenty-website/Dockerfile
new file mode 100644
index 000000000000..e3b7420ff76a
--- /dev/null
+++ b/packages/twenty-docker/twenty-website/Dockerfile
@@ -0,0 +1,29 @@
+FROM node:18.17.1-alpine as twenty-website-build
+
+
+WORKDIR /app
+
+COPY ./package.json .
+COPY ./yarn.lock .
+COPY ./.yarnrc.yml .
+COPY ./.yarn/releases /app/.yarn/releases
+COPY ./tools/eslint-rules /app/tools/eslint-rules
+COPY ./packages/twenty-website/package.json /app/packages/twenty-website/package.json
+
+RUN yarn
+
+COPY ./packages/twenty-website /app/packages/twenty-website
+RUN npx nx build twenty-website
+
+FROM node:18.17.1-alpine as twenty-website
+
+WORKDIR /app/packages/twenty-website
+
+COPY --from=twenty-website-build /app /app
+
+WORKDIR /app/packages/twenty-website
+
+LABEL org.opencontainers.image.source=https://github.com/twentyhq/twenty
+LABEL org.opencontainers.image.description="This image provides a consistent and reproducible environment for the website."
+
+CMD ["/bin/sh", "-c", "npx nx start"]
\ 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 5950c81d5cc1..7f053fc6b10f 100644
--- a/packages/twenty-front/src/generated/graphql.tsx
+++ b/packages/twenty-front/src/generated/graphql.tsx
@@ -141,6 +141,7 @@ export enum CaptchaDriverType {
export type ClientConfig = {
__typename?: 'ClientConfig';
+ analyticsEnabled: Scalars['Boolean'];
api: ApiConfig;
authProviders: AuthProviders;
billing: Billing;
@@ -1599,7 +1600,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
-export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } };
+export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } };
export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>;
@@ -2765,6 +2766,7 @@ export const GetClientConfigDocument = gql`
signInPrefilled
signUpDisabled
debugMode
+ analyticsEnabled
support {
supportDriver
supportFrontChatId
diff --git a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx
index feeba5aabc61..4b61fa58eadb 100644
--- a/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx
+++ b/packages/twenty-front/src/modules/action-menu/actions/record-actions/components/SingleRecordActionMenuEntriesSetter.tsx
@@ -4,9 +4,9 @@ import { ManageFavoritesActionEffect } from '@/action-menu/actions/record-action
export const SingleRecordActionMenuEntriesSetter = () => {
const actionEffects = [
+ ManageFavoritesActionEffect,
ExportRecordsActionEffect,
DeleteRecordsActionEffect,
- ManageFavoritesActionEffect,
];
return (
<>
diff --git a/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx b/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx
index 15d371f9f44a..202b58b963e5 100644
--- a/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx
+++ b/packages/twenty-front/src/modules/app/effect-components/GotoHotkeysEffect.tsx
@@ -11,6 +11,7 @@ export const GotoHotkeys = () => {
return nonSystemActiveObjectMetadataItems.map((objectMetadataItem) => (
diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts
index 7a7de0807f1b..ae13d831fb7a 100644
--- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts
+++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts
@@ -32,6 +32,8 @@ import {
import { isDefined } from '~/utils/isDefined';
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
+import { DateFormat } from '@/localization/constants/DateFormat';
+import { TimeFormat } from '@/localization/constants/TimeFormat';
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
@@ -143,12 +145,12 @@ export const useAuth = () => {
? getDateFormatFromWorkspaceDateFormat(
user.workspaceMember.dateFormat,
)
- : detectDateFormat(),
+ : DateFormat[detectDateFormat()],
timeFormat: isDefined(user.workspaceMember.timeFormat)
? getTimeFormatFromWorkspaceTimeFormat(
user.workspaceMember.timeFormat,
)
- : detectTimeFormat(),
+ : TimeFormat[detectTimeFormat()],
});
}
diff --git a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx
index 9eccbeb98e10..ed06d3f0ee69 100644
--- a/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx
+++ b/packages/twenty-front/src/modules/client-config/components/ClientConfigProviderEffect.tsx
@@ -1,23 +1,24 @@
-import { useEffect } from 'react';
-import { useRecoilState, useSetRecoilState } from 'recoil';
-
import { apiConfigState } from '@/client-config/states/apiConfigState';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { billingState } from '@/client-config/states/billingState';
import { captchaProviderState } from '@/client-config/states/captchaProviderState';
import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState';
+import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState';
import { isClientConfigLoadedState } from '@/client-config/states/isClientConfigLoadedState';
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
import { isSignInPrefilledState } from '@/client-config/states/isSignInPrefilledState';
import { isSignUpDisabledState } from '@/client-config/states/isSignUpDisabledState';
import { sentryConfigState } from '@/client-config/states/sentryConfigState';
import { supportChatState } from '@/client-config/states/supportChatState';
+import { useEffect } from 'react';
+import { useRecoilState, useSetRecoilState } from 'recoil';
import { useGetClientConfigQuery } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
export const ClientConfigProviderEffect = () => {
const setAuthProviders = useSetRecoilState(authProvidersState);
const setIsDebugMode = useSetRecoilState(isDebugModeState);
+ const setIsAnalyticsEnabled = useSetRecoilState(isAnalyticsEnabledState);
const setIsSignInPrefilled = useSetRecoilState(isSignInPrefilledState);
const setIsSignUpDisabled = useSetRecoilState(isSignUpDisabledState);
@@ -50,6 +51,7 @@ export const ClientConfigProviderEffect = () => {
magicLink: false,
});
setIsDebugMode(data?.clientConfig.debugMode);
+ setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled);
setIsSignInPrefilled(data?.clientConfig.signInPrefilled);
setIsSignUpDisabled(data?.clientConfig.signUpDisabled);
@@ -84,6 +86,7 @@ export const ClientConfigProviderEffect = () => {
setCaptchaProvider,
setChromeExtensionId,
setApiConfig,
+ setIsAnalyticsEnabled,
]);
return <>>;
diff --git a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts
index e702acefa4f1..9a060b0d7b2b 100644
--- a/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts
+++ b/packages/twenty-front/src/modules/client-config/graphql/queries/getClientConfig.ts
@@ -16,6 +16,7 @@ export const GET_CLIENT_CONFIG = gql`
signInPrefilled
signUpDisabled
debugMode
+ analyticsEnabled
support {
supportDriver
supportFrontChatId
diff --git a/packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts b/packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts
new file mode 100644
index 000000000000..50c0f5c89c25
--- /dev/null
+++ b/packages/twenty-front/src/modules/client-config/states/isAnalyticsEnabledState.ts
@@ -0,0 +1,6 @@
+import { createState } from 'twenty-ui';
+
+export const isAnalyticsEnabledState = createState({
+ key: 'isAnalyticsEnabled',
+ defaultValue: false,
+});
diff --git a/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx b/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx
index cf106211405b..b975799fd499 100644
--- a/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx
+++ b/packages/twenty-front/src/modules/favorites/components/WorkspaceFavorites.tsx
@@ -2,17 +2,13 @@ import { useFilteredObjectMetadataItemsForWorkspaceFavorites } from '@/navigatio
import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems';
import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader';
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
-import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
-import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
-import { View } from '@/views/types/View';
export const WorkspaceFavorites = () => {
- const { records: views } = usePrefetchedData(PrefetchKey.AllViews);
-
const { activeObjectMetadataItems: objectMetadataItemsToDisplay } =
useFilteredObjectMetadataItemsForWorkspaceFavorites();
const loading = useIsPrefetchLoading();
+
if (loading) {
return ;
}
@@ -21,7 +17,6 @@ export const WorkspaceFavorites = () => {
);
diff --git a/packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts b/packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts
new file mode 100644
index 000000000000..a1c7f2af3b72
--- /dev/null
+++ b/packages/twenty-front/src/modules/localization/constants/DateFormatWithoutYear.ts
@@ -0,0 +1,11 @@
+import { DateFormat } from '@/localization/constants/DateFormat';
+
+type DateFormatWithoutYear = {
+ [K in keyof typeof DateFormat]: string;
+};
+export const DATE_FORMAT_WITHOUT_YEAR: DateFormatWithoutYear = {
+ SYSTEM: 'SYSTEM',
+ MONTH_FIRST: 'MMM d',
+ DAY_FIRST: 'd MMM',
+ YEAR_FIRST: 'MMM d',
+};
diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts b/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts
index 2b641f302a63..b267622bf0cc 100644
--- a/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts
+++ b/packages/twenty-front/src/modules/localization/utils/__tests__/detectDateFormat.test.ts
@@ -1,8 +1,7 @@
-import { DateFormat } from '@/localization/constants/DateFormat';
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
describe('detectDateFormat', () => {
- it('should return DateFormat.MONTH_FIRST if the detected format starts with month', () => {
+ it('should return MONTH_FIRST if the detected format starts with month', () => {
// Mock the Intl.DateTimeFormat to return a specific format
const mockDateTimeFormat = jest.fn().mockReturnValue({
formatToParts: () => [
@@ -16,10 +15,10 @@ describe('detectDateFormat', () => {
const result = detectDateFormat();
- expect(result).toBe(DateFormat.MONTH_FIRST);
+ expect(result).toBe('MONTH_FIRST');
});
- it('should return DateFormat.DAY_FIRST if the detected format starts with day', () => {
+ it('should return DAY_FIRST if the detected format starts with day', () => {
// Mock the Intl.DateTimeFormat to return a specific format
const mockDateTimeFormat = jest.fn().mockReturnValue({
formatToParts: () => [
@@ -32,10 +31,10 @@ describe('detectDateFormat', () => {
const result = detectDateFormat();
- expect(result).toBe(DateFormat.DAY_FIRST);
+ expect(result).toBe('DAY_FIRST');
});
- it('should return DateFormat.YEAR_FIRST if the detected format starts with year', () => {
+ it('should return YEAR_FIRST if the detected format starts with year', () => {
// Mock the Intl.DateTimeFormat to return a specific format
const mockDateTimeFormat = jest.fn().mockReturnValue({
formatToParts: () => [
@@ -48,10 +47,10 @@ describe('detectDateFormat', () => {
const result = detectDateFormat();
- expect(result).toBe(DateFormat.YEAR_FIRST);
+ expect(result).toBe('YEAR_FIRST');
});
- it('should return DateFormat.MONTH_FIRST by default if the detected format does not match any specific order', () => {
+ it('should return MONTH_FIRST by default if the detected format does not match any specific order', () => {
// Mock the Intl.DateTimeFormat to return a specific format
const mockDateTimeFormat = jest.fn().mockReturnValue({
formatToParts: () => [
@@ -64,6 +63,6 @@ describe('detectDateFormat', () => {
const result = detectDateFormat();
- expect(result).toBe(DateFormat.MONTH_FIRST);
+ expect(result).toBe('MONTH_FIRST');
});
});
diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts b/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts
index 6433495789ee..9445068a5f7f 100644
--- a/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts
+++ b/packages/twenty-front/src/modules/localization/utils/__tests__/detectTimeFormat.test.ts
@@ -1,8 +1,7 @@
-import { TimeFormat } from '@/localization/constants/TimeFormat';
import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
describe('detectTimeFormat', () => {
- it('should return TimeFormat.HOUR_12 if the hour format is 12-hour', () => {
+ it('should return HOUR_12 if the hour format is 12-hour', () => {
// Mock the resolvedOptions method to return hour12 as true
const mockResolvedOptions = jest.fn(() => ({ hour12: true }));
Intl.DateTimeFormat = jest.fn().mockImplementation(() => ({
@@ -11,11 +10,11 @@ describe('detectTimeFormat', () => {
const result = detectTimeFormat();
- expect(result).toBe(TimeFormat.HOUR_12);
+ expect(result).toBe('HOUR_12');
expect(mockResolvedOptions).toHaveBeenCalled();
});
- it('should return TimeFormat.HOUR_24 if the hour format is 24-hour', () => {
+ it('should return HOUR_24 if the hour format is 24-hour', () => {
// Mock the resolvedOptions method to return hour12 as false
const mockResolvedOptions = jest.fn(() => ({ hour12: false }));
Intl.DateTimeFormat = jest.fn().mockImplementation(() => ({
@@ -24,7 +23,7 @@ describe('detectTimeFormat', () => {
const result = detectTimeFormat();
- expect(result).toBe(TimeFormat.HOUR_24);
+ expect(result).toBe('HOUR_24');
expect(mockResolvedOptions).toHaveBeenCalled();
});
});
diff --git a/packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js b/packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js
new file mode 100644
index 000000000000..4caee3aedf0d
--- /dev/null
+++ b/packages/twenty-front/src/modules/localization/utils/__tests__/formatDateISOStringToDateTimeSimplified.test.js
@@ -0,0 +1,90 @@
+import { detectDateFormat } from '@/localization/utils/detectDateFormat';
+import { formatDateISOStringToDateTimeSimplified } from '@/localization/utils/formatDateISOStringToDateTimeSimplified';
+import { formatInTimeZone } from 'date-fns-tz';
+// Mock the imported modules
+jest.mock('@/localization/utils/detectDateFormat');
+jest.mock('date-fns-tz');
+
+describe('formatDateISOStringToDateTimeSimplified', () => {
+ const mockDate = new Date('2023-08-15T10:30:00Z');
+ const mockTimeZone = 'America/New_York';
+ const mockTimeFormat = 'HH:mm';
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('should format the date correctly when DATE_FORMAT is MONTH_FIRST', () => {
+ detectDateFormat.mockReturnValue('MONTH_FIRST');
+ formatInTimeZone.mockReturnValue('Oct 15 路 06:30');
+
+ const result = formatDateISOStringToDateTimeSimplified(
+ mockDate,
+ mockTimeZone,
+ mockTimeFormat,
+ );
+
+ expect(detectDateFormat).toHaveBeenCalled();
+ expect(formatInTimeZone).toHaveBeenCalledWith(
+ mockDate,
+ mockTimeZone,
+ 'MMM d 路 HH:mm',
+ );
+ expect(result).toBe('Oct 15 路 06:30');
+ });
+
+ it('should format the date correctly when DATE_FORMAT is DAY_FIRST', () => {
+ detectDateFormat.mockReturnValue('DAY_FIRST');
+ formatInTimeZone.mockReturnValue('15 Oct 路 06:30');
+
+ const result = formatDateISOStringToDateTimeSimplified(
+ mockDate,
+ mockTimeZone,
+ mockTimeFormat,
+ );
+
+ expect(detectDateFormat).toHaveBeenCalled();
+ expect(formatInTimeZone).toHaveBeenCalledWith(
+ mockDate,
+ mockTimeZone,
+ 'd MMM 路 HH:mm',
+ );
+ expect(result).toBe('15 Oct 路 06:30');
+ });
+
+ it('should use the provided time format', () => {
+ detectDateFormat.mockReturnValue('MONTH_FIRST');
+ formatInTimeZone.mockReturnValue('Oct 15 路 6:30 AM');
+
+ const result = formatDateISOStringToDateTimeSimplified(
+ mockDate,
+ mockTimeZone,
+ 'h:mm aa',
+ );
+
+ expect(formatInTimeZone).toHaveBeenCalledWith(
+ mockDate,
+ mockTimeZone,
+ 'MMM d 路 h:mm aa',
+ );
+ expect(result).toBe('Oct 15 路 6:30 AM');
+ });
+
+ it('should handle different time zones', () => {
+ detectDateFormat.mockReturnValue('MONTH_FIRST');
+ formatInTimeZone.mockReturnValue('Oct 16 路 02:30');
+
+ const result = formatDateISOStringToDateTimeSimplified(
+ mockDate,
+ 'Asia/Tokyo',
+ mockTimeFormat,
+ );
+
+ expect(formatInTimeZone).toHaveBeenCalledWith(
+ mockDate,
+ 'Asia/Tokyo',
+ 'MMM d 路 HH:mm',
+ );
+ expect(result).toBe('Oct 16 路 02:30');
+ });
+});
diff --git a/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts b/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts
index b503ef826e60..e38b018df445 100644
--- a/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts
+++ b/packages/twenty-front/src/modules/localization/utils/detectDateFormat.ts
@@ -1,6 +1,6 @@
import { DateFormat } from '@/localization/constants/DateFormat';
-export const detectDateFormat = (): DateFormat => {
+export const detectDateFormat = (): keyof typeof DateFormat => {
const date = new Date();
const formatter = new Intl.DateTimeFormat(navigator.language);
const parts = formatter.formatToParts(date);
@@ -9,9 +9,9 @@ export const detectDateFormat = (): DateFormat => {
.filter((part) => ['year', 'month', 'day'].includes(part.type))
.map((part) => part.type);
- if (partOrder[0] === 'month') return DateFormat.MONTH_FIRST;
- if (partOrder[0] === 'day') return DateFormat.DAY_FIRST;
- if (partOrder[0] === 'year') return DateFormat.YEAR_FIRST;
+ if (partOrder[0] === 'month') return 'MONTH_FIRST';
+ if (partOrder[0] === 'day') return 'DAY_FIRST';
+ if (partOrder[0] === 'year') return 'YEAR_FIRST';
- return DateFormat.MONTH_FIRST;
+ return 'MONTH_FIRST';
};
diff --git a/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts b/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts
index 01bad17167a5..d6d914d83637 100644
--- a/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts
+++ b/packages/twenty-front/src/modules/localization/utils/detectTimeFormat.ts
@@ -1,14 +1,14 @@
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { isDefined } from '~/utils/isDefined';
-export const detectTimeFormat = () => {
+export const detectTimeFormat = (): keyof typeof TimeFormat => {
const isHour12 = Intl.DateTimeFormat(navigator.language, {
hour: 'numeric',
}).resolvedOptions().hour12;
if (isDefined(isHour12) && isHour12) {
- return TimeFormat.HOUR_12;
+ return 'HOUR_12';
}
- return TimeFormat.HOUR_24;
+ return 'HOUR_24';
};
diff --git a/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts b/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts
new file mode 100644
index 000000000000..c96d9f2f885d
--- /dev/null
+++ b/packages/twenty-front/src/modules/localization/utils/formatDateISOStringToDateTimeSimplified.ts
@@ -0,0 +1,18 @@
+import { DATE_FORMAT_WITHOUT_YEAR } from '@/localization/constants/DateFormatWithoutYear';
+import { TimeFormat } from '@/localization/constants/TimeFormat';
+import { detectDateFormat } from '@/localization/utils/detectDateFormat';
+import { formatInTimeZone } from 'date-fns-tz';
+
+export const formatDateISOStringToDateTimeSimplified = (
+ date: Date,
+ timeZone: string,
+ timeFormat: TimeFormat,
+) => {
+ const simplifiedDateFormat = DATE_FORMAT_WITHOUT_YEAR[detectDateFormat()];
+
+ return formatInTimeZone(
+ date,
+ timeZone,
+ `${simplifiedDateFormat} 路 ${timeFormat}`,
+ );
+};
diff --git a/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts b/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts
index f32bdbb93355..09293fbb8ec8 100644
--- a/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts
+++ b/packages/twenty-front/src/modules/localization/utils/getDateFormatFromWorkspaceDateFormat.ts
@@ -7,7 +7,7 @@ export const getDateFormatFromWorkspaceDateFormat = (
) => {
switch (workspaceDateFormat) {
case WorkspaceMemberDateFormatEnum.System:
- return detectDateFormat();
+ return DateFormat[detectDateFormat()];
case WorkspaceMemberDateFormatEnum.MonthFirst:
return DateFormat.MONTH_FIRST;
case WorkspaceMemberDateFormatEnum.DayFirst:
diff --git a/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts b/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts
index f6aebb43779b..7519d0cb4068 100644
--- a/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts
+++ b/packages/twenty-front/src/modules/localization/utils/getTimeFormatFromWorkspaceTimeFormat.ts
@@ -7,7 +7,7 @@ export const getTimeFormatFromWorkspaceTimeFormat = (
) => {
switch (workspaceTimeFormat) {
case WorkspaceMemberTimeFormatEnum.System:
- return detectTimeFormat();
+ return TimeFormat[detectTimeFormat()];
case WorkspaceMemberTimeFormatEnum.Hour_24:
return TimeFormat.HOUR_24;
case WorkspaceMemberTimeFormatEnum.Hour_12:
diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerItemForObjectMetadataItem.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerItemForObjectMetadataItem.tsx
new file mode 100644
index 000000000000..8c7f1e3ceda2
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerItemForObjectMetadataItem.tsx
@@ -0,0 +1,84 @@
+import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
+import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
+import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
+import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
+import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
+import { NavigationDrawerItemsCollapsedContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer';
+import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
+import { getNavigationSubItemState } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemState';
+import { View } from '@/views/types/View';
+import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews';
+import { useLocation } from 'react-router-dom';
+import { useIcons } from 'twenty-ui';
+
+export type NavigationDrawerItemForObjectMetadataItemProps = {
+ objectMetadataItem: ObjectMetadataItem;
+};
+
+export const NavigationDrawerItemForObjectMetadataItem = ({
+ objectMetadataItem,
+}: NavigationDrawerItemForObjectMetadataItemProps) => {
+ const { records: views } = usePrefetchedData(PrefetchKey.AllViews);
+
+ const objectMetadataViews = getObjectMetadataItemViews(
+ objectMetadataItem.id,
+ views,
+ );
+
+ const { getIcon } = useIcons();
+ const currentPath = useLocation().pathname;
+ const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView();
+
+ const lastVisitedViewId = getLastVisitedViewIdFromObjectMetadataItemId(
+ objectMetadataItem.id,
+ );
+
+ const viewId = lastVisitedViewId ?? objectMetadataViews[0]?.id;
+
+ const navigationPath = `/objects/${objectMetadataItem.namePlural}${
+ viewId ? `?view=${viewId}` : ''
+ }`;
+
+ const isActive = currentPath === `/objects/${objectMetadataItem.namePlural}`;
+ const shouldSubItemsBeDisplayed = isActive && objectMetadataViews.length > 1;
+
+ const sortedObjectMetadataViews = [...objectMetadataViews].sort(
+ (viewA, viewB) =>
+ viewA.key === 'INDEX' ? -1 : viewA.position - viewB.position,
+ );
+
+ const selectedSubItemIndex = sortedObjectMetadataViews.findIndex(
+ (view) => viewId === view.id,
+ );
+
+ const subItemArrayLength = sortedObjectMetadataViews.length;
+
+ return (
+
+
+ {shouldSubItemsBeDisplayed &&
+ sortedObjectMetadataViews.map((view, index) => (
+
+ ))}
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx
index fb17b643078f..b17ad4c310e2 100644
--- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx
+++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerOpenedSection.tsx
@@ -5,9 +5,6 @@ import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata
import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
-import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
-import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
-import { View } from '@/views/types/View';
export const NavigationDrawerOpenedSection = () => {
const { activeObjectMetadataItems } = useFilteredObjectMetadataItems();
@@ -15,7 +12,6 @@ export const NavigationDrawerOpenedSection = () => {
(item) => !item.isRemote,
);
- const { records: views } = usePrefetchedData(PrefetchKey.AllViews);
const loading = useIsPrefetchLoading();
const currentObjectNamePlural = useParams().objectNamePlural;
@@ -49,7 +45,6 @@ export const NavigationDrawerOpenedSection = () => {
)
diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx
index 5e666e0cf542..f90a160b57c4 100644
--- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx
+++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItems.tsx
@@ -1,18 +1,13 @@
-import { useLastVisitedView } from '@/navigation/hooks/useLastVisitedView';
+import { NavigationDrawerItemForObjectMetadataItem } from '@/object-metadata/components/NavigationDrawerItemForObjectMetadataItem';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
-import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem';
-import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
-import { NavigationDrawerSubItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSubItem';
+import { NavigationDrawerSection } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSection';
+import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
import { useNavigationSection } from '@/ui/navigation/navigation-drawer/hooks/useNavigationSection';
-import { getNavigationSubItemState } from '@/ui/navigation/navigation-drawer/utils/getNavigationSubItemState';
-import { View } from '@/views/types/View';
-import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews';
-import { useLocation } from 'react-router-dom';
+import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
+import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
-import { useIcons } from 'twenty-ui';
-import { NavigationDrawerSectionTitle } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerSectionTitle';
-import { NavigationDrawerItemsCollapsedContainer } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItemsCollapsedContainer';
+
const ORDERED_STANDARD_OBJECTS = [
'person',
'company',
@@ -21,111 +16,59 @@ const ORDERED_STANDARD_OBJECTS = [
'note',
];
+const StyledObjectsMetaDataItemsWrapper = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.betweenSiblingsGap};
+ width: 100%;
+ margin-bottom: ${({ theme }) => theme.spacing(3)};
+ flex: 1;
+ overflow-y: auto;
+`;
+
export const NavigationDrawerSectionForObjectMetadataItems = ({
sectionTitle,
isRemote,
- views,
objectMetadataItems,
}: {
sectionTitle: string;
isRemote: boolean;
- views: View[];
objectMetadataItems: ObjectMetadataItem[];
}) => {
const { toggleNavigationSection, isNavigationSectionOpenState } =
useNavigationSection('Objects' + (isRemote ? 'Remote' : 'Workspace'));
const isNavigationSectionOpen = useRecoilValue(isNavigationSectionOpenState);
- const { getIcon } = useIcons();
- const currentPath = useLocation().pathname;
- const { getLastVisitedViewIdFromObjectMetadataItemId } = useLastVisitedView();
-
- const renderObjectMetadataItems = () => {
- return [
- ...objectMetadataItems
- .filter((item) => ORDERED_STANDARD_OBJECTS.includes(item.nameSingular))
- .sort((objectMetadataItemA, objectMetadataItemB) => {
- const indexA = ORDERED_STANDARD_OBJECTS.indexOf(
- objectMetadataItemA.nameSingular,
- );
- const indexB = ORDERED_STANDARD_OBJECTS.indexOf(
- objectMetadataItemB.nameSingular,
- );
- if (indexA === -1 || indexB === -1) {
- return objectMetadataItemA.nameSingular.localeCompare(
- objectMetadataItemB.nameSingular,
- );
- }
- return indexA - indexB;
- }),
- ...objectMetadataItems
- .filter((item) => !ORDERED_STANDARD_OBJECTS.includes(item.nameSingular))
- .sort((objectMetadataItemA, objectMetadataItemB) => {
- return new Date(objectMetadataItemA.createdAt) <
- new Date(objectMetadataItemB.createdAt)
- ? 1
- : -1;
- }),
- ].map((objectMetadataItem) => {
- const objectMetadataViews = getObjectMetadataItemViews(
- objectMetadataItem.id,
- views,
- );
- const lastVisitedViewId = getLastVisitedViewIdFromObjectMetadataItemId(
- objectMetadataItem.id,
- );
- const viewId = lastVisitedViewId ?? objectMetadataViews[0]?.id;
-
- const navigationPath = `/objects/${objectMetadataItem.namePlural}${
- viewId ? `?view=${viewId}` : ''
- }`;
-
- const isActive =
- currentPath === `/objects/${objectMetadataItem.namePlural}`;
- const shouldSubItemsBeDisplayed =
- isActive && objectMetadataViews.length > 1;
-
- const sortedObjectMetadataViews = [...objectMetadataViews].sort(
- (viewA, viewB) =>
- viewA.key === 'INDEX' ? -1 : viewA.position - viewB.position,
+ const sortedStandardObjectMetadataItems = [...objectMetadataItems]
+ .filter((item) => ORDERED_STANDARD_OBJECTS.includes(item.nameSingular))
+ .sort((objectMetadataItemA, objectMetadataItemB) => {
+ const indexA = ORDERED_STANDARD_OBJECTS.indexOf(
+ objectMetadataItemA.nameSingular,
);
-
- const selectedSubItemIndex = sortedObjectMetadataViews.findIndex(
- (view) => viewId === view.id,
+ const indexB = ORDERED_STANDARD_OBJECTS.indexOf(
+ objectMetadataItemB.nameSingular,
);
+ if (indexA === -1 || indexB === -1) {
+ return objectMetadataItemA.nameSingular.localeCompare(
+ objectMetadataItemB.nameSingular,
+ );
+ }
+ return indexA - indexB;
+ });
- const subItemArrayLength = sortedObjectMetadataViews.length;
-
- return (
-
-
- {shouldSubItemsBeDisplayed &&
- sortedObjectMetadataViews.map((view, index) => (
-
- ))}
-
- );
+ const sortedCustomObjectMetadataItems = [...objectMetadataItems]
+ .filter((item) => !ORDERED_STANDARD_OBJECTS.includes(item.nameSingular))
+ .sort((objectMetadataItemA, objectMetadataItemB) => {
+ return new Date(objectMetadataItemA.createdAt) <
+ new Date(objectMetadataItemB.createdAt)
+ ? 1
+ : -1;
});
- };
+
+ const objectMetadataItemsForNavigationItems = [
+ ...sortedStandardObjectMetadataItems,
+ ...sortedCustomObjectMetadataItems,
+ ];
return (
objectMetadataItems.length > 0 && (
@@ -136,7 +79,19 @@ export const NavigationDrawerSectionForObjectMetadataItems = ({
onClick={() => toggleNavigationSection()}
/>
- {isNavigationSectionOpen && renderObjectMetadataItems()}
+
+
+ {isNavigationSectionOpen &&
+ objectMetadataItemsForNavigationItems.map(
+ (objectMetadataItem) => (
+
+ ),
+ )}
+
+
)
);
diff --git a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper.tsx b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper.tsx
index 2127db1fc604..91a22ca5ab1e 100644
--- a/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper.tsx
+++ b/packages/twenty-front/src/modules/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsWrapper.tsx
@@ -6,9 +6,6 @@ import { NavigationDrawerSectionForObjectMetadataItems } from '@/object-metadata
import { NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader } from '@/object-metadata/components/NavigationDrawerSectionForObjectMetadataItemsSkeletonLoader';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useIsPrefetchLoading } from '@/prefetch/hooks/useIsPrefetchLoading';
-import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
-import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
-import { View } from '@/views/types/View';
export const NavigationDrawerSectionForObjectMetadataItemsWrapper = ({
isRemote,
@@ -21,8 +18,6 @@ export const NavigationDrawerSectionForObjectMetadataItemsWrapper = ({
const filteredActiveObjectMetadataItems = activeObjectMetadataItems.filter(
(item) => (isRemote ? item.isRemote : !item.isRemote),
);
-
- const { records: views } = usePrefetchedData(PrefetchKey.AllViews);
const loading = useIsPrefetchLoading();
if (loading && isDefined(currentUser)) {
@@ -33,7 +28,6 @@ export const NavigationDrawerSectionForObjectMetadataItemsWrapper = ({
);
diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsRelationPickerEffect.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsRelationPickerEffect.tsx
deleted file mode 100644
index 1d2d1ecd74b9..000000000000
--- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsRelationPickerEffect.tsx
+++ /dev/null
@@ -1,29 +0,0 @@
-import { useEffect } from 'react';
-
-import { useRelationPicker } from '@/object-record/relation-picker/hooks/useRelationPicker';
-
-export const ObjectMetadataItemsRelationPickerEffect = ({
- relationPickerScopeId,
-}: {
- relationPickerScopeId?: string;
-} = {}) => {
- const { setSearchQuery } = useRelationPicker({ relationPickerScopeId });
-
- const computeFilterFields = (relationPickerType: string) => {
- if (relationPickerType === 'company') {
- return ['name'];
- }
-
- if (['workspaceMember', 'person'].includes(relationPickerType)) {
- return ['name.firstName', 'name.lastName'];
- }
-
- return ['name'];
- };
-
- useEffect(() => {
- setSearchQuery({ computeFilterFields });
- }, [setSearchQuery]);
-
- return <>>;
-};
diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts
index db3506c2d12c..0ab2a8229854 100644
--- a/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts
+++ b/packages/twenty-front/src/modules/object-metadata/utils/mapPaginatedObjectMetadataItemsToObjectMetadataItems.ts
@@ -11,7 +11,7 @@ export const mapPaginatedObjectMetadataItemsToObjectMetadataItems = ({
pagedObjectMetadataItems?.objects.edges.map((object) => ({
...object.node,
fields: object.node.fields.edges.map((field) => field.node),
- indexMetadatas: object.node.indexMetadatas.edges.map((index) => ({
+ indexMetadatas: object.node.indexMetadatas?.edges.map((index) => ({
...index.node,
indexFieldMetadatas: index.node.indexFieldMetadatas?.edges.map(
(indexField) => indexField.node,
diff --git a/packages/twenty-front/src/modules/object-record/hooks/useSearchRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useSearchRecords.ts
index 175f84554f19..7c1f90162597 100644
--- a/packages/twenty-front/src/modules/object-record/hooks/useSearchRecords.ts
+++ b/packages/twenty-front/src/modules/object-record/hooks/useSearchRecords.ts
@@ -13,10 +13,11 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useQuery, WatchQueryFetchPolicy } from '@apollo/client';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
+import { isDefined } from '~/utils/isDefined';
import { logError } from '~/utils/logError';
export type UseSearchRecordsParams = ObjectMetadataItemIdentifier &
- RecordGqlOperationVariables & {
+ Pick & {
onError?: (error?: Error) => void;
skip?: boolean;
recordGqlFields?: RecordGqlOperationGqlRecordFields;
@@ -29,6 +30,7 @@ export const useSearchRecords = ({
searchInput,
limit,
skip,
+ filter,
recordGqlFields,
fetchPolicy,
}: UseSearchRecordsParams) => {
@@ -45,10 +47,14 @@ export const useSearchRecords = ({
const { data, loading, error, previousData } =
useQuery(searchRecordsQuery, {
skip:
- skip || !objectMetadataItem || !currentWorkspaceMember || !searchInput,
+ skip ||
+ !objectMetadataItem ||
+ !currentWorkspaceMember ||
+ !isDefined(searchInput),
variables: {
search: searchInput,
limit: limit,
+ filter: filter,
},
fetchPolicy: fetchPolicy,
onError: (error) => {
diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx
index ff408eb407de..e9487ea6eb23 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx
@@ -4,6 +4,8 @@ import { useContext, useRef } from 'react';
import { useRecoilCallback, useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
+import { RecordBoardHeader } from '@/object-record/record-board/components/RecordBoardHeader';
+import { RecordBoardStickyHeaderEffect } from '@/object-record/record-board/components/RecordBoardStickyHeaderEffect';
import { RecordBoardContext } from '@/object-record/record-board/contexts/RecordBoardContext';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection';
@@ -19,31 +21,26 @@ import { getScopeIdFromComponentId } from '@/ui/utilities/recoil-scope/utils/get
import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper';
import { useScrollRestoration } from '~/hooks/useScrollRestoration';
-export type RecordBoardProps = {
- recordBoardId: string;
-};
-
const StyledContainer = styled.div`
- border-top: 1px solid ${({ theme }) => theme.border.color.light};
- overflow: auto;
display: flex;
flex: 1;
flex-direction: row;
min-height: calc(100% - 1px);
+ height: 100%;
`;
-const StyledWrapper = styled.div`
+const StyledColumnContainer = styled.div`
+ display: flex;
+`;
+
+const StyledContainerContainer = styled.div`
display: flex;
flex-direction: column;
- height: 100%;
- overflow: hidden;
- position: relative;
- width: 100%;
`;
-const StyledBoardHeader = styled.div`
- position: relative;
- z-index: 1;
+const StyledBoardContentContainer = styled.div`
+ display: flex;
+ flex-direction: column;
`;
const RecordBoardScrollRestoreEffect = () => {
@@ -51,8 +48,8 @@ const RecordBoardScrollRestoreEffect = () => {
return null;
};
-export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => {
- const { updateOneRecord, selectFieldMetadataItem } =
+export const RecordBoard = () => {
+ const { updateOneRecord, selectFieldMetadataItem, recordBoardId } =
useContext(RecordBoardContext);
const boardRef = useRef(null);
@@ -75,7 +72,7 @@ export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => {
useScopedHotkeys([Key.Escape], resetRecordSelection, TableHotkeyScope.Table);
- const onDragEnd: OnDragEndResponder = useRecoilCallback(
+ const handleDragEnd: OnDragEndResponder = useRecoilCallback(
({ snapshot }) =>
(result) => {
if (!result.destination) return;
@@ -146,27 +143,32 @@ export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => {
onColumnsChange={() => {}}
onFieldsChange={() => {}}
>
-
-
-
-
-
- {columnIds.map((columnId) => (
-
- ))}
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+ {columnIds.map((columnId) => (
+
+ ))}
+
+
+
+
+
+
+
+
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx
new file mode 100644
index 000000000000..59e1acf47967
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardHeader.tsx
@@ -0,0 +1,34 @@
+import { useRecoilValue } from 'recoil';
+
+import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
+import { RecordBoardColumnHeaderWrapper } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper';
+import styled from '@emotion/styled';
+
+const StyledHeaderContainer = styled.div`
+ display: flex;
+ flex-direction: row;
+ height: 40px;
+ z-index: 10;
+
+ overflow: visible;
+ width: 100%;
+
+ &.header-sticky {
+ position: sticky;
+ top: 0;
+ }
+`;
+
+export const RecordBoardHeader = () => {
+ const { columnIdsState } = useRecordBoardStates();
+
+ const columnIds = useRecoilValue(columnIdsState);
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardStickyHeaderEffect.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardStickyHeaderEffect.tsx
new file mode 100644
index 000000000000..5544a7801a2f
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoardStickyHeaderEffect.tsx
@@ -0,0 +1,22 @@
+import { useEffect } from 'react';
+
+import { useScrollTopValue } from '@/ui/utilities/scroll/hooks/useScrollTopValue';
+
+export const RecordBoardStickyHeaderEffect = () => {
+ const scrollTop = useScrollTopValue('recordBoard');
+
+ // TODO: move this outside because it might cause way too many re-renders for other hooks
+ useEffect(() => {
+ if (scrollTop > 0) {
+ document
+ .getElementById('record-board-header')
+ ?.classList.add('header-sticky');
+ } else {
+ document
+ .getElementById('record-board-header')
+ ?.classList.remove('header-sticky');
+ }
+ }, [scrollTop]);
+
+ return <>>;
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-board/contexts/RecordBoardContext.ts b/packages/twenty-front/src/modules/object-record/record-board/contexts/RecordBoardContext.ts
index d8d7f1490738..266e1a445460 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/contexts/RecordBoardContext.ts
+++ b/packages/twenty-front/src/modules/object-record/record-board/contexts/RecordBoardContext.ts
@@ -16,6 +16,7 @@ type RecordBoardContextProps = {
updateOneRecordInput: Partial>;
}) => void;
deleteOneRecord: (idToDelete: string) => Promise;
+ recordBoardId: string;
};
export const RecordBoardContext = createContext(
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx
index 70660329bdcc..8fe97824425b 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumn.tsx
@@ -4,7 +4,6 @@ import { useRecoilValue } from 'recoil';
import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
import { RecordBoardColumnCardsContainer } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnCardsContainer';
-import { RecordBoardColumnHeader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeader';
import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
const StyledColumn = styled.div<{ isFirstColumn: boolean }>`
@@ -18,7 +17,12 @@ const StyledColumn = styled.div<{ isFirstColumn: boolean }>`
min-width: 200px;
padding: ${({ theme }) => theme.spacing(2)};
+
+ padding-top: 0px;
+
position: relative;
+
+ min-height: 100%;
`;
type RecordBoardColumnProps = {
@@ -61,12 +65,13 @@ export const RecordBoardColumn = ({
isFirstColumn: isFirstColumn,
isLastColumn: isLastColumn,
recordCount: recordIds.length,
+ columnId: recordBoardColumnId,
+ recordIds,
}}
>
{(droppableProvided) => (
-
theme.spacing(2)};
width: 100%;
`;
@@ -45,6 +44,7 @@ const StyledHeaderActions = styled.div`
margin-left: auto;
`;
const StyledHeaderContainer = styled.div`
+ background: ${({ theme }) => theme.background.primary};
display: flex;
justify-content: space-between;
width: 100%;
@@ -59,13 +59,29 @@ const StyledRightContainer = styled.div`
display: flex;
`;
+const StyledColumn = styled.div<{ isFirstColumn: boolean }>`
+ background-color: ${({ theme }) => theme.background.primary};
+ border-left: 1px solid
+ ${({ theme, isFirstColumn }) =>
+ isFirstColumn ? 'none' : theme.border.color.light};
+ display: flex;
+ flex-direction: column;
+ max-width: 200px;
+ min-width: 200px;
+
+ padding: ${({ theme }) => theme.spacing(2)};
+
+ position: relative;
+`;
+
export const RecordBoardColumnHeader = () => {
+ const { columnDefinition, isFirstColumn, recordCount } = useContext(
+ RecordBoardColumnContext,
+ );
const [isBoardColumnMenuOpen, setIsBoardColumnMenuOpen] = useState(false);
const [isHeaderHovered, setIsHeaderHovered] = useState(false);
+
const { objectMetadataItem } = useContext(RecordBoardContext);
- const { columnDefinition, recordCount } = useContext(
- RecordBoardColumnContext,
- );
const {
setHotkeyScopeAndMemorizePreviousScope,
@@ -94,7 +110,8 @@ export const RecordBoardColumnHeader = () => {
handleNewButtonClick,
handleCreateSuccess,
handleEntitySelect,
- } = useColumnNewCardActions(columnDefinition.id);
+ } = useColumnNewCardActions(columnDefinition?.id ?? '');
+
const { isOpportunitiesCompanyFieldDisabled } =
useIsOpportunitiesCompanyFieldDisabled();
@@ -103,7 +120,7 @@ export const RecordBoardColumnHeader = () => {
!isOpportunitiesCompanyFieldDisabled;
return (
- <>
+
setIsHeaderHovered(true)}
onMouseLeave={() => setIsHeaderHovered(false)}
@@ -181,6 +198,6 @@ export const RecordBoardColumnHeader = () => {
position="first"
/>
))}
- >
+
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper.tsx
new file mode 100644
index 000000000000..63f25794bd9d
--- /dev/null
+++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnHeaderWrapper.tsx
@@ -0,0 +1,48 @@
+import { isDefined } from 'twenty-ui';
+
+import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates';
+import { RecordBoardColumnHeader } from '@/object-record/record-board/record-board-column/components/RecordBoardColumnHeader';
+import { RecordBoardColumnContext } from '@/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext';
+import { useRecoilValue } from 'recoil';
+
+type RecordBoardColumnHeaderWrapperProps = {
+ columnId: string;
+};
+
+export const RecordBoardColumnHeaderWrapper = ({
+ columnId,
+}: RecordBoardColumnHeaderWrapperProps) => {
+ const {
+ isFirstColumnFamilyState,
+ isLastColumnFamilyState,
+ columnsFamilySelector,
+ recordIdsByColumnIdFamilyState,
+ } = useRecordBoardStates();
+
+ const columnDefinition = useRecoilValue(columnsFamilySelector(columnId));
+
+ const isFirstColumn = useRecoilValue(isFirstColumnFamilyState(columnId));
+
+ const isLastColumn = useRecoilValue(isLastColumnFamilyState(columnId));
+
+ const recordIds = useRecoilValue(recordIdsByColumnIdFamilyState(columnId));
+
+ if (!isDefined(columnDefinition)) {
+ return null;
+ }
+
+ return (
+
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext.ts b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext.ts
index 8a9ced3eb00b..f37c5c5cb3b3 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext.ts
+++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/contexts/RecordBoardColumnContext.ts
@@ -7,6 +7,8 @@ type RecordBoardColumnContextProps = {
isFirstColumn: boolean;
isLastColumn: boolean;
recordCount: number;
+ columnId: string;
+ recordIds: string[];
};
export const RecordBoardColumnContext =
diff --git a/packages/twenty-front/src/modules/object-record/record-board/scopes/RecordBoardScope.tsx b/packages/twenty-front/src/modules/object-record/record-board/scopes/RecordBoardScope.tsx
index 0de4d6025714..37c605185528 100644
--- a/packages/twenty-front/src/modules/object-record/record-board/scopes/RecordBoardScope.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-board/scopes/RecordBoardScope.tsx
@@ -12,6 +12,7 @@ type RecordBoardScopeProps = {
onColumnsChange: (column: RecordBoardColumnDefinition[]) => void;
};
+/** @deprecated */
export const RecordBoardScope = ({
children,
recordBoardScopeId,
diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx
index 7e3e93ec2c48..e416e50ce88a 100644
--- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/MultiItemFieldInput.tsx
@@ -18,6 +18,7 @@ import { useListenClickOutside } from '@/ui/utilities/pointer-event/hooks/useLis
import { FieldMetadataType } from '~/generated-metadata/graphql';
import { moveArrayItem } from '~/utils/array/moveArrayItem';
import { toSpliced } from '~/utils/array/toSpliced';
+import { turnIntoEmptyStringIfWhitespacesOnly } from '~/utils/string/turnIntoEmptyStringIfWhitespacesOnly';
const StyledDropdownMenu = styled(DropdownMenu)`
left: -1px;
@@ -190,7 +191,11 @@ export const MultiItemFieldInput = ({
})
: undefined
}
- onChange={(event) => handleOnChange(event.target.value)}
+ onChange={(event) =>
+ handleOnChange(
+ turnIntoEmptyStringIfWhitespacesOnly(event.target.value),
+ )
+ }
onEnter={handleSubmitInput}
rightComponent={
-
-
+
);
};
diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx
index 50ce4ed693d0..9aecee3e6160 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx
@@ -2,8 +2,6 @@ import styled from '@emotion/styled';
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
-import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
-import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
import { RecordIndexBoardContainer } from '@/object-record/record-index/components/RecordIndexBoardContainer';
import { RecordIndexBoardDataLoader } from '@/object-record/record-index/components/RecordIndexBoardDataLoader';
import { RecordIndexBoardDataLoaderEffect } from '@/object-record/record-index/components/RecordIndexBoardDataLoaderEffect';
@@ -45,12 +43,13 @@ const StyledContainer = styled.div`
flex-direction: column;
height: 100%;
width: 100%;
- overflow: auto;
+
+ overflow: hidden;
`;
-const StyledContainerWithPadding = styled.div<{ fullHeight?: boolean }>`
- height: ${({ fullHeight }) => (fullHeight ? '100%' : 'auto')};
- padding-left: ${({ theme }) => theme.table.horizontalCellPadding};
+const StyledContainerWithPadding = styled.div`
+ height: calc(100% - 40px);
+ width: 100%;
`;
export const RecordIndexContainer = () => {
@@ -58,17 +57,12 @@ export const RecordIndexContainer = () => {
recordIndexViewTypeState,
);
- const { objectNamePlural, recordIndexId } = useContext(
- RecordIndexRootPropsContext,
- );
-
- const { objectNameSingular } = useObjectNameSingularFromPlural({
+ const {
objectNamePlural,
- });
-
- const { objectMetadataItem } = useObjectMetadataItem({
+ recordIndexId,
+ objectMetadataItem,
objectNameSingular,
- });
+ } = useContext(RecordIndexRootPropsContext);
const { columnDefinitions, filterDefinitions, sortDefinitions } =
useColumnDefinitionsFromFieldMetadata(objectMetadataItem);
@@ -120,69 +114,56 @@ export const RecordIndexContainer = () => {
>
-
-
+
+ }
+ onCurrentViewChange={(view) => {
+ if (!view) {
+ return;
}
- onCurrentViewChange={(view) => {
- if (!view) {
- return;
- }
-
- onViewFieldsChange(view.viewFields);
- setTableFilters(
- mapViewFiltersToFilters(
- view.viewFilters,
- filterDefinitions,
- ),
- );
- setRecordIndexFilters(
- mapViewFiltersToFilters(
- view.viewFilters,
- filterDefinitions,
- ),
- );
- setTableSorts(
- mapViewSortsToSorts(view.viewSorts, sortDefinitions),
- );
- setRecordIndexSorts(
- mapViewSortsToSorts(view.viewSorts, sortDefinitions),
- );
- setRecordIndexViewType(view.type);
- setRecordIndexViewKanbanFieldMetadataIdState(
- view.kanbanFieldMetadataId,
- );
- setRecordIndexIsCompactModeActive(view.isCompact);
- }}
- />
-
-
-
+ onViewFieldsChange(view.viewFields);
+ setTableFilters(
+ mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
+ );
+ setRecordIndexFilters(
+ mapViewFiltersToFilters(view.viewFilters, filterDefinitions),
+ );
+ setTableSorts(
+ mapViewSortsToSorts(view.viewSorts, sortDefinitions),
+ );
+ setRecordIndexSorts(
+ mapViewSortsToSorts(view.viewSorts, sortDefinitions),
+ );
+ setRecordIndexViewType(view.type);
+ setRecordIndexViewKanbanFieldMetadataIdState(
+ view.kanbanFieldMetadataId,
+ );
+ setRecordIndexIsCompactModeActive(view.isCompact);
+ }}
+ />
+
+
{recordIndexViewType === ViewType.Table && (
<>
-
+
>
)}
{recordIndexViewType === ViewType.Kanban && (
-
+
{
+ const { recordIndexId, objectNameSingular } = useContext(
+ RecordIndexRootPropsContext,
+ );
+
+ const viewBarId = recordIndexId;
-export const RecordIndexTableContainerEffect = ({
- objectNameSingular,
- recordTableId,
- viewBarId,
-}: RecordIndexTableContainerEffectProps) => {
const {
setAvailableTableColumns,
setOnEntityCountChange,
@@ -28,7 +25,7 @@ export const RecordIndexTableContainerEffect = ({
setOnToggleColumnFilter,
setOnToggleColumnSort,
} = useRecordTable({
- recordTableId,
+ recordTableId: recordIndexId,
});
const setContextStoreTargetedRecordIds = useSetRecoilState(
diff --git a/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexRootPropsContext.ts b/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexRootPropsContext.ts
index 6de7cd552657..1546fd30a34b 100644
--- a/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexRootPropsContext.ts
+++ b/packages/twenty-front/src/modules/object-record/record-index/contexts/RecordIndexRootPropsContext.ts
@@ -1,3 +1,4 @@
+import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { createRootPropsContext } from '~/utils/createRootPropsContext';
export type RecordIndexRootPropsContextProps = {
@@ -6,6 +7,7 @@ export type RecordIndexRootPropsContextProps = {
onCreateRecord: () => void;
objectNamePlural: string;
objectNameSingular: string;
+ objectMetadataItem: ObjectMetadataItem;
recordIndexId: string;
};
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 81e0c37c76e7..432ff14fe5b9 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
@@ -4,7 +4,6 @@ import { useCallback, useContext } from 'react';
import { useRecoilValue } from 'recoil';
import { IconForbid, IconPencil, IconPlus } from 'twenty-ui';
-import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
@@ -209,7 +208,6 @@ export const RecordDetailRelationSection = ({
/>
) : (
<>
-
{
deleteCombinedViewFilter(
tableFilters.find(
(filter) =>
- filter.definition.label === 'Deleted at' &&
+ filter.definition.label === 'Deleted' &&
filter.operand === 'isNotEmpty',
)?.id ?? '',
);
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEffect.tsx
index 9550dac39bb5..506b186e7ca6 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEffect.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-body/components/RecordTableBodyEffect.tsx
@@ -9,7 +9,6 @@ import { RecordTableContext } from '@/object-record/record-table/contexts/Record
import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates';
import { hasRecordTableFetchedAllRecordsComponentStateV2 } from '@/object-record/record-table/states/hasRecordTableFetchedAllRecordsComponentStateV2';
import { isRecordTableScrolledLeftComponentState } from '@/object-record/record-table/states/isRecordTableScrolledLeftComponentState';
-import { isRecordTableScrolledTopComponentState } from '@/object-record/record-table/states/isRecordTableScrolledTopComponentState';
import { isFetchingMoreRecordsFamilyState } from '@/object-record/states/isFetchingMoreRecordsFamilyState';
import { useScrollLeftValue } from '@/ui/utilities/scroll/hooks/useScrollLeftValue';
import { useScrollTopValue } from '@/ui/utilities/scroll/hooks/useScrollTopValue';
@@ -41,16 +40,12 @@ export const RecordTableBodyEffect = () => {
const tableLastRowVisible = useRecoilValue(tableLastRowVisibleState);
const scrollTop = useScrollTopValue('recordTableWithWrappers');
- const setIsRecordTableScrolledTop = useSetRecoilComponentState(
- isRecordTableScrolledTopComponentState,
- );
const setHasRecordTableFetchedAllRecordsComponents =
useSetRecoilComponentState(hasRecordTableFetchedAllRecordsComponentStateV2);
// TODO: move this outside because it might cause way too many re-renders for other hooks
useEffect(() => {
- setIsRecordTableScrolledTop(scrollTop === 0);
if (scrollTop > 0) {
document
.getElementById('record-table-header')
@@ -60,7 +55,7 @@ export const RecordTableBodyEffect = () => {
.getElementById('record-table-header')
?.classList.remove('header-sticky');
}
- }, [scrollTop, setIsRecordTableScrolledTop]);
+ }, [scrollTop]);
const scrollLeft = useScrollLeftValue('recordTableWithWrappers');
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 556e1e9845d5..b17b50a237cc 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
@@ -8,10 +8,7 @@ import { RecordTableHeaderCheckboxColumn } from '@/object-record/record-table/re
import { RecordTableHeaderDragDropColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderDragDropColumn';
import { RecordTableHeaderLastColumn } from '@/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn';
-const StyledTableHead = styled.thead<{
- isScrolledTop?: boolean;
- isScrolledLeft?: boolean;
-}>`
+const StyledTableHead = styled.thead`
cursor: pointer;
th:nth-of-type(1) {
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx
index ff3fc98580ee..66cbddd5ed14 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCell.tsx
@@ -26,7 +26,6 @@ const StyledColumnHeaderCell = styled.th<{
isResizing?: boolean;
}>`
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
- border-top: 1px solid ${({ theme }) => theme.border.color.light};
color: ${({ theme }) => theme.font.color.tertiary};
padding: 0;
text-align: left;
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx
index 942e8865b557..5bcfd65d67cd 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderCheckboxColumn.tsx
@@ -17,7 +17,6 @@ const StyledColumnHeaderCell = styled.th`
background-color: ${({ theme }) => theme.background.primary};
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
border-right: transparent;
- border-top: 1px solid ${({ theme }) => theme.border.color.light};
max-width: 30px;
min-width: 30px;
width: 30px;
diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx
index 8a87fd63c560..9cf9df75ce9b 100644
--- a/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx
+++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-header/components/RecordTableHeaderLastColumn.tsx
@@ -22,7 +22,6 @@ const StyledPlusIconHeaderCell = styled.th<{
`;
}};
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
- border-top: 1px solid ${({ theme }) => theme.border.color.light};
background-color: ${({ theme }) => theme.background.primary};
border-left: none !important;
color: ${({ theme }) => theme.font.color.tertiary};
diff --git a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledTopComponentState.ts b/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledTopComponentState.ts
deleted file mode 100644
index 5a206e88b7a5..000000000000
--- a/packages/twenty-front/src/modules/object-record/record-table/states/isRecordTableScrolledTopComponentState.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { RecordTableScopeInternalContext } from '@/object-record/record-table/scopes/scope-internal-context/RecordTableScopeInternalContext';
-import { createComponentStateV2_alpha } from '@/ui/utilities/state/component-state/utils/createComponentStateV2_alpha';
-
-export const isRecordTableScrolledTopComponentState =
- createComponentStateV2_alpha({
- key: 'isRecordTableScrolledTopComponentState',
- componentContext: RecordTableScopeInternalContext,
- defaultValue: true,
- });
diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx
index 74c0541320b1..40c6fe337233 100644
--- a/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx
+++ b/packages/twenty-front/src/modules/object-record/relation-picker/components/SingleEntitySelectMenuItemsWithSearch.tsx
@@ -1,4 +1,3 @@
-import { ObjectMetadataItemsRelationPickerEffect } from '@/object-metadata/components/ObjectMetadataItemsRelationPickerEffect';
import {
SingleEntitySelectMenuItems,
SingleEntitySelectMenuItemsProps,
@@ -65,9 +64,6 @@ export const SingleEntitySelectMenuItemsWithSearch = ({
return (
<>
-
gql`
- query Search${capitalize(objectMetadataItem.namePlural)}($search: String, $limit: Int) {
- ${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(searchInput: $search, limit: $limit){
+ query Search${capitalize(objectMetadataItem.namePlural)}($search: String, $limit: Int, $filter: ${capitalize(
+ objectMetadataItem.nameSingular,
+ )}FilterInput) {
+ ${getSearchRecordsQueryResponseField(objectMetadataItem.namePlural)}(searchInput: $search, limit: $limit, filter: $filter){
edges {
node ${mapObjectMetadataToGraphQLQuery({
objectMetadataItems,
diff --git a/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchEntityQuery.test.tsx b/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchEntityQuery.test.tsx
index 263b70decf0f..6b2409af45a3 100644
--- a/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchEntityQuery.test.tsx
+++ b/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchEntityQuery.test.tsx
@@ -80,13 +80,11 @@ describe('useFilteredSearchEntityQuery', () => {
setMetadataItems(generatedMockObjectMetadataItems);
return useFilteredSearchEntityQuery({
- orderByField: 'name',
- filters: [{ fieldNames: ['name'], filter: 'Entity' }],
- sortOrder: 'AscNullsLast',
selectedIds: ['1'],
limit: 10,
excludeRecordIds: ['2'],
objectNameSingular: 'person',
+ searchFilter: 'Entity',
});
},
{ wrapper: Wrapper },
diff --git a/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts b/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts
index 06ba43f92e60..9d6973385230 100644
--- a/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts
+++ b/packages/twenty-front/src/modules/search/hooks/useFilteredSearchEntityQuery.ts
@@ -1,39 +1,26 @@
-import { isNonEmptyString } from '@sniptt/guards';
-
import { useMapToObjectRecordIdentifier } from '@/object-metadata/hooks/useMapToObjectRecordIdentifier';
import { DEFAULT_SEARCH_REQUEST_LIMIT } from '@/object-record/constants/DefaultSearchRequestLimit';
-import { RecordGqlOperationFilter } from '@/object-record/graphql/types/RecordGqlOperationFilter';
-import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
+import { useSearchRecords } from '@/object-record/hooks/useSearchRecords';
import { EntitiesForMultipleEntitySelect } from '@/object-record/relation-picker/types/EntitiesForMultipleEntitySelect';
import { EntityForSelect } from '@/object-record/relation-picker/types/EntityForSelect';
import { ObjectRecord } from '@/object-record/types/ObjectRecord';
-import { makeAndFilterVariables } from '@/object-record/utils/makeAndFilterVariables';
-import { makeOrFilterVariables } from '@/object-record/utils/makeOrFilterVariables';
-import { OrderBy } from '@/types/OrderBy';
-import { generateILikeFiltersForCompositeFields } from '~/utils/array/generateILikeFiltersForCompositeFields';
import { isDefined } from '~/utils/isDefined';
-type SearchFilter = { fieldNames: string[]; filter: string | number };
-
// TODO: use this for all search queries, because we need selectedEntities and entitiesToSelect each time we want to search
// Filtered entities to select are
export const useFilteredSearchEntityQuery = ({
- orderByField,
- filters,
- sortOrder = 'AscNullsLast',
selectedIds,
limit,
excludeRecordIds = [],
objectNameSingular,
+ searchFilter,
}: {
- orderByField: string;
- filters: SearchFilter[];
- sortOrder?: OrderBy;
selectedIds: string[];
limit?: number;
excludeRecordIds?: string[];
objectNameSingular: string;
+ searchFilter?: string;
}): EntitiesForMultipleEntitySelect => {
const { mapToObjectRecordIdentifier } = useMapToObjectRecordIdentifier({
objectNameSingular,
@@ -46,55 +33,21 @@ export const useFilteredSearchEntityQuery = ({
const selectedIdsFilter = { id: { in: selectedIds } };
const { loading: selectedRecordsLoading, records: selectedRecords } =
- useFindManyRecords({
+ useSearchRecords({
objectNameSingular,
filter: selectedIdsFilter,
- orderBy: [{ [orderByField]: sortOrder }],
skip: !selectedIds.length,
+ searchInput: searchFilter,
});
- const searchFilters = filters.map(({ fieldNames, filter }) => {
- if (!isNonEmptyString(filter)) {
- return undefined;
- }
-
- const formattedFilters = fieldNames.reduce(
- (previousValue: RecordGqlOperationFilter[], fieldName) => {
- const [parentFieldName, subFieldName] = fieldName.split('.');
-
- if (isNonEmptyString(subFieldName)) {
- // Composite field
- return [
- ...previousValue,
- ...generateILikeFiltersForCompositeFields(filter, parentFieldName, [
- subFieldName,
- ]),
- ];
- }
-
- return [
- ...previousValue,
- {
- [fieldName]: {
- ilike: `%${filter}%`,
- },
- },
- ];
- },
- [],
- );
-
- return makeOrFilterVariables(formattedFilters);
- });
-
const {
loading: filteredSelectedRecordsLoading,
records: filteredSelectedRecords,
- } = useFindManyRecords({
+ } = useSearchRecords({
objectNameSingular,
- filter: makeAndFilterVariables([...searchFilters, selectedIdsFilter]),
- orderBy: [{ [orderByField]: sortOrder }],
+ filter: selectedIdsFilter,
skip: !selectedIds.length,
+ searchInput: searchFilter,
});
const notFilterIds = [...selectedIds, ...excludeRecordIds];
@@ -102,11 +55,11 @@ export const useFilteredSearchEntityQuery = ({
? { not: { id: { in: notFilterIds } } }
: undefined;
const { loading: recordsToSelectLoading, records: recordsToSelect } =
- useFindManyRecords({
+ useSearchRecords({
objectNameSingular,
- filter: makeAndFilterVariables([...searchFilters, notFilter]),
+ filter: notFilter,
limit: limit ?? DEFAULT_SEARCH_REQUEST_LIMIT,
- orderBy: [{ [orderByField]: sortOrder }],
+ searchInput: searchFilter,
});
return {
diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTable.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTable.tsx
index a4c9f5306fad..3d513dc1eff4 100644
--- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTable.tsx
+++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTable.tsx
@@ -1,11 +1,10 @@
-import styled from '@emotion/styled';
-
import { BlocklistItem } from '@/accounts/types/BlocklistItem';
import { SettingsAccountsBlocklistTableRow } from '@/settings/accounts/components/SettingsAccountsBlocklistTableRow';
import { Table } from '@/ui/layout/table/components/Table';
import { TableBody } from '@/ui/layout/table/components/TableBody';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
+import styled from '@emotion/styled';
type SettingsAccountsBlocklistTableProps = {
blocklist: BlocklistItem[];
@@ -28,7 +27,10 @@ export const SettingsAccountsBlocklistTable = ({
<>
{blocklist.length > 0 && (
-
+
Email/Domain
Added to blocklist
diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTableRow.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTableRow.tsx
index 9a1148447a17..30cf3a37a313 100644
--- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTableRow.tsx
+++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistTableRow.tsx
@@ -1,4 +1,4 @@
-import { IconX } from 'twenty-ui';
+import { IconX, OverflowingTextWithTooltip } from 'twenty-ui';
import { BlocklistItem } from '@/accounts/types/BlocklistItem';
import { IconButton } from '@/ui/input/button/components/IconButton';
@@ -16,8 +16,14 @@ export const SettingsAccountsBlocklistTableRow = ({
onRemove,
}: SettingsAccountsBlocklistTableRowProps) => {
return (
-
- {blocklistItem.handle}
+
+
+
+
{blocklistItem.createdAt
? formatToHumanReadableDate(blocklistItem.createdAt)
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx
index 9b50515b10b9..ca1ef939abdb 100644
--- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/components/SettingsDataModelFieldSettingsFormCard.tsx
@@ -129,7 +129,9 @@ export const SettingsDataModelFieldSettingsFormCard = ({
fieldMetadataItem,
objectMetadataItem,
}: SettingsDataModelFieldSettingsFormCardProps) => {
- if (!previewableTypes.includes(fieldMetadataItem.type)) return null;
+ if (!previewableTypes.includes(fieldMetadataItem.type)) {
+ return null;
+ }
if (fieldMetadataItem.type === FieldMetadataType.Boolean) {
return (
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx
index 0372ba071417..59bee20064ea 100644
--- a/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/forms/relation/components/SettingsDataModelFieldRelationSettingsFormCard.tsx
@@ -23,7 +23,6 @@ type SettingsDataModelFieldRelationSettingsFormCardProps = {
} & Pick;
const StyledFieldPreviewCard = styled(SettingsDataModelFieldPreviewCard)`
- display: grid;
flex: 1 1 100%;
`;
diff --git a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx
index 06684997a4b2..360c9a300692 100644
--- a/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/fields/preview/components/SettingsDataModelFieldPreviewCard.tsx
@@ -19,7 +19,6 @@ const StyledCard = styled(Card)`
`;
const StyledCardContent = styled(CardContent)`
- display: grid;
padding: ${({ theme }) => theme.spacing(2)};
`;
diff --git a/packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectSummary.tsx b/packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectSummary.tsx
index f6b8aaa0ab54..931fb6990e09 100644
--- a/packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectSummary.tsx
+++ b/packages/twenty-front/src/modules/settings/data-model/objects/SettingsDataModelObjectSummary.tsx
@@ -1,6 +1,6 @@
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
-import { useIcons } from 'twenty-ui';
+import { OverflowingTextWithTooltip, useIcons } from 'twenty-ui';
import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem';
import { SettingsDataModelObjectTypeTag } from '@/settings/data-model/objects/SettingsDataModelObjectTypeTag';
@@ -14,15 +14,17 @@ export type SettingsDataModelObjectSummaryProps = {
const StyledObjectSummary = styled.div`
align-items: center;
display: flex;
- gap: ${({ theme }) => theme.spacing(2)};
justify-content: space-between;
`;
const StyledObjectName = styled.div`
- align-items: center;
display: flex;
- font-weight: ${({ theme }) => theme.font.weight.medium};
- gap: ${({ theme }) => theme.spacing(1)};
+ gap: ${({ theme }) => theme.spacing(2)};
+ max-width: 60%;
+`;
+
+const StyledIconContainer = styled.div`
+ flex-shrink: 0;
`;
export const SettingsDataModelObjectSummary = ({
@@ -38,8 +40,10 @@ export const SettingsDataModelObjectSummary = ({
return (
-
- {objectMetadataItem.labelPlural}
+
+
+
+
diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx
new file mode 100644
index 000000000000..40925c5d3830
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip.tsx
@@ -0,0 +1,89 @@
+import { formatDateISOStringToDateTimeSimplified } from '@/localization/utils/formatDateISOStringToDateTimeSimplified';
+import { UserContext } from '@/users/contexts/UserContext';
+import styled from '@emotion/styled';
+import { Point } from '@nivo/line';
+import { ReactElement, useContext } from 'react';
+
+const StyledTooltipContainer = styled.div`
+ align-items: center;
+ border-radius: ${({ theme }) => theme.border.radius.md};
+ border: 1px solid ${({ theme }) => theme.border.color.medium};
+ display: flex;
+ width: 128px;
+ flex-direction: column;
+ justify-content: center;
+ background: ${({ theme }) => theme.background.transparent.secondary};
+ box-shadow: ${({ theme }) => theme.boxShadow.light};
+ backdrop-filter: ${({ theme }) => theme.blur.medium};
+`;
+
+const StyledTooltipDateContainer = styled.div`
+ align-items: flex-start;
+ align-self: stretch;
+ display: flex;
+ justify-content: center;
+ font-weight: ${({ theme }) => theme.font.weight.medium};
+ font-family: ${({ theme }) => theme.font.family};
+ gap: ${({ theme }) => theme.spacing(2)};
+ color: ${({ theme }) => theme.font.color.secondary};
+ padding: ${({ theme }) => theme.spacing(2)};
+`;
+
+const StyledTooltipDataRow = styled.div`
+ align-items: flex-start;
+ align-self: stretch;
+ display: flex;
+ justify-content: space-between;
+ color: ${({ theme }) => theme.font.color.tertiary};
+ padding: ${({ theme }) => theme.spacing(2)};
+`;
+
+const StyledLine = styled.div`
+ background-color: ${({ theme }) => theme.border.color.medium};
+ height: 1px;
+ width: 100%;
+`;
+const StyledColorPoint = styled.div<{ color: string }>`
+ background-color: ${({ color }) => color};
+ border-radius: 50%;
+ height: 8px;
+ width: 8px;
+ display: inline-block;
+`;
+const StyledDataDefinition = styled.div`
+ display: flex;
+ align-items: center;
+ gap: ${({ theme }) => theme.spacing(2)};
+`;
+const StyledSpan = styled.span`
+ color: ${({ theme }) => theme.font.color.primary};
+`;
+type SettingsDevelopersWebhookTooltipProps = {
+ point: Point;
+};
+export const SettingsDevelopersWebhookTooltip = ({
+ point,
+}: SettingsDevelopersWebhookTooltipProps): ReactElement => {
+ const { timeFormat, timeZone } = useContext(UserContext);
+ const windowInterval = new Date(point.data.x);
+ const windowIntervalDate = formatDateISOStringToDateTimeSimplified(
+ windowInterval,
+ timeZone,
+ timeFormat,
+ );
+ return (
+
+
+ {windowIntervalDate}
+
+
+
+
+
+ {String(point.serieId)}
+
+ {String(point.data.y)}
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx
index eb2e359fff16..9626c6712eef 100644
--- a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx
+++ b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph.tsx
@@ -1,8 +1,13 @@
+import { SettingsDevelopersWebhookTooltip } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookTooltip';
+import { useGraphData } from '@/settings/developers/webhook/hooks/useGraphData';
import { webhookGraphDataState } from '@/settings/developers/webhook/states/webhookGraphDataState';
+import { Select } from '@/ui/input/components/Select';
+import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { ResponsiveLine } from '@nivo/line';
import { Section } from '@react-email/components';
-import { useRecoilValue } from 'recoil';
+import { useState } from 'react';
+import { useRecoilValue, useSetRecoilState } from 'recoil';
import { H2Title } from 'twenty-ui';
export type NivoLineInput = {
@@ -14,22 +19,102 @@ export type NivoLineInput = {
}>;
};
const StyledGraphContainer = styled.div`
- height: 200px;
- width: 100%;
+ background-color: ${({ theme }) => theme.background.secondary};
+ border: 1px solid ${({ theme }) => theme.border.color.medium};
+ border-radius: ${({ theme }) => theme.border.radius.md};
+ height: 199px;
+
+ padding: ${({ theme }) => theme.spacing(4, 2, 2, 2)};
+ width: 496px;
+`;
+const StyledTitleContainer = styled.div`
+ align-items: flex-start;
+ display: flex;
+ justify-content: space-between;
`;
-export const SettingsDeveloppersWebhookUsageGraph = () => {
+
+type SettingsDevelopersWebhookUsageGraphProps = {
+ webhookId: string;
+};
+
+export const SettingsDevelopersWebhookUsageGraph = ({
+ webhookId,
+}: SettingsDevelopersWebhookUsageGraphProps) => {
const webhookGraphData = useRecoilValue(webhookGraphDataState);
+ const setWebhookGraphData = useSetRecoilState(webhookGraphDataState);
+ const theme = useTheme();
+
+ const [windowLengthGraphOption, setWindowLengthGraphOption] = useState<
+ '7D' | '1D' | '12H' | '4H'
+ >('7D');
+
+ const { fetchGraphData } = useGraphData(webhookId);
return (
<>
{webhookGraphData.length ? (
-
+
+
+
+
d.color}
- margin={{ top: 0, right: 0, bottom: 50, left: 60 }}
+ theme={{
+ text: {
+ fill: theme.font.color.light,
+ fontSize: theme.font.size.sm,
+ fontFamily: theme.font.family,
+ },
+ axis: {
+ domain: {
+ line: {
+ stroke: theme.border.color.light,
+ },
+ },
+ ticks: {
+ line: {
+ stroke: theme.border.color.light,
+ },
+ },
+ },
+ grid: {
+ line: {
+ stroke: theme.border.color.light,
+ },
+ },
+
+ crosshair: {
+ line: {
+ stroke: theme.font.color.light,
+ strokeDasharray: '2 2',
+ },
+ },
+ }}
+ margin={{ top: 20, right: 0, bottom: 30, left: 30 }}
xFormat="time:%Y-%m-%d %H:%M%"
xScale={{
type: 'time',
@@ -37,17 +122,54 @@ export const SettingsDeveloppersWebhookUsageGraph = () => {
format: '%Y-%m-%d %H:%M:%S',
precision: 'hour',
}}
+ defs={[
+ {
+ colors: [
+ {
+ color: 'inherit',
+ offset: 0,
+ },
+ {
+ color: 'inherit',
+ offset: 100,
+ opacity: 0,
+ },
+ ],
+ id: 'gradientGraph',
+ type: 'linearGradient',
+ },
+ ]}
+ fill={[
+ {
+ id: 'gradientGraph',
+ match: '*',
+ },
+ ]}
yScale={{
type: 'linear',
}}
axisBottom={{
- tickValues: 'every day',
- format: '%b %d',
+ format: '%b %d, %I:%M %p',
+ tickValues: 2,
+ tickPadding: 5,
+ tickSize: 6,
+ }}
+ axisLeft={{
+ tickPadding: 5,
+ tickSize: 6,
+ tickValues: 4,
}}
- enableTouchCrosshair={true}
- enableGridY={false}
enableGridX={false}
+ lineWidth={1}
+ gridYValues={4}
enablePoints={false}
+ isInteractive={true}
+ useMesh={true}
+ enableSlices={false}
+ enableCrosshair={false}
+ tooltip={({ point }) => (
+
+ )}
/>
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
index 0c26350243b2..6d4fd06dc0cf 100644
--- a/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect.tsx
+++ b/packages/twenty-front/src/modules/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect.tsx
@@ -1,7 +1,5 @@
-import { NivoLineInput } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph';
+import { useGraphData } from '@/settings/developers/webhook/hooks/useGraphData';
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';
@@ -14,88 +12,13 @@ export const SettingsDevelopersWebhookUsageGraphEffect = ({
}: SettingsDevelopersWebhookUsageGraphEffectProps) => {
const setWebhookGraphData = useSetRecoilState(webhookGraphDataState);
- const { enqueueSnackBar } = useSnackBar();
+ const { fetchGraphData } = useGraphData(webhookId);
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();
+ fetchGraphData('7D').then((graphInput) => {
+ setWebhookGraphData(graphInput);
+ });
+ }, [fetchGraphData, setWebhookGraphData, webhookId]);
- 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/constants/WebhookGraphApiOptionsMap.ts b/packages/twenty-front/src/modules/settings/developers/webhook/constants/WebhookGraphApiOptionsMap.ts
new file mode 100644
index 000000000000..39ef67347ab6
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/developers/webhook/constants/WebhookGraphApiOptionsMap.ts
@@ -0,0 +1,6 @@
+export const WEBHOOK_GRAPH_API_OPTIONS_MAP = {
+ '7D': { windowInHours: '168', tickIntervalInMinutes: '420' },
+ '1D': { windowInHours: '24', tickIntervalInMinutes: '60' },
+ '12H': { windowInHours: '12', tickIntervalInMinutes: '30' },
+ '4H': { windowInHours: '4', tickIntervalInMinutes: '10' },
+};
diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx
new file mode 100644
index 000000000000..62fc6d4ad952
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/developers/webhook/hooks/useGraphData.tsx
@@ -0,0 +1,23 @@
+import { fetchGraphDataOrThrow } from '@/settings/developers/webhook/utils/fetchGraphDataOrThrow';
+import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
+import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
+
+export const useGraphData = (webhookId: string) => {
+ const { enqueueSnackBar } = useSnackBar();
+ const fetchGraphData = async (
+ windowLengthGraphOption: '7D' | '1D' | '12H' | '4H',
+ ) => {
+ try {
+ return await fetchGraphDataOrThrow({
+ webhookId,
+ windowLength: windowLengthGraphOption,
+ });
+ } catch (error) {
+ enqueueSnackBar('Something went wrong while fetching webhook usage', {
+ variant: SnackBarVariant.Error,
+ });
+ return [];
+ }
+ };
+ return { fetchGraphData };
+};
diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/utils/__tests__/fetchGraphDataOrThrow.test.js b/packages/twenty-front/src/modules/settings/developers/webhook/utils/__tests__/fetchGraphDataOrThrow.test.js
new file mode 100644
index 000000000000..365c964d2c97
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/developers/webhook/utils/__tests__/fetchGraphDataOrThrow.test.js
@@ -0,0 +1,115 @@
+import { WEBHOOK_GRAPH_API_OPTIONS_MAP } from '@/settings/developers/webhook/constants/WebhookGraphApiOptionsMap';
+import { fetchGraphDataOrThrow } from '@/settings/developers/webhook/utils/fetchGraphDataOrThrow';
+
+// Mock the global fetch function
+global.fetch = jest.fn();
+
+describe('fetchGraphDataOrThrow', () => {
+ const mockWebhookId = 'test-webhook-id';
+ const mockWindowLength = '7D';
+
+ beforeEach(() => {
+ jest.resetAllMocks();
+ });
+
+ it('should fetch and transform data successfully', async () => {
+ const mockResponse = {
+ ok: true,
+ json: jest.fn().mockResolvedValue({
+ data: [
+ { start_interval: '2023-05-01', failure_count: 2, success_count: 8 },
+ { start_interval: '2023-05-02', failure_count: 1, success_count: 9 },
+ ],
+ }),
+ };
+ global.fetch.mockResolvedValue(mockResponse);
+
+ const result = await fetchGraphDataOrThrow({
+ webhookId: mockWebhookId,
+ windowLength: mockWindowLength,
+ });
+
+ expect(global.fetch).toHaveBeenCalledWith(
+ expect.stringContaining(
+ `https://api.eu-central-1.aws.tinybird.co/v0/pipes/getWebhooksAnalyticsV2.json?`,
+ ),
+ expect.objectContaining({
+ headers: {
+ Authorization: expect.stringContaining('Bearer '),
+ },
+ }),
+ );
+
+ expect(result).toEqual([
+ {
+ id: 'Failed',
+ color: 'red',
+ data: [
+ { x: '2023-05-01', y: 2 },
+ { x: '2023-05-02', y: 1 },
+ ],
+ },
+ {
+ id: 'Succeeded',
+ color: 'blue',
+ data: [
+ { x: '2023-05-01', y: 8 },
+ { x: '2023-05-02', y: 9 },
+ ],
+ },
+ ]);
+ });
+
+ it('should throw an error when the response is not ok', async () => {
+ const mockResponse = {
+ ok: false,
+ json: jest.fn().mockResolvedValue({ error: 'Some error' }),
+ };
+ global.fetch.mockResolvedValue(mockResponse);
+
+ await expect(
+ fetchGraphDataOrThrow({
+ webhookId: mockWebhookId,
+ windowLength: mockWindowLength,
+ }),
+ ).rejects.toThrow('Something went wrong while fetching webhook usage');
+ });
+
+ it('should use correct query parameters based on window length', async () => {
+ const mockResponse = {
+ ok: true,
+ json: jest.fn().mockResolvedValue({ data: [] }),
+ };
+ global.fetch.mockResolvedValue(mockResponse);
+
+ await fetchGraphDataOrThrow({
+ webhookId: mockWebhookId,
+ windowLength: '1D',
+ });
+
+ expect(global.fetch).toHaveBeenCalledWith(
+ expect.stringContaining(
+ new URLSearchParams({
+ ...WEBHOOK_GRAPH_API_OPTIONS_MAP['1D'],
+ webhookIdRequest: mockWebhookId,
+ }).toString(),
+ ),
+ expect.any(Object),
+ );
+ });
+
+ it('should handle empty response data', async () => {
+ const mockResponse = {
+ ok: true,
+ json: jest.fn().mockResolvedValue({ data: [] }),
+ };
+ global.fetch.mockResolvedValue(mockResponse);
+
+ const result = await fetchGraphDataOrThrow({
+ webhookId: mockWebhookId,
+ windowLength: mockWindowLength,
+ });
+
+ expect(result).toEqual([]);
+ });
+});
diff --git a/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts b/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts
new file mode 100644
index 000000000000..b7123f712579
--- /dev/null
+++ b/packages/twenty-front/src/modules/settings/developers/webhook/utils/fetchGraphDataOrThrow.ts
@@ -0,0 +1,80 @@
+import { NivoLineInput } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph';
+import { WEBHOOK_GRAPH_API_OPTIONS_MAP } from '@/settings/developers/webhook/constants/WebhookGraphApiOptionsMap';
+
+type fetchGraphDataOrThrowProps = {
+ webhookId: string;
+ windowLength: '7D' | '1D' | '12H' | '4H';
+};
+
+export const fetchGraphDataOrThrow = async ({
+ webhookId,
+ windowLength,
+}: fetchGraphDataOrThrowProps) => {
+ const queryString = new URLSearchParams({
+ ...WEBHOOK_GRAPH_API_OPTIONS_MAP[windowLength],
+ webhookIdRequest: webhookId,
+ }).toString();
+ const token = 'REPLACE_ME';
+ const response = await fetch(
+ `https://api.eu-central-1.aws.tinybird.co/v0/pipes/getWebhooksAnalyticsV2.json?${queryString}`,
+ {
+ headers: {
+ Authorization: 'Bearer ' + token,
+ },
+ },
+ );
+ const result = await response.json();
+
+ if (!response.ok) {
+ throw new Error('Something went wrong while fetching webhook usage');
+ }
+ // Next steps: separate the map logic to a different component (response.data, {id:str, color:str}[])=>NivoLineInput[]
+
+ const graphInput = result.data
+ .flatMap(
+ (dataRow: {
+ start_interval: string;
+ failure_count: number;
+ success_count: number;
+ }) => [
+ {
+ x: dataRow.start_interval,
+ y: dataRow.failure_count,
+ id: 'Failed',
+ color: 'red', // need to refacto this
+ },
+ {
+ x: dataRow.start_interval,
+ y: dataRow.success_count,
+ id: 'Succeeded',
+ color: 'blue',
+ },
+ ],
+ )
+ .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 }] }];
+ }
+ },
+ [],
+ );
+ return graphInput;
+};
diff --git a/packages/twenty-front/src/modules/ui/input/components/Checkbox.tsx b/packages/twenty-front/src/modules/ui/input/components/Checkbox.tsx
index fd3327c63f23..e19d3cb70976 100644
--- a/packages/twenty-front/src/modules/ui/input/components/Checkbox.tsx
+++ b/packages/twenty-front/src/modules/ui/input/components/Checkbox.tsx
@@ -52,7 +52,10 @@ const StyledInputContainer = styled.div`
cursor: ${({ disabled }) => (disabled ? 'not-allowed' : 'pointer')};
display: flex;
- padding: ${({ theme }) => theme.spacing(1)};
+ padding: ${({ theme, checkboxSize }) =>
+ checkboxSize === CheckboxSize.Large
+ ? theme.spacing(1.5)
+ : theme.spacing(1.25)};
position: relative;
${({ hoverable, isChecked, theme, indeterminate, disabled }) => {
if (!hoverable || disabled === true) return '';
@@ -126,10 +129,9 @@ const StyledInput = styled.input`
}
& + label > svg {
- --padding: ${({ checkboxSize }) =>
- checkboxSize === CheckboxSize.Large ? '2px' : '1px'};
+ --padding: 0px;
--size: ${({ checkboxSize }) =>
- checkboxSize === CheckboxSize.Large ? '16px' : '12px'};
+ checkboxSize === CheckboxSize.Large ? '20px' : '14px'};
height: var(--size);
left: var(--padding);
position: absolute;
diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx
index 349c9cfe3edd..c66767a09039 100644
--- a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx
@@ -53,7 +53,7 @@ export const TabList = ({
return (
-
+
{tabs
.filter((tab) => !tab.hide)
diff --git a/packages/twenty-front/src/modules/ui/layout/top-bar/TopBar.tsx b/packages/twenty-front/src/modules/ui/layout/top-bar/TopBar.tsx
index f19e3efaf685..87bee383cbdf 100644
--- a/packages/twenty-front/src/modules/ui/layout/top-bar/TopBar.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/top-bar/TopBar.tsx
@@ -1,5 +1,5 @@
-import { ReactNode } from 'react';
import styled from '@emotion/styled';
+import { ReactNode } from 'react';
type TopBarProps = {
className?: string;
@@ -10,14 +10,15 @@ type TopBarProps = {
};
const StyledContainer = styled.div`
+ border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
display: flex;
+
flex-direction: column;
`;
-const StyledTopBar = styled.div<{ displayBottomBorder: boolean }>`
+const StyledTopBar = styled.div`
align-items: center;
- border-bottom: ${({ displayBottomBorder, theme }) =>
- displayBottomBorder ? `1px solid ${theme.border.color.light}` : 'none'};
+
box-sizing: border-box;
color: ${({ theme }) => theme.font.color.secondary};
display: flex;
@@ -26,6 +27,8 @@ const StyledTopBar = styled.div<{ displayBottomBorder: boolean }>`
height: 39px;
justify-content: space-between;
padding-right: ${({ theme }) => theme.spacing(2)};
+ padding-left: ${({ theme }) => theme.spacing(2)};
+
z-index: 7;
`;
@@ -44,10 +47,9 @@ export const TopBar = ({
leftComponent,
rightComponent,
bottomComponent,
- displayBottomBorder = true,
}: TopBarProps) => (
-
+
{leftComponent}
{rightComponent}
diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx
index 4fa845c6acfc..fd1cc17bd22e 100644
--- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx
+++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx
@@ -1,21 +1,21 @@
-import { useTheme } from '@emotion/react';
-import styled from '@emotion/styled';
-import { useState } from 'react';
-import { useRecoilValue } from 'recoil';
-import { IconChevronDown } from 'twenty-ui';
-
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { Workspaces } from '@/auth/states/workspaces';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { MenuItemSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemSelectAvatar';
+import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { MULTI_WORKSPACE_DROPDOWN_ID } from '@/ui/navigation/navigation-drawer/constants/MulitWorkspaceDropdownId';
import { useWorkspaceSwitching } from '@/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching';
import { NavigationDrawerHotKeyScope } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerHotKeyScope';
+import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
+import { useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+import { useState } from 'react';
+import { useRecoilState, useRecoilValue } from 'recoil';
+import { IconChevronDown } from 'twenty-ui';
import { getImageAbsoluteURI } from '~/utils/image/getImageAbsoluteURI';
-import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
const StyledLogo = styled.div<{ logo: string }>`
background: url(${({ logo }) => logo});
@@ -26,7 +26,7 @@ const StyledLogo = styled.div<{ logo: string }>`
width: 16px;
`;
-const StyledContainer = styled.div`
+const StyledContainer = styled.div<{ isNavigationDrawerExpanded: boolean }>`
align-items: center;
cursor: pointer;
color: ${({ theme }) => theme.font.color.primary};
@@ -34,10 +34,13 @@ const StyledContainer = styled.div`
border: 1px solid transparent;
display: flex;
justify-content: space-between;
- height: ${({ theme }) => theme.spacing(5)};
+ height: ${({ theme, isNavigationDrawerExpanded }) =>
+ isNavigationDrawerExpanded ? theme.spacing(5) : theme.spacing(4)};
padding: calc(${({ theme }) => theme.spacing(1)} - 1px);
- width: 100%;
-
+ width: ${({ isNavigationDrawerExpanded }) =>
+ isNavigationDrawerExpanded ? '100%' : 'auto'};
+ gap: ${({ theme, isNavigationDrawerExpanded }) =>
+ isNavigationDrawerExpanded ? theme.spacing(1) : '0'};
&:hover {
background-color: ${({ theme }) => theme.background.transparent.lighter};
border: 1px solid ${({ theme }) => theme.border.color.medium};
@@ -47,12 +50,13 @@ const StyledContainer = styled.div`
const StyledLabel = styled.div`
align-items: center;
display: flex;
- margin: 0 ${({ theme }) => theme.spacing(1)};
`;
const StyledIconChevronDown = styled(IconChevronDown)<{ disabled?: boolean }>`
+ align-items: center;
color: ${({ disabled, theme }) =>
disabled ? theme.font.color.extraLight : theme.font.color.tertiary};
+ display: flex;
`;
type MultiWorkspaceDropdownButtonProps = {
@@ -77,6 +81,9 @@ export const MultiWorkspaceDropdownButton = ({
closeDropdown();
await switchWorkspace(workspaceId);
};
+ const [isNavigationDrawerExpanded] = useRecoilState(
+ isNavigationDrawerExpandedState,
+ );
return (
+
theme.font.color.light};
cursor: pointer;
display: flex;
- height: ${({ theme }) => theme.spacing(5)};
+ height: ${({ theme }) => theme.spacing(4)};
justify-content: center;
user-select: none;
- width: ${({ theme }) => theme.spacing(6)};
-
- &:hover {
- background: ${({ theme }) => theme.background.quaternary};
- }
+ width: ${({ theme }) => theme.spacing(4)};
`;
type NavigationDrawerCollapseButtonProps = {
diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx
index d98fc547091c..65944df37e5d 100644
--- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx
+++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerItem.tsx
@@ -142,7 +142,6 @@ const StyledKeyBoardShortcut = styled.div`
const StyledNavigationDrawerItemContainer = styled.div`
display: flex;
- flex-grow: 1;
width: 100%;
`;
diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSection.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSection.tsx
index 2ba98503329b..afc7fe803744 100644
--- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSection.tsx
+++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerSection.tsx
@@ -6,6 +6,8 @@ const StyledSection = styled.div`
gap: ${({ theme }) => theme.betweenSiblingsGap};
width: 100%;
margin-bottom: ${({ theme }) => theme.spacing(3)};
+ flex-shrink: 1;
+ overflow: hidden;
`;
export { StyledSection as NavigationDrawerSection };
diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx b/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx
index d4e5c2e79f78..97d4560694fd 100644
--- a/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx
+++ b/packages/twenty-front/src/modules/ui/utilities/scroll/components/ScrollWrapper.tsx
@@ -26,16 +26,16 @@ const StyledScrollWrapper = styled.div`
export type ScrollWrapperProps = {
children: React.ReactNode;
className?: string;
- hideY?: boolean;
- hideX?: boolean;
+ enableXScroll?: boolean;
+ enableYScroll?: boolean;
contextProviderName: ContextProviderName;
};
export const ScrollWrapper = ({
children,
className,
- hideX,
- hideY,
+ enableXScroll = true,
+ enableYScroll = true,
contextProviderName,
}: ScrollWrapperProps) => {
const scrollableRef = useRef(null);
@@ -58,8 +58,8 @@ export const ScrollWrapper = ({
options: {
scrollbars: { autoHide: 'scroll' },
overflow: {
- y: hideY ? 'hidden' : undefined,
- x: hideX ? 'hidden' : undefined,
+ x: enableXScroll ? undefined : 'hidden',
+ y: enableYScroll ? undefined : 'hidden',
},
},
events: {
diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx b/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx
index b6f7d2103a60..1d83d1ddd94b 100644
--- a/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx
+++ b/packages/twenty-front/src/modules/ui/utilities/scroll/contexts/ScrollWrapperContexts.tsx
@@ -17,7 +17,8 @@ export type ContextProviderName =
| 'tabList'
| 'releases'
| 'test'
- | 'showPageActivityContainer';
+ | 'showPageActivityContainer'
+ | 'navigationDrawer';
const createScrollWrapperContext = (id: string) =>
createContext({
@@ -47,6 +48,8 @@ export const ReleasesScrollWrapperContext =
createScrollWrapperContext('releases');
export const ShowPageActivityContainerScrollWrapperContext =
createScrollWrapperContext('showPageActivityContainer');
+export const NavigationDrawerScrollWrapperContext =
+ createScrollWrapperContext('navigationDrawer');
export const TestScrollWrapperContext = createScrollWrapperContext('test');
export const getContextByProviderName = (
@@ -77,6 +80,8 @@ export const getContextByProviderName = (
return TestScrollWrapperContext;
case 'showPageActivityContainer':
return ShowPageActivityContainerScrollWrapperContext;
+ case 'navigationDrawer':
+ return NavigationDrawerScrollWrapperContext;
default:
throw new Error('Context Provider not available');
}
diff --git a/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx b/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx
index 4c51e6da446d..c41b7ff7e6cd 100644
--- a/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx
+++ b/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx
@@ -7,6 +7,8 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState';
import { workspacesState } from '@/auth/states/workspaces';
+import { DateFormat } from '@/localization/constants/DateFormat';
+import { TimeFormat } from '@/localization/constants/TimeFormat';
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
@@ -81,10 +83,10 @@ export const UserProviderEffect = () => {
: detectTimeZone(),
dateFormat: isDefined(workspaceMember.dateFormat)
? getDateFormatFromWorkspaceDateFormat(workspaceMember.dateFormat)
- : detectDateFormat(),
+ : DateFormat[detectDateFormat()],
timeFormat: isDefined(workspaceMember.timeFormat)
? getTimeFormatFromWorkspaceTimeFormat(workspaceMember.timeFormat)
- : detectTimeFormat(),
+ : TimeFormat[detectTimeFormat()],
});
}
diff --git a/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx b/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx
index 1b55e6a32790..55c2a77f3c59 100644
--- a/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx
+++ b/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx
@@ -40,6 +40,7 @@ const StyledChip = styled.div<{ variant: SortOrFitlerChipVariant }>`
font-size: ${({ theme }) => theme.font.size.sm};
font-weight: ${({ theme }) => theme.font.weight.medium};
padding: ${({ theme }) => theme.spacing(0.5) + ' ' + theme.spacing(2)};
+ margin-left: ${({ theme }) => theme.spacing(2)};
user-select: none;
white-space: nowrap;
diff --git a/packages/twenty-front/src/modules/views/components/ViewBar.tsx b/packages/twenty-front/src/modules/views/components/ViewBar.tsx
index 724b80e461c3..186f54b691ba 100644
--- a/packages/twenty-front/src/modules/views/components/ViewBar.tsx
+++ b/packages/twenty-front/src/modules/views/components/ViewBar.tsx
@@ -60,7 +60,6 @@ export const ViewBar = ({
leftComponent={
loading ? :
}
- displayBottomBorder={false}
rightComponent={
<>
theme.spacing(6)};
@@ -37,6 +37,7 @@ type WorkflowEditActionFormSendEmailProps =
type SendEmailFormData = {
connectedAccountId: string;
+ email: string;
subject: string;
body: string;
};
@@ -53,6 +54,7 @@ export const WorkflowEditActionFormSendEmail = (
const form = useForm({
defaultValues: {
connectedAccountId: '',
+ email: '',
subject: '',
body: '',
},
@@ -83,10 +85,11 @@ export const WorkflowEditActionFormSendEmail = (
useEffect(() => {
form.setValue(
'connectedAccountId',
- props.action.settings.connectedAccountId ?? '',
+ props.action.settings.input.connectedAccountId ?? '',
);
- form.setValue('subject', props.action.settings.subject ?? '');
- form.setValue('body', props.action.settings.body ?? '');
+ form.setValue('email', props.action.settings.input.email ?? '');
+ form.setValue('subject', props.action.settings.input.subject ?? '');
+ form.setValue('body', props.action.settings.input.body ?? '');
}, [props.action.settings, form]);
const saveAction = useDebouncedCallback(
@@ -99,9 +102,12 @@ export const WorkflowEditActionFormSendEmail = (
...props.action,
settings: {
...props.action.settings,
- connectedAccountId: formData.connectedAccountId,
- subject: formData.subject,
- body: formData.body,
+ input: {
+ connectedAccountId: formData.connectedAccountId,
+ email: formData.email,
+ subject: formData.subject,
+ body: formData.body,
+ },
},
});
@@ -134,12 +140,12 @@ export const WorkflowEditActionFormSendEmail = (
};
if (
- isDefined(props.action.settings.connectedAccountId) &&
- props.action.settings.connectedAccountId !== ''
+ isDefined(props.action.settings.input.connectedAccountId) &&
+ props.action.settings.input.connectedAccountId !== ''
) {
filter.or.push({
id: {
- eq: props.action.settings.connectedAccountId,
+ eq: props.action.settings.input.connectedAccountId,
},
});
}
@@ -198,6 +204,21 @@ export const WorkflowEditActionFormSendEmail = (
/>
)}
/>
+ (
+ {
+ field.onChange(email);
+ handleSave();
+ }}
+ />
+ )}
+ />
{
@@ -66,7 +66,9 @@ export const WorkflowEditActionFormServerlessFunction = (
...props.action,
settings: {
...props.action.settings,
- serverlessFunctionId: updatedFunction,
+ input: {
+ serverlessFunctionId: updatedFunction,
+ },
},
});
}}
diff --git a/packages/twenty-front/src/modules/workflow/types/Workflow.ts b/packages/twenty-front/src/modules/workflow/types/Workflow.ts
index 0ed8422846b9..65b2e9a25a15 100644
--- a/packages/twenty-front/src/modules/workflow/types/Workflow.ts
+++ b/packages/twenty-front/src/modules/workflow/types/Workflow.ts
@@ -10,13 +10,18 @@ type BaseWorkflowStepSettings = {
};
export type WorkflowCodeStepSettings = BaseWorkflowStepSettings & {
- serverlessFunctionId: string;
+ input: {
+ serverlessFunctionId: string;
+ };
};
export type WorkflowSendEmailStepSettings = BaseWorkflowStepSettings & {
- connectedAccountId: string;
- subject?: string;
- body?: string;
+ input: {
+ connectedAccountId: string;
+ email: string;
+ subject?: string;
+ body?: string;
+ };
};
type BaseWorkflowStep = {
diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/addCreateStepNodes.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/addCreateStepNodes.test.ts
index d0f46dde3576..c2c9d760fe15 100644
--- a/packages/twenty-front/src/modules/workflow/utils/__tests__/addCreateStepNodes.test.ts
+++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/addCreateStepNodes.test.ts
@@ -21,7 +21,9 @@ describe('addCreateStepNodes', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
},
{
@@ -34,7 +36,9 @@ describe('addCreateStepNodes', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
},
];
diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts
index ebf4f3c21020..663e1ef5661c 100644
--- a/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts
+++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/generateWorkflowDiagram.test.ts
@@ -42,7 +42,9 @@ describe('generateWorkflowDiagram', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
},
{
@@ -55,7 +57,9 @@ describe('generateWorkflowDiagram', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
},
];
@@ -96,7 +100,9 @@ describe('generateWorkflowDiagram', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
},
{
@@ -109,7 +115,9 @@ describe('generateWorkflowDiagram', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
},
];
diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/getWorkflowVersionDiagram.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/getWorkflowVersionDiagram.test.ts
index 907524a725e7..9c10c2af4f9f 100644
--- a/packages/twenty-front/src/modules/workflow/utils/__tests__/getWorkflowVersionDiagram.test.ts
+++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/getWorkflowVersionDiagram.test.ts
@@ -80,7 +80,9 @@ describe('getWorkflowVersionDiagram', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
type: 'CODE',
valid: true,
diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/insertStep.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/insertStep.test.ts
index ba93aa6c341e..b264d5edafbc 100644
--- a/packages/twenty-front/src/modules/workflow/utils/__tests__/insertStep.test.ts
+++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/insertStep.test.ts
@@ -25,7 +25,9 @@ describe('insertStep', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
type: 'CODE',
valid: true,
@@ -63,7 +65,9 @@ describe('insertStep', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
type: 'CODE',
valid: true,
@@ -95,7 +99,9 @@ describe('insertStep', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
type: 'CODE',
valid: true,
@@ -108,7 +114,9 @@ describe('insertStep', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
type: 'CODE',
valid: true,
@@ -129,7 +137,9 @@ describe('insertStep', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
type: 'CODE',
valid: true,
@@ -165,7 +175,9 @@ describe('insertStep', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
type: 'CODE',
valid: true,
@@ -178,7 +190,9 @@ describe('insertStep', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
type: 'CODE',
valid: true,
@@ -199,7 +213,9 @@ describe('insertStep', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
type: 'CODE',
valid: true,
diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/removeStep.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/removeStep.test.ts
index 01385411bf02..349d2f74200b 100644
--- a/packages/twenty-front/src/modules/workflow/utils/__tests__/removeStep.test.ts
+++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/removeStep.test.ts
@@ -10,7 +10,9 @@ it('returns a deep copy of the provided steps array instead of mutating it', ()
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'first',
+ input: {
+ serverlessFunctionId: 'first',
+ },
},
type: 'CODE',
valid: true,
@@ -47,7 +49,9 @@ it('removes a step in a non-empty steps array', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
type: 'CODE',
valid: true,
@@ -67,7 +71,9 @@ it('removes a step in a non-empty steps array', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
type: 'CODE',
valid: true,
@@ -81,7 +87,9 @@ it('removes a step in a non-empty steps array', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
type: 'CODE',
valid: true,
diff --git a/packages/twenty-front/src/modules/workflow/utils/__tests__/replaceStep.test.ts b/packages/twenty-front/src/modules/workflow/utils/__tests__/replaceStep.test.ts
index 93286c59013e..41e4f8cbae8b 100644
--- a/packages/twenty-front/src/modules/workflow/utils/__tests__/replaceStep.test.ts
+++ b/packages/twenty-front/src/modules/workflow/utils/__tests__/replaceStep.test.ts
@@ -11,7 +11,9 @@ describe('replaceStep', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'first',
+ input: {
+ serverlessFunctionId: 'first',
+ },
},
type: 'CODE',
valid: true,
@@ -39,7 +41,9 @@ describe('replaceStep', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'second',
+ input: {
+ serverlessFunctionId: 'second',
+ },
},
},
stepId: stepToBeReplaced.id,
@@ -57,7 +61,9 @@ describe('replaceStep', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
type: 'CODE',
valid: true,
@@ -77,7 +83,9 @@ describe('replaceStep', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
type: 'CODE',
valid: true,
@@ -91,7 +99,9 @@ describe('replaceStep', () => {
retryOnFailure: { value: true },
continueOnFailure: { value: false },
},
- serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ input: {
+ serverlessFunctionId: 'a5434be2-c10b-465c-acec-46492782a997',
+ },
},
type: 'CODE',
valid: true,
diff --git a/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts b/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts
index 48e8f9bd448f..fd63158d2548 100644
--- a/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts
+++ b/packages/twenty-front/src/modules/workflow/utils/getStepDefaultDefinition.ts
@@ -14,7 +14,9 @@ export const getStepDefaultDefinition = (
type: 'CODE',
valid: false,
settings: {
- serverlessFunctionId: '',
+ input: {
+ serverlessFunctionId: '',
+ },
errorHandlingOptions: {
continueOnFailure: {
value: false,
@@ -33,9 +35,12 @@ export const getStepDefaultDefinition = (
type: 'SEND_EMAIL',
valid: false,
settings: {
- connectedAccountId: '',
- subject: '',
- body: '',
+ input: {
+ connectedAccountId: '',
+ email: '',
+ subject: '',
+ body: '',
+ },
errorHandlingOptions: {
continueOnFailure: {
value: false,
diff --git a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts
index cde44977047b..5471c5d4d59e 100644
--- a/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts
+++ b/packages/twenty-front/src/modules/workspace/types/FeatureFlagKey.ts
@@ -10,9 +10,7 @@ export type FeatureFlagKey =
| 'IS_MESSAGE_THREAD_SUBSCRIBER_ENABLED'
| 'IS_WORKFLOW_ENABLED'
| 'IS_WORKSPACE_FAVORITE_ENABLED'
- | 'IS_SEARCH_ENABLED'
| 'IS_QUERY_RUNNER_TWENTY_ORM_ENABLED'
| 'IS_GMAIL_SEND_EMAIL_SCOPE_ENABLED'
- | 'IS_WORKSPACE_MIGRATED_FOR_SEARCH'
| 'IS_ANALYTICS_V2_ENABLED'
| 'IS_UNIQUE_INDEXES_ENABLED';
diff --git a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx
index edb11865dcda..44eb6f203b25 100644
--- a/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx
+++ b/packages/twenty-front/src/pages/object-record/RecordIndexPage.tsx
@@ -61,6 +61,7 @@ export const RecordIndexPage = () => {
recordIndexId,
objectNamePlural,
objectNameSingular,
+ objectMetadataItem,
onIndexRecordsLoaded: handleIndexRecordsLoaded,
onIndexIdentifierClick: handleIndexIdentifierClick,
onCreateRecord: handleCreateRecord,
diff --git a/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx b/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx
index f88ba926c6b8..006a4db6a88b 100644
--- a/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx
+++ b/packages/twenty-front/src/pages/settings/developers/webhooks/components/SettingsDevelopersWebhookDetail.tsx
@@ -3,6 +3,7 @@ import { useState } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { H2Title, IconTrash } from 'twenty-ui';
+import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabledState';
import { useObjectMetadataItems } from '@/object-metadata/hooks/useObjectMetadataItems';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
@@ -11,7 +12,7 @@ import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { Webhook } from '@/settings/developers/types/webhook/Webhook';
-import { SettingsDeveloppersWebhookUsageGraph } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph';
+import { SettingsDevelopersWebhookUsageGraph } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraph';
import { SettingsDevelopersWebhookUsageGraphEffect } from '@/settings/developers/webhook/components/SettingsDevelopersWebhookUsageGraphEffect';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath';
@@ -23,6 +24,7 @@ import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModa
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
+import { useRecoilValue } from 'recoil';
const StyledFilterRow = styled.div`
display: flex;
@@ -32,6 +34,8 @@ const StyledFilterRow = styled.div`
export const SettingsDevelopersWebhooksDetail = () => {
const { objectMetadataItems } = useObjectMetadataItems();
+ const isAnalyticsEnabled = useRecoilValue(isAnalyticsEnabledState);
+
const navigate = useNavigate();
const { webhookId = '' } = useParams();
@@ -178,10 +182,10 @@ export const SettingsDevelopersWebhooksDetail = () => {
/>
- {isAnalyticsV2Enabled ? (
+ {isAnalyticsEnabled && isAnalyticsV2Enabled ? (
<>
-
+
>
) : (
<>>
diff --git a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettings.tsx b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettings.tsx
index 6a2e58e52c1f..c1c20f324995 100644
--- a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettings.tsx
+++ b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettings.tsx
@@ -77,7 +77,7 @@ export const DateTimeSettings = () => {
);
dateTime[settingName] =
(value as DateFormat) === DateFormat.SYSTEM
- ? detectDateFormat()
+ ? DateFormat[detectDateFormat()]
: (value as DateFormat);
break;
}
@@ -87,7 +87,7 @@ export const DateTimeSettings = () => {
);
dateTime[settingName] =
(value as TimeFormat) === TimeFormat.SYSTEM
- ? detectTimeFormat()
+ ? TimeFormat[detectTimeFormat()]
: (value as TimeFormat);
break;
}
diff --git a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsDateFormatSelect.tsx b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsDateFormatSelect.tsx
index dcadd5ba9460..2957051875aa 100644
--- a/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsDateFormatSelect.tsx
+++ b/packages/twenty-front/src/pages/settings/profile/appearance/components/DateTimeSettingsDateFormatSelect.tsx
@@ -20,7 +20,7 @@ export const DateTimeSettingsDateFormatSelect = ({
const usedTimeZone = timeZone === 'system' ? systemTimeZone : timeZone;
- const systemDateFormat = detectDateFormat();
+ const systemDateFormat = DateFormat[detectDateFormat()];
return (