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 f7e14be3a000..312f4a45e691 100644
--- a/package.json
+++ b/package.json
@@ -6,7 +6,7 @@
"@aws-sdk/client-s3": "^3.363.0",
"@aws-sdk/credential-providers": "^3.363.0",
"@blocknote/core": "^0.12.1",
- "@blocknote/react": "^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",
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/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/docker-compose.mdx b/packages/twenty-docs/docs/start/self-hosting/docker-compose.mdx
index 0204e30fb281..a5594f7edf5b 100644
--- a/packages/twenty-docs/docs/start/self-hosting/docker-compose.mdx
+++ b/packages/twenty-docs/docs/start/self-hosting/docker-compose.mdx
@@ -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
diff --git a/packages/twenty-docs/package.json b/packages/twenty-docs/package.json
index b89e70bfa1f8..27d018ebc423 100644
--- a/packages/twenty-docs/package.json
+++ b/packages/twenty-docs/package.json
@@ -1,6 +1,6 @@
{
"name": "twenty-docs",
- "version": "0.3.2",
+ "version": "0.3.3",
"private": true,
"scripts": {
"nx": "NX_DEFAULT_PROJECT=twenty-docs node ../../node_modules/nx/bin/nx.js",
diff --git a/packages/twenty-emails/package.json b/packages/twenty-emails/package.json
index 9f22cd6e78ca..07a405c39e01 100644
--- a/packages/twenty-emails/package.json
+++ b/packages/twenty-emails/package.json
@@ -1,6 +1,6 @@
{
"name": "twenty-emails",
- "version": "0.3.2",
+ "version": "0.3.3",
"description": "",
"author": "",
"private": true,
diff --git a/packages/twenty-front/nyc.config.cjs b/packages/twenty-front/nyc.config.cjs
index bf92d9cde36d..9ce4ed713711 100644
--- a/packages/twenty-front/nyc.config.cjs
+++ b/packages/twenty-front/nyc.config.cjs
@@ -6,8 +6,8 @@ const globalCoverage = {
};
const modulesCoverage = {
- statements: 75,
- lines: 75,
+ statements: 70,
+ lines: 70,
functions: 70,
include: ['src/modules/**/*'],
exclude: ['src/**/*.ts'],
diff --git a/packages/twenty-front/package.json b/packages/twenty-front/package.json
index 9539629bfedd..219a903b5cdd 100644
--- a/packages/twenty-front/package.json
+++ b/packages/twenty-front/package.json
@@ -1,6 +1,6 @@
{
"name": "twenty-front",
- "version": "0.3.2",
+ "version": "0.3.3",
"private": true,
"type": "module",
"scripts": {
diff --git a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx b/packages/twenty-front/src/effect-components/PageChangeEffect.tsx
index 2cf692c3e3b5..820ce959cd60 100644
--- a/packages/twenty-front/src/effect-components/PageChangeEffect.tsx
+++ b/packages/twenty-front/src/effect-components/PageChangeEffect.tsx
@@ -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 63d820235287..927fde13800e 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -73,6 +73,7 @@ export type Billing = {
export type BillingSubscription = {
__typename?: 'BillingSubscription';
id: Scalars['ID']['output'];
+ interval?: Maybe;
status: Scalars['String']['output'];
};
@@ -263,7 +264,6 @@ export enum FieldMetadataType {
DateTime = 'DATE_TIME',
Email = 'EMAIL',
FullName = 'FULL_NAME',
- Json = 'JSON',
Link = 'LINK',
MultiSelect = 'MULTI_SELECT',
Number = 'NUMBER',
@@ -272,6 +272,7 @@ export enum FieldMetadataType {
Position = 'POSITION',
Probability = 'PROBABILITY',
Rating = 'RATING',
+ RawJson = 'RAW_JSON',
Relation = 'RELATION',
Select = 'SELECT',
Text = 'TEXT',
@@ -341,6 +342,7 @@ export type Mutation = {
renewToken: AuthTokens;
signUp: LoginToken;
track: Analytics;
+ updateBillingSubscription: UpdateBillingEntity;
updateOneField: Field;
updateOneObject: Object;
updatePasswordViaResetToken: InvalidatePassword;
@@ -545,6 +547,8 @@ export type Query = {
fields: FieldConnection;
findWorkspaceFromInviteHash: Workspace;
getProductPrices: ProductPricesEntity;
+ getTimelineCalendarEventsFromCompanyId: TimelineCalendarEventsWithTotal;
+ getTimelineCalendarEventsFromPersonId: TimelineCalendarEventsWithTotal;
getTimelineThreadsFromCompanyId: TimelineThreadsWithTotal;
getTimelineThreadsFromPersonId: TimelineThreadsWithTotal;
object: Object;
@@ -591,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'];
@@ -697,7 +715,7 @@ export type Sentry = {
export type SessionEntity = {
__typename?: 'SessionEntity';
- url: Scalars['String']['output'];
+ url?: Maybe;
};
/** Sort Directions */
@@ -724,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;
@@ -760,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;
diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx
index 1c89af101e9b..3a576f6b2008 100644
--- a/packages/twenty-front/src/generated/graphql.tsx
+++ b/packages/twenty-front/src/generated/graphql.tsx
@@ -68,6 +68,7 @@ export type Billing = {
export type BillingSubscription = {
__typename?: 'BillingSubscription';
id: Scalars['ID'];
+ interval?: Maybe;
status: Scalars['String'];
};
@@ -183,7 +184,6 @@ export enum FieldMetadataType {
DateTime = 'DATE_TIME',
Email = 'EMAIL',
FullName = 'FULL_NAME',
- Json = 'JSON',
Link = 'LINK',
MultiSelect = 'MULTI_SELECT',
Number = 'NUMBER',
@@ -192,6 +192,7 @@ export enum FieldMetadataType {
Position = 'POSITION',
Probability = 'PROBABILITY',
Rating = 'RATING',
+ RawJson = 'RAW_JSON',
Relation = 'RELATION',
Select = 'SELECT',
Text = 'TEXT',
@@ -254,10 +255,10 @@ export type Mutation = {
generateJWT: AuthTokens;
generateTransientToken: TransientToken;
impersonate: Verify;
- removeWorkspaceMember: Scalars['String'];
renewToken: AuthTokens;
signUp: LoginToken;
track: Analytics;
+ updateBillingSubscription: UpdateBillingEntity;
updateOneObject: Object;
updatePasswordViaResetToken: InvalidatePassword;
updateWorkspace: Workspace;
@@ -312,11 +313,6 @@ export type MutationImpersonateArgs = {
};
-export type MutationRemoveWorkspaceMemberArgs = {
- memberId: Scalars['String'];
-};
-
-
export type MutationRenewTokenArgs = {
refreshToken: Scalars['String'];
};
@@ -660,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;
@@ -1045,6 +1047,11 @@ 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; }>;
@@ -1083,14 +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 RemoveWorkspaceMemberMutationVariables = Exact<{
- memberId: Scalars['String'];
-}>;
-
-
-export type RemoveWorkspaceMemberMutation = { __typename?: 'Mutation', removeWorkspaceMember: string };
+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;
@@ -2006,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 {
@@ -2225,6 +2257,7 @@ export const GetCurrentUserDocument = gql`
}
currentBillingSubscription {
status
+ interval
}
}
workspaces {
@@ -2265,37 +2298,6 @@ export function useGetCurrentUserLazyQuery(baseOptions?: Apollo.LazyQueryHookOpt
export type GetCurrentUserQueryHookResult = ReturnType;
export type GetCurrentUserLazyQueryHookResult = ReturnType;
export type GetCurrentUserQueryResult = Apollo.QueryResult;
-export const RemoveWorkspaceMemberDocument = gql`
- mutation RemoveWorkspaceMember($memberId: String!) {
- removeWorkspaceMember(memberId: $memberId)
-}
- `;
-export type RemoveWorkspaceMemberMutationFn = Apollo.MutationFunction;
-
-/**
- * __useRemoveWorkspaceMemberMutation__
- *
- * To run a mutation, you first call `useRemoveWorkspaceMemberMutation` within a React component and pass it any options that fit your needs.
- * When your component renders, `useRemoveWorkspaceMemberMutation` 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 [removeWorkspaceMemberMutation, { data, loading, error }] = useRemoveWorkspaceMemberMutation({
- * variables: {
- * memberId: // value for 'memberId'
- * },
- * });
- */
-export function useRemoveWorkspaceMemberMutation(baseOptions?: Apollo.MutationHookOptions) {
- const options = {...defaultOptions, ...baseOptions}
- return Apollo.useMutation(RemoveWorkspaceMemberDocument, options);
- }
-export type RemoveWorkspaceMemberMutationHookResult = ReturnType;
-export type RemoveWorkspaceMemberMutationResult = Apollo.MutationResult;
-export type RemoveWorkspaceMemberMutationOptions = Apollo.BaseMutationOptions;
export const ActivateWorkspaceDocument = gql`
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
activateWorkspace(data: $input) {
diff --git a/packages/twenty-front/src/index.tsx b/packages/twenty-front/src/index.tsx
index ac59cf8827ea..c23babc438ea 100644
--- a/packages/twenty-front/src/index.tsx
+++ b/packages/twenty-front/src/index.tsx
@@ -2,7 +2,6 @@ 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';
@@ -34,10 +33,6 @@ 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(
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/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/object-metadata/components/ObjectMetadataNavItems.tsx b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx
index 43f5210bdff2..750d3ab418ac 100644
--- a/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx
+++ b/packages/twenty-front/src/modules/object-metadata/components/ObjectMetadataNavItems.tsx
@@ -6,6 +6,7 @@ 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 } = useObjectMetadataItemForSettings();
@@ -13,7 +14,9 @@ export const ObjectMetadataNavItems = () => {
const { getIcon } = useIcons();
const currentPath = useLocation().pathname;
- const { records } = usePrefetchedData(PrefetchKey.AllViews);
+ const { records: views } = usePrefetchedData(
+ PrefetchKey.AllViews,
+ );
return (
<>
@@ -45,9 +48,11 @@ export const ObjectMetadataNavItems = () => {
: -1;
}),
].map((objectMetadataItem) => {
- const viewId = records?.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/__mocks__/useCreateOneObjectMetadataItem.ts b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem.ts
index 263745e388b1..61df6cf56869 100644
--- a/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem.ts
+++ b/packages/twenty-front/src/modules/object-metadata/hooks/__mocks__/useCreateOneObjectMetadataItem.ts
@@ -21,6 +21,31 @@ export const query = gql`
}
`;
+export const findManyViewsQuery = gql`
+ query FindManyViews($filter: ViewFilterInput, $orderBy: ViewOrderByInput, $lastCursor: String, $limit: Float) {
+ views(filter: $filter, orderBy: $orderBy, first: $limit, after: $lastCursor) {
+ edges {
+ node {
+ __typename
+ id
+ objectMetadataId
+ type
+ createdAt
+ name
+ updatedAt
+ }
+ cursor
+ }
+ pageInfo {
+ hasNextPage
+ startCursor
+ endCursor
+ }
+ totalCount
+ }
+ }
+`;
+
export const variables = {
input: {
object: {
diff --git a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx
index 067eeff19cda..150db7f88da6 100644
--- a/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx
+++ b/packages/twenty-front/src/modules/object-metadata/hooks/__tests__/useCreateOneObjectMetadataItem.test.tsx
@@ -7,6 +7,7 @@ import { useCreateOneObjectMetadataItem } from '@/object-metadata/hooks/useCreat
import { TestApolloMetadataClientProvider } from '../__mocks__/ApolloMetadataClientProvider';
import {
+ findManyViewsQuery,
query,
responseData,
variables,
@@ -24,6 +25,27 @@ const mocks = [
},
})),
},
+ {
+ request: {
+ query: findManyViewsQuery,
+ variables: {},
+ },
+ result: jest.fn(() => ({
+ data: {
+ views: {
+ __typename: 'ViewConnection',
+ totalCount: 0,
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: false,
+ startCursor: '',
+ endCursor: '',
+ },
+ edges: [],
+ },
+ },
+ })),
+ },
];
const Wrapper = ({ children }: { children: ReactNode }) => (
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 624cc4066fd8..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,16 +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) ?? '',
- 'FindManyRecordsMultipleMetadataItems',
- ],
+ 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/types/CoreObjectNameSingular.ts b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts
index 13591a07d002..9886f09b722e 100644
--- a/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts
+++ b/packages/twenty-front/src/modules/object-metadata/types/CoreObjectNameSingular.ts
@@ -8,6 +8,7 @@ export enum CoreObjectNameSingular {
Comment = 'comment',
Company = 'company',
ConnectedAccount = 'connectedAccount',
+ Event = 'event',
Favorite = 'favorite',
Message = 'message',
MessageChannel = 'messageChannel',
@@ -15,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 f14d5607e409..e9549cf70904 100644
--- a/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts
+++ b/packages/twenty-front/src/modules/object-metadata/utils/getObjectMetadataItemsMock.ts
@@ -401,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',
@@ -436,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,
},
},
{
@@ -3537,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-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/record-board/hooks/internal/useRecordBoardStates.ts b/packages/twenty-front/src/modules/object-record/record-board/hooks/internal/useRecordBoardStates.ts
index 1c8f8a1849bd..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';
@@ -32,6 +33,10 @@ export const useRecordBoardStates = (recordBoardId?: string) => {
recordBoardObjectSingularNameComponentState,
scopeId,
),
+ kanbanFieldMetadataNameState: extractComponentState(
+ recordBoardKanbanFieldMetadataNameComponentState,
+ scopeId,
+ ),
isFetchingRecordState: extractComponentState(
isRecordBoardFetchingRecordsComponentState,
scopeId,
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 90f5a1ad93fd..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
@@ -10,6 +10,7 @@ export const useSetRecordBoardRecordIds = (recordBoardId?: string) => {
recordIdsByColumnIdFamilyState,
columnsFamilySelector,
columnIdsState,
+ kanbanFieldMetadataNameState,
} = useRecordBoardStates(recordBoardId);
const setRecordIds = useRecoilCallback(
@@ -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, columnIdsState, 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 f6a266d4746b..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
@@ -12,12 +12,16 @@ export const useRecordBoard = (recordBoardId?: string) => {
selectedRecordIdsSelector,
isCompactModeActiveState,
onFetchMoreVisibilityChangeState,
+ kanbanFieldMetadataNameState,
} = useRecordBoardStates(recordBoardId);
const { setColumns } = useSetRecordBoardColumns(recordBoardId);
const { setRecordIds } = useSetRecordBoardRecordIds(recordBoardId);
const setFieldDefinitions = useSetRecoilState(fieldDefinitionsState);
const setObjectSingularName = useSetRecoilState(objectSingularNameState);
+ const setKanbanFieldMetadataName = useSetRecoilState(
+ kanbanFieldMetadataNameState,
+ );
return {
scopeId,
@@ -25,6 +29,7 @@ export const useRecordBoard = (recordBoardId?: string) => {
setRecordIds,
setFieldDefinitions,
setObjectSingularName,
+ setKanbanFieldMetadataName,
selectedRecordIdsSelector,
isCompactModeActiveState,
onFetchMoreVisibilityChangeState,
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/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/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-index/components/RecordIndexBoardContainerEffect.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexBoardContainerEffect.tsx
index 3e7fff5bd0e7..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;
@@ -31,6 +34,7 @@ export const RecordIndexBoardContainerEffect = ({
selectedRecordIdsSelector,
setFieldDefinitions,
onFetchMoreVisibilityChangeState,
+ setKanbanFieldMetadataName,
} = useRecordBoard(recordBoardId);
const { fetchMoreRecords, loading } = useLoadRecordIndexBoard({
@@ -43,6 +47,10 @@ export const RecordIndexBoardContainerEffect = ({
onFetchMoreVisibilityChangeState,
);
+ const recordIndexKanbanFieldMetadataId = useRecoilValue(
+ recordIndexKanbanFieldMetadataIdState,
+ );
+
useEffect(() => {
setOnFetchMoreVisibilityChange(() => () => {
if (!loading) {
@@ -67,6 +75,7 @@ export const RecordIndexBoardContainerEffect = ({
setColumns(
computeRecordBoardColumnDefinitionsFromObjectMetadata(
objectMetadataItem,
+ recordIndexKanbanFieldMetadataId ?? '',
navigateToSelectSettings,
),
);
@@ -74,6 +83,7 @@ export const RecordIndexBoardContainerEffect = ({
navigateToSelectSettings,
objectMetadataItem,
objectNameSingular,
+ recordIndexKanbanFieldMetadataId,
setColumns,
]);
@@ -85,6 +95,24 @@ export const RecordIndexBoardContainerEffect = ({
setFieldDefinitions(recordIndexFieldDefinitions);
}, [objectMetadataItem, setFieldDefinitions, recordIndexFieldDefinitions]);
+ 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({
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 1d9e89b250d0..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';
@@ -65,6 +65,9 @@ export const RecordIndexContainer = ({
const setRecordIndexIsCompactModeActive = useSetRecoilState(
recordIndexIsCompactModeActiveState,
);
+ const setRecordIndexViewKanbanFieldMetadataIdState = useSetRecoilState(
+ recordIndexKanbanFieldMetadataIdState,
+ );
const { setTableFilters, setTableSorts, setTableColumns } = useRecordTable({
recordTableId: recordIndexId,
@@ -129,9 +132,11 @@ export const RecordIndexContainer = ({
mapViewSortsToSorts(view.viewSorts, sortDefinitions),
);
setRecordIndexViewType(view.type);
+ setRecordIndexViewKanbanFieldMetadataIdState(
+ view.kanbanFieldMetadataId,
+ );
setRecordIndexIsCompactModeActive(view.isCompact);
}}
- optionsDropdownScopeId={RECORD_INDEX_OPTIONS_DROPDOWN_ID}
/>
{
- const { setViewEditMode } = useViewBarEditMode(recordIndexId);
-
return (
}
- onClickOutside={() => setViewEditMode('none')}
/>
);
};
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 19f1e2594736..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,6 +1,5 @@
-import { useRef, useState } from 'react';
+import { useState } from 'react';
import { Key } from 'ts-key-enum';
-import { v4 } from 'uuid';
import { RECORD_INDEX_OPTIONS_DROPDOWN_ID } from '@/object-record/record-index/options/constants/RecordIndexOptionsDropdownId';
import { useRecordIndexOptionsForBoard } from '@/object-record/record-index/options/hooks/useRecordIndexOptionsForBoard';
@@ -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';
@@ -23,8 +21,6 @@ import { MenuItemToggle } from '@/ui/navigation/menu-item/components/MenuItemTog
import { useScopedHotkeys } from '@/ui/utilities/hotkey/hooks/useScopedHotkeys';
import { ViewFieldsVisibilityDropdownSection } from '@/views/components/ViewFieldsVisibilityDropdownSection';
import { useGetCurrentView } from '@/views/hooks/useGetCurrentView';
-import { useHandleViews } from '@/views/hooks/useHandleViews';
-import { useViewBarEditMode } from '@/views/hooks/useViewBarEditMode';
import { ViewType } from '@/views/types/ViewType';
type RecordIndexOptionsMenu = 'fields';
@@ -40,9 +36,6 @@ export const RecordIndexOptionsDropdownContent = ({
recordIndexId,
objectNameSingular,
}: RecordIndexOptionsDropdownContentProps) => {
- const { updateCurrentView, createEmptyView, selectView } =
- useHandleViews(recordIndexId);
- const { viewEditMode, setViewEditMode } = useViewBarEditMode(recordIndexId);
const { currentViewWithCombinedFiltersAndSorts } = useGetCurrentView();
const { closeDropdown } = useDropdown(RECORD_INDEX_OPTIONS_DROPDOWN_ID);
@@ -53,8 +46,6 @@ export const RecordIndexOptionsDropdownContent = ({
const resetMenu = () => setCurrentMenu(undefined);
- const viewEditInputRef = useRef(null);
-
const handleSelectMenu = (option: RecordIndexOptionsMenu) => {
setCurrentMenu(option);
};
@@ -67,25 +58,6 @@ export const RecordIndexOptionsDropdownContent = ({
TableOptionsHotkeyScope.Dropdown,
);
- useScopedHotkeys(
- Key.Enter,
- async () => {
- const name = viewEditInputRef.current?.value;
- if (viewEditMode === 'create') {
- const id = v4();
- await createEmptyView(id, name ?? '');
- selectView(id);
- } else {
- updateCurrentView({ name });
- }
-
- resetMenu();
- setViewEditMode('none');
- closeDropdown();
- },
- TableOptionsHotkeyScope.Dropdown,
- );
-
const {
handleColumnVisibilityChange,
handleReorderColumns,
@@ -128,37 +100,18 @@ export const RecordIndexOptionsDropdownContent = ({
return (
<>
{!currentMenu && (
- <>
-
+