diff --git a/.dockerignore b/.dockerignore
index 285200a8b127..f9fc785f92da 100644
--- a/.dockerignore
+++ b/.dockerignore
@@ -1,2 +1,4 @@
-server/node_modules/
-server/.env
\ No newline at end of file
+.git
+.env
+node_modules
+.nx/cache
diff --git a/package.json b/package.json
index 8f43c73685d3..312f4a45e691 100644
--- a/package.json
+++ b/package.json
@@ -5,8 +5,8 @@
"@apollo/server": "^4.7.3",
"@aws-sdk/client-s3": "^3.363.0",
"@aws-sdk/credential-providers": "^3.363.0",
- "@blocknote/core": "^0.11.2",
- "@blocknote/react": "^0.11.2",
+ "@blocknote/core": "^0.12.1",
+ "@blocknote/react": "^0.12.2",
"@chakra-ui/accordion": "^2.3.0",
"@chakra-ui/system": "^2.6.0",
"@codesandbox/sandpack-react": "^2.13.5",
@@ -108,6 +108,7 @@
"libphonenumber-js": "^1.10.26",
"lodash.camelcase": "^4.3.0",
"lodash.debounce": "^4.0.8",
+ "lodash.groupby": "^4.6.0",
"lodash.isempty": "^4.4.0",
"lodash.isequal": "^4.5.0",
"lodash.isobject": "^3.0.2",
@@ -229,6 +230,7 @@
"@types/js-cookie": "^3.0.3",
"@types/lodash.camelcase": "^4.3.7",
"@types/lodash.debounce": "^4.0.7",
+ "@types/lodash.groupby": "^4.6.9",
"@types/lodash.isempty": "^4.4.7",
"@types/lodash.isequal": "^4.5.7",
"@types/lodash.isobject": "^3.0.7",
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/BackgroundDark.ts b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/BackgroundDark.ts
index 5a7802dfb287..d4687782b729 100644
--- a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/BackgroundDark.ts
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/BackgroundDark.ts
@@ -19,6 +19,7 @@ export const BACKGROUND_DARK = {
light: RGBA(GRAY_SCALE.gray0, 0.06),
lighter: RGBA(GRAY_SCALE.gray0, 0.03),
danger: RGBA(COLOR.red, 0.08),
+ forBackdropFilter: RGBA(GRAY_SCALE.gray80, 0.5),
},
overlay: RGBA(GRAY_SCALE.gray80, 0.8),
radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`,
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/BackgroundLight.ts b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/BackgroundLight.ts
index a096887d16ba..e72ff80f98f7 100644
--- a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/BackgroundLight.ts
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/BackgroundLight.ts
@@ -19,6 +19,7 @@ export const BACKGROUND_LIGHT = {
light: RGBA(GRAY_SCALE.gray100, 0.04),
lighter: RGBA(GRAY_SCALE.gray100, 0.02),
danger: RGBA(COLOR.red, 0.08),
+ forBackdropFilter: RGBA(GRAY_SCALE.gray10, 0.5),
},
overlay: RGBA(GRAY_SCALE.gray80, 0.8),
radialGradient: `radial-gradient(50% 62.62% at 50% 0%, #505050 0%, ${GRAY_SCALE.gray60} 100%)`,
diff --git a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/OverlayBackground.ts b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/OverlayBackground.ts
index 50fe3a0ef8db..e23e08f33d79 100644
--- a/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/OverlayBackground.ts
+++ b/packages/twenty-chrome-extension/src/options/modules/ui/theme/constants/OverlayBackground.ts
@@ -3,7 +3,7 @@ import { css } from '@emotion/react';
import { ThemeType } from '@/ui/theme/constants/ThemeLight';
export const OVERLAY_BACKGROUND = (props: { theme: ThemeType }) => css`
- backdrop-filter: blur(8px);
- background: ${props.theme.background.transparent.secondary};
+ backdrop-filter: blur(12px) saturate(200%) contrast(50%) brightness(130%);
+ background: ${props.theme.background.transparent.forBackdropFilter};
box-shadow: ${props.theme.boxShadow.strong};
`;
diff --git a/packages/twenty-docker/Makefile b/packages/twenty-docker/Makefile
index 6e3324e5482b..e4510047ba9a 100644
--- a/packages/twenty-docker/Makefile
+++ b/packages/twenty-docker/Makefile
@@ -29,6 +29,12 @@ prod-docs-build:
prod-docs-run:
@docker run -d -p 3000:3000 --name twenty-docs twenty-docs
+prod-build:
+ @cd ../.. && docker build -f ./packages/twenty-docker/prod/twenty/Dockerfile --tag twenty . && cd -
+
+prod-run:
+ @docker run -d -p 3000:3000 --name twenty twenty
+
prod-front-build:
@cd ../.. && docker build -f ./packages/twenty-docker/prod/twenty-front/Dockerfile --tag twenty-front . && cd -
diff --git a/packages/twenty-docker/prod/.env.example b/packages/twenty-docker/prod/.env.example
new file mode 100644
index 000000000000..03628af3f815
--- /dev/null
+++ b/packages/twenty-docker/prod/.env.example
@@ -0,0 +1,16 @@
+TAG=latest
+
+POSTGRES_ADMIN_PASSWORD=replace_me_with_a_strong_password
+
+PG_DATABASE_HOST=db:5432
+
+SERVER_URL=http://localhost:3000
+# Uncoment if you are serving your front on another server than the API (eg. bucket)
+# FRONT_BASE_URL=http://localhost:3000
+
+# Use openssl rand -base64 32 for each secret
+# ACCESS_TOKEN_SECRET=replace_me_with_a_random_string_access
+# LOGIN_TOKEN_SECRET=replace_me_with_a_random_string_login
+# REFRESH_TOKEN_SECRET=replace_me_with_a_random_string_refresh
+
+SIGN_IN_PREFILLED=true
diff --git a/packages/twenty-docker/prod/docker-compose.yml b/packages/twenty-docker/prod/docker-compose.yml
new file mode 100644
index 000000000000..887b820c31d2
--- /dev/null
+++ b/packages/twenty-docker/prod/docker-compose.yml
@@ -0,0 +1,52 @@
+version: '3.8'
+name: twenty
+
+services:
+ server:
+ image: twentycrm/twenty:${TAG}
+ volumes:
+ - server-local-data:/app/.local-storage
+ ports:
+ - "3000:3000"
+ environment:
+ PORT: 3000
+ PG_DATABASE_URL: postgres://twenty:twenty@${PG_DATABASE_HOST}/default
+ SERVER_URL: ${SERVER_URL}
+ FRONT_BASE_URL: ${FRONT_BASE_URL:-$SERVER_URL}
+
+ ENABLE_DB_MIGRATIONS: true
+
+ SIGN_IN_PREFILLED: ${SIGN_IN_PREFILLED}
+ STORAGE_TYPE: local
+ STORAGE_LOCAL_PATH: .local-storage
+ ACCESS_TOKEN_SECRET: ${ACCESS_TOKEN_SECRET}
+ LOGIN_TOKEN_SECRET: ${LOGIN_TOKEN_SECRET}
+ REFRESH_TOKEN_SECRET: ${REFRESH_TOKEN_SECRET}
+ depends_on:
+ db:
+ condition: service_healthy
+ healthcheck:
+ test: ["CMD-SHELL", "curl --silent --fail http://localhost:3000/healthz | jq -e '.status == \"ok\"' > /dev/null || exit 1"]
+ interval: 5s
+ timeout: 5s
+ retries: 10
+ restart: always
+
+ db:
+ image: twentycrm/twenty-postgres:${TAG}
+ volumes:
+ - db-data:/var/lib/postgresql/data
+ environment:
+ POSTGRES_PASSWORD: ${POSTGRES_ADMIN_PASSWORD}
+ #POSTGRES_USER: ${POSTGRES_USER}
+ #POSTGRES_DB: ${POSTGRES_DB}
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U twenty -d default"]
+ interval: 5s
+ timeout: 5s
+ retries: 10
+ restart: always
+
+volumes:
+ db-data:
+ server-local-data:
diff --git a/packages/twenty-docker/prod/twenty-front/Dockerfile b/packages/twenty-docker/prod/twenty-front/Dockerfile
index d3edf786d3e6..7480b7e1c3a3 100644
--- a/packages/twenty-docker/prod/twenty-front/Dockerfile
+++ b/packages/twenty-docker/prod/twenty-front/Dockerfile
@@ -1,14 +1,13 @@
FROM node:18.17.1-alpine as twenty-front-build
ARG REACT_APP_SERVER_BASE_URL
-ARG REACT_APP_SERVER_AUTH_URL
-ARG REACT_APP_SERVER_FILES_URL
WORKDIR /app
COPY ./package.json .
COPY ./yarn.lock .
COPY ./.yarnrc.yml .
+COPY ./nx.json .
COPY ./tsconfig.base.json .
COPY ./.yarn/releases /app/.yarn/releases
COPY ./tools/eslint-rules /app/tools/eslint-rules
diff --git a/packages/twenty-docker/prod/twenty-server/Dockerfile b/packages/twenty-docker/prod/twenty-server/Dockerfile
index 954e1c39779e..12d281538860 100644
--- a/packages/twenty-docker/prod/twenty-server/Dockerfile
+++ b/packages/twenty-docker/prod/twenty-server/Dockerfile
@@ -1,4 +1,4 @@
-FROM node:18.16.0-alpine as twenty-server
+FROM node:18.16.0-alpine as twenty-server-build
WORKDIR /app
@@ -10,12 +10,22 @@ COPY ./nx.json .
COPY ./.yarn/releases /app/.yarn/releases
COPY ./packages/twenty-emails /app/packages/twenty-emails
COPY ./packages/twenty-server /app/packages/twenty-server
-RUN yarn workspaces focus twenty-emails twenty-server
+RUN yarn
+
RUN npx nx reset
+RUN npx nx run twenty-server:build
+RUN mv /app/packages/twenty-server/dist /app/packages/twenty-server/build
RUN npx nx run twenty-server:build:packageJson
RUN mv /app/packages/twenty-server/dist/package.json /app/packages/twenty-server/package.json
-RUN yarn workspaces focus twenty-emails twenty-server
-RUN npx nx run twenty-server:build
+RUN rm -rf /app/packages/twenty-server/dist
+RUN mv /app/packages/twenty-server/build /app/packages/twenty-server/dist
+
+WORKDIR /app
+RUN yarn workspaces focus --production twenty-emails twenty-server
+
+FROM node:18.17.1-alpine as twenty-server
+
+COPY --from=twenty-server-build /app /app
WORKDIR /app/packages/twenty-server
diff --git a/packages/twenty-docker/prod/twenty/Dockerfile b/packages/twenty-docker/prod/twenty/Dockerfile
new file mode 100644
index 000000000000..526e578c2df4
--- /dev/null
+++ b/packages/twenty-docker/prod/twenty/Dockerfile
@@ -0,0 +1,77 @@
+# Base image for common dependencies
+FROM node:18.17.1-alpine as common-deps
+
+WORKDIR /app
+
+# Copy only the necessary files for dependency resolution
+COPY ./package.json ./yarn.lock ./.yarnrc.yml ./tsconfig.base.json ./nx.json /app/
+COPY ./.yarn/releases /app/.yarn/releases
+
+COPY ./packages/twenty-emails/package.json /app/packages/twenty-emails/
+COPY ./packages/twenty-server/package.json /app/packages/twenty-server/
+COPY ./packages/twenty-server/patches /app/packages/twenty-server/patches
+COPY ./packages/twenty-ui/package.json /app/packages/twenty-ui/
+COPY ./packages/twenty-front/package.json /app/packages/twenty-front/
+
+# Install all dependencies
+RUN yarn && yarn cache clean && npx nx reset
+
+
+# Build the back
+FROM common-deps as twenty-server-build
+
+# Copy sourcecode after installing dependences to accelerate subsequents builds
+COPY ./packages/twenty-emails /app/packages/twenty-emails
+COPY ./packages/twenty-server /app/packages/twenty-server
+
+RUN npx nx run twenty-server:build && \
+ mv /app/packages/twenty-server/dist /app/packages/twenty-server/build && \
+ npx nx run twenty-server:build:packageJson && \
+ mv /app/packages/twenty-server/dist/package.json /app/packages/twenty-server/package.json && \
+ rm -rf /app/packages/twenty-server/dist && \
+ mv /app/packages/twenty-server/build /app/packages/twenty-server/dist
+
+RUN yarn workspaces focus --production twenty-emails twenty-server
+
+
+# Build the front
+FROM common-deps as twenty-front-build
+
+ARG REACT_APP_SERVER_BASE_URL
+
+COPY ./packages/twenty-front /app/packages/twenty-front
+COPY ./packages/twenty-ui /app/packages/twenty-ui
+RUN yarn nx build twenty-front
+
+
+# Final stage: Run the application
+FROM node:18.17.1-alpine as twenty
+
+# Used to run healthcheck in docker
+RUN apk add --no-cache curl jq
+
+COPY ./packages/twenty-docker/prod/twenty/entrypoint.sh /app/entrypoint.sh
+RUN chmod +x /app/entrypoint.sh
+
+WORKDIR /app/packages/twenty-server
+
+ARG REACT_APP_SERVER_BASE_URL
+ENV REACT_APP_SERVER_BASE_URL $REACT_APP_SERVER_BASE_URL
+
+# Copy built applications from previous stages
+COPY --chown=1000 --from=twenty-server-build /app /app
+COPY --chown=1000 --from=twenty-server-build /app/packages/twenty-server /app/packages/twenty-server
+COPY --chown=1000 --from=twenty-front-build /app/packages/twenty-front/build /app/packages/twenty-server/dist/front
+
+# Set metadata and labels
+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 backend and frontend, ensuring it deploys faster and runs the same way regardless of the deployment environment."
+
+RUN mkdir /app/.local-storage
+RUN chown -R 1000 /app
+
+# Use non root user with uid 1000
+USER 1000
+
+CMD ["node", "dist/src/main"]
+ENTRYPOINT ["/app/entrypoint.sh"]
diff --git a/packages/twenty-docker/prod/twenty/entrypoint.sh b/packages/twenty-docker/prod/twenty/entrypoint.sh
new file mode 100755
index 000000000000..a6167afcae5b
--- /dev/null
+++ b/packages/twenty-docker/prod/twenty/entrypoint.sh
@@ -0,0 +1,17 @@
+#!/bin/sh
+
+# Check if the initialization has already been done and that we enabled automatic migration
+if [ "${ENABLE_DB_MIGRATIONS}" = "true" ] && [ ! -f /app/${STORAGE_LOCAL_PATH}/db_initialized ]; then
+ echo "Running database setup and migrations..."
+
+ # Run setup and migration scripts
+ npx ts-node ./scripts/setup-db.ts
+ yarn database:migrate:prod
+
+ # Mark initialization as done
+ echo "Successfuly migrated DB!"
+ touch /app/${STORAGE_LOCAL_PATH}/db_initialized
+fi
+
+# Continue with the original Docker command
+exec "$@"
diff --git a/packages/twenty-docs/docs/start/local-setup/troubleshooting.mdx b/packages/twenty-docs/docs/start/local-setup/troubleshooting.mdx
index 72eccba95ced..fba34447f57d 100644
--- a/packages/twenty-docs/docs/start/local-setup/troubleshooting.mdx
+++ b/packages/twenty-docs/docs/start/local-setup/troubleshooting.mdx
@@ -41,3 +41,7 @@ This should work out of the box with the eslint extension installed. If this doe
"source.fixAll.eslint": "explicit"
}
```
+
+## Docker container build
+
+To successfully build Docker images, ensure that your system has a minimum of 2GB of memory available. For users of Docker Desktop, please verify that you've allocated sufficient resources to Docker within the application's settings.
diff --git a/packages/twenty-docs/docs/start/self-hosting/cloud-providers.mdx b/packages/twenty-docs/docs/start/self-hosting/cloud-providers.mdx
index 8250dfece6a8..0eae5ff1a405 100644
--- a/packages/twenty-docs/docs/start/self-hosting/cloud-providers.mdx
+++ b/packages/twenty-docs/docs/start/self-hosting/cloud-providers.mdx
@@ -229,14 +229,6 @@ resource "azurerm_container_app" "twenty_front" {
name = "REACT_APP_SERVER_BASE_URL"
value = "https://${azurerm_container_app.twenty_server.ingress[0].fqdn}"
}
- env {
- name = "REACT_APP_SERVER_AUTH_URL"
- value = "https://${azurerm_container_app.twenty_server.ingress[0].fqdn}/auth"
- }
- env {
- name = "REACT_APP_SERVER_FILES_URL"
- value = "https://${azurerm_container_app.twenty_server.ingress[0].fqdn}/files"
- }
}
}
}
diff --git a/packages/twenty-docs/docs/start/self-hosting/docker-compose.mdx b/packages/twenty-docs/docs/start/self-hosting/docker-compose.mdx
index 754cb99dc7a6..a5594f7edf5b 100644
--- a/packages/twenty-docs/docs/start/self-hosting/docker-compose.mdx
+++ b/packages/twenty-docs/docs/start/self-hosting/docker-compose.mdx
@@ -24,7 +24,7 @@ REFRESH_TOKEN_SECRET=replace_me_with_a_random_string_refresh
### Not able to login
-If you encounter errors, (not able to log into the application after inputting an email) after the inital setup, try running `docker exec -it twenty_backend_1 yarn database:reset` and see if that solves your issue.
+If you encounter errors, (not able to log into the application after inputting an email) after the inital setup, try running `docker exec -it twenty-backend-1 yarn database:reset` and see if that solves your issue.
### Cannot connect to server, running behind a reverse proxy
@@ -35,7 +35,7 @@ Complete step three and four with :
## Production docker containers
-Prebuilt images for both Postgres, frontend, and back-end can be found on [docker hub](https://hub.docker.com/r/twentycrm/).
+Prebuilt images for both Postgres, frontend, and back-end can be found on [docker hub](https://hub.docker.com/r/twentycrm/). Note that the Postgres container will not persist data if your server is not configured to be stateful (for example Heroku). You probably want to configure a special stateful resource for the database.
## Environment Variables
@@ -59,8 +59,6 @@ services:
environment:
- SIGN_IN_PREFILLED=${SIGN_IN_PREFILLED}
- REACT_APP_SERVER_BASE_URL=${LOCAL_SERVER_URL}
- - REACT_APP_SERVER_AUTH_URL=${LOCAL_SERVER_URL}/auth
- - REACT_APP_SERVER_FILES_URL=${LOCAL_SERVER_URL}/files
depends_on:
- backend
diff --git a/packages/twenty-docs/docs/start/self-hosting/self-hosting.mdx b/packages/twenty-docs/docs/start/self-hosting/self-hosting.mdx
index 343c7aca0ed0..29044fa58588 100644
--- a/packages/twenty-docs/docs/start/self-hosting/self-hosting.mdx
+++ b/packages/twenty-docs/docs/start/self-hosting/self-hosting.mdx
@@ -23,8 +23,6 @@ import TabItem from '@theme/TabItem';
diff --git a/packages/twenty-front/.env.example b/packages/twenty-front/.env.example
index e39840f2f67c..423b00b0733b 100644
--- a/packages/twenty-front/.env.example
+++ b/packages/twenty-front/.env.example
@@ -2,6 +2,4 @@ REACT_APP_SERVER_BASE_URL=http://localhost:3000
GENERATE_SOURCEMAP=false
# ———————— Optional ————————
-# REACT_APP_SERVER_AUTH_URL=http://localhost:3000/auth
-# REACT_APP_SERVER_FILES_URL=http://localhost:3000/files
# CHROMATIC_PROJECT_TOKEN=
diff --git a/packages/twenty-front/nyc.config.cjs b/packages/twenty-front/nyc.config.cjs
index bf92d9cde36d..0ee161423bb3 100644
--- a/packages/twenty-front/nyc.config.cjs
+++ b/packages/twenty-front/nyc.config.cjs
@@ -6,7 +6,7 @@ const globalCoverage = {
};
const modulesCoverage = {
- statements: 75,
+ statements: 70,
lines: 75,
functions: 70,
include: ['src/modules/**/*'],
diff --git a/packages/twenty-front/src/App.tsx b/packages/twenty-front/src/App.tsx
index 81b665165c1c..71f0e211c8f8 100644
--- a/packages/twenty-front/src/App.tsx
+++ b/packages/twenty-front/src/App.tsx
@@ -47,7 +47,7 @@ import { SettingsWorkspaceMembers } from '~/pages/settings/SettingsWorkspaceMemb
import { Tasks } from '~/pages/tasks/Tasks';
export const App = () => {
- const billing = useRecoilValue(billingState());
+ const billing = useRecoilValue(billingState);
return (
<>
diff --git a/packages/twenty-front/src/config/index.ts b/packages/twenty-front/src/config/index.ts
index f128fd760f79..4dbb196252fd 100644
--- a/packages/twenty-front/src/config/index.ts
+++ b/packages/twenty-front/src/config/index.ts
@@ -28,13 +28,3 @@ export const REACT_APP_SERVER_BASE_URL =
window._env_?.REACT_APP_SERVER_BASE_URL ||
process.env.REACT_APP_SERVER_BASE_URL ||
getDefaultUrl();
-
-export const REACT_APP_SERVER_AUTH_URL =
- window._env_?.REACT_APP_SERVER_AUTH_URL ||
- process.env.REACT_APP_SERVER_AUTH_URL ||
- REACT_APP_SERVER_BASE_URL + '/auth';
-
-export const REACT_APP_SERVER_FILES_URL =
- window._env_?.REACT_APP_SERVER_FILES_URL ||
- process.env.REACT_APP_SERVER_FILES_URL ||
- REACT_APP_SERVER_BASE_URL + '/files';
diff --git a/packages/twenty-front/src/effect-components/CommandMenuEffect.tsx b/packages/twenty-front/src/effect-components/CommandMenuEffect.tsx
index 7803c81032ef..ece319312e91 100644
--- a/packages/twenty-front/src/effect-components/CommandMenuEffect.tsx
+++ b/packages/twenty-front/src/effect-components/CommandMenuEffect.tsx
@@ -5,7 +5,7 @@ import { COMMAND_MENU_COMMANDS } from '@/command-menu/constants/CommandMenuComma
import { commandMenuCommandsState } from '@/command-menu/states/commandMenuCommandsState';
export const CommandMenuEffect = () => {
- const setCommands = useSetRecoilState(commandMenuCommandsState());
+ const setCommands = useSetRecoilState(commandMenuCommandsState);
const commands = COMMAND_MENU_COMMANDS;
useEffect(() => {
diff --git a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/effect-components/PageChangeEffect.tsx
index 65c57eefaceb..820ce959cd60 100644
--- a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx
+++ b/packages/twenty-front/src/effect-components/PageChangeEffect.tsx
@@ -45,7 +45,7 @@ export const PageChangeEffect = () => {
const openCreateActivity = useOpenCreateActivityDrawer();
- const isSignUpDisabled = useRecoilValue(isSignUpDisabledState());
+ const isSignUpDisabled = useRecoilValue(isSignUpDisabledState);
useEffect(() => {
if (!previousLocation || previousLocation !== location.pathname) {
@@ -113,11 +113,16 @@ export const PageChangeEffect = () => {
) {
navigate(AppPath.CreateProfile);
} else if (
- (onboardingStatus === OnboardingStatus.Completed ||
- onboardingStatus === OnboardingStatus.CompletedWithoutSubscription) &&
+ onboardingStatus === OnboardingStatus.Completed &&
isMatchingOnboardingRoute
) {
navigate(AppPath.Index);
+ } else if (
+ onboardingStatus === OnboardingStatus.CompletedWithoutSubscription &&
+ isMatchingOnboardingRoute &&
+ !isMatchingLocation(AppPath.PlanRequired)
+ ) {
+ navigate(AppPath.Index);
} else if (isMatchingLocation(AppPath.Invite)) {
const inviteHash =
matchPath({ path: '/invite/:workspaceInviteHash' }, location.pathname)
diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts
index 5ae4a8999565..927fde13800e 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -66,10 +66,33 @@ export type AuthTokens = {
export type Billing = {
__typename?: 'Billing';
billingFreeTrialDurationInDays?: Maybe;
- billingUrl: Scalars['String']['output'];
+ billingUrl?: Maybe;
isBillingEnabled: Scalars['Boolean']['output'];
};
+export type BillingSubscription = {
+ __typename?: 'BillingSubscription';
+ id: Scalars['ID']['output'];
+ interval?: Maybe;
+ status: Scalars['String']['output'];
+};
+
+export type BillingSubscriptionFilter = {
+ and?: InputMaybe>;
+ id?: InputMaybe;
+ or?: InputMaybe>;
+};
+
+export type BillingSubscriptionSort = {
+ direction: SortDirection;
+ field: BillingSubscriptionSortFields;
+ nulls?: InputMaybe;
+};
+
+export enum BillingSubscriptionSortFields {
+ Id = 'id'
+}
+
export type BooleanFieldComparison = {
is?: InputMaybe;
isNot?: InputMaybe;
@@ -249,6 +272,7 @@ export enum FieldMetadataType {
Position = 'POSITION',
Probability = 'PROBABILITY',
Rating = 'RATING',
+ RawJson = 'RAW_JSON',
Relation = 'RELATION',
Select = 'SELECT',
Text = 'TEXT',
@@ -301,7 +325,6 @@ export type Mutation = {
activateWorkspace: Workspace;
challenge: LoginToken;
checkoutSession: SessionEntity;
- createEvent: Analytics;
createOneField: Field;
createOneObject: Object;
createOneRefreshToken: RefreshToken;
@@ -318,6 +341,8 @@ export type Mutation = {
impersonate: Verify;
renewToken: AuthTokens;
signUp: LoginToken;
+ track: Analytics;
+ updateBillingSubscription: UpdateBillingEntity;
updateOneField: Field;
updateOneObject: Object;
updatePasswordViaResetToken: InvalidatePassword;
@@ -347,12 +372,6 @@ export type MutationCheckoutSessionArgs = {
};
-export type MutationCreateEventArgs = {
- data: Scalars['JSON']['input'];
- type: Scalars['String']['input'];
-};
-
-
export type MutationCreateOneFieldArgs = {
input: CreateOneFieldMetadataInput;
};
@@ -421,6 +440,12 @@ export type MutationSignUpArgs = {
};
+export type MutationTrackArgs = {
+ data: Scalars['JSON']['input'];
+ type: Scalars['String']['input'];
+};
+
+
export type MutationUpdateOneFieldArgs = {
input: UpdateOneFieldMetadataInput;
};
@@ -522,6 +547,8 @@ export type Query = {
fields: FieldConnection;
findWorkspaceFromInviteHash: Workspace;
getProductPrices: ProductPricesEntity;
+ getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
+ getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal;
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
object: Object;
@@ -568,6 +595,20 @@ export type QueryGetProductPricesArgs = {
};
+export type QueryGetTimelineCalendarEventsFromCompanyIdArgs = {
+ companyId: Scalars['ID']['input'];
+ page: Scalars['Int']['input'];
+ pageSize: Scalars['Int']['input'];
+};
+
+
+export type QueryGetTimelineCalendarEventsFromPersonIdArgs = {
+ page: Scalars['Int']['input'];
+ pageSize: Scalars['Int']['input'];
+ personId: Scalars['ID']['input'];
+};
+
+
export type QueryGetTimelineThreadsFromCompanyIdArgs = {
companyId: Scalars['ID']['input'];
page: Scalars['Int']['input'];
@@ -631,6 +672,23 @@ export type RelationConnection = {
pageInfo: PageInfo;
};
+export type RelationDefinition = {
+ __typename?: 'RelationDefinition';
+ direction: RelationDefinitionType;
+ sourceFieldMetadata: Field;
+ sourceObjectMetadata: Object;
+ targetFieldMetadata: Field;
+ targetObjectMetadata: Object;
+};
+
+/** Relation definition type */
+export enum RelationDefinitionType {
+ ManyToMany = 'MANY_TO_MANY',
+ ManyToOne = 'MANY_TO_ONE',
+ OneToMany = 'ONE_TO_MANY',
+ OneToOne = 'ONE_TO_ONE'
+}
+
export type RelationDeleteResponse = {
__typename?: 'RelationDeleteResponse';
createdAt?: Maybe;
@@ -657,7 +715,7 @@ export type Sentry = {
export type SessionEntity = {
__typename?: 'SessionEntity';
- url: Scalars['String']['output'];
+ url?: Maybe;
};
/** Sort Directions */
@@ -684,6 +742,45 @@ export type Telemetry = {
enabled: Scalars['Boolean']['output'];
};
+export type TimelineCalendarEvent = {
+ __typename?: 'TimelineCalendarEvent';
+ attendees: Array;
+ conferenceSolution: Scalars['String']['output'];
+ conferenceUri: Scalars['String']['output'];
+ description: Scalars['String']['output'];
+ endsAt: Scalars['DateTime']['output'];
+ id: Scalars['ID']['output'];
+ isCanceled: Scalars['Boolean']['output'];
+ isFullDay: Scalars['Boolean']['output'];
+ location: Scalars['String']['output'];
+ startsAt: Scalars['DateTime']['output'];
+ title: Scalars['String']['output'];
+ visibility: TimelineCalendarEventVisibility;
+};
+
+export type TimelineCalendarEventAttendee = {
+ __typename?: 'TimelineCalendarEventAttendee';
+ avatarUrl: Scalars['String']['output'];
+ displayName: Scalars['String']['output'];
+ firstName: Scalars['String']['output'];
+ handle: Scalars['String']['output'];
+ lastName: Scalars['String']['output'];
+ personId?: Maybe;
+ workspaceMemberId?: Maybe;
+};
+
+/** Visibility of the calendar event */
+export enum TimelineCalendarEventVisibility {
+ Metadata = 'METADATA',
+ ShareEverything = 'SHARE_EVERYTHING'
+}
+
+export type TimelineCalendarEventsWithTotal = {
+ __typename?: 'TimelineCalendarEventsWithTotal';
+ timelineCalendarEvents: Array;
+ totalNumberOfCalendarEvents: Scalars['Int']['output'];
+};
+
export type TimelineThread = {
__typename?: 'TimelineThread';
firstParticipant: TimelineThreadParticipant;
@@ -720,6 +817,12 @@ export type TransientToken = {
transientToken: AuthToken;
};
+export type UpdateBillingEntity = {
+ __typename?: 'UpdateBillingEntity';
+ /** Boolean that confirms query was successful */
+ success: Scalars['Boolean']['output'];
+};
+
export type UpdateFieldInput = {
defaultValue?: InputMaybe;
description?: InputMaybe;
@@ -831,7 +934,9 @@ export type Workspace = {
__typename?: 'Workspace';
activationStatus: Scalars['String']['output'];
allowImpersonation: Scalars['Boolean']['output'];
+ billingSubscriptions?: Maybe>;
createdAt: Scalars['DateTime']['output'];
+ currentBillingSubscription?: Maybe;
deletedAt?: Maybe;
displayName?: Maybe;
domainName?: Maybe;
@@ -844,6 +949,12 @@ export type Workspace = {
};
+export type WorkspaceBillingSubscriptionsArgs = {
+ filter?: BillingSubscriptionFilter;
+ sorting?: Array;
+};
+
+
export type WorkspaceFeatureFlagsArgs = {
filter?: FeatureFlagFilter;
sorting?: Array;
@@ -886,6 +997,7 @@ export type Field = {
label: Scalars['String']['output'];
name: Scalars['String']['output'];
options?: Maybe;
+ relationDefinition?: Maybe;
toRelationMetadata?: Maybe;
type: FieldMetadataType;
updatedAt: Scalars['DateTime']['output'];
diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx
index c96373fe9276..3a576f6b2008 100644
--- a/packages/twenty-front/src/generated/graphql.tsx
+++ b/packages/twenty-front/src/generated/graphql.tsx
@@ -61,13 +61,14 @@ export type AuthTokens = {
export type Billing = {
__typename?: 'Billing';
billingFreeTrialDurationInDays?: Maybe;
- billingUrl: Scalars['String'];
+ billingUrl?: Maybe;
isBillingEnabled: Scalars['Boolean'];
};
export type BillingSubscription = {
__typename?: 'BillingSubscription';
id: Scalars['ID'];
+ interval?: Maybe;
status: Scalars['String'];
};
@@ -191,6 +192,7 @@ export enum FieldMetadataType {
Position = 'POSITION',
Probability = 'PROBABILITY',
Rating = 'RATING',
+ RawJson = 'RAW_JSON',
Relation = 'RELATION',
Select = 'SELECT',
Text = 'TEXT',
@@ -243,7 +245,6 @@ export type Mutation = {
activateWorkspace: Workspace;
challenge: LoginToken;
checkoutSession: SessionEntity;
- createEvent: Analytics;
createOneObject: Object;
createOneRefreshToken: RefreshToken;
deleteCurrentWorkspace: Workspace;
@@ -256,6 +257,8 @@ export type Mutation = {
impersonate: Verify;
renewToken: AuthTokens;
signUp: LoginToken;
+ track: Analytics;
+ updateBillingSubscription: UpdateBillingEntity;
updateOneObject: Object;
updatePasswordViaResetToken: InvalidatePassword;
updateWorkspace: Workspace;
@@ -284,12 +287,6 @@ export type MutationCheckoutSessionArgs = {
};
-export type MutationCreateEventArgs = {
- data: Scalars['JSON'];
- type: Scalars['String'];
-};
-
-
export type MutationDeleteOneObjectArgs = {
input: DeleteOneObjectInput;
};
@@ -328,6 +325,12 @@ export type MutationSignUpArgs = {
};
+export type MutationTrackArgs = {
+ data: Scalars['JSON'];
+ type: Scalars['String'];
+};
+
+
export type MutationUpdatePasswordViaResetTokenArgs = {
newPassword: Scalars['String'];
passwordResetToken: Scalars['String'];
@@ -417,6 +420,8 @@ export type Query = {
currentWorkspace: Workspace;
findWorkspaceFromInviteHash: Workspace;
getProductPrices: ProductPricesEntity;
+ getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
+ getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal;
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
object: Object;
@@ -450,6 +455,20 @@ export type QueryGetProductPricesArgs = {
};
+export type QueryGetTimelineCalendarEventsFromCompanyIdArgs = {
+ companyId: Scalars['ID'];
+ page: Scalars['Int'];
+ pageSize: Scalars['Int'];
+};
+
+
+export type QueryGetTimelineCalendarEventsFromPersonIdArgs = {
+ page: Scalars['Int'];
+ pageSize: Scalars['Int'];
+ personId: Scalars['ID'];
+};
+
+
export type QueryGetTimelineThreadsFromCompanyIdArgs = {
companyId: Scalars['ID'];
page: Scalars['Int'];
@@ -492,6 +511,23 @@ export type RelationConnection = {
pageInfo: PageInfo;
};
+export type RelationDefinition = {
+ __typename?: 'RelationDefinition';
+ direction: RelationDefinitionType;
+ sourceFieldMetadata: Field;
+ sourceObjectMetadata: Object;
+ targetFieldMetadata: Field;
+ targetObjectMetadata: Object;
+};
+
+/** Relation definition type */
+export enum RelationDefinitionType {
+ ManyToMany = 'MANY_TO_MANY',
+ ManyToOne = 'MANY_TO_ONE',
+ OneToMany = 'ONE_TO_MANY',
+ OneToOne = 'ONE_TO_ONE'
+}
+
export type RelationDeleteResponse = {
__typename?: 'RelationDeleteResponse';
createdAt?: Maybe;
@@ -518,7 +554,7 @@ export type Sentry = {
export type SessionEntity = {
__typename?: 'SessionEntity';
- url: Scalars['String'];
+ url?: Maybe;
};
/** Sort Directions */
@@ -545,6 +581,45 @@ export type Telemetry = {
enabled: Scalars['Boolean'];
};
+export type TimelineCalendarEvent = {
+ __typename?: 'TimelineCalendarEvent';
+ attendees: Array;
+ conferenceSolution: Scalars['String'];
+ conferenceUri: Scalars['String'];
+ description: Scalars['String'];
+ endsAt: Scalars['DateTime'];
+ id: Scalars['ID'];
+ isCanceled: Scalars['Boolean'];
+ isFullDay: Scalars['Boolean'];
+ location: Scalars['String'];
+ startsAt: Scalars['DateTime'];
+ title: Scalars['String'];
+ visibility: TimelineCalendarEventVisibility;
+};
+
+export type TimelineCalendarEventAttendee = {
+ __typename?: 'TimelineCalendarEventAttendee';
+ avatarUrl: Scalars['String'];
+ displayName: Scalars['String'];
+ firstName: Scalars['String'];
+ handle: Scalars['String'];
+ lastName: Scalars['String'];
+ personId?: Maybe;
+ workspaceMemberId?: Maybe;
+};
+
+/** Visibility of the calendar event */
+export enum TimelineCalendarEventVisibility {
+ Metadata = 'METADATA',
+ ShareEverything = 'SHARE_EVERYTHING'
+}
+
+export type TimelineCalendarEventsWithTotal = {
+ __typename?: 'TimelineCalendarEventsWithTotal';
+ timelineCalendarEvents: Array;
+ totalNumberOfCalendarEvents: Scalars['Int'];
+};
+
export type TimelineThread = {
__typename?: 'TimelineThread';
firstParticipant: TimelineThreadParticipant;
@@ -581,6 +656,12 @@ export type TransientToken = {
transientToken: AuthToken;
};
+export type UpdateBillingEntity = {
+ __typename?: 'UpdateBillingEntity';
+ /** Boolean that confirms query was successful */
+ success: Scalars['Boolean'];
+};
+
export type UpdateWorkspaceInput = {
allowImpersonation?: InputMaybe;
displayName?: InputMaybe;
@@ -716,6 +797,7 @@ export type Field = {
label: Scalars['String'];
name: Scalars['String'];
options?: Maybe;
+ relationDefinition?: Maybe;
toRelationMetadata?: Maybe;
type: FieldMetadataType;
updatedAt: Scalars['DateTime'];
@@ -794,6 +876,30 @@ export type RelationEdge = {
node: Relation;
};
+export type AttendeeFragmentFragment = { __typename?: 'TimelineCalendarEventAttendee', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string };
+
+export type CalendarEventFragmentFragment = { __typename?: 'TimelineCalendarEvent', id: string, title: string, description: string, location: string, startsAt: string, endsAt: string, isFullDay: boolean, attendees: Array<{ __typename?: 'TimelineCalendarEventAttendee', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> };
+
+export type TimelineCalendarEventsWithTotalFragmentFragment = { __typename?: 'TimelineCalendarEventsWithTotal', totalNumberOfCalendarEvents: number, timelineCalendarEvents: Array<{ __typename?: 'TimelineCalendarEvent', id: string, title: string, description: string, location: string, startsAt: string, endsAt: string, isFullDay: boolean, attendees: Array<{ __typename?: 'TimelineCalendarEventAttendee', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> };
+
+export type GetTimelineCalendarEventsFromCompanyIdQueryVariables = Exact<{
+ companyId: Scalars['ID'];
+ page: Scalars['Int'];
+ pageSize: Scalars['Int'];
+}>;
+
+
+export type GetTimelineCalendarEventsFromCompanyIdQuery = { __typename?: 'Query', getTimelineCalendarEventsFromCompanyId: { __typename?: 'TimelineCalendarEventsWithTotal', totalNumberOfCalendarEvents: number, timelineCalendarEvents: Array<{ __typename?: 'TimelineCalendarEvent', id: string, title: string, description: string, location: string, startsAt: string, endsAt: string, isFullDay: boolean, attendees: Array<{ __typename?: 'TimelineCalendarEventAttendee', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> } };
+
+export type GetTimelineCalendarEventsFromPersonIdQueryVariables = Exact<{
+ personId: Scalars['ID'];
+ page: Scalars['Int'];
+ pageSize: Scalars['Int'];
+}>;
+
+
+export type GetTimelineCalendarEventsFromPersonIdQuery = { __typename?: 'Query', getTimelineCalendarEventsFromPersonId: { __typename?: 'TimelineCalendarEventsWithTotal', totalNumberOfCalendarEvents: number, timelineCalendarEvents: Array<{ __typename?: 'TimelineCalendarEvent', id: string, title: string, description: string, location: string, startsAt: string, endsAt: string, isFullDay: boolean, attendees: Array<{ __typename?: 'TimelineCalendarEventAttendee', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> }> } };
+
export type ParticipantFragmentFragment = { __typename?: 'TimelineThreadParticipant', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string };
export type TimelineThreadFragmentFragment = { __typename?: 'TimelineThread', id: string, read: boolean, visibility: string, lastMessageReceivedAt: string, lastMessageBody: string, subject: string, numberOfMessagesInThread: number, participantCount: number, firstParticipant: { __typename?: 'TimelineThreadParticipant', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }, lastTwoParticipants: Array<{ __typename?: 'TimelineThreadParticipant', personId?: string | null, workspaceMemberId?: string | null, firstName: string, lastName: string, displayName: string, avatarUrl: string, handle: string }> };
@@ -820,13 +926,13 @@ export type GetTimelineThreadsFromPersonIdQuery = { __typename?: 'Query', getTim
export type TimelineThreadFragment = { __typename?: 'TimelineThread', id: string, subject: string, lastMessageReceivedAt: string };
-export type CreateEventMutationVariables = Exact<{
+export type TrackMutationVariables = Exact<{
type: Scalars['String'];
data: Scalars['JSON'];
}>;
-export type CreateEventMutation = { __typename?: 'Mutation', createEvent: { __typename?: 'Analytics', success: boolean } };
+export type TrackMutation = { __typename?: 'Mutation', track: { __typename?: 'Analytics', success: boolean } };
export type AuthTokenFragmentFragment = { __typename?: 'AuthToken', token: string, expiresAt: string };
@@ -855,6 +961,13 @@ export type GenerateApiKeyTokenMutationVariables = Exact<{
export type GenerateApiKeyTokenMutation = { __typename?: 'Mutation', generateApiKeyToken: { __typename?: 'ApiKeyToken', token: string } };
+export type GenerateJwtMutationVariables = Exact<{
+ workspaceId: Scalars['String'];
+}>;
+
+
+export type GenerateJwtMutation = { __typename?: 'Mutation', generateJWT: { __typename?: 'AuthTokens', tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
+
export type GenerateTransientTokenMutationVariables = Exact<{ [key: string]: never; }>;
@@ -865,7 +978,7 @@ export type ImpersonateMutationVariables = Exact<{
}>;
-export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
+export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: string, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type RenewTokenMutationVariables = Exact<{
refreshToken: Scalars['String'];
@@ -896,7 +1009,7 @@ export type VerifyMutationVariables = Exact<{
}>;
-export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
+export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: string, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> }, tokens: { __typename?: 'AuthTokenPair', accessToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, refreshToken: { __typename?: 'AuthToken', token: string, expiresAt: string } } } };
export type CheckUserExistsQueryVariables = Exact<{
email: Scalars['String'];
@@ -917,7 +1030,7 @@ export type BillingPortalSessionQueryVariables = Exact<{
}>;
-export type BillingPortalSessionQuery = { __typename?: 'Query', billingPortalSession: { __typename?: 'SessionEntity', url: string } };
+export type BillingPortalSessionQuery = { __typename?: 'Query', billingPortalSession: { __typename?: 'SessionEntity', url?: string | null } };
export type CheckoutSessionMutationVariables = Exact<{
recurringInterval: Scalars['String'];
@@ -925,7 +1038,7 @@ export type CheckoutSessionMutationVariables = Exact<{
}>;
-export type CheckoutSessionMutation = { __typename?: 'Mutation', checkoutSession: { __typename?: 'SessionEntity', url: string } };
+export type CheckoutSessionMutation = { __typename?: 'Mutation', checkoutSession: { __typename?: 'SessionEntity', url?: string | null } };
export type GetProductPricesQueryVariables = Exact<{
product: Scalars['String'];
@@ -934,10 +1047,15 @@ export type GetProductPricesQueryVariables = Exact<{
export type GetProductPricesQuery = { __typename?: 'Query', getProductPrices: { __typename?: 'ProductPricesEntity', productPrices: Array<{ __typename?: 'ProductPriceEntity', created: number, recurringInterval: string, stripePriceId: string, unitAmount: number }> } };
+export type UpdateBillingSubscriptionMutationVariables = Exact<{ [key: string]: never; }>;
+
+
+export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updateBillingSubscription: { __typename?: 'UpdateBillingEntity', success: boolean } };
+
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
-export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl: string, billingFreeTrialDurationInDays?: number | null }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null } } };
+export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, signUpDisabled: boolean, debugMode: boolean, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean }, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, telemetry: { __typename?: 'Telemetry', enabled: boolean, anonymizationEnabled: boolean }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null } } };
export type UploadFileMutationVariables = Exact<{
file: Scalars['Upload'];
@@ -955,7 +1073,7 @@ export type UploadImageMutationVariables = Exact<{
export type UploadImageMutation = { __typename?: 'Mutation', uploadImage: string };
-export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null } };
+export type UserQueryFragmentFragment = { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: string, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
@@ -972,7 +1090,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
-export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', status: string } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null } | null }> } };
+export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: string, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: string, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, subscriptionStatus: string, activationStatus: string, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: string, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', status: string, interval?: string | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, domainName?: string | null } | null }> } };
export type ActivateWorkspaceMutationVariables = Exact<{
input: ActivateWorkspaceInput;
@@ -1007,6 +1125,39 @@ export type GetWorkspaceFromInviteHashQueryVariables = Exact<{
export type GetWorkspaceFromInviteHashQuery = { __typename?: 'Query', findWorkspaceFromInviteHash: { __typename?: 'Workspace', id: string, displayName?: string | null, logo?: string | null, allowImpersonation: boolean } };
+export const AttendeeFragmentFragmentDoc = gql`
+ fragment AttendeeFragment on TimelineCalendarEventAttendee {
+ personId
+ workspaceMemberId
+ firstName
+ lastName
+ displayName
+ avatarUrl
+ handle
+}
+ `;
+export const CalendarEventFragmentFragmentDoc = gql`
+ fragment CalendarEventFragment on TimelineCalendarEvent {
+ id
+ title
+ description
+ location
+ startsAt
+ endsAt
+ isFullDay
+ attendees {
+ ...AttendeeFragment
+ }
+}
+ ${AttendeeFragmentFragmentDoc}`;
+export const TimelineCalendarEventsWithTotalFragmentFragmentDoc = gql`
+ fragment TimelineCalendarEventsWithTotalFragment on TimelineCalendarEventsWithTotal {
+ totalNumberOfCalendarEvents
+ timelineCalendarEvents {
+ ...CalendarEventFragment
+ }
+}
+ ${CalendarEventFragmentFragmentDoc}`;
export const ParticipantFragmentFragmentDoc = gql`
fragment ParticipantFragment on TimelineThreadParticipant {
personId
@@ -1101,8 +1252,98 @@ export const UserQueryFragmentFragmentDoc = gql`
workspaceId
}
}
+ workspaces {
+ workspace {
+ id
+ logo
+ displayName
+ domainName
+ }
+ }
}
`;
+export const GetTimelineCalendarEventsFromCompanyIdDocument = gql`
+ query GetTimelineCalendarEventsFromCompanyId($companyId: ID!, $page: Int!, $pageSize: Int!) {
+ getTimelineCalendarEventsFromCompanyId(
+ companyId: $companyId
+ page: $page
+ pageSize: $pageSize
+ ) {
+ ...TimelineCalendarEventsWithTotalFragment
+ }
+}
+ ${TimelineCalendarEventsWithTotalFragmentFragmentDoc}`;
+
+/**
+ * __useGetTimelineCalendarEventsFromCompanyIdQuery__
+ *
+ * To run a query within a React component, call `useGetTimelineCalendarEventsFromCompanyIdQuery` and pass it any options that fit your needs.
+ * When your component renders, `useGetTimelineCalendarEventsFromCompanyIdQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useGetTimelineCalendarEventsFromCompanyIdQuery({
+ * variables: {
+ * companyId: // value for 'companyId'
+ * page: // value for 'page'
+ * pageSize: // value for 'pageSize'
+ * },
+ * });
+ */
+export function useGetTimelineCalendarEventsFromCompanyIdQuery(baseOptions: Apollo.QueryHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useQuery(GetTimelineCalendarEventsFromCompanyIdDocument, options);
+ }
+export function useGetTimelineCalendarEventsFromCompanyIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useLazyQuery(GetTimelineCalendarEventsFromCompanyIdDocument, options);
+ }
+export type GetTimelineCalendarEventsFromCompanyIdQueryHookResult = ReturnType;
+export type GetTimelineCalendarEventsFromCompanyIdLazyQueryHookResult = ReturnType;
+export type GetTimelineCalendarEventsFromCompanyIdQueryResult = Apollo.QueryResult;
+export const GetTimelineCalendarEventsFromPersonIdDocument = gql`
+ query GetTimelineCalendarEventsFromPersonId($personId: ID!, $page: Int!, $pageSize: Int!) {
+ getTimelineCalendarEventsFromPersonId(
+ personId: $personId
+ page: $page
+ pageSize: $pageSize
+ ) {
+ ...TimelineCalendarEventsWithTotalFragment
+ }
+}
+ ${TimelineCalendarEventsWithTotalFragmentFragmentDoc}`;
+
+/**
+ * __useGetTimelineCalendarEventsFromPersonIdQuery__
+ *
+ * To run a query within a React component, call `useGetTimelineCalendarEventsFromPersonIdQuery` and pass it any options that fit your needs.
+ * When your component renders, `useGetTimelineCalendarEventsFromPersonIdQuery` returns an object from Apollo Client that contains loading, error, and data properties
+ * you can use to render your UI.
+ *
+ * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
+ *
+ * @example
+ * const { data, loading, error } = useGetTimelineCalendarEventsFromPersonIdQuery({
+ * variables: {
+ * personId: // value for 'personId'
+ * page: // value for 'page'
+ * pageSize: // value for 'pageSize'
+ * },
+ * });
+ */
+export function useGetTimelineCalendarEventsFromPersonIdQuery(baseOptions: Apollo.QueryHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useQuery(GetTimelineCalendarEventsFromPersonIdDocument, options);
+ }
+export function useGetTimelineCalendarEventsFromPersonIdLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useLazyQuery(GetTimelineCalendarEventsFromPersonIdDocument, options);
+ }
+export type GetTimelineCalendarEventsFromPersonIdQueryHookResult = ReturnType;
+export type GetTimelineCalendarEventsFromPersonIdLazyQueryHookResult = ReturnType;
+export type GetTimelineCalendarEventsFromPersonIdQueryResult = Apollo.QueryResult;
export const GetTimelineThreadsFromCompanyIdDocument = gql`
query GetTimelineThreadsFromCompanyId($companyId: ID!, $page: Int!, $pageSize: Int!) {
getTimelineThreadsFromCompanyId(
@@ -1185,40 +1426,40 @@ export function useGetTimelineThreadsFromPersonIdLazyQuery(baseOptions?: Apollo.
export type GetTimelineThreadsFromPersonIdQueryHookResult = ReturnType;
export type GetTimelineThreadsFromPersonIdLazyQueryHookResult = ReturnType;
export type GetTimelineThreadsFromPersonIdQueryResult = Apollo.QueryResult;
-export const CreateEventDocument = gql`
- mutation CreateEvent($type: String!, $data: JSON!) {
- createEvent(type: $type, data: $data) {
+export const TrackDocument = gql`
+ mutation Track($type: String!, $data: JSON!) {
+ track(type: $type, data: $data) {
success
}
}
`;
-export type CreateEventMutationFn = Apollo.MutationFunction;
+export type TrackMutationFn = Apollo.MutationFunction;
/**
- * __useCreateEventMutation__
+ * __useTrackMutation__
*
- * To run a mutation, you first call `useCreateEventMutation` within a React component and pass it any options that fit your needs.
- * When your component renders, `useCreateEventMutation` returns a tuple that includes:
+ * To run a mutation, you first call `useTrackMutation` within a React component and pass it any options that fit your needs.
+ * When your component renders, `useTrackMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
- * const [createEventMutation, { data, loading, error }] = useCreateEventMutation({
+ * const [trackMutation, { data, loading, error }] = useTrackMutation({
* variables: {
* type: // value for 'type'
* data: // value for 'data'
* },
* });
*/
-export function useCreateEventMutation(baseOptions?: Apollo.MutationHookOptions) {
+export function useTrackMutation(baseOptions?: Apollo.MutationHookOptions) {
const options = {...defaultOptions, ...baseOptions}
- return Apollo.useMutation(CreateEventDocument, options);
+ return Apollo.useMutation(TrackDocument, options);
}
-export type CreateEventMutationHookResult = ReturnType;
-export type CreateEventMutationResult = Apollo.MutationResult;
-export type CreateEventMutationOptions = Apollo.BaseMutationOptions;
+export type TrackMutationHookResult = ReturnType;
+export type TrackMutationResult = Apollo.MutationResult;
+export type TrackMutationOptions = Apollo.BaseMutationOptions;
export const ChallengeDocument = gql`
mutation Challenge($email: String!, $password: String!) {
challenge(email: $email, password: $password) {
@@ -1322,6 +1563,41 @@ export function useGenerateApiKeyTokenMutation(baseOptions?: Apollo.MutationHook
export type GenerateApiKeyTokenMutationHookResult = ReturnType;
export type GenerateApiKeyTokenMutationResult = Apollo.MutationResult;
export type GenerateApiKeyTokenMutationOptions = Apollo.BaseMutationOptions;
+export const GenerateJwtDocument = gql`
+ mutation GenerateJWT($workspaceId: String!) {
+ generateJWT(workspaceId: $workspaceId) {
+ tokens {
+ ...AuthTokensFragment
+ }
+ }
+}
+ ${AuthTokensFragmentFragmentDoc}`;
+export type GenerateJwtMutationFn = Apollo.MutationFunction;
+
+/**
+ * __useGenerateJwtMutation__
+ *
+ * To run a mutation, you first call `useGenerateJwtMutation` within a React component and pass it any options that fit your needs.
+ * When your component renders, `useGenerateJwtMutation` returns a tuple that includes:
+ * - A mutate function that you can call at any time to execute the mutation
+ * - An object with fields that represent the current status of the mutation's execution
+ *
+ * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
+ *
+ * @example
+ * const [generateJwtMutation, { data, loading, error }] = useGenerateJwtMutation({
+ * variables: {
+ * workspaceId: // value for 'workspaceId'
+ * },
+ * });
+ */
+export function useGenerateJwtMutation(baseOptions?: Apollo.MutationHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useMutation(GenerateJwtDocument, options);
+ }
+export type GenerateJwtMutationHookResult = ReturnType;
+export type GenerateJwtMutationResult = Apollo.MutationResult;
+export type GenerateJwtMutationOptions = Apollo.BaseMutationOptions;
export const GenerateTransientTokenDocument = gql`
mutation generateTransientToken {
generateTransientToken {
@@ -1730,6 +2006,38 @@ export function useGetProductPricesLazyQuery(baseOptions?: Apollo.LazyQueryHookO
export type GetProductPricesQueryHookResult = ReturnType;
export type GetProductPricesLazyQueryHookResult = ReturnType;
export type GetProductPricesQueryResult = Apollo.QueryResult;
+export const UpdateBillingSubscriptionDocument = gql`
+ mutation UpdateBillingSubscription {
+ updateBillingSubscription {
+ success
+ }
+}
+ `;
+export type UpdateBillingSubscriptionMutationFn = Apollo.MutationFunction;
+
+/**
+ * __useUpdateBillingSubscriptionMutation__
+ *
+ * To run a mutation, you first call `useUpdateBillingSubscriptionMutation` within a React component and pass it any options that fit your needs.
+ * When your component renders, `useUpdateBillingSubscriptionMutation` returns a tuple that includes:
+ * - A mutate function that you can call at any time to execute the mutation
+ * - An object with fields that represent the current status of the mutation's execution
+ *
+ * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
+ *
+ * @example
+ * const [updateBillingSubscriptionMutation, { data, loading, error }] = useUpdateBillingSubscriptionMutation({
+ * variables: {
+ * },
+ * });
+ */
+export function useUpdateBillingSubscriptionMutation(baseOptions?: Apollo.MutationHookOptions) {
+ const options = {...defaultOptions, ...baseOptions}
+ return Apollo.useMutation(UpdateBillingSubscriptionDocument, options);
+ }
+export type UpdateBillingSubscriptionMutationHookResult = ReturnType;
+export type UpdateBillingSubscriptionMutationResult = Apollo.MutationResult;
+export type UpdateBillingSubscriptionMutationOptions = Apollo.BaseMutationOptions;
export const GetClientConfigDocument = gql`
query GetClientConfig {
clientConfig {
@@ -1949,6 +2257,7 @@ export const GetCurrentUserDocument = gql`
}
currentBillingSubscription {
status
+ interval
}
}
workspaces {
diff --git a/packages/twenty-front/src/hooks/useDefaultHomePagePath.tsx b/packages/twenty-front/src/hooks/useDefaultHomePagePath.tsx
index d8d59950864b..8ed52fe8cff4 100644
--- a/packages/twenty-front/src/hooks/useDefaultHomePagePath.tsx
+++ b/packages/twenty-front/src/hooks/useDefaultHomePagePath.tsx
@@ -1,25 +1,19 @@
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
-import { QueryMethodName } from '@/object-metadata/types/QueryMethodName';
-import { useCachedRootQuery } from '@/object-record/cache/hooks/useCachedRootQuery';
+import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData';
+import { PrefetchKey } from '@/prefetch/types/PrefetchKey';
export const useDefaultHomePagePath = () => {
const { objectMetadataItem: companyObjectMetadataItem } =
useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.Company,
});
- const { objectMetadataItem: viewObjectMetadataItem } = useObjectMetadataItem({
- objectNameSingular: CoreObjectNameSingular.View,
- });
- const { cachedRootQuery } = useCachedRootQuery({
- objectMetadataItem: viewObjectMetadataItem,
- queryMethodName: QueryMethodName.FindMany,
- });
- const companyViewId = cachedRootQuery?.views?.edges?.find(
- (view: any) =>
- view?.node?.objectMetadataId === companyObjectMetadataItem.id,
- )?.node.id;
+ const { records } = usePrefetchedData(PrefetchKey.AllViews);
+
+ const companyViewId = records.find(
+ (view: any) => view?.objectMetadataId === companyObjectMetadataItem.id,
+ )?.id;
const defaultHomePagePath =
'/objects/companies' + (companyViewId ? `?view=${companyViewId}` : '');
diff --git a/packages/twenty-front/src/index.tsx b/packages/twenty-front/src/index.tsx
index c30daece204d..ac59cf8827ea 100644
--- a/packages/twenty-front/src/index.tsx
+++ b/packages/twenty-front/src/index.tsx
@@ -2,6 +2,7 @@ import { StrictMode } from 'react';
import ReactDOM from 'react-dom/client';
import { HelmetProvider } from 'react-helmet-async';
import { BrowserRouter } from 'react-router-dom';
+import { loadDevMessages, loadErrorMessages } from '@apollo/client/dev';
import { RecoilRoot } from 'recoil';
import { ApolloProvider } from '@/apollo/components/ApolloProvider';
@@ -13,6 +14,7 @@ import { ExceptionHandlerProvider } from '@/error-handler/components/ExceptionHa
import { PromiseRejectionEffect } from '@/error-handler/components/PromiseRejectionEffect';
import { ApolloMetadataClientProvider } from '@/object-metadata/components/ApolloMetadataClientProvider';
import { ObjectMetadataItemsProvider } from '@/object-metadata/components/ObjectMetadataItemsProvider';
+import { PrefetchDataProvider } from '@/prefetch/components/PrefetchDataProvider';
import { IconsProvider } from '@/ui/display/icon/components/IconsProvider';
import { DialogManager } from '@/ui/feedback/dialog-manager/components/DialogManager';
import { DialogManagerScope } from '@/ui/feedback/dialog-manager/scopes/DialogManagerScope';
@@ -32,6 +34,10 @@ import 'react-loading-skeleton/dist/skeleton.css';
const root = ReactDOM.createRoot(document.getElementById('root')!);
+// Adds messages only in a dev environment
+loadDevMessages();
+loadErrorMessages();
+
root.render(
@@ -47,18 +53,20 @@ root.render(
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/packages/twenty-front/src/modules/activities/blocks/FileBlock.tsx b/packages/twenty-front/src/modules/activities/blocks/FileBlock.tsx
index de679ea553a0..1230d592b4bd 100644
--- a/packages/twenty-front/src/modules/activities/blocks/FileBlock.tsx
+++ b/packages/twenty-front/src/modules/activities/blocks/FileBlock.tsx
@@ -1,11 +1,4 @@
import { ChangeEvent, useRef } from 'react';
-import {
- BlockFromConfig,
- BlockNoteEditor,
- InlineContentSchema,
- PropSchema,
- StyleSchema,
-} from '@blocknote/core';
import { createReactBlockSpec } from '@blocknote/react';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
@@ -19,31 +12,10 @@ import { AttachmentIcon } from '../files/components/AttachmentIcon';
import { AttachmentType } from '../files/types/Attachment';
import { getFileType } from '../files/utils/getFileType';
-import { blockSpecs } from './blockSpecs';
-
-export const filePropSchema = {
- // File url
- url: {
- default: '' as string,
- },
- name: {
- default: '' as string,
- },
- fileType: {
- default: 'Other' as AttachmentType,
- },
-} satisfies PropSchema;
-
const StyledFileInput = styled.input`
display: none;
`;
-const FileBlockConfig = {
- type: 'file' as const,
- propSchema: filePropSchema,
- content: 'none' as const,
-};
-
const StyledFileLine = styled.div`
align-items: center;
display: flex;
@@ -66,75 +38,82 @@ const StyledUploadFileContainer = styled.div`
gap: ${({ theme }) => theme.spacing(2)};
`;
-const FileBlockRenderer = ({
- block,
- editor,
-}: {
- block: BlockFromConfig<
- typeof FileBlockConfig,
- InlineContentSchema,
- StyleSchema
- >;
- editor: BlockNoteEditor;
-}) => {
- const inputFileRef = useRef(null);
-
- const handleUploadAttachment = async (file: File) => {
- if (isUndefinedOrNull(file)) {
- return '';
- }
- const fileUrl = await editor.uploadFile?.(file);
-
- editor.updateBlock(block.id, {
- props: {
- ...block.props,
- ...{ url: fileUrl, fileType: getFileType(file.name), name: file.name },
+export const FileBlock = createReactBlockSpec(
+ {
+ type: 'file',
+ propSchema: {
+ url: {
+ default: '' as string,
+ },
+ name: {
+ default: '' as string,
+ },
+ fileType: {
+ default: 'Other' as AttachmentType,
},
- });
- };
- const handleUploadFileClick = () => {
- inputFileRef?.current?.click?.();
- };
- const handleFileChange = (e: ChangeEvent) => {
- if (isDefined(e.target.files)) handleUploadAttachment?.(e.target.files[0]);
- };
+ },
+ content: 'none',
+ },
+ {
+ render: ({ block, editor }) => {
+ // eslint-disable-next-line react-hooks/rules-of-hooks
+ const inputFileRef = useRef(null);
- if (isNonEmptyString(block.props.url)) {
- return (
-
-
-
-
- {block.props.name}
-
-
-
- );
- }
+ const handleUploadAttachment = async (file: File) => {
+ if (isUndefinedOrNull(file)) {
+ return '';
+ }
+ const fileUrl = await editor.uploadFile?.(file);
+
+ editor.updateBlock(block.id, {
+ props: {
+ ...block.props,
+ ...{
+ url: fileUrl,
+ fileType: getFileType(file.name),
+ name: file.name,
+ },
+ },
+ });
+ };
+ const handleUploadFileClick = () => {
+ inputFileRef?.current?.click?.();
+ };
+ const handleFileChange = (e: ChangeEvent) => {
+ if (isDefined(e.target.files))
+ handleUploadAttachment?.(e.target.files[0]);
+ };
- return (
-
-
-
-
-
-
- );
-};
+ if (isNonEmptyString(block.props.url)) {
+ return (
+
+
+
+
+ {block.props.name}
+
+
+
+ );
+ }
-export const FileBlock = createReactBlockSpec(FileBlockConfig, {
- render: (block) => {
- return (
-
- );
+ return (
+
+
+
+
+
+
+ );
+ },
},
-});
+);
diff --git a/packages/twenty-front/src/modules/activities/blocks/blockSpecs.ts b/packages/twenty-front/src/modules/activities/blocks/blockSpecs.ts
deleted file mode 100644
index fd95c52f241c..000000000000
--- a/packages/twenty-front/src/modules/activities/blocks/blockSpecs.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { defaultBlockSpecs } from '@blocknote/core';
-
-import { FileBlock } from './FileBlock';
-
-export const blockSpecs: any = {
- ...defaultBlockSpecs,
- file: FileBlock,
-};
diff --git a/packages/twenty-front/src/modules/activities/blocks/schema.ts b/packages/twenty-front/src/modules/activities/blocks/schema.ts
index 78e83e7d20d6..d6ea82eac19b 100644
--- a/packages/twenty-front/src/modules/activities/blocks/schema.ts
+++ b/packages/twenty-front/src/modules/activities/blocks/schema.ts
@@ -1,8 +1,10 @@
-import { BlockSchema, defaultBlockSchema } from '@blocknote/core';
+import { BlockNoteSchema, defaultBlockSpecs } from '@blocknote/core';
import { FileBlock } from './FileBlock';
-export const blockSchema: BlockSchema = {
- ...defaultBlockSchema,
- file: FileBlock.config,
-};
+export const blockSchema = BlockNoteSchema.create({
+ blockSpecs: {
+ ...defaultBlockSpecs,
+ file: FileBlock,
+ },
+});
diff --git a/packages/twenty-front/src/modules/activities/blocks/slashMenu.tsx b/packages/twenty-front/src/modules/activities/blocks/slashMenu.tsx
index 437d61f3fdcf..6689d1aa87f6 100644
--- a/packages/twenty-front/src/modules/activities/blocks/slashMenu.tsx
+++ b/packages/twenty-front/src/modules/activities/blocks/slashMenu.tsx
@@ -1,30 +1,45 @@
-import {
- BlockNoteEditor,
- InlineContentSchema,
- StyleSchema,
-} from '@blocknote/core';
import { getDefaultReactSlashMenuItems } from '@blocknote/react';
-import { IconFile } from '@/ui/display/icon';
+import {
+ IconFile,
+ IconH1,
+ IconH2,
+ IconH3,
+ IconList,
+ IconListNumbers,
+ IconPhoto,
+ IconPilcrow,
+ IconTable,
+} from '@/ui/display/icon';
+import { IconComponent } from '@/ui/display/icon/types/IconComponent';
+import { SuggestionItem } from '@/ui/input/editor/components/CustomSlashMenu';
import { blockSchema } from './schema';
-export const getSlashMenu = () => {
- const items = [
- ...getDefaultReactSlashMenuItems(blockSchema),
+const Icons: Record = {
+ 'Heading 1': IconH1,
+ 'Heading 2': IconH2,
+ 'Heading 3': IconH3,
+ 'Numbered List': IconListNumbers,
+ 'Bullet List': IconList,
+ Paragraph: IconPilcrow,
+ Table: IconTable,
+ Image: IconPhoto,
+};
+
+export const getSlashMenu = (editor: typeof blockSchema.BlockNoteEditor) => {
+ const items: SuggestionItem[] = [
+ ...getDefaultReactSlashMenuItems(editor).map((x) => ({
+ ...x,
+ Icon: Icons[x.title],
+ })),
{
- name: 'File',
+ title: 'File',
aliases: ['file', 'folder'],
- group: 'Media',
- icon: ,
- hint: 'Insert a file',
- execute: (
- editor: BlockNoteEditor<
- typeof blockSchema,
- InlineContentSchema,
- StyleSchema
- >,
- ) => {
+ Icon: IconFile,
+ onItemClick: () => {
+ const currentBlock = editor.getTextCursorPosition().block;
+
editor.insertBlocks(
[
{
@@ -34,7 +49,7 @@ export const getSlashMenu = () => {
},
},
],
- editor.getTextCursorPosition().block,
+ currentBlock,
'before',
);
},
diff --git a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx
index efd85a128263..59981e9479a4 100644
--- a/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx
+++ b/packages/twenty-front/src/modules/activities/calendar/components/Calendar.tsx
@@ -4,10 +4,11 @@ import { format, getYear } from 'date-fns';
import { CalendarMonthCard } from '@/activities/calendar/components/CalendarMonthCard';
import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext';
import { useCalendarEvents } from '@/activities/calendar/hooks/useCalendarEvents';
-import { sortCalendarEventsDesc } from '@/activities/calendar/utils/sortCalendarEvents';
+import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { H3Title } from '@/ui/display/typography/components/H3Title';
import { Section } from '@/ui/layout/section/components/Section';
-import { mockedCalendarEvents } from '~/testing/mock-data/calendar';
const StyledContainer = styled.div`
box-sizing: border-box;
@@ -23,9 +24,11 @@ const StyledYear = styled.span`
`;
export const Calendar = () => {
- const sortedCalendarEvents = [...mockedCalendarEvents].sort(
- sortCalendarEventsDesc,
- );
+ const { records: calendarEvents } = useFindManyRecords({
+ objectNameSingular: CoreObjectNameSingular.CalendarEvent,
+ orderBy: { startsAt: 'DescNullsLast', endsAt: 'DescNullsLast' },
+ useRecordsWithoutConnection: true,
+ });
const {
calendarEventsByDayTime,
@@ -35,7 +38,13 @@ export const Calendar = () => {
monthTimes,
monthTimesByYear,
updateCurrentCalendarEvent,
- } = useCalendarEvents(sortedCalendarEvents);
+ } = useCalendarEvents(
+ calendarEvents.map((calendarEvent) => ({
+ ...calendarEvent,
+ // TODO: retrieve CalendarChannel visibility from backend
+ visibility: 'SHARE_EVERYTHING',
+ })),
+ );
return (
{
const theme = useTheme();
- const endOfDayDate = endOfDay(calendarEvents[0].startsAt);
- const endsIn = differenceInSeconds(endOfDayDate, Date.now());
+ const endOfDayDate = endOfDay(getCalendarEventStartDate(calendarEvents[0]));
+ const dayEndsIn = differenceInSeconds(endOfDayDate, Date.now());
const weekDayLabel = format(endOfDayDate, 'EE');
const monthDayLabel = format(endOfDayDate, 'dd');
@@ -71,7 +72,7 @@ export const CalendarDayCardContent = ({
animate="ended"
variants={upcomingDayCardContentVariants}
transition={{
- delay: Math.max(0, endsIn),
+ delay: Math.max(0, dayEndsIn),
duration: theme.animation.duration.fast,
}}
>
diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx
new file mode 100644
index 000000000000..ce919021fd94
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventDetails.tsx
@@ -0,0 +1,146 @@
+import { css, useTheme } from '@emotion/react';
+import styled from '@emotion/styled';
+
+import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
+import { useObjectMetadataItemOnly } from '@/object-metadata/hooks/useObjectMetadataItemOnly';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { formatFieldMetadataItemAsFieldDefinition } from '@/object-metadata/utils/formatFieldMetadataItemAsFieldDefinition';
+import { FieldContext } from '@/object-record/record-field/contexts/FieldContext';
+import { FieldDefinition } from '@/object-record/record-field/types/FieldDefinition';
+import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata';
+import { RecordInlineCell } from '@/object-record/record-inline-cell/components/RecordInlineCell';
+import { PropertyBox } from '@/object-record/record-inline-cell/property-box/components/PropertyBox';
+import {
+ Chip,
+ ChipAccent,
+ ChipSize,
+ ChipVariant,
+} from '@/ui/display/chip/components/Chip';
+import { IconCalendarEvent } from '@/ui/display/icon';
+import { mapArrayToObject } from '~/utils/array/mapArrayToObject';
+import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
+
+type CalendarEventDetailsProps = {
+ calendarEvent: CalendarEvent;
+};
+
+const StyledContainer = styled.div`
+ background: ${({ theme }) => theme.background.secondary};
+ align-items: flex-start;
+ border-bottom: 1px solid ${({ theme }) => theme.border.color.medium};
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spacing(6)};
+ padding: ${({ theme }) => theme.spacing(6)};
+`;
+
+const StyledEventChip = styled(Chip)`
+ gap: ${({ theme }) => theme.spacing(2)};
+ padding-left: ${({ theme }) => theme.spacing(2)};
+ padding-right: ${({ theme }) => theme.spacing(2)};
+`;
+
+const StyledHeader = styled.header``;
+
+const StyledTitle = styled.h2<{ canceled?: boolean }>`
+ color: ${({ theme }) => theme.font.color.primary};
+ font-weight: ${({ theme }) => theme.font.weight.semiBold};
+ margin: ${({ theme }) => theme.spacing(0, 0, 2)};
+
+ ${({ canceled }) =>
+ canceled &&
+ css`
+ text-decoration: line-through;
+ `}
+`;
+
+const StyledCreatedAt = styled.div`
+ color: ${({ theme }) => theme.font.color.tertiary};
+`;
+
+const StyledFields = styled.div`
+ display: flex;
+ flex-direction: column;
+ gap: ${({ theme }) => theme.spacing(3)};
+`;
+
+const StyledPropertyBox = styled(PropertyBox)`
+ height: ${({ theme }) => theme.spacing(6)};
+ padding: 0;
+`;
+
+export const CalendarEventDetails = ({
+ calendarEvent,
+}: CalendarEventDetailsProps) => {
+ const theme = useTheme();
+ const { objectMetadataItem } = useObjectMetadataItemOnly({
+ objectNameSingular: CoreObjectNameSingular.CalendarEvent,
+ });
+
+ const fieldsToDisplay: Partial<
+ Record<
+ keyof CalendarEvent,
+ Partial, 'label'>>
+ >
+ > = {
+ startsAt: { label: 'Start Date' },
+ endsAt: { label: 'End Date' },
+ conferenceUri: { label: 'Meet link' },
+ location: {},
+ description: {},
+ };
+ const fieldsByName = mapArrayToObject(
+ objectMetadataItem.fields,
+ ({ name }) => name,
+ );
+
+ return (
+
+ }
+ label="Event"
+ />
+
+
+ {calendarEvent.title}
+
+
+ Created{' '}
+ {beautifyPastDateRelativeToNow(
+ new Date(calendarEvent.externalCreatedAt),
+ )}
+
+
+
+ {Object.entries(fieldsToDisplay).map(([fieldName, fieldOverride]) => (
+
+ [() => undefined, { loading: false }],
+ }}
+ >
+
+
+
+ ))}
+
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx
index 8523de07244a..8ea22bc6dbc7 100644
--- a/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx
+++ b/packages/twenty-front/src/modules/activities/calendar/components/CalendarEventRow.tsx
@@ -6,8 +6,10 @@ import { useRecoilValue } from 'recoil';
import { CalendarCurrentEventCursor } from '@/activities/calendar/components/CalendarCurrentEventCursor';
import { CalendarContext } from '@/activities/calendar/contexts/CalendarContext';
+import { useOpenCalendarEventRightDrawer } from '@/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer';
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
import { getCalendarEventEndDate } from '@/activities/calendar/utils/getCalendarEventEndDate';
+import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate';
import { hasCalendarEventEnded } from '@/activities/calendar/utils/hasCalendarEventEnded';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { IconArrowRight, IconLock } from '@/ui/display/icon';
@@ -22,12 +24,13 @@ type CalendarEventRowProps = {
className?: string;
};
-const StyledContainer = styled.div`
+const StyledContainer = styled.div<{ showTitle?: boolean }>`
align-items: center;
display: inline-flex;
gap: ${({ theme }) => theme.spacing(3)};
height: ${({ theme }) => theme.spacing(6)};
position: relative;
+ cursor: ${({ showTitle }) => (showTitle ? 'pointer' : 'not-allowed')};
`;
const StyledAttendanceIndicator = styled.div<{ active?: boolean }>`
@@ -99,23 +102,34 @@ export const CalendarEventRow = ({
className,
}: CalendarEventRowProps) => {
const theme = useTheme();
- const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState());
+ const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { displayCurrentEventCursor = false } = useContext(CalendarContext);
+ const { openCalendarEventRightDrawer } = useOpenCalendarEventRightDrawer();
+ const startsAt = getCalendarEventStartDate(calendarEvent);
const endsAt = getCalendarEventEndDate(calendarEvent);
const hasEnded = hasCalendarEventEnded(calendarEvent);
const startTimeLabel = calendarEvent.isFullDay
? 'All day'
- : format(calendarEvent.startsAt, 'HH:mm');
+ : format(startsAt, 'HH:mm');
const endTimeLabel = calendarEvent.isFullDay ? '' : format(endsAt, 'HH:mm');
- const isCurrentWorkspaceMemberAttending = !!calendarEvent.attendees?.find(
+ const isCurrentWorkspaceMemberAttending = calendarEvent.attendees?.some(
({ workspaceMemberId }) => workspaceMemberId === currentWorkspaceMember?.id,
);
+ const showTitle = calendarEvent.visibility === 'SHARE_EVERYTHING';
return (
-
+ openCalendarEventRightDrawer(calendarEvent.id)
+ : undefined
+ }
+ >
@@ -127,17 +141,17 @@ export const CalendarEventRow = ({
>
)}
- {calendarEvent.visibility === 'METADATA' ? (
+ {showTitle ? (
+
+ {calendarEvent.title}
+
+ ) : (
Not shared
- ) : (
-
- {calendarEvent.title}
-
)}
{!!calendarEvent.attendees?.length && (
diff --git a/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx b/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx
index 26c9083c2f00..65b8e0b17bc7 100644
--- a/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx
+++ b/packages/twenty-front/src/modules/activities/calendar/components/__stories__/Calendar.stories.tsx
@@ -2,13 +2,16 @@ import { Meta, StoryObj } from '@storybook/react';
import { Calendar } from '@/activities/calendar/components/Calendar';
import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
+import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
+import { graphqlMocks } from '~/testing/graphqlMocks';
const meta: Meta = {
title: 'Modules/Activities/Calendar/Calendar',
component: Calendar,
- decorators: [ComponentDecorator],
+ decorators: [ComponentDecorator, SnackBarDecorator],
parameters: {
container: { width: 728 },
+ msw: graphqlMocks,
},
};
diff --git a/packages/twenty-front/src/modules/activities/calendar/contexts/CalendarContext.ts b/packages/twenty-front/src/modules/activities/calendar/contexts/CalendarContext.ts
index ff81adcf6d00..4370ec88bcb6 100644
--- a/packages/twenty-front/src/modules/activities/calendar/contexts/CalendarContext.ts
+++ b/packages/twenty-front/src/modules/activities/calendar/contexts/CalendarContext.ts
@@ -4,7 +4,7 @@ import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
type CalendarContextValue = {
calendarEventsByDayTime: Record;
- currentCalendarEvent: CalendarEvent;
+ currentCalendarEvent?: CalendarEvent;
displayCurrentEventCursor?: boolean;
getNextCalendarEvent: (
calendarEvent: CalendarEvent,
@@ -14,7 +14,6 @@ type CalendarContextValue = {
export const CalendarContext = createContext({
calendarEventsByDayTime: {},
- currentCalendarEvent: {} as CalendarEvent,
getNextCalendarEvent: () => undefined,
updateCurrentCalendarEvent: () => {},
});
diff --git a/packages/twenty-front/src/modules/activities/calendar/hooks/__tests__/useCalendarEvents.test.tsx b/packages/twenty-front/src/modules/activities/calendar/hooks/__tests__/useCalendarEvents.test.tsx
new file mode 100644
index 000000000000..498cc5ff75af
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/hooks/__tests__/useCalendarEvents.test.tsx
@@ -0,0 +1,53 @@
+import { act, renderHook } from '@testing-library/react';
+
+import { useCalendarEvents } from '@/activities/calendar/hooks/useCalendarEvents';
+import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
+
+const calendarEvents: CalendarEvent[] = [
+ {
+ id: '1234',
+ externalCreatedAt: '2024-02-17T20:45:43.854Z',
+ isFullDay: false,
+ startsAt: '2024-02-17T21:45:27.822Z',
+ visibility: 'METADATA',
+ },
+ {
+ id: '5678',
+ externalCreatedAt: '2024-02-18T19:43:37.854Z',
+ isFullDay: false,
+ startsAt: '2024-02-18T21:43:27.754Z',
+ visibility: 'SHARE_EVERYTHING',
+ },
+ {
+ id: '91011',
+ externalCreatedAt: '2024-02-19T20:45:20.854Z',
+ isFullDay: true,
+ startsAt: '2024-02-19T22:05:27.653Z',
+ visibility: 'METADATA',
+ },
+ {
+ id: '121314',
+ externalCreatedAt: '2024-02-20T20:45:12.854Z',
+ isFullDay: true,
+ startsAt: '2024-02-20T23:15:23.150Z',
+ visibility: 'SHARE_EVERYTHING',
+ },
+];
+
+describe('useCalendar', () => {
+ it('returns calendar events', () => {
+ const { result } = renderHook(() => useCalendarEvents(calendarEvents));
+
+ expect(result.current.currentCalendarEvent).toBe(calendarEvents[0]);
+
+ expect(result.current.getNextCalendarEvent(calendarEvents[1])).toBe(
+ calendarEvents[0],
+ );
+
+ act(() => {
+ result.current.updateCurrentCalendarEvent();
+ });
+
+ expect(result.current.currentCalendarEvent).toBe(calendarEvents[0]);
+ });
+});
diff --git a/packages/twenty-front/src/modules/activities/calendar/hooks/useCalendarEvents.ts b/packages/twenty-front/src/modules/activities/calendar/hooks/useCalendarEvents.ts
index 3621c942e654..6552541bfb08 100644
--- a/packages/twenty-front/src/modules/activities/calendar/hooks/useCalendarEvents.ts
+++ b/packages/twenty-front/src/modules/activities/calendar/hooks/useCalendarEvents.ts
@@ -3,6 +3,7 @@ import { getYear, isThisMonth, startOfDay, startOfMonth } from 'date-fns';
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
import { findUpcomingCalendarEvent } from '@/activities/calendar/utils/findUpcomingCalendarEvent';
+import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate';
import { groupArrayItemsBy } from '~/utils/array/groupArrayItemsBy';
import { isDefined } from '~/utils/isDefined';
import { sortDesc } from '~/utils/sort';
@@ -10,7 +11,8 @@ import { sortDesc } from '~/utils/sort';
export const useCalendarEvents = (calendarEvents: CalendarEvent[]) => {
const calendarEventsByDayTime = groupArrayItemsBy(
calendarEvents,
- ({ startsAt }) => startOfDay(startsAt).getTime(),
+ (calendarEvent) =>
+ startOfDay(getCalendarEventStartDate(calendarEvent)).getTime(),
);
const sortedDayTimes = Object.keys(calendarEventsByDayTime)
@@ -45,17 +47,21 @@ export const useCalendarEvents = (calendarEvents: CalendarEvent[]) => {
() => findUpcomingCalendarEvent(calendarEvents),
[calendarEvents],
);
- const lastEventInCalendar = calendarEvents[0];
+ const lastEventInCalendar = calendarEvents.length
+ ? calendarEvents[0]
+ : undefined;
const [currentCalendarEvent, setCurrentCalendarEvent] = useState(
(initialUpcomingCalendarEvent &&
- (isThisMonth(initialUpcomingCalendarEvent.startsAt)
+ (isThisMonth(getCalendarEventStartDate(initialUpcomingCalendarEvent))
? initialUpcomingCalendarEvent
: getPreviousCalendarEvent(initialUpcomingCalendarEvent))) ||
lastEventInCalendar,
);
const updateCurrentCalendarEvent = () => {
+ if (!currentCalendarEvent) return;
+
const nextCurrentCalendarEvent = getNextCalendarEvent(currentCalendarEvent);
if (isDefined(nextCurrentCalendarEvent)) {
diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/attendeeFragment.ts b/packages/twenty-front/src/modules/activities/calendar/queries/fragments/attendeeFragment.ts
new file mode 100644
index 000000000000..fc6b600dd354
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/queries/fragments/attendeeFragment.ts
@@ -0,0 +1,13 @@
+import { gql } from '@apollo/client';
+
+export const attendeeFragment = gql`
+ fragment AttendeeFragment on TimelineCalendarEventAttendee {
+ personId
+ workspaceMemberId
+ firstName
+ lastName
+ displayName
+ avatarUrl
+ handle
+ }
+`;
diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/calendarEventFragment.ts b/packages/twenty-front/src/modules/activities/calendar/queries/fragments/calendarEventFragment.ts
new file mode 100644
index 000000000000..5188d3d77b94
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/queries/fragments/calendarEventFragment.ts
@@ -0,0 +1,19 @@
+import { gql } from '@apollo/client';
+
+import { attendeeFragment } from '@/activities/calendar/queries/fragments/attendeeFragment';
+
+export const calendarEventFragment = gql`
+ fragment CalendarEventFragment on TimelineCalendarEvent {
+ id
+ title
+ description
+ location
+ startsAt
+ endsAt
+ isFullDay
+ attendees {
+ ...AttendeeFragment
+ }
+ }
+ ${attendeeFragment}
+`;
diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/fragments/calendarEventFragmentWithTotalFragment.ts b/packages/twenty-front/src/modules/activities/calendar/queries/fragments/calendarEventFragmentWithTotalFragment.ts
new file mode 100644
index 000000000000..5788bb09c566
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/queries/fragments/calendarEventFragmentWithTotalFragment.ts
@@ -0,0 +1,13 @@
+import { gql } from '@apollo/client';
+
+import { calendarEventFragment } from '@/activities/calendar/queries/fragments/calendarEventFragment';
+
+export const timelineCalendarEventWithTotalFragment = gql`
+ fragment TimelineCalendarEventsWithTotalFragment on TimelineCalendarEventsWithTotal {
+ totalNumberOfCalendarEvents
+ timelineCalendarEvents {
+ ...CalendarEventFragment
+ }
+ }
+ ${calendarEventFragment}
+`;
diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/getCalendarEventsFromCompanyId.ts b/packages/twenty-front/src/modules/activities/calendar/queries/getCalendarEventsFromCompanyId.ts
new file mode 100644
index 000000000000..1928fded25ac
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/queries/getCalendarEventsFromCompanyId.ts
@@ -0,0 +1,20 @@
+import { gql } from '@apollo/client';
+
+import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/queries/fragments/calendarEventFragmentWithTotalFragment';
+
+export const getTimelineCalendarEventsFromCompanyId = gql`
+ query GetTimelineCalendarEventsFromCompanyId(
+ $companyId: ID!
+ $page: Int!
+ $pageSize: Int!
+ ) {
+ getTimelineCalendarEventsFromCompanyId(
+ companyId: $companyId
+ page: $page
+ pageSize: $pageSize
+ ) {
+ ...TimelineCalendarEventsWithTotalFragment
+ }
+ }
+ ${timelineCalendarEventWithTotalFragment}
+`;
diff --git a/packages/twenty-front/src/modules/activities/calendar/queries/getCalendarEventsFromPersonId.ts b/packages/twenty-front/src/modules/activities/calendar/queries/getCalendarEventsFromPersonId.ts
new file mode 100644
index 000000000000..764282a6be96
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/queries/getCalendarEventsFromPersonId.ts
@@ -0,0 +1,20 @@
+import { gql } from '@apollo/client';
+
+import { timelineCalendarEventWithTotalFragment } from '@/activities/calendar/queries/fragments/calendarEventFragmentWithTotalFragment';
+
+export const getTimelineCalendarEventsFromPersonId = gql`
+ query GetTimelineCalendarEventsFromPersonId(
+ $personId: ID!
+ $page: Int!
+ $pageSize: Int!
+ ) {
+ getTimelineCalendarEventsFromPersonId(
+ personId: $personId
+ page: $page
+ pageSize: $pageSize
+ ) {
+ ...TimelineCalendarEventsWithTotalFragment
+ }
+ }
+ ${timelineCalendarEventWithTotalFragment}
+`;
diff --git a/packages/twenty-front/src/modules/activities/calendar/right-drawer/components/RightDrawerCalendarEvent.tsx b/packages/twenty-front/src/modules/activities/calendar/right-drawer/components/RightDrawerCalendarEvent.tsx
new file mode 100644
index 000000000000..845885e7cd7d
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/right-drawer/components/RightDrawerCalendarEvent.tsx
@@ -0,0 +1,22 @@
+import { useRecoilValue } from 'recoil';
+
+import { CalendarEventDetails } from '@/activities/calendar/components/CalendarEventDetails';
+import { viewableCalendarEventIdState } from '@/activities/calendar/states/viewableCalendarEventIdState';
+import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
+import { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore';
+
+export const RightDrawerCalendarEvent = () => {
+ const { setRecords } = useSetRecordInStore();
+ const viewableCalendarEventId = useRecoilValue(viewableCalendarEventIdState);
+ const { record: calendarEvent } = useFindOneRecord({
+ objectNameSingular: CoreObjectNameSingular.CalendarEvent,
+ objectRecordId: viewableCalendarEventId ?? '',
+ onCompleted: (record) => setRecords([record]),
+ });
+
+ if (!calendarEvent) return null;
+
+ return ;
+};
diff --git a/packages/twenty-front/src/modules/activities/calendar/right-drawer/hooks/__tests__/useOpenCalendarEventRightDrawer.test.tsx b/packages/twenty-front/src/modules/activities/calendar/right-drawer/hooks/__tests__/useOpenCalendarEventRightDrawer.test.tsx
new file mode 100644
index 000000000000..027f7f87557a
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/right-drawer/hooks/__tests__/useOpenCalendarEventRightDrawer.test.tsx
@@ -0,0 +1,35 @@
+import { act, renderHook } from '@testing-library/react';
+import { RecoilRoot, useRecoilValue } from 'recoil';
+
+import { useOpenCalendarEventRightDrawer } from '@/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer';
+import { viewableCalendarEventIdState } from '@/activities/calendar/states/viewableCalendarEventIdState';
+import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
+
+describe('useOpenCalendarEventRightDrawer', () => {
+ it('opens the right drawer with the calendar event', () => {
+ const { result } = renderHook(
+ () => {
+ const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState);
+ const viewableCalendarEventId = useRecoilValue(
+ viewableCalendarEventIdState,
+ );
+ return {
+ ...useOpenCalendarEventRightDrawer(),
+ isRightDrawerOpen,
+ viewableCalendarEventId,
+ };
+ },
+ { wrapper: RecoilRoot },
+ );
+
+ expect(result.current.isRightDrawerOpen).toBe(false);
+ expect(result.current.viewableCalendarEventId).toBeNull();
+
+ act(() => {
+ result.current.openCalendarEventRightDrawer('1234');
+ });
+
+ expect(result.current.isRightDrawerOpen).toBe(true);
+ expect(result.current.viewableCalendarEventId).toBe('1234');
+ });
+});
diff --git a/packages/twenty-front/src/modules/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer.ts b/packages/twenty-front/src/modules/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer.ts
new file mode 100644
index 000000000000..9cd27d017e52
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/right-drawer/hooks/useOpenCalendarEventRightDrawer.ts
@@ -0,0 +1,23 @@
+import { useSetRecoilState } from 'recoil';
+
+import { viewableCalendarEventIdState } from '@/activities/calendar/states/viewableCalendarEventIdState';
+import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
+import { RightDrawerHotkeyScope } from '@/ui/layout/right-drawer/types/RightDrawerHotkeyScope';
+import { RightDrawerPages } from '@/ui/layout/right-drawer/types/RightDrawerPages';
+import { useSetHotkeyScope } from '@/ui/utilities/hotkey/hooks/useSetHotkeyScope';
+
+export const useOpenCalendarEventRightDrawer = () => {
+ const { openRightDrawer } = useRightDrawer();
+ const setHotkeyScope = useSetHotkeyScope();
+ const setViewableCalendarEventId = useSetRecoilState(
+ viewableCalendarEventIdState,
+ );
+
+ const openCalendarEventRightDrawer = (calendarEventId: string) => {
+ setHotkeyScope(RightDrawerHotkeyScope.RightDrawer, { goto: false });
+ openRightDrawer(RightDrawerPages.ViewCalendarEvent);
+ setViewableCalendarEventId(calendarEventId);
+ };
+
+ return { openCalendarEventRightDrawer };
+};
diff --git a/packages/twenty-front/src/modules/activities/calendar/states/viewableCalendarEventIdState.ts b/packages/twenty-front/src/modules/activities/calendar/states/viewableCalendarEventIdState.ts
new file mode 100644
index 000000000000..8ecfe54f3699
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/states/viewableCalendarEventIdState.ts
@@ -0,0 +1,6 @@
+import { createState } from '@/ui/utilities/state/utils/createState';
+
+export const viewableCalendarEventIdState = createState({
+ key: 'viewableCalendarEventIdState',
+ defaultValue: null,
+});
diff --git a/packages/twenty-front/src/modules/activities/calendar/types/CalendarEvent.ts b/packages/twenty-front/src/modules/activities/calendar/types/CalendarEvent.ts
index c12c5f19feb1..1e3cfb85067b 100644
--- a/packages/twenty-front/src/modules/activities/calendar/types/CalendarEvent.ts
+++ b/packages/twenty-front/src/modules/activities/calendar/types/CalendarEvent.ts
@@ -1,10 +1,14 @@
// TODO: use backend CalendarEvent type when ready
export type CalendarEvent = {
- endsAt?: Date;
+ conferenceUri?: string;
+ description?: string;
+ endsAt?: string;
+ externalCreatedAt: string;
id: string;
- isFullDay: boolean;
- startsAt: Date;
isCanceled?: boolean;
+ isFullDay: boolean;
+ location?: string;
+ startsAt: string;
title?: string;
visibility: 'METADATA' | 'SHARE_EVERYTHING';
attendees?: {
diff --git a/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/findUpcomingCalendarEvent.test.ts b/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/findUpcomingCalendarEvent.test.ts
index d6d3d556fb4a..dce86cc71c20 100644
--- a/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/findUpcomingCalendarEvent.test.ts
+++ b/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/findUpcomingCalendarEvent.test.ts
@@ -5,32 +5,32 @@ import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
import { findUpcomingCalendarEvent } from '../findUpcomingCalendarEvent';
const pastEvent: Pick = {
- startsAt: subHours(new Date(), 2),
- endsAt: subHours(new Date(), 1),
+ startsAt: subHours(new Date(), 2).toISOString(),
+ endsAt: subHours(new Date(), 1).toISOString(),
isFullDay: false,
};
const fullDayPastEvent: Pick = {
- startsAt: subDays(new Date(), 1),
+ startsAt: subDays(new Date(), 1).toISOString(),
isFullDay: true,
};
const currentEvent: Pick = {
- startsAt: addHours(new Date(), 1),
- endsAt: addHours(new Date(), 2),
+ startsAt: addHours(new Date(), 1).toISOString(),
+ endsAt: addHours(new Date(), 2).toISOString(),
isFullDay: false,
};
const currentFullDayEvent: Pick = {
- startsAt: startOfDay(new Date()),
+ startsAt: startOfDay(new Date()).toISOString(),
isFullDay: true,
};
const futureEvent: Pick = {
- startsAt: addDays(new Date(), 1),
- endsAt: addDays(new Date(), 2),
+ startsAt: addDays(new Date(), 1).toISOString(),
+ endsAt: addDays(new Date(), 2).toISOString(),
isFullDay: false,
};
const fullDayFutureEvent: Pick = {
- startsAt: addDays(new Date(), 2),
+ startsAt: addDays(new Date(), 2).toISOString(),
isFullDay: false,
};
diff --git a/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/hasCalendarEventEnded.test.ts b/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/hasCalendarEventEnded.test.ts
index 0efcf75e4b4a..7519fb7e06f2 100644
--- a/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/hasCalendarEventEnded.test.ts
+++ b/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/hasCalendarEventEnded.test.ts
@@ -6,8 +6,8 @@ describe('hasCalendarEventEnded', () => {
describe('Event with end date', () => {
it('returns true for an event with a past end date', () => {
// Given
- const startsAt = subHours(new Date(), 2);
- const endsAt = subHours(new Date(), 1);
+ const startsAt = subHours(new Date(), 2).toISOString();
+ const endsAt = subHours(new Date(), 1).toISOString();
const isFullDay = false;
// When
@@ -21,27 +21,10 @@ describe('hasCalendarEventEnded', () => {
expect(result).toBe(true);
});
- it('returns false for an event if end date is now', () => {
- // Given
- const startsAt = subHours(new Date(), 1);
- const endsAt = new Date();
- const isFullDay = false;
-
- // When
- const result = hasCalendarEventEnded({
- startsAt,
- endsAt,
- isFullDay,
- });
-
- // Then
- expect(result).toBe(false);
- });
-
it('returns false for an event with a future end date', () => {
// Given
- const startsAt = new Date();
- const endsAt = addHours(new Date(), 1);
+ const startsAt = new Date().toISOString();
+ const endsAt = addHours(new Date(), 1).toISOString();
const isFullDay = false;
// When
@@ -59,7 +42,7 @@ describe('hasCalendarEventEnded', () => {
describe('Full day event', () => {
it('returns true for a past full day event', () => {
// Given
- const startsAt = subDays(new Date(), 1);
+ const startsAt = subDays(new Date(), 1).toISOString();
const isFullDay = true;
// When
@@ -74,7 +57,7 @@ describe('hasCalendarEventEnded', () => {
it('returns false for a future full day event', () => {
// Given
- const startsAt = addDays(new Date(), 1);
+ const startsAt = addDays(new Date(), 1).toISOString();
const isFullDay = true;
// When
@@ -89,7 +72,7 @@ describe('hasCalendarEventEnded', () => {
it('returns false if the full day event is today', () => {
// Given
- const startsAt = new Date();
+ const startsAt = new Date().toISOString();
const isFullDay = true;
// When
diff --git a/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/hasCalendarEventStarted.test.ts b/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/hasCalendarEventStarted.test.ts
index 2dcc38876fed..5e0ddaea4756 100644
--- a/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/hasCalendarEventStarted.test.ts
+++ b/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/hasCalendarEventStarted.test.ts
@@ -5,7 +5,7 @@ import { hasCalendarEventStarted } from '../hasCalendarEventStarted';
describe('hasCalendarEventStarted', () => {
it('returns true for an event with a past start date', () => {
// Given
- const startsAt = subHours(new Date(), 2);
+ const startsAt = subHours(new Date(), 2).toISOString();
// When
const result = hasCalendarEventStarted({ startsAt });
@@ -16,7 +16,7 @@ describe('hasCalendarEventStarted', () => {
it('returns false for an event if start date is now', () => {
// Given
- const startsAt = new Date();
+ const startsAt = new Date().toISOString();
// When
const result = hasCalendarEventStarted({ startsAt });
@@ -27,7 +27,7 @@ describe('hasCalendarEventStarted', () => {
it('returns false for an event with a future start date', () => {
// Given
- const startsAt = addHours(new Date(), 1);
+ const startsAt = addHours(new Date(), 1).toISOString();
// When
const result = hasCalendarEventStarted({ startsAt });
diff --git a/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/sortCalendarEvents.test.ts b/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/sortCalendarEvents.test.ts
index 2e0208d6eeef..dc1a42a9cc16 100644
--- a/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/sortCalendarEvents.test.ts
+++ b/packages/twenty-front/src/modules/activities/calendar/utils/__tests__/sortCalendarEvents.test.ts
@@ -14,13 +14,13 @@ describe('sortCalendarEventsAsc', () => {
it('sorts non-intersecting events by ascending order', () => {
// Given
const eventA = {
- startsAt: someDate,
- endsAt: someDatePlusOneHour,
+ startsAt: someDate.toISOString(),
+ endsAt: someDatePlusOneHour.toISOString(),
isFullDay: false,
};
const eventB = {
- startsAt: someDatePlusTwoHours,
- endsAt: someDatePlusThreeHours,
+ startsAt: someDatePlusTwoHours.toISOString(),
+ endsAt: someDatePlusThreeHours.toISOString(),
isFullDay: false,
};
@@ -36,13 +36,13 @@ describe('sortCalendarEventsAsc', () => {
it('sorts intersecting events by start date ascending order', () => {
// Given
const eventA = {
- startsAt: someDate,
- endsAt: someDatePlusTwoHours,
+ startsAt: someDate.toISOString(),
+ endsAt: someDatePlusTwoHours.toISOString(),
isFullDay: false,
};
const eventB = {
- startsAt: someDatePlusOneHour,
- endsAt: someDatePlusThreeHours,
+ startsAt: someDatePlusOneHour.toISOString(),
+ endsAt: someDatePlusThreeHours.toISOString(),
isFullDay: false,
};
@@ -58,13 +58,13 @@ describe('sortCalendarEventsAsc', () => {
it('sorts events with same start date by end date ascending order', () => {
// Given
const eventA = {
- startsAt: someDate,
- endsAt: someDatePlusTwoHours,
+ startsAt: someDate.toISOString(),
+ endsAt: someDatePlusTwoHours.toISOString(),
isFullDay: false,
};
const eventB = {
- startsAt: someDate,
- endsAt: someDatePlusThreeHours,
+ startsAt: someDate.toISOString(),
+ endsAt: someDatePlusThreeHours.toISOString(),
isFullDay: false,
};
@@ -80,13 +80,13 @@ describe('sortCalendarEventsAsc', () => {
it('sorts events with same end date by start date ascending order', () => {
// Given
const eventA = {
- startsAt: someDate,
- endsAt: someDatePlusThreeHours,
+ startsAt: someDate.toISOString(),
+ endsAt: someDatePlusThreeHours.toISOString(),
isFullDay: false,
};
const eventB = {
- startsAt: someDatePlusOneHour,
- endsAt: someDatePlusThreeHours,
+ startsAt: someDatePlusOneHour.toISOString(),
+ endsAt: someDatePlusThreeHours.toISOString(),
isFullDay: false,
};
@@ -102,11 +102,11 @@ describe('sortCalendarEventsAsc', () => {
it('sorts events without end date by start date ascending order', () => {
// Given
const eventA = {
- startsAt: someDate,
+ startsAt: someDate.toISOString(),
isFullDay: true,
};
const eventB = {
- startsAt: someDatePlusOneHour,
+ startsAt: someDatePlusOneHour.toISOString(),
isFullDay: true,
};
@@ -122,11 +122,11 @@ describe('sortCalendarEventsAsc', () => {
it('returns 0 for full day events with the same start date', () => {
// Given
const eventA = {
- startsAt: someDate,
+ startsAt: someDate.toISOString(),
isFullDay: true,
};
const eventB = {
- startsAt: someDate,
+ startsAt: someDate.toISOString(),
isFullDay: true,
};
@@ -142,12 +142,12 @@ describe('sortCalendarEventsAsc', () => {
it('sorts the full day event last for two events with the same start date if one is full day', () => {
// Given
const eventA = {
- startsAt: someDate,
- endsAt: someDatePlusOneHour,
+ startsAt: someDate.toISOString(),
+ endsAt: someDatePlusOneHour.toISOString(),
isFullDay: false,
};
const eventB = {
- startsAt: someDate,
+ startsAt: someDate.toISOString(),
isFullDay: true,
};
@@ -165,13 +165,13 @@ describe('sortCalendarEventsDesc', () => {
it('sorts non-intersecting events by descending order', () => {
// Given
const eventA = {
- startsAt: someDate,
- endsAt: someDatePlusOneHour,
+ startsAt: someDate.toISOString(),
+ endsAt: someDatePlusOneHour.toISOString(),
isFullDay: false,
};
const eventB = {
- startsAt: someDatePlusTwoHours,
- endsAt: someDatePlusThreeHours,
+ startsAt: someDatePlusTwoHours.toISOString(),
+ endsAt: someDatePlusThreeHours.toISOString(),
isFullDay: false,
};
@@ -187,13 +187,13 @@ describe('sortCalendarEventsDesc', () => {
it('sorts intersecting events by start date descending order', () => {
// Given
const eventA = {
- startsAt: someDate,
- endsAt: someDatePlusTwoHours,
+ startsAt: someDate.toISOString(),
+ endsAt: someDatePlusTwoHours.toISOString(),
isFullDay: false,
};
const eventB = {
- startsAt: someDatePlusOneHour,
- endsAt: someDatePlusThreeHours,
+ startsAt: someDatePlusOneHour.toISOString(),
+ endsAt: someDatePlusThreeHours.toISOString(),
isFullDay: false,
};
@@ -209,13 +209,13 @@ describe('sortCalendarEventsDesc', () => {
it('sorts events with same start date by end date descending order', () => {
// Given
const eventA = {
- startsAt: someDate,
- endsAt: someDatePlusTwoHours,
+ startsAt: someDate.toISOString(),
+ endsAt: someDatePlusTwoHours.toISOString(),
isFullDay: false,
};
const eventB = {
- startsAt: someDate,
- endsAt: someDatePlusThreeHours,
+ startsAt: someDate.toISOString(),
+ endsAt: someDatePlusThreeHours.toISOString(),
isFullDay: false,
};
@@ -231,13 +231,13 @@ describe('sortCalendarEventsDesc', () => {
it('sorts events with same end date by start date descending order', () => {
// Given
const eventA = {
- startsAt: someDate,
- endsAt: someDatePlusThreeHours,
+ startsAt: someDate.toISOString(),
+ endsAt: someDatePlusThreeHours.toISOString(),
isFullDay: false,
};
const eventB = {
- startsAt: someDatePlusOneHour,
- endsAt: someDatePlusThreeHours,
+ startsAt: someDatePlusOneHour.toISOString(),
+ endsAt: someDatePlusThreeHours.toISOString(),
isFullDay: false,
};
@@ -253,11 +253,11 @@ describe('sortCalendarEventsDesc', () => {
it('sorts events without end date by start date descending order', () => {
// Given
const eventA = {
- startsAt: someDate,
+ startsAt: someDate.toISOString(),
isFullDay: true,
};
const eventB = {
- startsAt: someDatePlusOneHour,
+ startsAt: someDatePlusOneHour.toISOString(),
isFullDay: true,
};
@@ -273,11 +273,11 @@ describe('sortCalendarEventsDesc', () => {
it('returns 0 for full day events with the same start date', () => {
// Given
const eventA = {
- startsAt: someDate,
+ startsAt: someDate.toISOString(),
isFullDay: true,
};
const eventB = {
- startsAt: someDate,
+ startsAt: someDate.toISOString(),
isFullDay: true,
};
@@ -293,12 +293,12 @@ describe('sortCalendarEventsDesc', () => {
it('sorts the full day event first for two events with the same start date if one is full day', () => {
// Given
const eventA = {
- startsAt: someDate,
- endsAt: someDatePlusOneHour,
+ startsAt: someDate.toISOString(),
+ endsAt: someDatePlusOneHour.toISOString(),
isFullDay: false,
};
const eventB = {
- startsAt: someDate,
+ startsAt: someDate.toISOString(),
isFullDay: true,
};
diff --git a/packages/twenty-front/src/modules/activities/calendar/utils/getCalendarEventEndDate.ts b/packages/twenty-front/src/modules/activities/calendar/utils/getCalendarEventEndDate.ts
index 0d25a605bb8e..8463f2236faa 100644
--- a/packages/twenty-front/src/modules/activities/calendar/utils/getCalendarEventEndDate.ts
+++ b/packages/twenty-front/src/modules/activities/calendar/utils/getCalendarEventEndDate.ts
@@ -1,7 +1,11 @@
import { endOfDay } from 'date-fns';
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
+import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate';
export const getCalendarEventEndDate = (
calendarEvent: Pick,
-) => calendarEvent.endsAt ?? endOfDay(calendarEvent.startsAt);
+) =>
+ calendarEvent.endsAt
+ ? new Date(calendarEvent.endsAt)
+ : endOfDay(getCalendarEventStartDate(calendarEvent));
diff --git a/packages/twenty-front/src/modules/activities/calendar/utils/getCalendarEventStartDate.ts b/packages/twenty-front/src/modules/activities/calendar/utils/getCalendarEventStartDate.ts
new file mode 100644
index 000000000000..db7a08de3ee3
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/calendar/utils/getCalendarEventStartDate.ts
@@ -0,0 +1,5 @@
+import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
+
+export const getCalendarEventStartDate = (
+ calendarEvent: Pick,
+) => new Date(calendarEvent.startsAt);
diff --git a/packages/twenty-front/src/modules/activities/calendar/utils/hasCalendarEventStarted.ts b/packages/twenty-front/src/modules/activities/calendar/utils/hasCalendarEventStarted.ts
index ec7d7a2c5d7f..f351edc9d90d 100644
--- a/packages/twenty-front/src/modules/activities/calendar/utils/hasCalendarEventStarted.ts
+++ b/packages/twenty-front/src/modules/activities/calendar/utils/hasCalendarEventStarted.ts
@@ -1,7 +1,8 @@
import { isPast } from 'date-fns';
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
+import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate';
export const hasCalendarEventStarted = (
calendarEvent: Pick,
-) => isPast(calendarEvent.startsAt);
+) => isPast(getCalendarEventStartDate(calendarEvent));
diff --git a/packages/twenty-front/src/modules/activities/calendar/utils/sortCalendarEvents.ts b/packages/twenty-front/src/modules/activities/calendar/utils/sortCalendarEvents.ts
index fc3b77353886..8ea50406c36c 100644
--- a/packages/twenty-front/src/modules/activities/calendar/utils/sortCalendarEvents.ts
+++ b/packages/twenty-front/src/modules/activities/calendar/utils/sortCalendarEvents.ts
@@ -1,5 +1,6 @@
import { CalendarEvent } from '@/activities/calendar/types/CalendarEvent';
import { getCalendarEventEndDate } from '@/activities/calendar/utils/getCalendarEventEndDate';
+import { getCalendarEventStartDate } from '@/activities/calendar/utils/getCalendarEventStartDate';
import { sortAsc } from '~/utils/sort';
export const sortCalendarEventsAsc = (
@@ -7,18 +8,16 @@ export const sortCalendarEventsAsc = (
calendarEventB: Pick,
) => {
const startsAtSort = sortAsc(
- calendarEventA.startsAt.getTime(),
- calendarEventB.startsAt.getTime(),
+ getCalendarEventStartDate(calendarEventA).getTime(),
+ getCalendarEventStartDate(calendarEventB).getTime(),
);
- if (startsAtSort === 0) {
- const endsAtA = getCalendarEventEndDate(calendarEventA);
- const endsAtB = getCalendarEventEndDate(calendarEventB);
+ if (startsAtSort !== 0) return startsAtSort;
- return sortAsc(endsAtA.getTime(), endsAtB.getTime());
- }
-
- return startsAtSort;
+ return sortAsc(
+ getCalendarEventEndDate(calendarEventA).getTime(),
+ getCalendarEventEndDate(calendarEventB).getTime(),
+ );
};
export const sortCalendarEventsDesc = (
diff --git a/packages/twenty-front/src/modules/activities/comment/__stories__/Comment.stories.tsx b/packages/twenty-front/src/modules/activities/comment/__stories__/Comment.stories.tsx
index 3c1b39bcbc79..876d5a2517ad 100644
--- a/packages/twenty-front/src/modules/activities/comment/__stories__/Comment.stories.tsx
+++ b/packages/twenty-front/src/modules/activities/comment/__stories__/Comment.stories.tsx
@@ -11,7 +11,7 @@ import { Comment } from '../Comment';
import { mockComment, mockCommentWithLongValues } from './mock-comment';
const CommentSetterEffect = () => {
- const setViewableActivity = useSetRecoilState(viewableActivityIdState());
+ const setViewableActivity = useSetRecoilState(viewableActivityIdState);
useEffect(() => {
setViewableActivity('test-id');
diff --git a/packages/twenty-front/src/modules/activities/comment/__stories__/CommentHeader.stories.tsx b/packages/twenty-front/src/modules/activities/comment/__stories__/CommentHeader.stories.tsx
index 5206587ac08f..d61ebc25b23e 100644
--- a/packages/twenty-front/src/modules/activities/comment/__stories__/CommentHeader.stories.tsx
+++ b/packages/twenty-front/src/modules/activities/comment/__stories__/CommentHeader.stories.tsx
@@ -13,7 +13,7 @@ import { CommentHeader } from '../CommentHeader';
import { mockComment, mockCommentWithLongValues } from './mock-comment';
const CommentHeaderSetterEffect = () => {
- const setViewableActivity = useSetRecoilState(viewableActivityIdState());
+ const setViewableActivity = useSetRecoilState(viewableActivityIdState);
useEffect(() => {
setViewableActivity('test-id');
diff --git a/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx b/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx
index 16a862c405e2..b9c4c84c242a 100644
--- a/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx
+++ b/packages/twenty-front/src/modules/activities/components/ActivityBodyEditor.tsx
@@ -1,6 +1,5 @@
-import { useCallback, useMemo } from 'react';
-import { BlockNoteEditor } from '@blocknote/core';
-import { useBlockNote } from '@blocknote/react';
+import { ClipboardEvent, useCallback, useMemo } from 'react';
+import { useCreateBlockNote } from '@blocknote/react';
import styled from '@emotion/styled';
import { isArray, isNonEmptyString } from '@sniptt/guards';
import { useRecoilCallback, useRecoilState } from 'recoil';
@@ -8,6 +7,7 @@ import { Key } from 'ts-key-enum';
import { useDebouncedCallback } from 'use-debounce';
import { v4 } from 'uuid';
+import { blockSchema } from '@/activities/blocks/schema';
import { useUpsertActivity } from '@/activities/hooks/useUpsertActivity';
import { activityBodyFamilyState } from '@/activities/states/activityBodyFamilyState';
import { activityTitleHasBeenSetFamilyState } from '@/activities/states/activityTitleHasBeenSetFamilyState';
@@ -28,8 +28,6 @@ import { FileFolder, useUploadFileMutation } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull';
-import { blockSpecs } from '../blocks/blockSpecs';
-import { getSlashMenu } from '../blocks/slashMenu';
import { getFileType } from '../files/utils/getFileType';
import '@blocknote/react/style.css';
@@ -118,11 +116,9 @@ export const ActivityBodyEditor = ({
);
const [canCreateActivity, setCanCreateActivity] = useRecoilState(
- canCreateActivityState(),
+ canCreateActivityState,
);
- const slashMenuItems = getSlashMenu();
-
const [uploadFile] = useUploadFileMutation();
const handleUploadAttachment = async (file: File): Promise => {
@@ -216,8 +212,8 @@ export const ActivityBodyEditor = ({
const handleBodyChangeDebounced = useDebouncedCallback(handleBodyChange, 500);
- const handleEditorChange = (newEditor: BlockNoteEditor) => {
- const newStringifiedBody = JSON.stringify(newEditor.topLevelBlocks) ?? '';
+ const handleEditorChange = () => {
+ const newStringifiedBody = JSON.stringify(editor.document) ?? '';
setActivityBody(newStringifiedBody);
@@ -238,16 +234,11 @@ export const ActivityBodyEditor = ({
}
}, [activity, activityBody]);
- const editor: BlockNoteEditor | null = useBlockNote({
+ const editor = useCreateBlockNote({
initialContent: initialBody,
domAttributes: { editor: { class: 'editor' } },
- onEditorContentChange: handleEditorChange,
- slashMenuItems,
- blockSpecs: blockSpecs,
+ schema: blockSchema,
uploadFile: handleUploadAttachment,
- onEditorReady: (editor: BlockNoteEditor) => {
- editor.domElement.addEventListener('paste', handleImagePaste);
- },
});
const handleImagePaste = async (event: ClipboardEvent) => {
@@ -348,7 +339,7 @@ export const ActivityBodyEditor = ({
if (
isDefined(currentBlockContent) &&
isArray(currentBlockContent) &&
- currentBlockContent[0] &&
+ isDefined(currentBlockContent[0]) &&
currentBlockContent[0].type === 'text'
) {
// Text block case
@@ -361,7 +352,7 @@ export const ActivityBodyEditor = ({
const newBlockId = v4();
const newBlock = {
id: newBlockId,
- type: 'paragraph',
+ type: 'paragraph' as const,
content: keyboardEvent.key,
};
editor.insertBlocks([newBlock], blockIdentifier, 'after');
@@ -387,6 +378,8 @@ export const ActivityBodyEditor = ({
diff --git a/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx b/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx
index 8647c485e8cc..16bdbed6961c 100644
--- a/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx
+++ b/packages/twenty-front/src/modules/activities/components/ActivityComments.tsx
@@ -61,7 +61,7 @@ export const ActivityComments = ({
objectNameSingular: CoreObjectNameSingular.Comment,
});
- const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState());
+ const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { records: comments } = useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.Comment,
diff --git a/packages/twenty-front/src/modules/activities/components/ActivityEditorEffect.tsx b/packages/twenty-front/src/modules/activities/components/ActivityEditorEffect.tsx
index 1f839640e9d8..93b6f5405671 100644
--- a/packages/twenty-front/src/modules/activities/components/ActivityEditorEffect.tsx
+++ b/packages/twenty-front/src/modules/activities/components/ActivityEditorEffect.tsx
@@ -29,15 +29,15 @@ export const ActivityEditorEffect = ({
({ snapshot, set }) =>
() => {
const isUpsertingActivityInDB = snapshot
- .getLoadable(isUpsertingActivityInDBState())
+ .getLoadable(isUpsertingActivityInDBState)
.getValue();
const canCreateActivity = snapshot
- .getLoadable(canCreateActivityState())
+ .getLoadable(canCreateActivityState)
.getValue();
const isActivityInCreateMode = snapshot
- .getLoadable(isActivityInCreateModeState())
+ .getLoadable(isActivityInCreateModeState)
.getValue();
const activityFromStore = snapshot
@@ -71,7 +71,7 @@ export const ActivityEditorEffect = ({
deleteActivityFromCache(activity);
}
- set(isActivityInCreateModeState(), false);
+ set(isActivityInCreateModeState, false);
} else if (isDefined(activity)) {
if (
activity.title !== activityTitle ||
diff --git a/packages/twenty-front/src/modules/activities/components/ActivityTitle.tsx b/packages/twenty-front/src/modules/activities/components/ActivityTitle.tsx
index 7a4a11f273b1..05cc0bdd3e52 100644
--- a/packages/twenty-front/src/modules/activities/components/ActivityTitle.tsx
+++ b/packages/twenty-front/src/modules/activities/components/ActivityTitle.tsx
@@ -71,7 +71,7 @@ export const ActivityTitle = ({ activityId }: ActivityTitleProps) => {
const activity = activityInStore as Activity;
const [canCreateActivity, setCanCreateActivity] = useRecoilState(
- canCreateActivityState(),
+ canCreateActivityState,
);
const { upsertActivity } = useUpsertActivity();
diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadPreview.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadPreview.tsx
index 98f5cf004a3d..ce085a8e7b3d 100644
--- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreadPreview.tsx
+++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreadPreview.tsx
@@ -4,7 +4,7 @@ import { useRecoilCallback } from 'recoil';
import { EmailThreadNotShared } from '@/activities/emails/components/EmailThreadNotShared';
import { useEmailThread } from '@/activities/emails/hooks/useEmailThread';
-import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/state/lastViewableEmailThreadIdState';
+import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/states/lastViewableEmailThreadIdState';
import { CardContent } from '@/ui/layout/card/components/CardContent';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { GRAY_SCALE } from '@/ui/theme/constants/GrayScale';
@@ -122,7 +122,7 @@ export const EmailThreadPreview = ({
isSameEventThanRightDrawerClose(event.nativeEvent);
const emailThreadIdWhenEmailThreadWasClosed = snapshot
- .getLoadable(emailThreadIdWhenEmailThreadWasClosedState())
+ .getLoadable(emailThreadIdWhenEmailThreadWasClosedState)
.getValue();
const canOpen =
diff --git a/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx b/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx
index 65251d76be43..91cc33ece60e 100644
--- a/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx
+++ b/packages/twenty-front/src/modules/activities/emails/components/EmailThreads.tsx
@@ -58,12 +58,12 @@ export const EmailThreads = ({
}) => {
const { enqueueSnackBar } = useSnackBar();
- const { getEmailThreadsPageState } = useEmailThreadStates({
+ const { emailThreadsPageState } = useEmailThreadStates({
emailThreadScopeId: getScopeIdFromComponentId(entity.id),
});
const [emailThreadsPage, setEmailThreadsPage] = useRecoilState(
- getEmailThreadsPageState(),
+ emailThreadsPageState,
);
const [isFetchingMoreEmails, setIsFetchingMoreEmails] = useState(false);
diff --git a/packages/twenty-front/src/modules/activities/emails/hooks/__tests__/useEmailThread.test.ts b/packages/twenty-front/src/modules/activities/emails/hooks/__tests__/useEmailThread.test.ts
deleted file mode 100644
index a3bccb817dcd..000000000000
--- a/packages/twenty-front/src/modules/activities/emails/hooks/__tests__/useEmailThread.test.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { act, renderHook } from '@testing-library/react';
-
-import { useEmailThread } from '../useEmailThread';
-
-const mockSetViewableEmailThreadId = jest.fn();
-const mockUseOpenEmailThreadRightDrawer = jest.fn();
-
-jest.mock('recoil', () => ({
- useSetRecoilState: () => mockSetViewableEmailThreadId,
-}));
-
-jest.mock(
- '@/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer',
- () => ({
- useOpenEmailThreadRightDrawer: () => mockUseOpenEmailThreadRightDrawer,
- }),
-);
-
-jest.mock('@/activities/emails/state/viewableEmailThreadIdState', () => ({
- viewableEmailThreadIdState: () => 'mockViewableEmailThreadIdState',
-}));
-
-describe('useEmailThread hook', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it('openEmailThread function', () => {
- const { result } = renderHook(() => useEmailThread());
-
- act(() => {
- result.current.openEmailThread('mockThreadId');
- });
-
- expect(mockSetViewableEmailThreadId).toHaveBeenCalledWith('mockThreadId');
- expect(mockUseOpenEmailThreadRightDrawer).toHaveBeenCalled();
- });
-});
diff --git a/packages/twenty-front/src/modules/activities/emails/hooks/__tests__/useEmailThread.test.tsx b/packages/twenty-front/src/modules/activities/emails/hooks/__tests__/useEmailThread.test.tsx
new file mode 100644
index 000000000000..4dae422ca2fd
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/emails/hooks/__tests__/useEmailThread.test.tsx
@@ -0,0 +1,69 @@
+import { act, renderHook } from '@testing-library/react';
+import { RecoilRoot, useRecoilState, useRecoilValue } from 'recoil';
+
+import { useEmailThread } from '@/activities/emails/hooks/useEmailThread';
+import { viewableEmailThreadIdState } from '@/activities/emails/states/viewableEmailThreadIdState';
+import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
+
+const viewableEmailThreadId = '1234';
+
+describe('useEmailThread', () => {
+ it('should open email thread', () => {
+ const { result } = renderHook(
+ () => {
+ const emailThread = useEmailThread();
+ const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState);
+ const viewableEmailThreadId = useRecoilValue(
+ viewableEmailThreadIdState,
+ );
+
+ return { ...emailThread, isRightDrawerOpen, viewableEmailThreadId };
+ },
+ { wrapper: RecoilRoot },
+ );
+
+ expect(result.current.isRightDrawerOpen).toBe(false);
+ expect(result.current.viewableEmailThreadId).toBeNull();
+
+ act(() => {
+ result.current.openEmailThread(viewableEmailThreadId);
+ });
+
+ expect(result.current.isRightDrawerOpen).toBe(true);
+ expect(result.current.viewableEmailThreadId).toBe(viewableEmailThreadId);
+ });
+
+ it('should close email thread if trying to open the same thread id', () => {
+ const { result } = renderHook(
+ () => {
+ const emailThread = useEmailThread();
+ const [isRightDrawerOpen, setIsRightDrawerOpen] = useRecoilState(
+ isRightDrawerOpenState,
+ );
+ const [viewableEmailThreadId, setViewableEmailThreadId] =
+ useRecoilState(viewableEmailThreadIdState);
+
+ return {
+ ...emailThread,
+ isRightDrawerOpen,
+ viewableEmailThreadId,
+ setIsRightDrawerOpen,
+ setViewableEmailThreadId,
+ };
+ },
+ { wrapper: RecoilRoot },
+ );
+
+ act(() => {
+ result.current.setIsRightDrawerOpen(true);
+ result.current.setViewableEmailThreadId(viewableEmailThreadId);
+ });
+
+ act(() => {
+ result.current.openEmailThread(viewableEmailThreadId);
+ });
+
+ expect(result.current.isRightDrawerOpen).toBe(false);
+ expect(result.current.viewableEmailThreadId).toBeNull();
+ });
+});
diff --git a/packages/twenty-front/src/modules/activities/emails/hooks/internal/__tests__/useEmailThreadStates.test.ts b/packages/twenty-front/src/modules/activities/emails/hooks/internal/__tests__/useEmailThreadStates.test.ts
index a1a65c99c27a..0f77de3a96c1 100644
--- a/packages/twenty-front/src/modules/activities/emails/hooks/internal/__tests__/useEmailThreadStates.test.ts
+++ b/packages/twenty-front/src/modules/activities/emails/hooks/internal/__tests__/useEmailThreadStates.test.ts
@@ -30,7 +30,7 @@ describe('useEmailThreadStates hook', () => {
);
expect(result.current.scopeId).toBe(mockScopeId);
- expect(result.current.getEmailThreadsPageState).toBe(
+ expect(result.current.emailThreadsPageState).toBe(
mockGetEmailThreadsPageState,
);
});
diff --git a/packages/twenty-front/src/modules/activities/emails/hooks/internal/useEmailThreadStates.ts b/packages/twenty-front/src/modules/activities/emails/hooks/internal/useEmailThreadStates.ts
index 946a43e0f835..472377dfecdf 100644
--- a/packages/twenty-front/src/modules/activities/emails/hooks/internal/useEmailThreadStates.ts
+++ b/packages/twenty-front/src/modules/activities/emails/hooks/internal/useEmailThreadStates.ts
@@ -1,4 +1,4 @@
-import { emailThreadsPageComponentState } from '@/activities/emails/state/emailThreadsPageComponentState';
+import { emailThreadsPageComponentState } from '@/activities/emails/states/emailThreadsPageComponentState';
import { TabListScopeInternalContext } from '@/ui/layout/tab/scopes/scope-internal-context/TabListScopeInternalContext';
import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId';
import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState';
@@ -17,7 +17,7 @@ export const useEmailThreadStates = ({
return {
scopeId,
- getEmailThreadsPageState: extractComponentState(
+ emailThreadsPageState: extractComponentState(
emailThreadsPageComponentState,
scopeId,
),
diff --git a/packages/twenty-front/src/modules/activities/emails/hooks/useEmailThread.ts b/packages/twenty-front/src/modules/activities/emails/hooks/useEmailThread.ts
index a5fca6861b1d..a0757a18af57 100644
--- a/packages/twenty-front/src/modules/activities/emails/hooks/useEmailThread.ts
+++ b/packages/twenty-front/src/modules/activities/emails/hooks/useEmailThread.ts
@@ -1,7 +1,7 @@
import { useRecoilCallback } from 'recoil';
import { useOpenEmailThreadRightDrawer } from '@/activities/emails/right-drawer/hooks/useOpenEmailThreadRightDrawer';
-import { viewableEmailThreadIdState } from '@/activities/emails/state/viewableEmailThreadIdState';
+import { viewableEmailThreadIdState } from '@/activities/emails/states/viewableEmailThreadIdState';
import { useRightDrawer } from '@/ui/layout/right-drawer/hooks/useRightDrawer';
import { isRightDrawerOpenState } from '@/ui/layout/right-drawer/states/isRightDrawerOpenState';
@@ -13,21 +13,21 @@ export const useEmailThread = () => {
({ snapshot, set }) =>
(threadId: string) => {
const isRightDrawerOpen = snapshot
- .getLoadable(isRightDrawerOpenState())
+ .getLoadable(isRightDrawerOpenState)
.getValue();
const viewableEmailThreadId = snapshot
- .getLoadable(viewableEmailThreadIdState())
+ .getLoadable(viewableEmailThreadIdState)
.getValue();
if (isRightDrawerOpen && viewableEmailThreadId === threadId) {
- set(viewableEmailThreadIdState(), null);
+ set(viewableEmailThreadIdState, null);
closeRightDrawer();
return;
}
openEmailThredRightDrawer();
- set(viewableEmailThreadIdState(), threadId);
+ set(viewableEmailThreadIdState, threadId);
},
[closeRightDrawer, openEmailThredRightDrawer],
);
diff --git a/packages/twenty-front/src/modules/activities/emails/queries/__tests__/getTimelineThreadsFromCompanyId.test.ts b/packages/twenty-front/src/modules/activities/emails/queries/__tests__/getTimelineThreadsFromCompanyId.test.ts
new file mode 100644
index 000000000000..9b3c3ba70fde
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/emails/queries/__tests__/getTimelineThreadsFromCompanyId.test.ts
@@ -0,0 +1,16 @@
+import { gql } from '@apollo/client';
+
+import { getTimelineThreadsFromCompanyId } from '../getTimelineThreadsFromCompanyId';
+
+jest.mock('@apollo/client', () => ({
+ gql: jest.fn().mockImplementation((strings) => {
+ return strings.map((str: string) => str.trim()).join(' ');
+ }),
+}));
+
+describe('getTimelineThreadsFromCompanyId query', () => {
+ test('should construct the query correctly', () => {
+ expect(gql).toHaveBeenCalled();
+ expect(getTimelineThreadsFromCompanyId).toBeDefined();
+ });
+});
diff --git a/packages/twenty-front/src/modules/activities/emails/queries/__tests__/getTimelineThreadsFromPersonId.test.ts b/packages/twenty-front/src/modules/activities/emails/queries/__tests__/getTimelineThreadsFromPersonId.test.ts
new file mode 100644
index 000000000000..66862b7288ee
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/emails/queries/__tests__/getTimelineThreadsFromPersonId.test.ts
@@ -0,0 +1,16 @@
+import { gql } from '@apollo/client';
+
+import { getTimelineThreadsFromPersonId } from '../getTimelineThreadsFromPersonId';
+
+jest.mock('@apollo/client', () => ({
+ gql: jest.fn().mockImplementation((strings) => {
+ return strings.map((str: string) => str.trim()).join(' ');
+ }),
+}));
+
+describe('getTimelineThreadsFromPersonId query', () => {
+ test('should construct the query correctly', () => {
+ expect(gql).toHaveBeenCalled();
+ expect(getTimelineThreadsFromPersonId).toBeDefined();
+ });
+});
diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx b/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx
index c901644f6444..e353f1dd4a9b 100644
--- a/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx
+++ b/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThread.tsx
@@ -6,7 +6,7 @@ import { EmailThreadFetchMoreLoader } from '@/activities/emails/components/Email
import { EmailThreadHeader } from '@/activities/emails/components/EmailThreadHeader';
import { EmailThreadMessage } from '@/activities/emails/components/EmailThreadMessage';
import { useRightDrawerEmailThread } from '@/activities/emails/right-drawer/hooks/useRightDrawerEmailThread';
-import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/state/lastViewableEmailThreadIdState';
+import { emailThreadIdWhenEmailThreadWasClosedState } from '@/activities/emails/states/lastViewableEmailThreadIdState';
import { RIGHT_DRAWER_CLICK_OUTSIDE_LISTENER_ID } from '@/ui/layout/right-drawer/constants/RightDrawerClickOutsideListener';
import { useClickOutsideListener } from '@/ui/utilities/pointer-event/hooks/useClickOutsideListener';
@@ -34,7 +34,7 @@ export const RightDrawerEmailThread = () => {
callbackFunction: useRecoilCallback(
({ set }) =>
() => {
- set(emailThreadIdWhenEmailThreadWasClosedState(), thread.id);
+ set(emailThreadIdWhenEmailThreadWasClosedState, thread.id);
},
[thread],
),
diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThreadTopBar.tsx b/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThreadTopBar.tsx
deleted file mode 100644
index 29e71fe6e127..000000000000
--- a/packages/twenty-front/src/modules/activities/emails/right-drawer/components/RightDrawerEmailThreadTopBar.tsx
+++ /dev/null
@@ -1,24 +0,0 @@
-import styled from '@emotion/styled';
-
-import { StyledRightDrawerTopBar } from '@/ui/layout/right-drawer/components/StyledRightDrawerTopBar';
-import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
-
-import { RightDrawerTopBarCloseButton } from '../../../../ui/layout/right-drawer/components/RightDrawerTopBarCloseButton';
-import { RightDrawerTopBarExpandButton } from '../../../../ui/layout/right-drawer/components/RightDrawerTopBarExpandButton';
-
-const StyledTopBarWrapper = styled.div`
- display: flex;
-`;
-
-export const RightDrawerEmailThreadTopBar = () => {
- const isMobile = useIsMobile();
-
- return (
-
-
-
- {!isMobile && }
-
-
- );
-};
diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/components/__stories__/RightDrawerEmailThreadTopBar.stories.tsx b/packages/twenty-front/src/modules/activities/emails/right-drawer/components/__stories__/RightDrawerEmailThreadTopBar.stories.tsx
deleted file mode 100644
index ebbfc80dcb92..000000000000
--- a/packages/twenty-front/src/modules/activities/emails/right-drawer/components/__stories__/RightDrawerEmailThreadTopBar.stories.tsx
+++ /dev/null
@@ -1,26 +0,0 @@
-import { Meta, StoryObj } from '@storybook/react';
-
-import { RightDrawerEmailThreadTopBar } from '@/activities/emails/right-drawer/components/RightDrawerEmailThreadTopBar';
-import { ComponentDecorator } from '~/testing/decorators/ComponentDecorator';
-import { graphqlMocks } from '~/testing/graphqlMocks';
-
-const meta: Meta = {
- title: 'Modules/Activities/Emails/RightDrawer/RightDrawerEmailThreadTopBar',
- component: RightDrawerEmailThreadTopBar,
- decorators: [
- (Story) => (
-
-
-
- ),
- ComponentDecorator,
- ],
- parameters: {
- msw: graphqlMocks,
- },
-};
-
-export default meta;
-type Story = StoryObj;
-
-export const Default: Story = {};
diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useRightDrawerEmailThread.test.tsx b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useRightDrawerEmailThread.test.tsx
new file mode 100644
index 000000000000..63f1b09b06de
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/__tests__/useRightDrawerEmailThread.test.tsx
@@ -0,0 +1,40 @@
+import { MockedProvider } from '@apollo/client/testing';
+import { renderHook } from '@testing-library/react';
+import { RecoilRoot } from 'recoil';
+
+import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
+
+import { useRightDrawerEmailThread } from '../useRightDrawerEmailThread';
+
+jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
+ __esModule: true,
+ useFindManyRecords: jest.fn(),
+}));
+
+describe('useRightDrawerEmailThread', () => {
+ it('should return correct values', async () => {
+ const mockMessages = [
+ { id: '1', text: 'Message 1' },
+ { id: '2', text: 'Message 2' },
+ ];
+ const mockFetchMoreRecords = jest.fn();
+ (useFindManyRecords as jest.Mock).mockReturnValue({
+ records: mockMessages,
+ loading: false,
+ fetchMoreRecords: mockFetchMoreRecords,
+ });
+
+ const { result } = renderHook(() => useRightDrawerEmailThread(), {
+ wrapper: ({ children }) => (
+
+ {children}
+
+ ),
+ });
+
+ expect(result.current.thread).toBeDefined();
+ expect(result.current.messages).toEqual(mockMessages);
+ expect(result.current.loading).toBeFalsy();
+ expect(result.current.fetchMoreMessages).toBeInstanceOf(Function);
+ });
+});
diff --git a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts
index 7415ba501f82..a6abbd80e27b 100644
--- a/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts
+++ b/packages/twenty-front/src/modules/activities/emails/right-drawer/hooks/useRightDrawerEmailThread.ts
@@ -3,13 +3,13 @@ import { useApolloClient } from '@apollo/client';
import gql from 'graphql-tag';
import { useRecoilValue } from 'recoil';
-import { viewableEmailThreadIdState } from '@/activities/emails/state/viewableEmailThreadIdState';
+import { viewableEmailThreadIdState } from '@/activities/emails/states/viewableEmailThreadIdState';
import { EmailThreadMessage as EmailThreadMessageType } from '@/activities/emails/types/EmailThreadMessage';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
export const useRightDrawerEmailThread = () => {
- const viewableEmailThreadId = useRecoilValue(viewableEmailThreadIdState());
+ const viewableEmailThreadId = useRecoilValue(viewableEmailThreadIdState);
const apolloClient = useApolloClient();
const thread = apolloClient.readFragment({
diff --git a/packages/twenty-front/src/modules/activities/emails/state/emailThreadsPageComponentState.ts b/packages/twenty-front/src/modules/activities/emails/states/emailThreadsPageComponentState.ts
similarity index 100%
rename from packages/twenty-front/src/modules/activities/emails/state/emailThreadsPageComponentState.ts
rename to packages/twenty-front/src/modules/activities/emails/states/emailThreadsPageComponentState.ts
diff --git a/packages/twenty-front/src/modules/activities/emails/state/lastViewableEmailThreadIdState.ts b/packages/twenty-front/src/modules/activities/emails/states/lastViewableEmailThreadIdState.ts
similarity index 100%
rename from packages/twenty-front/src/modules/activities/emails/state/lastViewableEmailThreadIdState.ts
rename to packages/twenty-front/src/modules/activities/emails/states/lastViewableEmailThreadIdState.ts
diff --git a/packages/twenty-front/src/modules/activities/emails/state/viewableEmailThreadIdState.ts b/packages/twenty-front/src/modules/activities/emails/states/viewableEmailThreadIdState.ts
similarity index 100%
rename from packages/twenty-front/src/modules/activities/emails/state/viewableEmailThreadIdState.ts
rename to packages/twenty-front/src/modules/activities/emails/states/viewableEmailThreadIdState.ts
diff --git a/packages/twenty-front/src/modules/activities/emails/utils/__tests__/getDisplayNameFromParticipant.test.ts b/packages/twenty-front/src/modules/activities/emails/utils/__tests__/getDisplayNameFromParticipant.test.ts
new file mode 100644
index 000000000000..e7647d869483
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/emails/utils/__tests__/getDisplayNameFromParticipant.test.ts
@@ -0,0 +1,100 @@
+import { EmailThreadMessageParticipant } from '@/activities/emails/types/EmailThreadMessageParticipant';
+
+import { getDisplayNameFromParticipant } from '../getDisplayNameFromParticipant';
+
+describe('getDisplayNameFromParticipant', () => {
+ const participantWithName: EmailThreadMessageParticipant = {
+ displayName: '',
+ handle: '',
+ role: 'from',
+ person: {
+ id: '1',
+ createdAt: '',
+ updatedAt: '',
+ deletedAt: null,
+ name: {
+ firstName: 'John',
+ lastName: 'Doe',
+ },
+ avatarUrl: '',
+ jobTitle: '',
+ linkedinLink: {
+ url: '',
+ label: '',
+ },
+ xLink: {
+ url: '',
+ label: '',
+ },
+ city: '',
+ email: '',
+ phone: '',
+ companyId: '',
+ },
+ workspaceMember: {
+ id: '1',
+ name: {
+ firstName: 'Jane',
+ lastName: 'Smith',
+ },
+ locale: '',
+ createdAt: '',
+ updatedAt: '',
+ userEmail: '',
+ userId: '',
+ },
+ };
+
+ const participantWithHandle: any = {
+ displayName: '',
+ handle: 'user_handle',
+ role: 'from',
+ };
+
+ const participantWithDisplayName: any = {
+ displayName: 'User123',
+ handle: '',
+ role: 'from',
+ };
+
+ const participantWithoutInfo: any = {
+ displayName: '',
+ handle: '',
+ role: 'from',
+ };
+
+ it('should return full name when shouldUseFullName is true', () => {
+ expect(
+ getDisplayNameFromParticipant({
+ participant: participantWithName,
+ shouldUseFullName: true,
+ }),
+ ).toBe('John Doe');
+ });
+
+ it('should return first name when shouldUseFullName is false', () => {
+ expect(
+ getDisplayNameFromParticipant({ participant: participantWithName }),
+ ).toBe('John');
+ });
+
+ it('should return displayName if it is a non-empty string', () => {
+ expect(
+ getDisplayNameFromParticipant({
+ participant: participantWithDisplayName,
+ }),
+ ).toBe('User123');
+ });
+
+ it('should return handle if displayName is not available', () => {
+ expect(
+ getDisplayNameFromParticipant({ participant: participantWithHandle }),
+ ).toBe('user_handle');
+ });
+
+ it('should return Unknown if no suitable information is available', () => {
+ expect(
+ getDisplayNameFromParticipant({ participant: participantWithoutInfo }),
+ ).toBe('Unknown');
+ });
+});
diff --git a/packages/twenty-front/src/modules/activities/events/components/EventList.tsx b/packages/twenty-front/src/modules/activities/events/components/EventList.tsx
new file mode 100644
index 000000000000..158e3ba1215f
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/events/components/EventList.tsx
@@ -0,0 +1,22 @@
+import { ReactElement } from 'react';
+
+import { EventRow } from '@/activities/events/components/EventRow';
+import { Event } from '@/activities/events/types/Event';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
+
+type EventListProps = {
+ targetableObject: ActivityTargetableObject;
+ title: string;
+ events: Event[];
+ button?: ReactElement | false;
+};
+
+export const EventList = ({ events }: EventListProps) => {
+ return (
+ <>
+ {events &&
+ events.length > 0 &&
+ events.map((event: Event) => )}
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/activities/events/components/EventRow.tsx b/packages/twenty-front/src/modules/activities/events/components/EventRow.tsx
new file mode 100644
index 000000000000..d7bbc2d7556b
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/events/components/EventRow.tsx
@@ -0,0 +1,11 @@
+import { Event } from '@/activities/events/types/Event';
+
+export const EventRow = ({ event }: { event: Event }) => {
+ return (
+ <>
+
+ {event.name}:
{event.properties}
+
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/activities/events/components/Events.tsx b/packages/twenty-front/src/modules/activities/events/components/Events.tsx
new file mode 100644
index 000000000000..11536d79ac9d
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/events/components/Events.tsx
@@ -0,0 +1,25 @@
+import { isNonEmptyArray } from '@sniptt/guards';
+
+import { EventList } from '@/activities/events/components/EventList';
+import { useEvents } from '@/activities/events/hooks/useEvents';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
+
+export const Events = ({
+ targetableObject,
+}: {
+ targetableObject: ActivityTargetableObject;
+}) => {
+ const { events } = useEvents(targetableObject);
+
+ if (!isNonEmptyArray(events)) {
+ return No log yet
;
+ }
+
+ return (
+
+ );
+};
diff --git a/packages/twenty-front/src/modules/activities/events/hooks/__tests__/useEvents.test.ts b/packages/twenty-front/src/modules/activities/events/hooks/__tests__/useEvents.test.ts
new file mode 100644
index 000000000000..25c6d5bf1209
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/events/hooks/__tests__/useEvents.test.ts
@@ -0,0 +1,91 @@
+import { renderHook } from '@testing-library/react';
+
+import { useEvents } from '@/activities/events/hooks/useEvents';
+
+jest.mock('@/object-record/hooks/useFindManyRecords', () => ({
+ useFindManyRecords: jest.fn(),
+}));
+
+describe('useEvent', () => {
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('fetches events correctly for a given targetableObject', () => {
+ const mockEvents = [
+ {
+ __typename: 'Event',
+ id: '166ec73f-26b1-4934-bb3b-c86c8513b99b',
+ opportunityId: null,
+ opportunity: null,
+ personId: null,
+ person: null,
+ company: {
+ __typename: 'Company',
+ address: 'Paris',
+ linkedinLink: {
+ __typename: 'Link',
+ label: '',
+ url: '',
+ },
+ xLink: {
+ __typename: 'Link',
+ label: '',
+ url: '',
+ },
+ position: 4,
+ domainName: 'microsoft.com',
+ employees: null,
+ createdAt: '2024-03-21T16:01:41.809Z',
+ annualRecurringRevenue: {
+ __typename: 'Currency',
+ amountMicros: 100000000,
+ currencyCode: 'USD',
+ },
+ idealCustomerProfile: false,
+ accountOwnerId: null,
+ updatedAt: '2024-03-22T08:28:44.812Z',
+ name: 'Microsoft',
+ id: '460b6fb1-ed89-413a-b31a-962986e67bb4',
+ },
+ workspaceMember: {
+ __typename: 'WorkspaceMember',
+ locale: 'en',
+ avatarUrl: '',
+ updatedAt: '2024-03-21T16:01:41.839Z',
+ name: {
+ __typename: 'FullName',
+ firstName: 'Tim',
+ lastName: 'Apple',
+ },
+ id: '20202020-0687-4c41-b707-ed1bfca972a7',
+ userEmail: 'tim@apple.dev',
+ colorScheme: 'Light',
+ createdAt: '2024-03-21T16:01:41.839Z',
+ userId: '20202020-9e3b-46d4-a556-88b9ddc2b034',
+ },
+ workspaceMemberId: '20202020-0687-4c41-b707-ed1bfca972a7',
+ createdAt: '2024-03-22T08:28:44.830Z',
+ name: 'updated.company',
+ companyId: '460b6fb1-ed89-413a-b31a-962986e67bb4',
+ properties: '{"diff": {"address": {"after": "Paris", "before": ""}}}',
+ updatedAt: '2024-03-22T08:28:44.830Z',
+ },
+ ];
+ const mockTargetableObject = {
+ id: '1',
+ targetObjectNameSingular: 'Opportunity',
+ };
+
+ const useFindManyRecordsMock = jest.requireMock(
+ '@/object-record/hooks/useFindManyRecords',
+ );
+ useFindManyRecordsMock.useFindManyRecords.mockReturnValue({
+ records: mockEvents,
+ });
+
+ const { result } = renderHook(() => useEvents(mockTargetableObject));
+
+ expect(result.current.events).toEqual(mockEvents);
+ });
+});
diff --git a/packages/twenty-front/src/modules/activities/events/hooks/useEvents.tsx b/packages/twenty-front/src/modules/activities/events/hooks/useEvents.tsx
new file mode 100644
index 000000000000..8e37947a6566
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/events/hooks/useEvents.tsx
@@ -0,0 +1,28 @@
+import { Event } from '@/activities/events/types/Event';
+import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
+import { getActivityTargetObjectFieldIdName } from '@/activities/utils/getTargetObjectFilterFieldName';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
+
+// do we need to test this?
+export const useEvents = (targetableObject: ActivityTargetableObject) => {
+ const targetableObjectFieldIdName = getActivityTargetObjectFieldIdName({
+ nameSingular: targetableObject.targetObjectNameSingular,
+ });
+
+ const { records: events } = useFindManyRecords({
+ objectNameSingular: CoreObjectNameSingular.Event,
+ filter: {
+ [targetableObjectFieldIdName]: {
+ eq: targetableObject.id,
+ },
+ },
+ orderBy: {
+ createdAt: 'DescNullsFirst',
+ },
+ });
+
+ return {
+ events: events as Event[],
+ };
+};
diff --git a/packages/twenty-front/src/modules/activities/events/types/Event.ts b/packages/twenty-front/src/modules/activities/events/types/Event.ts
new file mode 100644
index 000000000000..1752b8367c2a
--- /dev/null
+++ b/packages/twenty-front/src/modules/activities/events/types/Event.ts
@@ -0,0 +1,12 @@
+export type Event = {
+ id: string;
+ createdAt: string;
+ updatedAt: string;
+ deletedAt: string | null;
+ opportunityId: string | null;
+ companyId: string;
+ personId: string;
+ workspaceMemberId: string;
+ properties: any;
+ name: string;
+};
diff --git a/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx b/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx
index bfc66abd4846..1a0cceee7a5d 100644
--- a/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx
+++ b/packages/twenty-front/src/modules/activities/files/hooks/useUploadAttachmentFile.tsx
@@ -10,7 +10,7 @@ import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { FileFolder, useUploadFileMutation } from '~/generated/graphql';
export const useUploadAttachmentFile = () => {
- const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState());
+ const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const [uploadFile] = useUploadFileMutation();
const { createOneRecord: createOneAttachment } =
diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivities.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivities.test.tsx
index 56730f9c8ffe..53d32b2694f6 100644
--- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivities.test.tsx
+++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivities.test.tsx
@@ -201,7 +201,7 @@ describe('useActivities', () => {
const { result } = renderHook(
() => {
const setCurrentWorkspaceMember = useSetRecoilState(
- currentWorkspaceMemberState(),
+ currentWorkspaceMemberState,
);
const activities = useActivities({
diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityById.test.ts b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityById.test.ts
deleted file mode 100644
index 40f4521a9b33..000000000000
--- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityById.test.ts
+++ /dev/null
@@ -1,38 +0,0 @@
-import { renderHook } from '@testing-library/react';
-import { RecoilRoot } from 'recoil';
-
-import { useActivityById } from '../useActivityById';
-
-jest.mock('@/object-record/hooks/useFindOneRecord', () => ({
- useFindOneRecord: jest.fn(() => ({
- record: {
- activity: {
- id: 'test-activity-id',
- name: 'Test Activity',
- description: 'This is a test activity',
- },
- },
- loading: false,
- })),
-}));
-
-describe('useActivityById', () => {
- it('fetches activity by id and returns the activity and loading state', async () => {
- const activityId = 'test-activity-id';
- const { result } = renderHook(() => useActivityById({ activityId }), {
- wrapper: RecoilRoot,
- });
-
- expect(result.current.loading).toBe(false);
-
- expect(result.current.activity).toEqual({
- activity: {
- id: 'test-activity-id',
- name: 'Test Activity',
- description: 'This is a test activity',
- },
- activityTargets: [],
- comments: [],
- });
- });
-});
diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityConnectionUtils.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityConnectionUtils.test.tsx
index ddda904ee442..c596ecfbd67e 100644
--- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityConnectionUtils.test.tsx
+++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityConnectionUtils.test.tsx
@@ -58,7 +58,7 @@ describe('useActivityConnectionUtils', () => {
{
snapshot.set(
- objectMetadataItemsState(),
+ objectMetadataItemsState,
getObjectMetadataItemsMock(),
);
}}
@@ -86,7 +86,7 @@ describe('useActivityConnectionUtils', () => {
{
snapshot.set(
- objectMetadataItemsState(),
+ objectMetadataItemsState,
getObjectMetadataItemsMock(),
);
}}
diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx
index 0a1b8f7b405f..e1041843dc88 100644
--- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx
+++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetObjectRecords.test.tsx
@@ -186,10 +186,10 @@ describe('useActivityTargetObjectRecords', () => {
const { result } = renderHook(
() => {
const setCurrentWorkspaceMember = useSetRecoilState(
- currentWorkspaceMemberState(),
+ currentWorkspaceMemberState,
);
const setObjectMetadataItems = useSetRecoilState(
- objectMetadataItemsState(),
+ objectMetadataItemsState,
);
const { activityTargetObjectRecords, loadingActivityTargets } =
diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetsForTargetableObject.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetsForTargetableObject.test.tsx
index e1e55e9c3102..dedd2e3a0cfd 100644
--- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetsForTargetableObject.test.tsx
+++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useActivityTargetsForTargetableObject.test.tsx
@@ -102,7 +102,7 @@ describe('useActivityTargetsForTargetableObject', () => {
const { result } = renderHook(
() => {
const setCurrentWorkspaceMember = useSetRecoilState(
- currentWorkspaceMemberState(),
+ currentWorkspaceMemberState,
);
const res = useActivityTargetsForTargetableObject({
diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useAttachRelationInBothDirections.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useAttachRelationInBothDirections.test.tsx
index e55118b7a05c..a3e8fd73f063 100644
--- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useAttachRelationInBothDirections.test.tsx
+++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useAttachRelationInBothDirections.test.tsx
@@ -29,10 +29,10 @@ describe('useAttachRelationInBothDirections', () => {
const { result } = renderHook(
() => {
const setCurrentWorkspaceMember = useSetRecoilState(
- currentWorkspaceMemberState(),
+ currentWorkspaceMemberState,
);
const setObjectMetadataItems = useSetRecoilState(
- objectMetadataItemsState(),
+ objectMetadataItemsState,
);
const res = useAttachRelationInBothDirections();
diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useCreateActivityInCache.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useCreateActivityInCache.test.tsx
index 08a571a2e77c..a23ee3b4eb85 100644
--- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useCreateActivityInCache.test.tsx
+++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useCreateActivityInCache.test.tsx
@@ -59,10 +59,10 @@ describe('useCreateActivityInCache', () => {
const { result } = renderHook(
() => {
const setCurrentWorkspaceMember = useSetRecoilState(
- currentWorkspaceMemberState(),
+ currentWorkspaceMemberState,
);
const setObjectMetadataItems = useSetRecoilState(
- objectMetadataItemsState(),
+ objectMetadataItemsState,
);
const res = useCreateActivityInCache();
diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenActivityRightDrawer.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenActivityRightDrawer.test.tsx
index 2a323f3bde7c..19a027551a21 100644
--- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenActivityRightDrawer.test.tsx
+++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenActivityRightDrawer.test.tsx
@@ -18,8 +18,8 @@ describe('useOpenActivityRightDrawer', () => {
const { result } = renderHook(
() => {
const openActivityRightDrawer = useOpenActivityRightDrawer();
- const viewableActivityId = useRecoilValue(viewableActivityIdState());
- const activityIdInDrawer = useRecoilValue(activityIdInDrawerState());
+ const viewableActivityId = useRecoilValue(viewableActivityIdState);
+ const activityIdInDrawer = useRecoilValue(activityIdInDrawerState);
return {
openActivityRightDrawer,
activityIdInDrawer,
diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawer.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawer.test.tsx
index ee294657c8ef..eb60717a4125 100644
--- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawer.test.tsx
+++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawer.test.tsx
@@ -28,10 +28,10 @@ describe('useOpenCreateActivityDrawer', () => {
const { result } = renderHook(
() => {
const openActivityRightDrawer = useOpenCreateActivityDrawer();
- const viewableActivityId = useRecoilValue(viewableActivityIdState());
- const activityIdInDrawer = useRecoilValue(activityIdInDrawerState());
+ const viewableActivityId = useRecoilValue(viewableActivityIdState);
+ const activityIdInDrawer = useRecoilValue(activityIdInDrawerState);
const setObjectMetadataItems = useSetRecoilState(
- objectMetadataItemsState(),
+ objectMetadataItemsState,
);
return {
openActivityRightDrawer,
diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawerForSelectedRowIds.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawerForSelectedRowIds.test.tsx
index 6a4ecc83de2b..7df977a2ad65 100644
--- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawerForSelectedRowIds.test.tsx
+++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useOpenCreateActivityDrawerForSelectedRowIds.test.tsx
@@ -41,10 +41,10 @@ describe('useOpenCreateActivityDrawerForSelectedRowIds', () => {
() => {
const openCreateActivityDrawerForSelectedRowIds =
useOpenCreateActivityDrawerForSelectedRowIds(recordTableId);
- const viewableActivityId = useRecoilValue(viewableActivityIdState());
- const activityIdInDrawer = useRecoilValue(activityIdInDrawerState());
+ const viewableActivityId = useRecoilValue(viewableActivityIdState);
+ const activityIdInDrawer = useRecoilValue(activityIdInDrawerState);
const setObjectMetadataItems = useSetRecoilState(
- objectMetadataItemsState(),
+ objectMetadataItemsState,
);
const scopeId = `${recordTableId}-scope`;
const setTableRowIds = useSetRecoilState(
diff --git a/packages/twenty-front/src/modules/activities/hooks/__tests__/useUpsertActivity.test.tsx b/packages/twenty-front/src/modules/activities/hooks/__tests__/useUpsertActivity.test.tsx
index fcfb2c94c225..ddf4d40e5eea 100644
--- a/packages/twenty-front/src/modules/activities/hooks/__tests__/useUpsertActivity.test.tsx
+++ b/packages/twenty-front/src/modules/activities/hooks/__tests__/useUpsertActivity.test.tsx
@@ -105,7 +105,7 @@ describe('useUpsertActivity', () => {
() => {
const res = useUpsertActivity();
const setIsActivityInCreateMode = useSetRecoilState(
- isActivityInCreateModeState(),
+ isActivityInCreateModeState,
);
return { ...res, setIsActivityInCreateMode };
@@ -134,7 +134,7 @@ describe('useUpsertActivity', () => {
() => {
const res = useUpsertActivity();
const setIsActivityInCreateMode = useSetRecoilState(
- isActivityInCreateModeState(),
+ isActivityInCreateModeState,
);
const setObjectShowPageTargetableObject = useSetRecoilState(
objectShowPageTargetableObjectState,
diff --git a/packages/twenty-front/src/modules/activities/hooks/useActivityTargetObjectRecords.ts b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetObjectRecords.ts
index 63e87ba07d27..359b1c7550b6 100644
--- a/packages/twenty-front/src/modules/activities/hooks/useActivityTargetObjectRecords.ts
+++ b/packages/twenty-front/src/modules/activities/hooks/useActivityTargetObjectRecords.ts
@@ -14,7 +14,7 @@ export const useActivityTargetObjectRecords = ({
}: {
activityId: string;
}) => {
- const objectMetadataItems = useRecoilValue(objectMetadataItemsState());
+ const objectMetadataItems = useRecoilValue(objectMetadataItemsState);
const { records: activityTargets, loading: loadingActivityTargets } =
useFindManyRecords({
diff --git a/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInCache.ts b/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInCache.ts
index 1479b0580a13..4c1a927f7ad3 100644
--- a/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInCache.ts
+++ b/packages/twenty-front/src/modules/activities/hooks/useCreateActivityInCache.ts
@@ -27,7 +27,7 @@ export const useCreateActivityInCache = () => {
objectNameSingular: CoreObjectNameSingular.Activity,
});
- const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState());
+ const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { record: currentWorkspaceMemberRecord } = useFindOneRecord({
objectNameSingular: CoreObjectNameSingular.WorkspaceMember,
diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts
index ae4d579a7c16..b0279e17eece 100644
--- a/packages/twenty-front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts
+++ b/packages/twenty-front/src/modules/activities/hooks/useOpenActivityRightDrawer.ts
@@ -12,9 +12,9 @@ export const useOpenActivityRightDrawer = () => {
const { openRightDrawer, isRightDrawerOpen, rightDrawerPage } =
useRightDrawer();
const [viewableActivityId, setViewableActivityId] = useRecoilState(
- viewableActivityIdState(),
+ viewableActivityIdState,
);
- const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState());
+ const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState);
const setHotkeyScope = useSetHotkeyScope();
return (activityId: string) => {
diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts
index 5b4c313fbb0d..bcd10fcd776a 100644
--- a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts
+++ b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawer.ts
@@ -24,22 +24,20 @@ export const useOpenCreateActivityDrawer = () => {
const { createActivityInCache } = useCreateActivityInCache();
const setActivityTargetableEntityArray = useSetRecoilState(
- activityTargetableEntityArrayState(),
+ activityTargetableEntityArrayState,
);
- const setViewableActivityId = useSetRecoilState(viewableActivityIdState());
+ const setViewableActivityId = useSetRecoilState(viewableActivityIdState);
- const setIsCreatingActivity = useSetRecoilState(
- isActivityInCreateModeState(),
- );
+ const setIsCreatingActivity = useSetRecoilState(isActivityInCreateModeState);
const setTemporaryActivityForEditor = useSetRecoilState(
- temporaryActivityForEditorState(),
+ temporaryActivityForEditorState,
);
- const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState());
+ const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState);
const [, setIsUpsertingActivityInDB] = useRecoilState(
- isUpsertingActivityInDBState(),
+ isUpsertingActivityInDBState,
);
const openCreateActivityDrawer = async ({
diff --git a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts
index 11cd38d5416a..90713931b61f 100644
--- a/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts
+++ b/packages/twenty-front/src/modules/activities/hooks/useOpenCreateActivityDrawerForSelectedRowIds.ts
@@ -14,7 +14,7 @@ export const useOpenCreateActivityDrawerForSelectedRowIds = (
) => {
const openCreateActivityDrawer = useOpenCreateActivityDrawer();
- const { getSelectedRowIdsSelector } = useRecordTableStates(recordTableId);
+ const { selectedRowIdsSelector } = useRecordTableStates(recordTableId);
return useRecoilCallback(
({ snapshot }) =>
@@ -25,7 +25,7 @@ export const useOpenCreateActivityDrawerForSelectedRowIds = (
) => {
const selectedRowIds = getSnapshotValue(
snapshot,
- getSelectedRowIdsSelector(),
+ selectedRowIdsSelector(),
);
let activityTargetableObjectArray: ActivityTargetableObject[] =
@@ -59,6 +59,6 @@ export const useOpenCreateActivityDrawerForSelectedRowIds = (
targetableObjects: activityTargetableObjectArray,
});
},
- [openCreateActivityDrawer, getSelectedRowIdsSelector],
+ [selectedRowIdsSelector, openCreateActivityDrawer],
);
};
diff --git a/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts b/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts
index 2634eddc1057..c383d22dfee4 100644
--- a/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts
+++ b/packages/twenty-front/src/modules/activities/hooks/useUpsertActivity.ts
@@ -21,7 +21,7 @@ import { isDefined } from '~/utils/isDefined';
// TODO: create a generic way to have records only in cache for create mode and delete them afterwards ?
export const useUpsertActivity = () => {
const [isActivityInCreateMode, setIsActivityInCreateMode] = useRecoilState(
- isActivityInCreateModeState(),
+ isActivityInCreateModeState,
);
const { updateOneRecord: updateOneActivity } = useUpdateOneRecord({
@@ -31,10 +31,10 @@ export const useUpsertActivity = () => {
const { createActivityInDB } = useCreateActivityInDB();
const [, setIsUpsertingActivityInDB] = useRecoilState(
- isUpsertingActivityInDBState(),
+ isUpsertingActivityInDBState,
);
- const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState());
+ const setActivityIdInDrawer = useSetRecoilState(activityIdInDrawerState);
const objectShowPageTargetableObject = useRecoilValue(
objectShowPageTargetableObjectState,
diff --git a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx
index 2c2c3a68edb4..3275f09233ee 100644
--- a/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx
+++ b/packages/twenty-front/src/modules/activities/inline-cell/components/ActivityTargetInlineCellEditMode.tsx
@@ -34,9 +34,7 @@ export const ActivityTargetInlineCellEditMode = ({
activity,
activityTargetWithTargetRecords,
}: ActivityTargetInlineCellEditModeProps) => {
- const [isActivityInCreateMode] = useRecoilState(
- isActivityInCreateModeState(),
- );
+ const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
const selectedTargetObjectIds = activityTargetWithTargetRecords.map(
(activityTarget) => ({
diff --git a/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx b/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx
index 1069e8f794fe..40ed872f97fb 100644
--- a/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx
+++ b/packages/twenty-front/src/modules/activities/right-drawer/components/ActivityActionBar.tsx
@@ -36,13 +36,13 @@ const StyledButtonContainer = styled.div`
`;
export const ActivityActionBar = () => {
- const viewableActivityId = useRecoilValue(viewableActivityIdState());
- const activityIdInDrawer = useRecoilValue(activityIdInDrawerState());
+ const viewableActivityId = useRecoilValue(viewableActivityIdState);
+ const activityIdInDrawer = useRecoilValue(activityIdInDrawerState);
const activityTargetableEntityArray = useRecoilValue(
- activityTargetableEntityArrayState(),
+ activityTargetableEntityArrayState,
);
- const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState());
+ const [, setIsRightDrawerOpen] = useRecoilState(isRightDrawerOpenState);
const { deleteOneRecord: deleteOneActivity } = useDeleteOneRecord({
objectNameSingular: CoreObjectNameSingular.Activity,
});
@@ -54,15 +54,13 @@ export const ActivityActionBar = () => {
);
const [temporaryActivityForEditor, setTemporaryActivityForEditor] =
- useRecoilState(temporaryActivityForEditorState());
+ useRecoilState(temporaryActivityForEditorState);
const { deleteActivityFromCache } = useDeleteActivityFromCache();
- const [isActivityInCreateMode] = useRecoilState(
- isActivityInCreateModeState(),
- );
+ const [isActivityInCreateMode] = useRecoilState(isActivityInCreateModeState);
const [isUpsertingActivityInDB] = useRecoilState(
- isUpsertingActivityInDBState(),
+ isUpsertingActivityInDBState,
);
const objectShowPageTargetableObject = useRecoilValue(
diff --git a/packages/twenty-front/src/modules/activities/right-drawer/components/RightDrawerActivityTopBar.tsx b/packages/twenty-front/src/modules/activities/right-drawer/components/RightDrawerActivityTopBar.tsx
index 628fc3893d8f..2e3c35fe4cf2 100644
--- a/packages/twenty-front/src/modules/activities/right-drawer/components/RightDrawerActivityTopBar.tsx
+++ b/packages/twenty-front/src/modules/activities/right-drawer/components/RightDrawerActivityTopBar.tsx
@@ -1,17 +1,20 @@
import styled from '@emotion/styled';
import { ActivityActionBar } from '@/activities/right-drawer/components/ActivityActionBar';
+import { RightDrawerTopBarCloseButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarCloseButton';
+import { RightDrawerTopBarExpandButton } from '@/ui/layout/right-drawer/components/RightDrawerTopBarExpandButton';
import { StyledRightDrawerTopBar } from '@/ui/layout/right-drawer/components/StyledRightDrawerTopBar';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
-import { RightDrawerTopBarCloseButton } from '../../../ui/layout/right-drawer/components/RightDrawerTopBarCloseButton';
-import { RightDrawerTopBarExpandButton } from '../../../ui/layout/right-drawer/components/RightDrawerTopBarExpandButton';
+type RightDrawerActivityTopBarProps = { showActionBar?: boolean };
const StyledTopBarWrapper = styled.div`
display: flex;
`;
-export const RightDrawerActivityTopBar = () => {
+export const RightDrawerActivityTopBar = ({
+ showActionBar = true,
+}: RightDrawerActivityTopBarProps) => {
const isMobile = useIsMobile();
return (
@@ -20,7 +23,7 @@ export const RightDrawerActivityTopBar = () => {
{!isMobile && }
-
+ {showActionBar && }
);
};
diff --git a/packages/twenty-front/src/modules/activities/right-drawer/components/create/RightDrawerCreateActivity.tsx b/packages/twenty-front/src/modules/activities/right-drawer/components/create/RightDrawerCreateActivity.tsx
index 4ec49de22aca..fe6ac8ecaf43 100644
--- a/packages/twenty-front/src/modules/activities/right-drawer/components/create/RightDrawerCreateActivity.tsx
+++ b/packages/twenty-front/src/modules/activities/right-drawer/components/create/RightDrawerCreateActivity.tsx
@@ -5,7 +5,7 @@ import { viewableActivityIdState } from '@/activities/states/viewableActivityIdS
import { RightDrawerActivity } from '../RightDrawerActivity';
export const RightDrawerCreateActivity = () => {
- const viewableActivityId = useRecoilValue(viewableActivityIdState());
+ const viewableActivityId = useRecoilValue(viewableActivityIdState);
return (
<>
diff --git a/packages/twenty-front/src/modules/activities/right-drawer/components/edit/RightDrawerEditActivity.tsx b/packages/twenty-front/src/modules/activities/right-drawer/components/edit/RightDrawerEditActivity.tsx
index 971d34b81bb4..288d7aca88af 100644
--- a/packages/twenty-front/src/modules/activities/right-drawer/components/edit/RightDrawerEditActivity.tsx
+++ b/packages/twenty-front/src/modules/activities/right-drawer/components/edit/RightDrawerEditActivity.tsx
@@ -5,7 +5,7 @@ import { viewableActivityIdState } from '@/activities/states/viewableActivityIdS
import { RightDrawerActivity } from '../RightDrawerActivity';
export const RightDrawerEditActivity = () => {
- const viewableActivityId = useRecoilValue(viewableActivityIdState());
+ const viewableActivityId = useRecoilValue(viewableActivityIdState);
return (
<>
diff --git a/packages/twenty-front/src/modules/activities/tasks/components/CurrentUserDueTaskCountEffect.tsx b/packages/twenty-front/src/modules/activities/tasks/components/CurrentUserDueTaskCountEffect.tsx
index a6819f2342ca..3f82be635be6 100644
--- a/packages/twenty-front/src/modules/activities/tasks/components/CurrentUserDueTaskCountEffect.tsx
+++ b/packages/twenty-front/src/modules/activities/tasks/components/CurrentUserDueTaskCountEffect.tsx
@@ -9,10 +9,10 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { parseDate } from '~/utils/date-utils';
export const CurrentUserDueTaskCountEffect = () => {
- const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState());
+ const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const [currentUserDueTaskCount, setCurrentUserDueTaskCount] = useRecoilState(
- currentUserDueTaskCountState(),
+ currentUserDueTaskCountState,
);
const { records: tasks } = useFindManyRecords({
diff --git a/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx b/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx
index bdc9b1c17049..3d1ad6a43bb1 100644
--- a/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx
+++ b/packages/twenty-front/src/modules/activities/tasks/components/TaskGroups.tsx
@@ -48,8 +48,8 @@ export const TaskGroups = ({
const openCreateActivity = useOpenCreateActivityDrawer();
- const { getActiveTabIdState } = useTabList(TASKS_TAB_LIST_COMPONENT_ID);
- const activeTabId = useRecoilValue(getActiveTabIdState());
+ const { activeTabIdState } = useTabList(TASKS_TAB_LIST_COMPONENT_ID);
+ const activeTabId = useRecoilValue(activeTabIdState);
if (!initialized) {
return <>>;
diff --git a/packages/twenty-front/src/modules/activities/tasks/hooks/__tests__/useCompleteTask.test.ts b/packages/twenty-front/src/modules/activities/tasks/hooks/__tests__/useCompleteTask.test.ts
deleted file mode 100644
index e74a741b16f4..000000000000
--- a/packages/twenty-front/src/modules/activities/tasks/hooks/__tests__/useCompleteTask.test.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-import { renderHook } from '@testing-library/react';
-
-import { useCompleteTask } from '@/activities/tasks/hooks/useCompleteTask';
-
-const mockUpdateOneRecord = jest.fn();
-jest.mock('@/object-record/hooks/useUpdateOneRecord', () => ({
- useUpdateOneRecord: () => ({
- updateOneRecord: mockUpdateOneRecord,
- }),
-}));
-
-describe('useCompleteTask', () => {
- it('should complete the task when called with true', async () => {
- const taskId = 'test-task-id';
- const { result } = renderHook(() =>
- useCompleteTask({ id: taskId, completedAt: null }),
- );
-
- const { completeTask } = result.current;
- completeTask(true);
-
- expect(mockUpdateOneRecord).toHaveBeenCalledWith({
- idToUpdate: taskId,
- updateOneRecordInput: {
- completedAt: expect.any(String),
- },
- });
- });
-
- it('should uncomplete the task when called with false', async () => {
- const taskId = 'test-task-id';
- const { result } = renderHook(() =>
- useCompleteTask({ id: taskId, completedAt: '2021-01-01T00:00:00' }),
- );
-
- const { completeTask } = result.current;
-
- completeTask(false);
-
- expect(mockUpdateOneRecord).toHaveBeenCalledWith({
- idToUpdate: taskId,
- updateOneRecordInput: {
- completedAt: null,
- },
- });
- });
-});
diff --git a/packages/twenty-front/src/modules/activities/tasks/hooks/useCurrentUserDueTaskCount.ts b/packages/twenty-front/src/modules/activities/tasks/hooks/useCurrentUserDueTaskCount.ts
index 8ef7e168f925..836ed21bad86 100644
--- a/packages/twenty-front/src/modules/activities/tasks/hooks/useCurrentUserDueTaskCount.ts
+++ b/packages/twenty-front/src/modules/activities/tasks/hooks/useCurrentUserDueTaskCount.ts
@@ -7,7 +7,7 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { parseDate } from '~/utils/date-utils';
export const useCurrentUserTaskCount = () => {
- const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState());
+ const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const { records: tasks } = useFindManyRecords({
objectNameSingular: CoreObjectNameSingular.Activity,
diff --git a/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts b/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts
index b4d7b0179f00..0adbe01f18b3 100644
--- a/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts
+++ b/packages/twenty-front/src/modules/activities/tasks/hooks/useTasks.ts
@@ -1,7 +1,7 @@
import { useEffect, useMemo } from 'react';
import { isNonEmptyArray } from '@sniptt/guards';
import { DateTime } from 'luxon';
-import { useRecoilState } from 'recoil';
+import { useRecoilState, useRecoilValue } from 'recoil';
import { useActivities } from '@/activities/hooks/useActivities';
import { currentCompletedTaskQueryVariablesState } from '@/activities/tasks/states/currentCompletedTaskQueryVariablesState';
@@ -23,10 +23,12 @@ export const useTasks = ({
targetableObjects,
filterDropdownId,
}: UseTasksProps) => {
- const { selectedFilter } = useFilterDropdown({
+ const { selectedFilterState } = useFilterDropdown({
filterDropdownId,
});
+ const selectedFilter = useRecoilValue(selectedFilterState);
+
const assigneeIdFilter = useMemo(
() =>
selectedFilter
diff --git a/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx b/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx
index 96621b34a45a..37cc77560ed9 100644
--- a/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx
+++ b/packages/twenty-front/src/modules/activities/timeline/components/Timeline.tsx
@@ -33,7 +33,7 @@ export const Timeline = ({
targetableObject: ActivityTargetableObject;
}) => {
const { initialized, noActivities } = useRecoilValue(
- timelineActivitiesNetworkingState(),
+ timelineActivitiesNetworkingState,
);
const showEmptyState = noActivities;
diff --git a/packages/twenty-front/src/modules/activities/timeline/components/TimelineCreateButtonGroup.tsx b/packages/twenty-front/src/modules/activities/timeline/components/TimelineCreateButtonGroup.tsx
index b8542cd19fee..e53b9ddd84f4 100644
--- a/packages/twenty-front/src/modules/activities/timeline/components/TimelineCreateButtonGroup.tsx
+++ b/packages/twenty-front/src/modules/activities/timeline/components/TimelineCreateButtonGroup.tsx
@@ -16,8 +16,8 @@ export const TimelineCreateButtonGroup = ({
}: {
targetableObject: ActivityTargetableObject;
}) => {
- const { getActiveTabIdState } = useTabList(TAB_LIST_COMPONENT_ID);
- const setActiveTabId = useSetRecoilState(getActiveTabIdState());
+ const { activeTabIdState } = useTabList(TAB_LIST_COMPONENT_ID);
+ const setActiveTabId = useSetRecoilState(activeTabIdState);
const openCreateActivity = useOpenCreateActivityDrawer();
diff --git a/packages/twenty-front/src/modules/activities/timeline/components/TimelineItemsContainer.tsx b/packages/twenty-front/src/modules/activities/timeline/components/TimelineItemsContainer.tsx
index 59a5a925e529..f509a4781aae 100644
--- a/packages/twenty-front/src/modules/activities/timeline/components/TimelineItemsContainer.tsx
+++ b/packages/twenty-front/src/modules/activities/timeline/components/TimelineItemsContainer.tsx
@@ -26,7 +26,7 @@ const StyledScrollWrapper = styled(ScrollWrapper)``;
export const TimelineItemsContainer = () => {
const timelineActivitiesForGroup = useRecoilValue(
- timelineActivitiesForGroupState(),
+ timelineActivitiesForGroupState,
);
const groupedActivities = groupActivitiesByMonth(timelineActivitiesForGroup);
diff --git a/packages/twenty-front/src/modules/activities/timeline/components/TimelineQueryEffect.tsx b/packages/twenty-front/src/modules/activities/timeline/components/TimelineQueryEffect.tsx
index cc0f24fdc144..26a2b781b791 100644
--- a/packages/twenty-front/src/modules/activities/timeline/components/TimelineQueryEffect.tsx
+++ b/packages/twenty-front/src/modules/activities/timeline/components/TimelineQueryEffect.tsx
@@ -35,10 +35,10 @@ export const TimelineQueryEffect = ({
});
const [timelineActivitiesNetworking, setTimelineActivitiesNetworking] =
- useRecoilState(timelineActivitiesNetworkingState());
+ useRecoilState(timelineActivitiesNetworkingState);
const [timelineActivitiesForGroup, setTimelineActivitiesForGroup] =
- useRecoilState(timelineActivitiesForGroupState());
+ useRecoilState(timelineActivitiesForGroupState);
useEffect(() => {
if (!isDefined(targetableObject)) {
diff --git a/packages/twenty-front/src/modules/activities/timeline/hooks/__tests__/useInjectIntoTimelineActivitiesQueries.test.ts b/packages/twenty-front/src/modules/activities/timeline/hooks/__tests__/useInjectIntoTimelineActivitiesQueries.test.ts
deleted file mode 100644
index 7b033859e1ba..000000000000
--- a/packages/twenty-front/src/modules/activities/timeline/hooks/__tests__/useInjectIntoTimelineActivitiesQueries.test.ts
+++ /dev/null
@@ -1,85 +0,0 @@
-import { renderHook } from '@testing-library/react';
-
-import { useInjectIntoActivitiesQueries } from '@/activities/hooks/useInjectIntoActivitiesQueries';
-import { useInjectIntoTimelineActivitiesQueries } from '@/activities/timeline/hooks/useInjectIntoTimelineActivitiesQueries';
-import { Activity } from '@/activities/types/Activity';
-import { ActivityTarget } from '@/activities/types/ActivityTarget';
-import { ActivityTargetableObject } from '@/activities/types/ActivityTargetableEntity';
-
-jest.mock('@/activities/hooks/useInjectIntoActivitiesQueries', () => ({
- useInjectIntoActivitiesQueries: jest.fn(() => ({
- injectActivitiesQueries: jest.fn(),
- })),
-}));
-
-describe('useInjectIntoTimelineActivitiesQueries', () => {
- beforeEach(() => {
- jest.clearAllMocks();
- });
-
- it('should inject activities into timeline activities queries correctly', () => {
- const mockActivityToInject: Activity = {
- id: 'activity1',
- createdAt: '2022-01-01T00:00:00',
- updatedAt: '2022-01-01T00:00:00',
- completedAt: '2022-01-01T00:00:00',
- __typename: 'Activity',
- reminderAt: '2022-01-01T00:00:00',
- dueAt: '2022-01-01T00:00:00',
- type: 'Task',
- activityTargets: [],
- title: 'Activity 1',
- body: 'Activity 1 body',
- author: {
- id: 'author1',
- name: {
- firstName: 'John',
- lastName: 'Doe',
- },
- avatarUrl: 'https://example.com/avatar1.jpg',
- },
- authorId: 'author1',
- assignee: null,
- assigneeId: null,
- comments: [],
- };
- const mockActivityTargetsToInject: ActivityTarget[] = [
- {
- id: 'target1',
- updatedAt: '2022-01-01T00:00:00',
- createdAt: '2022-01-01T00:00:00',
- activity: {
- id: 'activity1',
- createdAt: '2022-01-01T00:00:00',
- updatedAt: '2022-01-01T00:00:00',
- },
- },
- {
- id: 'target2',
- updatedAt: '2022-01-01T00:00:00',
- createdAt: '2022-01-01T00:00:00',
- activity: {
- id: 'activity1',
- createdAt: '2022-01-01T00:00:00',
- updatedAt: '2022-01-01T00:00:00',
- },
- },
- ];
- const mockTimelineTargetableObject: ActivityTargetableObject = {
- id: 'timelineTarget1',
- targetObjectNameSingular: 'Timeline',
- };
-
- const { result } = renderHook(() =>
- useInjectIntoTimelineActivitiesQueries(),
- );
-
- result.current.injectIntoTimelineActivitiesQueries({
- activityToInject: mockActivityToInject,
- activityTargetsToInject: mockActivityTargetsToInject,
- timelineTargetableObject: mockTimelineTargetableObject,
- });
-
- expect(useInjectIntoActivitiesQueries).toHaveBeenCalledTimes(1);
- });
-});
diff --git a/packages/twenty-front/src/modules/analytics/graphql/queries/createEvent.ts b/packages/twenty-front/src/modules/analytics/graphql/queries/createEvent.ts
deleted file mode 100644
index 6183b9107325..000000000000
--- a/packages/twenty-front/src/modules/analytics/graphql/queries/createEvent.ts
+++ /dev/null
@@ -1,9 +0,0 @@
-import { gql } from '@apollo/client';
-
-export const CREATE_EVENT = gql`
- mutation CreateEvent($type: String!, $data: JSON!) {
- createEvent(type: $type, data: $data) {
- success
- }
- }
-`;
diff --git a/packages/twenty-front/src/modules/analytics/graphql/queries/track.ts b/packages/twenty-front/src/modules/analytics/graphql/queries/track.ts
new file mode 100644
index 000000000000..3aa7bba0fcf4
--- /dev/null
+++ b/packages/twenty-front/src/modules/analytics/graphql/queries/track.ts
@@ -0,0 +1,9 @@
+import { gql } from '@apollo/client';
+
+export const TRACK = gql`
+ mutation Track($type: String!, $data: JSON!) {
+ track(type: $type, data: $data) {
+ success
+ }
+ }
+`;
diff --git a/packages/twenty-front/src/modules/analytics/hooks/__tests__/useEventTracker.test.tsx b/packages/twenty-front/src/modules/analytics/hooks/__tests__/useEventTracker.test.tsx
index 85d60977d7a9..9b60f1ffa646 100644
--- a/packages/twenty-front/src/modules/analytics/hooks/__tests__/useEventTracker.test.tsx
+++ b/packages/twenty-front/src/modules/analytics/hooks/__tests__/useEventTracker.test.tsx
@@ -11,8 +11,8 @@ const mocks: MockedResponse[] = [
{
request: {
query: gql`
- mutation CreateEvent($type: String!, $data: JSON!) {
- createEvent(type: $type, data: $data) {
+ mutation Track($type: String!, $data: JSON!) {
+ track(type: $type, data: $data) {
success
}
}
@@ -24,7 +24,7 @@ const mocks: MockedResponse[] = [
},
result: jest.fn(() => ({
data: {
- createEvent: {
+ track: {
success: true,
},
},
diff --git a/packages/twenty-front/src/modules/analytics/hooks/__tests__/useTrackEvent.test.tsx b/packages/twenty-front/src/modules/analytics/hooks/__tests__/useTrackEvent.test.tsx
index 69eeecdf1ca8..f82b7323a658 100644
--- a/packages/twenty-front/src/modules/analytics/hooks/__tests__/useTrackEvent.test.tsx
+++ b/packages/twenty-front/src/modules/analytics/hooks/__tests__/useTrackEvent.test.tsx
@@ -4,10 +4,10 @@ import { RecoilRoot } from 'recoil';
import { useTrackEvent } from '../useTrackEvent';
-const mockCreateEventMutation = jest.fn();
+const mockTrackMutation = jest.fn();
jest.mock('~/generated/graphql', () => ({
- useCreateEventMutation: () => [mockCreateEventMutation],
+ useTrackMutation: () => [mockTrackMutation],
}));
describe('useTrackEvent', () => {
@@ -17,8 +17,8 @@ describe('useTrackEvent', () => {
renderHook(() => useTrackEvent(eventType, eventData), {
wrapper: RecoilRoot,
});
- expect(mockCreateEventMutation).toHaveBeenCalledTimes(1);
- expect(mockCreateEventMutation).toHaveBeenCalledWith({
+ expect(mockTrackMutation).toHaveBeenCalledTimes(1);
+ expect(mockTrackMutation).toHaveBeenCalledWith({
variables: { type: eventType, data: eventData },
});
});
diff --git a/packages/twenty-front/src/modules/analytics/hooks/useEventTracker.ts b/packages/twenty-front/src/modules/analytics/hooks/useEventTracker.ts
index 75f80b4ba7c0..88d1d656740b 100644
--- a/packages/twenty-front/src/modules/analytics/hooks/useEventTracker.ts
+++ b/packages/twenty-front/src/modules/analytics/hooks/useEventTracker.ts
@@ -2,7 +2,7 @@ import { useCallback } from 'react';
import { useRecoilValue } from 'recoil';
import { telemetryState } from '@/client-config/states/telemetryState';
-import { useCreateEventMutation } from '~/generated/graphql';
+import { useTrackMutation } from '~/generated/graphql';
interface EventLocation {
pathname: string;
@@ -13,8 +13,8 @@ export interface EventData {
}
export const useEventTracker = () => {
- const telemetry = useRecoilValue(telemetryState());
- const [createEventMutation] = useCreateEventMutation();
+ const telemetry = useRecoilValue(telemetryState);
+ const [createEventMutation] = useTrackMutation();
return useCallback(
(eventType: string, eventData: EventData) => {
diff --git a/packages/twenty-front/src/modules/apollo/hooks/__tests__/useApolloFactory.test.tsx b/packages/twenty-front/src/modules/apollo/hooks/__tests__/useApolloFactory.test.tsx
index bea66cc15c5f..d1608ab9308c 100644
--- a/packages/twenty-front/src/modules/apollo/hooks/__tests__/useApolloFactory.test.tsx
+++ b/packages/twenty-front/src/modules/apollo/hooks/__tests__/useApolloFactory.test.tsx
@@ -77,8 +77,8 @@ describe('useApolloFactory', () => {
await act(async () => {
await result.current.factory.mutate({
mutation: gql`
- mutation CreateEvent($type: String!, $data: JSON!) {
- createEvent(type: $type, data: $data) {
+ mutation Track($type: String!, $data: JSON!) {
+ track(type: $type, data: $data) {
success
}
}
diff --git a/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts b/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts
index 4a2014779358..4da2bc786311 100644
--- a/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts
+++ b/packages/twenty-front/src/modules/apollo/hooks/useApolloFactory.ts
@@ -16,11 +16,11 @@ import { ApolloFactory } from '../services/apollo.factory';
export const useApolloFactory = () => {
// eslint-disable-next-line @nx/workspace-no-state-useref
const apolloRef = useRef | null>(null);
- const [isDebugMode] = useRecoilState(isDebugModeState());
+ const [isDebugMode] = useRecoilState(isDebugModeState);
const navigate = useNavigate();
const isMatchingLocation = useIsMatchingLocation();
- const [tokenPair, setTokenPair] = useRecoilState(tokenPairState());
+ const [tokenPair, setTokenPair] = useRecoilState(tokenPairState);
const apolloClient = useMemo(() => {
apolloRef.current = new ApolloFactory({
diff --git a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect.ts b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect.ts
index 0597a89ed733..f76074e284a6 100644
--- a/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect.ts
+++ b/packages/twenty-front/src/modules/apollo/optimistic-effect/utils/triggerUpdateRelationsOptimisticEffect.ts
@@ -26,127 +26,129 @@ export const triggerUpdateRelationsOptimisticEffect = ({
currentSourceRecord: CachedObjectRecord | null;
updatedSourceRecord: CachedObjectRecord | null;
objectMetadataItems: ObjectMetadataItem[];
-}) =>
- sourceObjectMetadataItem.fields.forEach((fieldMetadataItemOnSourceRecord) => {
- const notARelationField =
- fieldMetadataItemOnSourceRecord.type !== FieldMetadataType.Relation;
-
- if (notARelationField) {
- return;
- }
-
- const fieldDoesNotExist =
- isDefined(updatedSourceRecord) &&
- !(fieldMetadataItemOnSourceRecord.name in updatedSourceRecord);
-
- if (fieldDoesNotExist) {
- return;
- }
-
- const relationDefinition = getRelationDefinition({
- fieldMetadataItemOnSourceRecord,
- objectMetadataItems,
- });
-
- if (!relationDefinition) {
- return;
- }
-
- const { targetObjectMetadataItem, fieldMetadataItemOnTargetRecord } =
- relationDefinition;
-
- const currentFieldValueOnSourceRecord:
- | ObjectRecordConnection
- | CachedObjectRecord
- | null = currentSourceRecord?.[fieldMetadataItemOnSourceRecord.name];
-
- const updatedFieldValueOnSourceRecord:
- | ObjectRecordConnection
- | CachedObjectRecord
- | null = updatedSourceRecord?.[fieldMetadataItemOnSourceRecord.name];
-
- if (
- isDeeplyEqual(
- currentFieldValueOnSourceRecord,
- updatedFieldValueOnSourceRecord,
- )
- ) {
- return;
- }
-
- // TODO: replace this by a relation type check, if it's one to many,
- // it's an object record connection (we can still check it though as a safeguard)
- const currentFieldValueOnSourceRecordIsARecordConnection =
- isObjectRecordConnection(
- targetObjectMetadataItem.nameSingular,
- currentFieldValueOnSourceRecord,
- );
-
- const targetRecordsToDetachFrom =
- currentFieldValueOnSourceRecordIsARecordConnection
- ? currentFieldValueOnSourceRecord.edges.map(
- ({ node }) => node as CachedObjectRecord,
- )
- : [currentFieldValueOnSourceRecord].filter(isDefined);
-
- const updatedFieldValueOnSourceRecordIsARecordConnection =
- isObjectRecordConnection(
- targetObjectMetadataItem.nameSingular,
- updatedFieldValueOnSourceRecord,
- );
-
- const targetRecordsToAttachTo =
- updatedFieldValueOnSourceRecordIsARecordConnection
- ? updatedFieldValueOnSourceRecord.edges.map(
- ({ node }) => node as CachedObjectRecord,
- )
- : [updatedFieldValueOnSourceRecord].filter(isDefined);
-
- const shouldDetachSourceFromAllTargets =
- isDefined(currentSourceRecord) && targetRecordsToDetachFrom.length > 0;
-
- if (shouldDetachSourceFromAllTargets) {
- // TODO: see if we can de-hardcode this, put cascade delete in relation metadata item
- // Instead of hardcoding it here
- const shouldCascadeDeleteTargetRecords =
- CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH.includes(
- targetObjectMetadataItem.nameSingular as CoreObjectNameSingular,
+}) => {
+ return sourceObjectMetadataItem.fields.forEach(
+ (fieldMetadataItemOnSourceRecord) => {
+ const notARelationField =
+ fieldMetadataItemOnSourceRecord.type !== FieldMetadataType.Relation;
+
+ if (notARelationField) {
+ return;
+ }
+
+ const fieldDoesNotExist =
+ isDefined(updatedSourceRecord) &&
+ !(fieldMetadataItemOnSourceRecord.name in updatedSourceRecord);
+
+ if (fieldDoesNotExist) {
+ return;
+ }
+
+ const relationDefinition = getRelationDefinition({
+ fieldMetadataItemOnSourceRecord,
+ objectMetadataItems,
+ });
+ if (!relationDefinition) {
+ return;
+ }
+
+ const { targetObjectMetadataItem, fieldMetadataItemOnTargetRecord } =
+ relationDefinition;
+
+ const currentFieldValueOnSourceRecord:
+ | ObjectRecordConnection
+ | CachedObjectRecord
+ | null = currentSourceRecord?.[fieldMetadataItemOnSourceRecord.name];
+
+ const updatedFieldValueOnSourceRecord:
+ | ObjectRecordConnection
+ | CachedObjectRecord
+ | null = updatedSourceRecord?.[fieldMetadataItemOnSourceRecord.name];
+
+ if (
+ isDeeplyEqual(
+ currentFieldValueOnSourceRecord,
+ updatedFieldValueOnSourceRecord,
+ )
+ ) {
+ return;
+ }
+
+ // TODO: replace this by a relation type check, if it's one to many,
+ // it's an object record connection (we can still check it though as a safeguard)
+ const currentFieldValueOnSourceRecordIsARecordConnection =
+ isObjectRecordConnection(
+ targetObjectMetadataItem.nameSingular,
+ currentFieldValueOnSourceRecord,
);
- if (shouldCascadeDeleteTargetRecords) {
- triggerDeleteRecordsOptimisticEffect({
- cache,
- objectMetadataItem: targetObjectMetadataItem,
- recordsToDelete: targetRecordsToDetachFrom,
- objectMetadataItems,
- });
- } else {
- targetRecordsToDetachFrom.forEach((targetRecordToDetachFrom) => {
- triggerDetachRelationOptimisticEffect({
+ const targetRecordsToDetachFrom =
+ currentFieldValueOnSourceRecordIsARecordConnection
+ ? currentFieldValueOnSourceRecord.edges.map(
+ ({ node }) => node as CachedObjectRecord,
+ )
+ : [currentFieldValueOnSourceRecord].filter(isDefined);
+
+ const updatedFieldValueOnSourceRecordIsARecordConnection =
+ isObjectRecordConnection(
+ targetObjectMetadataItem.nameSingular,
+ updatedFieldValueOnSourceRecord,
+ );
+
+ const targetRecordsToAttachTo =
+ updatedFieldValueOnSourceRecordIsARecordConnection
+ ? updatedFieldValueOnSourceRecord.edges.map(
+ ({ node }) => node as CachedObjectRecord,
+ )
+ : [updatedFieldValueOnSourceRecord].filter(isDefined);
+
+ const shouldDetachSourceFromAllTargets =
+ isDefined(currentSourceRecord) && targetRecordsToDetachFrom.length > 0;
+
+ if (shouldDetachSourceFromAllTargets) {
+ // TODO: see if we can de-hardcode this, put cascade delete in relation metadata item
+ // Instead of hardcoding it here
+ const shouldCascadeDeleteTargetRecords =
+ CORE_OBJECT_NAMES_TO_DELETE_ON_TRIGGER_RELATION_DETACH.includes(
+ targetObjectMetadataItem.nameSingular as CoreObjectNameSingular,
+ );
+
+ if (shouldCascadeDeleteTargetRecords) {
+ triggerDeleteRecordsOptimisticEffect({
+ cache,
+ objectMetadataItem: targetObjectMetadataItem,
+ recordsToDelete: targetRecordsToDetachFrom,
+ objectMetadataItems,
+ });
+ } else {
+ targetRecordsToDetachFrom.forEach((targetRecordToDetachFrom) => {
+ triggerDetachRelationOptimisticEffect({
+ cache,
+ sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
+ sourceRecordId: currentSourceRecord.id,
+ fieldNameOnTargetRecord: fieldMetadataItemOnTargetRecord.name,
+ targetObjectNameSingular: targetObjectMetadataItem.nameSingular,
+ targetRecordId: targetRecordToDetachFrom.id,
+ });
+ });
+ }
+ }
+
+ const shouldAttachSourceToAllTargets =
+ isDefined(updatedSourceRecord) && targetRecordsToAttachTo.length > 0;
+
+ if (shouldAttachSourceToAllTargets) {
+ targetRecordsToAttachTo.forEach((targetRecordToAttachTo) =>
+ triggerAttachRelationOptimisticEffect({
cache,
sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
- sourceRecordId: currentSourceRecord.id,
+ sourceRecordId: updatedSourceRecord.id,
fieldNameOnTargetRecord: fieldMetadataItemOnTargetRecord.name,
targetObjectNameSingular: targetObjectMetadataItem.nameSingular,
- targetRecordId: targetRecordToDetachFrom.id,
- });
- });
+ targetRecordId: targetRecordToAttachTo.id,
+ }),
+ );
}
- }
-
- const shouldAttachSourceToAllTargets =
- isDefined(updatedSourceRecord) && targetRecordsToAttachTo.length > 0;
-
- if (shouldAttachSourceToAllTargets) {
- targetRecordsToAttachTo.forEach((targetRecordToAttachTo) =>
- triggerAttachRelationOptimisticEffect({
- cache,
- sourceObjectNameSingular: sourceObjectMetadataItem.nameSingular,
- sourceRecordId: updatedSourceRecord.id,
- fieldNameOnTargetRecord: fieldMetadataItemOnTargetRecord.name,
- targetObjectNameSingular: targetObjectMetadataItem.nameSingular,
- targetRecordId: targetRecordToAttachTo.id,
- }),
- );
- }
- });
+ },
+ );
+};
diff --git a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts
index 7bd43be26a84..d0ba37512438 100644
--- a/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts
+++ b/packages/twenty-front/src/modules/apollo/services/__tests__/apollo.factory.test.ts
@@ -41,8 +41,8 @@ const makeRequest = async () => {
await client.mutate({
mutation: gql`
- mutation CreateEvent($type: String!, $data: JSON!) {
- createEvent(type: $type, data: $data) {
+ mutation Track($type: String!, $data: JSON!) {
+ track(type: $type, data: $data) {
success
}
}
diff --git a/packages/twenty-front/src/modules/auth/graphql/mutations/generateJWT.ts b/packages/twenty-front/src/modules/auth/graphql/mutations/generateJWT.ts
new file mode 100644
index 000000000000..7f7d19ae71be
--- /dev/null
+++ b/packages/twenty-front/src/modules/auth/graphql/mutations/generateJWT.ts
@@ -0,0 +1,11 @@
+import { gql } from '@apollo/client';
+
+export const GENERATE_JWT = gql`
+ mutation GenerateJWT($workspaceId: String!) {
+ generateJWT(workspaceId: $workspaceId) {
+ tokens {
+ ...AuthTokensFragment
+ }
+ }
+ }
+`;
diff --git a/packages/twenty-front/src/modules/auth/hooks/__test__/useAuth.test.tsx b/packages/twenty-front/src/modules/auth/hooks/__test__/useAuth.test.tsx
index 230075cdb5a5..25bfda7511b2 100644
--- a/packages/twenty-front/src/modules/auth/hooks/__test__/useAuth.test.tsx
+++ b/packages/twenty-front/src/modules/auth/hooks/__test__/useAuth.test.tsx
@@ -76,13 +76,13 @@ describe('useAuth', () => {
const { result } = renderHook(
() => {
const client = useApolloClient();
- const icons = useRecoilValue(iconsState());
- const authProviders = useRecoilValue(authProvidersState());
- const billing = useRecoilValue(billingState());
- const isSignInPrefilled = useRecoilValue(isSignInPrefilledState());
- const supportChat = useRecoilValue(supportChatState());
- const telemetry = useRecoilValue(telemetryState());
- const isDebugMode = useRecoilValue(isDebugModeState());
+ const icons = useRecoilValue(iconsState);
+ const authProviders = useRecoilValue(authProvidersState);
+ const billing = useRecoilValue(billingState);
+ const isSignInPrefilled = useRecoilValue(isSignInPrefilledState);
+ const supportChat = useRecoilValue(supportChatState);
+ const telemetry = useRecoilValue(telemetryState);
+ const isDebugMode = useRecoilValue(isDebugModeState);
return {
...useAuth(),
client,
diff --git a/packages/twenty-front/src/modules/auth/hooks/__test__/useIsLogged.test.ts b/packages/twenty-front/src/modules/auth/hooks/__test__/useIsLogged.test.ts
index a441c479708b..2674f26d9c93 100644
--- a/packages/twenty-front/src/modules/auth/hooks/__test__/useIsLogged.test.ts
+++ b/packages/twenty-front/src/modules/auth/hooks/__test__/useIsLogged.test.ts
@@ -9,7 +9,7 @@ const renderHooks = () => {
const { result } = renderHook(
() => {
const isLogged = useIsLogged();
- const setTokenPair = useSetRecoilState(tokenPairState());
+ const setTokenPair = useSetRecoilState(tokenPairState);
return {
isLogged,
diff --git a/packages/twenty-front/src/modules/auth/hooks/__test__/useOnboardingStatus.test.ts b/packages/twenty-front/src/modules/auth/hooks/__test__/useOnboardingStatus.test.ts
index ec035d3c80ad..c57337c914c9 100644
--- a/packages/twenty-front/src/modules/auth/hooks/__test__/useOnboardingStatus.test.ts
+++ b/packages/twenty-front/src/modules/auth/hooks/__test__/useOnboardingStatus.test.ts
@@ -38,13 +38,13 @@ const renderHooks = () => {
const { result } = renderHook(
() => {
const onboardingStatus = useOnboardingStatus();
- const setBilling = useSetRecoilState(billingState());
- const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState());
+ const setBilling = useSetRecoilState(billingState);
+ const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setCurrentWorkspaceMember = useSetRecoilState(
- currentWorkspaceMemberState(),
+ currentWorkspaceMemberState,
);
- const setTokenPair = useSetRecoilState(tokenPairState());
- const setVerifyPending = useSetRecoilState(isVerifyPendingState());
+ const setTokenPair = useSetRecoilState(tokenPairState);
+ const setVerifyPending = useSetRecoilState(isVerifyPendingState);
return {
onboardingStatus,
diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts
index d598b69806a3..5f6e97ea77e6 100644
--- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts
+++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts
@@ -11,6 +11,7 @@ import {
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isVerifyPendingState } from '@/auth/states/isVerifyPendingState';
+import { workspacesState } from '@/auth/states/workspaces';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { billingState } from '@/client-config/states/billingState';
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
@@ -19,7 +20,7 @@ import { supportChatState } from '@/client-config/states/supportChatState';
import { telemetryState } from '@/client-config/states/telemetryState';
import { iconsState } from '@/ui/display/icon/states/iconsState';
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
-import { REACT_APP_SERVER_AUTH_URL } from '~/config';
+import { REACT_APP_SERVER_BASE_URL } from '~/config';
import {
useChallengeMutation,
useCheckUserExistsLazyQuery,
@@ -32,14 +33,15 @@ import { currentUserState } from '../states/currentUserState';
import { tokenPairState } from '../states/tokenPairState';
export const useAuth = () => {
- const [, setTokenPair] = useRecoilState(tokenPairState());
- const setCurrentUser = useSetRecoilState(currentUserState());
+ const [, setTokenPair] = useRecoilState(tokenPairState);
+ const setCurrentUser = useSetRecoilState(currentUserState);
const setCurrentWorkspaceMember = useSetRecoilState(
- currentWorkspaceMemberState(),
+ currentWorkspaceMemberState,
);
- const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState());
- const setIsVerifyPendingState = useSetRecoilState(isVerifyPendingState());
+ const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
+ const setIsVerifyPendingState = useSetRecoilState(isVerifyPendingState);
+ const setWorkspaces = useSetRecoilState(workspacesState);
const [challenge] = useChallengeMutation();
const [signUp] = useSignUpMutation();
@@ -101,6 +103,15 @@ export const useAuth = () => {
}
const workspace = user.defaultWorkspace ?? null;
setCurrentWorkspace(workspace);
+ if (isDefined(verifyResult.data?.verify.user.workspaces)) {
+ const validWorkspaces = verifyResult.data?.verify.user.workspaces
+ .filter(
+ ({ workspace }) => workspace !== null && workspace !== undefined,
+ )
+ .map((validWorkspace) => validWorkspace.workspace!);
+
+ setWorkspaces(validWorkspaces);
+ }
return {
user,
workspaceMember,
@@ -114,6 +125,7 @@ export const useAuth = () => {
setCurrentUser,
setCurrentWorkspaceMember,
setCurrentWorkspace,
+ setWorkspaces,
],
);
@@ -141,26 +153,26 @@ export const useAuth = () => {
({ snapshot }) =>
async () => {
const emptySnapshot = snapshot_UNSTABLE();
- const iconsValue = snapshot.getLoadable(iconsState()).getValue();
+ const iconsValue = snapshot.getLoadable(iconsState).getValue();
const authProvidersValue = snapshot
- .getLoadable(authProvidersState())
+ .getLoadable(authProvidersState)
.getValue();
- const billing = snapshot.getLoadable(billingState()).getValue();
+ const billing = snapshot.getLoadable(billingState).getValue();
const isSignInPrefilled = snapshot
- .getLoadable(isSignInPrefilledState())
+ .getLoadable(isSignInPrefilledState)
.getValue();
- const supportChat = snapshot.getLoadable(supportChatState()).getValue();
- const telemetry = snapshot.getLoadable(telemetryState()).getValue();
- const isDebugMode = snapshot.getLoadable(isDebugModeState()).getValue();
+ const supportChat = snapshot.getLoadable(supportChatState).getValue();
+ const telemetry = snapshot.getLoadable(telemetryState).getValue();
+ const isDebugMode = snapshot.getLoadable(isDebugModeState).getValue();
const initialSnapshot = emptySnapshot.map(({ set }) => {
- set(iconsState(), iconsValue);
- set(authProvidersState(), authProvidersValue);
- set(billingState(), billing);
- set(isSignInPrefilledState(), isSignInPrefilled);
- set(supportChatState(), supportChat);
- set(telemetryState(), telemetry);
- set(isDebugModeState(), isDebugMode);
+ set(iconsState, iconsValue);
+ set(authProvidersState, authProvidersValue);
+ set(billingState, billing);
+ set(isSignInPrefilledState, isSignInPrefilled);
+ set(supportChatState, supportChat);
+ set(telemetryState, telemetry);
+ set(isDebugModeState, isDebugMode);
return undefined;
});
@@ -204,9 +216,9 @@ export const useAuth = () => {
);
const handleGoogleLogin = useCallback((workspaceInviteHash?: string) => {
- const authServerUrl = REACT_APP_SERVER_AUTH_URL;
+ const authServerUrl = REACT_APP_SERVER_BASE_URL;
window.location.href =
- `${authServerUrl}/google/${
+ `${authServerUrl}/auth/google/${
workspaceInviteHash ? '?inviteHash=' + workspaceInviteHash : ''
}` || '';
}, []);
diff --git a/packages/twenty-front/src/modules/auth/hooks/useIsLogged.ts b/packages/twenty-front/src/modules/auth/hooks/useIsLogged.ts
index 1c235a747e9d..4399b564c069 100644
--- a/packages/twenty-front/src/modules/auth/hooks/useIsLogged.ts
+++ b/packages/twenty-front/src/modules/auth/hooks/useIsLogged.ts
@@ -5,8 +5,8 @@ import { isVerifyPendingState } from '@/auth/states/isVerifyPendingState';
import { tokenPairState } from '../states/tokenPairState';
export const useIsLogged = (): boolean => {
- const [tokenPair] = useRecoilState(tokenPairState());
- const isVerifyPending = useRecoilValue(isVerifyPendingState());
+ const [tokenPair] = useRecoilState(tokenPairState);
+ const isVerifyPending = useRecoilValue(isVerifyPendingState);
return !!tokenPair && !isVerifyPending;
};
diff --git a/packages/twenty-front/src/modules/auth/hooks/useOnboardingStatus.ts b/packages/twenty-front/src/modules/auth/hooks/useOnboardingStatus.ts
index bd8ed6b0dfe1..91a8a60e84e9 100644
--- a/packages/twenty-front/src/modules/auth/hooks/useOnboardingStatus.ts
+++ b/packages/twenty-front/src/modules/auth/hooks/useOnboardingStatus.ts
@@ -11,9 +11,9 @@ import {
} from '../utils/getOnboardingStatus';
export const useOnboardingStatus = (): OnboardingStatus | undefined => {
- const billing = useRecoilValue(billingState());
- const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState());
- const currentWorkspace = useRecoilValue(currentWorkspaceState());
+ const billing = useRecoilValue(billingState);
+ const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
+ const currentWorkspace = useRecoilValue(currentWorkspaceState);
const isLoggedIn = useIsLogged();
return getOnboardingStatus({
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx
index c0bb530f6bd0..001afa8c18d1 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/components/SignInUpForm.tsx
@@ -46,7 +46,7 @@ const StyledInputContainer = styled.div`
`;
export const SignInUpForm = () => {
- const [authProviders] = useRecoilState(authProvidersState());
+ const [authProviders] = useRecoilState(authProvidersState);
const [showErrors, setShowErrors] = useState(false);
const { handleResetPassword } = useHandleResetPassword();
const workspace = useWorkspaceFromInviteHash();
@@ -54,6 +54,7 @@ export const SignInUpForm = () => {
const { form } = useSignInUpForm();
const {
+ isInviteMode,
signInUpStep,
signInUpMode,
continueWithCredentials,
@@ -90,14 +91,14 @@ export const SignInUpForm = () => {
}, [signInUpMode, signInUpStep]);
const title = useMemo(() => {
- if (signInUpMode === SignInUpMode.Invite) {
+ if (isInviteMode) {
return `Join ${workspace?.displayName ?? ''} team`;
}
return signInUpMode === SignInUpMode.SignIn
? 'Sign in to Twenty'
: 'Sign up to Twenty';
- }, [signInUpMode, workspace?.displayName]);
+ }, [signInUpMode, workspace?.displayName, isInviteMode]);
const theme = useTheme();
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useNavigateAfterSignInUp.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useNavigateAfterSignInUp.ts
index e659696c4cef..ee090c127086 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useNavigateAfterSignInUp.ts
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useNavigateAfterSignInUp.ts
@@ -9,7 +9,7 @@ import { WorkspaceMember } from '~/generated/graphql.tsx';
export const useNavigateAfterSignInUp = () => {
const navigate = useNavigate();
- const billing = useRecoilValue(billingState());
+ const billing = useRecoilValue(billingState);
const navigateAfterSignInUp = useCallback(
(
currentWorkspace: CurrentWorkspace,
diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx
index 46849f44e338..3ca514fc4195 100644
--- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx
+++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUp.tsx
@@ -15,7 +15,6 @@ import { useAuth } from '../../hooks/useAuth';
export enum SignInUpMode {
SignIn = 'sign-in',
SignUp = 'sign-up',
- Invite = 'invite',
}
export enum SignInUpStep {
@@ -33,15 +32,13 @@ export const useSignInUp = (form: UseFormReturn