5899 display a banner to alert users which need to reconnect their account (#6301)
Closes #5899 <img width="1280" alt="Index - banner" src="https://github.com/twentyhq/twenty/assets/71827178/313cf20d-eb34-496a-8c7c-7589fbd55954"> --------- Co-authored-by: Charles Bochet <charles@twenty.com>
This commit is contained in:
@ -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<Array<LinkMetadata>>;
|
||||
};
|
||||
|
||||
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<OnboardingStatus>;
|
||||
passwordHash?: Maybe<Scalars['String']['output']>;
|
||||
/** @deprecated field migrated into the AppTokens Table ref: https://github.com/twentyhq/twenty/issues/5021 */
|
||||
passwordResetToken?: Maybe<Scalars['String']['output']>;
|
||||
/** @deprecated field migrated into the AppTokens Table ref: https://github.com/twentyhq/twenty/issues/5021 */
|
||||
passwordResetTokenExpiresAt?: Maybe<Scalars['DateTime']['output']>;
|
||||
supportUserHash?: Maybe<Scalars['String']['output']>;
|
||||
updatedAt: Scalars['DateTime']['output'];
|
||||
userVars: Scalars['JSONObject']['output'];
|
||||
workspaceMember?: Maybe<WorkspaceMember>;
|
||||
workspaces: Array<UserWorkspace>;
|
||||
};
|
||||
@ -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'];
|
||||
};
|
||||
|
||||
@ -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<Array<LinkMetadata>>;
|
||||
};
|
||||
|
||||
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<OnboardingStatus>;
|
||||
passwordHash?: Maybe<Scalars['String']>;
|
||||
/** @deprecated field migrated into the AppTokens Table ref: https://github.com/twentyhq/twenty/issues/5021 */
|
||||
passwordResetToken?: Maybe<Scalars['String']>;
|
||||
/** @deprecated field migrated into the AppTokens Table ref: https://github.com/twentyhq/twenty/issues/5021 */
|
||||
passwordResetTokenExpiresAt?: Maybe<Scalars['DateTime']>;
|
||||
supportUserHash?: Maybe<Scalars['String']>;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
userVars: Scalars['JSONObject'];
|
||||
workspaceMember?: Maybe<WorkspaceMember>;
|
||||
workspaces: Array<UserWorkspace>;
|
||||
};
|
||||
@ -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`
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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<CurrentUser | null>({
|
||||
|
||||
@ -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 && (
|
||||
<InformationBannerAccountToReconnect
|
||||
accountIdToReconnect={firstAccountIdToReconnect}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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<ConnectedAccount>({
|
||||
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
|
||||
objectRecordId: accountIdToReconnect,
|
||||
});
|
||||
|
||||
const { triggerGoogleApisOAuth } = useTriggerGoogleApisOAuth();
|
||||
|
||||
if (!accountToReconnect?.record) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Banner>
|
||||
Sync lost with mailbox {accountToReconnect?.record?.handle}. Please
|
||||
reconnect for updates:
|
||||
<Button
|
||||
variant="secondary"
|
||||
title="Reconnect"
|
||||
Icon={IconRefresh}
|
||||
size="small"
|
||||
inverted
|
||||
onClick={() => triggerGoogleApisOAuth()}
|
||||
/>
|
||||
</Banner>
|
||||
);
|
||||
};
|
||||
@ -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 (
|
||||
<StyledContainer>
|
||||
<InformationBanner />
|
||||
<RecordFieldValueSelectorContextProvider>
|
||||
<SpreadsheetImportProvider>
|
||||
<StyledContainerWithPadding>
|
||||
|
||||
@ -51,8 +51,8 @@ export const SettingsAccountsCalendarChannelsGeneral = () => {
|
||||
startsAt: exampleStartDate.toISOString(),
|
||||
conferenceSolution: '',
|
||||
conferenceLink: {
|
||||
label: '',
|
||||
url: '',
|
||||
primaryLinkLabel: '',
|
||||
primaryLinkUrl: '',
|
||||
},
|
||||
description: '',
|
||||
isCanceled: false,
|
||||
|
||||
@ -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 (
|
||||
<StyledContainer isMobile={isMobile} className={className}>
|
||||
{isMobile && <PageHeader title={title} Icon={Icon} />}
|
||||
<PageBody>{children}</PageBody>
|
||||
<PageBody>
|
||||
<InformationBanner />
|
||||
{children}
|
||||
</PageBody>
|
||||
</StyledContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -49,5 +49,6 @@ export const USER_QUERY_FRAGMENT = gql`
|
||||
domainName
|
||||
}
|
||||
}
|
||||
userVars
|
||||
}
|
||||
`;
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -6,12 +6,10 @@ import { SubMenuTopBarContainer } from '@/ui/layout/page/SubMenuTopBarContainer'
|
||||
|
||||
export const SettingsIntegrationEditDatabaseConnection = () => {
|
||||
return (
|
||||
<>
|
||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||
<SettingsPageContainer>
|
||||
<SettingsIntegrationEditDatabaseConnectionContainer />
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
</>
|
||||
<SubMenuTopBarContainer Icon={IconSettings} title="Settings">
|
||||
<SettingsPageContainer>
|
||||
<SettingsIntegrationEditDatabaseConnectionContainer />
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 = (
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddKeyValuePairType1721139150487 implements MigrationInterface {
|
||||
name = 'AddKeyValuePairType1721139150487';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."keyValuePair" DROP COLUMN "type"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TYPE "core"."keyValuePair_type_enum"`);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,25 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class MigrateKeyValueTypeToJsonb1721656106498
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'MigrateKeyValueTypeToJsonb1721656106498';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
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<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."keyValuePair" DROP COLUMN "value"`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."keyValuePair" RENAME COLUMN "textValueDeprecated" TO "value"`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,6 @@ import { GraphQLISODateTime } from '@nestjs/graphql';
|
||||
import {
|
||||
GraphQLBoolean,
|
||||
GraphQLEnumType,
|
||||
GraphQLFloat,
|
||||
GraphQLID,
|
||||
GraphQLInputObjectType,
|
||||
GraphQLInputType,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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<WorkspaceMemberWorkspaceEntity>(
|
||||
'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,
|
||||
|
||||
@ -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<User>;
|
||||
|
||||
@Column({ nullable: true })
|
||||
userId: string;
|
||||
userId: string | null;
|
||||
|
||||
@ManyToOne(() => Workspace, (workspace) => workspace.keyValuePairs, {
|
||||
onDelete: 'CASCADE',
|
||||
@ -50,15 +56,28 @@ export class KeyValuePair {
|
||||
workspace: Relation<Workspace>;
|
||||
|
||||
@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;
|
||||
|
||||
@ -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<TYPE> {
|
||||
export class KeyValuePairService<
|
||||
KeyValueTypesMap extends Record<string, any> = Record<string, any>,
|
||||
> {
|
||||
constructor(
|
||||
@InjectRepository(KeyValuePair, 'core')
|
||||
private readonly keyValuePairRepository: Repository<KeyValuePair>,
|
||||
) {}
|
||||
|
||||
async get<K extends keyof TYPE>({
|
||||
async get<K extends keyof KeyValueTypesMap>({
|
||||
userId,
|
||||
workspaceId,
|
||||
type,
|
||||
key,
|
||||
}: {
|
||||
userId?: string;
|
||||
workspaceId?: string;
|
||||
key: K;
|
||||
}): Promise<TYPE[K] | undefined> {
|
||||
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<K, string>;
|
||||
}): Promise<Array<KeyValueTypesMap[K]>> {
|
||||
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<KeyValueTypesMap[K]>;
|
||||
|
||||
return keyValuePairs.map((keyValuePair) => ({
|
||||
...keyValuePair,
|
||||
value: keyValuePair.value ?? keyValuePair.textValueDeprecated,
|
||||
}));
|
||||
}
|
||||
|
||||
async set<K extends keyof TYPE>({
|
||||
async set<K extends keyof KeyValueTypesMap>({
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
value,
|
||||
type,
|
||||
}: {
|
||||
userId?: string;
|
||||
workspaceId?: string;
|
||||
key: K;
|
||||
value: TYPE[K];
|
||||
userId?: string | null;
|
||||
workspaceId?: string | null;
|
||||
key: Extract<K, string>;
|
||||
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<TYPE> {
|
||||
indexPredicate,
|
||||
});
|
||||
}
|
||||
|
||||
async delete({
|
||||
userId,
|
||||
workspaceId,
|
||||
type,
|
||||
key,
|
||||
}: {
|
||||
userId?: string | null;
|
||||
workspaceId?: string | null;
|
||||
type: KeyValuePairType;
|
||||
key: Extract<keyof KeyValueTypesMap, string>;
|
||||
}) {
|
||||
await this.keyValuePairRepository.delete({
|
||||
...(userId === undefined
|
||||
? {}
|
||||
: userId === null
|
||||
? { userId: IsNull() }
|
||||
: { userId }),
|
||||
...(workspaceId === undefined
|
||||
? {}
|
||||
: workspaceId === null
|
||||
? { workspaceId: IsNull() }
|
||||
: { workspaceId }),
|
||||
type,
|
||||
key,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -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],
|
||||
|
||||
@ -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<OnboardingKeyValueType>,
|
||||
private readonly userVarsService: UserVarsService<OnboardingKeyValueTypeMap>,
|
||||
@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,
|
||||
|
||||
@ -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<User> {
|
||||
constructor(
|
||||
|
||||
@ -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<string, any> = Record<string, any>,
|
||||
> {
|
||||
constructor(private readonly keyValuePairService: KeyValuePairService) {}
|
||||
|
||||
public async get<K extends keyof KeyValueTypesMap>({
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
}: {
|
||||
userId?: string;
|
||||
workspaceId?: string;
|
||||
key: Extract<K, string>;
|
||||
}): Promise<KeyValueTypesMap[K]> {
|
||||
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<Map<Extract<keyof KeyValueTypesMap, string>, any>> {
|
||||
const userVarsWorkspaceLevel = await this.keyValuePairService.get({
|
||||
type: KeyValuePairType.USER_VAR,
|
||||
userId: null,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const userVarsUserLevel = await this.keyValuePairService.get({
|
||||
type: KeyValuePairType.USER_VAR,
|
||||
userId,
|
||||
});
|
||||
|
||||
return mergeUserVars<Extract<keyof KeyValueTypesMap, string>>([
|
||||
...userVarsWorkspaceLevel,
|
||||
...userVarsUserLevel,
|
||||
]);
|
||||
}
|
||||
|
||||
set<K extends keyof KeyValueTypesMap>({
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
value,
|
||||
}: {
|
||||
userId?: string;
|
||||
workspaceId?: string;
|
||||
key: Extract<K, string>;
|
||||
value: KeyValueTypesMap[K];
|
||||
}) {
|
||||
return this.keyValuePairService.set({
|
||||
userId,
|
||||
workspaceId,
|
||||
key: key,
|
||||
value,
|
||||
type: KeyValuePairType.USER_VAR,
|
||||
});
|
||||
}
|
||||
|
||||
async delete({
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
}: {
|
||||
userId?: string;
|
||||
workspaceId?: string;
|
||||
key: Extract<keyof KeyValueTypesMap, string>;
|
||||
}) {
|
||||
return this.keyValuePairService.delete({
|
||||
userId,
|
||||
workspaceId,
|
||||
key,
|
||||
type: KeyValuePairType.USER_VAR,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,12 @@
|
||||
/* eslint-disable no-restricted-imports */
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { KeyValuePairModule } from 'src/engine/core-modules/key-value-pair/key-value-pair.module';
|
||||
import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service';
|
||||
|
||||
@Module({
|
||||
imports: [KeyValuePairModule],
|
||||
exports: [UserVarsService],
|
||||
providers: [UserVarsService],
|
||||
})
|
||||
export class UserVarsModule {}
|
||||
@ -0,0 +1,128 @@
|
||||
import { mergeUserVars } from 'src/engine/core-modules/user/user-vars/utils/merge-user-vars.util';
|
||||
|
||||
describe('mergeUserVars', () => {
|
||||
it('should merge user vars correctly', () => {
|
||||
const userVars = [
|
||||
{
|
||||
key: 'key1',
|
||||
value: JSON.parse('"value1"'),
|
||||
userId: 'userId1',
|
||||
workspaceId: 'workspaceId1',
|
||||
},
|
||||
{
|
||||
key: 'key2',
|
||||
value: JSON.parse('"value2"'),
|
||||
userId: 'userId1',
|
||||
workspaceId: null,
|
||||
},
|
||||
{
|
||||
key: 'key3',
|
||||
value: JSON.parse('"value3"'),
|
||||
userId: null,
|
||||
workspaceId: 'workspaceId1',
|
||||
},
|
||||
];
|
||||
|
||||
const mergedUserVars = mergeUserVars(userVars);
|
||||
|
||||
expect(mergedUserVars).toEqual(
|
||||
new Map([
|
||||
['key1', JSON.parse('"value1"')],
|
||||
['key2', JSON.parse('"value2"')],
|
||||
['key3', JSON.parse('"value3"')],
|
||||
]),
|
||||
);
|
||||
});
|
||||
|
||||
it('should merge user vars correctly when user vars are empty', () => {
|
||||
const userVars = [];
|
||||
|
||||
const mergedUserVars = mergeUserVars(userVars);
|
||||
|
||||
expect(mergedUserVars).toEqual(new Map());
|
||||
});
|
||||
|
||||
it('should overwrite user vars correctly', () => {
|
||||
const userVars1 = [
|
||||
{
|
||||
key: 'key',
|
||||
value: JSON.parse('"value1"'),
|
||||
userId: 'userId',
|
||||
workspaceId: 'workspaceId',
|
||||
},
|
||||
{
|
||||
key: 'key',
|
||||
value: JSON.parse('"value2"'),
|
||||
userId: 'userId',
|
||||
workspaceId: null,
|
||||
},
|
||||
{
|
||||
key: 'key',
|
||||
value: JSON.parse('"value3"'),
|
||||
userId: null,
|
||||
workspaceId: 'workspaceId',
|
||||
},
|
||||
];
|
||||
|
||||
const mergedUserVars1 = mergeUserVars(userVars1);
|
||||
|
||||
const userVars2 = [
|
||||
{
|
||||
key: 'key',
|
||||
value: JSON.parse('"value1"'),
|
||||
userId: 'userId',
|
||||
workspaceId: 'workspaceId',
|
||||
},
|
||||
{
|
||||
key: 'key',
|
||||
value: JSON.parse('"value2"'),
|
||||
userId: 'userId',
|
||||
workspaceId: null,
|
||||
},
|
||||
];
|
||||
|
||||
const mergedUserVars2 = mergeUserVars(userVars2);
|
||||
|
||||
const userVars3 = [
|
||||
{
|
||||
key: 'key',
|
||||
value: JSON.parse('"value2"'),
|
||||
userId: 'userId',
|
||||
workspaceId: null,
|
||||
},
|
||||
{
|
||||
key: 'key',
|
||||
value: JSON.parse('"value3"'),
|
||||
userId: null,
|
||||
workspaceId: 'workspaceId',
|
||||
},
|
||||
];
|
||||
|
||||
const mergedUserVars3 = mergeUserVars(userVars3);
|
||||
|
||||
const userVars4 = [
|
||||
{
|
||||
key: 'key',
|
||||
value: JSON.parse('"value1"'),
|
||||
userId: 'userId',
|
||||
workspaceId: 'workspaceId',
|
||||
},
|
||||
{
|
||||
key: 'key',
|
||||
value: JSON.parse('"value3"'),
|
||||
userId: null,
|
||||
workspaceId: 'workspaceId',
|
||||
},
|
||||
];
|
||||
|
||||
const mergedUserVars4 = mergeUserVars(userVars4);
|
||||
|
||||
expect(mergedUserVars1).toEqual(new Map([['key', JSON.parse('"value1"')]]));
|
||||
|
||||
expect(mergedUserVars2).toEqual(new Map([['key', JSON.parse('"value1"')]]));
|
||||
|
||||
expect(mergedUserVars3).toEqual(new Map([['key', JSON.parse('"value2"')]]));
|
||||
|
||||
expect(mergedUserVars4).toEqual(new Map([['key', JSON.parse('"value1"')]]));
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,31 @@
|
||||
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
|
||||
export const mergeUserVars = <T>(
|
||||
userVars: Pick<KeyValuePair, 'key' | 'value' | 'userId' | 'workspaceId'>[],
|
||||
): Map<T, JSON> => {
|
||||
const workspaceUserVarMap = new Map<T, JSON>();
|
||||
const userUserVarMap = new Map<T, JSON>();
|
||||
const userWorkspaceUserVarMap = new Map<T, JSON>();
|
||||
|
||||
for (const { key, value, userId, workspaceId } of userVars) {
|
||||
if (!userId && workspaceId) {
|
||||
workspaceUserVarMap.set(key as T, value);
|
||||
}
|
||||
|
||||
if (userId && !workspaceId) {
|
||||
userUserVarMap.set(key as T, value);
|
||||
}
|
||||
|
||||
if (userId && workspaceId) {
|
||||
userWorkspaceUserVarMap.set(key as T, value);
|
||||
}
|
||||
}
|
||||
|
||||
const mergedUserVars = new Map<T, JSON>([
|
||||
...workspaceUserVarMap,
|
||||
...userUserVarMap,
|
||||
...userWorkspaceUserVarMap,
|
||||
]);
|
||||
|
||||
return mergedUserVars;
|
||||
};
|
||||
@ -1,17 +1,20 @@
|
||||
/* eslint-disable no-restricted-imports */
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { NestjsQueryGraphQLModule } from '@ptc-org/nestjs-query-graphql';
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
||||
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
|
||||
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { UserResolver } from 'src/engine/core-modules/user/user.resolver';
|
||||
import { TypeORMService } from 'src/database/typeorm/typeorm.service';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module';
|
||||
import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module';
|
||||
import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module';
|
||||
import { DataSourceModule } from 'src/engine/metadata-modules/data-source/data-source.module';
|
||||
|
||||
import { userAutoResolverOpts } from './user.auto-resolver-opts';
|
||||
|
||||
@ -30,6 +33,8 @@ import { UserService } from './services/user.service';
|
||||
FileUploadModule,
|
||||
WorkspaceModule,
|
||||
OnboardingModule,
|
||||
TypeOrmModule.forFeature([KeyValuePair], 'core'),
|
||||
UserVarsModule,
|
||||
],
|
||||
exports: [UserService],
|
||||
providers: [UserService, UserResolver, TypeORMService],
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import {
|
||||
Args,
|
||||
Mutation,
|
||||
@ -6,30 +7,33 @@ import {
|
||||
ResolveField,
|
||||
Resolver,
|
||||
} from '@nestjs/graphql';
|
||||
import { UseGuards } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import crypto from 'crypto';
|
||||
|
||||
import { GraphQLJSONObject } from 'graphql-type-json';
|
||||
import { FileUpload, GraphQLUpload } from 'graphql-upload';
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { SupportDriver } from 'src/engine/integrations/environment/interfaces/support.interface';
|
||||
import { FileFolder } from 'src/engine/core-modules/file/interfaces/file-folder.interface';
|
||||
import { SupportDriver } from 'src/engine/integrations/environment/interfaces/support.interface';
|
||||
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.service';
|
||||
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service';
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
|
||||
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
||||
import { User } from 'src/engine/core-modules/user/user.entity';
|
||||
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||
import { OnboardingStatus } from 'src/engine/core-modules/onboarding/enums/onboarding-status.enum';
|
||||
import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service';
|
||||
import { WorkspaceMember } from 'src/engine/core-modules/user/dtos/workspace-member.dto';
|
||||
import { UserService } from 'src/engine/core-modules/user/services/user.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 { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { DemoEnvGuard } from 'src/engine/guards/demo.env.guard';
|
||||
import { JwtAuthGuard } from 'src/engine/guards/jwt.auth.guard';
|
||||
import { EnvironmentService } from 'src/engine/integrations/environment/environment.service';
|
||||
import { LoadServiceWithWorkspaceContext } from 'src/engine/twenty-orm/context/load-service-with-workspace.context';
|
||||
import { assert } from 'src/utils/assert';
|
||||
import { streamToBuffer } from 'src/utils/stream-to-buffer';
|
||||
|
||||
const getHMACKey = (email?: string, key?: string | null) => {
|
||||
if (!email || !key) return null;
|
||||
@ -50,13 +54,14 @@ export class UserResolver {
|
||||
private readonly fileUploadService: FileUploadService,
|
||||
private readonly onboardingService: OnboardingService,
|
||||
private readonly loadServiceWithWorkspaceContext: LoadServiceWithWorkspaceContext,
|
||||
private readonly userVarService: UserVarsService,
|
||||
) {}
|
||||
|
||||
@Query(() => User)
|
||||
async currentUser(@AuthUser() { id }: User): Promise<User> {
|
||||
async currentUser(@AuthUser() { id: userId }: User): Promise<User> {
|
||||
const user = await this.userRepository.findOne({
|
||||
where: {
|
||||
id,
|
||||
id: userId,
|
||||
},
|
||||
relations: ['defaultWorkspace', 'workspaces', 'workspaces.workspace'],
|
||||
});
|
||||
@ -66,6 +71,28 @@ export class UserResolver {
|
||||
return user;
|
||||
}
|
||||
|
||||
@ResolveField(() => GraphQLJSONObject)
|
||||
async userVars(
|
||||
@Parent() user: User,
|
||||
@AuthWorkspace() workspace: Workspace,
|
||||
): Promise<Record<string, any>> {
|
||||
const userVars = await this.userVarService.getAll({
|
||||
userId: user.id,
|
||||
workspaceId: workspace?.id ?? user.defaultWorkspaceId,
|
||||
});
|
||||
|
||||
const userVarAllowList = [
|
||||
'SYNC_EMAIL_ONBOARDING_STEP',
|
||||
'ACCOUNTS_TO_RECONNECT',
|
||||
];
|
||||
|
||||
const filteredMap = new Map(
|
||||
[...userVars].filter(([key]) => userVarAllowList.includes(key)),
|
||||
);
|
||||
|
||||
return Object.fromEntries(filteredMap);
|
||||
}
|
||||
|
||||
@ResolveField(() => WorkspaceMember, {
|
||||
nullable: true,
|
||||
})
|
||||
|
||||
@ -3,6 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { BillingModule } from 'src/engine/core-modules/billing/billing.module';
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
|
||||
import { DataSourceEntity } from 'src/engine/metadata-modules/data-source/data-source.entity';
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { TwentyORMModule } from 'src/engine/twenty-orm/twenty-orm.module';
|
||||
@ -51,6 +52,7 @@ import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/sta
|
||||
BillingModule,
|
||||
RefreshAccessTokenManagerModule,
|
||||
CalendarEventParticipantManagerModule,
|
||||
UserVarsModule,
|
||||
],
|
||||
providers: [
|
||||
CalendarChannelSyncStatusService,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service';
|
||||
import { CacheStorageService } from 'src/engine/integrations/cache-storage/cache-storage.service';
|
||||
import { InjectCacheStorage } from 'src/engine/integrations/cache-storage/decorators/cache-storage.decorator';
|
||||
import { CacheStorageNamespace } from 'src/engine/integrations/cache-storage/types/cache-storage-namespace.enum';
|
||||
@ -9,6 +10,10 @@ import {
|
||||
CalendarChannelSyncStatus,
|
||||
CalendarChannelWorkspaceEntity,
|
||||
} from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import {
|
||||
ConnectedAccountKeyValueType,
|
||||
ConnectedAccountKeys,
|
||||
} from 'src/modules/connected-account/types/connected-account-key-value.type';
|
||||
|
||||
@Injectable()
|
||||
export class CalendarChannelSyncStatusService {
|
||||
@ -16,6 +21,7 @@ export class CalendarChannelSyncStatusService {
|
||||
private readonly twentyORMManager: TwentyORMManager,
|
||||
@InjectCacheStorage(CacheStorageNamespace.Calendar)
|
||||
private readonly cacheStorage: CacheStorageService,
|
||||
private readonly userVarsService: UserVarsService<ConnectedAccountKeyValueType>,
|
||||
) {}
|
||||
|
||||
public async scheduleFullCalendarEventListFetch(calendarChannelId: string) {
|
||||
@ -157,5 +163,55 @@ export class CalendarChannelSyncStatusService {
|
||||
syncStatus: CalendarChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS,
|
||||
syncStage: CalendarChannelSyncStage.FAILED,
|
||||
});
|
||||
|
||||
await this.addToAccountsToReconnect(calendarChannelId, workspaceId);
|
||||
}
|
||||
|
||||
private async addToAccountsToReconnect(
|
||||
calendarChannelId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const calendarChannelRepository =
|
||||
await this.twentyORMManager.getRepository<CalendarChannelWorkspaceEntity>(
|
||||
'calendarChannel',
|
||||
);
|
||||
|
||||
const calendarChannel = await calendarChannelRepository.findOne({
|
||||
where: {
|
||||
id: calendarChannelId,
|
||||
},
|
||||
relations: {
|
||||
connectedAccount: {
|
||||
accountOwner: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!calendarChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = calendarChannel.connectedAccount.accountOwner.userId;
|
||||
const connectedAccountId = calendarChannel.connectedAccount.id;
|
||||
|
||||
const accountsToReconnect =
|
||||
(await this.userVarsService.get({
|
||||
userId,
|
||||
workspaceId,
|
||||
key: ConnectedAccountKeys.ACCOUNTS_TO_RECONNECT,
|
||||
})) ?? [];
|
||||
|
||||
if (accountsToReconnect.includes(connectedAccountId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
accountsToReconnect.push(connectedAccountId);
|
||||
|
||||
await this.userVarsService.set({
|
||||
userId,
|
||||
workspaceId,
|
||||
key: ConnectedAccountKeys.ACCOUNTS_TO_RECONNECT,
|
||||
value: accountsToReconnect,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
|
||||
import { ConnectedAccountListener } from 'src/modules/connected-account/listeners/connected-account.listener';
|
||||
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
|
||||
|
||||
@Module({
|
||||
imports: [UserVarsModule],
|
||||
providers: [AccountsToReconnectService, ConnectedAccountListener],
|
||||
exports: [AccountsToReconnectService],
|
||||
})
|
||||
export class ConnectedAccountModule {}
|
||||
@ -0,0 +1,42 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { OnEvent } from '@nestjs/event-emitter';
|
||||
|
||||
import { ObjectRecordDeleteEvent } from 'src/engine/integrations/event-emitter/types/object-record-delete.event';
|
||||
import { TwentyORMGlobalManager } from 'src/engine/twenty-orm/twenty-orm-global.manager';
|
||||
import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
|
||||
import { ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { WorkspaceMemberWorkspaceEntity } from 'src/modules/workspace-member/standard-objects/workspace-member.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
export class ConnectedAccountListener {
|
||||
constructor(
|
||||
private readonly twentyORMGlobalManager: TwentyORMGlobalManager,
|
||||
private readonly accountsToReconnectService: AccountsToReconnectService,
|
||||
) {}
|
||||
|
||||
@OnEvent('connectedAccount.deleted')
|
||||
async handleDeletedEvent(
|
||||
payload: ObjectRecordDeleteEvent<ConnectedAccountWorkspaceEntity>,
|
||||
) {
|
||||
const workspaceMemberId = payload.properties.before.accountOwnerId;
|
||||
const workspaceId = payload.workspaceId;
|
||||
const workspaceMemberRepository =
|
||||
await this.twentyORMGlobalManager.getRepositoryForWorkspace<WorkspaceMemberWorkspaceEntity>(
|
||||
workspaceId,
|
||||
'workspaceMember',
|
||||
);
|
||||
const workspaceMember = await workspaceMemberRepository.findOneOrFail({
|
||||
where: { id: workspaceMemberId },
|
||||
});
|
||||
|
||||
const userId = workspaceMember.userId;
|
||||
|
||||
const connectedAccountId = payload.properties.before.id;
|
||||
|
||||
await this.accountsToReconnectService.removeAccountToReconnect(
|
||||
userId,
|
||||
workspaceId,
|
||||
connectedAccountId,
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,51 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service';
|
||||
import {
|
||||
ConnectedAccountKeys,
|
||||
ConnectedAccountKeyValueType,
|
||||
} from 'src/modules/connected-account/types/connected-account-key-value.type';
|
||||
|
||||
@Injectable()
|
||||
export class AccountsToReconnectService {
|
||||
constructor(
|
||||
private readonly userVarsService: UserVarsService<ConnectedAccountKeyValueType>,
|
||||
) {}
|
||||
|
||||
public async removeAccountToReconnect(
|
||||
userId: string,
|
||||
workspaceId: string,
|
||||
connectedAccountId: string,
|
||||
) {
|
||||
const accountsToReconnect = await this.userVarsService.get({
|
||||
userId,
|
||||
workspaceId,
|
||||
key: ConnectedAccountKeys.ACCOUNTS_TO_RECONNECT,
|
||||
});
|
||||
|
||||
if (!accountsToReconnect) {
|
||||
return;
|
||||
}
|
||||
|
||||
const updatedAccountsToReconnect = accountsToReconnect.filter(
|
||||
(id) => id !== connectedAccountId,
|
||||
);
|
||||
|
||||
if (updatedAccountsToReconnect.length === 0) {
|
||||
await this.userVarsService.delete({
|
||||
userId,
|
||||
workspaceId,
|
||||
key: ConnectedAccountKeys.ACCOUNTS_TO_RECONNECT,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await this.userVarsService.set({
|
||||
userId,
|
||||
workspaceId,
|
||||
key: ConnectedAccountKeys.ACCOUNTS_TO_RECONNECT,
|
||||
value: updatedAccountsToReconnect,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,7 @@
|
||||
export enum ConnectedAccountKeys {
|
||||
ACCOUNTS_TO_RECONNECT = 'ACCOUNTS_TO_RECONNECT',
|
||||
}
|
||||
|
||||
export type ConnectedAccountKeyValueType = {
|
||||
[ConnectedAccountKeys.ACCOUNTS_TO_RECONNECT]: string[];
|
||||
};
|
||||
@ -2,6 +2,7 @@ import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagEntity } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { UserVarsModule } from 'src/engine/core-modules/user/user-vars/user-vars.module';
|
||||
import { ObjectMetadataRepositoryModule } from 'src/engine/object-metadata-repository/object-metadata-repository.module';
|
||||
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
|
||||
import { MessagingChannelSyncStatusService } from 'src/modules/messaging/common/services/messaging-channel-sync-status.service';
|
||||
@ -20,6 +21,7 @@ import { PersonWorkspaceEntity } from 'src/modules/person/standard-objects/perso
|
||||
MessageThreadWorkspaceEntity,
|
||||
]),
|
||||
TypeOrmModule.forFeature([FeatureFlagEntity], 'core'),
|
||||
UserVarsModule,
|
||||
],
|
||||
providers: [MessagingChannelSyncStatusService],
|
||||
exports: [MessagingChannelSyncStatusService],
|
||||
|
||||
@ -1,14 +1,20 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { UserVarsService } from 'src/engine/core-modules/user/user-vars/services/user-vars.service';
|
||||
import { CacheStorageService } from 'src/engine/integrations/cache-storage/cache-storage.service';
|
||||
import { InjectCacheStorage } from 'src/engine/integrations/cache-storage/decorators/cache-storage.decorator';
|
||||
import { CacheStorageNamespace } from 'src/engine/integrations/cache-storage/types/cache-storage-namespace.enum';
|
||||
import { InjectObjectMetadataRepository } from 'src/engine/object-metadata-repository/object-metadata-repository.decorator';
|
||||
import { TwentyORMManager } from 'src/engine/twenty-orm/twenty-orm.manager';
|
||||
import {
|
||||
ConnectedAccountKeys,
|
||||
ConnectedAccountKeyValueType,
|
||||
} from 'src/modules/connected-account/types/connected-account-key-value.type';
|
||||
import { MessageChannelRepository } from 'src/modules/messaging/common/repositories/message-channel.repository';
|
||||
import {
|
||||
MessageChannelWorkspaceEntity,
|
||||
MessageChannelSyncStage,
|
||||
MessageChannelSyncStatus,
|
||||
MessageChannelWorkspaceEntity,
|
||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
|
||||
@Injectable()
|
||||
@ -18,6 +24,8 @@ export class MessagingChannelSyncStatusService {
|
||||
private readonly messageChannelRepository: MessageChannelRepository,
|
||||
@InjectCacheStorage(CacheStorageNamespace.Messaging)
|
||||
private readonly cacheStorage: CacheStorageService,
|
||||
private readonly userVarsService: UserVarsService<ConnectedAccountKeyValueType>,
|
||||
private readonly twentyORMManager: TwentyORMManager,
|
||||
) {}
|
||||
|
||||
public async scheduleFullMessageListFetch(
|
||||
@ -160,5 +168,55 @@ export class MessagingChannelSyncStatusService {
|
||||
MessageChannelSyncStatus.FAILED_INSUFFICIENT_PERMISSIONS,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
await this.addToAccountsToReconnect(messageChannelId, workspaceId);
|
||||
}
|
||||
|
||||
private async addToAccountsToReconnect(
|
||||
messageChannelId: string,
|
||||
workspaceId: string,
|
||||
) {
|
||||
const messageChannelRepository =
|
||||
await this.twentyORMManager.getRepository<MessageChannelWorkspaceEntity>(
|
||||
'messageChannel',
|
||||
);
|
||||
|
||||
const messageChannel = await messageChannelRepository.findOne({
|
||||
where: {
|
||||
id: messageChannelId,
|
||||
},
|
||||
relations: {
|
||||
connectedAccount: {
|
||||
accountOwner: true,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!messageChannel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const userId = messageChannel.connectedAccount.accountOwner.userId;
|
||||
const connectedAccountId = messageChannel.connectedAccount.id;
|
||||
|
||||
const accountsToReconnect =
|
||||
(await this.userVarsService.get({
|
||||
userId,
|
||||
workspaceId,
|
||||
key: ConnectedAccountKeys.ACCOUNTS_TO_RECONNECT,
|
||||
})) ?? [];
|
||||
|
||||
if (accountsToReconnect.includes(connectedAccountId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
accountsToReconnect.push(connectedAccountId);
|
||||
|
||||
await this.userVarsService.set({
|
||||
userId,
|
||||
workspaceId,
|
||||
key: ConnectedAccountKeys.ACCOUNTS_TO_RECONNECT,
|
||||
value: accountsToReconnect,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,17 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { CalendarModule } from 'src/modules/calendar/calendar.module';
|
||||
import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
|
||||
import { MessagingModule } from 'src/modules/messaging/messaging.module';
|
||||
import { ViewModule } from 'src/modules/view/view.module';
|
||||
|
||||
@Module({
|
||||
imports: [MessagingModule, CalendarModule, ViewModule],
|
||||
imports: [
|
||||
MessagingModule,
|
||||
CalendarModule,
|
||||
ConnectedAccountModule,
|
||||
ViewModule,
|
||||
],
|
||||
providers: [],
|
||||
exports: [],
|
||||
})
|
||||
|
||||
@ -1,8 +1,6 @@
|
||||
import { Meta, StoryObj } from '@storybook/react';
|
||||
import { ComponentDecorator, IconRefresh } from 'twenty-ui';
|
||||
|
||||
import { Button } from '@/ui/input/button/components/Button';
|
||||
|
||||
import { ComponentDecorator } from '@ui/testing';
|
||||
import { Banner } from '../Banner';
|
||||
|
||||
const meta: Meta<typeof Banner> = {
|
||||
@ -13,13 +11,6 @@ const meta: Meta<typeof Banner> = {
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<Banner {...args}>
|
||||
Sync lost with mailbox hello@twenty.com. Please reconnect for updates:
|
||||
<Button
|
||||
variant="secondary"
|
||||
title="Reconnect"
|
||||
Icon={IconRefresh}
|
||||
size="small"
|
||||
inverted
|
||||
/>
|
||||
</Banner>
|
||||
),
|
||||
argTypes: {
|
||||
@ -4,6 +4,7 @@ export * from './avatar/components/states/isInvalidAvatarUrlState';
|
||||
export * from './avatar/constants/AvatarPropertiesBySize';
|
||||
export * from './avatar/types/AvatarSize';
|
||||
export * from './avatar/types/AvatarType';
|
||||
export * from './banner/components/Banner';
|
||||
export * from './checkmark/components/AnimatedCheckmark';
|
||||
export * from './checkmark/components/Checkmark';
|
||||
export * from './chip/components/AvatarChip';
|
||||
|
||||
Reference in New Issue
Block a user