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
) => { const { navigateAfterSignInUp } = useNavigateAfterSignInUp(); + const [isInviteMode] = useState(() => isMatchingLocation(AppPath.Invite)); + const [signInUpStep, setSignInUpStep] = useState( SignInUpStep.Init, ); const [signInUpMode, setSignInUpMode] = useState(() => { - if (isMatchingLocation(AppPath.Invite)) { - return SignInUpMode.Invite; - } - return isMatchingLocation(AppPath.SignIn) ? SignInUpMode.SignIn : SignInUpMode.SignUp; @@ -79,24 +76,14 @@ export const useSignInUp = (form: UseFormReturn) => { }, onCompleted: (data) => { if (data?.checkUserExists.exists) { - isMatchingLocation(AppPath.Invite) - ? setSignInUpMode(SignInUpMode.Invite) - : setSignInUpMode(SignInUpMode.SignIn); + setSignInUpMode(SignInUpMode.SignIn); } else { - isMatchingLocation(AppPath.Invite) - ? setSignInUpMode(SignInUpMode.Invite) - : setSignInUpMode(SignInUpMode.SignUp); + setSignInUpMode(SignInUpMode.SignUp); } setSignInUpStep(SignInUpStep.Password); }, }); - }, [ - isMatchingLocation, - setSignInUpStep, - checkUserExistsQuery, - form, - setSignInUpMode, - ]); + }, [setSignInUpStep, checkUserExistsQuery, form, setSignInUpMode]); const submitCredentials: SubmitHandler = useCallback( async (data) => { @@ -109,7 +96,7 @@ export const useSignInUp = (form: UseFormReturn) => { workspace: currentWorkspace, workspaceMember: currentWorkspaceMember, } = - signInUpMode === SignInUpMode.SignIn + signInUpMode === SignInUpMode.SignIn && !isInviteMode ? await signInWithCredentials( data.email.toLowerCase().trim(), data.password, @@ -129,6 +116,7 @@ export const useSignInUp = (form: UseFormReturn) => { }, [ signInUpMode, + isInviteMode, signInWithCredentials, signUpWithCredentials, workspaceInviteHash, @@ -163,6 +151,7 @@ export const useSignInUp = (form: UseFormReturn) => { ); return { + isInviteMode, signInUpStep, signInUpMode, continueWithCredentials, diff --git a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts index 6677754d26ab..591abf368fb2 100644 --- a/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts +++ b/packages/twenty-front/src/modules/auth/sign-in-up/hooks/useSignInUpForm.ts @@ -19,7 +19,7 @@ const validationSchema = z export type Form = z.infer; export const useSignInUpForm = () => { - const isSignInPrefilled = useRecoilValue(isSignInPrefilledState()); + const isSignInPrefilled = useRecoilValue(isSignInPrefilledState); const form = useForm({ mode: 'onChange', defaultValues: { diff --git a/packages/twenty-front/src/modules/auth/states/workspaces.ts b/packages/twenty-front/src/modules/auth/states/workspaces.ts new file mode 100644 index 000000000000..dc0be614a72b --- /dev/null +++ b/packages/twenty-front/src/modules/auth/states/workspaces.ts @@ -0,0 +1,9 @@ +import { createState } from '@/ui/utilities/state/utils/createState'; +import { Workspace } from '~/generated/graphql'; + +export type Workspaces = Pick; + +export const workspacesState = createState({ + key: 'workspacesState', + defaultValue: [], +}); diff --git a/packages/twenty-front/src/modules/billing/graphql/updateBillingSubscription.ts b/packages/twenty-front/src/modules/billing/graphql/updateBillingSubscription.ts new file mode 100644 index 000000000000..2f75c7610b37 --- /dev/null +++ b/packages/twenty-front/src/modules/billing/graphql/updateBillingSubscription.ts @@ -0,0 +1,9 @@ +import { gql } from '@apollo/client'; + +export const UPDATE_BILLING_SUBSCRIPTION = gql` + mutation UpdateBillingSubscription { + updateBillingSubscription { + success + } + } +`; diff --git a/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx b/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx index f7caad94b25f..db62ad64fa8e 100644 --- a/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx +++ b/packages/twenty-front/src/modules/client-config/components/ClientConfigProvider.tsx @@ -15,17 +15,17 @@ import { isDefined } from '~/utils/isDefined'; export const ClientConfigProvider: React.FC = ({ children, }) => { - const setAuthProviders = useSetRecoilState(authProvidersState()); - const setIsDebugMode = useSetRecoilState(isDebugModeState()); + const setAuthProviders = useSetRecoilState(authProvidersState); + const setIsDebugMode = useSetRecoilState(isDebugModeState); - const setIsSignInPrefilled = useSetRecoilState(isSignInPrefilledState()); - const setIsSignUpDisabled = useSetRecoilState(isSignUpDisabledState()); + const setIsSignInPrefilled = useSetRecoilState(isSignInPrefilledState); + const setIsSignUpDisabled = useSetRecoilState(isSignUpDisabledState); - const setBilling = useSetRecoilState(billingState()); - const setTelemetry = useSetRecoilState(telemetryState()); - const setSupportChat = useSetRecoilState(supportChatState()); + const setBilling = useSetRecoilState(billingState); + const setTelemetry = useSetRecoilState(telemetryState); + const setSupportChat = useSetRecoilState(supportChatState); - const setSentryConfig = useSetRecoilState(sentryConfigState()); + const setSentryConfig = useSetRecoilState(sentryConfigState); const { data, loading } = useGetClientConfigQuery(); diff --git a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx index 0a7a6c554174..b7f281eb1560 100644 --- a/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/CommandMenu.tsx @@ -109,9 +109,9 @@ export const CommandMenu = () => { const openActivityRightDrawer = useOpenActivityRightDrawer(); const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState); const [commandMenuSearch, setCommandMenuSearch] = useRecoilState( - commandMenuSearchState(), + commandMenuSearchState, ); - const commandMenuCommands = useRecoilValue(commandMenuCommandsState()); + const commandMenuCommands = useRecoilValue(commandMenuCommandsState); const { closeKeyboardShortcutMenu } = useKeyboardShortcutMenu(); const handleSearchChange = (event: React.ChangeEvent) => { diff --git a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx index 95e4da7d182d..6cf5c7fbf3c5 100644 --- a/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx +++ b/packages/twenty-front/src/modules/command-menu/components/__stories__/CommandMenu.stories.tsx @@ -27,9 +27,9 @@ const meta: Meta = { component: CommandMenu, decorators: [ (Story) => { - const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState()); + const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); const setCurrentWorkspaceMember = useSetRecoilState( - currentWorkspaceMemberState(), + currentWorkspaceMemberState, ); const { addToCommandMenu, setToInitialCommandMenu, openCommandMenu } = useCommandMenu(); diff --git a/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx b/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx index 2cdec0fdc4a4..e1ee5501382f 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx +++ b/packages/twenty-front/src/modules/command-menu/hooks/__test__/useCommandMenu.test.tsx @@ -25,7 +25,7 @@ const renderHooks = () => { const commandMenu = useCommandMenu(); const isCommandMenuOpened = useRecoilValue(isCommandMenuOpenedState); const [commandMenuCommands, setCommandMenuCommands] = useRecoilState( - commandMenuCommandsState(), + commandMenuCommandsState, ); return { diff --git a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts index 438e3b11d8ac..1a8e085064e2 100644 --- a/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts +++ b/packages/twenty-front/src/modules/command-menu/hooks/useCommandMenu.ts @@ -17,7 +17,7 @@ import { Command } from '../types/Command'; export const useCommandMenu = () => { const navigate = useNavigate(); const setIsCommandMenuOpened = useSetRecoilState(isCommandMenuOpenedState); - const setCommands = useSetRecoilState(commandMenuCommandsState()); + const setCommands = useSetRecoilState(commandMenuCommandsState); const { resetSelectedItem } = useSelectableList('command-menu-list'); const { setHotkeyScopeAndMemorizePreviousScope, @@ -52,7 +52,7 @@ export const useCommandMenu = () => { .getLoadable(isCommandMenuOpenedState) .getValue(); - set(commandMenuSearchState(), ''); + set(commandMenuSearchState, ''); if (isCommandMenuOpened) { closeCommandMenu(); diff --git a/packages/twenty-front/src/modules/debug/components/ApolloDevLogEffect.tsx b/packages/twenty-front/src/modules/debug/components/ApolloDevLogEffect.tsx index 7ce1508463ac..93a6af6f0c5b 100644 --- a/packages/twenty-front/src/modules/debug/components/ApolloDevLogEffect.tsx +++ b/packages/twenty-front/src/modules/debug/components/ApolloDevLogEffect.tsx @@ -5,7 +5,7 @@ import { useRecoilValue } from 'recoil'; import { isDebugModeState } from '@/client-config/states/isDebugModeState'; export const ApolloDevLogEffect = () => { - const isDebugMode = useRecoilValue(isDebugModeState()); + const isDebugMode = useRecoilValue(isDebugModeState); useEffect(() => { if (isDebugMode) { diff --git a/packages/twenty-front/src/modules/debug/components/RecoilDebugObserver.tsx b/packages/twenty-front/src/modules/debug/components/RecoilDebugObserver.tsx index e603623f78ec..5031c1963f9c 100644 --- a/packages/twenty-front/src/modules/debug/components/RecoilDebugObserver.tsx +++ b/packages/twenty-front/src/modules/debug/components/RecoilDebugObserver.tsx @@ -15,7 +15,7 @@ const formatTitle = (stateName: string) => { }; export const RecoilDebugObserverEffect = () => { - const isDebugMode = useRecoilValue(isDebugModeState()); + const isDebugMode = useRecoilValue(isDebugModeState); useRecoilTransactionObserver_UNSTABLE(({ snapshot }) => { if (!isDebugMode) { diff --git a/packages/twenty-front/src/modules/error-handler/components/SentryInitiEffect.tsx b/packages/twenty-front/src/modules/error-handler/components/SentryInitiEffect.tsx index 874b5ac4b7f9..463810674409 100644 --- a/packages/twenty-front/src/modules/error-handler/components/SentryInitiEffect.tsx +++ b/packages/twenty-front/src/modules/error-handler/components/SentryInitiEffect.tsx @@ -11,11 +11,11 @@ import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { isDefined } from '~/utils/isDefined'; export const SentryInitEffect = () => { - const sentryConfig = useRecoilValue(sentryConfigState()); + const sentryConfig = useRecoilValue(sentryConfigState); - const currentUser = useRecoilValue(currentUserState()); - const currentWorkspace = useRecoilValue(currentWorkspaceState()); - const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState()); + const currentUser = useRecoilValue(currentUserState); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const [isSentryInitialized, setIsSentryInitialized] = useState(false); diff --git a/packages/twenty-front/src/modules/favorites/hooks/__tests__/useFavorites.test.tsx b/packages/twenty-front/src/modules/favorites/hooks/__tests__/useFavorites.test.tsx index a060750e48fc..3d9347f0bcf0 100644 --- a/packages/twenty-front/src/modules/favorites/hooks/__tests__/useFavorites.test.tsx +++ b/packages/twenty-front/src/modules/favorites/hooks/__tests__/useFavorites.test.tsx @@ -46,11 +46,11 @@ describe('useFavorites', () => { const { result } = renderHook( () => { const setCurrentWorkspaceMember = useSetRecoilState( - currentWorkspaceMemberState(), + currentWorkspaceMemberState, ); setCurrentWorkspaceMember(mockWorkspaceMember); - const setMetadataItems = useSetRecoilState(objectMetadataItemsState()); + const setMetadataItems = useSetRecoilState(objectMetadataItemsState); setMetadataItems(mockObjectMetadataItems); return useFavorites(); @@ -67,11 +67,11 @@ describe('useFavorites', () => { const { result } = renderHook( () => { const setCurrentWorkspaceMember = useSetRecoilState( - currentWorkspaceMemberState(), + currentWorkspaceMemberState, ); setCurrentWorkspaceMember(mockWorkspaceMember); - const setMetadataItems = useSetRecoilState(objectMetadataItemsState()); + const setMetadataItems = useSetRecoilState(objectMetadataItemsState); setMetadataItems(mockObjectMetadataItems); return useFavorites(); @@ -95,11 +95,11 @@ describe('useFavorites', () => { const { result } = renderHook( () => { const setCurrentWorkspaceMember = useSetRecoilState( - currentWorkspaceMemberState(), + currentWorkspaceMemberState, ); setCurrentWorkspaceMember(mockWorkspaceMember); - const setMetadataItems = useSetRecoilState(objectMetadataItemsState()); + const setMetadataItems = useSetRecoilState(objectMetadataItemsState); setMetadataItems(mockObjectMetadataItems); return useFavorites(); @@ -120,11 +120,11 @@ describe('useFavorites', () => { const { result } = renderHook( () => { const setCurrentWorkspaceMember = useSetRecoilState( - currentWorkspaceMemberState(), + currentWorkspaceMemberState, ); setCurrentWorkspaceMember(mockWorkspaceMember); - const setMetadataItems = useSetRecoilState(objectMetadataItemsState()); + const setMetadataItems = useSetRecoilState(objectMetadataItemsState); setMetadataItems(mockObjectMetadataItems); return useFavorites(); diff --git a/packages/twenty-front/src/modules/favorites/hooks/useFavorites.ts b/packages/twenty-front/src/modules/favorites/hooks/useFavorites.ts index cf5f646aa34c..f01cdd1d4fc2 100644 --- a/packages/twenty-front/src/modules/favorites/hooks/useFavorites.ts +++ b/packages/twenty-front/src/modules/favorites/hooks/useFavorites.ts @@ -6,38 +6,38 @@ import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMembe import { Favorite } from '@/favorites/types/Favorite'; import { useGetObjectRecordIdentifierByNameSingular } from '@/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord'; import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord'; +import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; +import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; import { FieldMetadataType } from '~/generated-metadata/graphql'; import { isDefined } from '~/utils/isDefined'; export const useFavorites = () => { - const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState()); - - const favoriteObjectNameSingular = 'favorite'; + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const { objectMetadataItem: favoriteObjectMetadataItem } = useObjectMetadataItem({ - objectNameSingular: favoriteObjectNameSingular, + objectNameSingular: CoreObjectNameSingular.Favorite, }); const { deleteOneRecord } = useDeleteOneRecord({ - objectNameSingular: favoriteObjectNameSingular, + objectNameSingular: CoreObjectNameSingular.Favorite, }); const { updateOneRecord: updateOneFavorite } = useUpdateOneRecord({ - objectNameSingular: favoriteObjectNameSingular, + objectNameSingular: CoreObjectNameSingular.Favorite, }); const { createOneRecord: createOneFavorite } = useCreateOneRecord({ - objectNameSingular: favoriteObjectNameSingular, + objectNameSingular: CoreObjectNameSingular.Favorite, }); - const { records: favorites } = useFindManyRecords({ - objectNameSingular: favoriteObjectNameSingular, - }); + const { records: favorites } = usePrefetchedData( + PrefetchKey.AllFavorites, + ); const favoriteRelationFieldMetadataItems = useMemo( () => diff --git a/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx b/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx index 216a12462f58..0a8a50cdd21d 100644 --- a/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx +++ b/packages/twenty-front/src/modules/navigation/components/AppNavigationDrawer.tsx @@ -28,12 +28,12 @@ export const AppNavigationDrawer = ({ const isMobile = useIsMobile(); const isSettingsPage = useIsSettingsPage(); const currentMobileNavigationDrawer = useRecoilValue( - currentMobileNavigationDrawerState(), + currentMobileNavigationDrawerState, ); const setIsNavigationDrawerOpen = useSetRecoilState( isNavigationDrawerOpenState, ); - const currentWorkspace = useRecoilValue(currentWorkspaceState()); + const currentWorkspace = useRecoilValue(currentWorkspaceState); const isSettingsDrawer = isMobile ? currentMobileNavigationDrawer === 'settings' diff --git a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx index 102f5d7cf333..f8a437ed7c55 100644 --- a/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx +++ b/packages/twenty-front/src/modules/navigation/components/MainNavigationDrawerItems.tsx @@ -24,13 +24,11 @@ export const MainNavigationDrawerItems = () => { const isMobile = useIsMobile(); const { toggleCommandMenu } = useCommandMenu(); const isTasksPage = useIsTasksPage(); - const currentUserDueTaskCount = useRecoilValue( - currentUserDueTaskCountState(), - ); + const currentUserDueTaskCount = useRecoilValue(currentUserDueTaskCountState); const navigate = useNavigate(); const location = useLocation(); const setNavigationMemorizedUrl = useSetRecoilState( - navigationMemorizedUrlState(), + navigationMemorizedUrlState, ); return ( diff --git a/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx b/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx index d671a284e852..3680415ba45a 100644 --- a/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx +++ b/packages/twenty-front/src/modules/navigation/components/MobileNavigationBar.tsx @@ -30,7 +30,7 @@ export const MobileNavigationBar = () => { isNavigationDrawerOpenState, ); const [currentMobileNavigationDrawer, setCurrentMobileNavigationDrawer] = - useRecoilState(currentMobileNavigationDrawerState()); + useRecoilState(currentMobileNavigationDrawerState); const activeItemName = isNavigationDrawerOpen ? currentMobileNavigationDrawer diff --git a/packages/twenty-front/src/modules/navigation/components/__stories__/AppNavigationDrawer.stories.tsx b/packages/twenty-front/src/modules/navigation/components/__stories__/AppNavigationDrawer.stories.tsx index 3c538ccd6352..e267c7e24aad 100644 --- a/packages/twenty-front/src/modules/navigation/components/__stories__/AppNavigationDrawer.stories.tsx +++ b/packages/twenty-front/src/modules/navigation/components/__stories__/AppNavigationDrawer.stories.tsx @@ -24,7 +24,7 @@ const MobileNavigationDrawerStateSetterEffect = ({ isNavigationDrawerOpenState, ); const setCurrentMobileNavigationDrawer = useSetRecoilState( - currentMobileNavigationDrawerState(), + currentMobileNavigationDrawerState, ); useEffect(() => { diff --git a/packages/twenty-front/src/modules/object-metadata/components/ApolloMetadataClientProvider.tsx b/packages/twenty-front/src/modules/object-metadata/components/ApolloMetadataClientProvider.tsx index 38bd8fc66d1f..2af8362986e1 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/ApolloMetadataClientProvider.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/ApolloMetadataClientProvider.tsx @@ -13,7 +13,7 @@ export const ApolloMetadataClientProvider = ({ }: { children: React.ReactNode; }) => { - const [tokenPair] = useRecoilState(tokenPairState()); + const [tokenPair] = useRecoilState(tokenPairState); const apolloMetadataClient = useMemo(() => { if (isNonEmptyString(tokenPair?.accessToken.token)) { return new ApolloClient({ diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx index 0313a164fbb1..9288ec017b39 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsLoadEffect.tsx @@ -10,7 +10,7 @@ export const ObjectMetadataItemsLoadEffect = () => { useFindManyObjectMetadataItems(); const [objectMetadataItems, setObjectMetadataItems] = useRecoilState( - objectMetadataItemsState(), + objectMetadataItemsState, ); useEffect(() => { diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsProvider.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsProvider.tsx index a4a50ac352b5..0c3ebdadc586 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsProvider.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataItemsProvider.tsx @@ -9,8 +9,8 @@ import { RelationPickerScope } from '@/object-record/relation-picker/scopes/Rela export const ObjectMetadataItemsProvider = ({ children, }: React.PropsWithChildren) => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState()); - const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState()); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const shouldDisplayChildren = objectMetadataItems.length > 0 || !currentWorkspaceMember; diff --git a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx index 547a7e67f574..750d3ab418ac 100644 --- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx +++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx @@ -1,37 +1,22 @@ import { useLocation, useNavigate } from 'react-router-dom'; import { useObjectMetadataItemForSettings } from '@/object-metadata/hooks/useObjectMetadataItemForSettings'; -import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; -import { QueryMethodName } from '@/object-metadata/types/QueryMethodName'; -import { useCachedRootQuery } from '@/object-record/cache/hooks/useCachedRootQuery'; -import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { usePrefetchedData } from '@/prefetch/hooks/usePrefetchedData'; +import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; import { useIcons } from '@/ui/display/icon/hooks/useIcons'; import { NavigationDrawerItem } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerItem'; +import { GraphQLView } from '@/views/types/GraphQLView'; +import { getObjectMetadataItemViews } from '@/views/utils/getObjectMetadataItemViews'; export const ObjectMetadataNavItems = () => { - const { activeObjectMetadataItems, findObjectMetadataItemByNamePlural } = - useObjectMetadataItemForSettings(); + const { activeObjectMetadataItems } = useObjectMetadataItemForSettings(); const navigate = useNavigate(); const { getIcon } = useIcons(); const currentPath = useLocation().pathname; - const viewObjectMetadataItem = findObjectMetadataItemByNamePlural('views'); - - const { cachedRootQuery } = useCachedRootQuery({ - objectMetadataItem: viewObjectMetadataItem, - queryMethodName: QueryMethodName.FindMany, - }); - - const { records } = useFindManyRecords({ - skip: cachedRootQuery?.views, - objectNameSingular: CoreObjectNameSingular.View, - useRecordsWithoutConnection: true, - }); - - const views = - records.length > 0 - ? records - : cachedRootQuery?.views?.edges?.map((edge: any) => edge?.node); + const { records: views } = usePrefetchedData( + PrefetchKey.AllViews, + ); return ( <> @@ -63,9 +48,11 @@ export const ObjectMetadataNavItems = () => { : -1; }), ].map((objectMetadataItem) => { - const viewId = views?.find( - (view: any) => view?.objectMetadataId === objectMetadataItem.id, - )?.id; + const objectMetadataViews = getObjectMetadataItemViews( + objectMetadataItem.id, + views, + ); + const viewId = objectMetadataViews[0]?.id; const navigationPath = `/objects/${objectMetadataItem.namePlural}${ viewId ? `?view=${viewId}` : '' diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFilterOutUnexistingObjectMetadataItems.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFilterOutUnexistingObjectMetadataItems.test.tsx index d23f347d67a4..e544f642c0b5 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFilterOutUnexistingObjectMetadataItems.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useFilterOutUnexistingObjectMetadataItems.test.tsx @@ -12,7 +12,7 @@ describe('useFilterOutUnexistingObjectMetadataItems', () => { it('should work as expected', async () => { const { result } = renderHook( () => { - const setMetadataItems = useSetRecoilState(objectMetadataItemsState()); + const setMetadataItems = useSetRecoilState(objectMetadataItemsState); setMetadataItems(mockObjectMetadataItems.slice(1)); return useFilterOutUnexistingObjectMetadataItems(); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetObjectRecordIdentifierByNameSingular.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetObjectRecordIdentifierByNameSingular.test.tsx index 6f25e039ca6b..1a4c0e2cbfcc 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetObjectRecordIdentifierByNameSingular.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetObjectRecordIdentifierByNameSingular.test.tsx @@ -17,7 +17,7 @@ describe('useGetObjectRecordIdentifierByNameSingular', () => { record: any; objectNameSingular: string; }) => { - const setMetadataItems = useSetRecoilState(objectMetadataItemsState()); + const setMetadataItems = useSetRecoilState(objectMetadataItemsState); setMetadataItems(mockObjectMetadataItems); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetRelationMetadata.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetRelationMetadata.test.tsx index 45486629086c..b13554ed6e14 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetRelationMetadata.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useGetRelationMetadata.test.tsx @@ -31,7 +31,7 @@ describe('useGetRelationMetadata', () => { const { result } = renderHook( () => { - const setMetadataItems = useSetRecoilState(objectMetadataItemsState()); + const setMetadataItems = useSetRecoilState(objectMetadataItemsState); useEffect(() => { setMetadataItems(objectMetadataItems); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemForSettings.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemForSettings.test.tsx index e13c468e4ead..c93057e2e8f5 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemForSettings.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useObjectMetadataItemForSettings.test.tsx @@ -44,7 +44,7 @@ describe('useObjectMetadataItemForSettings', () => { it('should findActiveObjectMetadataItemBySlug', async () => { const { result } = renderHook( () => { - const setMetadataItems = useSetRecoilState(objectMetadataItemsState()); + const setMetadataItems = useSetRecoilState(objectMetadataItemsState); setMetadataItems(mockObjectMetadataItems); return useObjectMetadataItemForSettings(); @@ -64,7 +64,7 @@ describe('useObjectMetadataItemForSettings', () => { it('should findObjectMetadataItemById', async () => { const { result } = renderHook( () => { - const setMetadataItems = useSetRecoilState(objectMetadataItemsState()); + const setMetadataItems = useSetRecoilState(objectMetadataItemsState); setMetadataItems(mockObjectMetadataItems); return useObjectMetadataItemForSettings(); @@ -86,7 +86,7 @@ describe('useObjectMetadataItemForSettings', () => { it('should findObjectMetadataItemByNamePlural', async () => { const { result } = renderHook( () => { - const setMetadataItems = useSetRecoilState(objectMetadataItemsState()); + const setMetadataItems = useSetRecoilState(objectMetadataItemsState); setMetadataItems(mockObjectMetadataItems); return useObjectMetadataItemForSettings(); diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts index e187ba832105..7e4f54f65203 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useCreateOneObjectMetadataItem.ts @@ -1,6 +1,8 @@ -import { ApolloClient, useMutation } from '@apollo/client'; +import { ApolloClient, useApolloClient, useMutation } from '@apollo/client'; import { getOperationName } from '@apollo/client/utilities'; +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; import { CreateObjectInput, CreateOneObjectMetadataItemMutation, @@ -14,6 +16,10 @@ import { useApolloMetadataClient } from './useApolloMetadataClient'; export const useCreateOneObjectMetadataItem = () => { const apolloMetadataClient = useApolloMetadataClient(); + const apolloClient = useApolloClient(); + const { findManyRecordsQuery } = useObjectMetadataItem({ + objectNameSingular: CoreObjectNameSingular.View, + }); const [mutate] = useMutation< CreateOneObjectMetadataItemMutation, @@ -23,13 +29,20 @@ export const useCreateOneObjectMetadataItem = () => { }); const createOneObjectMetadataItem = async (input: CreateObjectInput) => { - return await mutate({ + const createdObjectMetadata = await mutate({ variables: { input: { object: input }, }, awaitRefetchQueries: true, refetchQueries: [getOperationName(FIND_MANY_OBJECT_METADATA_ITEMS) ?? ''], }); + + await apolloClient.query({ + query: findManyRecordsQuery, + fetchPolicy: 'network-only', + }); + + return createdObjectMetadata; }; return { diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular.ts index 53c878f873ab..be32e2162949 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useGetObjectRecordIdentifierByNameSingular.ts @@ -5,7 +5,7 @@ import { getObjectRecordIdentifier } from '@/object-metadata/utils/getObjectReco import { ObjectRecordIdentifier } from '@/object-record/types/ObjectRecordIdentifier'; export const useGetObjectRecordIdentifierByNameSingular = () => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState()); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); return (record: any, objectNameSingular: string): ObjectRecordIdentifier => { const objectMetadataItem = objectMetadataItems.find( diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts index 5bd2c94a36f0..a4edf8c39c59 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItem.ts @@ -42,7 +42,7 @@ export const useObjectMetadataItem = ( depth?: number, eagerLoadedRelations?: Record, ) => { - const currentWorkspace = useRecoilValue(currentWorkspaceState()); + const currentWorkspace = useRecoilValue(currentWorkspaceState); const mockObjectMetadataItems = getObjectMetadataItemsMock(); @@ -53,7 +53,7 @@ export const useObjectMetadataItem = ( }), ); - let objectMetadataItems = useRecoilValue(objectMetadataItemsState()); + let objectMetadataItems = useRecoilValue(objectMetadataItemsState); if (currentWorkspace?.activationStatus !== 'active') { objectMetadataItem = diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemForSettings.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemForSettings.ts index fce0388690b8..eb8bfe651f18 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemForSettings.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemForSettings.ts @@ -5,7 +5,7 @@ import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadat import { getObjectSlug } from '../utils/getObjectSlug'; export const useObjectMetadataItemForSettings = () => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState()); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const activeObjectMetadataItems = objectMetadataItems.filter( ({ isActive, isSystem }) => isActive && !isSystem, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemOnly.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemOnly.ts index aa74d57cc4b2..d5e5b3f552dc 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemOnly.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItemOnly.ts @@ -12,7 +12,7 @@ import { ObjectMetadataItemIdentifier } from '../types/ObjectMetadataItemIdentif export const useObjectMetadataItemOnly = ({ objectNameSingular, }: ObjectMetadataItemIdentifier) => { - const currentWorkspace = useRecoilValue(currentWorkspaceState()); + const currentWorkspace = useRecoilValue(currentWorkspaceState); const mockObjectMetadataItems = getObjectMetadataItemsMock(); @@ -23,7 +23,7 @@ export const useObjectMetadataItemOnly = ({ }), ); - let objectMetadataItems = useRecoilValue(objectMetadataItemsState()); + let objectMetadataItems = useRecoilValue(objectMetadataItemsState); if (currentWorkspace?.activationStatus !== 'active') { objectMetadataItem = diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItems.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItems.ts index 0085cccabcfa..215d61200c57 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItems.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectMetadataItems.ts @@ -3,7 +3,7 @@ import { useRecoilValue } from 'recoil'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; export const useObjectMetadataItems = () => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState()); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); return { objectMetadataItems, diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts index 5af4b781f970..3cf1c60bbb7c 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNamePluralFromSingular.ts @@ -10,7 +10,7 @@ export const useObjectNamePluralFromSingular = ({ }: { objectNameSingular: string; }) => { - const currentWorkspace = useRecoilValue(currentWorkspaceState()); + const currentWorkspace = useRecoilValue(currentWorkspaceState); const mockObjectMetadataItems = getObjectMetadataItemsMock(); let objectMetadataItem = useRecoilValue( diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts index 3e189e224784..432a3b718700 100644 --- a/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts +++ b/packages/twenty-front/src/modules/object-metadata/hooks/useObjectNameSingularFromPlural.ts @@ -10,7 +10,7 @@ export const useObjectNameSingularFromPlural = ({ }: { objectNamePlural: string; }) => { - const currentWorkspace = useRecoilValue(currentWorkspaceState()); + const currentWorkspace = useRecoilValue(currentWorkspaceState); const mockObjectMetadataItems = getObjectMetadataItemsMock(); diff --git a/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemFamilySelector.ts b/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemFamilySelector.ts index 7e0d695b3e6f..b264758fbaef 100644 --- a/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemFamilySelector.ts +++ b/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemFamilySelector.ts @@ -16,7 +16,7 @@ export const objectMetadataItemFamilySelector = selectorFamily< get: ({ objectNameType, objectName }: ObjectMetadataItemSelector) => ({ get }) => { - const objectMetadataItems = get(objectMetadataItemsState()); + const objectMetadataItems = get(objectMetadataItemsState); if (objectNameType === 'singular') { return ( diff --git a/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemsByNamePluralMapSelector.ts b/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemsByNamePluralMapSelector.ts index 7168e5054ea6..30dfb115ef8e 100644 --- a/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemsByNamePluralMapSelector.ts +++ b/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemsByNamePluralMapSelector.ts @@ -8,7 +8,7 @@ export const objectMetadataItemsByNamePluralMapSelector = selector< >({ key: 'objectMetadataItemsByNamePluralMapSelector', get: ({ get }) => { - const objectMetadataItems = get(objectMetadataItemsState()); + const objectMetadataItems = get(objectMetadataItemsState); return new Map( objectMetadataItems.map((objectMetadataItem) => [ diff --git a/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemsByNameSingularMapSelector.ts b/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemsByNameSingularMapSelector.ts index 9111cc6c2d4b..2eddad42b1f9 100644 --- a/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemsByNameSingularMapSelector.ts +++ b/packages/twenty-front/src/modules/object-metadata/states/objectMetadataItemsByNameSingularMapSelector.ts @@ -8,7 +8,7 @@ export const objectMetadataItemsByNameSingularMapSelector = selector< >({ key: 'objectMetadataItemsByNameSingularMapSelector', get: ({ get }) => { - const objectMetadataItems = get(objectMetadataItemsState()); + const objectMetadataItems = get(objectMetadataItemsState); return new Map( objectMetadataItems.map((objectMetadataItem) => [ diff --git a/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts index e148338d90e4..9886f09b722e 100644 --- a/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts +++ b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts @@ -4,9 +4,11 @@ export enum CoreObjectNameSingular { ApiKey = 'apiKey', Attachment = 'attachment', Blocklist = 'blocklist', + CalendarEvent = 'calendarEvent', Comment = 'comment', Company = 'company', ConnectedAccount = 'connectedAccount', + Event = 'event', Favorite = 'favorite', Message = 'message', MessageChannel = 'messageChannel', @@ -14,7 +16,6 @@ export enum CoreObjectNameSingular { MessageThread = 'messageThread', Opportunity = 'opportunity', Person = 'person', - PipelineStep = 'pipelineStep', View = 'view', ViewField = 'viewField', ViewFilter = 'viewFilter', diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.tsx b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.tsx index 4ca474d31f13..c71702c94238 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapFieldMetadataToGraphQLQuery.test.tsx @@ -150,7 +150,6 @@ personId pointOfContactId updatedAt companyId -pipelineStepId probability closeDate amount diff --git a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.tsx b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.tsx index 7473906175f9..a1d34e9b5b75 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-metadata/utils/__tests__/mapObjectMetadataToGraphQLQuery.test.tsx @@ -78,7 +78,6 @@ personId pointOfContactId updatedAt companyId -pipelineStepId probability closeDate amount @@ -106,7 +105,6 @@ personId pointOfContactId updatedAt companyId -pipelineStepId probability closeDate amount diff --git a/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts b/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts index 69202da6b0e0..e9549cf70904 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts @@ -271,6 +271,23 @@ export const getObjectMetadataItemsMock = () => { }, ], }, + { + __typename: 'object', + id: '20202020-ddee-40de-9c9b-5f82a3503361', + dataSourceId: '20202020-7f63-47a9-b1b3-6c7290ca9fb1', + nameSingular: 'calendarEvent', + namePlural: 'calendarEvents', + labelSingular: 'Calendar Event', + labelPlural: 'Calendar Events', + description: 'A calendar event', + icon: 'IconCalendarEvent', + isCustom: false, + isActive: true, + isSystem: true, + createdAt: '2023-11-30T11:13:15.206Z', + updatedAt: '2023-11-30T11:13:15.206Z', + fields: [], + }, { __typename: 'object', id: '20202020-cae9-4ff4-9579-f7d9fe44c937', @@ -384,23 +401,6 @@ export const getObjectMetadataItemsMock = () => { fromRelationMetadata: null, toRelationMetadata: null, }, - { - __typename: 'field', - id: '20202020-0a2e-4676-8011-3fdb2c30d7f8', - type: 'UUID', - name: 'pipelineStepId', - label: 'Pipeline Step ID (foreign key)', - description: 'Foreign key for pipeline step', - icon: null, - isCustom: false, - isActive: true, - isSystem: true, - isNullable: true, - createdAt: '2023-11-30T11:13:15.308Z', - updatedAt: '2023-11-30T11:13:15.308Z', - fromRelationMetadata: null, - toRelationMetadata: null, - }, { __typename: 'field', id: '20202020-3b9c-4e58-a3d2-c617d3b596b1', @@ -419,32 +419,63 @@ export const getObjectMetadataItemsMock = () => { toRelationMetadata: null, }, { - __typename: 'field', - id: '20202020-0a2e-4676-8011-3fdb2c30c258', - type: 'RELATION', - name: 'pipelineStep', - label: 'Pipeline Step', - description: 'Opportunity pipeline step', - icon: 'IconKanban', - isCustom: false, - isActive: true, - isSystem: true, - isNullable: true, - createdAt: '2023-11-30T11:13:15.308Z', - updatedAt: '2023-11-30T11:13:15.308Z', - fromRelationMetadata: null, - toRelationMetadata: { - __typename: 'relation', - id: 'dfb44970-3e09-49f2-9f1d-51c8c451b8f5', - relationType: 'ONE_TO_MANY', - fromObjectMetadata: { - __typename: 'object', - id: '20202020-1029-4661-9e91-83bad932bdcd', - dataSourceId: '20202020-7f63-47a9-b1b3-6c7290ca9fb1', - nameSingular: 'pipelineStep', - namePlural: 'pipelineSteps', + __typename: 'fieldEdge', + node: { + __typename: 'field', + id: '20202020-46cc-42bb-90d5-c724921a012d', + type: 'SELECT', + name: 'stage', + label: 'Stage', + description: 'Opportunity stage', + icon: 'IconProgressCheck', + isCustom: false, + isActive: true, + isSystem: false, + isNullable: false, + createdAt: '2024-03-21T16:48:40.384Z', + updatedAt: '2024-03-21T16:48:40.384Z', + defaultValue: { + value: 'NEW', }, - fromFieldMetadataId: '20202020-22c4-443a-b114-43c97dda5867', + options: [ + { + id: '20202020-aa3b-4c0b-bd90-9d071e3b9bf2', + color: 'red', + label: 'New', + value: 'NEW', + position: 0, + }, + { + id: '20202020-8f9b-4bc3-b0a0-ce6a5085c1cf', + color: 'purple', + label: 'Screening', + value: 'SCREENING', + position: 1, + }, + { + id: '20202020-9797-448d-81e4-49b055a1d19b', + color: 'sky', + label: 'Meeting', + value: 'MEETING', + position: 2, + }, + { + id: '20202020-d542-479c-bc88-3c6d4ee78d09', + color: 'turquoise', + label: 'Proposal', + value: 'PROPOSAL', + position: 3, + }, + { + id: '20202020-b69a-4c9c-ac16-adcba0ec972d', + color: 'yellow', + label: 'Customer', + value: 'CUSTOMER', + position: 4, + }, + ], + fromRelationMetadata: null, + toRelationMetadata: null, }, }, { @@ -3520,155 +3551,6 @@ export const getObjectMetadataItemsMock = () => { }, ], }, - { - __typename: 'object', - id: '20202020-1029-4661-9e91-83bad932bdcd', - dataSourceId: '20202020-7f63-47a9-b1b3-6c7290ca9fb1', - nameSingular: 'pipelineStep', - namePlural: 'pipelineSteps', - labelSingular: 'Pipeline Step', - labelPlural: 'Pipeline Steps', - description: 'A pipeline step', - icon: 'IconLayoutKanban', - isCustom: false, - isActive: true, - isSystem: true, - createdAt: '2023-11-30T11:13:15.206Z', - updatedAt: '2023-11-30T11:13:15.206Z', - fields: [ - { - __typename: 'field', - id: '20202020-f294-430e-b800-3a411fc05ad3', - type: 'TEXT', - name: 'name', - label: 'Name', - description: 'Pipeline Step name', - icon: 'IconCurrencyDollar', - isCustom: false, - isActive: true, - isSystem: false, - isNullable: false, - createdAt: '2023-11-30T11:13:15.337Z', - updatedAt: '2023-11-30T11:13:15.337Z', - fromRelationMetadata: null, - toRelationMetadata: null, - }, - { - __typename: 'field', - id: '20202020-039a-4fbd-b4c1-66dfa9e4bd3f', - type: 'UUID', - name: 'id', - label: 'Id', - description: null, - icon: null, - isCustom: false, - isActive: true, - isSystem: true, - isNullable: false, - createdAt: '2023-11-30T11:13:15.337Z', - updatedAt: '2023-11-30T11:13:15.337Z', - fromRelationMetadata: null, - toRelationMetadata: null, - }, - { - __typename: 'field', - id: '20202020-816f-4861-9b36-4a2f8ae2791c', - type: 'DATE_TIME', - name: 'createdAt', - label: 'Creation date', - description: null, - icon: 'IconCalendar', - isCustom: false, - isActive: true, - isSystem: true, - isNullable: false, - createdAt: '2023-11-30T11:13:15.337Z', - updatedAt: '2023-11-30T11:13:15.337Z', - fromRelationMetadata: null, - toRelationMetadata: null, - }, - { - __typename: 'field', - id: '20202020-22c4-443a-b114-43c97dda5867', - type: 'RELATION', - name: 'opportunities', - label: 'Opportunities', - description: 'Opportunities linked to the step.', - icon: 'IconTargetArrow', - isCustom: false, - isActive: true, - isSystem: false, - isNullable: true, - createdAt: '2023-11-30T11:13:15.337Z', - updatedAt: '2023-11-30T11:13:15.337Z', - fromRelationMetadata: { - __typename: 'relation', - id: 'dfb44970-3e09-49f2-9f1d-51c8c451b8f5', - relationType: 'ONE_TO_MANY', - toObjectMetadata: { - __typename: 'object', - id: '20202020-cae9-4ff4-9579-f7d9fe44c937', - dataSourceId: '20202020-7f63-47a9-b1b3-6c7290ca9fb1', - nameSingular: 'opportunity', - namePlural: 'opportunities', - }, - toFieldMetadataId: '20202020-0a2e-4676-8011-3fdb2c30c258', - }, - toRelationMetadata: null, - }, - { - __typename: 'field', - id: '20202020-6296-4cab-aafb-121ef5822b13', - type: 'NUMBER', - name: 'position', - label: 'Position', - description: 'Pipeline Step position', - icon: 'IconHierarchy2', - isCustom: false, - isActive: true, - isSystem: false, - isNullable: false, - createdAt: '2023-11-30T11:13:15.337Z', - updatedAt: '2023-11-30T11:13:15.337Z', - fromRelationMetadata: null, - toRelationMetadata: null, - }, - { - __typename: 'field', - id: '20202020-5b93-4b28-8c45-7988ea68f91b', - type: 'TEXT', - name: 'color', - label: 'Color', - description: 'Pipeline Step color', - icon: 'IconColorSwatch', - isCustom: false, - isActive: true, - isSystem: false, - isNullable: false, - createdAt: '2023-11-30T11:13:15.337Z', - updatedAt: '2023-11-30T11:13:15.337Z', - fromRelationMetadata: null, - toRelationMetadata: null, - }, - { - __typename: 'field', - id: '20202020-2d73-4829-b774-522c2f5627d7', - type: 'DATE_TIME', - name: 'updatedAt', - label: 'Update date', - description: null, - icon: 'IconCalendar', - isCustom: false, - isActive: true, - isSystem: true, - isNullable: false, - createdAt: '2023-11-30T11:13:15.337Z', - updatedAt: '2023-11-30T11:13:15.337Z', - fromRelationMetadata: null, - toRelationMetadata: null, - }, - ], - }, ]; // Todo fix typing here (the backend is not in sync with the frontend) diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts index 4603ac0cefc0..e6dc29a1c31e 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapFieldMetadataToGraphQLQuery.ts @@ -34,6 +34,7 @@ export const mapFieldMetadataToGraphQLQuery = ({ 'RATING', 'SELECT', 'POSITION', + 'RAW_JSON', ] as FieldMetadataType[] ).includes(fieldType); diff --git a/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts b/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts index e56fff7e12f6..444c566f2b80 100644 --- a/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts +++ b/packages/twenty-front/src/modules/object-metadata/utils/mapObjectMetadataToGraphQLQuery.ts @@ -11,7 +11,7 @@ export const mapObjectMetadataToGraphQLQuery = ({ eagerLoadedRelations, }: { objectMetadataItems: ObjectMetadataItem[]; - objectMetadataItem: Pick; + objectMetadataItem: Pick; depth?: number; eagerLoadedRelations?: Record; }): any => { diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useAddRecordInCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useAddRecordInCache.ts index c8d821927e2d..3ef788f6cc16 100644 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useAddRecordInCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useAddRecordInCache.ts @@ -15,7 +15,7 @@ export const useAddRecordInCache = ({ }: { objectMetadataItem: ObjectMetadataItem; }) => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState()); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const apolloClient = useApolloClient(); const { injectIntoFindOneRecordQueryCache } = diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useCachedRootQuery.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useCachedRootQuery.ts deleted file mode 100644 index 8ffd3ad71555..000000000000 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useCachedRootQuery.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { useApolloClient } from '@apollo/client/react/hooks/useApolloClient'; -import gql from 'graphql-tag'; -import { useRecoilValue } from 'recoil'; - -import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; -import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; -import { QueryMethodName } from '@/object-metadata/types/QueryMethodName'; -import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; - -export const useCachedRootQuery = ({ - objectMetadataItem, - queryMethodName, -}: { - objectMetadataItem: ObjectMetadataItem | undefined; - queryMethodName: QueryMethodName; -}) => { - const apolloClient = useApolloClient(); - const objectMetadataItems = useRecoilValue(objectMetadataItemsState()); - - if (!objectMetadataItem) { - return { cachedRootQuery: null }; - } - - const cacheReadFragment = gql` - fragment RootQuery on Query { - ${ - QueryMethodName.FindMany === queryMethodName - ? objectMetadataItem.namePlural - : objectMetadataItem.nameSingular - } - ${QueryMethodName.FindMany === queryMethodName ? '{ edges { node ' : ''} - ${mapObjectMetadataToGraphQLQuery({ - objectMetadataItems, - objectMetadataItem, - depth: 0, - })} - ${QueryMethodName.FindMany === queryMethodName ? '}}' : ''} - } - `; - - const cachedRootQuery = apolloClient.readFragment({ - id: 'ROOT_QUERY', - fragment: cacheReadFragment, - }); - - return { cachedRootQuery }; -}; diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useGetRecordFromCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useGetRecordFromCache.ts index ae30c5897648..3f9e35a95cb7 100644 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useGetRecordFromCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useGetRecordFromCache.ts @@ -1,3 +1,4 @@ +import { useCallback } from 'react'; import { gql, useApolloClient } from '@apollo/client'; import { useRecoilValue } from 'recoil'; @@ -13,21 +14,22 @@ export const useGetRecordFromCache = ({ }: { objectMetadataItem: ObjectMetadataItem; }) => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState()); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const apolloClient = useApolloClient(); - return ( - recordId: string, - cache = apolloClient.cache, - ) => { - if (isUndefinedOrNull(objectMetadataItem)) { - return null; - } + return useCallback( + ( + recordId: string, + cache = apolloClient.cache, + ) => { + if (isUndefinedOrNull(objectMetadataItem)) { + return null; + } - const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular); + const capitalizedObjectName = capitalize(objectMetadataItem.nameSingular); - const cacheReadFragment = gql` + const cacheReadFragment = gql` fragment ${capitalizedObjectName}Fragment on ${capitalizedObjectName} ${mapObjectMetadataToGraphQLQuery( { objectMetadataItems, @@ -36,14 +38,16 @@ export const useGetRecordFromCache = ({ )} `; - const cachedRecordId = cache.identify({ - __typename: capitalize(objectMetadataItem.nameSingular), - id: recordId, - }); - - return cache.readFragment({ - id: cachedRecordId, - fragment: cacheReadFragment, - }); - }; + const cachedRecordId = cache.identify({ + __typename: capitalize(objectMetadataItem.nameSingular), + id: recordId, + }); + + return cache.readFragment({ + id: cachedRecordId, + fragment: cacheReadFragment, + }); + }, + [objectMetadataItem, objectMetadataItems, apolloClient], + ); }; diff --git a/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts b/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts index 9a11224b1372..297be5db3c36 100644 --- a/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts +++ b/packages/twenty-front/src/modules/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache.ts @@ -10,7 +10,10 @@ import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQu export const useUpsertFindManyRecordsQueryInCache = ({ objectMetadataItem, }: { - objectMetadataItem: ObjectMetadataItem; + objectMetadataItem: Pick< + ObjectMetadataItem, + 'fields' | 'namePlural' | 'nameSingular' + >; }) => { const apolloClient = useApolloClient(); diff --git a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordEdgeFromRecord.ts b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordEdgeFromRecord.ts index 8921edae21e7..86a09e9f8c15 100644 --- a/packages/twenty-front/src/modules/object-record/cache/utils/getRecordEdgeFromRecord.ts +++ b/packages/twenty-front/src/modules/object-record/cache/utils/getRecordEdgeFromRecord.ts @@ -1,5 +1,6 @@ import { getEdgeTypename } from '@/object-record/cache/utils/getEdgeTypename'; import { getNodeTypename } from '@/object-record/cache/utils/getNodeTypename'; +import { getRecordConnectionFromRecords } from '@/object-record/cache/utils/getRecordConnectionFromRecords'; import { ObjectRecord } from '@/object-record/types/ObjectRecord'; import { ObjectRecordEdge } from '@/object-record/types/ObjectRecordEdge'; @@ -10,11 +11,27 @@ export const getRecordEdgeFromRecord = ({ objectNameSingular: string; record: T; }) => { + const nestedRecord = Object.fromEntries( + Object.entries(record).map(([key, value]) => { + if (Array.isArray(value)) { + return [ + key, + getRecordConnectionFromRecords({ + // Todo: this is a ugly and broken hack to get the singular, we need to infer this from metadata + objectNameSingular: key.slice(0, -1), + records: value as ObjectRecord[], + }), + ]; + } + return [key, value]; + }), + ) as T; // Todo fix typing once we have investigated apollo edges / nodes removal + return { __typename: getEdgeTypename({ objectNameSingular }), node: { __typename: getNodeTypename({ objectNameSingular }), - ...record, + ...nestedRecord, }, cursor: '', } as ObjectRecordEdge; diff --git a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindManyRecords.ts index 84eb553d97e9..7a756cac76ef 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/__mocks__/useFindManyRecords.ts @@ -26,7 +26,7 @@ export const query = gql` pointOfContactId updatedAt companyId - pipelineStepId + stage probability closeDate amount { @@ -52,7 +52,7 @@ export const query = gql` pointOfContactId updatedAt companyId - pipelineStepId + stage probability closeDate amount { diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx index 9da0f77fc28c..480e74bab650 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useFindManyRecords.test.tsx @@ -57,7 +57,7 @@ describe('useFindManyRecords', () => { const { result } = renderHook( () => { const setCurrentWorkspaceMember = useSetRecoilState( - currentWorkspaceMemberState(), + currentWorkspaceMemberState, ); setCurrentWorkspaceMember({ id: '32219445-f587-4c40-b2b1-6d3205ed96da', @@ -67,7 +67,7 @@ describe('useFindManyRecords', () => { const mockObjectMetadataItems = getObjectMetadataItemsMock(); - const setMetadataItems = useSetRecoilState(objectMetadataItemsState()); + const setMetadataItems = useSetRecoilState(objectMetadataItemsState); setMetadataItems(mockObjectMetadataItems); diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.test.tsx index 2b1ef39614ea..9120bb026ee4 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.test.tsx @@ -16,7 +16,7 @@ describe('useGenerateFindManyRecordsForMultipleMetadataItemsQuery', () => { const mockObjectMetadataItems = getObjectMetadataItemsMock(); return useGenerateFindManyRecordsForMultipleMetadataItemsQuery({ - objectMetadataItems: mockObjectMetadataItems.slice(0, 2), + targetObjectMetadataItems: mockObjectMetadataItems.slice(0, 2), }); }, { diff --git a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useMapConnectionToRecords.test.tsx b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useMapConnectionToRecords.test.tsx index a975e7cb077f..6d58d3b7dbf3 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/__tests__/useMapConnectionToRecords.test.tsx +++ b/packages/twenty-front/src/modules/object-record/hooks/__tests__/useMapConnectionToRecords.test.tsx @@ -18,7 +18,7 @@ import { isDefined } from '~/utils/isDefined'; const Wrapper = getJestHookWrapper({ apolloMocks: [], onInitializeRecoilSnapshot: (snapshot) => { - snapshot.set(objectMetadataItemsState(), getObjectMetadataItemsMock()); + snapshot.set(objectMetadataItemsState, getObjectMetadataItemsMock()); }, }); diff --git a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts index f3d201bac482..4531e47e6e12 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useFindManyRecords.ts @@ -69,7 +69,7 @@ export const useFindManyRecords = ({ ); const { enqueueSnackBar } = useSnackBar(); - const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState()); + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const { data, loading, error, fetchMore } = useQuery< ObjectRecordQueryResult diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateManyRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateManyRecordMutation.ts index c36c99f5ce5d..686a5cc5ca49 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateManyRecordMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateManyRecordMutation.ts @@ -17,7 +17,7 @@ export const useGenerateCreateManyRecordMutation = ({ }: { objectMetadataItem: ObjectMetadataItem; }) => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState()); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); if (isUndefinedOrNull(objectMetadataItem)) { return EMPTY_MUTATION; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateOneRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateOneRecordMutation.ts index 61cef84604ac..b6837892af73 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateOneRecordMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateCreateOneRecordMutation.ts @@ -17,7 +17,7 @@ export const useGenerateCreateOneRecordMutation = ({ }: { objectMetadataItem: ObjectMetadataItem; }) => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState()); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); if (isUndefinedOrNull(objectMetadataItem)) { return EMPTY_MUTATION; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateExecuteQuickActionOnOneRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateExecuteQuickActionOnOneRecordMutation.ts index 80719fe39fe6..0eb9d6b6c7d2 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateExecuteQuickActionOnOneRecordMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateExecuteQuickActionOnOneRecordMutation.ts @@ -21,7 +21,7 @@ export const useGenerateExecuteQuickActionOnOneRecordMutation = ({ }: { objectMetadataItem: ObjectMetadataItem; }) => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState()); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); if (isUndefinedOrNull(objectMetadataItem)) { return EMPTY_MUTATION; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindDuplicateRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindDuplicateRecordsQuery.ts index 51bba8e5feed..747018d0da8d 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindDuplicateRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindDuplicateRecordsQuery.ts @@ -11,7 +11,7 @@ export const getFindDuplicateRecordsQueryResponseField = ( ) => `${objectNameSingular}Duplicates`; export const useGenerateFindDuplicateRecordsQuery = () => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState()); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); return ({ objectMetadataItem, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.ts index c453b0c1bcb4..60b1ca38f086 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery.ts @@ -1,18 +1,21 @@ import { gql } from '@apollo/client'; +import { useRecoilValue } from 'recoil'; +import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { ObjectMetadataItem } from '@/object-metadata/types/ObjectMetadataItem'; import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObjectMetadataToGraphQLQuery'; import { isNonEmptyArray } from '~/utils/isNonEmptyArray'; import { capitalize } from '~/utils/string/capitalize'; export const useGenerateFindManyRecordsForMultipleMetadataItemsQuery = ({ - objectMetadataItems, + targetObjectMetadataItems, depth, }: { - objectMetadataItems: ObjectMetadataItem[]; + targetObjectMetadataItems: ObjectMetadataItem[]; depth?: number; }) => { - const capitalizedObjectNameSingulars = objectMetadataItems.map( + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); + const capitalizedObjectNameSingulars = targetObjectMetadataItems.map( ({ nameSingular }) => capitalize(nameSingular), ); @@ -44,7 +47,7 @@ export const useGenerateFindManyRecordsForMultipleMetadataItemsQuery = ({ const limitPerMetadataItemArray = capitalizedObjectNameSingulars .map( (capitalizedObjectNameSingular) => - `$limit${capitalizedObjectNameSingular}: Float = 5`, + `$limit${capitalizedObjectNameSingular}: Float`, ) .join(', '); @@ -55,7 +58,7 @@ export const useGenerateFindManyRecordsForMultipleMetadataItemsQuery = ({ ${lastCursorPerMetadataItemArray}, ${limitPerMetadataItemArray} ) { - ${objectMetadataItems + ${targetObjectMetadataItems .map( (objectMetadataItem) => `${objectMetadataItem.namePlural}(filter: $filter${capitalize( @@ -69,7 +72,7 @@ export const useGenerateFindManyRecordsForMultipleMetadataItemsQuery = ({ )}){ edges { node ${mapObjectMetadataToGraphQLQuery({ - objectMetadataItems, + objectMetadataItems: objectMetadataItems, objectMetadataItem, depth, })} @@ -80,6 +83,7 @@ export const useGenerateFindManyRecordsForMultipleMetadataItemsQuery = ({ startCursor endCursor } + totalCount }`, ) .join('\n')} diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts index db6d0778177e..5a21fe3de6ad 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindManyRecordsQuery.ts @@ -7,7 +7,7 @@ import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObje import { capitalize } from '~/utils/string/capitalize'; export const useGenerateFindManyRecordsQuery = () => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState()); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); return ({ objectMetadataItem, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindOneRecordQuery.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindOneRecordQuery.ts index 77a14395a845..e9260cd710f7 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindOneRecordQuery.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateFindOneRecordQuery.ts @@ -7,7 +7,7 @@ import { mapObjectMetadataToGraphQLQuery } from '@/object-metadata/utils/mapObje import { capitalize } from '~/utils/string/capitalize'; export const useGenerateFindOneRecordQuery = () => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState()); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); return ({ objectMetadataItem, diff --git a/packages/twenty-front/src/modules/object-record/hooks/useGenerateUpdateOneRecordMutation.ts b/packages/twenty-front/src/modules/object-record/hooks/useGenerateUpdateOneRecordMutation.ts index 1ba33a915d6d..8880f3a0b820 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useGenerateUpdateOneRecordMutation.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useGenerateUpdateOneRecordMutation.ts @@ -17,7 +17,7 @@ export const useGenerateUpdateOneRecordMutation = ({ }: { objectMetadataItem: ObjectMetadataItem; }) => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState()); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); if (isUndefinedOrNull(objectMetadataItem)) { return EMPTY_MUTATION; diff --git a/packages/twenty-front/src/modules/object-record/hooks/useMapConnectionToRecords.ts b/packages/twenty-front/src/modules/object-record/hooks/useMapConnectionToRecords.ts index bde4e4e92cb2..a682f1e2b6dd 100644 --- a/packages/twenty-front/src/modules/object-record/hooks/useMapConnectionToRecords.ts +++ b/packages/twenty-front/src/modules/object-record/hooks/useMapConnectionToRecords.ts @@ -11,7 +11,7 @@ import { FieldMetadataType } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; export const useMapConnectionToRecords = () => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState()); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const mapConnectionToRecords = useCallback( ({ diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx index 070d8bf8e4e4..152fb1f82a53 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent.tsx @@ -1,3 +1,5 @@ +import { useRecoilValue } from 'recoil'; + import { ObjectFilterDropdownSearchInput } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; @@ -20,11 +22,21 @@ export const MultipleFiltersDropdownContent = ({ filterDropdownId, }: MultipleFiltersDropdownContentProps) => { const { - isObjectFilterDropdownOperandSelectUnfolded, - filterDefinitionUsedInDropdown, - selectedOperandInDropdown, + isObjectFilterDropdownOperandSelectUnfoldedState, + filterDefinitionUsedInDropdownState, + selectedOperandInDropdownState, } = useFilterDropdown({ filterDropdownId }); + const isObjectFilterDropdownOperandSelectUnfolded = useRecoilValue( + isObjectFilterDropdownOperandSelectUnfoldedState, + ); + const filterDefinitionUsedInDropdown = useRecoilValue( + filterDefinitionUsedInDropdownState, + ); + const selectedOperandInDropdown = useRecoilValue( + selectedOperandInDropdownState, + ); + return ( <> {!filterDefinitionUsedInDropdown ? ( diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownButton.tsx index 0a4a16e62b41..3b5672783714 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownButton.tsx @@ -1,3 +1,5 @@ +import { useRecoilValue } from 'recoil'; + import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { ObjectFilterDropdownScope } from '@/object-record/object-filter-dropdown/scopes/ObjectFilterDropdownScope'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; @@ -14,9 +16,14 @@ export const ObjectFilterDropdownButton = ({ filterDropdownId, hotkeyScope, }: ObjectFilterDropdownButtonProps) => { - const { availableFilterDefinitions } = useFilterDropdown({ + const { availableFilterDefinitionsState } = useFilterDropdown({ filterDropdownId: filterDropdownId, }); + + const availableFilterDefinitions = useRecoilValue( + availableFilterDefinitionsState, + ); + const hasOnlyOneEntityFilter = availableFilterDefinitions.length === 1 && availableFilterDefinitions[0].type === 'RELATION'; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx index d3098859ef9e..11c9f1ab2820 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownDateInput.tsx @@ -1,15 +1,24 @@ +import { useRecoilValue } from 'recoil'; + import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { InternalDatePicker } from '@/ui/input/components/internal/date/components/InternalDatePicker'; import { isDefined } from '~/utils/isDefined'; export const ObjectFilterDropdownDateInput = () => { const { - filterDefinitionUsedInDropdown, - selectedOperandInDropdown, + filterDefinitionUsedInDropdownState, + selectedOperandInDropdownState, setIsObjectFilterDropdownUnfolded, selectFilter, } = useFilterDropdown(); + const filterDefinitionUsedInDropdown = useRecoilValue( + filterDefinitionUsedInDropdownState, + ); + const selectedOperandInDropdown = useRecoilValue( + selectedOperandInDropdownState, + ); + const handleChange = (date: Date | null) => { if (!filterDefinitionUsedInDropdown || !selectedOperandInDropdown) return; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchSelect.tsx index 0d19879da2ca..27b155975058 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownEntitySearchSelect.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { useRecoilValue } from 'recoil'; import { OBJECT_FILTER_DROPDOWN_ID } from '@/object-record/object-filter-dropdown/constants/ObjectFilterDropdownId'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; @@ -15,13 +16,24 @@ export const ObjectFilterDropdownEntitySearchSelect = ({ }) => { const { setObjectFilterDropdownSelectedEntityId, - filterDefinitionUsedInDropdown, - selectedOperandInDropdown, - objectFilterDropdownSearchInput, - selectedFilter, + filterDefinitionUsedInDropdownState, + selectedOperandInDropdownState, + objectFilterDropdownSearchInputState, + selectedFilterState, selectFilter, } = useFilterDropdown(); + const filterDefinitionUsedInDropdown = useRecoilValue( + filterDefinitionUsedInDropdownState, + ); + const selectedOperandInDropdown = useRecoilValue( + selectedOperandInDropdownState, + ); + const objectFilterDropdownSearchInput = useRecoilValue( + objectFilterDropdownSearchInputState, + ); + const selectedFilter = useRecoilValue(selectedFilterState); + const { closeDropdown } = useDropdown(OBJECT_FILTER_DROPDOWN_ID); const [isAllEntitySelected, setIsAllEntitySelected] = useState(false); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx index 93bc71a1be4b..676fcb9a32bb 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownFilterSelect.tsx @@ -1,3 +1,5 @@ +import { useRecoilValue } from 'recoil'; + import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { RelationPickerHotkeyScope } from '@/object-record/relation-picker/types/RelationPickerHotkeyScope'; import { useIcons } from '@/ui/display/icon/hooks/useIcons'; @@ -12,9 +14,13 @@ export const ObjectFilterDropdownFilterSelect = () => { setFilterDefinitionUsedInDropdown, setSelectedOperandInDropdown, setObjectFilterDropdownSearchInput, - availableFilterDefinitions, + availableFilterDefinitionsState, } = useFilterDropdown(); + const availableFilterDefinitions = useRecoilValue( + availableFilterDefinitionsState, + ); + const { getIcon } = useIcons(); const setHotkeyScope = useSetHotkeyScope(); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput.tsx index aec637714094..d6cc92eaaf1f 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownNumberInput.tsx @@ -1,15 +1,23 @@ import { ChangeEvent } from 'react'; +import { useRecoilValue } from 'recoil'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput'; export const ObjectFilterDropdownNumberInput = () => { const { - selectedOperandInDropdown, - filterDefinitionUsedInDropdown, + selectedOperandInDropdownState, + filterDefinitionUsedInDropdownState, selectFilter, } = useFilterDropdown(); + const filterDefinitionUsedInDropdown = useRecoilValue( + filterDefinitionUsedInDropdownState, + ); + const selectedOperandInDropdown = useRecoilValue( + selectedOperandInDropdownState, + ); + return ( filterDefinitionUsedInDropdown && selectedOperandInDropdown && ( diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandButton.tsx index 7c5e0b251353..6641de0dd480 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandButton.tsx @@ -1,3 +1,5 @@ +import { useRecoilValue } from 'recoil'; + import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { IconChevronDown } from '@/ui/display/icon'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; @@ -6,11 +8,18 @@ import { getOperandLabel } from '../utils/getOperandLabel'; export const ObjectFilterDropdownOperandButton = () => { const { - selectedOperandInDropdown, + selectedOperandInDropdownState, setIsObjectFilterDropdownOperandSelectUnfolded, - isObjectFilterDropdownOperandSelectUnfolded, + isObjectFilterDropdownOperandSelectUnfoldedState, } = useFilterDropdown(); + const selectedOperandInDropdown = useRecoilValue( + selectedOperandInDropdownState, + ); + const isObjectFilterDropdownOperandSelectUnfolded = useRecoilValue( + isObjectFilterDropdownOperandSelectUnfoldedState, + ); + if (isObjectFilterDropdownOperandSelectUnfolded) { return null; } diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx index 43886b392152..d3f0ccccfd6a 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOperandSelect.tsx @@ -1,3 +1,5 @@ +import { useRecoilValue } from 'recoil'; + import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; @@ -9,14 +11,24 @@ import { getOperandsForFilterType } from '../utils/getOperandsForFilterType'; export const ObjectFilterDropdownOperandSelect = () => { const { - filterDefinitionUsedInDropdown, + filterDefinitionUsedInDropdownState, setSelectedOperandInDropdown, - isObjectFilterDropdownOperandSelectUnfolded, + isObjectFilterDropdownOperandSelectUnfoldedState, setIsObjectFilterDropdownOperandSelectUnfolded, - selectedFilter, + selectedFilterState, selectFilter, } = useFilterDropdown(); + const filterDefinitionUsedInDropdown = useRecoilValue( + filterDefinitionUsedInDropdownState, + ); + + const isObjectFilterDropdownOperandSelectUnfolded = useRecoilValue( + isObjectFilterDropdownOperandSelectUnfoldedState, + ); + + const selectedFilter = useRecoilValue(selectedFilterState); + const operandsForFilterType = getOperandsForFilterType( filterDefinitionUsedInDropdown?.type, ); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx index db46aa88d131..9e09777fa50a 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownOptionSelect.tsx @@ -1,4 +1,5 @@ import { useEffect, useState } from 'react'; +import { useRecoilValue } from 'recoil'; import { MenuItem, MenuItemMultiSelect } from 'tsup.ui.index'; import { FieldMetadataItemOption } from '@/object-metadata/types/FieldMetadataItem'; @@ -16,13 +17,26 @@ type SelectOptionForFilter = FieldMetadataItemOption & { export const ObjectFilterDropdownOptionSelect = () => { const { - filterDefinitionUsedInDropdown, - objectFilterDropdownSearchInput, - selectedOperandInDropdown, - objectFilterDropdownSelectedOptionValues, + filterDefinitionUsedInDropdownState, + objectFilterDropdownSearchInputState, + selectedOperandInDropdownState, + objectFilterDropdownSelectedOptionValuesState, selectFilter, } = useFilterDropdown(); + const filterDefinitionUsedInDropdown = useRecoilValue( + filterDefinitionUsedInDropdownState, + ); + const selectedOperandInDropdown = useRecoilValue( + selectedOperandInDropdownState, + ); + const objectFilterDropdownSearchInput = useRecoilValue( + objectFilterDropdownSearchInputState, + ); + const objectFilterDropdownSelectedOptionValues = useRecoilValue( + objectFilterDropdownSelectedOptionValuesState, + ); + const fieldMetaDataId = filterDefinitionUsedInDropdown?.fieldMetadataId ?? ''; const { selectOptions } = useOptionsForSelect(fieldMetaDataId); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx index 0b41a5c5df00..65a2c3263dc3 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordSelect.tsx @@ -1,3 +1,5 @@ +import { useRecoilValue } from 'recoil'; + import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { MultipleRecordSelectDropdown } from '@/object-record/select/components/MultipleRecordSelectDropdown'; import { useRecordsForSelect } from '@/object-record/select/hooks/useRecordsForSelect'; @@ -9,15 +11,28 @@ export const MAX_RECORDS_TO_DISPLAY = 3; export const ObjectFilterDropdownRecordSelect = () => { const { - filterDefinitionUsedInDropdown, - objectFilterDropdownSearchInput, - selectedOperandInDropdown, + filterDefinitionUsedInDropdownState, + objectFilterDropdownSearchInputState, + selectedOperandInDropdownState, setObjectFilterDropdownSelectedRecordIds, - objectFilterDropdownSelectedRecordIds, + objectFilterDropdownSelectedRecordIdsState, selectFilter, emptyFilterButKeepDefinition, } = useFilterDropdown(); + const filterDefinitionUsedInDropdown = useRecoilValue( + filterDefinitionUsedInDropdownState, + ); + const objectFilterDropdownSearchInput = useRecoilValue( + objectFilterDropdownSearchInputState, + ); + const selectedOperandInDropdown = useRecoilValue( + selectedOperandInDropdownState, + ); + const objectFilterDropdownSelectedRecordIds = useRecoilValue( + objectFilterDropdownSelectedRecordIdsState, + ); + const objectNameSingular = filterDefinitionUsedInDropdown?.relationObjectMetadataNameSingular ?? ''; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput.tsx index 1a45f6aa3794..d84d2e4dde8e 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownSearchInput.tsx @@ -1,16 +1,27 @@ import { ChangeEvent } from 'react'; +import { useRecoilValue } from 'recoil'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; export const ObjectFilterDropdownSearchInput = () => { const { - filterDefinitionUsedInDropdown, - selectedOperandInDropdown, - objectFilterDropdownSearchInput, + filterDefinitionUsedInDropdownState, + selectedOperandInDropdownState, + objectFilterDropdownSearchInputState, setObjectFilterDropdownSearchInput, } = useFilterDropdown(); + const filterDefinitionUsedInDropdown = useRecoilValue( + filterDefinitionUsedInDropdownState, + ); + const selectedOperandInDropdown = useRecoilValue( + selectedOperandInDropdownState, + ); + const objectFilterDropdownSearchInput = useRecoilValue( + objectFilterDropdownSearchInputState, + ); + return ( filterDefinitionUsedInDropdown && selectedOperandInDropdown && ( diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput.tsx index 999fd6403020..445d080ad699 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/ObjectFilterDropdownTextSearchInput.tsx @@ -1,18 +1,30 @@ import { ChangeEvent } from 'react'; +import { useRecoilValue } from 'recoil'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; import { DropdownMenuSearchInput } from '@/ui/layout/dropdown/components/DropdownMenuSearchInput'; export const ObjectFilterDropdownTextSearchInput = () => { const { - filterDefinitionUsedInDropdown, - selectedOperandInDropdown, - objectFilterDropdownSearchInput, + filterDefinitionUsedInDropdownState, + selectedOperandInDropdownState, + objectFilterDropdownSearchInputState, setObjectFilterDropdownSearchInput, - selectedFilter, + selectedFilterState, selectFilter, } = useFilterDropdown(); + const filterDefinitionUsedInDropdown = useRecoilValue( + filterDefinitionUsedInDropdownState, + ); + const selectedOperandInDropdown = useRecoilValue( + selectedOperandInDropdownState, + ); + const objectFilterDropdownSearchInput = useRecoilValue( + objectFilterDropdownSearchInputState, + ); + const selectedFilter = useRecoilValue(selectedFilterState); + return ( filterDefinitionUsedInDropdown && selectedOperandInDropdown && ( diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx index 8e8d52fe5b95..e6de651105e7 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/components/SingleEntityObjectFilterDropdownButton.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { useTheme } from '@emotion/react'; +import { useRecoilValue } from 'recoil'; import { ObjectFilterDropdownRecordRemoveFilterMenuItem } from '@/object-record/object-filter-dropdown/components/ObjectFilterDropdownRecordRemoveFilterMenuItem'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; @@ -22,12 +23,17 @@ export const SingleEntityObjectFilterDropdownButton = ({ hotkeyScope: HotkeyScope; }) => { const { - availableFilterDefinitions, - selectedFilter, + availableFilterDefinitionsState, + selectedFilterState, setFilterDefinitionUsedInDropdown, setSelectedOperandInDropdown, } = useFilterDropdown(); + const availableFilterDefinitions = useRecoilValue( + availableFilterDefinitionsState, + ); + const selectedFilter = useRecoilValue(selectedFilterState); + const availableFilter = availableFilterDefinitions[0]; React.useEffect(() => { diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/__tests__/useFilterDropdown.test.tsx b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/__tests__/useFilterDropdown.test.tsx index 76316b9dcd8d..3b3cd45a4431 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/__tests__/useFilterDropdown.test.tsx +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/__tests__/useFilterDropdown.test.tsx @@ -1,8 +1,9 @@ import { expect } from '@storybook/test'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; +import { RecoilRoot, useRecoilState } from 'recoil'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { useFilterDropdownStates } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdownStates'; import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; @@ -31,10 +32,15 @@ const mockFilter: Filter = { describe('useFilterDropdown', () => { it('should set availableFilterDefinitions', async () => { - const { result } = renderHook( - () => useFilterDropdown({ filterDropdownId }), - renderHookConfig, - ); + const { result } = renderHook(() => { + useFilterDropdown({ filterDropdownId }); + const { availableFilterDefinitionsState } = + useFilterDropdownStates(filterDropdownId); + + const [availableFilterDefinitions, setAvailableFilterDefinitions] = + useRecoilState(availableFilterDefinitionsState); + return { availableFilterDefinitions, setAvailableFilterDefinitions }; + }, renderHookConfig); expect(result.current.availableFilterDefinitions).toEqual([]); @@ -50,10 +56,14 @@ describe('useFilterDropdown', () => { }); it('should set onFilterSelect', async () => { - const { result } = renderHook( - () => useFilterDropdown({ filterDropdownId }), - renderHookConfig, - ); + const { result } = renderHook(() => { + useFilterDropdown({ filterDropdownId }); + const { onFilterSelectState } = useFilterDropdownStates(filterDropdownId); + + const [onFilterSelect, setOnFilterSelect] = + useRecoilState(onFilterSelectState); + return { onFilterSelect, setOnFilterSelect }; + }, renderHookConfig); expect(result.current.onFilterSelect).toBeUndefined(); @@ -68,10 +78,16 @@ describe('useFilterDropdown', () => { }); it('should set selectedOperandInDropdown', async () => { - const { result } = renderHook( - () => useFilterDropdown({ filterDropdownId }), - renderHookConfig, - ); + const { result } = renderHook(() => { + useFilterDropdown({ filterDropdownId }); + const { selectedOperandInDropdownState } = + useFilterDropdownStates(filterDropdownId); + + const [selectedOperandInDropdown, setSelectedOperandInDropdown] = + useRecoilState(selectedOperandInDropdownState); + return { selectedOperandInDropdown, setSelectedOperandInDropdown }; + }, renderHookConfig); + const mockOperand = ViewFilterOperand.Contains; expect(result.current.selectedOperandInDropdown).toBeNull(); @@ -84,10 +100,14 @@ describe('useFilterDropdown', () => { }); it('should set selectedFilter', async () => { - const { result } = renderHook( - () => useFilterDropdown({ filterDropdownId }), - renderHookConfig, - ); + const { result } = renderHook(() => { + useFilterDropdown({ filterDropdownId }); + const { selectedFilterState } = useFilterDropdownStates(filterDropdownId); + + const [selectedFilter, setSelectedFilter] = + useRecoilState(selectedFilterState); + return { selectedFilter, setSelectedFilter }; + }, renderHookConfig); expect(result.current.selectedFilter).toBeUndefined(); @@ -101,10 +121,20 @@ describe('useFilterDropdown', () => { }); it('should set filterDefinitionUsedInDropdown', async () => { - const { result } = renderHook( - () => useFilterDropdown({ filterDropdownId }), - renderHookConfig, - ); + const { result } = renderHook(() => { + useFilterDropdown({ filterDropdownId }); + const { filterDefinitionUsedInDropdownState } = + useFilterDropdownStates(filterDropdownId); + + const [ + filterDefinitionUsedInDropdown, + setFilterDefinitionUsedInDropdown, + ] = useRecoilState(filterDefinitionUsedInDropdownState); + return { + filterDefinitionUsedInDropdown, + setFilterDefinitionUsedInDropdown, + }; + }, renderHookConfig); expect(result.current.filterDefinitionUsedInDropdown).toBeNull(); @@ -121,10 +151,20 @@ describe('useFilterDropdown', () => { it('should set objectFilterDropdownSearchInput', async () => { const mockResult = 'value'; - const { result } = renderHook( - () => useFilterDropdown({ filterDropdownId }), - renderHookConfig, - ); + const { result } = renderHook(() => { + useFilterDropdown({ filterDropdownId }); + const { objectFilterDropdownSearchInputState } = + useFilterDropdownStates(filterDropdownId); + + const [ + objectFilterDropdownSearchInput, + setObjectFilterDropdownSearchInput, + ] = useRecoilState(objectFilterDropdownSearchInputState); + return { + objectFilterDropdownSearchInput, + setObjectFilterDropdownSearchInput, + }; + }, renderHookConfig); expect(result.current.objectFilterDropdownSearchInput).toBe(''); @@ -139,10 +179,20 @@ describe('useFilterDropdown', () => { it('should set objectFilterDropdownSelectedEntityId', async () => { const mockResult = 'value'; - const { result } = renderHook( - () => useFilterDropdown({ filterDropdownId }), - renderHookConfig, - ); + const { result } = renderHook(() => { + useFilterDropdown({ filterDropdownId }); + const { objectFilterDropdownSelectedEntityIdState } = + useFilterDropdownStates(filterDropdownId); + + const [ + objectFilterDropdownSelectedEntityId, + setObjectFilterDropdownSelectedEntityId, + ] = useRecoilState(objectFilterDropdownSelectedEntityIdState); + return { + objectFilterDropdownSelectedEntityId, + setObjectFilterDropdownSelectedEntityId, + }; + }, renderHookConfig); expect(result.current.objectFilterDropdownSelectedEntityId).toBeNull(); @@ -159,10 +209,20 @@ describe('useFilterDropdown', () => { it('should set objectFilterDropdownSelectedRecordIds', async () => { const mockResult = ['id-0', 'id-1', 'id-2']; - const { result } = renderHook( - () => useFilterDropdown({ filterDropdownId }), - renderHookConfig, - ); + const { result } = renderHook(() => { + useFilterDropdown({ filterDropdownId }); + const { objectFilterDropdownSelectedRecordIdsState } = + useFilterDropdownStates(filterDropdownId); + + const [ + objectFilterDropdownSelectedRecordIds, + setObjectFilterDropdownSelectedRecordIds, + ] = useRecoilState(objectFilterDropdownSelectedRecordIdsState); + return { + objectFilterDropdownSelectedRecordIds, + setObjectFilterDropdownSelectedRecordIds, + }; + }, renderHookConfig); expect(result.current.objectFilterDropdownSelectedRecordIds).toHaveLength( 0, @@ -180,10 +240,20 @@ describe('useFilterDropdown', () => { }); it('should set isObjectFilterDropdownOperandSelectUnfolded', async () => { - const { result } = renderHook( - () => useFilterDropdown({ filterDropdownId }), - renderHookConfig, - ); + const { result } = renderHook(() => { + useFilterDropdown({ filterDropdownId }); + const { isObjectFilterDropdownOperandSelectUnfoldedState } = + useFilterDropdownStates(filterDropdownId); + + const [ + isObjectFilterDropdownOperandSelectUnfolded, + setIsObjectFilterDropdownOperandSelectUnfolded, + ] = useRecoilState(isObjectFilterDropdownOperandSelectUnfoldedState); + return { + isObjectFilterDropdownOperandSelectUnfolded, + setIsObjectFilterDropdownOperandSelectUnfolded, + }; + }, renderHookConfig); expect(result.current.isObjectFilterDropdownOperandSelectUnfolded).toBe( false, @@ -201,10 +271,20 @@ describe('useFilterDropdown', () => { }); it('should set isObjectFilterDropdownUnfolded', async () => { - const { result } = renderHook( - () => useFilterDropdown({ filterDropdownId }), - renderHookConfig, - ); + const { result } = renderHook(() => { + useFilterDropdown({ filterDropdownId }); + const { isObjectFilterDropdownUnfoldedState } = + useFilterDropdownStates(filterDropdownId); + + const [ + isObjectFilterDropdownUnfolded, + setIsObjectFilterDropdownUnfolded, + ] = useRecoilState(isObjectFilterDropdownUnfoldedState); + return { + isObjectFilterDropdownUnfolded, + setIsObjectFilterDropdownUnfolded, + }; + }, renderHookConfig); expect(result.current.isObjectFilterDropdownUnfolded).toBe(false); @@ -218,10 +298,16 @@ describe('useFilterDropdown', () => { }); it('should reset filter', async () => { - const { result } = renderHook( - () => useFilterDropdown({ filterDropdownId }), - renderHookConfig, - ); + const { result } = renderHook(() => { + const { selectFilter, resetFilter } = useFilterDropdown({ + filterDropdownId, + }); + const { selectedFilterState } = useFilterDropdownStates(filterDropdownId); + + const [selectedFilter, setSelectedFilter] = + useRecoilState(selectedFilterState); + return { selectedFilter, setSelectedFilter, selectFilter, resetFilter }; + }, renderHookConfig); act(() => { result.current.selectFilter(mockFilter); @@ -241,10 +327,14 @@ describe('useFilterDropdown', () => { }); it('should call onFilterSelect when a filter option is set', async () => { - const { result } = renderHook( - () => useFilterDropdown({ filterDropdownId }), - renderHookConfig, - ); + const { result } = renderHook(() => { + const { selectFilter } = useFilterDropdown({ filterDropdownId }); + const { onFilterSelectState } = useFilterDropdownStates(filterDropdownId); + + const [onFilterSelect, setOnFilterSelect] = + useRecoilState(onFilterSelectState); + return { onFilterSelect, setOnFilterSelect, selectFilter }; + }, renderHookConfig); const onFilterSelectMock = jest.fn(); expect(result.current.onFilterSelect).toBeUndefined(); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts index 90d6f77f1e35..945d19c68954 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdown.ts @@ -1,7 +1,8 @@ -import { useCallback } from 'react'; +import { useRecoilCallback, useSetRecoilState } from 'recoil'; import { useFilterDropdownStates } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdownStates'; import { useAvailableScopeIdOrThrow } from '@/ui/utilities/recoil-scope/scopes-internal/hooks/useAvailableScopeId'; +import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; import { ObjectFilterDropdownScopeInternalContext } from '../scopes/scope-internal-context/ObjectFilterDropdownScopeInternalContext'; import { Filter } from '../types/Filter'; @@ -17,92 +18,122 @@ export const useFilterDropdown = (props?: UseFilterDropdownProps) => { ); const { - availableFilterDefinitions, - setAvailableFilterDefinitions, - filterDefinitionUsedInDropdown, - setFilterDefinitionUsedInDropdown, - objectFilterDropdownSearchInput, - setObjectFilterDropdownSearchInput, - objectFilterDropdownSelectedEntityId, - setObjectFilterDropdownSelectedEntityId, - objectFilterDropdownSelectedRecordIds, - setObjectFilterDropdownSelectedRecordIds, - objectFilterDropdownSelectedOptionValues, - setObjectFilterDropdownSelectedOptionValues, - isObjectFilterDropdownOperandSelectUnfolded, - setIsObjectFilterDropdownOperandSelectUnfolded, - isObjectFilterDropdownUnfolded, - setIsObjectFilterDropdownUnfolded, - selectedFilter, - setSelectedFilter, - selectedOperandInDropdown, - setSelectedOperandInDropdown, - onFilterSelect, - setOnFilterSelect, + availableFilterDefinitionsState, + filterDefinitionUsedInDropdownState, + objectFilterDropdownSearchInputState, + objectFilterDropdownSelectedEntityIdState, + objectFilterDropdownSelectedRecordIdsState, + objectFilterDropdownSelectedOptionValuesState, + isObjectFilterDropdownOperandSelectUnfoldedState, + isObjectFilterDropdownUnfoldedState, + selectedFilterState, + selectedOperandInDropdownState, + onFilterSelectState, } = useFilterDropdownStates(scopeId); - const selectFilter = useCallback( - (filter: Filter | null) => { - setSelectedFilter(filter); - onFilterSelect?.(filter); - }, - [setSelectedFilter, onFilterSelect], + const selectFilter = useRecoilCallback( + ({ set, snapshot }) => + (filter: Filter | null) => { + set(selectedFilterState, filter); + const onFilterSelect = getSnapshotValue(snapshot, onFilterSelectState); + + onFilterSelect?.(filter); + }, + [selectedFilterState, onFilterSelectState], ); - const emptyFilterButKeepDefinition = useCallback(() => { - setObjectFilterDropdownSearchInput(''); - setObjectFilterDropdownSelectedEntityId(null); - setObjectFilterDropdownSelectedRecordIds([]); - setSelectedFilter(undefined); - }, [ - setSelectedFilter, - setObjectFilterDropdownSelectedRecordIds, - setObjectFilterDropdownSelectedEntityId, - setObjectFilterDropdownSearchInput, - ]); + const emptyFilterButKeepDefinition = useRecoilCallback( + ({ set }) => + () => { + set(objectFilterDropdownSearchInputState, ''); + set(objectFilterDropdownSelectedEntityIdState, null); + set(objectFilterDropdownSelectedRecordIdsState, []); + set(selectedFilterState, undefined); + }, + [ + objectFilterDropdownSearchInputState, + objectFilterDropdownSelectedEntityIdState, + objectFilterDropdownSelectedRecordIdsState, + selectedFilterState, + ], + ); - const resetFilter = useCallback(() => { - setObjectFilterDropdownSearchInput(''); - setObjectFilterDropdownSelectedEntityId(null); - setObjectFilterDropdownSelectedRecordIds([]); - setSelectedFilter(undefined); - setFilterDefinitionUsedInDropdown(null); - setSelectedOperandInDropdown(null); - }, [ - setFilterDefinitionUsedInDropdown, - setObjectFilterDropdownSearchInput, - setObjectFilterDropdownSelectedEntityId, - setObjectFilterDropdownSelectedRecordIds, - setSelectedFilter, - setSelectedOperandInDropdown, - ]); + const resetFilter = useRecoilCallback( + ({ set }) => + () => { + set(objectFilterDropdownSearchInputState, ''); + set(objectFilterDropdownSelectedEntityIdState, null); + set(objectFilterDropdownSelectedRecordIdsState, []); + set(selectedFilterState, undefined); + set(filterDefinitionUsedInDropdownState, null); + set(selectedOperandInDropdownState, null); + }, + [ + filterDefinitionUsedInDropdownState, + objectFilterDropdownSearchInputState, + objectFilterDropdownSelectedEntityIdState, + objectFilterDropdownSelectedRecordIdsState, + selectedFilterState, + selectedOperandInDropdownState, + ], + ); + + const setAvailableFilterDefinitions = useSetRecoilState( + availableFilterDefinitionsState, + ); + const setSelectedFilter = useSetRecoilState(selectedFilterState); + const setSelectedOperandInDropdown = useSetRecoilState( + selectedOperandInDropdownState, + ); + const setFilterDefinitionUsedInDropdown = useSetRecoilState( + filterDefinitionUsedInDropdownState, + ); + const setObjectFilterDropdownSearchInput = useSetRecoilState( + objectFilterDropdownSearchInputState, + ); + const setObjectFilterDropdownSelectedEntityId = useSetRecoilState( + objectFilterDropdownSelectedEntityIdState, + ); + const setObjectFilterDropdownSelectedRecordIds = useSetRecoilState( + objectFilterDropdownSelectedRecordIdsState, + ); + const setObjectFilterDropdownSelectedOptionValues = useSetRecoilState( + objectFilterDropdownSelectedOptionValuesState, + ); + const setIsObjectFilterDropdownOperandSelectUnfolded = useSetRecoilState( + isObjectFilterDropdownOperandSelectUnfoldedState, + ); + const setIsObjectFilterDropdownUnfolded = useSetRecoilState( + isObjectFilterDropdownUnfoldedState, + ); + const setOnFilterSelect = useSetRecoilState(onFilterSelectState); return { scopeId, - availableFilterDefinitions, + selectFilter, + resetFilter, + setSelectedFilter, + setSelectedOperandInDropdown, setAvailableFilterDefinitions, - filterDefinitionUsedInDropdown, setFilterDefinitionUsedInDropdown, - objectFilterDropdownSearchInput, setObjectFilterDropdownSearchInput, - objectFilterDropdownSelectedEntityId, setObjectFilterDropdownSelectedEntityId, - objectFilterDropdownSelectedRecordIds, setObjectFilterDropdownSelectedRecordIds, - objectFilterDropdownSelectedOptionValues, setObjectFilterDropdownSelectedOptionValues, - isObjectFilterDropdownOperandSelectUnfolded, setIsObjectFilterDropdownOperandSelectUnfolded, - isObjectFilterDropdownUnfolded, setIsObjectFilterDropdownUnfolded, - selectedFilter, - setSelectedFilter, - selectedOperandInDropdown, - setSelectedOperandInDropdown, - selectFilter, - resetFilter, - onFilterSelect, setOnFilterSelect, emptyFilterButKeepDefinition, + availableFilterDefinitionsState, + filterDefinitionUsedInDropdownState, + objectFilterDropdownSearchInputState, + objectFilterDropdownSelectedEntityIdState, + objectFilterDropdownSelectedRecordIdsState, + objectFilterDropdownSelectedOptionValuesState, + isObjectFilterDropdownOperandSelectUnfoldedState, + isObjectFilterDropdownUnfoldedState, + selectedFilterState, + selectedOperandInDropdownState, + onFilterSelectState, }; }; diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownStates.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownStates.ts index 94b969b84a34..47a4835c1d18 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownStates.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/hooks/useFilterDropdownStates.ts @@ -1,97 +1,84 @@ -import { objectFilterDropdownSelectedRecordIdsScopedState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsScopedState'; -import { onFilterSelectScopedState } from '@/object-record/object-filter-dropdown/states/onFilterSelectScopedState'; -import { useRecoilScopedStateV2 } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedStateV2'; - -import { availableFilterDefinitionsScopedState } from '../states/availableFilterDefinitionsScopedState'; -import { filterDefinitionUsedInDropdownScopedState } from '../states/filterDefinitionUsedInDropdownScopedState'; -import { isObjectFilterDropdownOperandSelectUnfoldedScopedState } from '../states/isObjectFilterDropdownOperandSelectUnfoldedScopedState'; -import { isObjectFilterDropdownUnfoldedScopedState } from '../states/isObjectFilterDropdownUnfoldedScopedState'; -import { objectFilterDropdownSearchInputScopedState } from '../states/objectFilterDropdownSearchInputScopedState'; -import { objectFilterDropdownSelectedEntityIdScopedState } from '../states/objectFilterDropdownSelectedEntityIdScopedState'; -import { objectFilterDropdownSelectedOptionValuesScopedState } from '../states/objectFilterDropdownSelectedOptionValuesScopedState'; -import { selectedFilterScopedState } from '../states/selectedFilterScopedState'; -import { selectedOperandInDropdownScopedState } from '../states/selectedOperandInDropdownScopedState'; +import { filterDefinitionUsedInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState'; +import { isObjectFilterDropdownOperandSelectUnfoldedComponentState } from '@/object-record/object-filter-dropdown/states/isObjectFilterDropdownOperandSelectUnfoldedComponentState'; +import { isObjectFilterDropdownUnfoldedComponentState } from '@/object-record/object-filter-dropdown/states/isObjectFilterDropdownUnfoldedComponentState'; +import { objectFilterDropdownSearchInputComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState'; +import { objectFilterDropdownSelectedEntityIdComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedEntityIdComponentState'; +import { objectFilterDropdownSelectedOptionValuesComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState'; +import { objectFilterDropdownSelectedRecordIdsComponentState } from '@/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState'; +import { onFilterSelectComponentState } from '@/object-record/object-filter-dropdown/states/onFilterSelectComponentState'; +import { selectedFilterComponentState } from '@/object-record/object-filter-dropdown/states/selectedFilterComponentState'; +import { selectedOperandInDropdownComponentState } from '@/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState'; +import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; +import { availableFilterDefinitionsComponentState } from '@/views/states/availableFilterDefinitionsComponentState'; export const useFilterDropdownStates = (scopeId: string) => { - const [availableFilterDefinitions, setAvailableFilterDefinitions] = - useRecoilScopedStateV2(availableFilterDefinitionsScopedState, scopeId); - - const [filterDefinitionUsedInDropdown, setFilterDefinitionUsedInDropdown] = - useRecoilScopedStateV2(filterDefinitionUsedInDropdownScopedState, scopeId); + const availableFilterDefinitionsState = extractComponentState( + availableFilterDefinitionsComponentState, + scopeId, + ); - const [objectFilterDropdownSearchInput, setObjectFilterDropdownSearchInput] = - useRecoilScopedStateV2(objectFilterDropdownSearchInputScopedState, scopeId); + const filterDefinitionUsedInDropdownState = extractComponentState( + filterDefinitionUsedInDropdownComponentState, + scopeId, + ); - const [ - objectFilterDropdownSelectedEntityId, - setObjectFilterDropdownSelectedEntityId, - ] = useRecoilScopedStateV2( - objectFilterDropdownSelectedEntityIdScopedState, + const objectFilterDropdownSearchInputState = extractComponentState( + objectFilterDropdownSearchInputComponentState, scopeId, ); - const [ - objectFilterDropdownSelectedRecordIds, - setObjectFilterDropdownSelectedRecordIds, - ] = useRecoilScopedStateV2( - objectFilterDropdownSelectedRecordIdsScopedState, + const objectFilterDropdownSelectedEntityIdState = extractComponentState( + objectFilterDropdownSelectedEntityIdComponentState, scopeId, ); - const [ - objectFilterDropdownSelectedOptionValues, - setObjectFilterDropdownSelectedOptionValues, - ] = useRecoilScopedStateV2( - objectFilterDropdownSelectedOptionValuesScopedState, + const objectFilterDropdownSelectedRecordIdsState = extractComponentState( + objectFilterDropdownSelectedRecordIdsComponentState, scopeId, ); - const [ - isObjectFilterDropdownOperandSelectUnfolded, - setIsObjectFilterDropdownOperandSelectUnfolded, - ] = useRecoilScopedStateV2( - isObjectFilterDropdownOperandSelectUnfoldedScopedState, + const objectFilterDropdownSelectedOptionValuesState = extractComponentState( + objectFilterDropdownSelectedOptionValuesComponentState, scopeId, ); - const [isObjectFilterDropdownUnfolded, setIsObjectFilterDropdownUnfolded] = - useRecoilScopedStateV2(isObjectFilterDropdownUnfoldedScopedState, scopeId); + const isObjectFilterDropdownOperandSelectUnfoldedState = + extractComponentState( + isObjectFilterDropdownOperandSelectUnfoldedComponentState, + scopeId, + ); - const [selectedFilter, setSelectedFilter] = useRecoilScopedStateV2( - selectedFilterScopedState, + const isObjectFilterDropdownUnfoldedState = extractComponentState( + isObjectFilterDropdownUnfoldedComponentState, scopeId, ); - const [selectedOperandInDropdown, setSelectedOperandInDropdown] = - useRecoilScopedStateV2(selectedOperandInDropdownScopedState, scopeId); + const selectedFilterState = extractComponentState( + selectedFilterComponentState, + scopeId, + ); + + const selectedOperandInDropdownState = extractComponentState( + selectedOperandInDropdownComponentState, + scopeId, + ); - const [onFilterSelect, setOnFilterSelect] = useRecoilScopedStateV2( - onFilterSelectScopedState, + const onFilterSelectState = extractComponentState( + onFilterSelectComponentState, scopeId, ); return { - availableFilterDefinitions, - setAvailableFilterDefinitions, - filterDefinitionUsedInDropdown, - setFilterDefinitionUsedInDropdown, - objectFilterDropdownSearchInput, - setObjectFilterDropdownSearchInput, - objectFilterDropdownSelectedEntityId, - setObjectFilterDropdownSelectedEntityId, - objectFilterDropdownSelectedRecordIds, - objectFilterDropdownSelectedOptionValues, - setObjectFilterDropdownSelectedOptionValues, - setObjectFilterDropdownSelectedRecordIds, - isObjectFilterDropdownOperandSelectUnfolded, - setIsObjectFilterDropdownOperandSelectUnfolded, - isObjectFilterDropdownUnfolded, - setIsObjectFilterDropdownUnfolded, - selectedFilter, - setSelectedFilter, - selectedOperandInDropdown, - setSelectedOperandInDropdown, - onFilterSelect, - setOnFilterSelect, + availableFilterDefinitionsState, + filterDefinitionUsedInDropdownState, + objectFilterDropdownSearchInputState, + objectFilterDropdownSelectedEntityIdState, + objectFilterDropdownSelectedRecordIdsState, + objectFilterDropdownSelectedOptionValuesState, + isObjectFilterDropdownOperandSelectUnfoldedState, + isObjectFilterDropdownUnfoldedState, + selectedFilterState, + selectedOperandInDropdownState, + onFilterSelectState, }; }; diff --git a/packages/twenty-front/src/modules/views/states/availableFilterDefinitionsScopedState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/availableFilterDefinitionsComponentState.ts similarity index 66% rename from packages/twenty-front/src/modules/views/states/availableFilterDefinitionsScopedState.ts rename to packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/availableFilterDefinitionsComponentState.ts index 8433868e1b12..9a3f649ea491 100644 --- a/packages/twenty-front/src/modules/views/states/availableFilterDefinitionsScopedState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/availableFilterDefinitionsComponentState.ts @@ -1,9 +1,9 @@ import { FilterDefinition } from '@/object-record/object-filter-dropdown/types/FilterDefinition'; import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; -export const availableFilterDefinitionsScopedState = createComponentState< +export const availableFilterDefinitionsComponentState = createComponentState< FilterDefinition[] >({ - key: 'availableFilterDefinitionsScopedState', + key: 'availableFilterDefinitionsComponentState', defaultValue: [], }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownScopedState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState.ts similarity index 67% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownScopedState.ts rename to packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState.ts index 874d06a63059..c7fd89f92902 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownScopedState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/filterDefinitionUsedInDropdownComponentState.ts @@ -2,8 +2,8 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils import { FilterDefinition } from '../types/FilterDefinition'; -export const filterDefinitionUsedInDropdownScopedState = +export const filterDefinitionUsedInDropdownComponentState = createComponentState({ - key: 'filterDefinitionUsedInDropdownScopedState', + key: 'filterDefinitionUsedInDropdownComponentState', defaultValue: null, }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/isObjectFilterDropdownOperandSelectUnfoldedComponentState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/isObjectFilterDropdownOperandSelectUnfoldedComponentState.ts new file mode 100644 index 000000000000..389c6d30f928 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/isObjectFilterDropdownOperandSelectUnfoldedComponentState.ts @@ -0,0 +1,7 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +export const isObjectFilterDropdownOperandSelectUnfoldedComponentState = + createComponentState({ + key: 'isObjectFilterDropdownOperandSelectUnfoldedComponentState', + defaultValue: false, + }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/isObjectFilterDropdownUnfoldedScopedState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/isObjectFilterDropdownUnfoldedComponentState.ts similarity index 78% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/isObjectFilterDropdownUnfoldedScopedState.ts rename to packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/isObjectFilterDropdownUnfoldedComponentState.ts index e07f31ca1b9b..cfd6dea10c8f 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/isObjectFilterDropdownUnfoldedScopedState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/isObjectFilterDropdownUnfoldedComponentState.ts @@ -1,6 +1,6 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; -export const isObjectFilterDropdownUnfoldedScopedState = +export const isObjectFilterDropdownUnfoldedComponentState = createComponentState({ key: 'isObjectFilterDropdownUnfoldedScopedState', defaultValue: false, diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState.ts new file mode 100644 index 000000000000..8addf2fb6cc6 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSearchInputComponentState.ts @@ -0,0 +1,7 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +export const objectFilterDropdownSearchInputComponentState = + createComponentState({ + key: 'objectFilterDropdownSearchInputComponentState', + defaultValue: '', + }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedEntityIdScopedState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedEntityIdComponentState.ts similarity index 57% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedEntityIdScopedState.ts rename to packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedEntityIdComponentState.ts index 9eba05a6b796..3f62c3d65b1b 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedEntityIdScopedState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedEntityIdComponentState.ts @@ -1,7 +1,7 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; -export const objectFilterDropdownSelectedEntityIdScopedState = +export const objectFilterDropdownSelectedEntityIdComponentState = createComponentState({ - key: 'objectFilterDropdownSelectedEntityIdScopedState', + key: 'objectFilterDropdownSelectedEntityIdComponentState', defaultValue: null, }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsScopedState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState.ts similarity index 55% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsScopedState.ts rename to packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState.ts index e5e1b396c0d2..feef87220c1e 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsScopedState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesComponentState.ts @@ -1,7 +1,7 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; -export const objectFilterDropdownSelectedRecordIdsScopedState = +export const objectFilterDropdownSelectedOptionValuesComponentState = createComponentState({ - key: 'objectFilterDropdownSelectedRecordIdsScopedState', + key: 'objectFilterDropdownSelectedOptionValuesComponentState', defaultValue: [], }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesScopedState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState.ts similarity index 57% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesScopedState.ts rename to packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState.ts index db1124959fc7..de23b3b0daa5 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedOptionValuesScopedState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/objectFilterDropdownSelectedRecordIdsComponentState.ts @@ -1,7 +1,7 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; -export const objectFilterDropdownSelectedOptionValuesScopedState = +export const objectFilterDropdownSelectedRecordIdsComponentState = createComponentState({ - key: 'objectFilterDropdownSelectedOptionValuesScopedState', + key: 'objectFilterDropdownSelectedRecordIdsComponentState', defaultValue: [], }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/onFilterSelectScopedState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/onFilterSelectComponentState.ts similarity index 68% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/onFilterSelectScopedState.ts rename to packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/onFilterSelectComponentState.ts index 43a90fa7aa4c..b73c2cbe962f 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/onFilterSelectScopedState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/onFilterSelectComponentState.ts @@ -2,9 +2,9 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils import { Filter } from '../types/Filter'; -export const onFilterSelectScopedState = createComponentState< +export const onFilterSelectComponentState = createComponentState< ((filter: Filter | null) => void) | undefined >({ - key: 'onFilterSelectScopedState', + key: 'onFilterSelectComponentState', defaultValue: undefined, }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedFilterScopedState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedFilterComponentState.ts similarity index 66% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedFilterScopedState.ts rename to packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedFilterComponentState.ts index c9df2abcea31..7b4183c74841 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedFilterScopedState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedFilterComponentState.ts @@ -2,9 +2,9 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils import { Filter } from '../types/Filter'; -export const selectedFilterScopedState = createComponentState< +export const selectedFilterComponentState = createComponentState< Filter | undefined | null >({ - key: 'selectedFilterScopedState', + key: 'selectedFilterComponentState', defaultValue: undefined, }); diff --git a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedOperandInDropdownScopedState.ts b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState.ts similarity index 70% rename from packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedOperandInDropdownScopedState.ts rename to packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState.ts index 087ac6d07f49..eef8fe552a96 100644 --- a/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedOperandInDropdownScopedState.ts +++ b/packages/twenty-front/src/modules/object-record/object-filter-dropdown/states/selectedOperandInDropdownComponentState.ts @@ -1,8 +1,8 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; import { ViewFilterOperand } from '@/views/types/ViewFilterOperand'; -export const selectedOperandInDropdownScopedState = +export const selectedOperandInDropdownComponentState = createComponentState({ - key: 'selectedOperandInDropdownScopedState', + key: 'selectedOperandInDropdownComponentState', defaultValue: null, }); diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx index 4487463fa5e1..2b3fb2541c95 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/components/ObjectSortDropdownButton.tsx @@ -1,4 +1,5 @@ import { useCallback, useState } from 'react'; +import { useRecoilValue } from 'recoil'; import { OBJECT_SORT_DROPDOWN_ID } from '@/object-record/object-sort-dropdown/constants/ObjectSortDropdownId'; import { useSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useSortDropdown'; @@ -37,7 +38,7 @@ export const ObjectSortDropdownButton = ({ setSelectedSortDirection('asc'); }, []); - const { isSortSelected } = useSortDropdown({ + const { isSortSelectedState } = useSortDropdown({ sortDropdownId: sortDropdownId, }); @@ -48,10 +49,16 @@ export const ObjectSortDropdownButton = ({ resetState(); }; - const { availableSortDefinitions, onSortSelect } = useSortDropdown({ + const { availableSortDefinitionsState, onSortSelectState } = useSortDropdown({ sortDropdownId: sortDropdownId, }); + const isSortSelected = useRecoilValue(isSortSelectedState); + const availableSortDefinitions = useRecoilValue( + availableSortDefinitionsState, + ); + const onSortSelect = useRecoilValue(onSortSelectState); + const handleAddSort = (selectedSortDefinition: SortDefinition) => { toggleDropdown(); onSortSelect?.({ diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/__tests__/useSortDropdown.test.tsx b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/__tests__/useSortDropdown.test.tsx index 108055202573..6bf6ddf0d53e 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/__tests__/useSortDropdown.test.tsx +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/__tests__/useSortDropdown.test.tsx @@ -1,7 +1,9 @@ +import { expect } from '@storybook/test'; import { act, renderHook, waitFor } from '@testing-library/react'; -import { RecoilRoot } from 'recoil'; +import { RecoilRoot, useRecoilState } from 'recoil'; import { useSortDropdown } from '@/object-record/object-sort-dropdown/hooks/useSortDropdown'; +import { useSortDropdownStates } from '@/object-record/object-sort-dropdown/hooks/useSortDropdownStates'; import { Sort } from '@/object-record/object-sort-dropdown/types/Sort'; import { SortDefinition } from '@/object-record/object-sort-dropdown/types/SortDefinition'; @@ -20,10 +22,19 @@ const sortDefinitions: SortDefinition[] = [ describe('useSortDropdown', () => { it('should set availableSortDefinitions', async () => { - const { result } = renderHook( - () => useSortDropdown({ sortDropdownId }), - renderHookConfig, - ); + const { result } = renderHook(() => { + useSortDropdown({ sortDropdownId }); + const { availableSortDefinitionsState } = + useSortDropdownStates(sortDropdownId); + + const [availableSortDefinitions, setAvailableSortDefinitions] = + useRecoilState(availableSortDefinitionsState); + + return { + availableSortDefinitions, + setAvailableSortDefinitions, + }; + }, renderHookConfig); expect(result.current.availableSortDefinitions).toEqual([]); act(() => { result.current.setAvailableSortDefinitions(sortDefinitions); @@ -35,10 +46,18 @@ describe('useSortDropdown', () => { }); it('should set isSortSelected', async () => { - const { result } = renderHook( - () => useSortDropdown({ sortDropdownId }), - renderHookConfig, - ); + const { result } = renderHook(() => { + useSortDropdown({ sortDropdownId }); + const { isSortSelectedState } = useSortDropdownStates(sortDropdownId); + + const [isSortSelected, setIsSortSelected] = + useRecoilState(isSortSelectedState); + + return { + isSortSelected, + setIsSortSelected, + }; + }, renderHookConfig); expect(result.current.isSortSelected).toBe(false); @@ -54,10 +73,17 @@ describe('useSortDropdown', () => { it('should set onSortSelect', async () => { const OnSortSelectFunction = () => {}; const mockOnSortSelect = jest.fn(() => OnSortSelectFunction); - const { result } = renderHook( - () => useSortDropdown({ sortDropdownId }), - renderHookConfig, - ); + const { result } = renderHook(() => { + useSortDropdown({ sortDropdownId }); + const { onSortSelectState } = useSortDropdownStates(sortDropdownId); + + const [onSortSelect, setOnSortSelect] = useRecoilState(onSortSelectState); + + return { + onSortSelect, + setOnSortSelect, + }; + }, renderHookConfig); expect(result.current.onSortSelect).toBeUndefined(); @@ -78,10 +104,17 @@ describe('useSortDropdown', () => { definition: sortDefinitions[0], }; - const { result } = renderHook( - () => useSortDropdown({ sortDropdownId }), - renderHookConfig, - ); + const { result } = renderHook(() => { + useSortDropdown({ sortDropdownId }); + const { onSortSelectState } = useSortDropdownStates(sortDropdownId); + + const [onSortSelect, setOnSortSelect] = useRecoilState(onSortSelectState); + + return { + onSortSelect, + setOnSortSelect, + }; + }, renderHookConfig); act(() => { result.current.setOnSortSelect(mockOnSortSelect); diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/useSortDropdown.ts b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/useSortDropdown.ts index 696dae8cd631..aee777aa1217 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/useSortDropdown.ts +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/useSortDropdown.ts @@ -13,21 +13,15 @@ export const useSortDropdown = (props?: UseSortProps) => { props?.sortDropdownId, ); const { - availableSortDefinitions, - setAvailableSortDefinitions, - isSortSelected, - setIsSortSelected, - onSortSelect, - setOnSortSelect, + availableSortDefinitionsState, + isSortSelectedState, + onSortSelectState, } = useSortDropdownStates(scopeId); return { scopeId, - availableSortDefinitions, - isSortSelected, - setIsSortSelected, - setAvailableSortDefinitions, - onSortSelect, - setOnSortSelect, + availableSortDefinitionsState, + isSortSelectedState, + onSortSelectState, }; }; diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/useSortDropdownStates.ts b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/useSortDropdownStates.ts index 9453508fee46..74c6bdb5481a 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/useSortDropdownStates.ts +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/hooks/useSortDropdownStates.ts @@ -1,29 +1,27 @@ -import { onSortSelectScopedState } from '@/object-record/object-sort-dropdown/states/onSortSelectScopedState'; -import { useRecoilScopedStateV2 } from '@/ui/utilities/recoil-scope/hooks/useRecoilScopedStateV2'; - -import { availableSortDefinitionsScopedState } from '../states/availableSortDefinitionsScopedState'; -import { isSortSelectedScopedState } from '../states/isSortSelectedScopedState'; +import { isSortSelectedComponentState } from '@/object-record/object-sort-dropdown/states/isSortSelectedScopedState'; +import { onSortSelectComponentState } from '@/object-record/object-sort-dropdown/states/onSortSelectScopedState'; +import { extractComponentState } from '@/ui/utilities/state/component-state/utils/extractComponentState'; +import { availableSortDefinitionsComponentState } from '@/views/states/availableSortDefinitionsComponentState'; export const useSortDropdownStates = (scopeId: string) => { - const [availableSortDefinitions, setAvailableSortDefinitions] = - useRecoilScopedStateV2(availableSortDefinitionsScopedState, scopeId); + const availableSortDefinitionsState = extractComponentState( + availableSortDefinitionsComponentState, + scopeId, + ); - const [isSortSelected, setIsSortSelected] = useRecoilScopedStateV2( - isSortSelectedScopedState, + const isSortSelectedState = extractComponentState( + isSortSelectedComponentState, scopeId, ); - const [onSortSelect, setOnSortSelect] = useRecoilScopedStateV2( - onSortSelectScopedState, + const onSortSelectState = extractComponentState( + onSortSelectComponentState, scopeId, ); return { - availableSortDefinitions, - setAvailableSortDefinitions, - isSortSelected, - setIsSortSelected, - onSortSelect, - setOnSortSelect, + availableSortDefinitionsState, + isSortSelectedState, + onSortSelectState, }; }; diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/availableSortDefinitionsScopedState.ts b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/availableSortDefinitionsComponentState.ts similarity index 62% rename from packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/availableSortDefinitionsScopedState.ts rename to packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/availableSortDefinitionsComponentState.ts index 3f8165b63d59..db559fa52f6b 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/availableSortDefinitionsScopedState.ts +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/availableSortDefinitionsComponentState.ts @@ -2,9 +2,9 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils import { SortDefinition } from '../types/SortDefinition'; -export const availableSortDefinitionsScopedState = createComponentState< +export const availableSortDefinitionsComponentState = createComponentState< SortDefinition[] >({ - key: 'availableSortDefinitionsScopedState', + key: 'availableSortDefinitionsComponentState', defaultValue: [], }); diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/isSortSelectedScopedState.ts b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/isSortSelectedScopedState.ts index 071d662741ce..180a2f017221 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/isSortSelectedScopedState.ts +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/isSortSelectedScopedState.ts @@ -1,6 +1,6 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; -export const isSortSelectedScopedState = createComponentState({ - key: 'isSortSelectedScopedState', +export const isSortSelectedComponentState = createComponentState({ + key: 'isSortSelectedComponentState', defaultValue: false, }); diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/onSortSelectScopedState.ts b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/onSortSelectScopedState.ts index 9e0661722961..8b6ca0a8cb35 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/onSortSelectScopedState.ts +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/states/onSortSelectScopedState.ts @@ -2,9 +2,9 @@ import { createComponentState } from '@/ui/utilities/state/component-state/utils import { Sort } from '../types/Sort'; -export const onSortSelectScopedState = createComponentState< +export const onSortSelectComponentState = createComponentState< ((sort: Sort) => void) | undefined >({ - key: 'onSortSelectScopedState', + key: 'onSortSelectComponentState', defaultValue: undefined, }); diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.tsx b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.tsx index b46ee87ac717..5a9dc2de62a5 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.tsx +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/__tests__/turnSortsIntoOrderBy.test.tsx @@ -55,7 +55,7 @@ describe('turnSortsIntoOrderBy', () => { }); }); - it('should throw error if field not found', () => { + it('should ignore if field not found', () => { const sorts: Sort[] = [ { fieldMetadataId: 'invalidField', @@ -63,8 +63,8 @@ describe('turnSortsIntoOrderBy', () => { definition: sortDefinition, }, ]; - expect(() => turnSortsIntoOrderBy(sorts, [])).toThrow( - 'Could not find field invalidField in metadata object', - ); + expect(turnSortsIntoOrderBy(sorts, [])).toEqual({ + position: 'AscNullsFirst', + }); }); }); diff --git a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts index b2c487f6ec92..85452ddfcf65 100644 --- a/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts +++ b/packages/twenty-front/src/modules/object-record/object-sort-dropdown/utils/turnSortsIntoOrderBy.ts @@ -2,6 +2,7 @@ import { OrderBy } from '@/object-metadata/types/OrderBy'; import { OrderByField } from '@/object-metadata/types/OrderByField'; import { Field } from '~/generated/graphql'; import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; +import { isDefined } from '~/utils/isDefined'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; import { Sort } from '../types/Sort'; @@ -12,20 +13,20 @@ export const turnSortsIntoOrderBy = ( ): OrderByField => { const fieldsById = mapArrayToObject(fields, ({ id }) => id); const sortsOrderBy = Object.fromEntries( - sorts.map((sort) => { - const correspondingField = fieldsById[sort.fieldMetadataId]; + sorts + .map((sort) => { + const correspondingField = fieldsById[sort.fieldMetadataId]; - if (isUndefinedOrNull(correspondingField)) { - throw new Error( - `Could not find field ${sort.fieldMetadataId} in metadata object`, - ); - } + if (isUndefinedOrNull(correspondingField)) { + return undefined; + } - const direction: OrderBy = - sort.direction === 'asc' ? 'AscNullsFirst' : 'DescNullsLast'; + const direction: OrderBy = + sort.direction === 'asc' ? 'AscNullsFirst' : 'DescNullsLast'; - return [correspondingField.name, direction]; - }), + return [correspondingField.name, direction]; + }) + .filter(isDefined), ); return { diff --git a/packages/twenty-front/src/modules/object-record/query-keys/types/QueryKey.ts b/packages/twenty-front/src/modules/object-record/query-keys/types/QueryKey.ts new file mode 100644 index 000000000000..fc1c16a14e99 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/query-keys/types/QueryKey.ts @@ -0,0 +1,7 @@ +import { ObjectRecordQueryVariables } from '@/object-record/types/ObjectRecordQueryVariables'; + +export type QueryKey = { + objectNameSingular: string; + variables: ObjectRecordQueryVariables; + depth: number; +}; diff --git a/packages/twenty-front/src/modules/object-record/record-action-bar/hooks/useRecordActionBar.tsx b/packages/twenty-front/src/modules/object-record/record-action-bar/hooks/useRecordActionBar.tsx index 98ae1ee541da..090647575d5a 100644 --- a/packages/twenty-front/src/modules/object-record/record-action-bar/hooks/useRecordActionBar.tsx +++ b/packages/twenty-front/src/modules/object-record/record-action-bar/hooks/useRecordActionBar.tsx @@ -34,8 +34,8 @@ export const useRecordActionBar = ({ selectedRecordIds, callback, }: useRecordActionBarProps) => { - const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState()); - const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState()); + const setContextMenuEntries = useSetRecoilState(contextMenuEntriesState); + const setActionBarEntriesState = useSetRecoilState(actionBarEntriesState); const { createFavorite, favorites, deleteFavorite } = useFavorites(); @@ -106,7 +106,7 @@ export const useRecordActionBar = ({ const baseActions: ContextMenuEntry[] = useMemo( () => [ { - label: 'Delete', + label: `Delete (${selectedRecordIds.length})`, Icon: IconTrash, accent: 'danger', onClick: () => handleDeleteClick(), @@ -118,7 +118,7 @@ export const useRecordActionBar = ({ onClick: () => download(), }, ], - [handleDeleteClick, download, progress], + [handleDeleteClick, download, progress, selectedRecordIds], ); const dataExecuteQuickActionOnmentEnabled = useIsFeatureEnabled( diff --git a/packages/twenty-front/src/modules/object-record/record-board/action-bar/components/RecordBoardActionBar.tsx b/packages/twenty-front/src/modules/object-record/record-board/action-bar/components/RecordBoardActionBar.tsx index d0d810222011..2bb43e96ceee 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/action-bar/components/RecordBoardActionBar.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/action-bar/components/RecordBoardActionBar.tsx @@ -10,9 +10,9 @@ type RecordBoardActionBarProps = { export const RecordBoardActionBar = ({ recordBoardId, }: RecordBoardActionBarProps) => { - const { getSelectedRecordIdsSelector } = useRecordBoardStates(recordBoardId); + const { selectedRecordIdsSelector } = useRecordBoardStates(recordBoardId); - const selectedRecordIds = useRecoilValue(getSelectedRecordIdsSelector()); + const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector()); if (!selectedRecordIds.length) { return null; diff --git a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx index f8aa66909a56..c877865d9810 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/components/RecordBoard.tsx @@ -50,12 +50,12 @@ export const RecordBoard = ({ recordBoardId }: RecordBoardProps) => { const boardRef = useRef(null); const { - getColumnIdsState, + columnIdsState, columnsFamilySelector, recordIdsByColumnIdFamilyState, } = useRecordBoardStates(recordBoardId); - const columnIds = useRecoilValue(getColumnIdsState()); + const columnIds = useRecoilValue(columnIdsState); const { resetRecordSelection, setRecordAsSelected } = useRecordBoardSelection(recordBoardId); diff --git a/packages/twenty-front/src/modules/object-record/record-board/context-menu/components/RecordBoardContextMenu.tsx b/packages/twenty-front/src/modules/object-record/record-board/context-menu/components/RecordBoardContextMenu.tsx index b98245d90165..fed35a6506d3 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/context-menu/components/RecordBoardContextMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/context-menu/components/RecordBoardContextMenu.tsx @@ -10,9 +10,9 @@ type RecordBoardContextMenuProps = { export const RecordBoardContextMenu = ({ recordBoardId, }: RecordBoardContextMenuProps) => { - const { getSelectedRecordIdsSelector } = useRecordBoardStates(recordBoardId); + const { selectedRecordIdsSelector } = useRecordBoardStates(recordBoardId); - const selectedRecordIds = useRecoilValue(getSelectedRecordIdsSelector()); + const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector()); if (!selectedRecordIds.length) { return null; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardStates.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardStates.ts index cd5b2cf31412..7758ca97fae1 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardStates.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardStates.ts @@ -8,6 +8,7 @@ import { onRecordBoardFetchMoreVisibilityChangeComponentState } from '@/object-r import { recordBoardColumnIdsComponentState } from '@/object-record/record-board/states/recordBoardColumnIdsComponentState'; import { recordBoardFieldDefinitionsComponentState } from '@/object-record/record-board/states/recordBoardFieldDefinitionsComponentState'; import { recordBoardFiltersComponentState } from '@/object-record/record-board/states/recordBoardFiltersComponentState'; +import { recordBoardKanbanFieldMetadataNameComponentState } from '@/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState'; import { recordBoardObjectSingularNameComponentState } from '@/object-record/record-board/states/recordBoardObjectSingularNameComponentState'; import { recordBoardRecordIdsByColumnIdComponentFamilyState } from '@/object-record/record-board/states/recordBoardRecordIdsByColumnIdComponentFamilyState'; import { recordBoardSortsComponentState } from '@/object-record/record-board/states/recordBoardSortsComponentState'; @@ -28,15 +29,19 @@ export const useRecordBoardStates = (recordBoardId?: string) => { return { scopeId, - getObjectSingularNameState: extractComponentState( + objectSingularNameState: extractComponentState( recordBoardObjectSingularNameComponentState, scopeId, ), - getIsFetchingRecordState: extractComponentState( + kanbanFieldMetadataNameState: extractComponentState( + recordBoardKanbanFieldMetadataNameComponentState, + scopeId, + ), + isFetchingRecordState: extractComponentState( isRecordBoardFetchingRecordsComponentState, scopeId, ), - getColumnIdsState: extractComponentState( + columnIdsState: extractComponentState( recordBoardColumnIdsComponentState, scopeId, ), @@ -53,19 +58,16 @@ export const useRecordBoardStates = (recordBoardId?: string) => { scopeId, ), - getFiltersState: extractComponentState( + filtersState: extractComponentState( recordBoardFiltersComponentState, scopeId, ), - getSortsState: extractComponentState( - recordBoardSortsComponentState, - scopeId, - ), - getFieldDefinitionsState: extractComponentState( + sortsState: extractComponentState(recordBoardSortsComponentState, scopeId), + fieldDefinitionsState: extractComponentState( recordBoardFieldDefinitionsComponentState, scopeId, ), - getVisibleFieldDefinitionsState: extractComponentReadOnlySelector( + visibleFieldDefinitionsState: extractComponentReadOnlySelector( recordBoardVisibleFieldDefinitionsComponentSelector, scopeId, ), @@ -78,17 +80,17 @@ export const useRecordBoardStates = (recordBoardId?: string) => { isRecordBoardCardSelectedComponentFamilyState, scopeId, ), - getSelectedRecordIdsSelector: extractComponentReadOnlySelector( + selectedRecordIdsSelector: extractComponentReadOnlySelector( recordBoardSelectedRecordIdsComponentSelector, scopeId, ), - getIsCompactModeActiveState: extractComponentState( + isCompactModeActiveState: extractComponentState( isRecordBoardCompactModeActiveComponentState, scopeId, ), - getOnFetchMoreVisibilityChangeState: extractComponentState( + onFetchMoreVisibilityChangeState: extractComponentState( onRecordBoardFetchMoreVisibilityChangeComponentState, scopeId, ), diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardColumns.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardColumns.ts index 34ca406203a9..d330a476d64f 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardColumns.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardColumns.ts @@ -5,14 +5,14 @@ import { RecordBoardColumnDefinition } from '@/object-record/record-board/types/ import { isDeeplyEqual } from '~/utils/isDeeplyEqual'; export const useSetRecordBoardColumns = (recordBoardId?: string) => { - const { scopeId, getColumnIdsState, columnsFamilySelector } = + const { scopeId, columnIdsState, columnsFamilySelector } = useRecordBoardStates(recordBoardId); const setColumns = useRecoilCallback( ({ set, snapshot }) => (columns: RecordBoardColumnDefinition[]) => { const currentColumnsIds = snapshot - .getLoadable(getColumnIdsState()) + .getLoadable(columnIdsState) .getValue(); const columnIds = columns.map(({ id }) => id); @@ -22,7 +22,7 @@ export const useSetRecordBoardColumns = (recordBoardId?: string) => { } set( - getColumnIdsState(), + columnIdsState, columns.map((column) => column.id), ); @@ -38,7 +38,7 @@ export const useSetRecordBoardColumns = (recordBoardId?: string) => { set(columnsFamilySelector(column.id), column); }); }, - [columnsFamilySelector, getColumnIdsState], + [columnsFamilySelector, columnIdsState], ); return { diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardRecordIds.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardRecordIds.ts index 03db29158c6c..8ead970305d8 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardRecordIds.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useSetRecordBoardRecordIds.ts @@ -9,13 +9,14 @@ export const useSetRecordBoardRecordIds = (recordBoardId?: string) => { scopeId, recordIdsByColumnIdFamilyState, columnsFamilySelector, - getColumnIdsState, + columnIdsState, + kanbanFieldMetadataNameState, } = useRecordBoardStates(recordBoardId); const setRecordIds = useRecoilCallback( ({ set, snapshot }) => (records: ObjectRecord[]) => { - const columnIds = snapshot.getLoadable(getColumnIdsState()).getValue(); + const columnIds = snapshot.getLoadable(columnIdsState).getValue(); columnIds.forEach((columnId) => { const column = snapshot @@ -26,8 +27,18 @@ export const useSetRecordBoardRecordIds = (recordBoardId?: string) => { .getLoadable(recordIdsByColumnIdFamilyState(columnId)) .getValue(); + const kanbanFieldMetadataName = snapshot + .getLoadable(kanbanFieldMetadataNameState) + .getValue(); + + if (!kanbanFieldMetadataName) { + return; + } + const columnRecordIds = records - .filter((record) => record.stage === column?.value) + .filter( + (record) => record[kanbanFieldMetadataName] === column?.value, + ) .sort(sortRecordsByPosition) .map((record) => record.id); @@ -36,7 +47,12 @@ export const useSetRecordBoardRecordIds = (recordBoardId?: string) => { } }); }, - [columnsFamilySelector, getColumnIdsState, recordIdsByColumnIdFamilyState], + [ + columnIdsState, + columnsFamilySelector, + recordIdsByColumnIdFamilyState, + kanbanFieldMetadataNameState, + ], ); return { diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoard.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoard.ts index 59429b254652..25a358e7ac91 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoard.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoard.ts @@ -7,17 +7,21 @@ import { useSetRecordBoardRecordIds } from '@/object-record/record-board/hooks/i export const useRecordBoard = (recordBoardId?: string) => { const { scopeId, - getFieldDefinitionsState, - getObjectSingularNameState, - getSelectedRecordIdsSelector, - getIsCompactModeActiveState, - getOnFetchMoreVisibilityChangeState, + fieldDefinitionsState, + objectSingularNameState, + selectedRecordIdsSelector, + isCompactModeActiveState, + onFetchMoreVisibilityChangeState, + kanbanFieldMetadataNameState, } = useRecordBoardStates(recordBoardId); const { setColumns } = useSetRecordBoardColumns(recordBoardId); const { setRecordIds } = useSetRecordBoardRecordIds(recordBoardId); - const setFieldDefinitions = useSetRecoilState(getFieldDefinitionsState()); - const setObjectSingularName = useSetRecoilState(getObjectSingularNameState()); + const setFieldDefinitions = useSetRecoilState(fieldDefinitionsState); + const setObjectSingularName = useSetRecoilState(objectSingularNameState); + const setKanbanFieldMetadataName = useSetRecoilState( + kanbanFieldMetadataNameState, + ); return { scopeId, @@ -25,8 +29,9 @@ export const useRecordBoard = (recordBoardId?: string) => { setRecordIds, setFieldDefinitions, setObjectSingularName, - getSelectedRecordIdsSelector, - getIsCompactModeActiveState, - getOnFetchMoreVisibilityChangeState, + setKanbanFieldMetadataName, + selectedRecordIdsSelector, + isCompactModeActiveState, + onFetchMoreVisibilityChangeState, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts index ae89223c79ee..a2024d4ba683 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts +++ b/packages/twenty-front/src/modules/object-record/record-board/hooks/useRecordBoardSelection.ts @@ -3,21 +3,21 @@ import { useRecoilCallback } from 'recoil'; import { useRecordBoardStates } from '@/object-record/record-board/hooks/internal/useRecordBoardStates'; export const useRecordBoardSelection = (recordBoardId?: string) => { - const { getSelectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState } = + const { selectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState } = useRecordBoardStates(recordBoardId); const resetRecordSelection = useRecoilCallback( ({ snapshot, set }) => () => { const recordIds = snapshot - .getLoadable(getSelectedRecordIdsSelector()) + .getLoadable(selectedRecordIdsSelector()) .getValue(); for (const recordId of recordIds) { set(isRecordBoardCardSelectedFamilyState(recordId), false); } }, - [getSelectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState], + [selectedRecordIdsSelector, isRecordBoardCardSelectedFamilyState], ); const setRecordAsSelected = useRecoilCallback( diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx index 034466873714..dda3bc34e83c 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-card/components/RecordBoardCard.tsx @@ -133,12 +133,12 @@ export const RecordBoardCard = () => { const { updateOneRecord, objectMetadataItem } = useContext(RecordBoardContext); const { - getIsCompactModeActiveState, + isCompactModeActiveState, isRecordBoardCardSelectedFamilyState, - getVisibleFieldDefinitionsState, + visibleFieldDefinitionsState, } = useRecordBoardStates(); - const isCompactModeActive = useRecoilValue(getIsCompactModeActiveState()); + const isCompactModeActive = useRecoilValue(isCompactModeActiveState); const [isCardInCompactMode, setIsCardInCompactMode] = useState(true); @@ -146,14 +146,14 @@ export const RecordBoardCard = () => { isRecordBoardCardSelectedFamilyState(recordId), ); - const visibleBoardCardFieldDefinitions = useRecoilValue( - getVisibleFieldDefinitionsState(), + const visibleFieldDefinitions = useRecoilValue( + visibleFieldDefinitionsState(), ); const record = useRecoilValue(recordStoreFamilyState(recordId)); - const setContextMenuPosition = useSetRecoilState(contextMenuPositionState()); - const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState()); + const setContextMenuPosition = useSetRecoilState(contextMenuPositionState); + const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState); const handleContextMenu = (event: React.MouseEvent) => { event.preventDefault(); @@ -247,7 +247,7 @@ export const RecordBoardCard = () => { isOpen={!isCardInCompactMode || !isCompactModeActive} initial={false} > - {visibleBoardCardFieldDefinitions.map((fieldDefinition) => ( + {visibleFieldDefinitions.map((fieldDefinition) => ( diff --git a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader.tsx b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader.tsx index 072120d68063..56127562c05b 100644 --- a/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-board/record-board-column/components/RecordBoardColumnFetchMoreLoader.tsx @@ -16,12 +16,12 @@ const StyledText = styled.div` `; export const RecordBoardColumnFetchMoreLoader = () => { - const { getIsFetchingRecordState, getOnFetchMoreVisibilityChangeState } = + const { isFetchingRecordState, onFetchMoreVisibilityChangeState } = useRecordBoardStates(); - const isFetchingRecords = useRecoilValue(getIsFetchingRecordState()); + const isFetchingRecord = useRecoilValue(isFetchingRecordState); const onFetchMoreVisibilityChange = useRecoilValue( - getOnFetchMoreVisibilityChangeState(), + onFetchMoreVisibilityChangeState, ); const { ref } = useInView({ @@ -30,7 +30,7 @@ export const RecordBoardColumnFetchMoreLoader = () => { return (
- {isFetchingRecords && Loading more...} + {isFetchingRecord && Loading more...}
); }; diff --git a/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState.ts b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState.ts new file mode 100644 index 000000000000..26490c9298b3 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-board/states/recordBoardKanbanFieldMetadataNameComponentState.ts @@ -0,0 +1,7 @@ +import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState'; + +export const recordBoardKanbanFieldMetadataNameComponentState = + createComponentState({ + key: 'recordBoardKanbanFieldMetadataNameComponentState', + defaultValue: undefined, + }); diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useClearField.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useClearField.ts index f65fbe900ad6..548e212f98c7 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useClearField.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useClearField.ts @@ -20,7 +20,7 @@ export const useClearField = () => { ({ snapshot, set }) => () => { const objectMetadataItems = snapshot - .getLoadable(objectMetadataItemsState()) + .getLoadable(objectMetadataItemsState) .getValue(); const foundObjectMetadataItem = objectMetadataItems.find( diff --git a/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldInputOnly.ts b/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldInputOnly.ts index d1566823ce98..750ea8b0b463 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldInputOnly.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/hooks/useIsFieldInputOnly.ts @@ -7,9 +7,5 @@ import { isFieldRating } from '../types/guards/isFieldRating'; export const useIsFieldInputOnly = () => { const { fieldDefinition } = useContext(FieldContext); - if (isFieldBoolean(fieldDefinition) || isFieldRating(fieldDefinition)) { - return true; - } - - return false; + return isFieldBoolean(fieldDefinition) || isFieldRating(fieldDefinition); }; diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/FullNameFieldInput.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/FullNameFieldInput.tsx index 77fa108e15ed..7756c1ef678b 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/FullNameFieldInput.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/FullNameFieldInput.tsx @@ -66,6 +66,10 @@ export const FullNameFieldInput = ({ setDraftValue(convertToFullName(newDoubleText)); }; + const handlePaste = (newDoubleText: FieldDoubleText) => { + setDraftValue(convertToFullName(newDoubleText)); + }; + return ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx index 9d8dd603400f..3cbe87a498aa 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx +++ b/packages/twenty-front/src/modules/object-record/record-field/meta-types/input/components/__stories__/RelationFieldInput.stories.tsx @@ -30,9 +30,9 @@ import { } from '../RelationFieldInput'; const RelationWorkspaceSetterEffect = () => { - const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState()); + const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); const setCurrentWorkspaceMember = useSetRecoilState( - currentWorkspaceMemberState(), + currentWorkspaceMemberState, ); useEffect(() => { diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts index 74f8bbe88461..7f228198369d 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldMetadata.ts @@ -69,6 +69,11 @@ export type FieldRatingMetadata = { fieldName: string; }; +export type FieldRawJsonMetadata = { + objectMetadataNameSingular?: string; + fieldName: string; +}; + export type FieldDefinitionRelationType = | 'FROM_MANY_OBJECTS' | 'FROM_ONE_OBJECT' diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/FieldType.ts b/packages/twenty-front/src/modules/object-record/record-field/types/FieldType.ts index e09af18f81cb..d59a6bb84d67 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/FieldType.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/FieldType.ts @@ -17,4 +17,5 @@ export type FieldType = | 'URL' | 'UUID' | 'MULTI_SELECT' - | 'NUMERIC'; + | 'NUMERIC' + | 'RAW_JSON'; diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts index 86f1922cbeea..0cf44a152c3e 100644 --- a/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/assertFieldMetadata.ts @@ -10,6 +10,7 @@ import { FieldNumberMetadata, FieldPhoneMetadata, FieldRatingMetadata, + FieldRawJsonMetadata, FieldRelationMetadata, FieldSelectMetadata, FieldTextMetadata, @@ -47,7 +48,9 @@ type AssertFieldMetadataFunction = < ? FieldTextMetadata : E extends 'UUID' ? FieldUuidMetadata - : never, + : E extends 'RAW_JSON' + ? FieldRawJsonMetadata + : never, >( fieldType: E, fieldTypeGuard: ( diff --git a/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRawJson.ts b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRawJson.ts new file mode 100644 index 000000000000..3decadfb864a --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-field/types/guards/isFieldRawJson.ts @@ -0,0 +1,6 @@ +import { FieldDefinition } from '../FieldDefinition'; +import { FieldMetadata, FieldRawJsonMetadata } from '../FieldMetadata'; + +export const isFieldRawJson = ( + field: Pick, 'type'>, +): field is FieldDefinition => field.type === 'RAW_JSON'; diff --git a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts index 49918c73718c..728c681d5cf2 100644 --- a/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts +++ b/packages/twenty-front/src/modules/object-record/record-filter/utils/turnObjectDropdownFilterIntoQueryFilter.ts @@ -35,13 +35,11 @@ export const turnObjectDropdownFilterIntoQueryFilter = ( ); if (!correspondingField) { - throw new Error( - `Could not find field ${rawUIFilter.fieldMetadataId} in metadata object`, - ); + continue; } if (!isDefined(rawUIFilter.value) || rawUIFilter.value === '') { - return undefined; + continue; } switch (rawUIFilter.definition.type) { diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainerEffect.tsx index 08e3bd35d900..df517ccbe8f4 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainerEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainerEffect.tsx @@ -8,7 +8,10 @@ import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoar import { useRecordBoardSelection } from '@/object-record/record-board/hooks/useRecordBoardSelection'; import { useLoadRecordIndexBoard } from '@/object-record/record-index/hooks/useLoadRecordIndexBoard'; import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; +import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; import { computeRecordBoardColumnDefinitionsFromObjectMetadata } from '@/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata'; +import { FieldMetadataType } from '~/generated-metadata/graphql'; +import { isDefined } from '~/utils/isDefined'; type RecordIndexBoardContainerEffectProps = { objectNameSingular: string; @@ -28,9 +31,10 @@ export const RecordIndexBoardContainerEffect = ({ const { setColumns, setObjectSingularName, - getSelectedRecordIdsSelector, + selectedRecordIdsSelector, setFieldDefinitions, - getOnFetchMoreVisibilityChangeState, + onFetchMoreVisibilityChangeState, + setKanbanFieldMetadataName, } = useRecordBoard(recordBoardId); const { fetchMoreRecords, loading } = useLoadRecordIndexBoard({ @@ -40,7 +44,11 @@ export const RecordIndexBoardContainerEffect = ({ }); const setOnFetchMoreVisibilityChange = useSetRecoilState( - getOnFetchMoreVisibilityChangeState(), + onFetchMoreVisibilityChangeState, + ); + + const recordIndexKanbanFieldMetadataId = useRecoilValue( + recordIndexKanbanFieldMetadataIdState, ); useEffect(() => { @@ -67,6 +75,7 @@ export const RecordIndexBoardContainerEffect = ({ setColumns( computeRecordBoardColumnDefinitionsFromObjectMetadata( objectMetadataItem, + recordIndexKanbanFieldMetadataId ?? '', navigateToSelectSettings, ), ); @@ -74,18 +83,37 @@ export const RecordIndexBoardContainerEffect = ({ navigateToSelectSettings, objectMetadataItem, objectNameSingular, + recordIndexKanbanFieldMetadataId, setColumns, ]); const recordIndexFieldDefinitions = useRecoilValue( - recordIndexFieldDefinitionsState(), + recordIndexFieldDefinitionsState, ); useEffect(() => { setFieldDefinitions(recordIndexFieldDefinitions); }, [objectMetadataItem, setFieldDefinitions, recordIndexFieldDefinitions]); - const selectedRecordIds = useRecoilValue(getSelectedRecordIdsSelector()); + useEffect(() => { + if (isDefined(recordIndexKanbanFieldMetadataId)) { + const kanbanFieldMetadataName = objectMetadataItem?.fields.find( + (field) => + field.type === FieldMetadataType.Select && + field.id === recordIndexKanbanFieldMetadataId, + )?.name; + + if (isDefined(kanbanFieldMetadataName)) { + setKanbanFieldMetadataName(kanbanFieldMetadataName); + } + } + }, [ + objectMetadataItem, + recordIndexKanbanFieldMetadataId, + setKanbanFieldMetadataName, + ]); + + const selectedRecordIds = useRecoilValue(selectedRecordIdsSelector()); const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({ objectMetadataItem, diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx index d9873a1b14bd..3fceb3f2cce3 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainer.tsx @@ -10,10 +10,10 @@ import { RecordIndexTableContainer } from '@/object-record/record-index/componen import { RecordIndexTableContainerEffect } from '@/object-record/record-index/components/RecordIndexTableContainerEffect'; import { RecordIndexViewBarEffect } from '@/object-record/record-index/components/RecordIndexViewBarEffect'; import { RecordIndexOptionsDropdown } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdown'; -import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId'; import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; import { recordIndexFiltersState } from '@/object-record/record-index/states/recordIndexFiltersState'; import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState'; +import { recordIndexKanbanFieldMetadataIdState } from '@/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState'; import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState'; import { recordIndexViewTypeState } from '@/object-record/record-index/states/recordIndexViewTypeState'; import { useRecordTable } from '@/object-record/record-table/hooks/useRecordTable'; @@ -47,7 +47,7 @@ export const RecordIndexContainer = ({ objectNamePlural, }: RecordIndexContainerProps) => { const [recordIndexViewType, setRecordIndexViewType] = useRecoilState( - recordIndexViewTypeState(), + recordIndexViewTypeState, ); const { objectNameSingular } = useObjectNameSingularFromPlural({ objectNamePlural, @@ -57,13 +57,16 @@ export const RecordIndexContainer = ({ objectNameSingular, }); - const { columnDefinitions } = + const { columnDefinitions, filterDefinitions, sortDefinitions } = useColumnDefinitionsFromFieldMetadata(objectMetadataItem); - const setRecordIndexFilters = useSetRecoilState(recordIndexFiltersState()); - const setRecordIndexSorts = useSetRecoilState(recordIndexSortsState()); + const setRecordIndexFilters = useSetRecoilState(recordIndexFiltersState); + const setRecordIndexSorts = useSetRecoilState(recordIndexSortsState); const setRecordIndexIsCompactModeActive = useSetRecoilState( - recordIndexIsCompactModeActiveState(), + recordIndexIsCompactModeActiveState, + ); + const setRecordIndexViewKanbanFieldMetadataIdState = useSetRecoilState( + recordIndexKanbanFieldMetadataIdState, ); const { setTableFilters, setTableSorts, setTableColumns } = useRecordTable({ @@ -85,7 +88,7 @@ export const RecordIndexContainer = ({ ); const existingRecordIndexFieldDefinitions = snapshot - .getLoadable(recordIndexFieldDefinitionsState()) + .getLoadable(recordIndexFieldDefinitionsState) .getValue(); if ( @@ -94,10 +97,7 @@ export const RecordIndexContainer = ({ newRecordIndexFieldDefinitions, ) ) { - set( - recordIndexFieldDefinitionsState(), - newRecordIndexFieldDefinitions, - ); + set(recordIndexFieldDefinitionsState, newRecordIndexFieldDefinitions); } }, [columnDefinitions, setTableColumns], @@ -115,21 +115,27 @@ export const RecordIndexContainer = ({ viewType={recordIndexViewType ?? ViewType.Table} /> } - optionsDropdownScopeId={RECORD_INDEX_OPTIONS_DROPDOWN_ID} - onViewFieldsChange={onViewFieldsChange} - onViewFiltersChange={(viewFilters) => { - setTableFilters(mapViewFiltersToFilters(viewFilters)); - setRecordIndexFilters(mapViewFiltersToFilters(viewFilters)); - }} - onViewSortsChange={(viewSorts) => { - setTableSorts(mapViewSortsToSorts(viewSorts)); - setRecordIndexSorts(mapViewSortsToSorts(viewSorts)); - }} - onViewTypeChange={(viewType: ViewType) => { - setRecordIndexViewType(viewType); - }} - onViewCompactModeChange={(isCompactModeActive: boolean) => { - setRecordIndexIsCompactModeActive(isCompactModeActive); + onCurrentViewChange={(view) => { + if (!view) { + return; + } + + onViewFieldsChange(view.viewFields); + setTableFilters( + mapViewFiltersToFilters(view.viewFilters, filterDefinitions), + ); + setRecordIndexFilters( + mapViewFiltersToFilters(view.viewFilters, filterDefinitions), + ); + setTableSorts(mapViewSortsToSorts(view.viewSorts, sortDefinitions)); + setRecordIndexSorts( + mapViewSortsToSorts(view.viewSorts, sortDefinitions), + ); + setRecordIndexViewType(view.type); + setRecordIndexViewKanbanFieldMetadataIdState( + view.kanbanFieldMetadataId, + ); + setRecordIndexIsCompactModeActive(view.isCompact); }} /> { setAvailableTableColumns(columnDefinitions); }, [columnDefinitions, setAvailableTableColumns]); - const selectedRowIds = useRecoilValue(getSelectedRowIdsSelector()); + const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); const { setActionBarEntries, setContextMenuEntries } = useRecordActionBar({ objectMetadataItem, @@ -57,9 +56,9 @@ export const RecordIndexTableContainerEffect = ({ useEffect(() => { setOnEntityCountChange( - () => (entityCount: number) => setEntityCountInCurrentView(entityCount), + () => (entityCount: number) => setRecordCountInCurrentView(entityCount), ); - }, [setEntityCountInCurrentView, setOnEntityCountChange]); + }, [setRecordCountInCurrentView, setOnEntityCountChange]); return <>; }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexViewBarEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexViewBarEffect.tsx index 51d27b0c38d0..44ae8879625f 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexViewBarEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexViewBarEffect.tsx @@ -3,7 +3,7 @@ import { useEffect } from 'react'; import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata'; import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural'; -import { useViewBar } from '@/views/hooks/useViewBar'; +import { useInitViewBar } from '@/views/hooks/useInitViewBar'; import { isUndefinedOrNull } from '~/utils/isUndefinedOrNull'; type RecordIndexViewBarEffectProps = { @@ -31,7 +31,7 @@ export const RecordIndexViewBarEffect = ({ setAvailableSortDefinitions, setAvailableFilterDefinitions, setAvailableFieldDefinitions, - } = useViewBar({ viewBarId }); + } = useInitViewBar(viewBarId); useEffect(() => { if (isUndefinedOrNull(objectMetadataItem)) { diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts index fa894ebb19a5..0efe9e2f5e75 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexBoard.ts @@ -11,7 +11,7 @@ import { recordIndexFiltersState } from '@/object-record/record-index/states/rec import { recordIndexIsCompactModeActiveState } from '@/object-record/record-index/states/recordIndexIsCompactModeActiveState'; import { recordIndexSortsState } from '@/object-record/record-index/states/recordIndexSortsState'; import { useSetRecordInStore } from '@/object-record/record-store/hooks/useSetRecordInStore'; -import { useViewBar } from '@/views/hooks/useViewBar'; +import { useSetRecordCountInCurrentView } from '@/views/hooks/useSetRecordCountInCurrentView'; type UseLoadRecordIndexBoardProps = { objectNameSingular: string; @@ -30,19 +30,19 @@ export const useLoadRecordIndexBoard = ({ const { setRecordIds: setRecordIdsInBoard, setFieldDefinitions, - getIsCompactModeActiveState, + isCompactModeActiveState, } = useRecordBoard(recordBoardId); const { setRecords: setRecordsInStore } = useSetRecordInStore(); const recordIndexFieldDefinitions = useRecoilValue( - recordIndexFieldDefinitionsState(), + recordIndexFieldDefinitionsState, ); useEffect(() => { setFieldDefinitions(recordIndexFieldDefinitions); }, [recordIndexFieldDefinitions, setFieldDefinitions]); - const recordIndexFilters = useRecoilValue(recordIndexFiltersState()); - const recordIndexSorts = useRecoilValue(recordIndexSortsState()); + const recordIndexFilters = useRecoilValue(recordIndexFiltersState); + const recordIndexSorts = useRecoilValue(recordIndexSortsState); const requestFilters = turnObjectDropdownFilterIntoQueryFilter( recordIndexFilters, objectMetadataItem?.fields ?? [], @@ -53,7 +53,7 @@ export const useLoadRecordIndexBoard = ({ ); const recordIndexIsCompactModeActive = useRecoilValue( - recordIndexIsCompactModeActiveState(), + recordIndexIsCompactModeActiveState, ); const { @@ -68,13 +68,10 @@ export const useLoadRecordIndexBoard = ({ orderBy, }); - const { setEntityCountInCurrentView } = useViewBar({ - viewBarId, - }); + const { setRecordCountInCurrentView } = + useSetRecordCountInCurrentView(viewBarId); - const setIsCompactModeActive = useSetRecoilState( - getIsCompactModeActiveState(), - ); + const setIsCompactModeActive = useSetRecoilState(isCompactModeActiveState); useEffect(() => { setRecordIdsInBoard(records); @@ -85,8 +82,8 @@ export const useLoadRecordIndexBoard = ({ }, [records, setRecordsInStore]); useEffect(() => { - setEntityCountInCurrentView(totalCount); - }, [totalCount, setEntityCountInCurrentView]); + setRecordCountInCurrentView(totalCount); + }, [totalCount, setRecordCountInCurrentView]); useEffect(() => { setIsCompactModeActive(recordIndexIsCompactModeActive); diff --git a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts index 90c4d46e200e..4594d516d409 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/hooks/useLoadRecordIndexTable.ts @@ -18,11 +18,11 @@ export const useFindManyParams = ( objectNameSingular, }); - const { getTableFiltersState, getTableSortsState } = + const { tableFiltersState, tableSortsState } = useRecordTableStates(recordTableId); - const tableFilters = useRecoilValue(getTableFiltersState()); - const tableSorts = useRecoilValue(getTableSortsState()); + const tableFilters = useRecoilValue(tableFiltersState); + const tableSorts = useRecoilValue(tableSortsState); const filter = turnObjectDropdownFilterIntoQueryFilter( tableFilters, @@ -40,9 +40,9 @@ export const useFindManyParams = ( export const useLoadRecordIndexTable = (objectNameSingular: string) => { const { setRecordTableData, setIsRecordTableInitialLoading } = useRecordTable(); - const { getTableLastRowVisibleState } = useRecordTableStates(); - const setLastRowVisible = useSetRecoilState(getTableLastRowVisibleState()); - const currentWorkspace = useRecoilValue(currentWorkspaceState()); + const { tableLastRowVisibleState } = useRecordTableStates(); + const setLastRowVisible = useSetRecoilState(tableLastRowVisibleState); + const currentWorkspace = useRecoilValue(currentWorkspaceState); const params = useFindManyParams(objectNameSingular); const { diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdown.tsx b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdown.tsx index d79427c0fef9..4b1d32086319 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdown.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdown.tsx @@ -1,11 +1,8 @@ import { RecordIndexOptionsDropdownButton } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdownButton'; import { RecordIndexOptionsDropdownContent } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdownContent'; import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId'; -import { useRecordTableStates } from '@/object-record/record-table/hooks/internal/useRecordTableStates'; -import { RecordTableScope } from '@/object-record/record-table/scopes/RecordTableScope'; import { TableOptionsHotkeyScope } from '@/object-record/record-table/types/TableOptionsHotkeyScope'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; -import { useViewBar } from '@/views/hooks/useViewBar'; import { ViewType } from '@/views/types/ViewType'; type RecordIndexOptionsDropdownProps = { @@ -19,28 +16,19 @@ export const RecordIndexOptionsDropdown = ({ objectNameSingular, viewType, }: RecordIndexOptionsDropdownProps) => { - const { setViewEditMode } = useViewBar(); - const { scopeId } = useRecordTableStates(recordIndexId); - return ( - false} - > - } - dropdownHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }} - dropdownOffset={{ y: 8 }} - dropdownComponents={ - - } - onClickOutside={() => setViewEditMode('none')} - /> - + } + dropdownHotkeyScope={{ scope: TableOptionsHotkeyScope.Dropdown }} + dropdownOffset={{ y: 8 }} + dropdownComponents={ + + } + /> ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx index 59831da2936d..9d48f624c2cb 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/options/components/RecordIndexOptionsDropdownContent.tsx @@ -1,5 +1,4 @@ -import { useRef, useState } from 'react'; -import { useRecoilValue } from 'recoil'; +import { useState } from 'react'; import { Key } from 'ts-key-enum'; import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId'; @@ -14,7 +13,6 @@ import { IconTag, } from '@/ui/display/icon'; import { DropdownMenuHeader } from '@/ui/layout/dropdown/components/DropdownMenuHeader'; -import { DropdownMenuInput } from '@/ui/layout/dropdown/components/DropdownMenuInput'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; import { DropdownMenuSeparator } from '@/ui/layout/dropdown/components/DropdownMenuSeparator'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; @@ -22,8 +20,7 @@ import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { MenuItemToggle } from '@/ui/navigation/menu-item/components/MenuItemToggle'; import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys'; import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection'; -import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates'; -import { useViewBar } from '@/views/hooks/useViewBar'; +import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; import { ViewType } from '@/views/types/ViewType'; type RecordIndexOptionsMenu = 'fields'; @@ -39,13 +36,8 @@ export const RecordIndexOptionsDropdownContent = ({ recordIndexId, objectNameSingular, }: RecordIndexOptionsDropdownContentProps) => { - const { setViewEditMode, handleViewNameSubmit } = useViewBar({ - viewBarId: recordIndexId, - }); - const { viewEditModeState, currentViewSelector } = useViewScopedStates(); + const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(); - const viewEditMode = useRecoilValue(viewEditModeState); - const currentView = useRecoilValue(currentViewSelector); const { closeDropdown } = useDropdown(RECORD_INDEX_OPTIONS_DROPDOWN_ID); const [currentMenu, setCurrentMenu] = useState< @@ -54,8 +46,6 @@ export const RecordIndexOptionsDropdownContent = ({ const resetMenu = () => setCurrentMenu(undefined); - const viewEditInputRef = useRef(null); - const handleSelectMenu = (option: RecordIndexOptionsMenu) => { setCurrentMenu(option); }; @@ -68,18 +58,6 @@ export const RecordIndexOptionsDropdownContent = ({ TableOptionsHotkeyScope.Dropdown, ); - useScopedHotkeys( - Key.Enter, - () => { - const name = viewEditInputRef.current?.value; - handleViewNameSubmit(name); - resetMenu(); - setViewEditMode('none'); - closeDropdown(); - }, - TableOptionsHotkeyScope.Dropdown, - ); - const { handleColumnVisibilityChange, handleReorderColumns, @@ -122,33 +100,18 @@ export const RecordIndexOptionsDropdownContent = ({ return ( <> {!currentMenu && ( - <> - + handleSelectMenu('fields')} + LeftIcon={IconTag} + text="Fields" /> - - - handleSelectMenu('fields')} - LeftIcon={IconTag} - text="Fields" - /> - openRecordSpreadsheetImport()} - LeftIcon={IconFileImport} - text="Import" - /> - - + openRecordSpreadsheetImport()} + LeftIcon={IconFileImport} + text="Import" + /> + )} {currentMenu === 'fields' && ( <> @@ -185,7 +148,7 @@ export const RecordIndexOptionsDropdownContent = ({ onToggleChange={() => setAndPersistIsCompactModeActive( !isCompactModeActive, - currentView, + currentViewWithCombinedFiltersAndSorts, ) } toggled={isCompactModeActive} diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts index ab72add28050..8ee5ecb1726f 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useExportTableData.ts @@ -109,11 +109,11 @@ export const useExportTableData = ({ const [progress, setProgress] = useState(undefined); const [hasNextPage, setHasNextPage] = useState(true); - const { getVisibleTableColumnsSelector, getSelectedRowIdsSelector } = + const { visibleTableColumnsSelector, selectedRowIdsSelector } = useRecordTableStates(recordIndexId); - const columns = useRecoilValue(getVisibleTableColumnsSelector()); - const selectedRowIds = useRecoilValue(getSelectedRowIdsSelector()); + const columns = useRecoilValue(visibleTableColumnsSelector()); + const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); const hasSelectedRows = selectedRowIds.length > 0; diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard.ts index 33da264c9679..da059ea5045d 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard.ts @@ -8,8 +8,8 @@ import { useRecordBoard } from '@/object-record/record-board/hooks/useRecordBoar import { FieldMetadata } from '@/object-record/record-field/types/FieldMetadata'; import { recordIndexFieldDefinitionsState } from '@/object-record/record-index/states/recordIndexFieldDefinitionsState'; import { ColumnDefinition } from '@/object-record/record-table/types/ColumnDefinition'; -import { useViewFields } from '@/views/hooks/internal/useViewFields'; -import { useViews } from '@/views/hooks/internal/useViews'; +import { useHandleViews } from '@/views/hooks/useHandleViews'; +import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields'; import { GraphQLView } from '@/views/types/GraphQLView'; import { mapBoardFieldDefinitionsToViewFields } from '@/views/utils/mapBoardFieldDefinitionsToViewFields'; import { mapArrayToObject } from '~/utils/array/mapArrayToObject'; @@ -28,14 +28,14 @@ export const useRecordIndexOptionsForBoard = ({ viewBarId, }: useRecordIndexOptionsForBoardParams) => { const [recordIndexFieldDefinitions, setRecordIndexFieldDefinitions] = - useRecoilState(recordIndexFieldDefinitionsState()); + useRecoilState(recordIndexFieldDefinitionsState); - const { persistViewFields } = useViewFields(viewBarId); - const { updateView } = useViews(viewBarId); - const { getIsCompactModeActiveState } = useRecordBoard(recordBoardId); + const { saveViewFields } = useSaveCurrentViewFields(viewBarId); + const { updateCurrentView } = useHandleViews(viewBarId); + const { isCompactModeActiveState } = useRecordBoard(recordBoardId); const [isCompactModeActive, setIsCompactModeActive] = useRecoilState( - getIsCompactModeActiveState(), + isCompactModeActiveState, ); const { objectMetadataItem } = useObjectMetadataItemOnly({ @@ -105,20 +105,14 @@ export const useRecordIndexOptionsForBoard = ({ if (isDeeplyEqual(visibleBoardFields, reorderedVisibleBoardFields)) return; - const updatedFields = [ - ...reorderedVisibleBoardFields, - ...hiddenBoardFields, - ].map((field, index) => ({ ...field, position: index })); + const updatedFields = [...reorderedVisibleBoardFields].map( + (field, index) => ({ ...field, position: index }), + ); setRecordIndexFieldDefinitions(updatedFields); - persistViewFields(mapBoardFieldDefinitionsToViewFields(updatedFields)); + saveViewFields(mapBoardFieldDefinitionsToViewFields(updatedFields)); }, - [ - hiddenBoardFields, - persistViewFields, - setRecordIndexFieldDefinitions, - visibleBoardFields, - ], + [saveViewFields, setRecordIndexFieldDefinitions, visibleBoardFields], ); // Todo : this seems over complex and should at least be extracted to an util with unit test. @@ -172,14 +166,14 @@ export const useRecordIndexOptionsForBoard = ({ setRecordIndexFieldDefinitions(updatedFieldsDefinitions); - persistViewFields( + saveViewFields( mapBoardFieldDefinitionsToViewFields(updatedFieldsDefinitions), ); }, [ recordIndexFieldDefinitionsByKey, setRecordIndexFieldDefinitions, - persistViewFields, + saveViewFields, availableColumnDefinitions, visibleBoardFields, recordIndexFieldDefinitions, @@ -190,12 +184,11 @@ export const useRecordIndexOptionsForBoard = ({ (isCompactModeActive: boolean, view: GraphQLView | undefined) => { if (!view) return; setIsCompactModeActive(isCompactModeActive); - updateView({ - ...view, + updateCurrentView({ isCompact: isCompactModeActive, }); }, - [setIsCompactModeActive, updateView], + [setIsCompactModeActive, updateCurrentView], ); return { diff --git a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordIndexOptionsForTable.ts b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordIndexOptionsForTable.ts index 011c6f87d42d..e72bc7534306 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordIndexOptionsForTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-index/options/hooks/useRecordIndexOptionsForTable.ts @@ -7,11 +7,11 @@ import { useTableColumns } from '@/object-record/record-table/hooks/useTableColu import { moveArrayItem } from '~/utils/array/moveArrayItem'; export const useRecordIndexOptionsForTable = (recordTableId: string) => { - const { getHiddenTableColumnsSelector, getVisibleTableColumnsSelector } = + const { hiddenTableColumnsSelector, visibleTableColumnsSelector } = useRecordTableStates(recordTableId); - const hiddenTableColumns = useRecoilValue(getHiddenTableColumnsSelector()); - const visibleTableColumns = useRecoilValue(getVisibleTableColumnsSelector()); + const hiddenTableColumns = useRecoilValue(hiddenTableColumnsSelector()); + const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector()); const { handleColumnVisibilityChange, handleColumnReorder } = useTableColumns( { recordTableId: recordTableId }, diff --git a/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState.ts b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState.ts new file mode 100644 index 000000000000..5ff8ba4feb72 --- /dev/null +++ b/packages/twenty-front/src/modules/object-record/record-index/states/recordIndexKanbanFieldMetadataIdState.ts @@ -0,0 +1,8 @@ +import { createState } from '@/ui/utilities/state/utils/createState'; + +export const recordIndexKanbanFieldMetadataIdState = createState( + { + key: 'recordIndexKanbanFieldMetadataIdState', + defaultValue: null, + }, +); diff --git a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx index fd677eecdcab..bb2c8fc6bd9a 100644 --- a/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-inline-cell/components/RecordInlineCell.tsx @@ -15,7 +15,11 @@ import { useInlineCell } from '../hooks/useInlineCell'; import { RecordInlineCellContainer } from './RecordInlineCellContainer'; -export const RecordInlineCell = () => { +type RecordInlineCellProps = { + readonly?: boolean; +}; + +export const RecordInlineCell = ({ readonly }: RecordInlineCellProps) => { const { fieldDefinition, entityId } = useContext(FieldContext); const buttonIcon = useGetButtonIcon(); @@ -63,6 +67,7 @@ export const RecordInlineCell = () => { return ( ` display: flex; gap: ${({ theme }) => theme.spacing(1)}; width: 100%; + + ${({ readonly }) => + !readonly && + css` + cursor: pointer; + `}; `; const StyledInlineCellBaseContainer = styled.div` @@ -83,6 +88,7 @@ const StyledTooltip = styled(Tooltip)` `; type RecordInlineCellContainerProps = { + readonly?: boolean; IconLabel?: IconComponent; label?: string; labelWidth?: number; @@ -98,6 +104,7 @@ type RecordInlineCellContainerProps = { }; export const RecordInlineCellContainer = ({ + readonly, IconLabel, label, labelWidth, @@ -115,17 +122,21 @@ export const RecordInlineCellContainer = ({ const [isHovered, setIsHovered] = useState(false); const handleContainerMouseEnter = () => { - setIsHovered(true); + if (!readonly) { + setIsHovered(true); + } }; const handleContainerMouseLeave = () => { - setIsHovered(false); + if (!readonly) { + setIsHovered(false); + } }; const { isInlineCellInEditMode, openInlineCell } = useInlineCell(); const handleDisplayModeClick = () => { - if (!editModeContentOnly) { + if (!readonly && !editModeContentOnly) { openInlineCell(customEditHotkeyScope); } }; @@ -167,10 +178,10 @@ export const RecordInlineCellContainer = ({ )} - {isInlineCellInEditMode ? ( + {!readonly && isInlineCellInEditMode ? ( {editModeContent} ) : editModeContentOnly ? ( - + ) : ( - + { const recordFromStore = get(recordStoreFamilyState(recordId)); - const objectMetadataItems = get(objectMetadataItemsState()); + const objectMetadataItems = get(objectMetadataItemsState); const objectMetadataItem = objectMetadataItems.find( (item) => item.nameSingular === objectNameSingular, diff --git a/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx b/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx index 86b5e3534b2c..14b3768d08be 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/action-bar/components/RecordTableActionBar.tsx @@ -8,9 +8,9 @@ export const RecordTableActionBar = ({ }: { recordTableId: string; }) => { - const { getSelectedRowIdsSelector } = useRecordTableStates(recordTableId); + const { selectedRowIdsSelector } = useRecordTableStates(recordTableId); - const selectedRowIds = useRecoilValue(getSelectedRowIdsSelector()); + const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); if (!selectedRowIds.length) { return null; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/CheckboxCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/CheckboxCell.tsx index d6c432c8154d..2aff77e1b162 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/CheckboxCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/CheckboxCell.tsx @@ -22,7 +22,7 @@ const StyledContainer = styled.div` export const CheckboxCell = () => { const { recordId } = useContext(RecordTableRowContext); const { isRowSelectedFamilyState } = useRecordTableStates(); - const setActionBarOpenState = useSetRecoilState(actionBarOpenState()); + const setActionBarOpenState = useSetRecoilState(actionBarOpenState); const { setCurrentRowSelected } = useSetCurrentRowSelected(); const currentRowSelected = useRecoilValue(isRowSelectedFamilyState(recordId)); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/ColumnHead.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/ColumnHead.tsx index 2cd9432bd2f2..5f3207bfa27e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/ColumnHead.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/ColumnHead.tsx @@ -53,7 +53,7 @@ export const ColumnHead = ({ column }: ColumnHeadProps) => { const { getIcon } = useIcons(); const Icon = getIcon(column.iconName); - const scrollLeft = useRecoilValue(scrollLeftState()); + const scrollLeft = useRecoilValue(scrollLeftState); return ( 0}> diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx index 6d9a60ca708d..80dbedafd405 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTable.tsx @@ -122,7 +122,7 @@ export const RecordTable = ({ createRecord, }: RecordTableProps) => { const { scopeId } = useRecordTableStates(recordTableId); - const scrollLeft = useRecoilValue(scrollLeftState()); + const scrollLeft = useRecoilValue(scrollLeftState); const { objectMetadataItem } = useObjectMetadataItemOnly({ objectNameSingular, diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBody.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBody.tsx index 4ba288f2282f..3f9ad3da582e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBody.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBody.tsx @@ -11,9 +11,9 @@ type RecordTableBodyProps = { export const RecordTableBody = ({ objectNameSingular, }: RecordTableBodyProps) => { - const { getTableRowIdsState } = useRecordTableStates(); + const { tableRowIdsState } = useRecordTableStates(); - const tableRowIds = useRecoilValue(getTableRowIdsState()); + const tableRowIds = useRecoilValue(tableRowIdsState); return ( <> diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffect.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffect.tsx index 6b215db7ed7b..bb70caea388b 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffect.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableBodyEffect.tsx @@ -21,10 +21,10 @@ export const RecordTableBodyEffect = ({ loading, } = useLoadRecordIndexTable(objectNameSingular); - const { getTableLastRowVisibleState } = useRecordTableStates(); + const { tableLastRowVisibleState } = useRecordTableStates(); const [tableLastRowVisible, setTableLastRowVisible] = useRecoilState( - getTableLastRowVisibleState(), + tableLastRowVisibleState, ); const isFetchingMoreObjects = useRecoilValue( diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableCellContainer.tsx index da9c038b561d..a879ed19afc1 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableCellContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableCellContainer.tsx @@ -23,8 +23,8 @@ const StyledContainer = styled.td<{ isSelected: boolean }>` `; export const RecordTableCellContainer = () => { - const setContextMenuPosition = useSetRecoilState(contextMenuPositionState()); - const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState()); + const setContextMenuPosition = useSetRecoilState(contextMenuPositionState); + const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState); const { setCurrentRowSelected } = useSetCurrentRowSelected(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableColumnDropdownMenu.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableColumnDropdownMenu.tsx index 911bb3d87ff7..17417a832970 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableColumnDropdownMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableColumnDropdownMenu.tsx @@ -17,9 +17,9 @@ export type RecordTableColumnDropdownMenuProps = { export const RecordTableColumnDropdownMenu = ({ column, }: RecordTableColumnDropdownMenuProps) => { - const { getVisibleTableColumnsSelector } = useRecordTableStates(); + const { visibleTableColumnsSelector } = useRecordTableStates(); - const visibleTableColumns = useRecoilValue(getVisibleTableColumnsSelector()); + const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector()); const secondVisibleColumn = visibleTableColumns[1]; const canMoveLeft = diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeader.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeader.tsx index 6cde82074ca4..90611c0eb29f 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeader.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeader.tsx @@ -56,16 +56,15 @@ export const RecordTableHeader = ({ }: { createRecord: () => void; }) => { - const { getHiddenTableColumnsSelector, getVisibleTableColumnsSelector } = - useRecordTableStates(); + const { visibleTableColumnsSelector } = useRecordTableStates(); const scrollWrapper = useScrollWrapperScopedRef(); const isTableWiderThanScreen = (scrollWrapper.current?.clientWidth ?? 0) < (scrollWrapper.current?.scrollWidth ?? 0); - const visibleTableColumns = useRecoilValue(getVisibleTableColumnsSelector()); - const hiddenTableColumns = useRecoilValue(getHiddenTableColumnsSelector()); + const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector()); + const hiddenTableColumns = useRecoilValue(visibleTableColumnsSelector()); const theme = useTheme(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderCell.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderCell.tsx index 9d1f7f89a90b..ec96d4990ff2 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderCell.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderCell.tsx @@ -82,14 +82,13 @@ export const RecordTableHeaderCell = ({ column: ColumnDefinition; createRecord: () => void; }) => { - const { getResizeFieldOffsetState, getTableColumnsState } = - useRecordTableStates(); + const { resizeFieldOffsetState, tableColumnsState } = useRecordTableStates(); const [resizeFieldOffset, setResizeFieldOffset] = useRecoilState( - getResizeFieldOffsetState(), + resizeFieldOffsetState, ); - const tableColumns = useRecoilValue(getTableColumnsState()); + const tableColumns = useRecoilValue(tableColumnsState); const tableColumnsByKey = useMemo( () => mapArrayToObject(tableColumns, ({ fieldMetadataId }) => fieldMetadataId), @@ -124,7 +123,7 @@ export const RecordTableHeaderCell = ({ const resizeFieldOffset = getSnapshotValue( snapshot, - getResizeFieldOffsetState(), + resizeFieldOffsetState, ); const nextWidth = Math.round( @@ -134,7 +133,7 @@ export const RecordTableHeaderCell = ({ ), ); - set(getResizeFieldOffsetState(), 0); + set(resizeFieldOffsetState, 0); setInitialPointerPositionX(null); setResizedFieldKey(null); @@ -150,7 +149,7 @@ export const RecordTableHeaderCell = ({ }, [ resizedFieldKey, - getResizeFieldOffsetState, + resizeFieldOffsetState, tableColumnsByKey, tableColumns, handleColumnsChange, @@ -165,7 +164,7 @@ export const RecordTableHeaderCell = ({ }); const isMobile = useIsMobile(); - const scrollLeft = useRecoilValue(scrollLeftState()); + const scrollLeft = useRecoilValue(scrollLeftState); const disableColumnResize = column.isLabelIdentifier && isMobile && scrollLeft > 0; diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderPlusButtonContent.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderPlusButtonContent.tsx index b08140cea4a5..91b3f8527cc6 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderPlusButtonContent.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableHeaderPlusButtonContent.tsx @@ -17,9 +17,9 @@ import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; export const RecordTableHeaderPlusButtonContent = () => { const { closeDropdown } = useDropdown(); - const { getHiddenTableColumnsSelector } = useRecordTableStates(); + const { hiddenTableColumnsSelector } = useRecordTableStates(); - const hiddenTableColumns = useRecoilValue(getHiddenTableColumnsSelector()); + const hiddenTableColumns = useRecoilValue(hiddenTableColumnsSelector()); const { getIcon } = useIcons(); const { handleColumnVisibilityChange } = useTableColumns(); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx index b6af608f61c3..9f018f8f5cc8 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableRow.tsx @@ -23,12 +23,12 @@ const StyledTd = styled.td` `; export const RecordTableRow = ({ recordId, rowIndex }: RecordTableRowProps) => { - const { getVisibleTableColumnsSelector, isRowSelectedFamilyState } = + const { visibleTableColumnsSelector, isRowSelectedFamilyState } = useRecordTableStates(); const currentRowSelected = useRecoilValue(isRowSelectedFamilyState(recordId)); const { objectMetadataItem } = useContext(RecordTableContext); - const visibleTableColumns = useRecoilValue(getVisibleTableColumnsSelector()); + const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector()); const scrollWrapperRef = useContext(ScrollWrapperContext); diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx index 284941f750e3..cb071bfffd45 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/RecordTableWithWrappers.tsx @@ -20,7 +20,7 @@ import { } from '@/ui/layout/animated-placeholder/components/EmptyPlaceholderStyled'; import { DragSelect } from '@/ui/utilities/drag-select/components/DragSelect'; import { ScrollWrapper } from '@/ui/utilities/scroll/components/ScrollWrapper'; -import { useViewFields } from '@/views/hooks/internal/useViewFields'; +import { useSaveCurrentViewFields } from '@/views/hooks/useSaveCurrentViewFields'; import { mapColumnDefinitionsToViewFields } from '@/views/utils/mapColumnDefinitionToViewField'; import { RecordUpdateContext } from '../contexts/EntityUpdateMutationHookContext'; @@ -59,13 +59,13 @@ export const RecordTableWithWrappers = ({ }: RecordTableWithWrappersProps) => { const tableBodyRef = useRef(null); - const { getNumberOfTableRowsState, getIsRecordTableInitialLoadingState } = + const { numberOfTableRowsState, isRecordTableInitialLoadingState } = useRecordTableStates(recordTableId); - const numberOfTableRows = useRecoilValue(getNumberOfTableRowsState()); + const numberOfTableRows = useRecoilValue(numberOfTableRowsState); const isRecordTableInitialLoading = useRecoilValue( - getIsRecordTableInitialLoadingState(), + isRecordTableInitialLoadingState, ); const { resetTableRowSelection, setRowSelectedState } = useRecordTable({ @@ -78,11 +78,11 @@ export const RecordTableWithWrappers = ({ }, ); - const { persistViewFields } = useViewFields(viewBarId); + const { saveViewFields } = useSaveCurrentViewFields(viewBarId); const { deleteOneRecord } = useDeleteOneRecord({ objectNameSingular }); - const objectLabel = foundObjectMetadataItem?.nameSingular; + const objectLabel = foundObjectMetadataItem?.labelSingular; return ( @@ -96,13 +96,13 @@ export const RecordTableWithWrappers = ({ objectNameSingular={objectNameSingular} onColumnsChange={useRecoilCallback( () => (columns) => { - persistViewFields( + saveViewFields( mapColumnDefinitionsToViewFields( columns as ColumnDefinition[], ), ); }, - [persistViewFields], + [saveViewFields], )} createRecord={createRecord} /> diff --git a/packages/twenty-front/src/modules/object-record/record-table/components/SelectAllCheckbox.tsx b/packages/twenty-front/src/modules/object-record/record-table/components/SelectAllCheckbox.tsx index 9f9a46a70ffd..c5036f288878 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/components/SelectAllCheckbox.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/components/SelectAllCheckbox.tsx @@ -17,11 +17,9 @@ const StyledContainer = styled.div` `; export const SelectAllCheckbox = () => { - const { getAllRowsSelectedStatusSelector } = useRecordTableStates(); + const { allRowsSelectedStatusSelector } = useRecordTableStates(); - const allRowsSelectedStatus = useRecoilValue( - getAllRowsSelectedStatusSelector(), - ); + const allRowsSelectedStatus = useRecoilValue(allRowsSelectedStatusSelector()); const { selectAllRows } = useRecordTable(); const checked = allRowsSelectedStatus === 'all'; diff --git a/packages/twenty-front/src/modules/object-record/record-table/context-menu/components/RecordTableContextMenu.tsx b/packages/twenty-front/src/modules/object-record/record-table/context-menu/components/RecordTableContextMenu.tsx index 2ccbae769852..d9f712ef82e4 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/context-menu/components/RecordTableContextMenu.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/context-menu/components/RecordTableContextMenu.tsx @@ -8,9 +8,9 @@ export const RecordTableContextMenu = ({ }: { recordTableId: string; }) => { - const { getSelectedRowIdsSelector } = useRecordTableStates(recordTableId); + const { selectedRowIdsSelector } = useRecordTableStates(recordTableId); - const selectedRowIds = useRecoilValue(getSelectedRowIdsSelector()); + const selectedRowIds = useRecoilValue(selectedRowIdsSelector()); if (!selectedRowIds.length) { return null; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode.ts index d8c21c844051..a9976aa05fac 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useCloseCurrentTableCellInEditMode.ts @@ -5,7 +5,7 @@ import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotV export const useCloseCurrentTableCellInEditMode = (recordTableId?: string) => { const { - getCurrentTableCellInEditModePositionState, + currentTableCellInEditModePositionState, isTableCellInEditModeFamilyState, } = useRecordTableStates(recordTableId); @@ -14,7 +14,7 @@ export const useCloseCurrentTableCellInEditMode = (recordTableId?: string) => { return async () => { const currentTableCellInEditModePosition = getSnapshotValue( snapshot, - getCurrentTableCellInEditModePositionState(), + currentTableCellInEditModePositionState, ); set( @@ -23,9 +23,6 @@ export const useCloseCurrentTableCellInEditMode = (recordTableId?: string) => { ); }; }, - [ - getCurrentTableCellInEditModePositionState, - isTableCellInEditModeFamilyState, - ], + [currentTableCellInEditModePositionState, isTableCellInEditModeFamilyState], ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useDisableSoftFocus.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useDisableSoftFocus.ts index fed4211c976e..2e1704dfa5c5 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useDisableSoftFocus.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useDisableSoftFocus.ts @@ -5,8 +5,8 @@ import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotV export const useDisableSoftFocus = (recordTableId?: string) => { const { - getSoftFocusPositionState, - getIsSoftFocusActiveState, + softFocusPositionState, + isSoftFocusActiveState, isSoftFocusOnTableCellFamilyState, } = useRecordTableStates(recordTableId); @@ -15,17 +15,17 @@ export const useDisableSoftFocus = (recordTableId?: string) => { return () => { const currentPosition = getSnapshotValue( snapshot, - getSoftFocusPositionState(), + softFocusPositionState, ); - set(getIsSoftFocusActiveState(), false); + set(isSoftFocusActiveState, false); set(isSoftFocusOnTableCellFamilyState(currentPosition), false); }; }, [ - getIsSoftFocusActiveState, - getSoftFocusPositionState, + isSoftFocusActiveState, + softFocusPositionState, isSoftFocusOnTableCellFamilyState, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useGetIsSomeCellInEditMode.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useGetIsSomeCellInEditMode.ts index 8a4b486258e2..ecc422eecd1e 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useGetIsSomeCellInEditMode.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useGetIsSomeCellInEditMode.ts @@ -5,7 +5,7 @@ import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotV export const useGetIsSomeCellInEditModeState = (recordTableId?: string) => { const { - getCurrentTableCellInEditModePositionState, + currentTableCellInEditModePositionState, isTableCellInEditModeFamilyState, } = useRecordTableStates(recordTableId); @@ -14,7 +14,7 @@ export const useGetIsSomeCellInEditModeState = (recordTableId?: string) => { () => { const currentTableCellInEditModePosition = getSnapshotValue( snapshot, - getCurrentTableCellInEditModePositionState(), + currentTableCellInEditModePositionState, ); const isSomeCellInEditModeState = isTableCellInEditModeFamilyState( @@ -23,9 +23,6 @@ export const useGetIsSomeCellInEditModeState = (recordTableId?: string) => { return isSomeCellInEditModeState; }, - [ - getCurrentTableCellInEditModePositionState, - isTableCellInEditModeFamilyState, - ], + [currentTableCellInEditModePositionState, isTableCellInEditModeFamilyState], ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts index ff2e2b877c40..0ee3aa62d6a8 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useLeaveTableFocus.ts @@ -14,18 +14,18 @@ export const useLeaveTableFocus = (recordTableId?: string) => { const closeCurrentCellInEditMode = useCloseCurrentTableCellInEditMode(recordTableId); - const { getIsSoftFocusActiveState } = useRecordTableStates(recordTableId); + const { isSoftFocusActiveState } = useRecordTableStates(recordTableId); return useRecoilCallback( ({ snapshot }) => () => { const isSoftFocusActive = getSnapshotValue( snapshot, - getIsSoftFocusActiveState(), + isSoftFocusActiveState, ); const currentHotkeyScope = snapshot - .getLoadable(currentHotkeyScopeState()) + .getLoadable(currentHotkeyScopeState) .getValue(); if (!isSoftFocusActive) { @@ -39,6 +39,6 @@ export const useLeaveTableFocus = (recordTableId?: string) => { closeCurrentCellInEditMode(); disableSoftFocus(); }, - [closeCurrentCellInEditMode, disableSoftFocus, getIsSoftFocusActiveState], + [closeCurrentCellInEditMode, disableSoftFocus, isSoftFocusActiveState], ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useMoveEditModeToCellPosition.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useMoveEditModeToCellPosition.ts index 5bb32c4abe99..02e04a259d31 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useMoveEditModeToCellPosition.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useMoveEditModeToCellPosition.ts @@ -8,7 +8,7 @@ import { TableCellPosition } from '../../types/TableCellPosition'; export const useMoveEditModeToTableCellPosition = (recordTableId?: string) => { const { isTableCellInEditModeFamilyState, - getCurrentTableCellInEditModePositionState, + currentTableCellInEditModePositionState, } = useRecordTableStates(recordTableId); return useRecoilCallback( @@ -16,7 +16,7 @@ export const useMoveEditModeToTableCellPosition = (recordTableId?: string) => { return (newPosition: TableCellPosition) => { const currentTableCellInEditModePosition = getSnapshotValue( snapshot, - getCurrentTableCellInEditModePositionState(), + currentTableCellInEditModePositionState, ); set( @@ -24,14 +24,11 @@ export const useMoveEditModeToTableCellPosition = (recordTableId?: string) => { false, ); - set(getCurrentTableCellInEditModePositionState(), newPosition); + set(currentTableCellInEditModePositionState, newPosition); set(isTableCellInEditModeFamilyState(newPosition), true); }; }, - [ - getCurrentTableCellInEditModePositionState, - isTableCellInEditModeFamilyState, - ], + [currentTableCellInEditModePositionState, isTableCellInEditModeFamilyState], ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts index 21414ec2d703..1d7fff60f152 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useRecordTableStates.ts @@ -35,44 +35,41 @@ export const useRecordTableStates = (recordTableId?: string) => { return { scopeId, - getAvailableTableColumnsState: extractComponentState( + availableTableColumnsState: extractComponentState( availableTableColumnsComponentState, scopeId, ), - getTableFiltersState: extractComponentState( + tableFiltersState: extractComponentState( tableFiltersComponentState, scopeId, ), - getTableSortsState: extractComponentState( - tableSortsComponentState, - scopeId, - ), - getTableColumnsState: extractComponentState( + tableSortsState: extractComponentState(tableSortsComponentState, scopeId), + tableColumnsState: extractComponentState( tableColumnsComponentState, scopeId, ), - getOnColumnsChangeState: extractComponentState( + onColumnsChangeState: extractComponentState( onColumnsChangeComponentState, scopeId, ), - getOnEntityCountChangeState: extractComponentState( + onEntityCountChangeState: extractComponentState( onEntityCountChangeComponentState, scopeId, ), - getTableLastRowVisibleState: extractComponentState( + tableLastRowVisibleState: extractComponentState( tableLastRowVisibleComponentState, scopeId, ), - getSoftFocusPositionState: extractComponentState( + softFocusPositionState: extractComponentState( softFocusPositionComponentState, scopeId, ), - getNumberOfTableRowsState: extractComponentState( + numberOfTableRowsState: extractComponentState( numberOfTableRowsComponentState, scopeId, ), - getCurrentTableCellInEditModePositionState: extractComponentState( + currentTableCellInEditModePositionState: extractComponentState( currentTableCellInEditModePositionComponentState, scopeId, ), @@ -80,19 +77,16 @@ export const useRecordTableStates = (recordTableId?: string) => { isTableCellInEditModeComponentFamilyState, scopeId, ), - getIsSoftFocusActiveState: extractComponentState( + isSoftFocusActiveState: extractComponentState( isSoftFocusActiveComponentState, scopeId, ), - getTableRowIdsState: extractComponentState( - tableRowIdsComponentState, - scopeId, - ), - getIsRecordTableInitialLoadingState: extractComponentState( + tableRowIdsState: extractComponentState(tableRowIdsComponentState, scopeId), + isRecordTableInitialLoadingState: extractComponentState( isRecordTableInitialLoadingComponentState, scopeId, ), - getResizeFieldOffsetState: extractComponentState( + resizeFieldOffsetState: extractComponentState( resizeFieldOffsetComponentState, scopeId, ), @@ -104,23 +98,23 @@ export const useRecordTableStates = (recordTableId?: string) => { isRowSelectedComponentFamilyState, scopeId, ), - getAllRowsSelectedStatusSelector: extractComponentReadOnlySelector( + allRowsSelectedStatusSelector: extractComponentReadOnlySelector( allRowsSelectedStatusComponentSelector, scopeId, ), - getHiddenTableColumnsSelector: extractComponentReadOnlySelector( + hiddenTableColumnsSelector: extractComponentReadOnlySelector( hiddenTableColumnsComponentSelector, scopeId, ), - getNumberOfTableColumnsSelector: extractComponentReadOnlySelector( + numberOfTableColumnsSelector: extractComponentReadOnlySelector( numberOfTableColumnsComponentSelector, scopeId, ), - getSelectedRowIdsSelector: extractComponentReadOnlySelector( + selectedRowIdsSelector: extractComponentReadOnlySelector( selectedRowIdsComponentSelector, scopeId, ), - getVisibleTableColumnsSelector: extractComponentReadOnlySelector( + visibleTableColumnsSelector: extractComponentReadOnlySelector( visibleTableColumnsComponentSelector, scopeId, ), diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts index c722d40bb509..7e86f581909c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useResetTableRowSelection.ts @@ -4,18 +4,18 @@ import { useRecordTableStates } from '@/object-record/record-table/hooks/interna import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotValue'; export const useResetTableRowSelection = (recordTableId?: string) => { - const { getTableRowIdsState, isRowSelectedFamilyState } = + const { tableRowIdsState, isRowSelectedFamilyState } = useRecordTableStates(recordTableId); return useRecoilCallback( ({ snapshot, set }) => () => { - const tableRowIds = getSnapshotValue(snapshot, getTableRowIdsState()); + const tableRowIds = getSnapshotValue(snapshot, tableRowIdsState); for (const rowId of tableRowIds) { set(isRowSelectedFamilyState(rowId), false); } }, - [getTableRowIdsState, isRowSelectedFamilyState], + [tableRowIdsState, isRowSelectedFamilyState], ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSelectAllRows.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSelectAllRows.ts index 1a7e563c15d2..c76683e6ae87 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSelectAllRows.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSelectAllRows.ts @@ -5,8 +5,8 @@ import { getSnapshotValue } from '@/ui/utilities/recoil-scope/utils/getSnapshotV export const useSelectAllRows = (recordTableId?: string) => { const { - getAllRowsSelectedStatusSelector, - getTableRowIdsState, + allRowsSelectedStatusSelector, + tableRowIdsState, isRowSelectedFamilyState, } = useRecordTableStates(recordTableId); @@ -15,10 +15,10 @@ export const useSelectAllRows = (recordTableId?: string) => { () => { const allRowsSelectedStatus = getSnapshotValue( snapshot, - getAllRowsSelectedStatusSelector(), + allRowsSelectedStatusSelector(), ); - const tableRowIds = getSnapshotValue(snapshot, getTableRowIdsState()); + const tableRowIds = getSnapshotValue(snapshot, tableRowIdsState); if ( allRowsSelectedStatus === 'none' || @@ -33,11 +33,7 @@ export const useSelectAllRows = (recordTableId?: string) => { } } }, - [ - getAllRowsSelectedStatusSelector, - getTableRowIdsState, - isRowSelectedFamilyState, - ], + [allRowsSelectedStatusSelector, tableRowIdsState, isRowSelectedFamilyState], ); return { diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts index 01fa47fe3b39..32c1986ec42d 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetRecordTableData.ts @@ -14,7 +14,7 @@ export const useSetRecordTableData = ({ recordTableId, onEntityCountChange, }: useSetRecordTableDataProps) => { - const { getTableRowIdsState, getNumberOfTableRowsState } = + const { tableRowIdsState, numberOfTableRowsState } = useRecordTableStates(recordTableId); return useRecoilCallback( @@ -30,17 +30,17 @@ export const useSetRecordTableData = ({ set(recordStoreFamilyState(entity.id), entity); } } - const currentRowIds = getSnapshotValue(snapshot, getTableRowIdsState()); + const currentRowIds = getSnapshotValue(snapshot, tableRowIdsState); const entityIds = newEntityArray.map((entity) => entity.id); if (!isDeeplyEqual(currentRowIds, entityIds)) { - set(getTableRowIdsState(), entityIds); + set(tableRowIdsState, entityIds); } - set(getNumberOfTableRowsState(), totalCount); + set(numberOfTableRowsState, totalCount); onEntityCountChange(totalCount); }, - [getNumberOfTableRowsState, getTableRowIdsState, onEntityCountChange], + [numberOfTableRowsState, tableRowIdsState, onEntityCountChange], ); }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetSoftFocusPosition.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetSoftFocusPosition.ts index 5c0408c625e3..edf8f7a904e0 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetSoftFocusPosition.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/internal/useSetSoftFocusPosition.ts @@ -7,8 +7,8 @@ import { TableCellPosition } from '../../types/TableCellPosition'; export const useSetSoftFocusPosition = (recordTableId?: string) => { const { - getSoftFocusPositionState, - getIsSoftFocusActiveState, + softFocusPositionState, + isSoftFocusActiveState, isSoftFocusOnTableCellFamilyState, } = useRecordTableStates(recordTableId); @@ -17,21 +17,21 @@ export const useSetSoftFocusPosition = (recordTableId?: string) => { return (newPosition: TableCellPosition) => { const currentPosition = getSnapshotValue( snapshot, - getSoftFocusPositionState(), + softFocusPositionState, ); - set(getIsSoftFocusActiveState(), true); + set(isSoftFocusActiveState, true); set(isSoftFocusOnTableCellFamilyState(currentPosition), false); - set(getSoftFocusPositionState(), newPosition); + set(softFocusPositionState, newPosition); set(isSoftFocusOnTableCellFamilyState(newPosition), true); }; }, [ - getSoftFocusPositionState, - getIsSoftFocusActiveState, + softFocusPositionState, + isSoftFocusActiveState, isSoftFocusOnTableCellFamilyState, ], ); diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts index a530bb63533b..e876c51844a1 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTable.ts @@ -32,51 +32,49 @@ export const useRecordTable = (props?: useRecordTableProps) => { const { scopeId, - getAvailableTableColumnsState, - getTableFiltersState, - getTableSortsState, - getTableColumnsState, - getOnEntityCountChangeState, - getOnColumnsChangeState, - getIsRecordTableInitialLoadingState, - getTableLastRowVisibleState, - getSelectedRowIdsSelector, + availableTableColumnsState, + tableFiltersState, + tableSortsState, + tableColumnsState, + onEntityCountChangeState, + onColumnsChangeState, + isRecordTableInitialLoadingState, + tableLastRowVisibleState, + selectedRowIdsSelector, } = useRecordTableStates(recordTableId); const setAvailableTableColumns = useRecoilCallback( ({ snapshot, set }) => (columns: ColumnDefinition[]) => { - const availableTableColumnsState = getSnapshotValue( + const availableTableColumns = getSnapshotValue( snapshot, - getAvailableTableColumnsState(), + availableTableColumnsState, ); - if (isDeeplyEqual(availableTableColumnsState, columns)) { + if (isDeeplyEqual(availableTableColumns, columns)) { return; } - set(getAvailableTableColumnsState(), columns); + set(availableTableColumnsState, columns); }, - [getAvailableTableColumnsState], + [availableTableColumnsState], ); - const setOnEntityCountChange = useSetRecoilState( - getOnEntityCountChangeState(), - ); + const setOnEntityCountChange = useSetRecoilState(onEntityCountChangeState); - const setTableFilters = useSetRecoilState(getTableFiltersState()); + const setTableFilters = useSetRecoilState(tableFiltersState); - const setTableSorts = useSetRecoilState(getTableSortsState()); + const setTableSorts = useSetRecoilState(tableSortsState); - const setTableColumns = useSetRecoilState(getTableColumnsState()); + const setTableColumns = useSetRecoilState(tableColumnsState); - const setOnColumnsChange = useSetRecoilState(getOnColumnsChangeState()); + const setOnColumnsChange = useSetRecoilState(onColumnsChangeState); const setIsRecordTableInitialLoading = useSetRecoilState( - getIsRecordTableInitialLoadingState(), + isRecordTableInitialLoadingState, ); const setRecordTableLastRowVisible = useSetRecoilState( - getTableLastRowVisibleState(), + tableLastRowVisibleState, ); const onColumnsChange = useRecoilCallback( @@ -84,12 +82,12 @@ export const useRecordTable = (props?: useRecordTableProps) => { (columns: ColumnDefinition[]) => { const onColumnsChange = getSnapshotValue( snapshot, - getOnColumnsChangeState(), + onColumnsChangeState, ); onColumnsChange?.(columns); }, - [getOnColumnsChangeState], + [onColumnsChangeState], ); const onEntityCountChange = useRecoilCallback( @@ -97,12 +95,12 @@ export const useRecordTable = (props?: useRecordTableProps) => { (count: number) => { const onEntityCountChange = getSnapshotValue( snapshot, - getOnEntityCountChangeState(), + onEntityCountChangeState, ); onEntityCountChange?.(count); }, - [getOnEntityCountChangeState], + [onEntityCountChangeState], ); const setRecordTableData = useSetRecordTableData({ @@ -116,7 +114,7 @@ export const useRecordTable = (props?: useRecordTableProps) => { const resetTableRowSelection = useResetTableRowSelection(recordTableId); - const upsertRecordTableItem = useUpsertRecordFromState(); + const upsertRecordTableItem = useUpsertRecordFromState; const setSoftFocusPosition = useSetSoftFocusPosition(recordTableId); @@ -128,7 +126,7 @@ export const useRecordTable = (props?: useRecordTableProps) => { const setHotkeyScope = useSetHotkeyScope(); const setIsSoftFocusUsingMouseState = useSetRecoilState( - isSoftFocusUsingMouseState(), + isSoftFocusUsingMouseState, ); useScopedHotkeys( @@ -212,6 +210,6 @@ export const useRecordTable = (props?: useRecordTableProps) => { setRecordTableLastRowVisible, setSoftFocusPosition, isSomeCellInEditModeState, - getSelectedRowIdsSelector, + selectedRowIdsSelector, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts index 7c5de53e7045..2291659af820 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useRecordTableMoveFocus.ts @@ -8,10 +8,10 @@ import { useSetSoftFocusPosition } from './internal/useSetSoftFocusPosition'; export const useRecordTableMoveFocus = (recordTableId?: string) => { const { scopeId, - getSoftFocusPositionState, - getNumberOfTableRowsState, - getNumberOfTableColumnsSelector, - getSelectedRowIdsSelector, + softFocusPositionState, + numberOfTableRowsState, + numberOfTableColumnsSelector, + selectedRowIdsSelector, } = useRecordTableStates(recordTableId); const setSoftFocusPosition = useSetSoftFocusPosition(recordTableId); @@ -21,7 +21,7 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { () => { const softFocusPosition = getSnapshotValue( snapshot, - getSoftFocusPositionState(), + softFocusPositionState, ); let newRowNumber = softFocusPosition.row - 1; @@ -35,7 +35,7 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { row: newRowNumber, }); }, - [getSoftFocusPositionState, setSoftFocusPosition], + [softFocusPositionState, setSoftFocusPosition], ); const moveDown = useRecoilCallback( @@ -43,12 +43,12 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { () => { const softFocusPosition = getSnapshotValue( snapshot, - getSoftFocusPositionState(), + softFocusPositionState, ); const numberOfTableRows = getSnapshotValue( snapshot, - getNumberOfTableRowsState(), + numberOfTableRowsState, ); let newRowNumber = softFocusPosition.row + 1; @@ -62,11 +62,7 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { row: newRowNumber, }); }, - [ - getNumberOfTableRowsState, - setSoftFocusPosition, - getSoftFocusPositionState, - ], + [numberOfTableRowsState, setSoftFocusPosition, softFocusPositionState], ); const moveRight = useRecoilCallback( @@ -74,17 +70,17 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { () => { const softFocusPosition = getSnapshotValue( snapshot, - getSoftFocusPositionState(), + softFocusPositionState, ); const numberOfTableColumns = getSnapshotValue( snapshot, - getNumberOfTableColumnsSelector(), + numberOfTableColumnsSelector(), ); const numberOfTableRows = getSnapshotValue( snapshot, - getNumberOfTableRowsState(), + numberOfTableRowsState, ); const currentColumnNumber = softFocusPosition.column; const currentRowNumber = softFocusPosition.row; @@ -117,9 +113,9 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { } }, [ - getSoftFocusPositionState, - getNumberOfTableColumnsSelector, - getNumberOfTableRowsState, + softFocusPositionState, + numberOfTableColumnsSelector, + numberOfTableRowsState, setSoftFocusPosition, ], ); @@ -129,12 +125,12 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { () => { const softFocusPosition = getSnapshotValue( snapshot, - getSoftFocusPositionState(), + softFocusPositionState, ); const numberOfTableColumns = getSnapshotValue( snapshot, - getNumberOfTableColumnsSelector(), + numberOfTableColumnsSelector(), ); const currentColumnNumber = softFocusPosition.column; @@ -165,8 +161,8 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { } }, [ - getNumberOfTableColumnsSelector, - getSoftFocusPositionState, + numberOfTableColumnsSelector, + softFocusPositionState, setSoftFocusPosition, ], ); @@ -178,6 +174,6 @@ export const useRecordTableMoveFocus = (recordTableId?: string) => { moveRight, moveUp, setSoftFocusPosition, - getSelectedRowIdsSelector, + selectedRowIdsSelector, }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts b/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts index 1cc9fa344416..534db9462588 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/hooks/useTableColumns.ts @@ -18,15 +18,15 @@ export const useTableColumns = (props?: useRecordTableProps) => { }); const { - getAvailableTableColumnsState, - getTableColumnsState, - getVisibleTableColumnsSelector, + availableTableColumnsState, + tableColumnsState, + visibleTableColumnsSelector, } = useRecordTableStates(props?.recordTableId); - const availableTableColumns = useRecoilValue(getAvailableTableColumnsState()); + const availableTableColumns = useRecoilValue(availableTableColumnsState); - const tableColumns = useRecoilValue(getTableColumnsState()); - const visibleTableColumns = useRecoilValue(getVisibleTableColumnsSelector()); + const tableColumns = useRecoilValue(tableColumnsState); + const visibleTableColumns = useRecoilValue(visibleTableColumnsSelector()); const { handleColumnMove } = useMoveViewColumns(); @@ -47,6 +47,11 @@ export const useTableColumns = (props?: useRecordTableProps) => { (tableColumns) => tableColumns.fieldMetadataId === viewField.fieldMetadataId, ); + const lastTableColumnPosition = [...tableColumns] + .sort((a, b) => b.position - a.position) + .map((column) => column.position); + + const lastPosition = lastTableColumnPosition[0] ?? 0; if (isNewColumn) { const newColumn = availableTableColumns.find( @@ -57,7 +62,7 @@ export const useTableColumns = (props?: useRecordTableProps) => { const nextColumns = [ ...tableColumns, - { ...newColumn, isVisible: true }, + { ...newColumn, isVisible: true, position: lastPosition + 1 }, ]; await handleColumnsChange(nextColumns); diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx index b5ac47aafccb..344988c941af 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellContainer.tsx @@ -64,7 +64,7 @@ export const RecordTableCellContainer = ({ const isSomeCellInEditMode = useRecoilValue(isSomeCellInEditModeState()); const setIsSoftFocusUsingMouseState = useSetRecoilState( - isSoftFocusUsingMouseState(), + isSoftFocusUsingMouseState, ); const moveSoftFocusToCurrentCellOnHover = diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx index b75d36e29a46..7f10d98ba72a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/components/RecordTableCellSoftFocusMode.tsx @@ -29,7 +29,7 @@ export const RecordTableCellSoftFocusMode = ({ const toggleEditOnlyInput = useToggleEditOnlyInput(); const scrollRef = useRef(null); - const isSoftFocusUsingMouse = useRecoilValue(isSoftFocusUsingMouseState()); + const isSoftFocusUsingMouse = useRecoilValue(isSoftFocusUsingMouseState); const clearField = useClearField(); useEffect(() => { diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useCloseRecordTableCell.test.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useCloseRecordTableCell.test.tsx index c608e921e635..7e6dfb6f501c 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useCloseRecordTableCell.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useCloseRecordTableCell.test.tsx @@ -55,11 +55,11 @@ describe('useCloseRecordTableCell', () => { const { result } = renderHook( () => { const { - getCurrentTableCellInEditModePositionState, + currentTableCellInEditModePositionState, isTableCellInEditModeFamilyState, } = useRecordTableStates(); const currentTableCellInEditModePosition = useRecoilValue( - getCurrentTableCellInEditModePositionState(), + currentTableCellInEditModePositionState, ); const isTableCellInEditMode = useRecoilValue( isTableCellInEditModeFamilyState(currentTableCellInEditModePosition), diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useMoveSoftFocusToCurrentCellOnHover.test.tsx b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useMoveSoftFocusToCurrentCellOnHover.test.tsx index 1ebad63a8fe2..b7946416501a 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useMoveSoftFocusToCurrentCellOnHover.test.tsx +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/__tests__/useMoveSoftFocusToCurrentCellOnHover.test.tsx @@ -65,11 +65,11 @@ jest.mock( '@/object-record/record-table/hooks/internal/useRecordTableStates', () => ({ useRecordTableStates: () => ({ - getSoftFocusPositionState: () => mockSoftFocusPositionState, - getIsSoftFocusActiveState: () => mockSoftFocusActiveState, + softFocusPositionState: mockSoftFocusPositionState, + isSoftFocusActiveState: mockSoftFocusActiveState, isSoftFocusOnTableCellFamilyState: () => mockIsSoftFocusOnTableCellFamilyState, - getCurrentTableCellInEditModePositionState: () => + currentTableCellInEditModePositionState: mockCurrentTableCellInEditModePositionState, isTableCellInEditModeFamilyState: () => mockIsTableCellInEditModeFamilyState, diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCurrentCellOnHover.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCurrentCellOnHover.ts index 0ddc6fe6a116..3d53b8d9b0c9 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCurrentCellOnHover.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useMoveSoftFocusToCurrentCellOnHover.ts @@ -12,7 +12,7 @@ export const useMoveSoftFocusToCurrentCellOnHover = () => { const setSoftFocusOnCurrentTableCell = useSetSoftFocusOnCurrentTableCell(); const { - getCurrentTableCellInEditModePositionState, + currentTableCellInEditModePositionState, isTableCellInEditModeFamilyState, } = useRecordTableStates(); @@ -21,7 +21,7 @@ export const useMoveSoftFocusToCurrentCellOnHover = () => { () => { const currentTableCellInEditModePosition = getSnapshotValue( snapshot, - getCurrentTableCellInEditModePositionState(), + currentTableCellInEditModePositionState, ); const isSomeCellInEditMode = snapshot @@ -33,7 +33,7 @@ export const useMoveSoftFocusToCurrentCellOnHover = () => { .getValue(); const currentHotkeyScope = snapshot - .getLoadable(currentHotkeyScopeState()) + .getLoadable(currentHotkeyScopeState) .getValue(); if ( @@ -49,7 +49,7 @@ export const useMoveSoftFocusToCurrentCellOnHover = () => { } }, [ - getCurrentTableCellInEditModePositionState, + currentTableCellInEditModePositionState, isTableCellInEditModeFamilyState, setSoftFocusOnCurrentTableCell, ], diff --git a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useSetSoftFocus.ts b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useSetSoftFocus.ts index 9c2ef5f73a14..9b630f4e2f36 100644 --- a/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useSetSoftFocus.ts +++ b/packages/twenty-front/src/modules/object-record/record-table/record-table-cell/hooks/useSetSoftFocus.ts @@ -10,7 +10,7 @@ import { TableHotkeyScope } from '../../types/TableHotkeyScope'; export const useSetSoftFocus = () => { const setSoftFocusPosition = useSetSoftFocusPosition(); - const { getIsSoftFocusActiveState } = useRecordTableStates(); + const { isSoftFocusActiveState } = useRecordTableStates(); const setHotkeyScope = useSetHotkeyScope(); @@ -19,10 +19,10 @@ export const useSetSoftFocus = () => { (newPosition: TableCellPosition) => { setSoftFocusPosition(newPosition); - set(getIsSoftFocusActiveState(), true); + set(isSoftFocusActiveState, true); setHotkeyScope(TableHotkeyScope.TableSoftFocus); }, - [setSoftFocusPosition, getIsSoftFocusActiveState, setHotkeyScope], + [setSoftFocusPosition, isSoftFocusActiveState, setHotkeyScope], ); }; diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.test.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.test.tsx index 92661b8e6113..decadcd2e519 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.test.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray.test.tsx @@ -58,7 +58,7 @@ describe('useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray' }, }, ), - setObjectMetadata: useSetRecoilState(objectMetadataItemsState()), + setObjectMetadata: useSetRecoilState(objectMetadataItemsState), }; }, { diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearch.test.tsx b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearch.test.tsx index 9ce41b117496..fa85d8629076 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearch.test.tsx +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/__tests__/useMultiObjectSearch.test.tsx @@ -13,7 +13,7 @@ const query = gql` $filterNameSingular: NameSingularFilterInput $orderByNameSingular: NameSingularOrderByInput $lastCursorNameSingular: String - $limitNameSingular: Float = 5 + $limitNameSingular: Float ) { namePlural( filter: $filterNameSingular @@ -33,6 +33,7 @@ const query = gql` startCursor endCursor } + totalCount } } `; @@ -104,7 +105,7 @@ describe('useMultiObjectSearch', () => { }, ], }), - setObjectMetadata: useSetRecoilState(objectMetadataItemsState()), + setObjectMetadata: useSetRecoilState(objectMetadataItemsState), }), { wrapper: Wrapper, diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery.ts index 2da828779022..3f91947262b4 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery.ts @@ -25,7 +25,7 @@ export const useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery = ({ searchFilterValue: string; limit?: number; }) => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState()); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const { searchFilterPerMetadataItemNameSingular } = useSearchFilterPerMetadataItem({ @@ -85,7 +85,7 @@ export const useMultiObjectSearchMatchesSearchFilterAndSelectedItemsQuery = ({ const multiSelectQueryForSelectedIds = useGenerateFindManyRecordsForMultipleMetadataItemsQuery({ - objectMetadataItems: objectMetadataItemsUsedInSelectedIdsQuery, + targetObjectMetadataItems: objectMetadataItemsUsedInSelectedIdsQuery, depth: 0, }); diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery.ts index 16ce8e1cf3b3..675dd938df0c 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery.ts +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchMatchesSearchFilterAndToSelectQuery.ts @@ -28,7 +28,7 @@ export const useMultiObjectSearchMatchesSearchFilterAndToSelectQuery = ({ searchFilterValue: string; limit?: number; }) => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState()); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const nonSystemObjectMetadataItems = objectMetadataItems.filter( ({ nameSingular, isSystem }) => @@ -86,7 +86,7 @@ export const useMultiObjectSearchMatchesSearchFilterAndToSelectQuery = ({ const multiSelectQuery = useGenerateFindManyRecordsForMultipleMetadataItemsQuery({ - objectMetadataItems: nonSystemObjectMetadataItems, + targetObjectMetadataItems: nonSystemObjectMetadataItems, depth: 0, }); diff --git a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchSelectedItemsQuery.ts b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchSelectedItemsQuery.ts index a390c13f7ca3..0cfdaebf79b2 100644 --- a/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchSelectedItemsQuery.ts +++ b/packages/twenty-front/src/modules/object-record/relation-picker/hooks/useMultiObjectSearchSelectedItemsQuery.ts @@ -26,7 +26,7 @@ export const useMultiObjectSearchSelectedItemsQuery = ({ }: { selectedObjectRecordIds: SelectedObjectRecordId[]; }) => { - const objectMetadataItems = useRecoilValue(objectMetadataItemsState()); + const objectMetadataItems = useRecoilValue(objectMetadataItemsState); const objectMetadataItemsUsedInSelectedIdsQuery = objectMetadataItems.filter( ({ nameSingular }) => { @@ -69,7 +69,7 @@ export const useMultiObjectSearchSelectedItemsQuery = ({ const multiSelectQueryForSelectedIds = useGenerateFindManyRecordsForMultipleMetadataItemsQuery({ - objectMetadataItems: objectMetadataItemsUsedInSelectedIdsQuery, + targetObjectMetadataItems: objectMetadataItemsUsedInSelectedIdsQuery, }); const { diff --git a/packages/twenty-front/src/modules/object-record/spreadsheet-import/__tests__/useSpreadsheetRecordImport.test.tsx b/packages/twenty-front/src/modules/object-record/spreadsheet-import/__tests__/useSpreadsheetRecordImport.test.tsx index 6e400e579d52..a925b36c8c74 100644 --- a/packages/twenty-front/src/modules/object-record/spreadsheet-import/__tests__/useSpreadsheetRecordImport.test.tsx +++ b/packages/twenty-front/src/modules/object-record/spreadsheet-import/__tests__/useSpreadsheetRecordImport.test.tsx @@ -92,7 +92,7 @@ describe('useSpreadsheetCompanyImport', () => { it('should work as expected', async () => { const { result } = renderHook( () => { - const spreadsheetImport = useRecoilValue(spreadsheetImportState()); + const spreadsheetImport = useRecoilValue(spreadsheetImportState); const { openRecordSpreadsheetImport } = useSpreadsheetRecordImport( CoreObjectNameSingular.Company, ); diff --git a/packages/twenty-front/src/modules/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata.ts b/packages/twenty-front/src/modules/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata.ts index 5af6540aecbb..8e70a4e5c0cb 100644 --- a/packages/twenty-front/src/modules/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata.ts +++ b/packages/twenty-front/src/modules/object-record/utils/computeRecordBoardColumnDefinitionsFromObjectMetadata.ts @@ -5,10 +5,13 @@ import { FieldMetadataType } from '~/generated-metadata/graphql'; export const computeRecordBoardColumnDefinitionsFromObjectMetadata = ( objectMetadataItem: ObjectMetadataItem, + kanbanFieldMetadataId: string, navigateToSelectSettings: () => void, ): RecordBoardColumnDefinition[] => { const selectFieldMetadataItem = objectMetadataItem.fields.find( - (field) => field.type === FieldMetadataType.Select, + (field) => + field.id === kanbanFieldMetadataId && + field.type === FieldMetadataType.Select, ); if (!selectFieldMetadataItem) { diff --git a/packages/twenty-front/src/modules/object-record/utils/filterAvailableTableColumns.ts b/packages/twenty-front/src/modules/object-record/utils/filterAvailableTableColumns.ts index 176ccbad2867..bb7cc3578b3d 100644 --- a/packages/twenty-front/src/modules/object-record/utils/filterAvailableTableColumns.ts +++ b/packages/twenty-front/src/modules/object-record/utils/filterAvailableTableColumns.ts @@ -16,12 +16,5 @@ export const filterAvailableTableColumns = ( return false; } - if ( - isFieldRelation(columnDefinition) && - columnDefinition.metadata?.fieldName === 'pipelineStep' - ) { - return false; - } - return true; }; diff --git a/packages/twenty-front/src/modules/prefetch/components/PrefetchDataProvider.tsx b/packages/twenty-front/src/modules/prefetch/components/PrefetchDataProvider.tsx new file mode 100644 index 000000000000..837d2498e576 --- /dev/null +++ b/packages/twenty-front/src/modules/prefetch/components/PrefetchDataProvider.tsx @@ -0,0 +1,12 @@ +import React from 'react'; + +import { PrefetchRunQueriesEffect } from '@/prefetch/components/PrefetchRunQueriesEffect'; + +export const PrefetchDataProvider = ({ children }: React.PropsWithChildren) => { + return ( + <> + + {children} + + ); +}; diff --git a/packages/twenty-front/src/modules/prefetch/components/PrefetchRunQueriesEffect.tsx b/packages/twenty-front/src/modules/prefetch/components/PrefetchRunQueriesEffect.tsx new file mode 100644 index 000000000000..0554b0388037 --- /dev/null +++ b/packages/twenty-front/src/modules/prefetch/components/PrefetchRunQueriesEffect.tsx @@ -0,0 +1,60 @@ +import { useEffect } from 'react'; +import { useQuery } from '@apollo/client'; +import { useRecoilValue } from 'recoil'; + +import { currentUserState } from '@/auth/states/currentUserState'; +import { EMPTY_QUERY } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useGenerateFindManyRecordsForMultipleMetadataItemsQuery } from '@/object-record/hooks/useGenerateFindManyRecordsForMultipleMetadataItemsQuery'; +import { MultiObjectRecordQueryResult } from '@/object-record/relation-picker/hooks/useMultiObjectRecordsQueryResultFormattedAsObjectRecordForSelectArray'; +import { usePrefetchRunQuery } from '@/prefetch/hooks/internal/usePrefetchRunQuery'; +import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; +import { isDefined } from '~/utils/isDefined'; + +export const PrefetchRunQueriesEffect = () => { + const currentUser = useRecoilValue(currentUserState); + + const { + objectMetadataItem: objectMetadataItemView, + upsertRecordsInCache: upsertViewsInCache, + } = usePrefetchRunQuery({ + prefetchKey: PrefetchKey.AllViews, + objectNameSingular: CoreObjectNameSingular.View, + }); + + const { + objectMetadataItem: objectMetadataItemFavorite, + upsertRecordsInCache: upsertFavoritesInCache, + } = usePrefetchRunQuery({ + prefetchKey: PrefetchKey.AllFavorites, + objectNameSingular: CoreObjectNameSingular.Favorite, + }); + + const prefetchFindManyQuery = + useGenerateFindManyRecordsForMultipleMetadataItemsQuery({ + targetObjectMetadataItems: [ + objectMetadataItemView, + objectMetadataItemFavorite, + ], + depth: 2, + }); + + const { data } = useQuery( + prefetchFindManyQuery ?? EMPTY_QUERY, + { + skip: !currentUser, + }, + ); + + useEffect(() => { + if (isDefined(data?.views)) { + upsertViewsInCache(data.views); + } + + if (isDefined(data?.favorites)) { + upsertFavoritesInCache(data.favorites); + } + }, [data, upsertViewsInCache, upsertFavoritesInCache]); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/prefetch/constants/PrefetchConfig.ts b/packages/twenty-front/src/modules/prefetch/constants/PrefetchConfig.ts new file mode 100644 index 000000000000..a79631158314 --- /dev/null +++ b/packages/twenty-front/src/modules/prefetch/constants/PrefetchConfig.ts @@ -0,0 +1,9 @@ +import { QueryKey } from '@/object-record/query-keys/types/QueryKey'; +import { ALL_FAVORITES_QUERY_KEY } from '@/prefetch/query-keys/AllFavoritesQueryKey'; +import { ALL_VIEWS_QUERY_KEY } from '@/prefetch/query-keys/AllViewsQueryKey'; +import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; + +export const PREFETCH_CONFIG: Record = { + ALL_VIEWS: ALL_VIEWS_QUERY_KEY, + ALL_FAVORITES: ALL_FAVORITES_QUERY_KEY, +}; diff --git a/packages/twenty-front/src/modules/prefetch/hooks/internal/usePrefetchRunQuery.ts b/packages/twenty-front/src/modules/prefetch/hooks/internal/usePrefetchRunQuery.ts new file mode 100644 index 000000000000..294c138a7553 --- /dev/null +++ b/packages/twenty-front/src/modules/prefetch/hooks/internal/usePrefetchRunQuery.ts @@ -0,0 +1,55 @@ +import { useSetRecoilState } from 'recoil'; + +import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem'; +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { useUpsertFindManyRecordsQueryInCache } from '@/object-record/cache/hooks/useUpsertFindManyRecordsQueryInCache'; +import { useMapConnectionToRecords } from '@/object-record/hooks/useMapConnectionToRecords'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { ObjectRecordConnection } from '@/object-record/types/ObjectRecordConnection'; +import { ALL_VIEWS_QUERY_KEY } from '@/prefetch/query-keys/AllViewsQueryKey'; +import { prefetchIsLoadedFamilyState } from '@/prefetch/states/prefetchIsLoadedFamilyState'; +import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; + +export type UsePrefetchRunQuery = { + prefetchKey: PrefetchKey; + objectNameSingular: CoreObjectNameSingular; +}; + +export const usePrefetchRunQuery = ({ + prefetchKey, + objectNameSingular, +}: UsePrefetchRunQuery) => { + const setPrefetchDataIsLoadedLoaded = useSetRecoilState( + prefetchIsLoadedFamilyState(prefetchKey), + ); + const { objectMetadataItem } = useObjectMetadataItem({ + objectNameSingular: objectNameSingular, + }); + + const { upsertFindManyRecordsQueryInCache } = + useUpsertFindManyRecordsQueryInCache({ + objectMetadataItem: objectMetadataItem, + }); + + const mapConnectionToRecords = useMapConnectionToRecords(); + + const upsertRecordsInCache = (records: ObjectRecordConnection) => { + upsertFindManyRecordsQueryInCache({ + queryVariables: ALL_VIEWS_QUERY_KEY.variables, + depth: ALL_VIEWS_QUERY_KEY.depth, + objectRecordsToOverwrite: + mapConnectionToRecords({ + objectRecordConnection: records, + objectNameSingular: CoreObjectNameSingular.View, + depth: 2, + }) ?? [], + }); + setPrefetchDataIsLoadedLoaded(true); + }; + + return { + objectMetadataItem, + setPrefetchDataIsLoadedLoaded, + upsertRecordsInCache, + }; +}; diff --git a/packages/twenty-front/src/modules/prefetch/hooks/usePrefetchedData.ts b/packages/twenty-front/src/modules/prefetch/hooks/usePrefetchedData.ts new file mode 100644 index 000000000000..3997bae7318d --- /dev/null +++ b/packages/twenty-front/src/modules/prefetch/hooks/usePrefetchedData.ts @@ -0,0 +1,27 @@ +import { useRecoilValue } from 'recoil'; + +import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords'; +import { ObjectRecord } from '@/object-record/types/ObjectRecord'; +import { PREFETCH_CONFIG } from '@/prefetch/constants/PrefetchConfig'; +import { prefetchIsLoadedFamilyState } from '@/prefetch/states/prefetchIsLoadedFamilyState'; +import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; + +export const usePrefetchedData = ( + prefetchKey: PrefetchKey, +) => { + const isDataPrefetched = useRecoilValue( + prefetchIsLoadedFamilyState(prefetchKey), + ); + const prefetchQueryKey = PREFETCH_CONFIG[prefetchKey]; + + const { records } = useFindManyRecords({ + skip: !isDataPrefetched, + ...prefetchQueryKey, + useRecordsWithoutConnection: true, + }); + + return { + isDataPrefetched, + records, + }; +}; diff --git a/packages/twenty-front/src/modules/prefetch/query-keys/AllFavoritesQueryKey.ts b/packages/twenty-front/src/modules/prefetch/query-keys/AllFavoritesQueryKey.ts new file mode 100644 index 000000000000..a5e442715881 --- /dev/null +++ b/packages/twenty-front/src/modules/prefetch/query-keys/AllFavoritesQueryKey.ts @@ -0,0 +1,8 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { QueryKey } from '@/object-record/query-keys/types/QueryKey'; + +export const ALL_FAVORITES_QUERY_KEY: QueryKey = { + objectNameSingular: CoreObjectNameSingular.Favorite, + variables: {}, + depth: 1, +}; diff --git a/packages/twenty-front/src/modules/prefetch/query-keys/AllViewsQueryKey.ts b/packages/twenty-front/src/modules/prefetch/query-keys/AllViewsQueryKey.ts new file mode 100644 index 000000000000..df1c78163048 --- /dev/null +++ b/packages/twenty-front/src/modules/prefetch/query-keys/AllViewsQueryKey.ts @@ -0,0 +1,8 @@ +import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular'; +import { QueryKey } from '@/object-record/query-keys/types/QueryKey'; + +export const ALL_VIEWS_QUERY_KEY: QueryKey = { + objectNameSingular: CoreObjectNameSingular.View, + variables: {}, + depth: 1, +}; diff --git a/packages/twenty-front/src/modules/prefetch/states/prefetchIsLoadedFamilyState.ts b/packages/twenty-front/src/modules/prefetch/states/prefetchIsLoadedFamilyState.ts new file mode 100644 index 000000000000..38069ee05801 --- /dev/null +++ b/packages/twenty-front/src/modules/prefetch/states/prefetchIsLoadedFamilyState.ts @@ -0,0 +1,10 @@ +import { PrefetchKey } from '@/prefetch/types/PrefetchKey'; +import { createFamilyState } from '@/ui/utilities/state/utils/createFamilyState'; + +export const prefetchIsLoadedFamilyState = createFamilyState< + boolean, + PrefetchKey +>({ + key: 'prefetchIsLoadedFamilyState', + defaultValue: false, +}); diff --git a/packages/twenty-front/src/modules/prefetch/types/PrefetchKey.ts b/packages/twenty-front/src/modules/prefetch/types/PrefetchKey.ts new file mode 100644 index 000000000000..f7ebf434e916 --- /dev/null +++ b/packages/twenty-front/src/modules/prefetch/types/PrefetchKey.ts @@ -0,0 +1,4 @@ +export enum PrefetchKey { + AllViews = 'ALL_VIEWS', + AllFavorites = 'ALL_FAVORITES', +} diff --git a/packages/twenty-front/src/modules/search/hooks/__mocks__/useFilteredSearchEntityQuery.ts b/packages/twenty-front/src/modules/search/hooks/__mocks__/useFilteredSearchEntityQuery.ts index 39201e66a8fc..cb1daf98147c 100644 --- a/packages/twenty-front/src/modules/search/hooks/__mocks__/useFilteredSearchEntityQuery.ts +++ b/packages/twenty-front/src/modules/search/hooks/__mocks__/useFilteredSearchEntityQuery.ts @@ -24,7 +24,7 @@ export const query = gql` pointOfContactId updatedAt companyId - pipelineStepId + stage probability closeDate amount { @@ -49,7 +49,7 @@ export const query = gql` pointOfContactId updatedAt companyId - pipelineStepId + stage probability closeDate amount { diff --git a/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchEntityQuery.test.tsx b/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchEntityQuery.test.tsx index ea46bac44bfa..57a93cdec66c 100644 --- a/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchEntityQuery.test.tsx +++ b/packages/twenty-front/src/modules/search/hooks/__tests__/useFilteredSearchEntityQuery.test.tsx @@ -67,7 +67,7 @@ describe('useFilteredSearchEntityQuery', () => { const { result } = renderHook( () => { const setCurrentWorkspaceMember = useSetRecoilState( - currentWorkspaceMemberState(), + currentWorkspaceMemberState, ); setCurrentWorkspaceMember({ id: '32219445-f587-4c40-b2b1-6d3205ed96da', @@ -76,7 +76,7 @@ describe('useFilteredSearchEntityQuery', () => { const mockObjectMetadataItems = getObjectMetadataItemsMock(); - const setMetadataItems = useSetRecoilState(objectMetadataItemsState()); + const setMetadataItems = useSetRecoilState(objectMetadataItemsState); setMetadataItems(mockObjectMetadataItems); diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarAccountsListCard.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarAccountsListCard.tsx index b157fb588fca..5425976316ec 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarAccountsListCard.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarAccountsListCard.tsx @@ -20,7 +20,7 @@ const StyledRowRightContainer = styled.div` `; export const SettingsAccountsCalendarAccountsListCard = () => { - const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState()); + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const navigate = useNavigate(); const { records: _accounts, loading } = useFindManyRecords({ diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsAccountsListCard.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsAccountsListCard.tsx index 7999f062b9a7..2fc6154f1e42 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsAccountsListCard.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsAccountsListCard.tsx @@ -20,7 +20,7 @@ const StyledRowRightContainer = styled.div` `; export const SettingsAccountsEmailsAccountsListCard = () => { - const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState()); + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const navigate = useNavigate(); const { records: accounts, loading: accountsLoading } = diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsBlocklistSection.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsBlocklistSection.tsx index 03e6a9e25bd5..28646b9d8cb9 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsBlocklistSection.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsEmailsBlocklistSection.tsx @@ -12,7 +12,7 @@ import { H2Title } from '@/ui/display/typography/components/H2Title'; import { Section } from '@/ui/layout/section/components/Section'; export const SettingsAccountsEmailsBlocklistSection = () => { - const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState()); + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); const { records: blocklist } = useFindManyRecords({ objectNameSingular: CoreObjectNameSingular.Blocklist, diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx index c8d807681439..9f891846bc88 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsListEmptyStateCard.tsx @@ -6,7 +6,7 @@ import { Button } from '@/ui/input/button/components/Button'; import { Card } from '@/ui/layout/card/components/Card'; import { CardContent } from '@/ui/layout/card/components/CardContent'; import { CardHeader } from '@/ui/layout/card/components/CardHeader'; -import { REACT_APP_SERVER_AUTH_URL } from '~/config'; +import { REACT_APP_SERVER_BASE_URL } from '~/config'; import { useGenerateTransientTokenMutation } from '~/generated/graphql'; const StyledHeader = styled(CardHeader)` @@ -30,14 +30,14 @@ export const SettingsAccountsListEmptyStateCard = ({ const [generateTransientToken] = useGenerateTransientTokenMutation(); const handleGmailLogin = useCallback(async () => { - const authServerUrl = REACT_APP_SERVER_AUTH_URL; + const authServerUrl = REACT_APP_SERVER_BASE_URL; const transientToken = await generateTransientToken(); const token = transientToken.data?.generateTransientToken.transientToken.token; - window.location.href = `${authServerUrl}/google-gmail?transientToken=${token}`; + window.location.href = `${authServerUrl}/auth/google-gmail?transientToken=${token}`; }, [generateTransientToken]); return ( diff --git a/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx b/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx index 0c9aa2f19d09..fb3ab5f1cc67 100644 --- a/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx +++ b/packages/twenty-front/src/modules/settings/components/SettingsNavigationDrawerItems.tsx @@ -37,7 +37,7 @@ export const SettingsNavigationDrawerItems = () => { }, [signOut, navigate]); const isCalendarEnabled = useIsFeatureEnabled('IS_CALENDAR_ENABLED'); - const billing = useRecoilValue(billingState()); + const billing = useRecoilValue(billingState); return ( <> diff --git a/packages/twenty-front/src/modules/settings/profile/components/ChangePassword.tsx b/packages/twenty-front/src/modules/settings/profile/components/ChangePassword.tsx index eb08cd04526d..ee60971528c5 100644 --- a/packages/twenty-front/src/modules/settings/profile/components/ChangePassword.tsx +++ b/packages/twenty-front/src/modules/settings/profile/components/ChangePassword.tsx @@ -9,7 +9,7 @@ import { useEmailPasswordResetLinkMutation } from '~/generated/graphql'; export const ChangePassword = () => { const { enqueueSnackBar } = useSnackBar(); - const currentUser = useRecoilValue(currentUserState()); + const currentUser = useRecoilValue(currentUserState); const [emailPasswordResetLink] = useEmailPasswordResetLinkMutation(); diff --git a/packages/twenty-front/src/modules/settings/profile/components/DeleteAccount.tsx b/packages/twenty-front/src/modules/settings/profile/components/DeleteAccount.tsx index bd0de3f17a2f..d3e52ad09eca 100644 --- a/packages/twenty-front/src/modules/settings/profile/components/DeleteAccount.tsx +++ b/packages/twenty-front/src/modules/settings/profile/components/DeleteAccount.tsx @@ -15,7 +15,7 @@ export const DeleteAccount = () => { useState(false); const [deleteUserAccount] = useDeleteUserAccountMutation(); - const currentUser = useRecoilValue(currentUserState()); + const currentUser = useRecoilValue(currentUserState); const userEmail = currentUser?.email; const { signOut } = useAuth(); const navigate = useNavigate(); diff --git a/packages/twenty-front/src/modules/settings/profile/components/DeleteWorkspace.tsx b/packages/twenty-front/src/modules/settings/profile/components/DeleteWorkspace.tsx index b0fcdf1215e2..4264bd928299 100644 --- a/packages/twenty-front/src/modules/settings/profile/components/DeleteWorkspace.tsx +++ b/packages/twenty-front/src/modules/settings/profile/components/DeleteWorkspace.tsx @@ -17,7 +17,7 @@ export const DeleteWorkspace = () => { useState(false); const [deleteCurrentWorkspace] = useDeleteCurrentWorkspaceMutation(); - const currentUser = useRecoilValue(currentUserState()); + const currentUser = useRecoilValue(currentUserState); const userEmail = currentUser?.email; const { signOut } = useAuth(); const navigate = useNavigate(); diff --git a/packages/twenty-front/src/modules/settings/profile/components/EmailField.tsx b/packages/twenty-front/src/modules/settings/profile/components/EmailField.tsx index 80494ebbbeef..02c1e87341a4 100644 --- a/packages/twenty-front/src/modules/settings/profile/components/EmailField.tsx +++ b/packages/twenty-front/src/modules/settings/profile/components/EmailField.tsx @@ -4,7 +4,7 @@ import { currentUserState } from '@/auth/states/currentUserState'; import { TextInput } from '@/ui/input/components/TextInput'; export const EmailField = () => { - const currentUser = useRecoilValue(currentUserState()); + const currentUser = useRecoilValue(currentUserState); return ( { - const currentUser = useRecoilValue(currentUserState()); + const currentUser = useRecoilValue(currentUserState); const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState( - currentWorkspaceMemberState(), + currentWorkspaceMemberState, ); const [firstName, setFirstName] = useState( diff --git a/packages/twenty-front/src/modules/settings/profile/components/ProfilePictureUploader.tsx b/packages/twenty-front/src/modules/settings/profile/components/ProfilePictureUploader.tsx index 8da64b0f1a94..4a65b1ff3aa3 100644 --- a/packages/twenty-front/src/modules/settings/profile/components/ProfilePictureUploader.tsx +++ b/packages/twenty-front/src/modules/settings/profile/components/ProfilePictureUploader.tsx @@ -15,7 +15,7 @@ export const ProfilePictureUploader = () => { useUploadProfilePictureMutation(); const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState( - currentWorkspaceMemberState(), + currentWorkspaceMemberState, ); const [uploadController, setUploadController] = diff --git a/packages/twenty-front/src/modules/settings/workspace/components/NameField.tsx b/packages/twenty-front/src/modules/settings/workspace/components/NameField.tsx index 62dc739305c7..b72e76b4e87d 100644 --- a/packages/twenty-front/src/modules/settings/workspace/components/NameField.tsx +++ b/packages/twenty-front/src/modules/settings/workspace/components/NameField.tsx @@ -27,7 +27,7 @@ export const NameField = ({ autoSave = true, onNameUpdate, }: NameFieldProps) => { - const currentWorkspace = useRecoilValue(currentWorkspaceState()); + const currentWorkspace = useRecoilValue(currentWorkspaceState); const [displayName, setDisplayName] = useState( currentWorkspace?.displayName ?? '', diff --git a/packages/twenty-front/src/modules/settings/workspace/components/ToggleImpersonate.tsx b/packages/twenty-front/src/modules/settings/workspace/components/ToggleImpersonate.tsx index ebb909ede871..453cfc21dd05 100644 --- a/packages/twenty-front/src/modules/settings/workspace/components/ToggleImpersonate.tsx +++ b/packages/twenty-front/src/modules/settings/workspace/components/ToggleImpersonate.tsx @@ -9,7 +9,7 @@ export const ToggleImpersonate = () => { const { enqueueSnackBar } = useSnackBar(); const [currentWorkspace, setCurrentWorkspace] = useRecoilState( - currentWorkspaceState(), + currentWorkspaceState, ); const [updateWorkspace] = useUpdateWorkspaceMutation(); diff --git a/packages/twenty-front/src/modules/settings/workspace/components/WorkspaceLogoUploader.tsx b/packages/twenty-front/src/modules/settings/workspace/components/WorkspaceLogoUploader.tsx index ba410823af07..009c32d54f5d 100644 --- a/packages/twenty-front/src/modules/settings/workspace/components/WorkspaceLogoUploader.tsx +++ b/packages/twenty-front/src/modules/settings/workspace/components/WorkspaceLogoUploader.tsx @@ -13,7 +13,7 @@ export const WorkspaceLogoUploader = () => { const [uploadLogo] = useUploadWorkspaceLogoMutation(); const [updateWorkspce] = useUpdateWorkspaceMutation(); const [currentWorkspace, setCurrentWorkspace] = useRecoilState( - currentWorkspaceState(), + currentWorkspaceState, ); const onUpload = async (file: File) => { diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx index 941647853c63..699b82af7bff 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx +++ b/packages/twenty-front/src/modules/sign-in-background-mock/components/SignInBackgroundMockContainer.tsx @@ -1,7 +1,6 @@ import styled from '@emotion/styled'; import { RecordIndexOptionsDropdown } from '@/object-record/record-index/options/components/RecordIndexOptionsDropdown'; -import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId'; import { RecordTableWithWrappers } from '@/object-record/record-table/components/RecordTableWithWrappers'; import { SignInBackgroundMockContainerEffect } from '@/sign-in-background-mock/components/SignInBackgroundMockContainerEffect'; import { ViewBar } from '@/views/components/ViewBar'; @@ -24,6 +23,7 @@ export const SignInBackgroundMockContainer = () => { {}} optionsDropdownButton={ { viewType={ViewType.Table} /> } - optionsDropdownScopeId={RECORD_INDEX_OPTIONS_DROPDOWN_ID} /> { setViewObjectMetadataId?.(objectMetadataItem.id); @@ -85,9 +88,9 @@ export const SignInBackgroundMockContainerEffect = ({ useEffect(() => { setOnEntityCountChange( - () => (entityCount: number) => setEntityCountInCurrentView(entityCount), + () => (entityCount: number) => setRecordCountInCurrentView(entityCount), ); - }, [setEntityCountInCurrentView, setOnEntityCountChange]); + }, [setRecordCountInCurrentView, setOnEntityCountChange]); return <>; }; diff --git a/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockCompanies.ts b/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockCompanies.ts index 1dac5f635214..7c0e709187f4 100644 --- a/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockCompanies.ts +++ b/packages/twenty-front/src/modules/sign-in-background-mock/constants/SignInBackgroundMockCompanies.ts @@ -401,8 +401,8 @@ export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = [ __typename: 'Opportunity', id: '53f66647-0543-4cc2-9f96-95cc699960f2', probability: '0.5', - pipelineStepId: 'd8361722-03fb-4e65-bd4f-ec9e52e5ec0a', pointOfContactId: '93c72d2e-f517-42fd-80ae-14173b3b70ae', + stage: 'NEW', amount: { __typename: 'Currency', amountMicros: 2000000, @@ -609,7 +609,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = [ __typename: 'Opportunity', id: '81ab695d-2f89-406f-90ea-180f433b2445', probability: '0.5', - pipelineStepId: '30b14887-d592-427d-bd97-6e670158db02', + stage: 'NEW', pointOfContactId: '9b324a88-6784-4449-afdf-dc62cb8702f2', amount: { __typename: 'Currency', @@ -629,7 +629,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = [ __typename: 'Opportunity', id: '9b059852-35b1-4045-9cde-42f715148954', probability: '0.5', - pipelineStepId: '30b14887-d592-427d-bd97-6e670158db02', + stage: 'NEW', pointOfContactId: '98406e26-80f1-4dff-b570-a74942528de3', amount: { __typename: 'Currency', @@ -1157,7 +1157,7 @@ export const SIGN_IN_BACKGROUND_MOCK_COMPANIES = [ __typename: 'Opportunity', id: '7c887ee3-be10-412b-a663-16bd3c2228e1', probability: '0.5', - pipelineStepId: '6edf4ead-006a-46e1-9c6d-228f1d0143c9', + stage: 'NEW', pointOfContactId: '86083141-1c0e-494c-a1b6-85b1c6fefaa5', amount: { __typename: 'Currency', diff --git a/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx b/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx index a2948066346d..c54a5498a539 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/hooks/__tests__/useSpreadsheetImport.test.tsx @@ -44,7 +44,7 @@ describe('useSpreadsheetImport', () => { const { result } = renderHook( () => ({ useSpreadsheetImport: useSpreadsheetImport(), - spreadsheetImportState: useRecoilState(spreadsheetImportState())[0], + spreadsheetImportState: useRecoilState(spreadsheetImportState)[0], }), { wrapper: Wrapper, diff --git a/packages/twenty-front/src/modules/spreadsheet-import/hooks/useSpreadsheetImport.ts b/packages/twenty-front/src/modules/spreadsheet-import/hooks/useSpreadsheetImport.ts index 160ba49caa81..0259faad6041 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/hooks/useSpreadsheetImport.ts +++ b/packages/twenty-front/src/modules/spreadsheet-import/hooks/useSpreadsheetImport.ts @@ -4,7 +4,7 @@ import { spreadsheetImportState } from '@/spreadsheet-import/states/spreadsheetI import { SpreadsheetOptions } from '@/spreadsheet-import/types'; export const useSpreadsheetImport = () => { - const setSpreadSheetImport = useSetRecoilState(spreadsheetImportState()); + const setSpreadSheetImport = useSetRecoilState(spreadsheetImportState); const openSpreadsheetImport = ( options: Omit, 'isOpen' | 'onClose'>, diff --git a/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImportProvider.tsx b/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImportProvider.tsx index 3bec782699da..5c5b7a137704 100644 --- a/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImportProvider.tsx +++ b/packages/twenty-front/src/modules/spreadsheet-import/provider/components/SpreadsheetImportProvider.tsx @@ -11,7 +11,7 @@ export const SpreadsheetImportProvider = ( props: SpreadsheetImportProviderProps, ) => { const [spreadsheetImport, setSpreadsheetImport] = useRecoilState( - spreadsheetImportState(), + spreadsheetImportState, ); const handleClose = () => { diff --git a/packages/twenty-front/src/modules/support/components/SupportChat.tsx b/packages/twenty-front/src/modules/support/components/SupportChat.tsx index ab15532b4a8d..4a41c61688ed 100644 --- a/packages/twenty-front/src/modules/support/components/SupportChat.tsx +++ b/packages/twenty-front/src/modules/support/components/SupportChat.tsx @@ -33,9 +33,9 @@ const insertScript = ({ }; export const SupportChat = () => { - const currentUser = useRecoilValue(currentUserState()); - const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState()); - const supportChat = useRecoilValue(supportChatState()); + const currentUser = useRecoilValue(currentUserState); + const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState); + const supportChat = useRecoilValue(supportChatState); const [isFrontChatLoaded, setIsFrontChatLoaded] = useState(false); const configureFront = useCallback( diff --git a/packages/twenty-front/src/modules/ui/display/chip/components/Chip.tsx b/packages/twenty-front/src/modules/ui/display/chip/components/Chip.tsx index 9e6f5843d5a1..cec0483d2620 100644 --- a/packages/twenty-front/src/modules/ui/display/chip/components/Chip.tsx +++ b/packages/twenty-front/src/modules/ui/display/chip/components/Chip.tsx @@ -1,4 +1,5 @@ import { MouseEvent, ReactNode } from 'react'; +import { css } from '@emotion/react'; import styled from '@emotion/styled'; import { OverflowingTextWithTooltip } from '../../tooltip/OverflowingTextWithTooltip'; @@ -34,86 +35,108 @@ type ChipProps = { onClick?: (event: MouseEvent) => void; }; -const StyledContainer = styled.div>` - align-items: center; +const StyledContainer = styled.div< + Pick< + ChipProps, + 'accent' | 'clickable' | 'disabled' | 'maxWidth' | 'size' | 'variant' + > +>` + --chip-horizontal-padding: ${({ theme }) => theme.spacing(1)}; + --chip-vertical-padding: ${({ theme }) => theme.spacing(1)}; - background-color: ${({ theme, variant }) => - variant === ChipVariant.Highlighted - ? theme.background.transparent.light - : variant === ChipVariant.Rounded - ? theme.background.transparent.lighter - : 'transparent'}; - border-color: ${({ theme, variant }) => - variant === ChipVariant.Rounded ? theme.border.color.medium : 'none'}; - border-radius: ${({ theme, variant }) => - variant === ChipVariant.Rounded ? '50px' : theme.border.radius.sm}; - border-style: ${({ variant }) => - variant === ChipVariant.Rounded ? 'solid' : 'none'}; - border-width: ${({ variant }) => - variant === ChipVariant.Rounded ? '1px' : '0px'}; - - color: ${({ theme, disabled, accent }) => - disabled - ? theme.font.color.light - : accent === ChipAccent.TextPrimary - ? theme.font.color.primary - : theme.font.color.secondary}; - cursor: ${({ clickable, disabled, variant }) => - disabled || variant === ChipVariant.Transparent - ? 'inherit' - : clickable - ? 'pointer' - : 'inherit'}; + align-items: center; + border-radius: ${({ theme }) => theme.border.radius.sm}; + color: ${({ theme, disabled }) => + disabled ? theme.font.color.light : theme.font.color.secondary}; + cursor: ${({ clickable, disabled }) => + clickable ? 'pointer' : disabled ? 'not-allowed' : 'inherit'}; display: inline-flex; - font-weight: ${({ theme, accent }) => - accent === ChipAccent.TextSecondary ? theme.font.weight.medium : 'inherit'}; gap: ${({ theme }) => theme.spacing(1)}; - - height: ${({ size }) => (size === ChipSize.Large ? '16px' : '12px')}; - --chip-horizontal-padding: ${({ theme, variant }) => - variant === ChipVariant.Rounded ? theme.spacing(2) : theme.spacing(1)}; + height: ${({ theme }) => theme.spacing(3)}; max-width: ${({ maxWidth }) => maxWidth - ? `calc( ${maxWidth}px - 2*var(--chip-horizontal-padding))` + ? `calc(${maxWidth}px - 2 * var(--chip-horizontal-padding))` : '200px'}; - - --chip-vertical-padding: ${({ theme, variant }) => - variant === ChipVariant.Rounded ? '3px' : theme.spacing(1)}; - overflow: hidden; padding: var(--chip-vertical-padding) var(--chip-horizontal-padding); user-select: none; - :hover { - ${({ variant, theme, disabled }) => { - if (!disabled) { - return ( - 'background-color: ' + - (variant === ChipVariant.Highlighted - ? theme.background.transparent.medium - : variant === ChipVariant.Regular - ? theme.background.transparent.light - : 'transparent') + - ';' - ); - } - }} - } - :active { - ${({ variant, theme, disabled }) => { - if (!disabled) { - return ( - 'background-color: ' + - (variant === ChipVariant.Highlighted - ? theme.background.transparent.strong - : variant === ChipVariant.Regular - ? theme.background.transparent.medium - : 'transparent') + - ';' - ); - } - }} - } + // Accent style overrides + ${({ accent, disabled, theme }) => { + if (accent === ChipAccent.TextPrimary) { + return ( + !disabled && + css` + color: ${theme.font.color.primary}; + ` + ); + } + + if (accent === ChipAccent.TextSecondary) { + return css` + font-weight: ${theme.font.weight.medium}; + `; + } + }} + + // Size style overrides + ${({ theme, size }) => + size === ChipSize.Large && + css` + height: ${theme.spacing(4)}; + `} + + // Variant style overrides + ${({ disabled, theme, variant }) => { + if (variant === ChipVariant.Regular) { + return ( + !disabled && + css` + :hover { + background-color: ${theme.background.transparent.light}; + } + + :active { + background-color: ${theme.background.transparent.medium}; + } + ` + ); + } + + if (variant === ChipVariant.Highlighted) { + return css` + background-color: ${theme.background.transparent.light}; + + ${!disabled && + css` + :hover { + background-color: ${theme.background.transparent.medium}; + } + + :active { + background-color: ${theme.background.transparent.strong}; + } + `} + `; + } + + if (variant === ChipVariant.Rounded) { + return css` + --chip-horizontal-padding: ${theme.spacing(2)}; + --chip-vertical-padding: 3px; + + background-color: ${theme.background.transparent.lighter}; + border: 1px solid ${theme.border.color.medium}; + border-radius: 50px; + `; + } + + if (variant === ChipVariant.Transparent) { + return css` + cursor: inherit; + `; + } + }} `; const StyledLabel = styled.span` diff --git a/packages/twenty-front/src/modules/ui/display/icon/components/IconsProvider.tsx b/packages/twenty-front/src/modules/ui/display/icon/components/IconsProvider.tsx index 16112d90fcbd..918d7d32d600 100644 --- a/packages/twenty-front/src/modules/ui/display/icon/components/IconsProvider.tsx +++ b/packages/twenty-front/src/modules/ui/display/icon/components/IconsProvider.tsx @@ -8,7 +8,7 @@ type IconsProviderProps = { }; export const IconsProvider = ({ children }: IconsProviderProps) => { - const setIcons = useSetRecoilState(iconsState()); + const setIcons = useSetRecoilState(iconsState); useEffect(() => { import('../constants/index').then((lazyLoadedIcons) => { diff --git a/packages/twenty-front/src/modules/ui/display/icon/constants/index.ts b/packages/twenty-front/src/modules/ui/display/icon/constants/index.ts index becf2f91bb37..cc521b61a0c6 100644 --- a/packages/twenty-front/src/modules/ui/display/icon/constants/index.ts +++ b/packages/twenty-front/src/modules/ui/display/icon/constants/index.ts @@ -950,6 +950,7 @@ import { IconCalendarBolt, IconCalendarCancel, IconCalendarCheck, + IconCalendarClock, IconCalendarCode, IconCalendarCog, IconCalendarDollar, @@ -4199,14 +4200,14 @@ import { } from '@tabler/icons-react'; export default { + Icon123, + Icon24Hours, Icon2fa, + Icon360, + Icon360View, Icon3dCubeSphere, Icon3dCubeSphereOff, Icon3dRotate, - Icon24Hours, - Icon123, - Icon360, - Icon360View, IconAB, IconAB2, IconAbacus, @@ -5149,6 +5150,7 @@ export default { IconCalendarBolt, IconCalendarCancel, IconCalendarCheck, + IconCalendarClock, IconCalendarCode, IconCalendarCog, IconCalendarDollar, @@ -5440,6 +5442,9 @@ export default { IconClockExclamation, IconClockHeart, IconClockHour1, + IconClockHour10, + IconClockHour11, + IconClockHour12, IconClockHour2, IconClockHour3, IconClockHour4, @@ -5448,9 +5453,6 @@ export default { IconClockHour7, IconClockHour8, IconClockHour9, - IconClockHour10, - IconClockHour11, - IconClockHour12, IconClockMinus, IconClockOff, IconClockPause, @@ -7117,10 +7119,10 @@ export default { IconMovieOff, IconMug, IconMugOff, - IconMultiplier1x, - IconMultiplier2x, IconMultiplier05x, IconMultiplier15x, + IconMultiplier1x, + IconMultiplier2x, IconMushroom, IconMushroomOff, IconMusic, @@ -7515,20 +7517,20 @@ export default { IconReservedLine, IconResize, IconRestore, - IconRewindBackward5, IconRewindBackward10, IconRewindBackward15, IconRewindBackward20, IconRewindBackward30, IconRewindBackward40, + IconRewindBackward5, IconRewindBackward50, IconRewindBackward60, - IconRewindForward5, IconRewindForward10, IconRewindForward15, IconRewindForward20, IconRewindForward30, IconRewindForward40, + IconRewindForward5, IconRewindForward50, IconRewindForward60, IconRibbonHealth, @@ -8080,11 +8082,11 @@ export default { IconTiltShift, IconTiltShiftOff, IconTimeDuration0, - IconTimeDuration5, IconTimeDuration10, IconTimeDuration15, IconTimeDuration30, IconTimeDuration45, + IconTimeDuration5, IconTimeDuration60, IconTimeDuration90, IconTimeDurationOff, diff --git a/packages/twenty-front/src/modules/ui/display/icon/hooks/useIcons.ts b/packages/twenty-front/src/modules/ui/display/icon/hooks/useIcons.ts index e3619b770da4..4efd71e7f63d 100644 --- a/packages/twenty-front/src/modules/ui/display/icon/hooks/useIcons.ts +++ b/packages/twenty-front/src/modules/ui/display/icon/hooks/useIcons.ts @@ -4,7 +4,7 @@ import { Icon123 } from '@/ui/display/icon'; import { iconsState } from '@/ui/display/icon/states/iconsState'; export const useIcons = () => { - const icons = useRecoilValue(iconsState()); + const icons = useRecoilValue(iconsState); const defaultIcon = Icon123; const getIcons = () => { diff --git a/packages/twenty-front/src/modules/ui/display/icon/index.ts b/packages/twenty-front/src/modules/ui/display/icon/index.ts index ae1b058dc92c..7bfc09d63b04 100644 --- a/packages/twenty-front/src/modules/ui/display/icon/index.ts +++ b/packages/twenty-front/src/modules/ui/display/icon/index.ts @@ -37,6 +37,7 @@ export { IconChevronUp, IconCircleDot, IconCircleOff, + IconCircleX, IconClick, IconCode, IconCoins, @@ -67,6 +68,9 @@ export { IconFilterOff, IconForbid, IconGripVertical, + IconH1, + IconH2, + IconH3, IconHeadphones, IconHeart, IconHeartOff, @@ -97,6 +101,7 @@ export { IconPencil, IconPhone, IconPhoto, + IconPilcrow, IconPlug, IconPlus, IconPresentation, diff --git a/packages/twenty-front/src/modules/ui/field/input/components/DoubleTextInput.tsx b/packages/twenty-front/src/modules/ui/field/input/components/DoubleTextInput.tsx index 3a5b387aa0d8..1ca95288f903 100644 --- a/packages/twenty-front/src/modules/ui/field/input/components/DoubleTextInput.tsx +++ b/packages/twenty-front/src/modules/ui/field/input/components/DoubleTextInput.tsx @@ -1,4 +1,10 @@ -import { ChangeEvent, useEffect, useRef, useState } from 'react'; +import { + ChangeEvent, + ClipboardEvent, + useEffect, + useRef, + useState, +} from 'react'; import styled from '@emotion/styled'; import { Key } from 'ts-key-enum'; @@ -39,6 +45,7 @@ type DoubleTextInputProps = { newDoubleTextValue: FieldDoubleText, ) => void; onChange?: (newDoubleTextValue: FieldDoubleText) => void; + onPaste?: (newDoubleTextValue: FieldDoubleText) => void; }; export const DoubleTextInput = ({ @@ -53,6 +60,7 @@ export const DoubleTextInput = ({ onShiftTab, onTab, onChange, + onPaste, }: DoubleTextInputProps) => { const [firstInternalValue, setFirstInternalValue] = useState(firstValue); const [secondInternalValue, setSecondInternalValue] = useState(secondValue); @@ -150,6 +158,20 @@ export const DoubleTextInput = ({ enabled: isDefined(onClickOutside), }); + const handleOnPaste = (event: ClipboardEvent) => { + if (firstInternalValue.length > 0 || secondInternalValue.length > 0) { + return; + } + + event.preventDefault(); + + const name = event.clipboardData.getData('Text'); + + const splittedName = name.split(' '); + + onPaste?.({ firstValue: splittedName[0], secondValue: splittedName[1] }); + }; + return ( ) => { handleChange(event.target.value, secondInternalValue); }} + onPaste={(event: ClipboardEvent) => + handleOnPaste(event) + } /> ) => void; @@ -28,7 +29,13 @@ export type ButtonProps = { const StyledButton = styled.button< Pick< ButtonProps, - 'fullWidth' | 'variant' | 'size' | 'position' | 'accent' | 'focus' + | 'fullWidth' + | 'variant' + | 'size' + | 'position' + | 'accent' + | 'focus' + | 'justify' > >` align-items: center; @@ -177,9 +184,7 @@ const StyledButton = styled.button< `; case 'danger': return css` - background: ${!disabled - ? theme.background.transparent.primary - : 'transparent'}; + background: transparent; border-color: ${variant === 'secondary' ? focus ? theme.color.red @@ -236,6 +241,7 @@ const StyledButton = styled.button< font-weight: 500; gap: ${({ theme }) => theme.spacing(1)}; height: ${({ size }) => (size === 'small' ? '24px' : '32px')}; + justify-content: ${({ justify }) => justify}; padding: ${({ theme }) => { return `0 ${theme.spacing(2)}`; }}; @@ -266,6 +272,7 @@ export const Button = ({ position = 'standalone', soon = false, disabled = false, + justify = 'flex-start', focus = false, onClick, }: ButtonProps) => { @@ -279,6 +286,7 @@ export const Button = ({ position={position} disabled={soon || disabled} focus={focus} + justify={justify} accent={accent} className={className} onClick={onClick} diff --git a/packages/twenty-front/src/modules/ui/input/button/components/LightIconButtonGroup.tsx b/packages/twenty-front/src/modules/ui/input/button/components/LightIconButtonGroup.tsx index ff548e5df72c..adb20d64bcac 100644 --- a/packages/twenty-front/src/modules/ui/input/button/components/LightIconButtonGroup.tsx +++ b/packages/twenty-front/src/modules/ui/input/button/components/LightIconButtonGroup.tsx @@ -31,6 +31,7 @@ export const LightIconButtonGroup = ({ diff --git a/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx b/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx index 2c4c195164e7..ebc209d63022 100644 --- a/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/IconPicker.tsx @@ -30,6 +30,7 @@ type IconPickerProps = { onOpen?: () => void; variant?: IconButtonVariant; className?: string; + disableBlur?: boolean; }; const StyledMenuIconItemsContainer = styled.div` @@ -86,6 +87,7 @@ export const IconPicker = ({ onClose, onOpen, variant = 'secondary', + disableBlur = false, className, }: IconPickerProps) => { const [searchString, setSearchString] = useState(''); @@ -148,6 +150,7 @@ export const IconPicker = ({ /> } dropdownMenuWidth={176} + disableBlur={disableBlur} dropdownComponents={ = { export type SelectProps = { className?: string; disabled?: boolean; + disableBlur?: boolean; dropdownId: string; dropdownWidth?: `${string}px` | 'auto' | number; emptyOption?: SelectOption; @@ -75,6 +76,7 @@ const StyledIconChevronDown = styled(IconChevronDown)<{ disabled?: boolean }>` export const Select = ({ className, disabled: disabledFromProps, + disableBlur = false, dropdownId, dropdownWidth = 176, emptyOption, @@ -141,6 +143,7 @@ export const Select = ({ dropdownMenuWidth={dropdownWidth} dropdownPlacement="bottom-start" clickableComponent={selectControl} + disableBlur={disableBlur} dropdownComponents={ <> {!!withSearchInput && ( diff --git a/packages/twenty-front/src/modules/ui/input/components/__stories__/IconPicker.stories.tsx b/packages/twenty-front/src/modules/ui/input/components/__stories__/IconPicker.stories.tsx index 95b433dfa1de..8167a00cb5ed 100644 --- a/packages/twenty-front/src/modules/ui/input/components/__stories__/IconPicker.stories.tsx +++ b/packages/twenty-front/src/modules/ui/input/components/__stories__/IconPicker.stories.tsx @@ -58,7 +58,7 @@ export const WithSearch: Story = { await sleep(100); - const searchedIcon = canvas.getByRole('button', { + const searchedIcon = await canvas.findByRole('button', { name: 'Icon Building Skyscraper', }); @@ -81,7 +81,7 @@ export const WithSearchAndClose: Story = { await sleep(100); - const searchedIcon = canvas.getByRole('button', { + const searchedIcon = await canvas.findByRole('button', { name: 'Icon Building Skyscraper', }); diff --git a/packages/twenty-front/src/modules/ui/input/editor/components/BlockEditor.tsx b/packages/twenty-front/src/modules/ui/input/editor/components/BlockEditor.tsx index 0ca49bca52be..fc5138359763 100644 --- a/packages/twenty-front/src/modules/ui/input/editor/components/BlockEditor.tsx +++ b/packages/twenty-front/src/modules/ui/input/editor/components/BlockEditor.tsx @@ -1,12 +1,22 @@ -import { BlockNoteEditor } from '@blocknote/core'; -import { BlockNoteView } from '@blocknote/react'; +import { ClipboardEvent } from 'react'; +import { filterSuggestionItems } from '@blocknote/core'; +import { BlockNoteView, SuggestionMenuController } from '@blocknote/react'; import { useTheme } from '@emotion/react'; import styled from '@emotion/styled'; +import { blockSchema } from '@/activities/blocks/schema'; +import { getSlashMenu } from '@/activities/blocks/slashMenu'; +import { + CustomSlashMenu, + SuggestionItem, +} from '@/ui/input/editor/components/CustomSlashMenu'; + interface BlockEditorProps { - editor: BlockNoteEditor; + editor: typeof blockSchema.BlockNoteEditor; onFocus?: () => void; onBlur?: () => void; + onPaste?: (event: ClipboardEvent) => void; + onChange?: () => void; } const StyledEditor = styled.div` @@ -22,7 +32,13 @@ const StyledEditor = styled.div` } `; -export const BlockEditor = ({ editor, onFocus, onBlur }: BlockEditorProps) => { +export const BlockEditor = ({ + editor, + onFocus, + onBlur, + onChange, + onPaste, +}: BlockEditorProps) => { const theme = useTheme(); const blockNoteTheme = theme.name === 'light' ? 'light' : 'dark'; @@ -34,14 +50,33 @@ export const BlockEditor = ({ editor, onFocus, onBlur }: BlockEditorProps) => { onBlur?.(); }; + const handleChange = () => { + onChange?.(); + }; + + const handlePaste = (event: ClipboardEvent) => { + onPaste?.(event); + }; + return ( + slashMenu={false} + > + + filterSuggestionItems(getSlashMenu(editor), query) + } + suggestionMenuComponent={CustomSlashMenu} + /> + ); }; diff --git a/packages/twenty-front/src/modules/ui/input/editor/components/CustomSlashMenu.tsx b/packages/twenty-front/src/modules/ui/input/editor/components/CustomSlashMenu.tsx new file mode 100644 index 000000000000..509fd9a740f1 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/input/editor/components/CustomSlashMenu.tsx @@ -0,0 +1,42 @@ +import { SuggestionMenuProps } from '@blocknote/react'; +import styled from '@emotion/styled'; + +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; +import { DropdownMenu } from '@/ui/layout/dropdown/components/DropdownMenu'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { MenuItemSuggestion } from '@/ui/navigation/menu-item/components/MenuItemSuggestion'; + +export type SuggestionItem = { + title: string; + onItemClick: () => void; + aliases?: string[]; + Icon?: IconComponent; +}; + +type CustomSlashMenuProps = SuggestionMenuProps; + +const StyledSlashMenu = styled.div` + * { + box-sizing: content-box; + } +`; + +export const CustomSlashMenu = (props: CustomSlashMenuProps) => { + return ( + + + + {props.items.map((item, index) => ( + item.onItemClick()} + text={item.title} + LeftIcon={item.Icon} + selected={props.selectedIndex === index} + /> + ))} + + + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/input/hooks/useIconPicker.ts b/packages/twenty-front/src/modules/ui/input/hooks/useIconPicker.ts index 998dd0dee4d1..141431c715cf 100644 --- a/packages/twenty-front/src/modules/ui/input/hooks/useIconPicker.ts +++ b/packages/twenty-front/src/modules/ui/input/hooks/useIconPicker.ts @@ -3,7 +3,7 @@ import { useRecoilState } from 'recoil'; import { iconPickerState } from '../states/iconPickerState'; export const useIconPicker = () => { - const [iconPicker, setIconPicker] = useRecoilState(iconPickerState()); + const [iconPicker, setIconPicker] = useRecoilState(iconPickerState); return { Icon: iconPicker.Icon, diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx index 6d303a54cc31..88d8479be24c 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/Dropdown.tsx @@ -35,6 +35,7 @@ type DropdownProps = { dropdownPlacement?: Placement; dropdownMenuWidth?: `${string}px` | `${number}%` | 'auto' | number; dropdownOffset?: { x?: number; y?: number }; + disableBlur?: boolean; onClickOutside?: () => void; onClose?: () => void; onOpen?: () => void; @@ -50,6 +51,7 @@ export const Dropdown = ({ dropdownHotkeyScope, dropdownPlacement = 'bottom-end', dropdownOffset = { x: 0, y: 0 }, + disableBlur = false, onClickOutside, onClose, onOpen, @@ -109,7 +111,10 @@ export const Dropdown = ({ {clickableComponent && (
{ + toggleDropdown(); + onClickOutside?.(); + }} className={className} > {clickableComponent} @@ -123,6 +128,7 @@ export const Dropdown = ({ )} {isDropdownOpen && ( ` - backdrop-filter: ${({ disableBlur }) => (disableBlur ? 'none' : 'blur(8px)')}; - background: ${({ theme }) => theme.background.transparent.secondary}; + backdrop-filter: ${({ disableBlur }) => + disableBlur + ? 'none' + : 'blur(12px) saturate(200%) contrast(50%) brightness(130%)'}; + + background: ${({ theme, disableBlur }) => + disableBlur + ? theme.background.primary + : theme.background.transparent.secondary}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; border-radius: ${({ theme }) => theme.border.radius.md}; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx index 68446f9a945b..3c652c86e902 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/DropdownMenuHeader.tsx @@ -11,14 +11,11 @@ const StyledHeader = styled.li` display: flex; font-size: ${({ theme }) => theme.font.size.sm}; font-weight: ${({ theme }) => theme.font.weight.medium}; + border-radius: ${({ theme }) => theme.border.radius.sm}; padding: ${({ theme }) => theme.spacing(1)}; user-select: none; - - &:hover { - background: ${({ theme }) => theme.background.transparent.light}; - } `; const StyledChildrenWrapper = styled.span` @@ -46,9 +43,10 @@ export const DropdownMenuHeader = ({ testId, }: DropdownMenuHeaderProps) => { return ( - + {StartIcon && ( {children} {EndIcon && ( ->(({ autoFocus, defaultValue, placeholder }, ref) => { +>(({ autoFocus, value, placeholder, onChange }, ref) => { return ( diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/DropdownMenu.stories.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/DropdownMenu.stories.tsx index 0e2a412a6f6e..d362e1ed7958 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/DropdownMenu.stories.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/DropdownMenu.stories.tsx @@ -274,7 +274,7 @@ export const WithInput: Story = { args: { dropdownComponents: ( <> - + {optionsMock.map(({ name }) => ( diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/DropdownMenuInput.stories.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/DropdownMenuInput.stories.tsx index 21e5d765399a..42838fca3b4d 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/DropdownMenuInput.stories.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/components/__stories__/DropdownMenuInput.stories.tsx @@ -8,7 +8,7 @@ const meta: Meta = { title: 'UI/Layout/Dropdown/DropdownMenuInput', component: DropdownMenuInput, decorators: [ComponentDecorator], - args: { defaultValue: 'Lorem ipsum' }, + args: { value: 'Lorem ipsum' }, }; export default meta; diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/__tests__/useInternalHotkeyScopeManagement.test.tsx b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/__tests__/useInternalHotkeyScopeManagement.test.tsx index 9bdb17bae49f..3bfd0f3a3585 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/__tests__/useInternalHotkeyScopeManagement.test.tsx +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/__tests__/useInternalHotkeyScopeManagement.test.tsx @@ -27,7 +27,7 @@ describe('useInternalHotkeyScopeManagement', () => { const { dropdownHotkeyScopeState } = useDropdownStates({ dropdownScopeId, }); - const dropdownHotkeyScope = useRecoilValue(dropdownHotkeyScopeState()); + const dropdownHotkeyScope = useRecoilValue(dropdownHotkeyScopeState); return { dropdownHotkeyScope }; }, { diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts index 0a671a40e9f4..f3b196f41808 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useDropdown.ts @@ -20,14 +20,12 @@ export const useDropdown = (dropdownId?: string) => { goBackToPreviousHotkeyScope, } = usePreviousHotkeyScope(); - const [dropdownHotkeyScope] = useRecoilState(dropdownHotkeyScopeState()); + const [dropdownHotkeyScope] = useRecoilState(dropdownHotkeyScopeState); - const [dropdownWidth, setDropdownWidth] = - useRecoilState(dropdownWidthState()); + const [dropdownWidth, setDropdownWidth] = useRecoilState(dropdownWidthState); - const [isDropdownOpen, setIsDropdownOpen] = useRecoilState( - isDropdownOpenState(), - ); + const [isDropdownOpen, setIsDropdownOpen] = + useRecoilState(isDropdownOpenState); const closeDropdown = () => { goBackToPreviousHotkeyScope(); diff --git a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useInternalHotkeyScopeManagement.ts b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useInternalHotkeyScopeManagement.ts index 74f29b508d17..9ea9db797362 100644 --- a/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useInternalHotkeyScopeManagement.ts +++ b/packages/twenty-front/src/modules/ui/layout/dropdown/hooks/useInternalHotkeyScopeManagement.ts @@ -15,7 +15,7 @@ export const useInternalHotkeyScopeManagement = ({ const { dropdownHotkeyScopeState } = useDropdownStates({ dropdownScopeId }); const [dropdownHotkeyScope, setDropdownHotkeyScope] = useRecoilState( - dropdownHotkeyScopeState(), + dropdownHotkeyScopeState, ); useEffect(() => { diff --git a/packages/twenty-front/src/modules/ui/layout/modal/components/ConfirmationModal.tsx b/packages/twenty-front/src/modules/ui/layout/modal/components/ConfirmationModal.tsx index e71955d62bad..515a27bfe0b1 100644 --- a/packages/twenty-front/src/modules/ui/layout/modal/components/ConfirmationModal.tsx +++ b/packages/twenty-front/src/modules/ui/layout/modal/components/ConfirmationModal.tsx @@ -7,7 +7,7 @@ import { H1Title, H1TitleFontColor, } from '@/ui/display/typography/components/H1Title'; -import { Button } from '@/ui/input/button/components/Button'; +import { Button, ButtonAccent } from '@/ui/input/button/components/Button'; import { TextInput } from '@/ui/input/components/TextInput'; import { Modal } from '@/ui/layout/modal/components/Modal'; import { @@ -25,6 +25,7 @@ export type ConfirmationModalProps = { deleteButtonText?: string; confirmationPlaceholder?: string; confirmationValue?: string; + confirmButtonAccent?: ButtonAccent; }; const StyledConfirmationModal = styled(Modal)` @@ -66,6 +67,7 @@ export const ConfirmationModal = ({ deleteButtonText = 'Delete', confirmationValue, confirmationPlaceholder, + confirmButtonAccent = 'danger', }: ConfirmationModalProps) => { const [inputConfirmationValue, setInputConfirmationValue] = useState(''); @@ -127,7 +129,7 @@ export const ConfirmationModal = ({ setIsOpen(false); }} variant="secondary" - accent="danger" + accent={confirmButtonAccent} title={deleteButtonText} disabled={!isValidValue} fullWidth diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx index 1ab9143e9844..a73e674dfd3f 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawer.tsx @@ -42,12 +42,12 @@ const StyledRightDrawer = styled.div` export const RightDrawer = () => { const [isRightDrawerOpen, setIsRightDrawerOpen] = useRecoilState( - isRightDrawerOpenState(), + isRightDrawerOpenState, ); - const isRightDrawerExpanded = useRecoilValue(isRightDrawerExpandedState()); + const isRightDrawerExpanded = useRecoilValue(isRightDrawerExpandedState); - const rightDrawerPage = useRecoilValue(rightDrawerPageState()); + const rightDrawerPage = useRecoilValue(rightDrawerPageState); const { closeRightDrawer } = useRightDrawer(); @@ -63,11 +63,11 @@ export const RightDrawer = () => { ({ snapshot, set }) => (event) => { const isRightDrawerOpen = snapshot - .getLoadable(isRightDrawerOpenState()) + .getLoadable(isRightDrawerOpenState) .getValue(); if (isRightDrawerOpen) { - set(rightDrawerCloseEventState(), event); + set(rightDrawerCloseEventState, event); closeRightDrawer(); } }, diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx index cb981739beb8..a0eb83b409e2 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerRouter.tsx @@ -1,8 +1,8 @@ import styled from '@emotion/styled'; import { useRecoilState } from 'recoil'; +import { RightDrawerCalendarEvent } from '@/activities/calendar/right-drawer/components/RightDrawerCalendarEvent'; import { RightDrawerEmailThread } from '@/activities/emails/right-drawer/components/RightDrawerEmailThread'; -import { RightDrawerEmailThreadTopBar } from '@/activities/emails/right-drawer/components/RightDrawerEmailThreadTopBar'; import { RightDrawerCreateActivity } from '@/activities/right-drawer/components/create/RightDrawerCreateActivity'; import { RightDrawerEditActivity } from '@/activities/right-drawer/components/edit/RightDrawerEditActivity'; @@ -27,28 +27,31 @@ const StyledRightDrawerBody = styled.div` position: relative; `; +const RIGHT_DRAWER_PAGES_CONFIG = { + [RightDrawerPages.CreateActivity]: { + page: , + topBar: , + }, + [RightDrawerPages.EditActivity]: { + page: , + topBar: , + }, + [RightDrawerPages.ViewEmailThread]: { + page: , + topBar: , + }, + [RightDrawerPages.ViewCalendarEvent]: { + page: , + topBar: , + }, +}; + export const RightDrawerRouter = () => { - const [rightDrawerPage] = useRecoilState(rightDrawerPageState()); - - let page = <>; - let topBar = <>; - - switch (rightDrawerPage) { - case RightDrawerPages.CreateActivity: - page = ; - topBar = ; - break; - case RightDrawerPages.EditActivity: - page = ; - topBar = ; - break; - case RightDrawerPages.ViewEmailThread: - page = ; - topBar = ; - break; - default: - break; - } + const [rightDrawerPage] = useRecoilState(rightDrawerPageState); + + const { topBar = null, page = null } = rightDrawerPage + ? RIGHT_DRAWER_PAGES_CONFIG[rightDrawerPage] + : {}; return ( diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarExpandButton.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarExpandButton.tsx index d22700e7324f..701c69edfabf 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarExpandButton.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/components/RightDrawerTopBarExpandButton.tsx @@ -10,7 +10,7 @@ import { isRightDrawerExpandedState } from '../states/isRightDrawerExpandedState export const RightDrawerTopBarExpandButton = () => { const [isRightDrawerExpanded, setIsRightDrawerExpanded] = useRecoilState( - isRightDrawerExpandedState(), + isRightDrawerExpandedState, ); const handleButtonClick = () => { diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/__tests__/useRightDrawer.test.tsx b/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/__tests__/useRightDrawer.test.tsx index 727f6a7ca901..843de0118c6c 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/__tests__/useRightDrawer.test.tsx +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/__tests__/useRightDrawer.test.tsx @@ -12,12 +12,10 @@ describe('useRightDrawer', () => { it('Should test the default behavior of useRightDrawer and change the states as the function calls', async () => { const useCombinedHooks = () => { const { openRightDrawer, closeRightDrawer } = useRightDrawer(); - const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState()); - const isRightDrawerExpanded = useRecoilValue( - isRightDrawerExpandedState(), - ); + const isRightDrawerOpen = useRecoilValue(isRightDrawerOpenState); + const isRightDrawerExpanded = useRecoilValue(isRightDrawerExpandedState); - const rightDrawerPage = useRecoilValue(rightDrawerPageState()); + const rightDrawerPage = useRecoilValue(rightDrawerPageState); return { openRightDrawer, diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts index fe04ac5a1b5f..5cf5cc621456 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/hooks/useRightDrawer.ts @@ -8,16 +8,16 @@ import { rightDrawerPageState } from '../states/rightDrawerPageState'; import { RightDrawerPages } from '../types/RightDrawerPages'; export const useRightDrawer = () => { - const [isRightDrawerOpen] = useRecoilState(isRightDrawerOpenState()); + const [isRightDrawerOpen] = useRecoilState(isRightDrawerOpenState); - const [rightDrawerPage] = useRecoilState(rightDrawerPageState()); + const [rightDrawerPage] = useRecoilState(rightDrawerPageState); const openRightDrawer = useRecoilCallback( ({ set }) => (rightDrawerPage: RightDrawerPages) => { - set(rightDrawerPageState(), rightDrawerPage); - set(isRightDrawerExpandedState(), false); - set(isRightDrawerOpenState(), true); + set(rightDrawerPageState, rightDrawerPage); + set(isRightDrawerExpandedState, false); + set(isRightDrawerOpenState, true); }, [], ); @@ -25,8 +25,8 @@ export const useRightDrawer = () => { const closeRightDrawer = useRecoilCallback( ({ set }) => () => { - set(isRightDrawerExpandedState(), false); - set(isRightDrawerOpenState(), false); + set(isRightDrawerExpandedState, false); + set(isRightDrawerOpenState, false); }, [], ); @@ -35,7 +35,7 @@ export const useRightDrawer = () => { ({ snapshot }) => (event: MouseEvent | TouchEvent) => { const rightDrawerCloseEvent = snapshot - .getLoadable(rightDrawerCloseEventState()) + .getLoadable(rightDrawerCloseEventState) .getValue(); const isSameEvent = diff --git a/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts b/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts index e0028a7c5e08..df0cd28ca53c 100644 --- a/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts +++ b/packages/twenty-front/src/modules/ui/layout/right-drawer/types/RightDrawerPages.ts @@ -2,4 +2,5 @@ export enum RightDrawerPages { CreateActivity = 'create-activity', EditActivity = 'edit-activity', ViewEmailThread = 'view-email-thread', + ViewCalendarEvent = 'view-calendar-event', } diff --git a/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/__tests__/useSelectableList.test.ts b/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/__tests__/useSelectableList.test.ts index a7bbe398dd50..930ea10ad521 100644 --- a/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/__tests__/useSelectableList.test.ts +++ b/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/__tests__/useSelectableList.test.ts @@ -21,7 +21,7 @@ describe('useSelectableList', () => { selectableListScopeId, }); - const selectableItemIds = useRecoilValue(selectableItemIdsState()); + const selectableItemIds = useRecoilValue(selectableItemIdsState); return { setSelectableItemIds, @@ -51,9 +51,8 @@ describe('useSelectableList', () => { selectableListScopeId, }); - const [selectedItemId, setSelectedItemId] = useRecoilState( - selectedItemIdState(), - ); + const [selectedItemId, setSelectedItemId] = + useRecoilState(selectedItemIdState); return { resetSelectedItem, diff --git a/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/internal/useSelectableListHotKeys.tsx b/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/internal/useSelectableListHotKeys.tsx index 91340db8d05c..9d668f3e0a3e 100644 --- a/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/internal/useSelectableListHotKeys.tsx +++ b/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/internal/useSelectableListHotKeys.tsx @@ -40,13 +40,10 @@ export const useSelectableListHotKeys = ( const handleSelect = useRecoilCallback( ({ snapshot, set }) => (direction: Direction) => { - const selectedItemId = getSnapshotValue( - snapshot, - selectedItemIdState(), - ); + const selectedItemId = getSnapshotValue(snapshot, selectedItemIdState); const selectableItemIds = getSnapshotValue( snapshot, - selectableItemIdsState(), + selectableItemIdsState, ); const currentPosition = findPosition(selectableItemIds, selectedItemId); @@ -107,7 +104,7 @@ export const useSelectableListHotKeys = ( if (selectedItemId !== nextId) { if (isNonEmptyString(nextId)) { set(isSelectedItemIdSelector(nextId), true); - set(selectedItemIdState(), nextId); + set(selectedItemIdState, nextId); } if (isNonEmptyString(selectedItemId)) { @@ -138,11 +135,11 @@ export const useSelectableListHotKeys = ( () => { const selectedItemId = getSnapshotValue( snapshot, - selectedItemIdState(), + selectedItemIdState, ); const onEnter = getSnapshotValue( snapshot, - selectableListOnEnterState(), + selectableListOnEnterState, ); if (isNonEmptyString(selectedItemId)) { diff --git a/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/useSelectableList.ts b/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/useSelectableList.ts index 7d33e15ac18e..37098d3c90c3 100644 --- a/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/useSelectableList.ts +++ b/packages/twenty-front/src/modules/ui/layout/selectable-list/hooks/useSelectableList.ts @@ -13,12 +13,12 @@ export const useSelectableList = (selectableListId?: string) => { selectableListScopeId: selectableListId, }); - const setSelectableItemIds = useSetRecoilState(selectableItemIdsState()); + const setSelectableItemIds = useSetRecoilState(selectableItemIdsState); const setSelectableListOnEnter = useSetRecoilState( - selectableListOnEnterState(), + selectableListOnEnterState, ); - const resetSelectedItemIdState = useResetRecoilState(selectedItemIdState()); + const resetSelectedItemIdState = useResetRecoilState(selectedItemIdState); const resetSelectedItem = () => { resetSelectedItemIdState(); diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageMoreButton.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageMoreButton.tsx index 3b75efe6269a..fee1454b6f8c 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageMoreButton.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageMoreButton.tsx @@ -26,7 +26,7 @@ export const ShowPageMoreButton = ({ objectNameSingular: string; }) => { const { closeDropdown, toggleDropdown } = useDropdown('more-show-page'); - const navigationMemorizedUrl = useRecoilValue(navigationMemorizedUrlState()); + const navigationMemorizedUrl = useRecoilValue(navigationMemorizedUrlState); const navigate = useNavigate(); const { deleteOneRecord } = useDeleteOneRecord({ diff --git a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx index f8d4b9deefcd..001904181cf3 100644 --- a/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx +++ b/packages/twenty-front/src/modules/ui/layout/show-page/components/ShowPageRightContainer.tsx @@ -3,6 +3,7 @@ import { useRecoilValue } from 'recoil'; import { Calendar } from '@/activities/calendar/components/Calendar'; import { EmailThreads } from '@/activities/emails/components/EmailThreads'; +import { Events } from '@/activities/events/components/Events'; import { Attachments } from '@/activities/files/components/Attachments'; import { Notes } from '@/activities/notes/components/Notes'; import { ObjectTasks } from '@/activities/tasks/components/ObjectTasks'; @@ -61,10 +62,12 @@ export const ShowPageRightContainer = ({ notes, emails, }: ShowPageRightContainerProps) => { - const { getActiveTabIdState } = useTabList(TAB_LIST_COMPONENT_ID); - const activeTabId = useRecoilValue(getActiveTabIdState()); + const { activeTabIdState } = useTabList(TAB_LIST_COMPONENT_ID); + const activeTabId = useRecoilValue(activeTabIdState); const shouldDisplayCalendarTab = useIsFeatureEnabled('IS_CALENDAR_ENABLED'); + const shouldDisplayLogTab = useIsFeatureEnabled('IS_EVENT_OBJECT_ENABLED'); + const shouldDisplayEmailsTab = (emails && targetableObject.targetObjectNameSingular === @@ -101,7 +104,6 @@ export const ShowPageRightContainer = ({ title: 'Emails', Icon: IconMail, hide: !shouldDisplayEmailsTab, - hasBetaPill: true, }, { id: 'calendar', @@ -109,6 +111,13 @@ export const ShowPageRightContainer = ({ Icon: IconCalendarEvent, hide: !shouldDisplayCalendarTab, }, + { + id: 'logs', + title: 'Logs', + Icon: IconTimelineEvent, + hide: !shouldDisplayLogTab, + hasBetaPill: true, + }, ]; return ( @@ -131,6 +140,7 @@ export const ShowPageRightContainer = ({ )} {activeTabId === 'emails' && } {activeTabId === 'calendar' && } + {activeTabId === 'logs' && } ); }; diff --git a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx index 8554be46b83c..ce1f2a942021 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/components/TabList.tsx @@ -36,9 +36,9 @@ const StyledContainer = styled.div` export const TabList = ({ tabs, tabListId }: TabListProps) => { const initialActiveTabId = tabs[0].id; - const { getActiveTabIdState, setActiveTabId } = useTabList(tabListId); + const { activeTabIdState, setActiveTabId } = useTabList(tabListId); - const activeTabId = useRecoilValue(getActiveTabIdState()); + const activeTabId = useRecoilValue(activeTabIdState); React.useEffect(() => { setActiveTabId(initialActiveTabId); diff --git a/packages/twenty-front/src/modules/ui/layout/tab/hooks/__tests__/useTabList.test.tsx b/packages/twenty-front/src/modules/ui/layout/tab/hooks/__tests__/useTabList.test.tsx index 986e243bfa8b..83e30532fcf3 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/hooks/__tests__/useTabList.test.tsx +++ b/packages/twenty-front/src/modules/ui/layout/tab/hooks/__tests__/useTabList.test.tsx @@ -8,12 +8,11 @@ describe('useTabList', () => { it('Should update the activeTabId state', async () => { const { result } = renderHook( () => { - const { getActiveTabIdState, setActiveTabId } = + const { activeTabIdState, setActiveTabId } = useTabList('TEST_TAB_LIST_ID'); - const activeTabId = useRecoilValue(getActiveTabIdState()); + const activeTabId = useRecoilValue(activeTabIdState); return { - getActiveTabIdState: getActiveTabIdState, activeTabId, setActiveTabId: setActiveTabId, }; @@ -22,7 +21,6 @@ describe('useTabList', () => { wrapper: RecoilRoot, }, ); - expect(result.current.getActiveTabIdState).toBeInstanceOf(Function); expect(result.current.setActiveTabId).toBeInstanceOf(Function); expect(result.current.activeTabId).toBeNull(); diff --git a/packages/twenty-front/src/modules/ui/layout/tab/hooks/internal/useTabListStates.ts b/packages/twenty-front/src/modules/ui/layout/tab/hooks/internal/useTabListStates.ts index 6915852548ef..2d33932be5a7 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/hooks/internal/useTabListStates.ts +++ b/packages/twenty-front/src/modules/ui/layout/tab/hooks/internal/useTabListStates.ts @@ -15,9 +15,6 @@ export const useTabListStates = ({ tabListScopeId }: useTabListStatesProps) => { return { scopeId, - getActiveTabIdState: extractComponentState( - activeTabIdComponentState, - scopeId, - ), + activeTabIdState: extractComponentState(activeTabIdComponentState, scopeId), }; }; diff --git a/packages/twenty-front/src/modules/ui/layout/tab/hooks/useTabList.ts b/packages/twenty-front/src/modules/ui/layout/tab/hooks/useTabList.ts index c02624ada45e..9925dec4c655 100644 --- a/packages/twenty-front/src/modules/ui/layout/tab/hooks/useTabList.ts +++ b/packages/twenty-front/src/modules/ui/layout/tab/hooks/useTabList.ts @@ -3,14 +3,14 @@ import { useSetRecoilState } from 'recoil'; import { useTabListStates } from '@/ui/layout/tab/hooks/internal/useTabListStates'; export const useTabList = (tabListId?: string) => { - const { getActiveTabIdState } = useTabListStates({ + const { activeTabIdState } = useTabListStates({ tabListScopeId: `${tabListId}-scope`, }); - const setActiveTabId = useSetRecoilState(getActiveTabIdState()); + const setActiveTabId = useSetRecoilState(activeTabIdState); return { - getActiveTabIdState, + activeTabIdState, setActiveTabId, }; }; diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx index 2c7b9db3b552..bf5c2586a0af 100644 --- a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/ActionBar.tsx @@ -28,8 +28,8 @@ const StyledContainerActionBar = styled.div` `; export const ActionBar = () => { - const contextMenuIsOpen = useRecoilValue(contextMenuIsOpenState()); - const actionBarEntries = useRecoilValue(actionBarEntriesState()); + const contextMenuIsOpen = useRecoilValue(contextMenuIsOpenState); + const actionBarEntries = useRecoilValue(actionBarEntriesState); const wrapperRef = useRef(null); if (contextMenuIsOpen) { diff --git a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/__stories__/ActionBar.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/__stories__/ActionBar.stories.tsx index 460ee2048789..24daaea85575 100644 --- a/packages/twenty-front/src/modules/ui/navigation/action-bar/components/__stories__/ActionBar.stories.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/action-bar/components/__stories__/ActionBar.stories.tsx @@ -9,7 +9,7 @@ import { actionBarOpenState } from '../../states/actionBarIsOpenState'; import { ActionBar } from '../ActionBar'; const FilledActionBar = () => { - const setActionBarOpenState = useSetRecoilState(actionBarOpenState()); + const setActionBarOpenState = useSetRecoilState(actionBarOpenState); setActionBarOpenState(true); return ; }; diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenu.tsx b/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenu.tsx index 519c6bf87a09..8a3a1cfd0e83 100644 --- a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenu.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/context-menu/components/ContextMenu.tsx @@ -37,10 +37,10 @@ const StyledContainerContextMenu = styled.div` `; export const ContextMenu = () => { - const contextMenuPosition = useRecoilValue(contextMenuPositionState()); - const contextMenuIsOpen = useRecoilValue(contextMenuIsOpenState()); - const contextMenuEntries = useRecoilValue(contextMenuEntriesState()); - const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState()); + const contextMenuPosition = useRecoilValue(contextMenuPositionState); + const contextMenuIsOpen = useRecoilValue(contextMenuIsOpenState); + const contextMenuEntries = useRecoilValue(contextMenuEntriesState); + const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState); const wrapperRef = useRef(null); useListenClickOutside({ diff --git a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/__stories__/ContextMenu.stories.tsx b/packages/twenty-front/src/modules/ui/navigation/context-menu/components/__stories__/ContextMenu.stories.tsx index d0a5f4a79823..ee9eddd2f817 100644 --- a/packages/twenty-front/src/modules/ui/navigation/context-menu/components/__stories__/ContextMenu.stories.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/context-menu/components/__stories__/ContextMenu.stories.tsx @@ -10,12 +10,12 @@ import { contextMenuPositionState } from '../../states/contextMenuPositionState' import { ContextMenu } from '../ContextMenu'; const FilledContextMenu = () => { - const setContextMenuPosition = useSetRecoilState(contextMenuPositionState()); + const setContextMenuPosition = useSetRecoilState(contextMenuPositionState); setContextMenuPosition({ x: 100, y: 10, }); - const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState()); + const setContextMenuOpenState = useSetRecoilState(contextMenuIsOpenState); setContextMenuOpenState(true); return ; }; diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemSuggestion.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemSuggestion.tsx new file mode 100644 index 000000000000..52d3a12ee8b4 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemSuggestion.tsx @@ -0,0 +1,79 @@ +import { MouseEvent } from 'react'; +import styled from '@emotion/styled'; + +import { IconComponent } from '@/ui/display/icon/types/IconComponent'; +import { HOVER_BACKGROUND } from '@/ui/theme/constants/HoverBackground'; + +import { MenuItemLeftContent } from '../internals/components/MenuItemLeftContent'; +import { StyledMenuItemLeftContent } from '../internals/components/StyledMenuItemBase'; + +export type MenuItemSuggestionProps = { + LeftIcon?: IconComponent | null; + text: string; + selected?: boolean; + className?: string; + onClick?: (event: MouseEvent) => void; +}; + +const StyledSuggestionMenuItem = styled.li<{ + selected?: boolean; +}>` + --horizontal-padding: ${({ theme }) => theme.spacing(1)}; + --vertical-padding: ${({ theme }) => theme.spacing(2)}; + + align-items: center; + + border-radius: ${({ theme }) => theme.border.radius.sm}; + cursor: pointer; + + display: flex; + + flex-direction: row; + + font-size: ${({ theme }) => theme.font.size.sm}; + + gap: ${({ theme }) => theme.spacing(2)}; + + height: calc(32px - 2 * var(--vertical-padding)); + justify-content: space-between; + + padding: var(--vertical-padding) var(--horizontal-padding); + + background: ${({ selected, theme }) => + selected ? theme.background.transparent.medium : ''}; + + ${HOVER_BACKGROUND}; + + position: relative; + user-select: none; + + width: calc(100% - 2 * var(--horizontal-padding)); +`; + +export const MenuItemSuggestion = ({ + LeftIcon, + text, + className, + selected, + onClick, +}: MenuItemSuggestionProps) => { + const handleMenuItemClick = (event: MouseEvent) => { + if (!onClick) return; + event.preventDefault(); + event.stopPropagation(); + + onClick?.(event); + }; + + return ( + + + + + + ); +}; diff --git a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemToggle.tsx b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemToggle.tsx index 70ed9dfb6a6e..99465e0ca8d5 100644 --- a/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemToggle.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/menu-item/components/MenuItemToggle.tsx @@ -24,12 +24,8 @@ export const MenuItemToggle = ({ onToggleChange, toggleSize, }: MenuItemToggleProps) => { - const handleOnClick = () => { - onToggleChange?.(!toggled); - }; - return ( - + ` align-items: center; - background: ${({ isKeySelected, theme }) => - isKeySelected - ? theme.background.transparent.light - : theme.background.secondary}; border-radius: ${({ theme }) => theme.border.radius.sm}; cursor: pointer; @@ -108,10 +104,10 @@ export const StyledHoverableMenuItemBase = styled(StyledMenuItemBase)<{ isMenuOpen: boolean; }>` & .hoverable-buttons { - opacity: ${({ isMenuOpen }) => (isMenuOpen ? 1 : 0)}; pointer-events: none; position: fixed; right: ${({ theme }) => theme.spacing(2)}; + opacity: 0; transition: opacity ${({ theme }) => theme.animation.duration.instant}s ease; } diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx new file mode 100644 index 000000000000..04e3217db7de --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton.tsx @@ -0,0 +1,124 @@ +import { useState } from 'react'; +import { useTheme } from '@emotion/react'; +import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; + +import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { Workspaces } from '@/auth/states/workspaces'; +import { IconChevronDown } from '@/ui/display/icon'; +import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; +import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; +import { MenuItemSelectAvatar } from '@/ui/navigation/menu-item/components/MenuItemSelectAvatar'; +import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo'; +import { MULTI_WORKSPACE_DROPDOWN_ID } from '@/ui/navigation/navigation-drawer/constants/MulitWorkspaceDropdownId'; +import { useWorkspaceSwitching } from '@/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching'; +import { NavigationDrawerHotKeyScope } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerHotKeyScope'; + +const StyledLogo = styled.div<{ logo: string }>` + background: url(${({ logo }) => logo}); + background-position: center; + background-size: cover; + border-radius: ${({ theme }) => theme.border.radius.xs}; + height: 16px; + width: 16px; +`; + +const StyledContainer = styled.div` + align-items: center; + cursor: pointer; + color: ${({ theme }) => theme.font.color.primary}; + border-radius: ${({ theme }) => theme.border.radius.sm}; + border: 1px solid transparent; + display: flex; + justify-content: space-between; + height: ${({ theme }) => theme.spacing(7)}; + padding: 0 ${({ theme }) => theme.spacing(2)}; + width: 100%; + + &:hover { + background-color: ${({ theme }) => theme.background.transparent.lighter}; + border: 1px solid ${({ theme }) => theme.border.color.medium}; + } +`; + +const StyledLabel = styled.div` + align-items: center; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; +`; + +const StyledIconChevronDown = styled(IconChevronDown)<{ disabled?: boolean }>` + color: ${({ disabled, theme }) => + disabled ? theme.font.color.extraLight : theme.font.color.tertiary}; +`; + +type MultiWorkspaceDropdownButtonProps = { + workspaces: Workspaces[]; +}; + +export const MultiWorkspaceDropdownButton = ({ + workspaces, +}: MultiWorkspaceDropdownButtonProps) => { + const theme = useTheme(); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + + const [isMultiWorkspaceDropdownOpen, setToggleMultiWorkspaceDropdown] = + useState(false); + + const { switchWorkspace } = useWorkspaceSwitching(); + + const { closeDropdown } = useDropdown(MULTI_WORKSPACE_DROPDOWN_ID); + + const handleChange = async (workspaceId: string) => { + setToggleMultiWorkspaceDropdown(!isMultiWorkspaceDropdownOpen); + closeDropdown(); + await switchWorkspace(workspaceId); + }; + + return ( + + + {currentWorkspace?.displayName ?? ''} + + + } + dropdownComponents={ + + {workspaces.map((workspace) => ( + + } + selected={currentWorkspace?.id === workspace.id} + onClick={() => handleChange(workspace.id)} + /> + ))} + + } + /> + ); +}; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx index 14f4cc3b6c91..30f478a9336f 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerBackButton.tsx @@ -37,7 +37,7 @@ export const NavigationDrawerBackButton = ({ }: NavigationDrawerBackButtonProps) => { const theme = useTheme(); const navigate = useNavigate(); - const navigationMemorizedUrl = useRecoilValue(navigationMemorizedUrlState()); + const navigationMemorizedUrl = useRecoilValue(navigationMemorizedUrlState); return ( diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx index 568fbb4b6b85..eae27e8c73d3 100644 --- a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/components/NavigationDrawerHeader.tsx @@ -1,5 +1,10 @@ import styled from '@emotion/styled'; +import { useRecoilValue } from 'recoil'; +import { workspacesState } from '@/auth/states/workspaces'; +import { MultiWorkspaceDropdownButton } from '@/ui/navigation/navigation-drawer/components/MultiWorkspaceDropdownButton'; +import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo'; +import { DEFAULT_WORKSPACE_NAME } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceName'; import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile'; import { NavigationDrawerCollapseButton } from './NavigationDrawerCollapseButton'; @@ -44,16 +49,24 @@ type NavigationDrawerHeaderProps = { }; export const NavigationDrawerHeader = ({ - name = 'Twenty', - logo = 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAACAAAAAgCAYAAABzenr0AAAACXBIWXMAAA7EAAAOxAGVKw4bAAACb0lEQVR4nO2VO4taQRTHr3AblbjxEVlwCwVhg7BoqqCIjy/gAyyFWNlYBOxsfH0KuxgQGwXRUkGuL2S7i1barGAgiwbdW93SnGOc4BonPiKahf3DwXFmuP/fPM4ZlvmlTxAhCBdzHnEQWYiv7Mr4C3NeuVYhQYDPzOUUQgDLBQGcLHNhvQK8DACPx8PTxiqVyvISG43GbyaT6Qfpn06n0m63e/tPAPF4vJ1MJu8kEsnWTCkWi1yr1RKGw+GDRqPBOTfr44vFQvD7/Q/lcpmaaVQAr9fLp1IpO22c47hGOBz+MB6PH+Vy+VYDAL8qlUoGtVotzOfzq4MAgsHgE/6KojiQyWR/bKVSqbSszHFM8Pl8z1YK48JsNltCOBwOnrYLO+8AAIjb+nHbycoTiUQfDJ7tFq4YAHiVSmXBxcD41u8flQU8z7fhzO0r83atVns3Go3u9Xr9x0O/RQXo9/tsIBBg6vX606a52Wz+bZ7P5/WwG29gxSJzhKgA6XTaDoFNF+krFAocmC//4yWEcSf2wTm7mCO19xFgSsKOLI16vV7b7XY7mRNoLwA0JymJ5uQIzgIAuX5PzDElT2m+E8BqtQ4ymcx7Yq7T6a6ZE4sKgOadTucaCwkxp1UzlEKh0GDxIXOwDWHAdi6Xe3swQDQa/Q7mywoolUpvsaptymazDWKxmBHTlWXZm405BFZoNpuGgwEmk4mE2SGtVivii4f1AO7J3ZopkQCQj7Ar1FeRChCJRJzVapX6DKNIfSc1Ax+wtQWQ55h6bH8FWDfYV4fO3wlwDr0C/BcADYiTPCxHqIEA2QsCZAkAKnRGkMbKN/sTX5YHPQ1e7SkAAAAASUVORK5CYII=', + name = DEFAULT_WORKSPACE_NAME, + logo = DEFAULT_WORKSPACE_LOGO, showCollapseButton, }: NavigationDrawerHeaderProps) => { const isMobile = useIsMobile(); + const workspaces = useRecoilValue(workspacesState); return ( - - {name} + {workspaces !== null && workspaces.length > 1 ? ( + + ) : ( + <> + + {name} + + )} + {!isMobile && ( { + const navigate = useNavigate(); + const setTokenPair = useSetRecoilState(tokenPairState); + const [generateJWT] = useGenerateJwtMutation(); + const currentWorkspace = useRecoilValue(currentWorkspaceState); + + const switchWorkspace = async (workspaceId: string) => { + if (currentWorkspace?.id === workspaceId) return; + const jwt = await generateJWT({ + variables: { + workspaceId, + }, + }); + + if (isDefined(jwt.errors)) { + throw jwt.errors; + } + + if (!isDefined(jwt.data?.generateJWT)) { + throw new Error('could not create token'); + } + + const { tokens } = jwt.data.generateJWT; + setTokenPair(tokens); + navigate(`/objects/companies`); + window.location.reload(); + }; + + return { switchWorkspace }; +}; diff --git a/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/types/NavigationDrawerHotKeyScope.ts b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/types/NavigationDrawerHotKeyScope.ts new file mode 100644 index 000000000000..834b5d50ea56 --- /dev/null +++ b/packages/twenty-front/src/modules/ui/navigation/navigation-drawer/types/NavigationDrawerHotKeyScope.ts @@ -0,0 +1,3 @@ +export enum NavigationDrawerHotKeyScope { + MultiWorkspaceDropdownButton = 'multi-workspace-dropdown', +} diff --git a/packages/twenty-front/src/modules/ui/navigation/step-bar/hooks/__tests__/useStepBar.test.tsx b/packages/twenty-front/src/modules/ui/navigation/step-bar/hooks/__tests__/useStepBar.test.tsx index 5dfbd9c79a89..9acd228fb980 100644 --- a/packages/twenty-front/src/modules/ui/navigation/step-bar/hooks/__tests__/useStepBar.test.tsx +++ b/packages/twenty-front/src/modules/ui/navigation/step-bar/hooks/__tests__/useStepBar.test.tsx @@ -11,7 +11,7 @@ const renderHooks = (initialStep: number) => { const { nextStep, prevStep, reset, setStep } = useStepBar({ initialStep, }); - const stepBarInternal = useRecoilValue(stepBarInternalState()); + const stepBarInternal = useRecoilValue(stepBarInternalState); return { nextStep, diff --git a/packages/twenty-front/src/modules/ui/navigation/step-bar/hooks/useStepBar.ts b/packages/twenty-front/src/modules/ui/navigation/step-bar/hooks/useStepBar.ts index f567d9c4da9e..0bc14494209c 100644 --- a/packages/twenty-front/src/modules/ui/navigation/step-bar/hooks/useStepBar.ts +++ b/packages/twenty-front/src/modules/ui/navigation/step-bar/hooks/useStepBar.ts @@ -8,9 +8,8 @@ export type StepsOptions = { }; export const useStepBar = ({ initialStep }: StepsOptions) => { - const [stepBarInternal, setStepBarInternal] = useRecoilState( - stepBarInternalState(), - ); + const [stepBarInternal, setStepBarInternal] = + useRecoilState(stepBarInternalState); const nextStep = () => { setStepBarInternal((prevState) => ({ diff --git a/packages/twenty-front/src/modules/ui/theme/constants/BackgroundDark.ts b/packages/twenty-front/src/modules/ui/theme/constants/BackgroundDark.ts index c5204dbbaf33..e91329882d07 100644 --- a/packages/twenty-front/src/modules/ui/theme/constants/BackgroundDark.ts +++ b/packages/twenty-front/src/modules/ui/theme/constants/BackgroundDark.ts @@ -20,6 +20,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-front/src/modules/ui/theme/constants/BackgroundLight.ts b/packages/twenty-front/src/modules/ui/theme/constants/BackgroundLight.ts index 3247f5f9fbf5..e78f8e6bb5cc 100644 --- a/packages/twenty-front/src/modules/ui/theme/constants/BackgroundLight.ts +++ b/packages/twenty-front/src/modules/ui/theme/constants/BackgroundLight.ts @@ -20,6 +20,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-front/src/modules/ui/theme/constants/OverlayBackground.ts b/packages/twenty-front/src/modules/ui/theme/constants/OverlayBackground.ts index b86741fff75a..25bcbe4493fe 100644 --- a/packages/twenty-front/src/modules/ui/theme/constants/OverlayBackground.ts +++ b/packages/twenty-front/src/modules/ui/theme/constants/OverlayBackground.ts @@ -3,7 +3,7 @@ import { css } from '@emotion/react'; import { ThemeType } from './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-front/src/modules/ui/theme/hooks/__tests__/useColorScheme.test.tsx b/packages/twenty-front/src/modules/ui/theme/hooks/__tests__/useColorScheme.test.tsx index 84125306122c..f84692bfad9e 100644 --- a/packages/twenty-front/src/modules/ui/theme/hooks/__tests__/useColorScheme.test.tsx +++ b/packages/twenty-front/src/modules/ui/theme/hooks/__tests__/useColorScheme.test.tsx @@ -32,7 +32,7 @@ describe('useColorScheme', () => { const colorScheme = useColorScheme(); const setCurrentWorkspaceMember = useSetRecoilState( - currentWorkspaceMemberState(), + currentWorkspaceMemberState, ); setCurrentWorkspaceMember(workspaceMember); diff --git a/packages/twenty-front/src/modules/ui/theme/hooks/useColorScheme.ts b/packages/twenty-front/src/modules/ui/theme/hooks/useColorScheme.ts index 46cfa93571d2..e3b46c7adbad 100644 --- a/packages/twenty-front/src/modules/ui/theme/hooks/useColorScheme.ts +++ b/packages/twenty-front/src/modules/ui/theme/hooks/useColorScheme.ts @@ -8,7 +8,7 @@ import { ColorScheme } from '@/workspace-member/types/WorkspaceMember'; export const useColorScheme = () => { const [currentWorkspaceMember, setCurrentWorkspaceMember] = useRecoilState( - currentWorkspaceMemberState(), + currentWorkspaceMemberState, ); const { updateOneRecord: updateOneWorkspaceMember } = useUpdateOneRecord({ diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/usePreviousHotkeyScope.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/usePreviousHotkeyScope.ts index d3eb6d043e7f..d332b9df0eb5 100644 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/usePreviousHotkeyScope.ts +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/usePreviousHotkeyScope.ts @@ -13,7 +13,7 @@ export const usePreviousHotkeyScope = () => { ({ snapshot, set }) => () => { const previousHotkeyScope = snapshot - .getLoadable(previousHotkeyScopeState()) + .getLoadable(previousHotkeyScopeState) .getValue(); if (!previousHotkeyScope) { @@ -25,7 +25,7 @@ export const usePreviousHotkeyScope = () => { previousHotkeyScope.customScopes, ); - set(previousHotkeyScopeState(), null); + set(previousHotkeyScopeState, null); }, [setHotkeyScope], ); @@ -34,11 +34,11 @@ export const usePreviousHotkeyScope = () => { ({ snapshot, set }) => (scope: string, customScopes?: CustomHotkeyScopes) => { const currentHotkeyScope = snapshot - .getLoadable(currentHotkeyScopeState()) + .getLoadable(currentHotkeyScopeState) .getValue(); setHotkeyScope(scope, customScopes); - set(previousHotkeyScopeState(), currentHotkeyScope); + set(previousHotkeyScopeState, currentHotkeyScope); }, [setHotkeyScope], ); diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeyCallback.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeyCallback.ts index 92ebc8b89821..6e3d2d102087 100644 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeyCallback.ts +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeyCallback.ts @@ -24,7 +24,7 @@ export const useScopedHotkeyCallback = () => preventDefault?: boolean; }) => { const currentHotkeyScopes = snapshot - .getLoadable(internalHotkeysEnabledScopesState()) + .getLoadable(internalHotkeysEnabledScopesState) .getValue(); if (!currentHotkeyScopes.includes(scope)) { diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeys.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeys.ts index b8059643c4a2..b8c7ff31dadf 100644 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeys.ts +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useScopedHotkeys.ts @@ -22,8 +22,7 @@ export const useScopedHotkeys = ( preventDefault: true, }, ) => { - const [pendingHotkey, setPendingHotkey] = - useRecoilState(pendingHotkeyState()); + const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState); const callScopedHotkeyCallback = useScopedHotkeyCallback(); diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useSequenceScopedHotkeys.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useSequenceScopedHotkeys.ts index e2a4ead6b0ad..1ab3fe8e04fa 100644 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useSequenceScopedHotkeys.ts +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useSequenceScopedHotkeys.ts @@ -20,8 +20,7 @@ export const useSequenceHotkeys = ( }, deps: any[] = [], ) => { - const [pendingHotkey, setPendingHotkey] = - useRecoilState(pendingHotkeyState()); + const [pendingHotkey, setPendingHotkey] = useRecoilState(pendingHotkeyState); const callScopedHotkeyCallback = useScopedHotkeyCallback(); diff --git a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts index aee1f213f231..ad890e08b465 100644 --- a/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts +++ b/packages/twenty-front/src/modules/ui/utilities/hotkey/hooks/useSetHotkeyScope.ts @@ -26,7 +26,7 @@ export const useSetHotkeyScope = () => ({ snapshot, set }) => async (hotkeyScopeToSet: string, customScopes?: CustomHotkeyScopes) => { const currentHotkeyScope = snapshot - .getLoadable(currentHotkeyScopeState()) + .getLoadable(currentHotkeyScopeState) .getValue(); if (currentHotkeyScope.scope === hotkeyScopeToSet) { @@ -76,8 +76,8 @@ export const useSetHotkeyScope = () => } scopesToSet.push(newHotkeyScope.scope); - set(internalHotkeysEnabledScopesState(), scopesToSet); - set(currentHotkeyScopeState(), newHotkeyScope); + set(internalHotkeysEnabledScopesState, scopesToSet); + set(currentHotkeyScopeState, newHotkeyScope); }, [], ); diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useClickOutsideListener.test.tsx b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useClickOutsideListener.test.tsx index 5916fa12ad7d..04d2b882544f 100644 --- a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useClickOutsideListener.test.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/__tests__/useClickOutsideListener.test.tsx @@ -15,9 +15,7 @@ describe('useClickOutsideListener', () => { return { useClickOutside: useClickOutsideListener(componentId), - isActivated: useRecoilValue( - getClickOutsideListenerIsActivatedState(), - ), + isActivated: useRecoilValue(getClickOutsideListenerIsActivatedState), }; }, { diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOutsideListener.ts b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOutsideListener.ts index e1db02e4837c..a03da62510f8 100644 --- a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOutsideListener.ts +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useClickOutsideListener.ts @@ -34,7 +34,7 @@ export const useClickOutsideListener = (componentId: string) => { callback(event); const additionalCallbacks = snapshot - .getLoadable(getClickOutsideListenerCallbacksState()) + .getLoadable(getClickOutsideListenerCallbacksState) .getValue(); additionalCallbacks.forEach((additionalCallback) => { @@ -51,7 +51,7 @@ export const useClickOutsideListener = (componentId: string) => { const toggleClickOutsideListener = useRecoilCallback( ({ set }) => (activated: boolean) => { - set(getClickOutsideListenerIsActivatedState(), activated); + set(getClickOutsideListenerIsActivatedState, activated); }, [getClickOutsideListenerIsActivatedState], ); @@ -60,7 +60,7 @@ export const useClickOutsideListener = (componentId: string) => { ({ set, snapshot }) => ({ callbackFunction, callbackId }: ClickOutsideListenerCallback) => { const existingCallbacks = snapshot - .getLoadable(getClickOutsideListenerCallbacksState()) + .getLoadable(getClickOutsideListenerCallbacksState) .getValue(); const existingCallbackWithSameId = existingCallbacks.find( @@ -74,7 +74,7 @@ export const useClickOutsideListener = (componentId: string) => { }); set( - getClickOutsideListenerCallbacksState(), + getClickOutsideListenerCallbacksState, existingCallbacksWithNewCallback, ); } else { @@ -95,7 +95,7 @@ export const useClickOutsideListener = (componentId: string) => { }; set( - getClickOutsideListenerCallbacksState(), + getClickOutsideListenerCallbacksState, existingCallbacksWithOverwrittenCallback, ); } @@ -107,7 +107,7 @@ export const useClickOutsideListener = (componentId: string) => { ({ set, snapshot }) => ({ callbackId }: { callbackId: string }) => { const existingCallbacks = snapshot - .getLoadable(getClickOutsideListenerCallbacksState()) + .getLoadable(getClickOutsideListenerCallbacksState) .getValue(); const indexOfCallbackToUnsubscribe = existingCallbacks.findIndex( @@ -121,7 +121,7 @@ export const useClickOutsideListener = (componentId: string) => { existingCallbacks.toSpliced(indexOfCallbackToUnsubscribe, 1); set( - getClickOutsideListenerCallbacksState(), + getClickOutsideListenerCallbacksState, newCallbacksWithoutCallbackToUnsubscribe, ); } diff --git a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutsideV2.ts b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutsideV2.ts index e7f178e7d761..287d6b0f8e6d 100644 --- a/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutsideV2.ts +++ b/packages/twenty-front/src/modules/ui/utilities/pointer-event/hooks/useListenClickOutsideV2.ts @@ -32,7 +32,7 @@ export const useListenClickOutsideV2 = ({ ({ snapshot, set }) => (event: MouseEvent | TouchEvent) => { const clickOutsideListenerIsActivated = snapshot - .getLoadable(getClickOutsideListenerIsActivatedState()) + .getLoadable(getClickOutsideListenerIsActivatedState) .getValue(); const isListening = clickOutsideListenerIsActivated && enabled; @@ -47,7 +47,7 @@ export const useListenClickOutsideV2 = ({ .some((ref) => ref.current?.contains(event.target as Node)); set( - getClickOutsideListenerIsMouseDownInsideState(), + getClickOutsideListenerIsMouseDownInsideState, clickedOnAtLeastOneRef, ); } @@ -84,7 +84,7 @@ export const useListenClickOutsideV2 = ({ }); set( - getClickOutsideListenerIsMouseDownInsideState(), + getClickOutsideListenerIsMouseDownInsideState, clickedOnAtLeastOneRef, ); } @@ -102,7 +102,7 @@ export const useListenClickOutsideV2 = ({ ({ snapshot }) => (event: MouseEvent | TouchEvent) => { const isMouseDownInside = snapshot - .getLoadable(getClickOutsideListenerIsMouseDownInsideState()) + .getLoadable(getClickOutsideListenerIsMouseDownInsideState) .getValue(); if (mode === ClickOutsideMode.compareHTMLRef) { diff --git a/packages/twenty-front/src/modules/ui/utilities/recoil-scope/utils/getScopedFamilyStateDeprecated.ts b/packages/twenty-front/src/modules/ui/utilities/recoil-scope/utils/getScopedFamilyStateDeprecated.ts deleted file mode 100644 index 89dad50b9498..000000000000 --- a/packages/twenty-front/src/modules/ui/utilities/recoil-scope/utils/getScopedFamilyStateDeprecated.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { RecoilState, SerializableParam } from 'recoil'; - -import { ComponentFamilyStateKey } from '@/ui/utilities/state/component-state/types/ComponentFamilyStateKey'; - -export const getScopedFamilyStateDeprecated = < - StateType, - FamilyKey extends SerializableParam, ->( - recoilState: ( - scopedFamilyKey: ComponentFamilyStateKey, - ) => RecoilState, - scopeId: string, - familyKey: FamilyKey, -) => { - return recoilState({ - scopeId, - familyKey: familyKey || ('' as FamilyKey), - }); -}; diff --git a/packages/twenty-front/src/modules/ui/utilities/recoil-scope/utils/getScopedSelectorDeprecated.ts b/packages/twenty-front/src/modules/ui/utilities/recoil-scope/utils/getScopedSelectorDeprecated.ts deleted file mode 100644 index 7ff7d3d9147f..000000000000 --- a/packages/twenty-front/src/modules/ui/utilities/recoil-scope/utils/getScopedSelectorDeprecated.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { RecoilScopedSelector } from '../types/RecoilScopedSelector'; - -export const getScopedSelectorDeprecated = ( - recoilScopedState: RecoilScopedSelector, - scopeId: string, -) => { - return recoilScopedState({ - scopeId, - }); -}; diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/__tests__/useListenScroll.test.tsx b/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/__tests__/useListenScroll.test.tsx index ccde6fbca899..4d254e0f99c4 100644 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/__tests__/useListenScroll.test.tsx +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/__tests__/useListenScroll.test.tsx @@ -24,7 +24,7 @@ describe('useListenScroll', () => { const { result } = renderHook( () => { useListenScroll({ scrollableRef: containerRef }); - const isScrolling = useRecoilValue(isScrollingState()); + const isScrolling = useRecoilValue(isScrollingState); return { isScrolling }; }, diff --git a/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useListenScroll.ts b/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useListenScroll.ts index 969f24e0e36e..7607c51fef7a 100644 --- a/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useListenScroll.ts +++ b/packages/twenty-front/src/modules/ui/utilities/scroll/hooks/useListenScroll.ts @@ -15,7 +15,7 @@ export const useListenScroll = ({ const hideScrollBarsCallback = useRecoilCallback( ({ snapshot }) => () => { - const isScrolling = snapshot.getLoadable(isScrollingState()).getValue(); + const isScrolling = snapshot.getLoadable(isScrollingState).getValue(); if (!isScrolling) { scrollableRef.current?.classList.remove('scrolling'); @@ -27,13 +27,13 @@ export const useListenScroll = ({ const handleScrollStart = useRecoilCallback( ({ set }) => (event: Event) => { - set(isScrollingState(), true); + set(isScrollingState, true); scrollableRef.current?.classList.add('scrolling'); const target = event.target as HTMLElement; - set(scrollTopState(), target.scrollTop); - set(scrollLeftState(), target.scrollLeft); + set(scrollTopState, target.scrollTop); + set(scrollLeftState, target.scrollLeft); }, [scrollableRef], ); @@ -41,7 +41,7 @@ export const useListenScroll = ({ const handleScrollEnd = useRecoilCallback( ({ set }) => () => { - set(isScrollingState(), false); + set(isScrollingState, false); debounce(hideScrollBarsCallback, 1000)(); }, [hideScrollBarsCallback], diff --git a/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/extractComponentState.ts b/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/extractComponentState.ts index c9b6478c2e50..ea797fe77ea7 100644 --- a/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/extractComponentState.ts +++ b/packages/twenty-front/src/modules/ui/utilities/state/component-state/utils/extractComponentState.ts @@ -8,5 +8,5 @@ export const extractComponentState = ( ) => RecoilState, scopeId: string, ) => { - return () => componentState({ scopeId }); + return componentState({ scopeId }); }; diff --git a/packages/twenty-front/src/modules/ui/utilities/state/utils/createState.ts b/packages/twenty-front/src/modules/ui/utilities/state/utils/createState.ts index b2fdfcdd0cfd..c81d9a86db73 100644 --- a/packages/twenty-front/src/modules/ui/utilities/state/utils/createState.ts +++ b/packages/twenty-front/src/modules/ui/utilities/state/utils/createState.ts @@ -14,5 +14,5 @@ export const createState = ({ default: defaultValue, effects, }); - return () => recoilState; + return recoilState; }; diff --git a/packages/twenty-front/src/modules/users/components/UserProvider.tsx b/packages/twenty-front/src/modules/users/components/UserProvider.tsx index e871755179a5..43e5f04e617e 100644 --- a/packages/twenty-front/src/modules/users/components/UserProvider.tsx +++ b/packages/twenty-front/src/modules/users/components/UserProvider.tsx @@ -1,10 +1,11 @@ -import { useEffect, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useQuery } from '@apollo/client'; import { useSetRecoilState } from 'recoil'; import { currentUserState } from '@/auth/states/currentUserState'; import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { Workspaces, workspacesState } from '@/auth/states/workspaces'; import { GET_CURRENT_USER } from '@/users/graphql/queries/getCurrentUser'; import { ColorScheme } from '@/workspace-member/types/WorkspaceMember'; import { isDefined } from '~/utils/isDefined'; @@ -12,11 +13,12 @@ import { isDefined } from '~/utils/isDefined'; export const UserProvider = ({ children }: React.PropsWithChildren) => { const [isLoading, setIsLoading] = useState(true); - const setCurrentUser = useSetRecoilState(currentUserState()); - const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState()); + const setCurrentUser = useSetRecoilState(currentUserState); + const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); + const setWorkspaces = useSetRecoilState(workspacesState); const setCurrentWorkspaceMember = useSetRecoilState( - currentWorkspaceMemberState(), + currentWorkspaceMemberState, ); const { loading: queryLoading, data: queryData } = useQuery(GET_CURRENT_USER); @@ -36,12 +38,25 @@ export const UserProvider = ({ children }: React.PropsWithChildren) => { colorScheme: (workspaceMember.colorScheme as ColorScheme) ?? 'Light', }); } + if (isDefined(queryData?.currentUser?.workspaces)) { + const validWorkspaces = queryData.currentUser.workspaces.filter( + (obj: any) => obj.workspace !== null && obj.workspace !== undefined, + ); + const workspaces: Workspaces[] = []; + validWorkspaces.forEach((validWorkspace: any) => { + const workspace = validWorkspace.workspace! as Workspaces; + workspaces.push(workspace); + }); + + setWorkspaces(workspaces); + } }, [ setCurrentUser, isLoading, queryLoading, setCurrentWorkspace, setCurrentWorkspaceMember, + setWorkspaces, queryData?.currentUser, ]); diff --git a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts index a19b5ff18f16..bd02380fae6b 100644 --- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts +++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts @@ -34,5 +34,13 @@ export const USER_QUERY_FRAGMENT = gql` workspaceId } } + workspaces { + workspace { + id + logo + displayName + domainName + } + } } `; diff --git a/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts b/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts index 5568ae49c464..03a6a06a4e61 100644 --- a/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts +++ b/packages/twenty-front/src/modules/users/graphql/queries/getCurrentUser.ts @@ -37,6 +37,7 @@ export const GET_CURRENT_USER = gql` } currentBillingSubscription { status + interval } } workspaces { diff --git a/packages/twenty-front/src/modules/users/utils/getProfilePictureAbsoluteURI.ts b/packages/twenty-front/src/modules/users/utils/getProfilePictureAbsoluteURI.ts index 86f97c1df47c..b38880af8fe0 100644 --- a/packages/twenty-front/src/modules/users/utils/getProfilePictureAbsoluteURI.ts +++ b/packages/twenty-front/src/modules/users/utils/getProfilePictureAbsoluteURI.ts @@ -1,4 +1,4 @@ -import { REACT_APP_SERVER_FILES_URL } from '~/config'; +import { REACT_APP_SERVER_BASE_URL } from '~/config'; export const getImageAbsoluteURIOrBase64 = (imageUrl?: string | null) => { if (!imageUrl) { @@ -13,7 +13,7 @@ export const getImageAbsoluteURIOrBase64 = (imageUrl?: string | null) => { return imageUrl; } - const serverFilesUrl = REACT_APP_SERVER_FILES_URL; + const serverFilesUrl = REACT_APP_SERVER_BASE_URL; - return `${serverFilesUrl}/${imageUrl}`; + return `${serverFilesUrl}/files/${imageUrl}`; }; diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx index b054c58fb8de..af75297298a9 100644 --- a/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx +++ b/packages/twenty-front/src/modules/views/components/EditableFilterChip.tsx @@ -1,10 +1,10 @@ +import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; import { getOperandLabelShort } from '@/object-record/object-filter-dropdown/utils/getOperandLabel'; import { useIcons } from '@/ui/display/icon/hooks/useIcons'; import { SortOrFilterChip } from '@/views/components/SortOrFilterChip'; -import { ViewFilter } from '@/views/types/ViewFilter'; type EditableFilterChipProps = { - viewFilter: ViewFilter; + viewFilter: Filter; onRemove: () => void; }; diff --git a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx index 0f9ef89b1b64..aa10156cdc03 100644 --- a/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx +++ b/packages/twenty-front/src/modules/views/components/EditableFilterDropdownButton.tsx @@ -1,18 +1,19 @@ import { useCallback, useEffect } from 'react'; +import { useRecoilValue } from 'recoil'; import { MultipleFiltersDropdownContent } from '@/object-record/object-filter-dropdown/components/MultipleFiltersDropdownContent'; import { useFilterDropdown } from '@/object-record/object-filter-dropdown/hooks/useFilterDropdown'; +import { Filter } from '@/object-record/object-filter-dropdown/types/Filter'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; import { EditableFilterChip } from '@/views/components/EditableFilterChip'; -import { useViewBar } from '@/views/hooks/useViewBar'; -import { ViewFilter } from '@/views/types/ViewFilter'; +import { useCombinedViewFilters } from '@/views/hooks/useCombinedViewFilters'; import { isDefined } from '~/utils/isDefined'; type EditableFilterDropdownButtonProps = { viewFilterDropdownId: string; - viewFilter: ViewFilter; + viewFilter: Filter; hotkeyScope: HotkeyScope; }; @@ -22,7 +23,7 @@ export const EditableFilterDropdownButton = ({ hotkeyScope, }: EditableFilterDropdownButtonProps) => { const { - availableFilterDefinitions, + availableFilterDefinitionsState, setFilterDefinitionUsedInDropdown, setSelectedOperandInDropdown, setSelectedFilter, @@ -30,9 +31,13 @@ export const EditableFilterDropdownButton = ({ filterDropdownId: viewFilterDropdownId, }); + const availableFilterDefinitions = useRecoilValue( + availableFilterDefinitionsState, + ); + const { closeDropdown } = useDropdown(viewFilterDropdownId); - const { removeViewFilter } = useViewBar(); + const { removeCombinedViewFilter } = useCombinedViewFilters(); useEffect(() => { const filterDefinition = availableFilterDefinitions.find( @@ -57,15 +62,15 @@ export const EditableFilterDropdownButton = ({ const handleRemove = () => { closeDropdown(); - removeViewFilter(viewFilter.fieldMetadataId); + removeCombinedViewFilter(viewFilter.fieldMetadataId); }; const handleDropdownClickOutside = useCallback(() => { const { value, fieldMetadataId } = viewFilter; if (!value) { - removeViewFilter(fieldMetadataId); + removeCombinedViewFilter(fieldMetadataId); } - }, [viewFilter, removeViewFilter]); + }, [viewFilter, removeCombinedViewFilter]); return ( { - const { removeViewSort, upsertViewSort } = useViewBar(); + const { removeCombinedViewSort, upsertCombinedViewSort } = + useCombinedViewSorts(); const handleRemoveClick = () => { - removeViewSort(viewSort.fieldMetadataId); + removeCombinedViewSort(viewSort.fieldMetadataId); }; const handleClick = () => { - upsertViewSort({ + upsertCombinedViewSort({ ...viewSort, direction: viewSort.direction === 'asc' ? 'desc' : 'asc', }); @@ -27,7 +28,6 @@ export const EditableSortChip = ({ viewSort }: EditableSortChipProps) => { testId={viewSort.fieldMetadataId} labelValue={viewSort.definition.label} Icon={viewSort.direction === 'desc' ? IconArrowDown : IconArrowUp} - isSort onRemove={handleRemoveClick} onClick={handleClick} /> diff --git a/packages/twenty-front/src/modules/views/components/FilterQueryParamsEffect.tsx b/packages/twenty-front/src/modules/views/components/FilterQueryParamsEffect.tsx deleted file mode 100644 index fe72b24bc0d4..000000000000 --- a/packages/twenty-front/src/modules/views/components/FilterQueryParamsEffect.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import { useEffect } from 'react'; -import { useRecoilValue, useSetRecoilState } from 'recoil'; - -import { useFiltersFromQueryParams } from '@/views/hooks/internal/useFiltersFromQueryParams'; -import { useViewScopedStates } from '@/views/hooks/internal/useViewScopedStates'; -import { useViewBar } from '@/views/hooks/useViewBar'; - -export const FilterQueryParamsEffect = () => { - const { hasFiltersQueryParams, getFiltersFromQueryParams } = - useFiltersFromQueryParams(); - const { currentViewFiltersState, onViewFiltersChangeState } = - useViewScopedStates(); - const setCurrentViewFilters = useSetRecoilState(currentViewFiltersState); - const onViewFiltersChange = useRecoilValue(onViewFiltersChangeState); - const { resetViewBar } = useViewBar(); - - useEffect(() => { - if (!hasFiltersQueryParams) return; - - getFiltersFromQueryParams().then((filtersFromParams) => { - if (Array.isArray(filtersFromParams)) { - setCurrentViewFilters(filtersFromParams); - onViewFiltersChange?.(filtersFromParams); - } - }); - - return () => { - resetViewBar(); - }; - }, [ - getFiltersFromQueryParams, - hasFiltersQueryParams, - onViewFiltersChange, - resetViewBar, - setCurrentViewFilters, - ]); - - return null; -}; diff --git a/packages/twenty-front/src/modules/views/components/QueryParamsFiltersEffect.tsx b/packages/twenty-front/src/modules/views/components/QueryParamsFiltersEffect.tsx new file mode 100644 index 000000000000..25278a77c9c4 --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/QueryParamsFiltersEffect.tsx @@ -0,0 +1,39 @@ +import { useEffect } from 'react'; +import { useSetRecoilState } from 'recoil'; + +import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams'; +import { useViewStates } from '@/views/hooks/internal/useViewStates'; +import { useResetCurrentView } from '@/views/hooks/useResetCurrentView'; + +export const QueryParamsFiltersEffect = () => { + const { hasFiltersQueryParams, getFiltersFromQueryParams } = + useViewFromQueryParams(); + const { unsavedToUpsertViewFiltersState } = useViewStates(); + const setUnsavedViewFilter = useSetRecoilState( + unsavedToUpsertViewFiltersState, + ); + const { resetCurrentView } = useResetCurrentView(); + + useEffect(() => { + if (!hasFiltersQueryParams) { + return; + } + + getFiltersFromQueryParams().then((filtersFromParams) => { + if (Array.isArray(filtersFromParams)) { + setUnsavedViewFilter(filtersFromParams); + } + }); + + return () => { + resetCurrentView(); + }; + }, [ + getFiltersFromQueryParams, + hasFiltersQueryParams, + resetCurrentView, + setUnsavedViewFilter, + ]); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/views/components/QueryParamsViewIdEffect.tsx b/packages/twenty-front/src/modules/views/components/QueryParamsViewIdEffect.tsx new file mode 100644 index 000000000000..a7130fb3bf17 --- /dev/null +++ b/packages/twenty-front/src/modules/views/components/QueryParamsViewIdEffect.tsx @@ -0,0 +1,38 @@ +import { useEffect } from 'react'; +import { isUndefined } from '@sniptt/guards'; +import { useRecoilState } from 'recoil'; + +import { useViewFromQueryParams } from '@/views/hooks/internal/useViewFromQueryParams'; +import { useViewStates } from '@/views/hooks/internal/useViewStates'; +import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; +import { isDefined } from '~/utils/isDefined'; + +export const QueryParamsViewIdEffect = () => { + const { getFiltersFromQueryParams, viewIdQueryParam } = + useViewFromQueryParams(); + const { currentViewIdState } = useViewStates(); + + const [currentViewId, setCurrentViewId] = useRecoilState(currentViewIdState); + const { viewsOnCurrentObject } = useGetCurrentView(); + + useEffect(() => { + const indexView = viewsOnCurrentObject.find((view) => view.key === 'INDEX'); + + if (isUndefined(viewIdQueryParam) && isDefined(indexView)) { + setCurrentViewId(indexView.id); + return; + } + + if (isDefined(viewIdQueryParam)) { + setCurrentViewId(viewIdQueryParam); + } + }, [ + currentViewId, + getFiltersFromQueryParams, + setCurrentViewId, + viewIdQueryParam, + viewsOnCurrentObject, + ]); + + return <>; +}; diff --git a/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx b/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx index b7fb3717c123..13fe04c9358b 100644 --- a/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx +++ b/packages/twenty-front/src/modules/views/components/SortOrFilterChip.tsx @@ -4,11 +4,7 @@ import styled from '@emotion/styled'; import { IconX } from '@/ui/display/icon/index'; import { IconComponent } from '@/ui/display/icon/types/IconComponent'; -type StyledChipProps = { - isSort?: boolean; -}; - -const StyledChip = styled.div` +const StyledChip = styled.div` align-items: center; background-color: ${({ theme }) => theme.accent.quaternary}; border: 1px solid ${({ theme }) => theme.accent.tertiary}; @@ -19,7 +15,7 @@ const StyledChip = styled.div` flex-direction: row; flex-shrink: 0; font-size: ${({ theme }) => theme.font.size.sm}; - font-weight: ${({ isSort }) => (isSort ? 'bold' : 'normal')}; + font-weight: ${({ theme }) => theme.font.weight.medium}; padding: ${({ theme }) => theme.spacing(1) + ' ' + theme.spacing(2)}; user-select: none; `; @@ -54,7 +50,6 @@ type SortOrFilterChipProps = { Icon?: IconComponent; onRemove: () => void; onClick?: () => void; - isSort?: boolean; testId?: string; }; @@ -63,7 +58,6 @@ export const SortOrFilterChip = ({ labelValue, Icon, onRemove, - isSort, testId, onClick, }: SortOrFilterChipProps) => { @@ -75,7 +69,7 @@ export const SortOrFilterChip = ({ }; return ( - + {Icon && ( diff --git a/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx b/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx index ac8c6da739e0..0bc6c43164f4 100644 --- a/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx +++ b/packages/twenty-front/src/modules/views/components/UpdateViewButtonGroup.tsx @@ -1,21 +1,24 @@ import { useCallback } from 'react'; import styled from '@emotion/styled'; -import { useRecoilValue } from 'recoil'; +import { useRecoilValue, useSetRecoilState } from 'recoil'; import { IconChevronDown, IconPlus } from '@/ui/display/icon'; import { Button } from '@/ui/input/button/components/Button'; import { ButtonGroup } from '@/ui/input/button/components/ButtonGroup'; import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown'; import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer'; +import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown'; import { MenuItem } from '@/ui/navigation/menu-item/components/MenuItem'; import { HotkeyScope } from '@/ui/utilities/hotkey/types/HotkeyScope'; -import { UPDATE_VIEW_DROPDOWN_ID } from '@/views/constants/UpdateViewDropdownId'; -import { useViewBar } from '@/views/hooks/useViewBar'; - -import { useViewScopedStates } from '../hooks/internal/useViewScopedStates'; +import { UPDATE_VIEW_BUTTON_DROPDOWN_ID } from '@/views/constants/UpdateViewButtonDropdownId'; +import { useViewStates } from '@/views/hooks/internal/useViewStates'; +import { useGetCurrentView } from '@/views/hooks/useGetCurrentView'; +import { useSaveCurrentViewFiltersAndSorts } from '@/views/hooks/useSaveCurrentViewFiltersAndSorts'; +import { VIEW_PICKER_DROPDOWN_ID } from '@/views/view-picker/constants/ViewPickerDropdownId'; +import { useViewPickerMode } from '@/views/view-picker/hooks/useViewPickerMode'; +import { useViewPickerStates } from '@/views/view-picker/hooks/useViewPickerStates'; const StyledContainer = styled.div` - background: ${({ theme }) => theme.color.blue}; border-radius: ${({ theme }) => theme.border.radius.md}; display: inline-flex; margin-right: ${({ theme }) => theme.spacing(2)}; @@ -24,29 +27,51 @@ const StyledContainer = styled.div` export type UpdateViewButtonGroupProps = { hotkeyScope: HotkeyScope; - onViewEditModeChange?: () => void; }; export const UpdateViewButtonGroup = ({ hotkeyScope, - onViewEditModeChange, }: UpdateViewButtonGroupProps) => { - const { updateCurrentView, setViewEditMode } = useViewBar(); - const { canPersistFiltersSelector, canPersistSortsSelector } = - useViewScopedStates(); + const { canPersistViewSelector, currentViewIdState } = useViewStates(); + const { saveCurrentViewFilterAndSorts } = useSaveCurrentViewFiltersAndSorts(); + + const { setViewPickerMode } = useViewPickerMode(); + const { viewPickerReferenceViewIdState } = useViewPickerStates(); + const canPersistView = useRecoilValue(canPersistViewSelector()); - const canPersistFilters = useRecoilValue(canPersistFiltersSelector); - const canPersistSorts = useRecoilValue(canPersistSortsSelector); + const { closeDropdown: closeUpdateViewButtonDropdown } = useDropdown( + UPDATE_VIEW_BUTTON_DROPDOWN_ID, + ); + const { openDropdown: openViewPickerDropdown } = useDropdown( + VIEW_PICKER_DROPDOWN_ID, + ); + const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView(); + + const currentViewId = useRecoilValue(currentViewIdState); + + const setViewPickerReferenceViewId = useSetRecoilState( + viewPickerReferenceViewIdState, + ); - const canPersistView = canPersistFilters || canPersistSorts; + const handleViewCreate = useCallback(() => { + if (!currentViewId) { + return; + } + openViewPickerDropdown(); + setViewPickerReferenceViewId(currentViewId); + setViewPickerMode('create'); - const handleCreateViewButtonClick = useCallback(() => { - setViewEditMode('create'); - onViewEditModeChange?.(); - }, [setViewEditMode, onViewEditModeChange]); + closeUpdateViewButtonDropdown(); + }, [ + closeUpdateViewButtonDropdown, + currentViewId, + openViewPickerDropdown, + setViewPickerMode, + setViewPickerReferenceViewId, + ]); - const handleViewSubmit = async () => { - await updateCurrentView?.(); + const handleViewUpdate = async () => { + await saveCurrentViewFilterAndSorts(); }; if (!canPersistView) { @@ -55,27 +80,42 @@ export const UpdateViewButtonGroup = ({ return ( - -