From 6728e402561e6216dadf5794942926ec411f371b Mon Sep 17 00:00:00 2001
From: bosiraphael <71827178+bosiraphael@users.noreply.github.com>
Date: Sat, 27 Jul 2024 18:34:52 +0200
Subject: [PATCH] 5899 display a banner to alert users which need to reconnect
their account (#6301)
Closes #5899
---------
Co-authored-by: Charles Bochet
---
.../src/generated-metadata/graphql.ts | 17 ++-
.../twenty-front/src/generated/graphql.tsx | 25 ++--
.../calendar/types/CalendarEvent.ts | 4 +-
.../modules/auth/states/currentUserState.ts | 7 +-
.../information-banner/InformationBanner.tsx | 26 ++++
.../InformationBannerReconnectAccount.tsx | 38 ++++++
.../components/RecordIndexContainer.tsx | 2 +
...ettingsAccountsCalendarChannelsGeneral.tsx | 4 +-
.../ui/layout/page/SubMenuTopBarContainer.tsx | 8 +-
.../users/components/UserProviderEffect.tsx | 2 +-
.../graphql/fragments/userQueryFragment.ts | 1 +
.../src/pages/settings/Releases.tsx | 2 +-
.../src/pages/settings/SettingsBilling.tsx | 2 +-
.../settings/SettingsWorkspaceMembers.tsx | 4 +-
.../crm-migration/SettingsCRMMigration.tsx | 3 +-
.../api-keys/SettingsDevelopersApiKeysNew.tsx | 2 +-
...tingsIntegrationEditDatabaseConnection.tsx | 12 +-
...ttingsIntegrationNewDatabaseConnection.tsx | 2 +-
.../mock-data/timeline-calendar-events.ts | 12 +-
.../src/testing/mock-data/users.ts | 2 +
.../1721139150487-addKeyValuePairType.ts | 21 +++
...721656106498-migrateKeyValueTypeToJsonb.ts | 25 ++++
.../services/type-mapper.service.ts | 1 -
.../engine/core-modules/auth/auth.module.ts | 38 +++---
.../auth/services/google-apis.service.ts | 20 +++
.../key-value-pair/key-value-pair.entity.ts | 31 ++++-
.../key-value-pair/key-value-pair.service.ts | 105 ++++++++++----
.../onboarding/onboarding.module.ts | 10 +-
.../onboarding/onboarding.service.ts | 14 +-
.../user/services/user.service.ts | 16 +--
.../user-vars/services/user-vars.service.ts | 110 +++++++++++++++
.../user/user-vars/user-vars.module.ts | 12 ++
.../utils/__tests__/merge-user-vars.spec.ts | 128 ++++++++++++++++++
.../user-vars/utils/merge-user-vars.util.ts | 31 +++++
.../engine/core-modules/user/user.module.ts | 15 +-
.../engine/core-modules/user/user.resolver.ts | 53 ++++++--
.../calendar-event-import-manager.module.ts | 2 +
.../calendar-channel-sync-status.service.ts | 56 ++++++++
.../connected-account.module.ts | 12 ++
.../listeners/connected-account.listener.ts | 42 ++++++
.../services/accounts-to-reconnect.service.ts | 51 +++++++
.../types/connected-account-key-value.type.ts | 7 +
.../common/messaging-common.module.ts | 2 +
.../messaging-channel-sync-status.service.ts | 60 +++++++-
.../src/modules/modules.module.ts | 8 +-
.../src/display}/banner/components/Banner.tsx | 0
.../components/__stories__/Banner.stories.tsx | 11 +-
packages/twenty-ui/src/display/index.ts | 1 +
48 files changed, 910 insertions(+), 147 deletions(-)
create mode 100644 packages/twenty-front/src/modules/information-banner/InformationBanner.tsx
create mode 100644 packages/twenty-front/src/modules/information-banner/InformationBannerReconnectAccount.tsx
create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/1721139150487-addKeyValuePairType.ts
create mode 100644 packages/twenty-server/src/database/typeorm/core/migrations/1721656106498-migrateKeyValueTypeToJsonb.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/user/user-vars/services/user-vars.service.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/user/user-vars/user-vars.module.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/user/user-vars/utils/__tests__/merge-user-vars.spec.ts
create mode 100644 packages/twenty-server/src/engine/core-modules/user/user-vars/utils/merge-user-vars.util.ts
create mode 100644 packages/twenty-server/src/modules/connected-account/connected-account.module.ts
create mode 100644 packages/twenty-server/src/modules/connected-account/listeners/connected-account.listener.ts
create mode 100644 packages/twenty-server/src/modules/connected-account/services/accounts-to-reconnect.service.ts
create mode 100644 packages/twenty-server/src/modules/connected-account/types/connected-account-key-value.type.ts
rename packages/{twenty-front/src/modules/ui/layout => twenty-ui/src/display}/banner/components/Banner.tsx (100%)
rename packages/{twenty-front/src/modules/ui/layout => twenty-ui/src/display}/banner/components/__stories__/Banner.stories.tsx (69%)
diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts
index 45e025a09..c33006259 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -20,6 +20,8 @@ export type Scalars = {
DateTime: { input: any; output: any; }
/** The `JSON` scalar type represents JSON values as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
JSON: { input: any; output: any; }
+ /** The `JSONObject` scalar type represents JSON objects as specified by [ECMA-404](http://www.ecma-international.org/publications/files/ECMA-ST/ECMA-404.pdf). */
+ JSONObject: { input: any; output: any; }
/** A UUID scalar type */
UUID: { input: any; output: any; }
/** The `Upload` scalar type represents a file upload. */
@@ -381,6 +383,13 @@ export type LinkMetadata = {
url: Scalars['String']['output'];
};
+export type LinksMetadata = {
+ __typename?: 'LinksMetadata';
+ primaryLinkLabel: Scalars['String']['output'];
+ primaryLinkUrl: Scalars['String']['output'];
+ secondaryLinks?: Maybe>;
+};
+
export type LoginToken = {
__typename?: 'LoginToken';
loginToken: AuthToken;
@@ -1025,7 +1034,7 @@ export type Telemetry = {
export type TimelineCalendarEvent = {
__typename?: 'TimelineCalendarEvent';
- conferenceLink: LinkMetadata;
+ conferenceLink: LinksMetadata;
conferenceSolution: Scalars['String']['output'];
description: Scalars['String']['output'];
endsAt: Scalars['DateTime']['output'];
@@ -1186,12 +1195,9 @@ export type User = {
lastName: Scalars['String']['output'];
onboardingStatus?: Maybe;
passwordHash?: Maybe;
- /** @deprecated field migrated into the AppTokens Table ref: https://github.com/twentyhq/twenty/issues/5021 */
- passwordResetToken?: Maybe;
- /** @deprecated field migrated into the AppTokens Table ref: https://github.com/twentyhq/twenty/issues/5021 */
- passwordResetTokenExpiresAt?: Maybe;
supportUserHash?: Maybe;
updatedAt: Scalars['DateTime']['output'];
+ userVars: Scalars['JSONObject']['output'];
workspaceMember?: Maybe;
workspaces: Array;
};
@@ -1412,6 +1418,7 @@ export type ServerlessFunction = {
createdAt: Scalars['DateTime']['output'];
id: Scalars['UUID']['output'];
name: Scalars['String']['output'];
+ sourceCodeHash: Scalars['String']['output'];
syncStatus: ServerlessFunctionSyncStatus;
updatedAt: Scalars['DateTime']['output'];
};
diff --git a/packages/twenty-front/src/generated/graphql.tsx b/packages/twenty-front/src/generated/graphql.tsx
index 4cfb626e9..4bf49074e 100644
--- a/packages/twenty-front/src/generated/graphql.tsx
+++ b/packages/twenty-front/src/generated/graphql.tsx
@@ -16,6 +16,7 @@ export type Scalars = {
ConnectionCursor: any;
DateTime: string;
JSON: any;
+ JSONObject: any;
UUID: any;
Upload: any;
};
@@ -280,6 +281,13 @@ export type LinkMetadata = {
url: Scalars['String'];
};
+export type LinksMetadata = {
+ __typename?: 'LinksMetadata';
+ primaryLinkLabel: Scalars['String'];
+ primaryLinkUrl: Scalars['String'];
+ secondaryLinks?: Maybe>;
+};
+
export type LoginToken = {
__typename?: 'LoginToken';
loginToken: AuthToken;
@@ -752,7 +760,7 @@ export type Telemetry = {
export type TimelineCalendarEvent = {
__typename?: 'TimelineCalendarEvent';
- conferenceLink: LinkMetadata;
+ conferenceLink: LinksMetadata;
conferenceSolution: Scalars['String'];
description: Scalars['String'];
endsAt: Scalars['DateTime'];
@@ -884,12 +892,9 @@ export type User = {
lastName: Scalars['String'];
onboardingStatus?: Maybe;
passwordHash?: Maybe;
- /** @deprecated field migrated into the AppTokens Table ref: https://github.com/twentyhq/twenty/issues/5021 */
- passwordResetToken?: Maybe;
- /** @deprecated field migrated into the AppTokens Table ref: https://github.com/twentyhq/twenty/issues/5021 */
- passwordResetTokenExpiresAt?: Maybe;
supportUserHash?: Maybe;
updatedAt: Scalars['DateTime'];
+ userVars: Scalars['JSONObject'];
workspaceMember?: Maybe;
workspaces: Array;
};
@@ -1090,6 +1095,7 @@ export type ServerlessFunction = {
createdAt: Scalars['DateTime'];
id: Scalars['UUID'];
name: Scalars['String'];
+ sourceCodeHash: Scalars['String'];
syncStatus: ServerlessFunctionSyncStatus;
updatedAt: Scalars['DateTime'];
};
@@ -1228,7 +1234,7 @@ export type ImpersonateMutationVariables = Exact<{
}>;
-export type ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: string, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, 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 ImpersonateMutation = { __typename?: 'Mutation', impersonate: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: string, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, 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<{
appToken: Scalars['String'];
@@ -1260,7 +1266,7 @@ export type VerifyMutationVariables = Exact<{
}>;
-export type VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: string, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, 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 VerifyMutation = { __typename?: 'Mutation', verify: { __typename?: 'Verify', user: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: string, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, 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'];
@@ -1321,7 +1327,7 @@ export type GetAisqlQueryQueryVariables = Exact<{
export type GetAisqlQueryQuery = { __typename?: 'Query', getAISQLQuery: { __typename?: 'AISQLQueryResult', sqlQuery: string, sqlQueryResult?: string | null, queryFailedErrorMessage?: string | null } };
-export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: string, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
+export type UserQueryFragmentFragment = { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: string, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> };
export type DeleteUserAccountMutationVariables = Exact<{ [key: string]: never; }>;
@@ -1338,7 +1344,7 @@ export type UploadProfilePictureMutation = { __typename?: 'Mutation', uploadProf
export type GetCurrentUserQueryVariables = Exact<{ [key: string]: never; }>;
-export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: string, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } };
+export type GetCurrentUserQuery = { __typename?: 'Query', currentUser: { __typename?: 'User', id: any, firstName: string, lastName: string, email: string, canImpersonate: boolean, supportUserHash?: string | null, onboardingStatus?: OnboardingStatus | null, userVars: any, workspaceMember?: { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale: string, name: { __typename?: 'FullName', firstName: string, lastName: string } } | null, defaultWorkspace: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, domainName?: string | null, inviteHash?: string | null, allowImpersonation: boolean, activationStatus: string, currentCacheVersion?: string | null, workspaceMembersCount?: number | null, featureFlags?: Array<{ __typename?: 'FeatureFlag', id: any, key: string, value: boolean, workspaceId: string }> | null, currentBillingSubscription?: { __typename?: 'BillingSubscription', id: any, status: SubscriptionStatus, interval?: SubscriptionInterval | null } | null }, workspaces: Array<{ __typename?: 'UserWorkspace', workspace?: { __typename?: 'Workspace', id: any, logo?: string | null, displayName?: string | null, domainName?: string | null } | null }> } };
export type AddUserToWorkspaceMutationVariables = Exact<{
inviteHash: Scalars['String'];
@@ -1523,6 +1529,7 @@ export const UserQueryFragmentFragmentDoc = gql`
domainName
}
}
+ userVars
}
`;
export const GetTimelineCalendarEventsFromCompanyIdDocument = gql`
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 9d2eb17ad..bedaa440c 100644
--- a/packages/twenty-front/src/modules/activities/calendar/types/CalendarEvent.ts
+++ b/packages/twenty-front/src/modules/activities/calendar/types/CalendarEvent.ts
@@ -4,8 +4,8 @@ import { CalendarChannelVisibility } from '~/generated/graphql';
// TODO: use backend CalendarEvent type when ready
export type CalendarEvent = {
conferenceLink?: {
- label: string;
- url: string;
+ primaryLinkLabel: string;
+ primaryLinkUrl: string;
};
description?: string;
endsAt?: string;
diff --git a/packages/twenty-front/src/modules/auth/states/currentUserState.ts b/packages/twenty-front/src/modules/auth/states/currentUserState.ts
index 2aab02507..2feedc94f 100644
--- a/packages/twenty-front/src/modules/auth/states/currentUserState.ts
+++ b/packages/twenty-front/src/modules/auth/states/currentUserState.ts
@@ -4,7 +4,12 @@ import { User } from '~/generated/graphql';
export type CurrentUser = Pick<
User,
- 'id' | 'email' | 'supportUserHash' | 'canImpersonate' | 'onboardingStatus'
+ | 'id'
+ | 'email'
+ | 'supportUserHash'
+ | 'canImpersonate'
+ | 'onboardingStatus'
+ | 'userVars'
>;
export const currentUserState = createState({
diff --git a/packages/twenty-front/src/modules/information-banner/InformationBanner.tsx b/packages/twenty-front/src/modules/information-banner/InformationBanner.tsx
new file mode 100644
index 000000000..692774397
--- /dev/null
+++ b/packages/twenty-front/src/modules/information-banner/InformationBanner.tsx
@@ -0,0 +1,26 @@
+import { currentUserState } from '@/auth/states/currentUserState';
+import { InformationBannerAccountToReconnect } from '@/information-banner/InformationBannerReconnectAccount';
+import { useRecoilValue } from 'recoil';
+
+export enum InformationBannerKeys {
+ ACCOUNTS_TO_RECONNECT = 'ACCOUNTS_TO_RECONNECT',
+}
+
+export const InformationBanner = () => {
+ const currentUser = useRecoilValue(currentUserState);
+
+ const userVars = currentUser?.userVars;
+
+ const firstAccountIdToReconnect =
+ userVars?.[InformationBannerKeys.ACCOUNTS_TO_RECONNECT]?.[0];
+
+ return (
+ <>
+ {firstAccountIdToReconnect && (
+
+ )}
+ >
+ );
+};
diff --git a/packages/twenty-front/src/modules/information-banner/InformationBannerReconnectAccount.tsx b/packages/twenty-front/src/modules/information-banner/InformationBannerReconnectAccount.tsx
new file mode 100644
index 000000000..94064100b
--- /dev/null
+++ b/packages/twenty-front/src/modules/information-banner/InformationBannerReconnectAccount.tsx
@@ -0,0 +1,38 @@
+import { ConnectedAccount } from '@/accounts/types/ConnectedAccount';
+import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
+import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
+import { useTriggerGoogleApisOAuth } from '@/settings/accounts/hooks/useTriggerGoogleApisOAuth';
+import { Button } from '@/ui/input/button/components/Button';
+import { Banner, IconRefresh } from 'twenty-ui';
+
+export const InformationBannerAccountToReconnect = ({
+ accountIdToReconnect,
+}: {
+ accountIdToReconnect: string;
+}) => {
+ const accountToReconnect = useFindOneRecord({
+ objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
+ objectRecordId: accountIdToReconnect,
+ });
+
+ const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
+
+ if (!accountToReconnect?.record) {
+ return null;
+ }
+
+ return (
+
+ Sync lost with mailbox {accountToReconnect?.record?.handle}. Please
+ reconnect for updates:
+
+ );
+};
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 03c1e3329..6354d1a8c 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
@@ -1,6 +1,7 @@
import styled from '@emotion/styled';
import { useRecoilCallback, useRecoilState, useSetRecoilState } from 'recoil';
+import { InformationBanner } from '@/information-banner/InformationBanner';
import { useColumnDefinitionsFromFieldMetadata } from '@/object-metadata/hooks/useColumnDefinitionsFromFieldMetadata';
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
import { useObjectNameSingularFromPlural } from '@/object-metadata/hooks/useObjectNameSingularFromPlural';
@@ -125,6 +126,7 @@ export const RecordIndexContainer = ({
return (
+
diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsGeneral.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsGeneral.tsx
index 14ffba784..b1e7c5134 100644
--- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsGeneral.tsx
+++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsCalendarChannelsGeneral.tsx
@@ -51,8 +51,8 @@ export const SettingsAccountsCalendarChannelsGeneral = () => {
startsAt: exampleStartDate.toISOString(),
conferenceSolution: '',
conferenceLink: {
- label: '',
- url: '',
+ primaryLinkLabel: '',
+ primaryLinkUrl: '',
},
description: '',
isCanceled: false,
diff --git a/packages/twenty-front/src/modules/ui/layout/page/SubMenuTopBarContainer.tsx b/packages/twenty-front/src/modules/ui/layout/page/SubMenuTopBarContainer.tsx
index becc29aae..4887ceedc 100644
--- a/packages/twenty-front/src/modules/ui/layout/page/SubMenuTopBarContainer.tsx
+++ b/packages/twenty-front/src/modules/ui/layout/page/SubMenuTopBarContainer.tsx
@@ -1,9 +1,10 @@
-import { JSX } from 'react';
import styled from '@emotion/styled';
+import { JSX } from 'react';
import { IconComponent } from 'twenty-ui';
import { useIsMobile } from '@/ui/utilities/responsive/hooks/useIsMobile';
+import { InformationBanner } from '@/information-banner/InformationBanner';
import { PageBody } from './PageBody';
import { PageHeader } from './PageHeader';
@@ -32,7 +33,10 @@ export const SubMenuTopBarContainer = ({
return (
{isMobile && }
- {children}
+
+
+ {children}
+
);
};
diff --git a/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx b/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx
index ebedecd4f..2951f4530 100644
--- a/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx
+++ b/packages/twenty-front/src/modules/users/components/UserProviderEffect.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState';
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 14dcf557a..67dfa226e 100644
--- a/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts
+++ b/packages/twenty-front/src/modules/users/graphql/fragments/userQueryFragment.ts
@@ -49,5 +49,6 @@ export const USER_QUERY_FRAGMENT = gql`
domainName
}
}
+ userVars
}
`;
diff --git a/packages/twenty-front/src/pages/settings/Releases.tsx b/packages/twenty-front/src/pages/settings/Releases.tsx
index 1d2d728a6..5e31d43eb 100644
--- a/packages/twenty-front/src/pages/settings/Releases.tsx
+++ b/packages/twenty-front/src/pages/settings/Releases.tsx
@@ -1,5 +1,5 @@
-import React, { useEffect, useState } from 'react';
import styled from '@emotion/styled';
+import React, { useEffect, useState } from 'react';
import rehypeStringify from 'rehype-stringify';
import remarkParse from 'remark-parse';
import remarkRehype from 'remark-rehype';
diff --git a/packages/twenty-front/src/pages/settings/SettingsBilling.tsx b/packages/twenty-front/src/pages/settings/SettingsBilling.tsx
index 2e738952b..fd128ecb2 100644
--- a/packages/twenty-front/src/pages/settings/SettingsBilling.tsx
+++ b/packages/twenty-front/src/pages/settings/SettingsBilling.tsx
@@ -1,5 +1,5 @@
-import React, { useState } from 'react';
import styled from '@emotion/styled';
+import { useState } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import {
H1Title,
diff --git a/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx b/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx
index 33215e7d0..8339d1cbb 100644
--- a/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx
+++ b/packages/twenty-front/src/pages/settings/SettingsWorkspaceMembers.tsx
@@ -1,5 +1,5 @@
-import { useState } from 'react';
import styled from '@emotion/styled';
+import { useState } from 'react';
import { useRecoilValue } from 'recoil';
import { H1Title, H2Title, IconSettings, IconTrash } from 'twenty-ui';
@@ -13,10 +13,10 @@ import { IconButton } from '@/ui/input/button/components/IconButton';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer';
import { Section } from '@/ui/layout/section/components/Section';
+import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
import { WorkspaceInviteLink } from '@/workspace/components/WorkspaceInviteLink';
import { WorkspaceInviteTeam } from '@/workspace/components/WorkspaceInviteTeam';
import { WorkspaceMemberCard } from '@/workspace/components/WorkspaceMemberCard';
-import { WorkspaceMember } from '@/workspace-member/types/WorkspaceMember';
const StyledH1Title = styled(H1Title)`
margin-bottom: 0;
diff --git a/packages/twenty-front/src/pages/settings/crm-migration/SettingsCRMMigration.tsx b/packages/twenty-front/src/pages/settings/crm-migration/SettingsCRMMigration.tsx
index 59c09a657..48c8ca733 100644
--- a/packages/twenty-front/src/pages/settings/crm-migration/SettingsCRMMigration.tsx
+++ b/packages/twenty-front/src/pages/settings/crm-migration/SettingsCRMMigration.tsx
@@ -1,8 +1,8 @@
-import React from 'react';
// @ts-expect-error external library has a typing issue
import { RevertConnect } from '@revertdotdev/revert-react';
import { IconSettings } from 'twenty-ui';
+import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { SettingsHeaderContainer } from '@/settings/components/SettingsHeaderContainer';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsReadDocumentationButton } from '@/settings/developers/components/SettingsReadDocumentationButton';
@@ -10,7 +10,6 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'
import { Section } from '@/ui/layout/section/components/Section';
import { Breadcrumb } from '@/ui/navigation/bread-crumb/components/Breadcrumb';
import { useRecoilValue } from 'recoil';
-import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
const REVERT_PUBLIC_KEY = 'pk_live_a87fee8c-28c7-494f-99a3-996ff89f9918';
diff --git a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx b/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx
index 9dea99bd2..a744030a6 100644
--- a/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx
+++ b/packages/twenty-front/src/pages/settings/developers/api-keys/SettingsDevelopersApiKeysNew.tsx
@@ -1,6 +1,6 @@
+import { DateTime } from 'luxon';
import { useState } from 'react';
import { useNavigate } from 'react-router-dom';
-import { DateTime } from 'luxon';
import { H2Title, IconSettings } from 'twenty-ui';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
diff --git a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationEditDatabaseConnection.tsx b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationEditDatabaseConnection.tsx
index 05d3789ff..284f4ed22 100644
--- a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationEditDatabaseConnection.tsx
+++ b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationEditDatabaseConnection.tsx
@@ -6,12 +6,10 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'
export const SettingsIntegrationEditDatabaseConnection = () => {
return (
- <>
-
-
-
-
-
- >
+
+
+
+
+
);
};
diff --git a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx
index b5c208c87..ddfa70336 100644
--- a/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx
+++ b/packages/twenty-front/src/pages/settings/integrations/SettingsIntegrationNewDatabaseConnection.tsx
@@ -1,7 +1,7 @@
+import { zodResolver } from '@hookform/resolvers/zod';
import { useEffect } from 'react';
import { FormProvider, useForm } from 'react-hook-form';
import { useNavigate, useParams } from 'react-router-dom';
-import { zodResolver } from '@hookform/resolvers/zod';
import { H2Title, IconSettings } from 'twenty-ui';
import { z } from 'zod';
diff --git a/packages/twenty-front/src/testing/mock-data/timeline-calendar-events.ts b/packages/twenty-front/src/testing/mock-data/timeline-calendar-events.ts
index 0eafecbe1..838e5fe62 100644
--- a/packages/twenty-front/src/testing/mock-data/timeline-calendar-events.ts
+++ b/packages/twenty-front/src/testing/mock-data/timeline-calendar-events.ts
@@ -11,8 +11,8 @@ export const mockedTimelineCalendarEvents: TimelineCalendarEvent[] = [
startsAt: '2024-05-16T12:00:00.000Z',
endsAt: '2024-05-16T13:00:00.000Z',
conferenceLink: {
- url: 'https://meet.google.com/xxx-xxx-xxx',
- label: 'Rejoindre la visio',
+ primaryLinkUrl: 'https://meet.google.com/xxx-xxx-xxx',
+ primaryLinkLabel: 'Rejoindre la visio',
},
conferenceSolution: 'GOOGLE_MEET',
isCanceled: false,
@@ -51,8 +51,8 @@ export const mockedTimelineCalendarEvents: TimelineCalendarEvent[] = [
endsAt: '2024-05-08T12:25:00.000Z',
isFullDay: false,
conferenceLink: {
- url: 'https://meet.google.com/xxx-xxx-xxx',
- label: 'Rejoindre la visio',
+ primaryLinkUrl: 'https://meet.google.com/xxx-xxx-xxx',
+ primaryLinkLabel: 'Rejoindre la visio',
},
conferenceSolution: 'GOOGLE_MEET',
isCanceled: false,
@@ -80,8 +80,8 @@ export const mockedTimelineCalendarEvents: TimelineCalendarEvent[] = [
endsAt: '2024-05-06T12:25:00.000Z',
isFullDay: false,
conferenceLink: {
- url: 'https://meet.google.com/xxx-xxx-xxx',
- label: 'Rejoindre la visio',
+ primaryLinkUrl: 'https://meet.google.com/xxx-xxx-xxx',
+ primaryLinkLabel: 'Rejoindre la visio',
},
conferenceSolution: 'GOOGLE_MEET',
isCanceled: false,
diff --git a/packages/twenty-front/src/testing/mock-data/users.ts b/packages/twenty-front/src/testing/mock-data/users.ts
index 806eb6265..1839303e9 100644
--- a/packages/twenty-front/src/testing/mock-data/users.ts
+++ b/packages/twenty-front/src/testing/mock-data/users.ts
@@ -17,6 +17,7 @@ type MockedUser = Pick<
| '__typename'
| 'supportUserHash'
| 'onboardingStatus'
+ | 'userVars'
> & {
workspaceMember: WorkspaceMember | null;
locale: string;
@@ -99,6 +100,7 @@ export const mockedUserData: MockedUser = {
locale: 'en',
workspaces: [{ workspace: mockDefaultWorkspace }],
onboardingStatus: OnboardingStatus.Completed,
+ userVars: {},
};
export const mockedOnboardingUserData = (
diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1721139150487-addKeyValuePairType.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1721139150487-addKeyValuePairType.ts
new file mode 100644
index 000000000..80defc1e9
--- /dev/null
+++ b/packages/twenty-server/src/database/typeorm/core/migrations/1721139150487-addKeyValuePairType.ts
@@ -0,0 +1,21 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class AddKeyValuePairType1721139150487 implements MigrationInterface {
+ name = 'AddKeyValuePairType1721139150487';
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `CREATE TYPE "core"."keyValuePair_type_enum" AS ENUM('USER_VAR', 'FEATURE_FLAG', 'SYSTEM_VAR')`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "core"."keyValuePair" ADD "type" "core"."keyValuePair_type_enum" NOT NULL DEFAULT 'USER_VAR'`,
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE "core"."keyValuePair" DROP COLUMN "type"`,
+ );
+ await queryRunner.query(`DROP TYPE "core"."keyValuePair_type_enum"`);
+ }
+}
diff --git a/packages/twenty-server/src/database/typeorm/core/migrations/1721656106498-migrateKeyValueTypeToJsonb.ts b/packages/twenty-server/src/database/typeorm/core/migrations/1721656106498-migrateKeyValueTypeToJsonb.ts
new file mode 100644
index 000000000..1ed6c8066
--- /dev/null
+++ b/packages/twenty-server/src/database/typeorm/core/migrations/1721656106498-migrateKeyValueTypeToJsonb.ts
@@ -0,0 +1,25 @@
+import { MigrationInterface, QueryRunner } from 'typeorm';
+
+export class MigrateKeyValueTypeToJsonb1721656106498
+ implements MigrationInterface
+{
+ name = 'MigrateKeyValueTypeToJsonb1721656106498';
+
+ public async up(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE "core"."keyValuePair" RENAME COLUMN "value" TO "textValueDeprecated"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "core"."keyValuePair" ADD "value" jsonb`,
+ );
+ }
+
+ public async down(queryRunner: QueryRunner): Promise {
+ await queryRunner.query(
+ `ALTER TABLE "core"."keyValuePair" DROP COLUMN "value"`,
+ );
+ await queryRunner.query(
+ `ALTER TABLE "core"."keyValuePair" RENAME COLUMN "textValueDeprecated" TO "value"`,
+ );
+ }
+}
diff --git a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts
index 531939cf4..161ee31a5 100644
--- a/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts
+++ b/packages/twenty-server/src/engine/api/graphql/workspace-schema-builder/services/type-mapper.service.ts
@@ -4,7 +4,6 @@ import { GraphQLISODateTime } from '@nestjs/graphql';
import {
GraphQLBoolean,
GraphQLEnumType,
- GraphQLFloat,
GraphQLID,
GraphQLInputObjectType,
GraphQLInputType,
diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts
index 5f60af70e..15567f48f 100644
--- a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts
+++ b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts
@@ -1,41 +1,42 @@
/* eslint-disable no-restricted-imports */
+import { HttpModule } from '@nestjs/axios';
import { Module } from '@nestjs/common';
import { JwtModule } from '@nestjs/jwt';
import { TypeOrmModule } from '@nestjs/typeorm';
-import { HttpModule } from '@nestjs/axios';
-import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
-import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
-import { User } from 'src/engine/core-modules/user/user.entity';
-import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
-import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
-import { UserModule } from 'src/engine/core-modules/user/user.module';
-import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
-import { GoogleAuthController } from 'src/engine/core-modules/auth/controllers/google-auth.controller';
+import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
+import { AppTokenService } from 'src/engine/core-modules/app-token/services/app-token.service';
import { GoogleAPIsAuthController } from 'src/engine/core-modules/auth/controllers/google-apis-auth.controller';
+import { GoogleAuthController } from 'src/engine/core-modules/auth/controllers/google-auth.controller';
+import { MicrosoftAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-auth.controller';
import { VerifyAuthController } from 'src/engine/core-modules/auth/controllers/verify-auth.controller';
-import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service';
-import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
+import { TokenService } from 'src/engine/core-modules/auth/services/token.service';
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
-import { MicrosoftAuthController } from 'src/engine/core-modules/auth/controllers/microsoft-auth.controller';
-import { AppTokenService } from 'src/engine/core-modules/app-token/services/app-token.service';
-import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
-import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
-import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
+import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
+import { User } from 'src/engine/core-modules/user/user.entity';
+import { UserModule } from 'src/engine/core-modules/user/user.module';
+import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
+import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
+import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
+import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
+import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
-import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
+import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
import { CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
+import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
+import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
+import { MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { AuthResolver } from './auth.resolver';
-import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
import { AuthService } from './services/auth.service';
+import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
const jwtModule = JwtModule.registerAsync({
useFactory: async (environmentService: EnvironmentService) => {
return {
@@ -70,6 +71,7 @@ const jwtModule = JwtModule.registerAsync({
OnboardingModule,
TwentyORMModule.forFeature([CalendarChannelWorkspaceEntity]),
WorkspaceDataSourceModule,
+ ConnectedAccountModule,
],
controllers: [
GoogleAuthController,
diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts
index 931da8141..4f0efc5f3 100644
--- a/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/auth/services/google-apis.service.ts
@@ -18,6 +18,7 @@ import {
CalendarChannelWorkspaceEntity,
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
import { ConnectedAccountRepository } from 'src/modules/connected-account/repositories/connected-account.repository';
+import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
import {
ConnectedAccountProvider,
ConnectedAccountWorkspaceEntity,
@@ -33,6 +34,7 @@ import {
MessagingMessageListFetchJob,
MessagingMessageListFetchJobData,
} from 'src/modules/messaging/message-import-manager/jobs/messaging-message-list-fetch.job';
+import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
@Injectable()
export class GoogleAPIsService {
@@ -47,6 +49,7 @@ export class GoogleAPIsService {
private readonly connectedAccountRepository: ConnectedAccountRepository,
@InjectObjectMetadataRepository(MessageChannelWorkspaceEntity)
private readonly messageChannelRepository: MessageChannelRepository,
+ private readonly accountsToReconnectService: AccountsToReconnectService,
) {}
async refreshGoogleRefreshToken(input: {
@@ -139,6 +142,23 @@ export class GoogleAPIsService {
manager,
);
+ const workspaceMemberRepository =
+ await this.twentyORMManager.getRepository(
+ 'workspaceMember',
+ );
+
+ const workspaceMember = await workspaceMemberRepository.findOneOrFail({
+ where: { id: workspaceMemberId },
+ });
+
+ const userId = workspaceMember.userId;
+
+ await this.accountsToReconnectService.removeAccountToReconnect(
+ userId,
+ workspaceId,
+ newOrExistingConnectedAccountId,
+ );
+
await this.messageChannelRepository.resetSync(
newOrExistingConnectedAccountId,
workspaceId,
diff --git a/packages/twenty-server/src/engine/core-modules/key-value-pair/key-value-pair.entity.ts b/packages/twenty-server/src/engine/core-modules/key-value-pair/key-value-pair.entity.ts
index 5374c948b..b34600106 100644
--- a/packages/twenty-server/src/engine/core-modules/key-value-pair/key-value-pair.entity.ts
+++ b/packages/twenty-server/src/engine/core-modules/key-value-pair/key-value-pair.entity.ts
@@ -1,5 +1,6 @@
import { Field, ObjectType } from '@nestjs/graphql';
+import { IDField } from '@ptc-org/nestjs-query-graphql';
import {
Column,
CreateDateColumn,
@@ -12,12 +13,17 @@ import {
Unique,
UpdateDateColumn,
} from 'typeorm';
-import { IDField } from '@ptc-org/nestjs-query-graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
+export enum KeyValuePairType {
+ USER_VAR = 'USER_VAR',
+ FEATURE_FLAG = 'FEATURE_FLAG',
+ SYSTEM_VAR = 'SYSTEM_VAR',
+}
+
@Entity({ name: 'keyValuePair', schema: 'core' })
@ObjectType('KeyValuePair')
@Unique('IndexOnKeyUserIdWorkspaceIdUnique', ['key', 'userId', 'workspaceId'])
@@ -41,7 +47,7 @@ export class KeyValuePair {
user: Relation;
@Column({ nullable: true })
- userId: string;
+ userId: string | null;
@ManyToOne(() => Workspace, (workspace) => workspace.keyValuePairs, {
onDelete: 'CASCADE',
@@ -50,15 +56,28 @@ export class KeyValuePair {
workspace: Relation;
@Column({ nullable: true })
- workspaceId: string;
+ workspaceId: string | null;
@Field(() => String)
@Column({ nullable: false, type: 'text' })
key: string;
- @Field(() => String, { nullable: true })
- @Column({ nullable: true, type: 'text' })
- value: string;
+ @Field(() => JSON, { nullable: true })
+ @Column('jsonb', { nullable: true })
+ value: JSON;
+
+ @Field(() => String)
+ @Column({ nullable: false, type: 'text' })
+ textValueDeprecated: string;
+
+ @Field(() => KeyValuePairType)
+ @Column({
+ type: 'enum',
+ enum: Object.values(KeyValuePairType),
+ nullable: false,
+ default: KeyValuePairType.USER_VAR,
+ })
+ type: KeyValuePairType;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
diff --git a/packages/twenty-server/src/engine/core-modules/key-value-pair/key-value-pair.service.ts b/packages/twenty-server/src/engine/core-modules/key-value-pair/key-value-pair.service.ts
index 5f74effd0..cd2b4261f 100644
--- a/packages/twenty-server/src/engine/core-modules/key-value-pair/key-value-pair.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/key-value-pair/key-value-pair.service.ts
@@ -1,59 +1,79 @@
import { InjectRepository } from '@nestjs/typeorm';
-import { BadRequestException } from '@nestjs/common';
-import { Repository } from 'typeorm';
+import { IsNull, Repository } from 'typeorm';
-import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
+import {
+ KeyValuePair,
+ KeyValuePairType,
+} from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
-export class KeyValuePairService {
+export class KeyValuePairService<
+ KeyValueTypesMap extends Record = Record,
+> {
constructor(
@InjectRepository(KeyValuePair, 'core')
private readonly keyValuePairRepository: Repository,
) {}
- async get({
+ async get({
userId,
workspaceId,
+ type,
key,
}: {
- userId?: string;
- workspaceId?: string;
- key: K;
- }): Promise {
- return (
- await this.keyValuePairRepository.findOne({
- where: {
- userId,
- workspaceId,
- key: key as string,
- },
- })
- )?.value as TYPE[K] | undefined;
+ userId?: string | null;
+ workspaceId?: string | null;
+ type: KeyValuePairType;
+ key?: Extract;
+ }): Promise> {
+ const keyValuePairs = (await this.keyValuePairRepository.find({
+ where: {
+ ...(userId === undefined
+ ? {}
+ : userId === null
+ ? { userId: IsNull() }
+ : { userId }),
+ ...(workspaceId === undefined
+ ? {}
+ : workspaceId === null
+ ? { workspaceId: IsNull() }
+ : { workspaceId }),
+ ...(key === undefined ? {} : { key }),
+ type,
+ },
+ })) as Array;
+
+ return keyValuePairs.map((keyValuePair) => ({
+ ...keyValuePair,
+ value: keyValuePair.value ?? keyValuePair.textValueDeprecated,
+ }));
}
- async set({
+ async set({
userId,
workspaceId,
key,
value,
+ type,
}: {
- userId?: string;
- workspaceId?: string;
- key: K;
- value: TYPE[K];
+ userId?: string | null;
+ workspaceId?: string | null;
+ key: Extract;
+ value: KeyValueTypesMap[K];
+ type: KeyValuePairType;
}) {
- if (!userId && !workspaceId) {
- throw new BadRequestException('userId and workspaceId are undefined');
- }
const upsertData = {
userId,
workspaceId,
- key: key as string,
- value: value as string,
+ key,
+ value,
+ type,
};
const conflictPaths = Object.keys(upsertData).filter(
- (key) => key !== 'value' && upsertData[key] !== undefined,
+ (key) =>
+ ['userId', 'workspaceId', 'key'].includes(key) &&
+ upsertData[key] !== undefined,
);
const indexPredicate = !userId
@@ -67,4 +87,31 @@ export class KeyValuePairService {
indexPredicate,
});
}
+
+ async delete({
+ userId,
+ workspaceId,
+ type,
+ key,
+ }: {
+ userId?: string | null;
+ workspaceId?: string | null;
+ type: KeyValuePairType;
+ key: Extract;
+ }) {
+ await this.keyValuePairRepository.delete({
+ ...(userId === undefined
+ ? {}
+ : userId === null
+ ? { userId: IsNull() }
+ : { userId }),
+ ...(workspaceId === undefined
+ ? {}
+ : workspaceId === null
+ ? { workspaceId: IsNull() }
+ : { workspaceId }),
+ type,
+ key,
+ });
+ }
}
diff --git a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.module.ts b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.module.ts
index a036cafee..a89f24c10 100644
--- a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.module.ts
+++ b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.module.ts
@@ -1,22 +1,22 @@
import { Module } from '@nestjs/common';
-import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
+import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
import { OnboardingResolver } from 'src/engine/core-modules/onboarding/onboarding.resolver';
-import { KeyValuePairModule } from 'src/engine/core-modules/key-value-pair/key-value-pair.module';
+import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module';
+import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
+import { EnvironmentModule } from 'src/engine/integrations/environment/environment.module';
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-manager.module';
-import { EnvironmentModule } from 'src/engine/integrations/environment/environment.module';
-import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
@Module({
imports: [
DataSourceModule,
WorkspaceManagerModule,
UserWorkspaceModule,
- KeyValuePairModule,
EnvironmentModule,
BillingModule,
+ UserVarsModule,
],
exports: [OnboardingService],
providers: [OnboardingService, OnboardingResolver],
diff --git a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts
index 5062bc54f..0a07f010c 100644
--- a/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/onboarding/onboarding.service.ts
@@ -3,9 +3,9 @@ import { Injectable } from '@nestjs/common';
import { BillingWorkspaceService } from 'src/engine/core-modules/billing/billing.workspace-service';
import { SubscriptionStatus } from 'src/engine/core-modules/billing/entities/billing-subscription.entity';
-import { KeyValuePairService } from 'src/engine/core-modules/key-value-pair/key-value-pair.service';
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
+import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
@@ -25,7 +25,7 @@ enum OnboardingStepKeys {
INVITE_TEAM_ONBOARDING_STEP = 'INVITE_TEAM_ONBOARDING_STEP',
}
-type OnboardingKeyValueType = {
+type OnboardingKeyValueTypeMap = {
[OnboardingStepKeys.SYNC_EMAIL_ONBOARDING_STEP]: OnboardingStepValues;
[OnboardingStepKeys.INVITE_TEAM_ONBOARDING_STEP]: OnboardingStepValues;
};
@@ -37,7 +37,7 @@ export class OnboardingService {
private readonly billingWorkspaceService: BillingWorkspaceService,
private readonly workspaceManagerService: WorkspaceManagerService,
private readonly userWorkspaceService: UserWorkspaceService,
- private readonly keyValuePairService: KeyValuePairService,
+ private readonly userVarsService: UserVarsService,
@InjectObjectMetadataRepository(ConnectedAccountWorkspaceEntity)
private readonly connectedAccountRepository: ConnectedAccountRepository,
) {}
@@ -86,7 +86,7 @@ export class OnboardingService {
}
private async isSyncEmailOnboardingStatus(user: User) {
- const syncEmailValue = await this.keyValuePairService.get({
+ const syncEmailValue = await this.userVarsService.get({
userId: user.id,
workspaceId: user.defaultWorkspaceId,
key: OnboardingStepKeys.SYNC_EMAIL_ONBOARDING_STEP,
@@ -102,7 +102,7 @@ export class OnboardingService {
}
private async isInviteTeamOnboardingStatus(workspace: Workspace) {
- const inviteTeamValue = await this.keyValuePairService.get({
+ const inviteTeamValue = await this.userVarsService.get({
workspaceId: workspace.id,
key: OnboardingStepKeys.INVITE_TEAM_ONBOARDING_STEP,
});
@@ -143,7 +143,7 @@ export class OnboardingService {
}
async skipInviteTeamOnboardingStep(workspaceId: string) {
- await this.keyValuePairService.set({
+ await this.userVarsService.set({
workspaceId,
key: OnboardingStepKeys.INVITE_TEAM_ONBOARDING_STEP,
value: OnboardingStepValues.SKIPPED,
@@ -151,7 +151,7 @@ export class OnboardingService {
}
async skipSyncEmailOnboardingStep(userId: string, workspaceId: string) {
- await this.keyValuePairService.set({
+ await this.userVarsService.set({
userId,
workspaceId,
key: OnboardingStepKeys.SYNC_EMAIL_ONBOARDING_STEP,
diff --git a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts
index 853bafed4..bb8c00ef9 100644
--- a/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts
+++ b/packages/twenty-server/src/engine/core-modules/user/services/user.service.ts
@@ -1,18 +1,18 @@
-import { InjectRepository } from '@nestjs/typeorm';
import { EventEmitter2 } from '@nestjs/event-emitter';
+import { InjectRepository } from '@nestjs/typeorm';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Repository } from 'typeorm';
-import { assert } from 'src/utils/assert';
-import { User } from 'src/engine/core-modules/user/user.entity';
-import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
-import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
-import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
-import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
-import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
+import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
+import { User } from 'src/engine/core-modules/user/user.entity';
import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service';
+import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
+import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
+import { DataSourceService } from 'src/engine/metadata-modules/data-source/data-source.service';
+import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
+import { assert } from 'src/utils/assert';
export class UserService extends TypeOrmQueryService {
constructor(
diff --git a/packages/twenty-server/src/engine/core-modules/user/user-vars/services/user-vars.service.ts b/packages/twenty-server/src/engine/core-modules/user/user-vars/services/user-vars.service.ts
new file mode 100644
index 000000000..ba6f74f1c
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/user/user-vars/services/user-vars.service.ts
@@ -0,0 +1,110 @@
+import { Injectable } from '@nestjs/common';
+
+import { KeyValuePairType } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
+import { KeyValuePairService } from 'src/engine/core-modules/key-value-pair/key-value-pair.service';
+import { mergeUserVars } from 'src/engine/core-modules/user/user-vars/utils/merge-user-vars.util';
+
+@Injectable()
+export class UserVarsService<
+ KeyValueTypesMap extends Record = Record,
+> {
+ constructor(private readonly keyValuePairService: KeyValuePairService) {}
+
+ public async get({
+ userId,
+ workspaceId,
+ key,
+ }: {
+ userId?: string;
+ workspaceId?: string;
+ key: Extract;
+ }): Promise {
+ const userVarWorkspaceLevel = await this.keyValuePairService.get({
+ type: KeyValuePairType.USER_VAR,
+ userId: null,
+ workspaceId,
+ key,
+ });
+
+ if (userVarWorkspaceLevel.length > 1) {
+ throw new Error(
+ `Multiple values found for key ${key} at workspace level`,
+ );
+ }
+
+ const userVarUserLevel = await this.keyValuePairService.get({
+ type: KeyValuePairType.USER_VAR,
+ userId,
+ key,
+ });
+
+ if (userVarUserLevel.length > 1) {
+ throw new Error(`Multiple values found for key ${key} at user level`);
+ }
+
+ return mergeUserVars([...userVarUserLevel, ...userVarWorkspaceLevel]).get(
+ key,
+ ) as KeyValueTypesMap[K];
+ }
+
+ public async getAll({
+ userId,
+ workspaceId,
+ }: {
+ userId?: string;
+ workspaceId?: string;
+ }): Promise