feat: secure and user-scope metadata resolvers for messaging infrastructure (#18787)
## Summary Builds on the messaging infrastructure migration (#18784) by securing and user-scoping all 4 metadata resolvers: ### DTOs secured - **ConnectedAccountDTO**: `@HideField()` on `accessToken`, `refreshToken`, `connectionParameters`, `oidcTokenClaims` - **MessageChannelDTO / CalendarChannelDTO**: `@HideField()` on `syncCursor` - **MessageFolderDTO**: `@HideField()` on `syncCursor`, `externalId` - **UpdateMessageFolderInputUpdates**: stripped to only `isSynced` (removed `name`, `syncCursor`, `pendingSyncAction`) ### Resolvers user-scoped via `@AuthUserWorkspaceId()` - `myConnectedAccounts` — returns only the calling user's accounts (no permission guard) - `myMessageChannels(connectedAccountId?)` — returns channels for the user's connected accounts - `myCalendarChannels(connectedAccountId?)` — same pattern - `myMessageFolders(messageChannelId?)` — returns folders through the ownership chain ### Admin-only listing with permission guard - `connectedAccounts` query retained with `SettingsPermissionGuard(CONNECTED_ACCOUNTS)` for admin listing of all workspace accounts ### Unsafe mutations removed - Removed `createConnectedAccount`, `updateConnectedAccount` (OAuth/IMAP flows create/refresh tokens server-side) - Removed `create*`/`delete*` mutations from MessageChannel, CalendarChannel, MessageFolder (managed by sync engine) ### Update mutations restricted with ownership verification - `deleteConnectedAccount(id)` — verifies `entity.userWorkspaceId === currentUserWorkspaceId` - `updateMessageChannel` / `updateCalendarChannel` / `updateMessageFolder` — verify ownership through connected account chain - New `OWNERSHIP_VIOLATION` exception codes map to `ForbiddenError` in GraphQL ### `@AuthUserWorkspaceId` decorator hardened - Added `allowUndefined` option (default: `false`) — throws `ForbiddenException` if `userWorkspaceId` is undefined (e.g. API key auth) - Existing callers updated to `@AuthUserWorkspaceId({ allowUndefined: true })` where needed - New user-scoped resolvers enforce non-undefined `userWorkspaceId` at decorator level ### Exception handler chaining - `MessageFolderGraphqlApiExceptionInterceptor`, `MessageChannelGraphqlApiExceptionInterceptor`, `CalendarChannelGraphqlApiExceptionInterceptor` chain upstream exception handling (ConnectedAccountException, MessageChannelException) for correct `ForbiddenError` propagation ### Metadata services enhanced - `findByUserWorkspaceId()`, `getUserConnectedAccountIds()`, `findByConnectedAccountIds()`, `findByMessageChannelIds()` - `findBy*ForUser()` methods encapsulate ownership checks before querying - `verifyOwnership()` on all 4 services with proper chain validation - Named parameters throughout for clarity ### Dev seeds for both schemas - Added JANE to connected account, message channel, calendar channel workspace seeds - Created message folder workspace seeds (TIM, JONY, JANE) - New `seed-metadata-entities.util.ts` seeds core schema tables (connectedAccount, messageChannel, calendarChannel, messageFolder) with same IDs as workspace seeds, mapping `accountOwnerId` → `userWorkspaceId` ### Integration tests (using seeds, not raw SQL) - 4 test suites (`connected-account`, `message-channel`, `calendar-channel`, `message-folder`) - Tests use seeded data IDs from seed constants — no raw SQL inserts/deletes - Tests read via GraphQL resolvers - Tests cover: user scoping, admin permission checks, sensitive field exclusion, ownership enforcement on mutations ### Frontend migration - Feature-flag-gated hooks (`useMyConnectedAccounts`, `useMyMessageChannels`, `useMyCalendarChannels`, `useMyMessageFolders`) - When `IS_CONNECTED_ACCOUNT_MIGRATED` is on: hooks use metadata API (`POST /metadata`) - When flag is off: hooks use existing workspace API (`POST /graphql`, current behavior) - Settings account pages updated to use new hooks - `useEffect` extracted to `SettingsAccountsSelectedMessageChannelEffect` component per project conventions - Error messages translated with Lingui ## Test plan - [x] Server typecheck passes - [x] Server lint passes - [x] Server unit tests pass (477 suites, 4269 tests) - [x] Frontend typecheck passes - [x] Frontend lint passes - [x] Integration tests verify user-scoping, ownership enforcement, hidden fields - [ ] CI green --------- Co-authored-by: neo773 <neo773@protonmail.com>
This commit is contained in:
@@ -770,7 +770,6 @@ export type CalendarChannel = {
|
||||
id: Scalars['UUID'];
|
||||
isContactAutoCreationEnabled: Scalars['Boolean'];
|
||||
isSyncEnabled: Scalars['Boolean'];
|
||||
syncCursor?: Maybe<Scalars['String']>;
|
||||
syncStage: CalendarChannelSyncStage;
|
||||
syncStageStartedAt?: Maybe<Scalars['DateTime']>;
|
||||
syncStatus: CalendarChannelSyncStatus;
|
||||
@@ -979,18 +978,14 @@ export type ConfigVariablesGroupData = {
|
||||
|
||||
export type ConnectedAccountDto = {
|
||||
__typename?: 'ConnectedAccountDTO';
|
||||
accessToken?: Maybe<Scalars['String']>;
|
||||
authFailedAt?: Maybe<Scalars['DateTime']>;
|
||||
connectionParameters?: Maybe<Scalars['JSON']>;
|
||||
createdAt: Scalars['DateTime'];
|
||||
handle: Scalars['String'];
|
||||
handleAliases?: Maybe<Array<Scalars['String']>>;
|
||||
id: Scalars['UUID'];
|
||||
lastCredentialsRefreshedAt?: Maybe<Scalars['DateTime']>;
|
||||
lastSignedInAt?: Maybe<Scalars['DateTime']>;
|
||||
oidcTokenClaims?: Maybe<Scalars['JSON']>;
|
||||
provider: Scalars['String'];
|
||||
refreshToken?: Maybe<Scalars['String']>;
|
||||
scopes?: Maybe<Array<Scalars['String']>>;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
userWorkspaceId: Scalars['UUID'];
|
||||
@@ -1077,17 +1072,6 @@ export type CreateApprovedAccessDomainInput = {
|
||||
email: Scalars['String'];
|
||||
};
|
||||
|
||||
export type CreateCalendarChannelInput = {
|
||||
connectedAccountId: Scalars['UUID'];
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy;
|
||||
handle: Scalars['String'];
|
||||
id?: InputMaybe<Scalars['UUID']>;
|
||||
isContactAutoCreationEnabled: Scalars['Boolean'];
|
||||
isSyncEnabled: Scalars['Boolean'];
|
||||
syncStage: CalendarChannelSyncStage;
|
||||
visibility: CalendarChannelVisibility;
|
||||
};
|
||||
|
||||
export type CreateCommandMenuItemInput = {
|
||||
availabilityObjectMetadataId?: InputMaybe<Scalars['UUID']>;
|
||||
availabilityType?: InputMaybe<CommandMenuItemAvailabilityType>;
|
||||
@@ -1103,16 +1087,6 @@ export type CreateCommandMenuItemInput = {
|
||||
workflowVersionId?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
export type CreateConnectedAccountInput = {
|
||||
accessToken?: InputMaybe<Scalars['String']>;
|
||||
handle: Scalars['String'];
|
||||
id?: InputMaybe<Scalars['UUID']>;
|
||||
provider: Scalars['String'];
|
||||
refreshToken?: InputMaybe<Scalars['String']>;
|
||||
scopes?: InputMaybe<Array<Scalars['String']>>;
|
||||
userWorkspaceId: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type CreateFieldInput = {
|
||||
defaultValue?: InputMaybe<Scalars['JSON']>;
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
@@ -1159,33 +1133,6 @@ export type CreateLogicFunctionFromSourceInput = {
|
||||
universalIdentifier?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
export type CreateMessageChannelInput = {
|
||||
connectedAccountId: Scalars['UUID'];
|
||||
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy;
|
||||
excludeGroupEmails: Scalars['Boolean'];
|
||||
excludeNonProfessionalEmails: Scalars['Boolean'];
|
||||
handle: Scalars['String'];
|
||||
id?: InputMaybe<Scalars['UUID']>;
|
||||
isContactAutoCreationEnabled: Scalars['Boolean'];
|
||||
isSyncEnabled: Scalars['Boolean'];
|
||||
messageFolderImportPolicy: MessageFolderImportPolicy;
|
||||
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction;
|
||||
syncStage: MessageChannelSyncStage;
|
||||
type: MessageChannelType;
|
||||
visibility: MessageChannelVisibility;
|
||||
};
|
||||
|
||||
export type CreateMessageFolderInput = {
|
||||
externalId?: InputMaybe<Scalars['String']>;
|
||||
id?: InputMaybe<Scalars['UUID']>;
|
||||
isSentFolder: Scalars['Boolean'];
|
||||
isSynced: Scalars['Boolean'];
|
||||
messageChannelId: Scalars['UUID'];
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
parentFolderId?: InputMaybe<Scalars['UUID']>;
|
||||
pendingSyncAction: MessageFolderPendingSyncAction;
|
||||
};
|
||||
|
||||
export type CreateNavigationMenuItemInput = {
|
||||
color?: InputMaybe<Scalars['String']>;
|
||||
folderId?: InputMaybe<Scalars['UUID']>;
|
||||
@@ -2421,7 +2368,6 @@ export type MessageChannel = {
|
||||
isSyncEnabled: Scalars['Boolean'];
|
||||
messageFolderImportPolicy: MessageFolderImportPolicy;
|
||||
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction;
|
||||
syncCursor?: Maybe<Scalars['String']>;
|
||||
syncStage: MessageChannelSyncStage;
|
||||
syncStageStartedAt?: Maybe<Scalars['DateTime']>;
|
||||
syncStatus: MessageChannelSyncStatus;
|
||||
@@ -2486,7 +2432,6 @@ export type MessageFolder = {
|
||||
name?: Maybe<Scalars['String']>;
|
||||
parentFolderId?: Maybe<Scalars['UUID']>;
|
||||
pendingSyncAction: MessageFolderPendingSyncAction;
|
||||
syncCursor?: Maybe<Scalars['String']>;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
};
|
||||
|
||||
@@ -2572,10 +2517,8 @@ export type Mutation = {
|
||||
createApplicationRegistration: CreateApplicationRegistration;
|
||||
createApplicationRegistrationVariable: ApplicationRegistrationVariable;
|
||||
createApprovedAccessDomain: ApprovedAccessDomain;
|
||||
createCalendarChannel: CalendarChannel;
|
||||
createChatThread: AgentChatThread;
|
||||
createCommandMenuItem: CommandMenuItem;
|
||||
createConnectedAccount: ConnectedAccountDto;
|
||||
createDatabaseConfigVariable: Scalars['Boolean'];
|
||||
createDevelopmentApplication: DevelopmentApplication;
|
||||
createEmailingDomain: EmailingDomain;
|
||||
@@ -2583,8 +2526,6 @@ export type Mutation = {
|
||||
createManyViewFieldGroups: Array<ViewFieldGroup>;
|
||||
createManyViewFields: Array<ViewField>;
|
||||
createManyViewGroups: Array<ViewGroup>;
|
||||
createMessageChannel: MessageChannel;
|
||||
createMessageFolder: MessageFolder;
|
||||
createNavigationMenuItem: NavigationMenuItem;
|
||||
createOIDCIdentityProvider: SetupSso;
|
||||
createObjectEvent: Analytics;
|
||||
@@ -2612,7 +2553,6 @@ export type Mutation = {
|
||||
deleteApplicationRegistration: Scalars['Boolean'];
|
||||
deleteApplicationRegistrationVariable: Scalars['Boolean'];
|
||||
deleteApprovedAccessDomain: Scalars['Boolean'];
|
||||
deleteCalendarChannel: CalendarChannel;
|
||||
deleteCommandMenuItem: CommandMenuItem;
|
||||
deleteConnectedAccount: ConnectedAccountDto;
|
||||
deleteCurrentWorkspace: Workspace;
|
||||
@@ -2620,8 +2560,6 @@ export type Mutation = {
|
||||
deleteEmailingDomain: Scalars['Boolean'];
|
||||
deleteFrontComponent: FrontComponent;
|
||||
deleteJobs: DeleteJobsResponse;
|
||||
deleteMessageChannel: MessageChannel;
|
||||
deleteMessageFolder: MessageFolder;
|
||||
deleteNavigationMenuItem: NavigationMenuItem;
|
||||
deleteOneAgent: Agent;
|
||||
deleteOneField: Field;
|
||||
@@ -2708,12 +2646,12 @@ export type Mutation = {
|
||||
updateApplicationRegistrationVariable: ApplicationRegistrationVariable;
|
||||
updateCalendarChannel: CalendarChannel;
|
||||
updateCommandMenuItem: CommandMenuItem;
|
||||
updateConnectedAccount: ConnectedAccountDto;
|
||||
updateDatabaseConfigVariable: Scalars['Boolean'];
|
||||
updateFrontComponent: FrontComponent;
|
||||
updateLabPublicFeatureFlag: FeatureFlag;
|
||||
updateMessageChannel: MessageChannel;
|
||||
updateMessageFolder: MessageFolder;
|
||||
updateMessageFolders: Array<MessageFolder>;
|
||||
updateNavigationMenuItem: NavigationMenuItem;
|
||||
updateOneAgent: Agent;
|
||||
updateOneApplicationVariable: Scalars['Boolean'];
|
||||
@@ -2831,21 +2769,11 @@ export type MutationCreateApprovedAccessDomainArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateCalendarChannelArgs = {
|
||||
input: CreateCalendarChannelInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateCommandMenuItemArgs = {
|
||||
input: CreateCommandMenuItemInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateConnectedAccountArgs = {
|
||||
input: CreateConnectedAccountInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateDatabaseConfigVariableArgs = {
|
||||
key: Scalars['String'];
|
||||
value: Scalars['JSON'];
|
||||
@@ -2884,16 +2812,6 @@ export type MutationCreateManyViewGroupsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateMessageChannelArgs = {
|
||||
input: CreateMessageChannelInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateMessageFolderArgs = {
|
||||
input: CreateMessageFolderInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateNavigationMenuItemArgs = {
|
||||
input: CreateNavigationMenuItemInput;
|
||||
};
|
||||
@@ -3032,11 +2950,6 @@ export type MutationDeleteApprovedAccessDomainArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteCalendarChannelArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteCommandMenuItemArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
@@ -3068,16 +2981,6 @@ export type MutationDeleteJobsArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteMessageChannelArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteMessageFolderArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteNavigationMenuItemArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
@@ -3490,11 +3393,6 @@ export type MutationUpdateCommandMenuItemArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateConnectedAccountArgs = {
|
||||
input: UpdateConnectedAccountInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateDatabaseConfigVariableArgs = {
|
||||
key: Scalars['String'];
|
||||
value: Scalars['JSON'];
|
||||
@@ -3521,6 +3419,11 @@ export type MutationUpdateMessageFolderArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateMessageFoldersArgs = {
|
||||
input: UpdateMessageFoldersInput;
|
||||
};
|
||||
|
||||
|
||||
export type MutationUpdateNavigationMenuItemArgs = {
|
||||
input: UpdateOneNavigationMenuItemInput;
|
||||
};
|
||||
@@ -4222,8 +4125,6 @@ export type Query = {
|
||||
applicationRegistrationTarballUrl?: Maybe<Scalars['String']>;
|
||||
barChartData: BarChartData;
|
||||
billingPortalSession: BillingSession;
|
||||
calendarChannel?: Maybe<CalendarChannel>;
|
||||
calendarChannels: Array<CalendarChannel>;
|
||||
chatMessages: Array<AgentMessage>;
|
||||
chatThread: AgentChatThread;
|
||||
chatThreads: AgentChatThreadConnection;
|
||||
@@ -4231,7 +4132,6 @@ export type Query = {
|
||||
checkWorkspaceInviteHashIsValid: WorkspaceInviteHashValid;
|
||||
commandMenuItem?: Maybe<CommandMenuItem>;
|
||||
commandMenuItems: Array<CommandMenuItem>;
|
||||
connectedAccount?: Maybe<ConnectedAccountDto>;
|
||||
connectedAccounts: Array<ConnectedAccountDto>;
|
||||
currentUser: User;
|
||||
currentWorkspace: Workspace;
|
||||
@@ -4307,11 +4207,11 @@ export type Query = {
|
||||
indexMetadatas: IndexConnection;
|
||||
lineChartData: LineChartData;
|
||||
listPlans: Array<BillingPlan>;
|
||||
messageChannel?: Maybe<MessageChannel>;
|
||||
messageChannels: Array<MessageChannel>;
|
||||
messageFolder?: Maybe<MessageFolder>;
|
||||
messageFolders: Array<MessageFolder>;
|
||||
minimalMetadata: MinimalMetadata;
|
||||
myCalendarChannels: Array<CalendarChannel>;
|
||||
myConnectedAccounts: Array<ConnectedAccountDto>;
|
||||
myMessageChannels: Array<MessageChannel>;
|
||||
myMessageFolders: Array<MessageFolder>;
|
||||
navigationMenuItem?: Maybe<NavigationMenuItem>;
|
||||
navigationMenuItems: Array<NavigationMenuItem>;
|
||||
object: Object;
|
||||
@@ -4352,16 +4252,6 @@ export type QueryBillingPortalSessionArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryCalendarChannelArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryCalendarChannelsArgs = {
|
||||
connectedAccountId?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryChatMessagesArgs = {
|
||||
threadId: Scalars['UUID'];
|
||||
};
|
||||
@@ -4395,11 +4285,6 @@ export type QueryCommandMenuItemArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryConnectedAccountArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryEnterpriseCheckoutSessionArgs = {
|
||||
billingInterval?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
@@ -4663,22 +4548,17 @@ export type QueryLineChartDataArgs = {
|
||||
};
|
||||
|
||||
|
||||
export type QueryMessageChannelArgs = {
|
||||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
|
||||
export type QueryMessageChannelsArgs = {
|
||||
export type QueryMyCalendarChannelsArgs = {
|
||||
connectedAccountId?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryMessageFolderArgs = {
|
||||
id: Scalars['UUID'];
|
||||
export type QueryMyMessageChannelsArgs = {
|
||||
connectedAccountId?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
|
||||
export type QueryMessageFoldersArgs = {
|
||||
export type QueryMyMessageFoldersArgs = {
|
||||
messageChannelId?: InputMaybe<Scalars['UUID']>;
|
||||
};
|
||||
|
||||
@@ -5222,18 +5102,6 @@ export type UpdateCommandMenuItemInput = {
|
||||
shortLabel?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type UpdateConnectedAccountInput = {
|
||||
id: Scalars['UUID'];
|
||||
update: UpdateConnectedAccountInputUpdates;
|
||||
};
|
||||
|
||||
export type UpdateConnectedAccountInputUpdates = {
|
||||
accessToken?: InputMaybe<Scalars['String']>;
|
||||
handleAliases?: InputMaybe<Array<Scalars['String']>>;
|
||||
refreshToken?: InputMaybe<Scalars['String']>;
|
||||
scopes?: InputMaybe<Array<Scalars['String']>>;
|
||||
};
|
||||
|
||||
export type UpdateFieldInput = {
|
||||
defaultValue?: InputMaybe<Scalars['JSON']>;
|
||||
description?: InputMaybe<Scalars['String']>;
|
||||
@@ -5312,9 +5180,11 @@ export type UpdateMessageFolderInput = {
|
||||
|
||||
export type UpdateMessageFolderInputUpdates = {
|
||||
isSynced?: InputMaybe<Scalars['Boolean']>;
|
||||
name?: InputMaybe<Scalars['String']>;
|
||||
pendingSyncAction?: InputMaybe<MessageFolderPendingSyncAction>;
|
||||
syncCursor?: InputMaybe<Scalars['String']>;
|
||||
};
|
||||
|
||||
export type UpdateMessageFoldersInput = {
|
||||
ids: Array<Scalars['UUID']>;
|
||||
update: UpdateMessageFolderInputUpdates;
|
||||
};
|
||||
|
||||
export type UpdateNavigationMenuItemInput = {
|
||||
@@ -6954,6 +6824,13 @@ export type PieChartDataQueryVariables = Exact<{
|
||||
|
||||
export type PieChartDataQuery = { __typename?: 'Query', pieChartData: { __typename?: 'PieChartData', showLegend: boolean, showDataLabels: boolean, showCenterMetric: boolean, hasTooManyGroups: boolean, formattedToRawLookup: any, data: Array<{ __typename?: 'PieChartDataItem', id: string, value: number }> } };
|
||||
|
||||
export type DeleteConnectedAccountMutationVariables = Exact<{
|
||||
id: Scalars['UUID'];
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteConnectedAccountMutation = { __typename?: 'Mutation', deleteConnectedAccount: { __typename?: 'ConnectedAccountDTO', id: string } };
|
||||
|
||||
export type SaveImapSmtpCaldavAccountMutationVariables = Exact<{
|
||||
accountOwnerId: Scalars['UUID'];
|
||||
handle: Scalars['String'];
|
||||
@@ -6971,6 +6848,34 @@ export type StartChannelSyncMutationVariables = Exact<{
|
||||
|
||||
export type StartChannelSyncMutation = { __typename?: 'Mutation', startChannelSync: { __typename?: 'ChannelSyncSuccess', success: boolean } };
|
||||
|
||||
export type UpdateCalendarChannelMutationVariables = Exact<{
|
||||
input: UpdateCalendarChannelInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateCalendarChannelMutation = { __typename?: 'Mutation', updateCalendarChannel: { __typename?: 'CalendarChannel', id: string, visibility: CalendarChannelVisibility, isContactAutoCreationEnabled: boolean, contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy } };
|
||||
|
||||
export type UpdateMessageChannelMutationVariables = Exact<{
|
||||
input: UpdateMessageChannelInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateMessageChannelMutation = { __typename?: 'Mutation', updateMessageChannel: { __typename?: 'MessageChannel', id: string, visibility: MessageChannelVisibility, contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy, excludeNonProfessionalEmails: boolean, excludeGroupEmails: boolean, messageFolderImportPolicy: MessageFolderImportPolicy } };
|
||||
|
||||
export type UpdateMessageFolderMutationVariables = Exact<{
|
||||
input: UpdateMessageFolderInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateMessageFolderMutation = { __typename?: 'Mutation', updateMessageFolder: { __typename?: 'MessageFolder', id: string, isSynced: boolean } };
|
||||
|
||||
export type UpdateMessageFoldersMutationVariables = Exact<{
|
||||
input: UpdateMessageFoldersInput;
|
||||
}>;
|
||||
|
||||
|
||||
export type UpdateMessageFoldersMutation = { __typename?: 'Mutation', updateMessageFolders: Array<{ __typename?: 'MessageFolder', id: string, isSynced: boolean }> };
|
||||
|
||||
export type GetConnectedImapSmtpCaldavAccountQueryVariables = Exact<{
|
||||
id: Scalars['UUID'];
|
||||
}>;
|
||||
@@ -6978,6 +6883,32 @@ export type GetConnectedImapSmtpCaldavAccountQueryVariables = Exact<{
|
||||
|
||||
export type GetConnectedImapSmtpCaldavAccountQuery = { __typename?: 'Query', getConnectedImapSmtpCaldavAccount: { __typename?: 'ConnectedImapSmtpCaldavAccount', id: string, handle: string, provider: string, accountOwnerId: string, connectionParameters?: { __typename?: 'ImapSmtpCaldavConnectionParameters', IMAP?: { __typename?: 'ConnectionParametersOutput', host: string, port: number, secure?: boolean | null, username?: string | null, password: string } | null, SMTP?: { __typename?: 'ConnectionParametersOutput', host: string, username?: string | null, port: number, secure?: boolean | null, password: string } | null, CALDAV?: { __typename?: 'ConnectionParametersOutput', host: string, username?: string | null, password: string } | null } | null } };
|
||||
|
||||
export type MyCalendarChannelsQueryVariables = Exact<{
|
||||
connectedAccountId?: InputMaybe<Scalars['UUID']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type MyCalendarChannelsQuery = { __typename?: 'Query', myCalendarChannels: Array<{ __typename?: 'CalendarChannel', id: string, handle: string, visibility: CalendarChannelVisibility, syncStatus: CalendarChannelSyncStatus, syncStage: CalendarChannelSyncStage, syncStageStartedAt?: string | null, isContactAutoCreationEnabled: boolean, contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy, isSyncEnabled: boolean, connectedAccountId: string, createdAt: string, updatedAt: string }> };
|
||||
|
||||
export type MyConnectedAccountsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type MyConnectedAccountsQuery = { __typename?: 'Query', myConnectedAccounts: Array<{ __typename?: 'ConnectedAccountDTO', id: string, handle: string, provider: string, authFailedAt?: string | null, scopes?: Array<string> | null, handleAliases?: Array<string> | null, lastSignedInAt?: string | null, userWorkspaceId: string, createdAt: string, updatedAt: string }> };
|
||||
|
||||
export type MyMessageChannelsQueryVariables = Exact<{
|
||||
connectedAccountId?: InputMaybe<Scalars['UUID']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type MyMessageChannelsQuery = { __typename?: 'Query', myMessageChannels: Array<{ __typename?: 'MessageChannel', id: string, handle: string, visibility: MessageChannelVisibility, type: MessageChannelType, isContactAutoCreationEnabled: boolean, contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy, messageFolderImportPolicy: MessageFolderImportPolicy, excludeNonProfessionalEmails: boolean, excludeGroupEmails: boolean, isSyncEnabled: boolean, syncStatus: MessageChannelSyncStatus, syncStage: MessageChannelSyncStage, syncStageStartedAt?: string | null, connectedAccountId: string, createdAt: string, updatedAt: string }> };
|
||||
|
||||
export type MyMessageFoldersQueryVariables = Exact<{
|
||||
messageChannelId?: InputMaybe<Scalars['UUID']>;
|
||||
}>;
|
||||
|
||||
|
||||
export type MyMessageFoldersQuery = { __typename?: 'Query', myMessageFolders: Array<{ __typename?: 'MessageFolder', id: string, name?: string | null, isSynced: boolean, isSentFolder: boolean, parentFolderId?: string | null, externalId?: string | null, messageChannelId: string, createdAt: string, updatedAt: string }> };
|
||||
|
||||
export type SetAdminAiModelEnabledMutationVariables = Exact<{
|
||||
modelId: Scalars['String'];
|
||||
enabled: Scalars['Boolean'];
|
||||
@@ -8079,9 +8010,18 @@ export const FindAllRecordPageLayoutsDocument = {"kind":"Document","definitions"
|
||||
export const BarChartDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"BarChartData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"BarChartDataInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"barChartData"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"data"}},{"kind":"Field","name":{"kind":"Name","value":"indexBy"}},{"kind":"Field","name":{"kind":"Name","value":"keys"}},{"kind":"Field","name":{"kind":"Name","value":"series"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"key"}},{"kind":"Field","name":{"kind":"Name","value":"label"}}]}},{"kind":"Field","name":{"kind":"Name","value":"xAxisLabel"}},{"kind":"Field","name":{"kind":"Name","value":"yAxisLabel"}},{"kind":"Field","name":{"kind":"Name","value":"showLegend"}},{"kind":"Field","name":{"kind":"Name","value":"showDataLabels"}},{"kind":"Field","name":{"kind":"Name","value":"layout"}},{"kind":"Field","name":{"kind":"Name","value":"groupMode"}},{"kind":"Field","name":{"kind":"Name","value":"hasTooManyGroups"}},{"kind":"Field","name":{"kind":"Name","value":"formattedToRawLookup"}}]}}]}}]} as unknown as DocumentNode<BarChartDataQuery, BarChartDataQueryVariables>;
|
||||
export const LineChartDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"LineChartData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"LineChartDataInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"lineChartData"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"series"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"x"}},{"kind":"Field","name":{"kind":"Name","value":"y"}}]}}]}},{"kind":"Field","name":{"kind":"Name","value":"xAxisLabel"}},{"kind":"Field","name":{"kind":"Name","value":"yAxisLabel"}},{"kind":"Field","name":{"kind":"Name","value":"showLegend"}},{"kind":"Field","name":{"kind":"Name","value":"showDataLabels"}},{"kind":"Field","name":{"kind":"Name","value":"hasTooManyGroups"}},{"kind":"Field","name":{"kind":"Name","value":"formattedToRawLookup"}}]}}]}}]} as unknown as DocumentNode<LineChartDataQuery, LineChartDataQueryVariables>;
|
||||
export const PieChartDataDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"PieChartData"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"PieChartDataInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"pieChartData"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"data"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"value"}}]}},{"kind":"Field","name":{"kind":"Name","value":"showLegend"}},{"kind":"Field","name":{"kind":"Name","value":"showDataLabels"}},{"kind":"Field","name":{"kind":"Name","value":"showCenterMetric"}},{"kind":"Field","name":{"kind":"Name","value":"hasTooManyGroups"}},{"kind":"Field","name":{"kind":"Name","value":"formattedToRawLookup"}}]}}]}}]} as unknown as DocumentNode<PieChartDataQuery, PieChartDataQueryVariables>;
|
||||
export const DeleteConnectedAccountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"DeleteConnectedAccount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"deleteConnectedAccount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}}]}}]}}]} as unknown as DocumentNode<DeleteConnectedAccountMutation, DeleteConnectedAccountMutationVariables>;
|
||||
export const SaveImapSmtpCaldavAccountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SaveImapSmtpCaldavAccount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"accountOwnerId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"handle"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectionParameters"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"EmailAccountConnectionParameters"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"saveImapSmtpCaldavAccount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"accountOwnerId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"accountOwnerId"}}},{"kind":"Argument","name":{"kind":"Name","value":"handle"},"value":{"kind":"Variable","name":{"kind":"Name","value":"handle"}}},{"kind":"Argument","name":{"kind":"Name","value":"connectionParameters"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectionParameters"}}},{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccountId"}}]}}]}}]} as unknown as DocumentNode<SaveImapSmtpCaldavAccountMutation, SaveImapSmtpCaldavAccountMutationVariables>;
|
||||
export const StartChannelSyncDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"StartChannelSync"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"startChannelSync"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"success"}}]}}]}}]} as unknown as DocumentNode<StartChannelSyncMutation, StartChannelSyncMutationVariables>;
|
||||
export const UpdateCalendarChannelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateCalendarChannel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateCalendarChannelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateCalendarChannel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}}]}}]}}]} as unknown as DocumentNode<UpdateCalendarChannelMutation, UpdateCalendarChannelMutationVariables>;
|
||||
export const UpdateMessageChannelDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageChannel"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageChannelInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageChannel"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"excludeNonProfessionalEmails"}},{"kind":"Field","name":{"kind":"Name","value":"excludeGroupEmails"}},{"kind":"Field","name":{"kind":"Name","value":"messageFolderImportPolicy"}}]}}]}}]} as unknown as DocumentNode<UpdateMessageChannelMutation, UpdateMessageChannelMutationVariables>;
|
||||
export const UpdateMessageFolderDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageFolder"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageFolderInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageFolder"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}}]}}]}}]} as unknown as DocumentNode<UpdateMessageFolderMutation, UpdateMessageFolderMutationVariables>;
|
||||
export const UpdateMessageFoldersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"UpdateMessageFolders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"input"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UpdateMessageFoldersInput"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"updateMessageFolders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"input"},"value":{"kind":"Variable","name":{"kind":"Name","value":"input"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}}]}}]}}]} as unknown as DocumentNode<UpdateMessageFoldersMutation, UpdateMessageFoldersMutationVariables>;
|
||||
export const GetConnectedImapSmtpCaldavAccountDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetConnectedImapSmtpCaldavAccount"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"id"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getConnectedImapSmtpCaldavAccount"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"id"},"value":{"kind":"Variable","name":{"kind":"Name","value":"id"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"accountOwnerId"}},{"kind":"Field","name":{"kind":"Name","value":"connectionParameters"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"IMAP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"secure"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}}]}},{"kind":"Field","name":{"kind":"Name","value":"SMTP"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"port"}},{"kind":"Field","name":{"kind":"Name","value":"secure"}},{"kind":"Field","name":{"kind":"Name","value":"password"}}]}},{"kind":"Field","name":{"kind":"Name","value":"CALDAV"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"host"}},{"kind":"Field","name":{"kind":"Name","value":"username"}},{"kind":"Field","name":{"kind":"Name","value":"password"}}]}}]}}]}}]}}]} as unknown as DocumentNode<GetConnectedImapSmtpCaldavAccountQuery, GetConnectedImapSmtpCaldavAccountQueryVariables>;
|
||||
export const MyCalendarChannelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyCalendarChannels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myCalendarChannels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"syncStage"}},{"kind":"Field","name":{"kind":"Name","value":"syncStageStartedAt"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"isSyncEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccountId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<MyCalendarChannelsQuery, MyCalendarChannelsQueryVariables>;
|
||||
export const MyConnectedAccountsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyConnectedAccounts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myConnectedAccounts"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"provider"}},{"kind":"Field","name":{"kind":"Name","value":"authFailedAt"}},{"kind":"Field","name":{"kind":"Name","value":"scopes"}},{"kind":"Field","name":{"kind":"Name","value":"handleAliases"}},{"kind":"Field","name":{"kind":"Name","value":"lastSignedInAt"}},{"kind":"Field","name":{"kind":"Name","value":"userWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<MyConnectedAccountsQuery, MyConnectedAccountsQueryVariables>;
|
||||
export const MyMessageChannelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyMessageChannels"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myMessageChannels"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"connectedAccountId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"connectedAccountId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"handle"}},{"kind":"Field","name":{"kind":"Name","value":"visibility"}},{"kind":"Field","name":{"kind":"Name","value":"type"}},{"kind":"Field","name":{"kind":"Name","value":"isContactAutoCreationEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"contactAutoCreationPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"messageFolderImportPolicy"}},{"kind":"Field","name":{"kind":"Name","value":"excludeNonProfessionalEmails"}},{"kind":"Field","name":{"kind":"Name","value":"excludeGroupEmails"}},{"kind":"Field","name":{"kind":"Name","value":"isSyncEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"syncStatus"}},{"kind":"Field","name":{"kind":"Name","value":"syncStage"}},{"kind":"Field","name":{"kind":"Name","value":"syncStageStartedAt"}},{"kind":"Field","name":{"kind":"Name","value":"connectedAccountId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<MyMessageChannelsQuery, MyMessageChannelsQueryVariables>;
|
||||
export const MyMessageFoldersDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"MyMessageFolders"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"messageChannelId"}},"type":{"kind":"NamedType","name":{"kind":"Name","value":"UUID"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"myMessageFolders"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"messageChannelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"messageChannelId"}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"isSynced"}},{"kind":"Field","name":{"kind":"Name","value":"isSentFolder"}},{"kind":"Field","name":{"kind":"Name","value":"parentFolderId"}},{"kind":"Field","name":{"kind":"Name","value":"externalId"}},{"kind":"Field","name":{"kind":"Name","value":"messageChannelId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]}}]} as unknown as DocumentNode<MyMessageFoldersQuery, MyMessageFoldersQueryVariables>;
|
||||
export const SetAdminAiModelEnabledDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"mutation","name":{"kind":"Name","value":"SetAdminAiModelEnabled"},"variableDefinitions":[{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"String"}}}},{"kind":"VariableDefinition","variable":{"kind":"Variable","name":{"kind":"Name","value":"enabled"}},"type":{"kind":"NonNullType","type":{"kind":"NamedType","name":{"kind":"Name","value":"Boolean"}}}}],"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"setAdminAiModelEnabled"},"arguments":[{"kind":"Argument","name":{"kind":"Name","value":"modelId"},"value":{"kind":"Variable","name":{"kind":"Name","value":"modelId"}}},{"kind":"Argument","name":{"kind":"Name","value":"enabled"},"value":{"kind":"Variable","name":{"kind":"Name","value":"enabled"}}}]}]}}]} as unknown as DocumentNode<SetAdminAiModelEnabledMutation, SetAdminAiModelEnabledMutationVariables>;
|
||||
export const GetAdminAiModelsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"GetAdminAiModels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"getAdminAiModels"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"autoEnableNewModels"}},{"kind":"Field","name":{"kind":"Name","value":"models"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"modelId"}},{"kind":"Field","name":{"kind":"Name","value":"label"}},{"kind":"Field","name":{"kind":"Name","value":"modelFamily"}},{"kind":"Field","name":{"kind":"Name","value":"inferenceProvider"}},{"kind":"Field","name":{"kind":"Name","value":"isAvailable"}},{"kind":"Field","name":{"kind":"Name","value":"isAdminEnabled"}},{"kind":"Field","name":{"kind":"Name","value":"deprecated"}},{"kind":"Field","name":{"kind":"Name","value":"isRecommended"}}]}}]}}]}}]} as unknown as DocumentNode<GetAdminAiModelsQuery, GetAdminAiModelsQueryVariables>;
|
||||
export const FindAllApplicationRegistrationsDocument = {"kind":"Document","definitions":[{"kind":"OperationDefinition","operation":"query","name":{"kind":"Name","value":"FindAllApplicationRegistrations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"findAllApplicationRegistrations"},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"FragmentSpread","name":{"kind":"Name","value":"ApplicationRegistrationFragment"}}]}}]}},{"kind":"FragmentDefinition","name":{"kind":"Name","value":"ApplicationRegistrationFragment"},"typeCondition":{"kind":"NamedType","name":{"kind":"Name","value":"ApplicationRegistration"}},"selectionSet":{"kind":"SelectionSet","selections":[{"kind":"Field","name":{"kind":"Name","value":"id"}},{"kind":"Field","name":{"kind":"Name","value":"universalIdentifier"}},{"kind":"Field","name":{"kind":"Name","value":"name"}},{"kind":"Field","name":{"kind":"Name","value":"description"}},{"kind":"Field","name":{"kind":"Name","value":"logoUrl"}},{"kind":"Field","name":{"kind":"Name","value":"author"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthClientId"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthRedirectUris"}},{"kind":"Field","name":{"kind":"Name","value":"oAuthScopes"}},{"kind":"Field","name":{"kind":"Name","value":"sourceType"}},{"kind":"Field","name":{"kind":"Name","value":"sourcePackage"}},{"kind":"Field","name":{"kind":"Name","value":"latestAvailableVersion"}},{"kind":"Field","name":{"kind":"Name","value":"websiteUrl"}},{"kind":"Field","name":{"kind":"Name","value":"termsUrl"}},{"kind":"Field","name":{"kind":"Name","value":"isListed"}},{"kind":"Field","name":{"kind":"Name","value":"isFeatured"}},{"kind":"Field","name":{"kind":"Name","value":"ownerWorkspaceId"}},{"kind":"Field","name":{"kind":"Name","value":"createdAt"}},{"kind":"Field","name":{"kind":"Name","value":"updatedAt"}}]}}]} as unknown as DocumentNode<FindAllApplicationRegistrationsQuery, FindAllApplicationRegistrationsQueryVariables>;
|
||||
|
||||
+25
-15
@@ -1,6 +1,9 @@
|
||||
import { type CalendarChannel } from '@/accounts/types/CalendarChannel';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { UPDATE_CALENDAR_CHANNEL } from '@/settings/accounts/graphql/mutations/updateCalendarChannel';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { SettingsAccountsEventVisibilitySettingsCard } from '@/settings/accounts/components/SettingsAccountsCalendarVisibilitySettingsCard';
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
import { styled } from '@linaria/react';
|
||||
@@ -27,26 +30,33 @@ type SettingsAccountsCalendarChannelDetailsProps = {
|
||||
export const SettingsAccountsCalendarChannelDetails = ({
|
||||
calendarChannel,
|
||||
}: SettingsAccountsCalendarChannelDetailsProps) => {
|
||||
const featureFlagsMap = useFeatureFlagsMap();
|
||||
const isMigrated =
|
||||
featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false;
|
||||
|
||||
const { updateOneRecord } = useUpdateOneRecord();
|
||||
const [updateMetadataChannel] = useMutation(UPDATE_CALENDAR_CHANNEL);
|
||||
|
||||
const updateChannel = (update: Record<string, unknown>) => {
|
||||
if (isMigrated) {
|
||||
updateMetadataChannel({
|
||||
variables: { input: { id: calendarChannel.id, update } },
|
||||
});
|
||||
} else {
|
||||
updateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.CalendarChannel,
|
||||
idToUpdate: calendarChannel.id,
|
||||
updateOneRecordInput: update,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleVisibilityChange = (value: CalendarChannelVisibility) => {
|
||||
updateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.CalendarChannel,
|
||||
idToUpdate: calendarChannel.id,
|
||||
updateOneRecordInput: {
|
||||
visibility: value,
|
||||
},
|
||||
});
|
||||
updateChannel({ visibility: value });
|
||||
};
|
||||
|
||||
const handleContactAutoCreationToggle = (value: boolean) => {
|
||||
updateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.CalendarChannel,
|
||||
idToUpdate: calendarChannel.id,
|
||||
updateOneRecordInput: {
|
||||
isContactAutoCreationEnabled: value,
|
||||
},
|
||||
});
|
||||
updateChannel({ isContactAutoCreationEnabled: value });
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
+2
-35
@@ -1,17 +1,9 @@
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import {
|
||||
type CalendarChannel,
|
||||
CalendarChannelSyncStage,
|
||||
} from '@/accounts/types/CalendarChannel';
|
||||
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { SettingsAccountsCalendarChannelDetails } from '@/settings/accounts/components/SettingsAccountsCalendarChannelDetails';
|
||||
import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection';
|
||||
import { SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountCalendarChannelsTabListComponentId';
|
||||
import { useMyCalendarChannels } from '@/settings/accounts/hooks/useMyCalendarChannels';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
@@ -27,33 +19,8 @@ export const SettingsAccountsCalendarChannelsContainer = () => {
|
||||
activeTabIdComponentState,
|
||||
SETTINGS_ACCOUNT_CALENDAR_CHANNELS_TAB_LIST_COMPONENT_ID,
|
||||
);
|
||||
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
||||
|
||||
const { records: accounts } = useFindManyRecords<ConnectedAccount>({
|
||||
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
|
||||
filter: {
|
||||
accountOwnerId: {
|
||||
eq: currentWorkspaceMember?.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { records: calendarChannels } = useFindManyRecords<
|
||||
CalendarChannel & {
|
||||
connectedAccount: ConnectedAccount;
|
||||
}
|
||||
>({
|
||||
objectNameSingular: CoreObjectNameSingular.CalendarChannel,
|
||||
filter: {
|
||||
connectedAccountId: {
|
||||
in: accounts.map((account) => account.id),
|
||||
},
|
||||
syncStage: {
|
||||
neq: CalendarChannelSyncStage.PENDING_CONFIGURATION,
|
||||
},
|
||||
},
|
||||
skip: !accounts.length,
|
||||
});
|
||||
const { channels: calendarChannels } = useMyCalendarChannels();
|
||||
|
||||
const tabs = [
|
||||
...calendarChannels.map((calendarChannel) => ({
|
||||
|
||||
+28
-34
@@ -5,8 +5,11 @@ import {
|
||||
type MessageChannelContactAutoCreationPolicy,
|
||||
type MessageFolderImportPolicy,
|
||||
} from '@/accounts/types/MessageChannel';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
import { UPDATE_MESSAGE_CHANNEL } from '@/settings/accounts/graphql/mutations/updateMessageChannel';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { SettingsAccountsMessageAutoCreationCard } from '@/settings/accounts/components/SettingsAccountsMessageAutoCreationCard';
|
||||
import { SettingsAccountsMessageFolderCard } from '@/settings/accounts/components/SettingsAccountsMessageFolderCard';
|
||||
import { SettingsAccountsMessageVisibilityCard } from '@/settings/accounts/components/SettingsAccountsMessageVisibilityCard';
|
||||
@@ -40,58 +43,49 @@ const StyledDetailsContainer = styled.div`
|
||||
export const SettingsAccountsMessageChannelDetails = ({
|
||||
messageChannel,
|
||||
}: SettingsAccountsMessageChannelDetailsProps) => {
|
||||
const featureFlagsMap = useFeatureFlagsMap();
|
||||
const isMigrated =
|
||||
featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false;
|
||||
|
||||
const { updateOneRecord } = useUpdateOneRecord();
|
||||
const [updateMetadataChannel] = useMutation(UPDATE_MESSAGE_CHANNEL);
|
||||
|
||||
const updateChannel = (update: Record<string, unknown>) => {
|
||||
if (isMigrated) {
|
||||
updateMetadataChannel({
|
||||
variables: { input: { id: messageChannel.id, update } },
|
||||
});
|
||||
} else {
|
||||
updateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||
idToUpdate: messageChannel.id,
|
||||
updateOneRecordInput: update,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleVisibilityChange = (value: MessageChannelVisibility) => {
|
||||
updateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||
idToUpdate: messageChannel.id,
|
||||
updateOneRecordInput: {
|
||||
visibility: value,
|
||||
},
|
||||
});
|
||||
updateChannel({ visibility: value });
|
||||
};
|
||||
|
||||
const handleContactAutoCreationChange = (
|
||||
value: MessageChannelContactAutoCreationPolicy,
|
||||
) => {
|
||||
updateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||
idToUpdate: messageChannel.id,
|
||||
updateOneRecordInput: {
|
||||
contactAutoCreationPolicy: value,
|
||||
},
|
||||
});
|
||||
updateChannel({ contactAutoCreationPolicy: value });
|
||||
};
|
||||
|
||||
const handleIsGroupEmailExcludedToggle = (value: boolean) => {
|
||||
updateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||
idToUpdate: messageChannel.id,
|
||||
updateOneRecordInput: {
|
||||
excludeGroupEmails: value,
|
||||
},
|
||||
});
|
||||
updateChannel({ excludeGroupEmails: value });
|
||||
};
|
||||
|
||||
const handleIsNonProfessionalEmailExcludedToggle = (value: boolean) => {
|
||||
updateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||
idToUpdate: messageChannel.id,
|
||||
updateOneRecordInput: {
|
||||
excludeNonProfessionalEmails: value,
|
||||
},
|
||||
});
|
||||
updateChannel({ excludeNonProfessionalEmails: value });
|
||||
};
|
||||
|
||||
const handleMessageFolderImportPolicyChange = (
|
||||
value: MessageFolderImportPolicy,
|
||||
) => {
|
||||
updateOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||
idToUpdate: messageChannel.id,
|
||||
updateOneRecordInput: { messageFolderImportPolicy: value },
|
||||
});
|
||||
updateChannel({ messageFolderImportPolicy: value });
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
+7
-49
@@ -1,23 +1,15 @@
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
||||
import {
|
||||
type MessageChannel,
|
||||
MessageChannelSyncStage,
|
||||
} from '@/accounts/types/MessageChannel';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { SettingsAccountsMessageChannelDetails } from '@/settings/accounts/components/SettingsAccountsMessageChannelDetails';
|
||||
import { SettingsAccountsSelectedMessageChannelEffect } from '@/settings/accounts/components/SettingsAccountsSelectedMessageChannelEffect';
|
||||
import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection';
|
||||
import { SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountMessageChannelsTabListComponentId';
|
||||
import { useMyMessageChannels } from '@/settings/accounts/hooks/useMyMessageChannels';
|
||||
import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { TabList } from '@/ui/layout/tab-list/components/TabList';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import React, { useCallback } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
@@ -31,48 +23,11 @@ export const SettingsAccountsMessageChannelsContainer = () => {
|
||||
activeTabIdComponentState,
|
||||
SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID,
|
||||
);
|
||||
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
||||
const setSettingsAccountsSelectedMessageChannel = useSetAtomState(
|
||||
settingsAccountsSelectedMessageChannelState,
|
||||
);
|
||||
|
||||
const { records: accounts } = useFindManyRecords<ConnectedAccount>({
|
||||
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
|
||||
filter: {
|
||||
accountOwnerId: {
|
||||
eq: currentWorkspaceMember?.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
const { records: messageChannels } = useFindManyRecords<
|
||||
MessageChannel & {
|
||||
connectedAccount: ConnectedAccount;
|
||||
}
|
||||
>({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||
filter: {
|
||||
connectedAccountId: {
|
||||
in: accounts.map((account) => account.id),
|
||||
},
|
||||
isSyncEnabled: {
|
||||
eq: true,
|
||||
},
|
||||
syncStage: {
|
||||
neq: MessageChannelSyncStage.PENDING_CONFIGURATION,
|
||||
},
|
||||
},
|
||||
recordGqlFields,
|
||||
onCompleted: (data) => {
|
||||
setSettingsAccountsSelectedMessageChannel(data[0]);
|
||||
},
|
||||
skip: !accounts.length,
|
||||
});
|
||||
const { channels: messageChannels } = useMyMessageChannels();
|
||||
|
||||
const tabs = messageChannels.map((messageChannel) => ({
|
||||
id: messageChannel.id,
|
||||
@@ -97,6 +52,9 @@ export const SettingsAccountsMessageChannelsContainer = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<SettingsAccountsSelectedMessageChannelEffect
|
||||
messageChannels={messageChannels}
|
||||
/>
|
||||
{tabs.length > 1 && (
|
||||
<StyledMessageContainer>
|
||||
<TabList
|
||||
|
||||
+22
-1
@@ -1,12 +1,16 @@
|
||||
import { useApolloClient, useMutation } from '@apollo/client/react';
|
||||
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
||||
import { CalendarChannelSyncStage } from '@/accounts/types/CalendarChannel';
|
||||
import { MessageChannelSyncStage } from '@/accounts/types/MessageChannel';
|
||||
import {
|
||||
CoreObjectNameSingular,
|
||||
ConnectedAccountProvider,
|
||||
FeatureFlagKey,
|
||||
SettingsPath,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { useDestroyOneRecord } from '@/object-record/hooks/useDestroyOneRecord';
|
||||
|
||||
import { useTriggerProviderReconnect } from '@/settings/accounts/hooks/useTriggerProviderReconnect';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
@@ -27,6 +31,8 @@ import {
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import { DELETE_CONNECTED_ACCOUNT } from '../graphql/mutations/deleteConnectedAccount';
|
||||
|
||||
type SettingsAccountsRowDropdownMenuProps = {
|
||||
account: ConnectedAccount;
|
||||
@@ -45,9 +51,17 @@ export const SettingsAccountsRowDropdownMenu = ({
|
||||
const navigate = useNavigateSettings();
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const featureFlagsMap = useFeatureFlagsMap();
|
||||
const isMigrated =
|
||||
featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false;
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
const { destroyOneRecord } = useDestroyOneRecord({
|
||||
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
|
||||
});
|
||||
const [deleteConnectedAccountMutation] = useMutation(
|
||||
DELETE_CONNECTED_ACCOUNT,
|
||||
);
|
||||
const { triggerProviderReconnect } = useTriggerProviderReconnect();
|
||||
|
||||
const hasPendingConfiguration =
|
||||
@@ -61,7 +75,14 @@ export const SettingsAccountsRowDropdownMenu = ({
|
||||
);
|
||||
|
||||
const deleteAccount = async () => {
|
||||
await destroyOneRecord(account.id);
|
||||
if (isMigrated) {
|
||||
await deleteConnectedAccountMutation({
|
||||
variables: { id: account.id },
|
||||
});
|
||||
await apolloClient.refetchQueries({ include: 'active' });
|
||||
} else {
|
||||
await destroyOneRecord(account.id);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
import { type MessageChannel } from '@/accounts/types/MessageChannel';
|
||||
import { SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountMessageChannelsTabListComponentId';
|
||||
import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState';
|
||||
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
|
||||
import { useAtomComponentStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomComponentStateValue';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
type SettingsAccountsSelectedMessageChannelEffectProps = {
|
||||
messageChannels: MessageChannel[];
|
||||
};
|
||||
|
||||
export const SettingsAccountsSelectedMessageChannelEffect = ({
|
||||
messageChannels,
|
||||
}: SettingsAccountsSelectedMessageChannelEffectProps) => {
|
||||
const activeTabId = useAtomComponentStateValue(
|
||||
activeTabIdComponentState,
|
||||
SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID,
|
||||
);
|
||||
|
||||
const setSettingsAccountsSelectedMessageChannel = useSetAtomState(
|
||||
settingsAccountsSelectedMessageChannelState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (messageChannels.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentSelectionStillExists = activeTabId
|
||||
? messageChannels.some((channel) => channel.id === activeTabId)
|
||||
: false;
|
||||
|
||||
if (!currentSelectionStillExists) {
|
||||
setSettingsAccountsSelectedMessageChannel(messageChannels[0]);
|
||||
}
|
||||
}, [messageChannels, activeTabId, setSettingsAccountsSelectedMessageChannel]);
|
||||
|
||||
return null;
|
||||
};
|
||||
+4
-17
@@ -1,13 +1,10 @@
|
||||
import { type MessageChannel } from '@/accounts/types/MessageChannel';
|
||||
import { type MessageFolder } from '@/accounts/types/MessageFolder';
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { SettingsMessageFoldersEmptyStateCard } from '@/settings/accounts/components/message-folders/SettingsMessageFoldersEmptyStateCard';
|
||||
import { SettingsMessageFoldersSkeletonLoader } from '@/settings/accounts/components/message-folders/SettingsMessageFoldersSkeletonLoader';
|
||||
import { SettingsMessageFoldersTreeItem } from '@/settings/accounts/components/message-folders/SettingsMessageFoldersTreeItem';
|
||||
import { computeFolderIdsForSyncToggle } from '@/settings/accounts/components/message-folders/utils/computeFolderIdsForSyncToggle';
|
||||
import { computeMessageFolderTree } from '@/settings/accounts/components/message-folders/utils/computeMessageFolderTree';
|
||||
import { useMyMessageFolders } from '@/settings/accounts/hooks/useMyMessageFolders';
|
||||
import { useUpdateMessageFoldersSyncStatus } from '@/settings/accounts/hooks/useUpdateMessageFoldersSyncStatus';
|
||||
import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
@@ -75,19 +72,9 @@ export const SettingsAccountsMessageFoldersCard = () => {
|
||||
const { updateMessageFoldersSyncStatus } =
|
||||
useUpdateMessageFoldersSyncStatus();
|
||||
|
||||
const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||
depth: 1,
|
||||
shouldOnlyLoadRelationIdentifiers: false,
|
||||
});
|
||||
|
||||
const { record: messageChannel, loading } = useFindOneRecord<MessageChannel>({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||
objectRecordId: settingsAccountsSelectedMessageChannel?.id,
|
||||
recordGqlFields,
|
||||
});
|
||||
|
||||
const { messageFolders = [] } = messageChannel ?? {};
|
||||
const { messageFolders, loading } = useMyMessageFolders(
|
||||
settingsAccountsSelectedMessageChannel?.id,
|
||||
);
|
||||
|
||||
const filteredMessageFolders = useMemo(() => {
|
||||
return messageFolders.filter((folder) =>
|
||||
|
||||
+21
@@ -146,4 +146,25 @@ describe('computeMessageFolderTree', () => {
|
||||
expect(result[0].children[0].folder.name).toBe('Clients');
|
||||
expect(result[0].children[1].folder.name).toBe('Projects');
|
||||
});
|
||||
|
||||
it('should resolve parent-child when parentFolderId references parent id instead of externalId', () => {
|
||||
const parent = createFolder(
|
||||
'20202020-aaaa-bbbb-cccc-000000000001',
|
||||
'custom folder',
|
||||
null,
|
||||
'Label_5900090362003645629',
|
||||
);
|
||||
const child = createFolder(
|
||||
'20202020-aaaa-bbbb-cccc-000000000002',
|
||||
'child folder',
|
||||
'20202020-aaaa-bbbb-cccc-000000000001',
|
||||
'Label_7713410187110265162',
|
||||
);
|
||||
const result = computeMessageFolderTree([parent, child]);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].folder.name).toBe('custom folder');
|
||||
expect(result[0].children).toHaveLength(1);
|
||||
expect(result[0].children[0].folder.name).toBe('child folder');
|
||||
});
|
||||
});
|
||||
|
||||
+10
-4
@@ -16,9 +16,11 @@ export const computeFolderIdsForSyncToggle = ({
|
||||
|
||||
const collectChildren = (id: string): string[] => {
|
||||
const folder = folderById.get(id);
|
||||
const children = folder?.externalId
|
||||
const children = folder
|
||||
? allFolders.filter(
|
||||
(childFolder) => childFolder.parentFolderId === folder.externalId,
|
||||
(childFolder) =>
|
||||
childFolder.parentFolderId === folder.externalId ||
|
||||
childFolder.parentFolderId === folder.id,
|
||||
)
|
||||
: [];
|
||||
|
||||
@@ -38,7 +40,9 @@ export const computeFolderIdsForSyncToggle = ({
|
||||
break;
|
||||
}
|
||||
|
||||
const parent = folderByExternalId.get(current.parentFolderId);
|
||||
const parent =
|
||||
folderByExternalId.get(current.parentFolderId) ??
|
||||
folderById.get(current.parentFolderId);
|
||||
|
||||
if (!parent) {
|
||||
break;
|
||||
@@ -63,7 +67,9 @@ export const computeFolderIdsForSyncToggle = ({
|
||||
|
||||
for (const parent of collectParents(folderId)) {
|
||||
const children = allFolders.filter(
|
||||
(folder) => folder.parentFolderId === parent.externalId,
|
||||
(folder) =>
|
||||
folder.parentFolderId === parent.externalId ||
|
||||
folder.parentFolderId === parent.id,
|
||||
);
|
||||
const hasOtherSyncedChild = children.some(
|
||||
(child) => child.isSynced && !idsToUnsync.has(child.id),
|
||||
|
||||
+10
-2
@@ -11,17 +11,22 @@ export const computeMessageFolderTree = (
|
||||
folders: MessageFolder[],
|
||||
): MessageFolderTreeNode[] => {
|
||||
const folderByExternalIdMap = new Map<string, MessageFolder>();
|
||||
const folderByIdMap = new Map<string, MessageFolder>();
|
||||
const childrenMap = new Map<string, MessageFolder[]>();
|
||||
|
||||
folders.forEach((folder) => {
|
||||
if (isDefined(folder.externalId)) {
|
||||
folderByExternalIdMap.set(folder.externalId, folder);
|
||||
}
|
||||
folderByIdMap.set(folder.id, folder);
|
||||
});
|
||||
|
||||
folders.forEach((folder) => {
|
||||
if (isDefined(folder.parentFolderId)) {
|
||||
const parent = folderByExternalIdMap.get(folder.parentFolderId);
|
||||
const parent =
|
||||
folderByExternalIdMap.get(folder.parentFolderId) ??
|
||||
folderByIdMap.get(folder.parentFolderId);
|
||||
|
||||
if (isDefined(parent)) {
|
||||
const siblings = childrenMap.get(parent.id) || [];
|
||||
siblings.push(folder);
|
||||
@@ -46,7 +51,10 @@ export const computeMessageFolderTree = (
|
||||
const rootFolders = folders.filter((folder) => {
|
||||
if (!folder.parentFolderId) return true;
|
||||
|
||||
return !folderByExternalIdMap.has(folder.parentFolderId);
|
||||
return (
|
||||
!folderByExternalIdMap.has(folder.parentFolderId) &&
|
||||
!folderByIdMap.has(folder.parentFolderId)
|
||||
);
|
||||
});
|
||||
|
||||
rootFolders.sort((a, b) => a.name.localeCompare(b.name));
|
||||
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETE_CONNECTED_ACCOUNT = gql`
|
||||
mutation DeleteConnectedAccount($id: UUID!) {
|
||||
deleteConnectedAccount(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`;
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPDATE_CALENDAR_CHANNEL = gql`
|
||||
mutation UpdateCalendarChannel($input: UpdateCalendarChannelInput!) {
|
||||
updateCalendarChannel(input: $input) {
|
||||
id
|
||||
visibility
|
||||
isContactAutoCreationEnabled
|
||||
contactAutoCreationPolicy
|
||||
}
|
||||
}
|
||||
`;
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPDATE_MESSAGE_CHANNEL = gql`
|
||||
mutation UpdateMessageChannel($input: UpdateMessageChannelInput!) {
|
||||
updateMessageChannel(input: $input) {
|
||||
id
|
||||
visibility
|
||||
contactAutoCreationPolicy
|
||||
excludeNonProfessionalEmails
|
||||
excludeGroupEmails
|
||||
messageFolderImportPolicy
|
||||
}
|
||||
}
|
||||
`;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPDATE_MESSAGE_FOLDER = gql`
|
||||
mutation UpdateMessageFolder($input: UpdateMessageFolderInput!) {
|
||||
updateMessageFolder(input: $input) {
|
||||
id
|
||||
isSynced
|
||||
}
|
||||
}
|
||||
`;
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const UPDATE_MESSAGE_FOLDERS = gql`
|
||||
mutation UpdateMessageFolders($input: UpdateMessageFoldersInput!) {
|
||||
updateMessageFolders(input: $input) {
|
||||
id
|
||||
isSynced
|
||||
}
|
||||
}
|
||||
`;
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_MY_CALENDAR_CHANNELS = gql`
|
||||
query MyCalendarChannels($connectedAccountId: UUID) {
|
||||
myCalendarChannels(connectedAccountId: $connectedAccountId) {
|
||||
id
|
||||
handle
|
||||
visibility
|
||||
syncStatus
|
||||
syncStage
|
||||
syncStageStartedAt
|
||||
isContactAutoCreationEnabled
|
||||
contactAutoCreationPolicy
|
||||
isSyncEnabled
|
||||
connectedAccountId
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_MY_CONNECTED_ACCOUNTS = gql`
|
||||
query MyConnectedAccounts {
|
||||
myConnectedAccounts {
|
||||
id
|
||||
handle
|
||||
provider
|
||||
authFailedAt
|
||||
scopes
|
||||
handleAliases
|
||||
lastSignedInAt
|
||||
userWorkspaceId
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_MY_MESSAGE_CHANNELS = gql`
|
||||
query MyMessageChannels($connectedAccountId: UUID) {
|
||||
myMessageChannels(connectedAccountId: $connectedAccountId) {
|
||||
id
|
||||
handle
|
||||
visibility
|
||||
type
|
||||
isContactAutoCreationEnabled
|
||||
contactAutoCreationPolicy
|
||||
messageFolderImportPolicy
|
||||
excludeNonProfessionalEmails
|
||||
excludeGroupEmails
|
||||
isSyncEnabled
|
||||
syncStatus
|
||||
syncStage
|
||||
syncStageStartedAt
|
||||
connectedAccountId
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_MY_MESSAGE_FOLDERS = gql`
|
||||
query MyMessageFolders($messageChannelId: UUID) {
|
||||
myMessageFolders(messageChannelId: $messageChannelId) {
|
||||
id
|
||||
name
|
||||
isSynced
|
||||
isSentFolder
|
||||
parentFolderId
|
||||
externalId
|
||||
messageChannelId
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
@@ -0,0 +1,111 @@
|
||||
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
||||
import {
|
||||
type CalendarChannel,
|
||||
CalendarChannelSyncStage,
|
||||
} from '@/accounts/types/CalendarChannel';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { GET_MY_CALENDAR_CHANNELS } from '@/settings/accounts/graphql/queries/getMyCalendarChannels';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import { useApolloClient, useQuery } from '@apollo/client/react';
|
||||
import { useMemo } from 'react';
|
||||
import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
type MetadataCalendarChannel = {
|
||||
id: string;
|
||||
handle: string;
|
||||
visibility: string;
|
||||
syncStatus: string;
|
||||
syncStage: string;
|
||||
syncStageStartedAt: string | null;
|
||||
isContactAutoCreationEnabled: boolean;
|
||||
contactAutoCreationPolicy: string;
|
||||
isSyncEnabled: boolean;
|
||||
connectedAccountId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export const useMyCalendarChannels = () => {
|
||||
const featureFlagsMap = useFeatureFlagsMap();
|
||||
const isMigrated =
|
||||
featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false;
|
||||
|
||||
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { records: workspaceAccounts } = useFindManyRecords<ConnectedAccount>({
|
||||
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
|
||||
filter: {
|
||||
accountOwnerId: {
|
||||
eq: currentWorkspaceMember?.id,
|
||||
},
|
||||
},
|
||||
skip: isMigrated,
|
||||
});
|
||||
|
||||
const { records: workspaceChannels, loading: workspaceLoading } =
|
||||
useFindManyRecords<
|
||||
CalendarChannel & { connectedAccount: ConnectedAccount }
|
||||
>({
|
||||
objectNameSingular: CoreObjectNameSingular.CalendarChannel,
|
||||
filter: {
|
||||
connectedAccountId: {
|
||||
in: workspaceAccounts.map((account) => account.id),
|
||||
},
|
||||
syncStage: {
|
||||
neq: CalendarChannelSyncStage.PENDING_CONFIGURATION,
|
||||
},
|
||||
},
|
||||
skip: isMigrated || !workspaceAccounts.length,
|
||||
});
|
||||
|
||||
const { data: metadataData, loading: metadataLoading } = useQuery<{
|
||||
myCalendarChannels: MetadataCalendarChannel[];
|
||||
}>(GET_MY_CALENDAR_CHANNELS, {
|
||||
client: apolloClient,
|
||||
skip: !isMigrated,
|
||||
});
|
||||
|
||||
const channels = useMemo(() => {
|
||||
if (!isMigrated) {
|
||||
return workspaceChannels;
|
||||
}
|
||||
|
||||
if (!metadataData?.myCalendarChannels) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return metadataData.myCalendarChannels
|
||||
.filter(
|
||||
(channel: MetadataCalendarChannel) =>
|
||||
channel.syncStage !== 'PENDING_CONFIGURATION',
|
||||
)
|
||||
.map(
|
||||
(channel: MetadataCalendarChannel) =>
|
||||
({
|
||||
id: channel.id,
|
||||
handle: channel.handle,
|
||||
visibility: channel.visibility,
|
||||
isContactAutoCreationEnabled: channel.isContactAutoCreationEnabled,
|
||||
contactAutoCreationPolicy: channel.contactAutoCreationPolicy,
|
||||
isSyncEnabled: channel.isSyncEnabled,
|
||||
syncStatus: channel.syncStatus,
|
||||
syncStage: channel.syncStage,
|
||||
syncCursor: '',
|
||||
syncStageStartedAt: channel.syncStageStartedAt
|
||||
? new Date(channel.syncStageStartedAt)
|
||||
: null,
|
||||
throttleFailureCount: 0,
|
||||
connectedAccountId: channel.connectedAccountId,
|
||||
__typename: 'CalendarChannel',
|
||||
}) as CalendarChannel,
|
||||
);
|
||||
}, [isMigrated, workspaceChannels, metadataData]);
|
||||
|
||||
return {
|
||||
channels,
|
||||
loading: isMigrated ? metadataLoading : workspaceLoading,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,116 @@
|
||||
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
|
||||
import { GET_MY_CONNECTED_ACCOUNTS } from '@/settings/accounts/graphql/queries/getMyConnectedAccounts';
|
||||
import { useMyCalendarChannels } from '@/settings/accounts/hooks/useMyCalendarChannels';
|
||||
import { useMyMessageChannels } from '@/settings/accounts/hooks/useMyMessageChannels';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import { useApolloClient, useQuery } from '@apollo/client/react';
|
||||
import { useMemo } from 'react';
|
||||
import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
type MetadataConnectedAccount = {
|
||||
id: string;
|
||||
handle: string;
|
||||
provider: string;
|
||||
authFailedAt: string | null;
|
||||
scopes: string[] | null;
|
||||
handleAliases: string[] | null;
|
||||
lastSignedInAt: string | null;
|
||||
userWorkspaceId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export const useMyConnectedAccounts = () => {
|
||||
const featureFlagsMap = useFeatureFlagsMap();
|
||||
const isMigrated =
|
||||
featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false;
|
||||
|
||||
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({
|
||||
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
|
||||
depth: 1,
|
||||
shouldOnlyLoadRelationIdentifiers: false,
|
||||
});
|
||||
|
||||
const { records: workspaceAccounts, loading: workspaceLoading } =
|
||||
useFindManyRecords<ConnectedAccount>({
|
||||
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
|
||||
filter: {
|
||||
accountOwnerId: {
|
||||
eq: currentWorkspaceMember?.id,
|
||||
},
|
||||
},
|
||||
recordGqlFields,
|
||||
skip: isMigrated,
|
||||
});
|
||||
|
||||
const { data: metadataData, loading: metadataLoading } = useQuery<{
|
||||
myConnectedAccounts: MetadataConnectedAccount[];
|
||||
}>(GET_MY_CONNECTED_ACCOUNTS, {
|
||||
client: apolloClient,
|
||||
skip: !isMigrated,
|
||||
});
|
||||
|
||||
const { channels: messageChannels, loading: messageChannelsLoading } =
|
||||
useMyMessageChannels();
|
||||
const { channels: calendarChannels, loading: calendarChannelsLoading } =
|
||||
useMyCalendarChannels();
|
||||
|
||||
const accounts = useMemo<ConnectedAccount[]>(() => {
|
||||
if (!isMigrated) {
|
||||
return workspaceAccounts;
|
||||
}
|
||||
|
||||
if (!metadataData?.myConnectedAccounts) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return metadataData.myConnectedAccounts.map(
|
||||
(account: MetadataConnectedAccount) =>
|
||||
({
|
||||
id: account.id,
|
||||
handle: account.handle,
|
||||
provider: account.provider,
|
||||
accessToken: '',
|
||||
refreshToken: '',
|
||||
accountOwnerId: account.userWorkspaceId,
|
||||
lastSyncHistoryId: '',
|
||||
authFailedAt: account.authFailedAt
|
||||
? new Date(account.authFailedAt)
|
||||
: null,
|
||||
messageChannels: messageChannels.filter(
|
||||
(channel) =>
|
||||
(channel as unknown as { connectedAccountId: string })
|
||||
.connectedAccountId === account.id,
|
||||
),
|
||||
calendarChannels: calendarChannels.filter(
|
||||
(channel) =>
|
||||
(channel as unknown as { connectedAccountId: string })
|
||||
.connectedAccountId === account.id,
|
||||
),
|
||||
scopes: account.scopes,
|
||||
__typename: 'ConnectedAccount',
|
||||
}) as ConnectedAccount,
|
||||
);
|
||||
}, [
|
||||
isMigrated,
|
||||
workspaceAccounts,
|
||||
metadataData,
|
||||
messageChannels,
|
||||
calendarChannels,
|
||||
]);
|
||||
|
||||
return {
|
||||
accounts,
|
||||
loading: isMigrated
|
||||
? metadataLoading || messageChannelsLoading || calendarChannelsLoading
|
||||
: workspaceLoading,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,127 @@
|
||||
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
||||
import {
|
||||
type MessageChannel,
|
||||
MessageChannelSyncStage,
|
||||
} from '@/accounts/types/MessageChannel';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
|
||||
import { GET_MY_MESSAGE_CHANNELS } from '@/settings/accounts/graphql/queries/getMyMessageChannels';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import { useApolloClient, useQuery } from '@apollo/client/react';
|
||||
import { useMemo } from 'react';
|
||||
import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
type MetadataMessageChannel = {
|
||||
id: string;
|
||||
handle: string;
|
||||
visibility: string;
|
||||
type: string;
|
||||
isContactAutoCreationEnabled: boolean;
|
||||
contactAutoCreationPolicy: string;
|
||||
messageFolderImportPolicy: string;
|
||||
excludeNonProfessionalEmails: boolean;
|
||||
excludeGroupEmails: boolean;
|
||||
isSyncEnabled: boolean;
|
||||
syncStatus: string;
|
||||
syncStage: string;
|
||||
syncStageStartedAt: string | null;
|
||||
connectedAccountId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export const useMyMessageChannels = () => {
|
||||
const featureFlagsMap = useFeatureFlagsMap();
|
||||
const isMigrated =
|
||||
featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false;
|
||||
|
||||
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { records: workspaceAccounts } = useFindManyRecords<ConnectedAccount>({
|
||||
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
|
||||
filter: {
|
||||
accountOwnerId: {
|
||||
eq: currentWorkspaceMember?.id,
|
||||
},
|
||||
},
|
||||
skip: isMigrated,
|
||||
});
|
||||
|
||||
const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||
depth: 1,
|
||||
});
|
||||
|
||||
const { records: workspaceChannels, loading: workspaceLoading } =
|
||||
useFindManyRecords<MessageChannel & { connectedAccount: ConnectedAccount }>(
|
||||
{
|
||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||
filter: {
|
||||
connectedAccountId: {
|
||||
in: workspaceAccounts.map((account) => account.id),
|
||||
},
|
||||
isSyncEnabled: { eq: true },
|
||||
syncStage: {
|
||||
neq: MessageChannelSyncStage.PENDING_CONFIGURATION,
|
||||
},
|
||||
},
|
||||
recordGqlFields,
|
||||
skip: isMigrated || !workspaceAccounts.length,
|
||||
},
|
||||
);
|
||||
|
||||
const { data: metadataData, loading: metadataLoading } = useQuery<{
|
||||
myMessageChannels: MetadataMessageChannel[];
|
||||
}>(GET_MY_MESSAGE_CHANNELS, {
|
||||
client: apolloClient,
|
||||
skip: !isMigrated,
|
||||
});
|
||||
|
||||
const channels = useMemo(() => {
|
||||
if (!isMigrated) {
|
||||
return workspaceChannels;
|
||||
}
|
||||
|
||||
if (!metadataData?.myMessageChannels) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return metadataData.myMessageChannels
|
||||
.filter(
|
||||
(channel: MetadataMessageChannel) =>
|
||||
channel.isSyncEnabled &&
|
||||
channel.syncStage !== 'PENDING_CONFIGURATION',
|
||||
)
|
||||
.map(
|
||||
(channel: MetadataMessageChannel) =>
|
||||
({
|
||||
id: channel.id,
|
||||
handle: channel.handle,
|
||||
visibility: channel.visibility,
|
||||
contactAutoCreationPolicy: channel.contactAutoCreationPolicy,
|
||||
excludeNonProfessionalEmails: channel.excludeNonProfessionalEmails,
|
||||
excludeGroupEmails: channel.excludeGroupEmails,
|
||||
isSyncEnabled: channel.isSyncEnabled,
|
||||
messageFolders: [],
|
||||
messageFolderImportPolicy: channel.messageFolderImportPolicy,
|
||||
syncStatus: channel.syncStatus,
|
||||
syncStage: channel.syncStage,
|
||||
syncCursor: '',
|
||||
syncStageStartedAt: channel.syncStageStartedAt
|
||||
? new Date(channel.syncStageStartedAt)
|
||||
: null,
|
||||
throttleFailureCount: 0,
|
||||
connectedAccountId: channel.connectedAccountId,
|
||||
__typename: 'MessageChannel',
|
||||
}) as MessageChannel,
|
||||
);
|
||||
}, [isMigrated, workspaceChannels, metadataData]);
|
||||
|
||||
return {
|
||||
channels,
|
||||
loading: isMigrated ? metadataLoading : workspaceLoading,
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,80 @@
|
||||
import { type MessageChannel } from '@/accounts/types/MessageChannel';
|
||||
import { type MessageFolder } from '@/accounts/types/MessageFolder';
|
||||
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
|
||||
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
|
||||
import { GET_MY_MESSAGE_FOLDERS } from '@/settings/accounts/graphql/queries/getMyMessageFolders';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import { useApolloClient, useQuery } from '@apollo/client/react';
|
||||
import { useMemo } from 'react';
|
||||
import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
type MetadataMessageFolder = {
|
||||
id: string;
|
||||
name: string | null;
|
||||
isSynced: boolean;
|
||||
isSentFolder: boolean;
|
||||
parentFolderId: string | null;
|
||||
externalId: string | null;
|
||||
messageChannelId: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export const useMyMessageFolders = (messageChannelId?: string) => {
|
||||
const featureFlagsMap = useFeatureFlagsMap();
|
||||
const isMigrated =
|
||||
featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false;
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||
depth: 1,
|
||||
shouldOnlyLoadRelationIdentifiers: false,
|
||||
});
|
||||
|
||||
const { record: messageChannel, loading: workspaceLoading } =
|
||||
useFindOneRecord<MessageChannel>({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||
objectRecordId: messageChannelId,
|
||||
recordGqlFields,
|
||||
skip: isMigrated || !messageChannelId,
|
||||
});
|
||||
|
||||
const { data: metadataData, loading: metadataLoading } = useQuery<{
|
||||
myMessageFolders: MetadataMessageFolder[];
|
||||
}>(GET_MY_MESSAGE_FOLDERS, {
|
||||
client: apolloClient,
|
||||
variables: messageChannelId ? { messageChannelId } : undefined,
|
||||
skip: !isMigrated,
|
||||
});
|
||||
|
||||
const messageFolders = useMemo<MessageFolder[]>(() => {
|
||||
if (!isMigrated) {
|
||||
return messageChannel?.messageFolders ?? [];
|
||||
}
|
||||
|
||||
if (!metadataData?.myMessageFolders) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return metadataData.myMessageFolders.map(
|
||||
(folder: MetadataMessageFolder) => ({
|
||||
id: folder.id,
|
||||
name: folder.name ?? '',
|
||||
syncCursor: '',
|
||||
isSynced: folder.isSynced,
|
||||
isSentFolder: folder.isSentFolder,
|
||||
parentFolderId: folder.parentFolderId,
|
||||
messageChannelId: folder.messageChannelId,
|
||||
externalId: folder.externalId,
|
||||
__typename: 'MessageFolder' as const,
|
||||
}),
|
||||
);
|
||||
}, [isMigrated, messageChannel, metadataData]);
|
||||
|
||||
return {
|
||||
messageFolders,
|
||||
loading: isMigrated ? metadataLoading : workspaceLoading,
|
||||
};
|
||||
};
|
||||
+36
-2
@@ -1,7 +1,10 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { CoreObjectNameSingular } from 'twenty-shared/types';
|
||||
import { useUpdateManyRecords } from '@/object-record/hooks/useUpdateManyRecords';
|
||||
import { UPDATE_MESSAGE_FOLDERS } from '@/settings/accounts/graphql/mutations/updateMessageFolders';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import { useApolloClient } from '@apollo/client/react';
|
||||
import { CoreObjectNameSingular, FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
type UpdateMessageFoldersSyncStatusArgs = {
|
||||
messageFolderIds: string[];
|
||||
@@ -9,6 +12,12 @@ type UpdateMessageFoldersSyncStatusArgs = {
|
||||
};
|
||||
|
||||
export const useUpdateMessageFoldersSyncStatus = () => {
|
||||
const featureFlagsMap = useFeatureFlagsMap();
|
||||
const isMigrated =
|
||||
featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false;
|
||||
|
||||
const apolloClient = useApolloClient();
|
||||
|
||||
const { updateManyRecords } = useUpdateManyRecords({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageFolder,
|
||||
recordGqlFields: {
|
||||
@@ -22,12 +31,37 @@ export const useUpdateMessageFoldersSyncStatus = () => {
|
||||
messageFolderIds,
|
||||
isSynced,
|
||||
}: UpdateMessageFoldersSyncStatusArgs) => {
|
||||
if (isMigrated) {
|
||||
if (messageFolderIds.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await apolloClient.mutate({
|
||||
mutation: UPDATE_MESSAGE_FOLDERS,
|
||||
variables: {
|
||||
input: {
|
||||
ids: messageFolderIds,
|
||||
update: { isSynced },
|
||||
},
|
||||
},
|
||||
optimisticResponse: {
|
||||
updateMessageFolders: messageFolderIds.map((id) => ({
|
||||
__typename: 'MessageFolder',
|
||||
id,
|
||||
isSynced,
|
||||
})),
|
||||
},
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await updateManyRecords({
|
||||
recordIdsToUpdate: messageFolderIds,
|
||||
updateOneRecordInput: { isSynced },
|
||||
});
|
||||
},
|
||||
[updateManyRecords],
|
||||
[isMigrated, apolloClient, updateManyRecords],
|
||||
);
|
||||
|
||||
return { updateMessageFoldersSyncStatus };
|
||||
|
||||
@@ -1,40 +1,20 @@
|
||||
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { CoreObjectNameSingular, SettingsPath } from 'twenty-shared/types';
|
||||
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { SettingsAccountLoader } from '@/settings/accounts/components/SettingsAccountLoader';
|
||||
import { SettingsAccountsBlocklistSection } from '@/settings/accounts/components/SettingsAccountsBlocklistSection';
|
||||
import { SettingsAccountsConnectedAccountsListCard } from '@/settings/accounts/components/SettingsAccountsConnectedAccountsListCard';
|
||||
import { SettingsAccountsSettingsSection } from '@/settings/accounts/components/SettingsAccountsSettingsSection';
|
||||
import { useMyConnectedAccounts } from '@/settings/accounts/hooks/useMyConnectedAccounts';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
|
||||
export const SettingsAccounts = () => {
|
||||
const { t } = useLingui();
|
||||
const currentWorkspaceMember = useAtomStateValue(currentWorkspaceMemberState);
|
||||
|
||||
const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({
|
||||
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
|
||||
depth: 1,
|
||||
shouldOnlyLoadRelationIdentifiers: false,
|
||||
});
|
||||
|
||||
const { records: allAccounts, loading } =
|
||||
useFindManyRecords<ConnectedAccount>({
|
||||
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
|
||||
filter: {
|
||||
accountOwnerId: {
|
||||
eq: currentWorkspaceMember?.id,
|
||||
},
|
||||
},
|
||||
recordGqlFields,
|
||||
});
|
||||
const { accounts: allAccounts, loading } = useMyConnectedAccounts();
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
|
||||
+73
-30
@@ -5,14 +5,22 @@ import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomStat
|
||||
|
||||
import { type CalendarChannel } from '@/accounts/types/CalendarChannel';
|
||||
import { type MessageChannel } from '@/accounts/types/MessageChannel';
|
||||
import { CoreObjectNameSingular, SettingsPath } from 'twenty-shared/types';
|
||||
import {
|
||||
CoreObjectNameSingular,
|
||||
FeatureFlagKey,
|
||||
SettingsPath,
|
||||
} from 'twenty-shared/types';
|
||||
import { useGenerateDepthRecordGqlFieldsFromObject } from '@/object-record/graphql/record-gql-fields/hooks/useGenerateDepthRecordGqlFieldsFromObject';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { GET_MY_CALENDAR_CHANNELS } from '@/settings/accounts/graphql/queries/getMyCalendarChannels';
|
||||
import { GET_MY_MESSAGE_CHANNELS } from '@/settings/accounts/graphql/queries/getMyMessageChannels';
|
||||
import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { useMutation } from '@apollo/client/react';
|
||||
import { StartChannelSyncDocument } from '~/generated-metadata/graphql';
|
||||
import { SettingsAccountsConfigurationSelectedMessageChannelEffect } from '~/pages/settings/accounts/SettingsAccountsConfigurationSelectedMessageChannelEffect';
|
||||
import { SettingsAccountsConfigurationStepCalendar } from '~/pages/settings/accounts/SettingsAccountsConfigurationStepCalendar';
|
||||
import { SettingsAccountsConfigurationStepEmail } from '~/pages/settings/accounts/SettingsAccountsConfigurationStepEmail';
|
||||
|
||||
@@ -40,38 +48,66 @@ export const SettingsAccountsConfiguration = () => {
|
||||
SettingsAccountsConfigurationStep.Email,
|
||||
);
|
||||
|
||||
const featureFlagsMap = useFeatureFlagsMap();
|
||||
const isMigrated =
|
||||
featureFlagsMap[FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED] ?? false;
|
||||
|
||||
const { recordGqlFields } = useGenerateDepthRecordGqlFieldsFromObject({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||
depth: 1,
|
||||
shouldOnlyLoadRelationIdentifiers: false,
|
||||
});
|
||||
|
||||
const { records: messageChannels } = useFindManyRecords<MessageChannel>({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||
filter: {
|
||||
connectedAccountId: {
|
||||
eq: connectedAccountId,
|
||||
const { records: workspaceMessageChannels } =
|
||||
useFindManyRecords<MessageChannel>({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||
filter: {
|
||||
connectedAccountId: {
|
||||
eq: connectedAccountId,
|
||||
},
|
||||
},
|
||||
},
|
||||
recordGqlFields,
|
||||
onCompleted: (data) => {
|
||||
if (isDefined(data[0])) {
|
||||
setSettingsAccountsSelectedMessageChannel(data[0]);
|
||||
}
|
||||
},
|
||||
skip: !connectedAccountId,
|
||||
recordGqlFields,
|
||||
onCompleted: (data) => {
|
||||
if (isDefined(data[0])) {
|
||||
setSettingsAccountsSelectedMessageChannel(data[0]);
|
||||
}
|
||||
},
|
||||
skip: !connectedAccountId || isMigrated,
|
||||
});
|
||||
|
||||
const { records: workspaceCalendarChannels } =
|
||||
useFindManyRecords<CalendarChannel>({
|
||||
objectNameSingular: CoreObjectNameSingular.CalendarChannel,
|
||||
filter: {
|
||||
connectedAccountId: {
|
||||
eq: connectedAccountId,
|
||||
},
|
||||
},
|
||||
skip: !connectedAccountId || isMigrated,
|
||||
});
|
||||
|
||||
const { data: metadataMessageChannelData } = useQuery<{
|
||||
myMessageChannels: MessageChannel[];
|
||||
}>(GET_MY_MESSAGE_CHANNELS, {
|
||||
variables: { connectedAccountId },
|
||||
skip: !isMigrated || !connectedAccountId,
|
||||
});
|
||||
|
||||
const { records: calendarChannels } = useFindManyRecords<CalendarChannel>({
|
||||
objectNameSingular: CoreObjectNameSingular.CalendarChannel,
|
||||
filter: {
|
||||
connectedAccountId: {
|
||||
eq: connectedAccountId,
|
||||
},
|
||||
},
|
||||
skip: !connectedAccountId,
|
||||
const { data: metadataCalendarChannelData } = useQuery<{
|
||||
myCalendarChannels: CalendarChannel[];
|
||||
}>(GET_MY_CALENDAR_CHANNELS, {
|
||||
variables: { connectedAccountId },
|
||||
skip: !isMigrated || !connectedAccountId,
|
||||
});
|
||||
|
||||
const messageChannels = isMigrated
|
||||
? (metadataMessageChannelData?.myMessageChannels ?? [])
|
||||
: workspaceMessageChannels;
|
||||
|
||||
const calendarChannels = isMigrated
|
||||
? (metadataCalendarChannelData?.myCalendarChannels ?? [])
|
||||
: workspaceCalendarChannels;
|
||||
|
||||
const messageChannel = messageChannels[0];
|
||||
const calendarChannel = calendarChannels[0];
|
||||
|
||||
@@ -106,13 +142,20 @@ export const SettingsAccountsConfiguration = () => {
|
||||
|
||||
if (showEmailStep) {
|
||||
return (
|
||||
<SettingsAccountsConfigurationStepEmail
|
||||
messageChannel={messageChannel}
|
||||
hasNextStep={isDefined(calendarChannel)}
|
||||
isSubmitting={isSubmitting}
|
||||
onNext={handleNext}
|
||||
onAddAccount={handleAddAccount}
|
||||
/>
|
||||
<>
|
||||
{isMigrated && (
|
||||
<SettingsAccountsConfigurationSelectedMessageChannelEffect
|
||||
messageChannel={messageChannel as unknown as MessageChannel}
|
||||
/>
|
||||
)}
|
||||
<SettingsAccountsConfigurationStepEmail
|
||||
messageChannel={messageChannel}
|
||||
hasNextStep={isDefined(calendarChannel)}
|
||||
isSubmitting={isSubmitting}
|
||||
onNext={handleNext}
|
||||
onAddAccount={handleAddAccount}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
import { type MessageChannel } from '@/accounts/types/MessageChannel';
|
||||
import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState';
|
||||
import { useSetAtomState } from '@/ui/utilities/state/jotai/hooks/useSetAtomState';
|
||||
import { useEffect } from 'react';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
type SettingsAccountsConfigurationSelectedMessageChannelEffectProps = {
|
||||
messageChannel: MessageChannel | undefined;
|
||||
};
|
||||
|
||||
export const SettingsAccountsConfigurationSelectedMessageChannelEffect = ({
|
||||
messageChannel,
|
||||
}: SettingsAccountsConfigurationSelectedMessageChannelEffectProps) => {
|
||||
const setSettingsAccountsSelectedMessageChannel = useSetAtomState(
|
||||
settingsAccountsSelectedMessageChannelState,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (isDefined(messageChannel)) {
|
||||
setSettingsAccountsSelectedMessageChannel(messageChannel);
|
||||
}
|
||||
}, [messageChannel, setSettingsAccountsSelectedMessageChannel]);
|
||||
|
||||
return null;
|
||||
};
|
||||
@@ -2215,7 +2215,6 @@ type CalendarChannel {
|
||||
isContactAutoCreationEnabled: Boolean!
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy!
|
||||
isSyncEnabled: Boolean!
|
||||
syncCursor: String
|
||||
syncedAt: DateTime
|
||||
syncStageStartedAt: DateTime
|
||||
throttleFailureCount: Float!
|
||||
@@ -2259,15 +2258,11 @@ type ConnectedAccountDTO {
|
||||
id: UUID!
|
||||
handle: String!
|
||||
provider: String!
|
||||
accessToken: String
|
||||
refreshToken: String
|
||||
lastCredentialsRefreshedAt: DateTime
|
||||
authFailedAt: DateTime
|
||||
handleAliases: [String!]
|
||||
scopes: [String!]
|
||||
connectionParameters: JSON
|
||||
lastSignedInAt: DateTime
|
||||
oidcTokenClaims: JSON
|
||||
userWorkspaceId: UUID!
|
||||
createdAt: DateTime!
|
||||
updatedAt: DateTime!
|
||||
@@ -2285,7 +2280,6 @@ type MessageChannel {
|
||||
excludeGroupEmails: Boolean!
|
||||
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction!
|
||||
isSyncEnabled: Boolean!
|
||||
syncCursor: String
|
||||
syncedAt: DateTime
|
||||
syncStatus: MessageChannelSyncStatus!
|
||||
syncStage: MessageChannelSyncStage!
|
||||
@@ -2347,7 +2341,6 @@ enum MessageChannelSyncStage {
|
||||
type MessageFolder {
|
||||
id: UUID!
|
||||
name: String
|
||||
syncCursor: String
|
||||
isSentFolder: Boolean!
|
||||
isSynced: Boolean!
|
||||
parentFolderId: UUID
|
||||
@@ -3169,14 +3162,11 @@ type Query {
|
||||
findWorkspaceFromInviteHash(inviteHash: String!): Workspace!
|
||||
validatePasswordResetToken(passwordResetToken: String!): ValidatePasswordResetToken!
|
||||
getSSOIdentityProviders: [FindAvailableSSOIDP!]!
|
||||
messageFolders(messageChannelId: UUID): [MessageFolder!]!
|
||||
messageFolder(id: UUID!): MessageFolder
|
||||
calendarChannels(connectedAccountId: UUID): [CalendarChannel!]!
|
||||
calendarChannel(id: UUID!): CalendarChannel
|
||||
messageChannels(connectedAccountId: UUID): [MessageChannel!]!
|
||||
messageChannel(id: UUID!): MessageChannel
|
||||
myMessageFolders(messageChannelId: UUID): [MessageFolder!]!
|
||||
myMessageChannels(connectedAccountId: UUID): [MessageChannel!]!
|
||||
myConnectedAccounts: [ConnectedAccountDTO!]!
|
||||
connectedAccounts: [ConnectedAccountDTO!]!
|
||||
connectedAccount(id: UUID!): ConnectedAccountDTO
|
||||
myCalendarChannels(connectedAccountId: UUID): [CalendarChannel!]!
|
||||
webhooks: [Webhook!]!
|
||||
webhook(id: UUID!): Webhook
|
||||
minimalMetadata: MinimalMetadata!
|
||||
@@ -3474,18 +3464,11 @@ type Mutation {
|
||||
createSAMLIdentityProvider(input: SetupSAMLSsoInput!): SetupSso!
|
||||
deleteSSOIdentityProvider(input: DeleteSsoInput!): DeleteSso!
|
||||
editSSOIdentityProvider(input: EditSsoInput!): EditSso!
|
||||
createMessageFolder(input: CreateMessageFolderInput!): MessageFolder!
|
||||
updateMessageFolder(input: UpdateMessageFolderInput!): MessageFolder!
|
||||
deleteMessageFolder(id: UUID!): MessageFolder!
|
||||
createCalendarChannel(input: CreateCalendarChannelInput!): CalendarChannel!
|
||||
updateCalendarChannel(input: UpdateCalendarChannelInput!): CalendarChannel!
|
||||
deleteCalendarChannel(id: UUID!): CalendarChannel!
|
||||
createMessageChannel(input: CreateMessageChannelInput!): MessageChannel!
|
||||
updateMessageFolders(input: UpdateMessageFoldersInput!): [MessageFolder!]!
|
||||
updateMessageChannel(input: UpdateMessageChannelInput!): MessageChannel!
|
||||
deleteMessageChannel(id: UUID!): MessageChannel!
|
||||
createConnectedAccount(input: CreateConnectedAccountInput!): ConnectedAccountDTO!
|
||||
updateConnectedAccount(input: UpdateConnectedAccountInput!): ConnectedAccountDTO!
|
||||
deleteConnectedAccount(id: UUID!): ConnectedAccountDTO!
|
||||
updateCalendarChannel(input: UpdateCalendarChannelInput!): CalendarChannel!
|
||||
createWebhook(input: CreateWebhookInput!): Webhook!
|
||||
updateWebhook(input: UpdateWebhookInput!): Webhook!
|
||||
deleteWebhook(id: UUID!): Webhook!
|
||||
@@ -4382,66 +4365,18 @@ input EditSsoInput {
|
||||
status: SSOIdentityProviderStatus!
|
||||
}
|
||||
|
||||
input CreateMessageFolderInput {
|
||||
id: UUID
|
||||
name: String
|
||||
isSentFolder: Boolean!
|
||||
isSynced: Boolean!
|
||||
externalId: String
|
||||
pendingSyncAction: MessageFolderPendingSyncAction!
|
||||
messageChannelId: UUID!
|
||||
parentFolderId: UUID
|
||||
}
|
||||
|
||||
input UpdateMessageFolderInput {
|
||||
id: UUID!
|
||||
update: UpdateMessageFolderInputUpdates!
|
||||
}
|
||||
|
||||
input UpdateMessageFolderInputUpdates {
|
||||
name: String
|
||||
syncCursor: String
|
||||
isSynced: Boolean
|
||||
pendingSyncAction: MessageFolderPendingSyncAction
|
||||
}
|
||||
|
||||
input CreateCalendarChannelInput {
|
||||
id: UUID
|
||||
handle: String!
|
||||
visibility: CalendarChannelVisibility!
|
||||
syncStage: CalendarChannelSyncStage!
|
||||
connectedAccountId: UUID!
|
||||
isContactAutoCreationEnabled: Boolean!
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy!
|
||||
isSyncEnabled: Boolean!
|
||||
}
|
||||
|
||||
input UpdateCalendarChannelInput {
|
||||
id: UUID!
|
||||
update: UpdateCalendarChannelInputUpdates!
|
||||
}
|
||||
|
||||
input UpdateCalendarChannelInputUpdates {
|
||||
visibility: CalendarChannelVisibility
|
||||
isContactAutoCreationEnabled: Boolean
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy
|
||||
isSyncEnabled: Boolean
|
||||
}
|
||||
|
||||
input CreateMessageChannelInput {
|
||||
id: UUID
|
||||
handle: String!
|
||||
visibility: MessageChannelVisibility!
|
||||
type: MessageChannelType!
|
||||
syncStage: MessageChannelSyncStage!
|
||||
connectedAccountId: UUID!
|
||||
isContactAutoCreationEnabled: Boolean!
|
||||
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy!
|
||||
messageFolderImportPolicy: MessageFolderImportPolicy!
|
||||
excludeNonProfessionalEmails: Boolean!
|
||||
excludeGroupEmails: Boolean!
|
||||
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction!
|
||||
isSyncEnabled: Boolean!
|
||||
input UpdateMessageFoldersInput {
|
||||
ids: [UUID!]!
|
||||
update: UpdateMessageFolderInputUpdates!
|
||||
}
|
||||
|
||||
input UpdateMessageChannelInput {
|
||||
@@ -4459,26 +4394,16 @@ input UpdateMessageChannelInputUpdates {
|
||||
excludeGroupEmails: Boolean
|
||||
}
|
||||
|
||||
input CreateConnectedAccountInput {
|
||||
id: UUID
|
||||
handle: String!
|
||||
provider: String!
|
||||
accessToken: String
|
||||
refreshToken: String
|
||||
scopes: [String!]
|
||||
userWorkspaceId: UUID!
|
||||
}
|
||||
|
||||
input UpdateConnectedAccountInput {
|
||||
input UpdateCalendarChannelInput {
|
||||
id: UUID!
|
||||
update: UpdateConnectedAccountInputUpdates!
|
||||
update: UpdateCalendarChannelInputUpdates!
|
||||
}
|
||||
|
||||
input UpdateConnectedAccountInputUpdates {
|
||||
accessToken: String
|
||||
refreshToken: String
|
||||
handleAliases: [String!]
|
||||
scopes: [String!]
|
||||
input UpdateCalendarChannelInputUpdates {
|
||||
visibility: CalendarChannelVisibility
|
||||
isContactAutoCreationEnabled: Boolean
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy
|
||||
isSyncEnabled: Boolean
|
||||
}
|
||||
|
||||
input CreateWebhookInput {
|
||||
|
||||
@@ -1886,7 +1886,6 @@ export interface CalendarChannel {
|
||||
isContactAutoCreationEnabled: Scalars['Boolean']
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy
|
||||
isSyncEnabled: Scalars['Boolean']
|
||||
syncCursor?: Scalars['String']
|
||||
syncedAt?: Scalars['DateTime']
|
||||
syncStageStartedAt?: Scalars['DateTime']
|
||||
throttleFailureCount: Scalars['Float']
|
||||
@@ -1908,15 +1907,11 @@ export interface ConnectedAccountDTO {
|
||||
id: Scalars['UUID']
|
||||
handle: Scalars['String']
|
||||
provider: Scalars['String']
|
||||
accessToken?: Scalars['String']
|
||||
refreshToken?: Scalars['String']
|
||||
lastCredentialsRefreshedAt?: Scalars['DateTime']
|
||||
authFailedAt?: Scalars['DateTime']
|
||||
handleAliases?: Scalars['String'][]
|
||||
scopes?: Scalars['String'][]
|
||||
connectionParameters?: Scalars['JSON']
|
||||
lastSignedInAt?: Scalars['DateTime']
|
||||
oidcTokenClaims?: Scalars['JSON']
|
||||
userWorkspaceId: Scalars['UUID']
|
||||
createdAt: Scalars['DateTime']
|
||||
updatedAt: Scalars['DateTime']
|
||||
@@ -1935,7 +1930,6 @@ export interface MessageChannel {
|
||||
excludeGroupEmails: Scalars['Boolean']
|
||||
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction
|
||||
isSyncEnabled: Scalars['Boolean']
|
||||
syncCursor?: Scalars['String']
|
||||
syncedAt?: Scalars['DateTime']
|
||||
syncStatus: MessageChannelSyncStatus
|
||||
syncStage: MessageChannelSyncStage
|
||||
@@ -1965,7 +1959,6 @@ export type MessageChannelSyncStage = 'PENDING_CONFIGURATION' | 'MESSAGE_LIST_FE
|
||||
export interface MessageFolder {
|
||||
id: Scalars['UUID']
|
||||
name?: Scalars['String']
|
||||
syncCursor?: Scalars['String']
|
||||
isSentFolder: Scalars['Boolean']
|
||||
isSynced: Scalars['Boolean']
|
||||
parentFolderId?: Scalars['UUID']
|
||||
@@ -2724,14 +2717,11 @@ export interface Query {
|
||||
findWorkspaceFromInviteHash: Workspace
|
||||
validatePasswordResetToken: ValidatePasswordResetToken
|
||||
getSSOIdentityProviders: FindAvailableSSOIDP[]
|
||||
messageFolders: MessageFolder[]
|
||||
messageFolder?: MessageFolder
|
||||
calendarChannels: CalendarChannel[]
|
||||
calendarChannel?: CalendarChannel
|
||||
messageChannels: MessageChannel[]
|
||||
messageChannel?: MessageChannel
|
||||
myMessageFolders: MessageFolder[]
|
||||
myMessageChannels: MessageChannel[]
|
||||
myConnectedAccounts: ConnectedAccountDTO[]
|
||||
connectedAccounts: ConnectedAccountDTO[]
|
||||
connectedAccount?: ConnectedAccountDTO
|
||||
myCalendarChannels: CalendarChannel[]
|
||||
webhooks: Webhook[]
|
||||
webhook?: Webhook
|
||||
minimalMetadata: MinimalMetadata
|
||||
@@ -2928,18 +2918,11 @@ export interface Mutation {
|
||||
createSAMLIdentityProvider: SetupSso
|
||||
deleteSSOIdentityProvider: DeleteSso
|
||||
editSSOIdentityProvider: EditSso
|
||||
createMessageFolder: MessageFolder
|
||||
updateMessageFolder: MessageFolder
|
||||
deleteMessageFolder: MessageFolder
|
||||
createCalendarChannel: CalendarChannel
|
||||
updateCalendarChannel: CalendarChannel
|
||||
deleteCalendarChannel: CalendarChannel
|
||||
createMessageChannel: MessageChannel
|
||||
updateMessageFolders: MessageFolder[]
|
||||
updateMessageChannel: MessageChannel
|
||||
deleteMessageChannel: MessageChannel
|
||||
createConnectedAccount: ConnectedAccountDTO
|
||||
updateConnectedAccount: ConnectedAccountDTO
|
||||
deleteConnectedAccount: ConnectedAccountDTO
|
||||
updateCalendarChannel: CalendarChannel
|
||||
createWebhook: Webhook
|
||||
updateWebhook: Webhook
|
||||
deleteWebhook: Webhook
|
||||
@@ -4994,7 +4977,6 @@ export interface CalendarChannelGenqlSelection{
|
||||
isContactAutoCreationEnabled?: boolean | number
|
||||
contactAutoCreationPolicy?: boolean | number
|
||||
isSyncEnabled?: boolean | number
|
||||
syncCursor?: boolean | number
|
||||
syncedAt?: boolean | number
|
||||
syncStageStartedAt?: boolean | number
|
||||
throttleFailureCount?: boolean | number
|
||||
@@ -5009,15 +4991,11 @@ export interface ConnectedAccountDTOGenqlSelection{
|
||||
id?: boolean | number
|
||||
handle?: boolean | number
|
||||
provider?: boolean | number
|
||||
accessToken?: boolean | number
|
||||
refreshToken?: boolean | number
|
||||
lastCredentialsRefreshedAt?: boolean | number
|
||||
authFailedAt?: boolean | number
|
||||
handleAliases?: boolean | number
|
||||
scopes?: boolean | number
|
||||
connectionParameters?: boolean | number
|
||||
lastSignedInAt?: boolean | number
|
||||
oidcTokenClaims?: boolean | number
|
||||
userWorkspaceId?: boolean | number
|
||||
createdAt?: boolean | number
|
||||
updatedAt?: boolean | number
|
||||
@@ -5037,7 +5015,6 @@ export interface MessageChannelGenqlSelection{
|
||||
excludeGroupEmails?: boolean | number
|
||||
pendingGroupEmailsAction?: boolean | number
|
||||
isSyncEnabled?: boolean | number
|
||||
syncCursor?: boolean | number
|
||||
syncedAt?: boolean | number
|
||||
syncStatus?: boolean | number
|
||||
syncStage?: boolean | number
|
||||
@@ -5054,7 +5031,6 @@ export interface MessageChannelGenqlSelection{
|
||||
export interface MessageFolderGenqlSelection{
|
||||
id?: boolean | number
|
||||
name?: boolean | number
|
||||
syncCursor?: boolean | number
|
||||
isSentFolder?: boolean | number
|
||||
isSynced?: boolean | number
|
||||
parentFolderId?: boolean | number
|
||||
@@ -5874,14 +5850,11 @@ export interface QueryGenqlSelection{
|
||||
findWorkspaceFromInviteHash?: (WorkspaceGenqlSelection & { __args: {inviteHash: Scalars['String']} })
|
||||
validatePasswordResetToken?: (ValidatePasswordResetTokenGenqlSelection & { __args: {passwordResetToken: Scalars['String']} })
|
||||
getSSOIdentityProviders?: FindAvailableSSOIDPGenqlSelection
|
||||
messageFolders?: (MessageFolderGenqlSelection & { __args?: {messageChannelId?: (Scalars['UUID'] | null)} })
|
||||
messageFolder?: (MessageFolderGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
calendarChannels?: (CalendarChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
calendarChannel?: (CalendarChannelGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
messageChannels?: (MessageChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
messageChannel?: (MessageChannelGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
myMessageFolders?: (MessageFolderGenqlSelection & { __args?: {messageChannelId?: (Scalars['UUID'] | null)} })
|
||||
myMessageChannels?: (MessageChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
myConnectedAccounts?: ConnectedAccountDTOGenqlSelection
|
||||
connectedAccounts?: ConnectedAccountDTOGenqlSelection
|
||||
connectedAccount?: (ConnectedAccountDTOGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
myCalendarChannels?: (CalendarChannelGenqlSelection & { __args?: {connectedAccountId?: (Scalars['UUID'] | null)} })
|
||||
webhooks?: WebhookGenqlSelection
|
||||
webhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
minimalMetadata?: MinimalMetadataGenqlSelection
|
||||
@@ -6103,18 +6076,11 @@ export interface MutationGenqlSelection{
|
||||
createSAMLIdentityProvider?: (SetupSsoGenqlSelection & { __args: {input: SetupSAMLSsoInput} })
|
||||
deleteSSOIdentityProvider?: (DeleteSsoGenqlSelection & { __args: {input: DeleteSsoInput} })
|
||||
editSSOIdentityProvider?: (EditSsoGenqlSelection & { __args: {input: EditSsoInput} })
|
||||
createMessageFolder?: (MessageFolderGenqlSelection & { __args: {input: CreateMessageFolderInput} })
|
||||
updateMessageFolder?: (MessageFolderGenqlSelection & { __args: {input: UpdateMessageFolderInput} })
|
||||
deleteMessageFolder?: (MessageFolderGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
createCalendarChannel?: (CalendarChannelGenqlSelection & { __args: {input: CreateCalendarChannelInput} })
|
||||
updateCalendarChannel?: (CalendarChannelGenqlSelection & { __args: {input: UpdateCalendarChannelInput} })
|
||||
deleteCalendarChannel?: (CalendarChannelGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
createMessageChannel?: (MessageChannelGenqlSelection & { __args: {input: CreateMessageChannelInput} })
|
||||
updateMessageFolders?: (MessageFolderGenqlSelection & { __args: {input: UpdateMessageFoldersInput} })
|
||||
updateMessageChannel?: (MessageChannelGenqlSelection & { __args: {input: UpdateMessageChannelInput} })
|
||||
deleteMessageChannel?: (MessageChannelGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
createConnectedAccount?: (ConnectedAccountDTOGenqlSelection & { __args: {input: CreateConnectedAccountInput} })
|
||||
updateConnectedAccount?: (ConnectedAccountDTOGenqlSelection & { __args: {input: UpdateConnectedAccountInput} })
|
||||
deleteConnectedAccount?: (ConnectedAccountDTOGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
updateCalendarChannel?: (CalendarChannelGenqlSelection & { __args: {input: UpdateCalendarChannelInput} })
|
||||
createWebhook?: (WebhookGenqlSelection & { __args: {input: CreateWebhookInput} })
|
||||
updateWebhook?: (WebhookGenqlSelection & { __args: {input: UpdateWebhookInput} })
|
||||
deleteWebhook?: (WebhookGenqlSelection & { __args: {id: Scalars['UUID']} })
|
||||
@@ -6439,29 +6405,19 @@ export interface DeleteSsoInput {identityProviderId: Scalars['UUID']}
|
||||
|
||||
export interface EditSsoInput {id: Scalars['UUID'],status: SSOIdentityProviderStatus}
|
||||
|
||||
export interface CreateMessageFolderInput {id?: (Scalars['UUID'] | null),name?: (Scalars['String'] | null),isSentFolder: Scalars['Boolean'],isSynced: Scalars['Boolean'],externalId?: (Scalars['String'] | null),pendingSyncAction: MessageFolderPendingSyncAction,messageChannelId: Scalars['UUID'],parentFolderId?: (Scalars['UUID'] | null)}
|
||||
|
||||
export interface UpdateMessageFolderInput {id: Scalars['UUID'],update: UpdateMessageFolderInputUpdates}
|
||||
|
||||
export interface UpdateMessageFolderInputUpdates {name?: (Scalars['String'] | null),syncCursor?: (Scalars['String'] | null),isSynced?: (Scalars['Boolean'] | null),pendingSyncAction?: (MessageFolderPendingSyncAction | null)}
|
||||
export interface UpdateMessageFolderInputUpdates {isSynced?: (Scalars['Boolean'] | null)}
|
||||
|
||||
export interface CreateCalendarChannelInput {id?: (Scalars['UUID'] | null),handle: Scalars['String'],visibility: CalendarChannelVisibility,syncStage: CalendarChannelSyncStage,connectedAccountId: Scalars['UUID'],isContactAutoCreationEnabled: Scalars['Boolean'],contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy,isSyncEnabled: Scalars['Boolean']}
|
||||
|
||||
export interface UpdateCalendarChannelInput {id: Scalars['UUID'],update: UpdateCalendarChannelInputUpdates}
|
||||
|
||||
export interface UpdateCalendarChannelInputUpdates {visibility?: (CalendarChannelVisibility | null),isContactAutoCreationEnabled?: (Scalars['Boolean'] | null),contactAutoCreationPolicy?: (CalendarChannelContactAutoCreationPolicy | null),isSyncEnabled?: (Scalars['Boolean'] | null)}
|
||||
|
||||
export interface CreateMessageChannelInput {id?: (Scalars['UUID'] | null),handle: Scalars['String'],visibility: MessageChannelVisibility,type: MessageChannelType,syncStage: MessageChannelSyncStage,connectedAccountId: Scalars['UUID'],isContactAutoCreationEnabled: Scalars['Boolean'],contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy,messageFolderImportPolicy: MessageFolderImportPolicy,excludeNonProfessionalEmails: Scalars['Boolean'],excludeGroupEmails: Scalars['Boolean'],pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction,isSyncEnabled: Scalars['Boolean']}
|
||||
export interface UpdateMessageFoldersInput {ids: Scalars['UUID'][],update: UpdateMessageFolderInputUpdates}
|
||||
|
||||
export interface UpdateMessageChannelInput {id: Scalars['UUID'],update: UpdateMessageChannelInputUpdates}
|
||||
|
||||
export interface UpdateMessageChannelInputUpdates {visibility?: (MessageChannelVisibility | null),isContactAutoCreationEnabled?: (Scalars['Boolean'] | null),contactAutoCreationPolicy?: (MessageChannelContactAutoCreationPolicy | null),messageFolderImportPolicy?: (MessageFolderImportPolicy | null),isSyncEnabled?: (Scalars['Boolean'] | null),excludeNonProfessionalEmails?: (Scalars['Boolean'] | null),excludeGroupEmails?: (Scalars['Boolean'] | null)}
|
||||
|
||||
export interface CreateConnectedAccountInput {id?: (Scalars['UUID'] | null),handle: Scalars['String'],provider: Scalars['String'],accessToken?: (Scalars['String'] | null),refreshToken?: (Scalars['String'] | null),scopes?: (Scalars['String'][] | null),userWorkspaceId: Scalars['UUID']}
|
||||
export interface UpdateCalendarChannelInput {id: Scalars['UUID'],update: UpdateCalendarChannelInputUpdates}
|
||||
|
||||
export interface UpdateConnectedAccountInput {id: Scalars['UUID'],update: UpdateConnectedAccountInputUpdates}
|
||||
|
||||
export interface UpdateConnectedAccountInputUpdates {accessToken?: (Scalars['String'] | null),refreshToken?: (Scalars['String'] | null),handleAliases?: (Scalars['String'][] | null),scopes?: (Scalars['String'][] | null)}
|
||||
export interface UpdateCalendarChannelInputUpdates {visibility?: (CalendarChannelVisibility | null),isContactAutoCreationEnabled?: (Scalars['Boolean'] | null),contactAutoCreationPolicy?: (CalendarChannelContactAutoCreationPolicy | null),isSyncEnabled?: (Scalars['Boolean'] | null)}
|
||||
|
||||
export interface CreateWebhookInput {id?: (Scalars['UUID'] | null),targetUrl: Scalars['String'],operations: Scalars['String'][],description?: (Scalars['String'] | null),secret?: (Scalars['String'] | null)}
|
||||
|
||||
|
||||
@@ -87,8 +87,8 @@ export default {
|
||||
352,
|
||||
364,
|
||||
365,
|
||||
483,
|
||||
484
|
||||
478,
|
||||
479
|
||||
],
|
||||
"types": {
|
||||
"BillingProductDTO": {
|
||||
@@ -4346,9 +4346,6 @@ export default {
|
||||
"isSyncEnabled": [
|
||||
6
|
||||
],
|
||||
"syncCursor": [
|
||||
1
|
||||
],
|
||||
"syncedAt": [
|
||||
4
|
||||
],
|
||||
@@ -4385,12 +4382,6 @@ export default {
|
||||
"provider": [
|
||||
1
|
||||
],
|
||||
"accessToken": [
|
||||
1
|
||||
],
|
||||
"refreshToken": [
|
||||
1
|
||||
],
|
||||
"lastCredentialsRefreshedAt": [
|
||||
4
|
||||
],
|
||||
@@ -4403,15 +4394,9 @@ export default {
|
||||
"scopes": [
|
||||
1
|
||||
],
|
||||
"connectionParameters": [
|
||||
15
|
||||
],
|
||||
"lastSignedInAt": [
|
||||
4
|
||||
],
|
||||
"oidcTokenClaims": [
|
||||
15
|
||||
],
|
||||
"userWorkspaceId": [
|
||||
3
|
||||
],
|
||||
@@ -4459,9 +4444,6 @@ export default {
|
||||
"isSyncEnabled": [
|
||||
6
|
||||
],
|
||||
"syncCursor": [
|
||||
1
|
||||
],
|
||||
"syncedAt": [
|
||||
4
|
||||
],
|
||||
@@ -4507,9 +4489,6 @@ export default {
|
||||
"name": [
|
||||
1
|
||||
],
|
||||
"syncCursor": [
|
||||
1
|
||||
],
|
||||
"isSentFolder": [
|
||||
6
|
||||
],
|
||||
@@ -6484,7 +6463,7 @@ export default {
|
||||
"getSSOIdentityProviders": [
|
||||
184
|
||||
],
|
||||
"messageFolders": [
|
||||
"myMessageFolders": [
|
||||
249,
|
||||
{
|
||||
"messageChannelId": [
|
||||
@@ -6492,33 +6471,7 @@ export default {
|
||||
]
|
||||
}
|
||||
],
|
||||
"messageFolder": [
|
||||
249,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
"UUID!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"calendarChannels": [
|
||||
235,
|
||||
{
|
||||
"connectedAccountId": [
|
||||
3
|
||||
]
|
||||
}
|
||||
],
|
||||
"calendarChannel": [
|
||||
235,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
"UUID!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"messageChannels": [
|
||||
"myMessageChannels": [
|
||||
241,
|
||||
{
|
||||
"connectedAccountId": [
|
||||
@@ -6526,24 +6479,17 @@ export default {
|
||||
]
|
||||
}
|
||||
],
|
||||
"messageChannel": [
|
||||
241,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
"UUID!"
|
||||
]
|
||||
}
|
||||
"myConnectedAccounts": [
|
||||
240
|
||||
],
|
||||
"connectedAccounts": [
|
||||
240
|
||||
],
|
||||
"connectedAccount": [
|
||||
240,
|
||||
"myCalendarChannels": [
|
||||
235,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
"UUID!"
|
||||
"connectedAccountId": [
|
||||
3
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -8446,66 +8392,21 @@ export default {
|
||||
]
|
||||
}
|
||||
],
|
||||
"createMessageFolder": [
|
||||
249,
|
||||
{
|
||||
"input": [
|
||||
459,
|
||||
"CreateMessageFolderInput!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"updateMessageFolder": [
|
||||
249,
|
||||
{
|
||||
"input": [
|
||||
460,
|
||||
459,
|
||||
"UpdateMessageFolderInput!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"deleteMessageFolder": [
|
||||
"updateMessageFolders": [
|
||||
249,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
"UUID!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"createCalendarChannel": [
|
||||
235,
|
||||
{
|
||||
"input": [
|
||||
462,
|
||||
"CreateCalendarChannelInput!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"updateCalendarChannel": [
|
||||
235,
|
||||
{
|
||||
"input": [
|
||||
463,
|
||||
"UpdateCalendarChannelInput!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"deleteCalendarChannel": [
|
||||
235,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
"UUID!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"createMessageChannel": [
|
||||
241,
|
||||
{
|
||||
"input": [
|
||||
465,
|
||||
"CreateMessageChannelInput!"
|
||||
461,
|
||||
"UpdateMessageFoldersInput!"
|
||||
]
|
||||
}
|
||||
],
|
||||
@@ -8513,38 +8414,11 @@ export default {
|
||||
241,
|
||||
{
|
||||
"input": [
|
||||
466,
|
||||
462,
|
||||
"UpdateMessageChannelInput!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"deleteMessageChannel": [
|
||||
241,
|
||||
{
|
||||
"id": [
|
||||
3,
|
||||
"UUID!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"createConnectedAccount": [
|
||||
240,
|
||||
{
|
||||
"input": [
|
||||
468,
|
||||
"CreateConnectedAccountInput!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"updateConnectedAccount": [
|
||||
240,
|
||||
{
|
||||
"input": [
|
||||
469,
|
||||
"UpdateConnectedAccountInput!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"deleteConnectedAccount": [
|
||||
240,
|
||||
{
|
||||
@@ -8554,11 +8428,20 @@ export default {
|
||||
]
|
||||
}
|
||||
],
|
||||
"updateCalendarChannel": [
|
||||
235,
|
||||
{
|
||||
"input": [
|
||||
464,
|
||||
"UpdateCalendarChannelInput!"
|
||||
]
|
||||
}
|
||||
],
|
||||
"createWebhook": [
|
||||
256,
|
||||
{
|
||||
"input": [
|
||||
471,
|
||||
466,
|
||||
"CreateWebhookInput!"
|
||||
]
|
||||
}
|
||||
@@ -8567,7 +8450,7 @@ export default {
|
||||
256,
|
||||
{
|
||||
"input": [
|
||||
472,
|
||||
467,
|
||||
"UpdateWebhookInput!"
|
||||
]
|
||||
}
|
||||
@@ -8588,7 +8471,7 @@ export default {
|
||||
221,
|
||||
{
|
||||
"input": [
|
||||
474,
|
||||
469,
|
||||
"CreateSkillInput!"
|
||||
]
|
||||
}
|
||||
@@ -8597,7 +8480,7 @@ export default {
|
||||
221,
|
||||
{
|
||||
"input": [
|
||||
475,
|
||||
470,
|
||||
"UpdateSkillInput!"
|
||||
]
|
||||
}
|
||||
@@ -8694,7 +8577,7 @@ export default {
|
||||
"String!"
|
||||
],
|
||||
"connectionParameters": [
|
||||
476,
|
||||
471,
|
||||
"EmailAccountConnectionParameters!"
|
||||
],
|
||||
"id": [
|
||||
@@ -8706,7 +8589,7 @@ export default {
|
||||
154,
|
||||
{
|
||||
"input": [
|
||||
478,
|
||||
473,
|
||||
"UpdateLabPublicFeatureFlagInput!"
|
||||
]
|
||||
}
|
||||
@@ -8879,7 +8762,7 @@ export default {
|
||||
68,
|
||||
{
|
||||
"input": [
|
||||
479,
|
||||
474,
|
||||
"CreateOneAppTokenInput!"
|
||||
]
|
||||
}
|
||||
@@ -8912,7 +8795,7 @@ export default {
|
||||
6,
|
||||
{
|
||||
"workspaceMigration": [
|
||||
481,
|
||||
476,
|
||||
"WorkspaceMigrationInput!"
|
||||
]
|
||||
}
|
||||
@@ -8986,7 +8869,7 @@ export default {
|
||||
"String!"
|
||||
],
|
||||
"fileFolder": [
|
||||
484,
|
||||
479,
|
||||
"FileFolder!"
|
||||
],
|
||||
"filePath": [
|
||||
@@ -10983,159 +10866,31 @@ export default {
|
||||
1
|
||||
]
|
||||
},
|
||||
"CreateMessageFolderInput": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"name": [
|
||||
1
|
||||
],
|
||||
"isSentFolder": [
|
||||
6
|
||||
],
|
||||
"isSynced": [
|
||||
6
|
||||
],
|
||||
"externalId": [
|
||||
1
|
||||
],
|
||||
"pendingSyncAction": [
|
||||
250
|
||||
],
|
||||
"messageChannelId": [
|
||||
3
|
||||
],
|
||||
"parentFolderId": [
|
||||
3
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"UpdateMessageFolderInput": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"update": [
|
||||
461
|
||||
460
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"UpdateMessageFolderInputUpdates": {
|
||||
"name": [
|
||||
1
|
||||
],
|
||||
"syncCursor": [
|
||||
1
|
||||
],
|
||||
"isSynced": [
|
||||
6
|
||||
],
|
||||
"pendingSyncAction": [
|
||||
250
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"CreateCalendarChannelInput": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"handle": [
|
||||
1
|
||||
],
|
||||
"visibility": [
|
||||
238
|
||||
],
|
||||
"syncStage": [
|
||||
237
|
||||
],
|
||||
"connectedAccountId": [
|
||||
3
|
||||
],
|
||||
"isContactAutoCreationEnabled": [
|
||||
6
|
||||
],
|
||||
"contactAutoCreationPolicy": [
|
||||
239
|
||||
],
|
||||
"isSyncEnabled": [
|
||||
6
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"UpdateCalendarChannelInput": {
|
||||
"id": [
|
||||
"UpdateMessageFoldersInput": {
|
||||
"ids": [
|
||||
3
|
||||
],
|
||||
"update": [
|
||||
464
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"UpdateCalendarChannelInputUpdates": {
|
||||
"visibility": [
|
||||
238
|
||||
],
|
||||
"isContactAutoCreationEnabled": [
|
||||
6
|
||||
],
|
||||
"contactAutoCreationPolicy": [
|
||||
239
|
||||
],
|
||||
"isSyncEnabled": [
|
||||
6
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"CreateMessageChannelInput": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"handle": [
|
||||
1
|
||||
],
|
||||
"visibility": [
|
||||
242
|
||||
],
|
||||
"type": [
|
||||
243
|
||||
],
|
||||
"syncStage": [
|
||||
248
|
||||
],
|
||||
"connectedAccountId": [
|
||||
3
|
||||
],
|
||||
"isContactAutoCreationEnabled": [
|
||||
6
|
||||
],
|
||||
"contactAutoCreationPolicy": [
|
||||
244
|
||||
],
|
||||
"messageFolderImportPolicy": [
|
||||
245
|
||||
],
|
||||
"excludeNonProfessionalEmails": [
|
||||
6
|
||||
],
|
||||
"excludeGroupEmails": [
|
||||
6
|
||||
],
|
||||
"pendingGroupEmailsAction": [
|
||||
246
|
||||
],
|
||||
"isSyncEnabled": [
|
||||
6
|
||||
460
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -11146,7 +10901,7 @@ export default {
|
||||
3
|
||||
],
|
||||
"update": [
|
||||
467
|
||||
463
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -11178,55 +10933,29 @@ export default {
|
||||
1
|
||||
]
|
||||
},
|
||||
"CreateConnectedAccountInput": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"handle": [
|
||||
1
|
||||
],
|
||||
"provider": [
|
||||
1
|
||||
],
|
||||
"accessToken": [
|
||||
1
|
||||
],
|
||||
"refreshToken": [
|
||||
1
|
||||
],
|
||||
"scopes": [
|
||||
1
|
||||
],
|
||||
"userWorkspaceId": [
|
||||
3
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"UpdateConnectedAccountInput": {
|
||||
"UpdateCalendarChannelInput": {
|
||||
"id": [
|
||||
3
|
||||
],
|
||||
"update": [
|
||||
470
|
||||
465
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"UpdateConnectedAccountInputUpdates": {
|
||||
"accessToken": [
|
||||
1
|
||||
"UpdateCalendarChannelInputUpdates": {
|
||||
"visibility": [
|
||||
238
|
||||
],
|
||||
"refreshToken": [
|
||||
1
|
||||
"isContactAutoCreationEnabled": [
|
||||
6
|
||||
],
|
||||
"handleAliases": [
|
||||
1
|
||||
"contactAutoCreationPolicy": [
|
||||
239
|
||||
],
|
||||
"scopes": [
|
||||
1
|
||||
"isSyncEnabled": [
|
||||
6
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -11257,7 +10986,7 @@ export default {
|
||||
3
|
||||
],
|
||||
"update": [
|
||||
473
|
||||
468
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -11331,13 +11060,13 @@ export default {
|
||||
},
|
||||
"EmailAccountConnectionParameters": {
|
||||
"IMAP": [
|
||||
477
|
||||
472
|
||||
],
|
||||
"SMTP": [
|
||||
477
|
||||
472
|
||||
],
|
||||
"CALDAV": [
|
||||
477
|
||||
472
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -11376,7 +11105,7 @@ export default {
|
||||
},
|
||||
"CreateOneAppTokenInput": {
|
||||
"appToken": [
|
||||
480
|
||||
475
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -11392,7 +11121,7 @@ export default {
|
||||
},
|
||||
"WorkspaceMigrationInput": {
|
||||
"actions": [
|
||||
482
|
||||
477
|
||||
],
|
||||
"__typename": [
|
||||
1
|
||||
@@ -11400,7 +11129,7 @@ export default {
|
||||
},
|
||||
"WorkspaceMigrationDeleteActionInput": {
|
||||
"type": [
|
||||
483
|
||||
478
|
||||
],
|
||||
"metadataName": [
|
||||
252
|
||||
@@ -11428,7 +11157,7 @@ export default {
|
||||
218,
|
||||
{
|
||||
"input": [
|
||||
486,
|
||||
481,
|
||||
"LogicFunctionLogsInput!"
|
||||
]
|
||||
}
|
||||
|
||||
+34
-16
@@ -309,26 +309,44 @@ export class MigrateMessagingInfrastructureToMetadataCommand extends ActiveOrSus
|
||||
}
|
||||
|
||||
if (messageFolders.length > 0) {
|
||||
const externalIdToFolderIdMap = new Map(
|
||||
messageFolders.map((folder) => [folder.externalId, folder.id]),
|
||||
);
|
||||
|
||||
const coreMessageFolders = messageFolders
|
||||
.filter((workspaceEntity) =>
|
||||
migratedMessageChannelIds.has(workspaceEntity.messageChannelId),
|
||||
)
|
||||
.map((workspaceEntity) => ({
|
||||
id: workspaceEntity.id,
|
||||
name: workspaceEntity.name,
|
||||
syncCursor: workspaceEntity.syncCursor,
|
||||
isSentFolder: workspaceEntity.isSentFolder,
|
||||
isSynced: workspaceEntity.isSynced,
|
||||
parentFolderId: isNonEmptyString(workspaceEntity.parentFolderId)
|
||||
? workspaceEntity.parentFolderId
|
||||
: null,
|
||||
externalId: workspaceEntity.externalId,
|
||||
pendingSyncAction: workspaceEntity.pendingSyncAction,
|
||||
messageChannelId: workspaceEntity.messageChannelId,
|
||||
workspaceId,
|
||||
createdAt: workspaceEntity.createdAt,
|
||||
updatedAt: workspaceEntity.updatedAt,
|
||||
}));
|
||||
.map((workspaceEntity) => {
|
||||
let resolvedParentFolderId: string | null = null;
|
||||
|
||||
if (isNonEmptyString(workspaceEntity.parentFolderId)) {
|
||||
resolvedParentFolderId =
|
||||
externalIdToFolderIdMap.get(workspaceEntity.parentFolderId) ??
|
||||
null;
|
||||
|
||||
if (!resolvedParentFolderId) {
|
||||
this.logger.warn(
|
||||
`Message folder ${workspaceEntity.id}: could not resolve parentFolderId externalId "${workspaceEntity.parentFolderId}" to a UUID`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: workspaceEntity.id,
|
||||
name: workspaceEntity.name,
|
||||
syncCursor: workspaceEntity.syncCursor,
|
||||
isSentFolder: workspaceEntity.isSentFolder,
|
||||
isSynced: workspaceEntity.isSynced,
|
||||
parentFolderId: resolvedParentFolderId,
|
||||
externalId: workspaceEntity.externalId,
|
||||
pendingSyncAction: workspaceEntity.pendingSyncAction,
|
||||
messageChannelId: workspaceEntity.messageChannelId,
|
||||
workspaceId,
|
||||
createdAt: workspaceEntity.createdAt,
|
||||
updatedAt: workspaceEntity.updatedAt,
|
||||
};
|
||||
});
|
||||
|
||||
await this.messageFolderRepository.save(
|
||||
coreMessageFolders as unknown as MessageFolderEntity[],
|
||||
|
||||
+5
-5
@@ -1,9 +1,9 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class AddMessagingInfrastructureMetadataEntities1773945207801
|
||||
export class AddMessagingInfrastructureMetadataEntities1774005903909
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'AddMessagingInfrastructureMetadataEntities1773945207801';
|
||||
name = 'AddMessagingInfrastructureMetadataEntities1774005903909';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
@@ -31,13 +31,13 @@ export class AddMessagingInfrastructureMetadataEntities1773945207801
|
||||
`CREATE TYPE "core"."messageChannel_syncstage_enum" AS ENUM('PENDING_CONFIGURATION', 'MESSAGE_LIST_FETCH_PENDING', 'MESSAGE_LIST_FETCH_SCHEDULED', 'MESSAGE_LIST_FETCH_ONGOING', 'MESSAGES_IMPORT_PENDING', 'MESSAGES_IMPORT_SCHEDULED', 'MESSAGES_IMPORT_ONGOING', 'FAILED')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "core"."messageChannel" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "visibility" "core"."messageChannel_visibility_enum" NOT NULL, "handle" character varying NOT NULL, "type" "core"."messageChannel_type_enum" NOT NULL, "isContactAutoCreationEnabled" boolean NOT NULL, "contactAutoCreationPolicy" "core"."messageChannel_contactautocreationpolicy_enum" NOT NULL, "messageFolderImportPolicy" "core"."messageChannel_messagefolderimportpolicy_enum" NOT NULL, "excludeNonProfessionalEmails" boolean NOT NULL, "excludeGroupEmails" boolean NOT NULL, "pendingGroupEmailsAction" "core"."messageChannel_pendinggroupemailsaction_enum" NOT NULL, "isSyncEnabled" boolean NOT NULL, "syncCursor" character varying, "syncedAt" TIMESTAMP WITH TIME ZONE, "syncStatus" "core"."messageChannel_syncstatus_enum" NOT NULL DEFAULT 'NOT_SYNCED', "syncStage" "core"."messageChannel_syncstage_enum" NOT NULL, "syncStageStartedAt" TIMESTAMP WITH TIME ZONE, "throttleFailureCount" integer NOT NULL DEFAULT '0', "throttleRetryAfter" TIMESTAMP WITH TIME ZONE, "connectedAccountId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_438b9412475f39712ed065f77af" PRIMARY KEY ("id"))`,
|
||||
`CREATE TABLE "core"."messageChannel" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "visibility" "core"."messageChannel_visibility_enum" NOT NULL, "handle" character varying NOT NULL, "type" "core"."messageChannel_type_enum" NOT NULL, "isContactAutoCreationEnabled" boolean NOT NULL DEFAULT true, "contactAutoCreationPolicy" "core"."messageChannel_contactautocreationpolicy_enum" NOT NULL DEFAULT 'SENT', "messageFolderImportPolicy" "core"."messageChannel_messagefolderimportpolicy_enum" NOT NULL DEFAULT 'ALL_FOLDERS', "excludeNonProfessionalEmails" boolean NOT NULL DEFAULT true, "excludeGroupEmails" boolean NOT NULL DEFAULT true, "pendingGroupEmailsAction" "core"."messageChannel_pendinggroupemailsaction_enum" NOT NULL, "isSyncEnabled" boolean NOT NULL DEFAULT true, "syncCursor" character varying, "syncedAt" TIMESTAMP WITH TIME ZONE, "syncStatus" "core"."messageChannel_syncstatus_enum" NOT NULL DEFAULT 'NOT_SYNCED', "syncStage" "core"."messageChannel_syncstage_enum" NOT NULL, "syncStageStartedAt" TIMESTAMP WITH TIME ZONE, "throttleFailureCount" integer NOT NULL DEFAULT '0', "throttleRetryAfter" TIMESTAMP WITH TIME ZONE, "connectedAccountId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_438b9412475f39712ed065f77af" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."messageFolder_pendingsyncaction_enum" AS ENUM('FOLDER_DELETION', 'NONE')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "core"."messageFolder" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying, "syncCursor" character varying, "isSentFolder" boolean NOT NULL, "isSynced" boolean NOT NULL, "parentFolderId" uuid, "externalId" character varying, "pendingSyncAction" "core"."messageFolder_pendingsyncaction_enum" NOT NULL, "messageChannelId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_85cb5a339d9f7f1106dde9e4db8" PRIMARY KEY ("id"))`,
|
||||
`CREATE TABLE "core"."messageFolder" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "name" character varying, "syncCursor" character varying, "isSentFolder" boolean NOT NULL, "isSynced" boolean NOT NULL, "parentFolderId" uuid, "externalId" character varying, "pendingSyncAction" "core"."messageFolder_pendingsyncaction_enum" NOT NULL DEFAULT 'NONE', "messageChannelId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_85cb5a339d9f7f1106dde9e4db8" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."calendarChannel_syncstatus_enum" AS ENUM('NOT_SYNCED', 'ONGOING', 'ACTIVE', 'FAILED_INSUFFICIENT_PERMISSIONS', 'FAILED_UNKNOWN')`,
|
||||
@@ -52,7 +52,7 @@ export class AddMessagingInfrastructureMetadataEntities1773945207801
|
||||
`CREATE TYPE "core"."calendarChannel_contactautocreationpolicy_enum" AS ENUM('AS_PARTICIPANT_AND_ORGANIZER', 'AS_PARTICIPANT', 'AS_ORGANIZER', 'NONE')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "core"."calendarChannel" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "handle" character varying NOT NULL, "syncStatus" "core"."calendarChannel_syncstatus_enum" NOT NULL DEFAULT 'NOT_SYNCED', "syncStage" "core"."calendarChannel_syncstage_enum" NOT NULL, "visibility" "core"."calendarChannel_visibility_enum" NOT NULL, "isContactAutoCreationEnabled" boolean NOT NULL, "contactAutoCreationPolicy" "core"."calendarChannel_contactautocreationpolicy_enum" NOT NULL, "isSyncEnabled" boolean NOT NULL, "syncCursor" character varying, "syncedAt" TIMESTAMP WITH TIME ZONE, "syncStageStartedAt" TIMESTAMP WITH TIME ZONE, "throttleFailureCount" integer NOT NULL DEFAULT '0', "connectedAccountId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_796d701c0c35518517d0f3e0e0b" PRIMARY KEY ("id"))`,
|
||||
`CREATE TABLE "core"."calendarChannel" ("workspaceId" uuid NOT NULL, "id" uuid NOT NULL DEFAULT uuid_generate_v4(), "handle" character varying NOT NULL, "syncStatus" "core"."calendarChannel_syncstatus_enum" NOT NULL DEFAULT 'NOT_SYNCED', "syncStage" "core"."calendarChannel_syncstage_enum" NOT NULL, "visibility" "core"."calendarChannel_visibility_enum" NOT NULL, "isContactAutoCreationEnabled" boolean NOT NULL DEFAULT true, "contactAutoCreationPolicy" "core"."calendarChannel_contactautocreationpolicy_enum" NOT NULL DEFAULT 'AS_PARTICIPANT_AND_ORGANIZER', "isSyncEnabled" boolean NOT NULL DEFAULT true, "syncCursor" character varying, "syncedAt" TIMESTAMP WITH TIME ZONE, "syncStageStartedAt" TIMESTAMP WITH TIME ZONE, "throttleFailureCount" integer NOT NULL DEFAULT '0', "connectedAccountId" uuid NOT NULL, "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), CONSTRAINT "PK_796d701c0c35518517d0f3e0e0b" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."connectedAccount" ADD CONSTRAINT "FK_1c7af038a011e99c27044793c6a" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
@@ -48,7 +48,8 @@ export class McpCoreController {
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthApiKey() apiKey: ApiKeyEntity | undefined,
|
||||
@AuthUser({ allowUndefined: true }) user: UserEntity | undefined,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
) {
|
||||
const result = await this.mcpProtocolService.handleMCPCoreQuery(body, {
|
||||
|
||||
+1
-1
@@ -112,7 +112,7 @@ export class ApplicationDevelopmentResolver {
|
||||
@Args() { applicationId }: GenerateApplicationTokenInput,
|
||||
@AuthWorkspace() { id: workspaceId }: WorkspaceEntity,
|
||||
@AuthUser({ allowUndefined: true }) user?: { id: string },
|
||||
@AuthUserWorkspaceId() userWorkspaceId?: string,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true }) userWorkspaceId?: string,
|
||||
): Promise<ApplicationTokenPairDTO> {
|
||||
return this.applicationTokenService.generateApplicationTokenPair({
|
||||
workspaceId,
|
||||
|
||||
@@ -37,7 +37,8 @@ export class SearchResolver {
|
||||
@Query(() => SearchResultConnectionDTO)
|
||||
async search(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
@AuthApiKey() apiKey: ApiKeyEntity | undefined,
|
||||
@Args()
|
||||
{
|
||||
|
||||
@@ -362,7 +362,8 @@ export class WorkspaceResolver {
|
||||
@ResolveField(() => [ViewDTO])
|
||||
async views(
|
||||
@Parent() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
): Promise<ViewDTO[]> {
|
||||
return this.viewService.findByWorkspaceId(workspace.id, userWorkspaceId);
|
||||
}
|
||||
|
||||
+16
-2
@@ -1,11 +1,25 @@
|
||||
import { type ExecutionContext, createParamDecorator } from '@nestjs/common';
|
||||
import {
|
||||
type ExecutionContext,
|
||||
ForbiddenException,
|
||||
createParamDecorator,
|
||||
} from '@nestjs/common';
|
||||
|
||||
import { getRequest } from 'src/utils/extract-request';
|
||||
|
||||
interface DecoratorOptions {
|
||||
allowUndefined?: boolean;
|
||||
}
|
||||
|
||||
export const AuthUserWorkspaceId = createParamDecorator(
|
||||
(_data: unknown, ctx: ExecutionContext) => {
|
||||
(options: DecoratorOptions | undefined, ctx: ExecutionContext) => {
|
||||
const request = getRequest(ctx);
|
||||
|
||||
if (!options?.allowUndefined && !request.userWorkspaceId) {
|
||||
throw new ForbiddenException(
|
||||
'This endpoint requires a user context. API keys are not supported.',
|
||||
);
|
||||
}
|
||||
|
||||
return request.userWorkspaceId;
|
||||
},
|
||||
);
|
||||
|
||||
+2
@@ -7,6 +7,7 @@ import { CalendarChannelMetadataService } from 'src/engine/metadata-modules/cale
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { CalendarChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/calendar-channel/interceptors/calendar-channel-graphql-api-exception.interceptor';
|
||||
import { CalendarChannelResolver } from 'src/engine/metadata-modules/calendar-channel/resolvers/calendar-channel.resolver';
|
||||
import { ConnectedAccountMetadataModule } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
|
||||
@Module({
|
||||
@@ -15,6 +16,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
AuthModule,
|
||||
PermissionsModule,
|
||||
FeatureFlagModule,
|
||||
ConnectedAccountMetadataModule,
|
||||
],
|
||||
providers: [
|
||||
CalendarChannelMetadataService,
|
||||
|
||||
+128
-17
@@ -1,43 +1,144 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
CalendarChannelSyncStage,
|
||||
CalendarChannelVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
CalendarChannelException,
|
||||
CalendarChannelExceptionCode,
|
||||
} from 'src/engine/metadata-modules/calendar-channel/calendar-channel.exception';
|
||||
import { CalendarChannelDTO } from 'src/engine/metadata-modules/calendar-channel/dtos/calendar-channel.dto';
|
||||
import { CalendarChannelEntity } from 'src/engine/metadata-modules/calendar-channel/entities/calendar-channel.entity';
|
||||
import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service';
|
||||
|
||||
@Injectable()
|
||||
export class CalendarChannelMetadataService {
|
||||
constructor(
|
||||
@InjectRepository(CalendarChannelEntity)
|
||||
private readonly repository: Repository<CalendarChannelEntity>,
|
||||
private readonly connectedAccountMetadataService: ConnectedAccountMetadataService,
|
||||
) {}
|
||||
|
||||
async findAll(workspaceId: string): Promise<CalendarChannelDTO[]> {
|
||||
return this.repository.find({ where: { workspaceId } });
|
||||
}
|
||||
|
||||
async findByConnectedAccountId(
|
||||
connectedAccountId: string,
|
||||
workspaceId: string,
|
||||
): Promise<CalendarChannelDTO[]> {
|
||||
async findByUserWorkspaceId({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
}: {
|
||||
userWorkspaceId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<CalendarChannelDTO[]> {
|
||||
const userAccountIds =
|
||||
await this.connectedAccountMetadataService.getUserConnectedAccountIds({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return this.findByConnectedAccountIds({
|
||||
connectedAccountIds: userAccountIds,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
async findByConnectedAccountIdForUser({
|
||||
connectedAccountId,
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
}: {
|
||||
connectedAccountId: string;
|
||||
userWorkspaceId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<CalendarChannelDTO[]> {
|
||||
await this.connectedAccountMetadataService.verifyOwnership({
|
||||
id: connectedAccountId,
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return this.findByConnectedAccountId({ connectedAccountId, workspaceId });
|
||||
}
|
||||
|
||||
async findByConnectedAccountId({
|
||||
connectedAccountId,
|
||||
workspaceId,
|
||||
}: {
|
||||
connectedAccountId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<CalendarChannelDTO[]> {
|
||||
return this.repository.find({
|
||||
where: { connectedAccountId, workspaceId },
|
||||
});
|
||||
}
|
||||
|
||||
async findById(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
): Promise<CalendarChannelDTO | null> {
|
||||
async findByConnectedAccountIds({
|
||||
connectedAccountIds,
|
||||
workspaceId,
|
||||
}: {
|
||||
connectedAccountIds: string[];
|
||||
workspaceId: string;
|
||||
}): Promise<CalendarChannelDTO[]> {
|
||||
if (connectedAccountIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.repository.find({
|
||||
where: { connectedAccountId: In(connectedAccountIds), workspaceId },
|
||||
});
|
||||
}
|
||||
|
||||
async findById({
|
||||
id,
|
||||
workspaceId,
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
}): Promise<CalendarChannelDTO | null> {
|
||||
return this.repository.findOne({ where: { id, workspaceId } });
|
||||
}
|
||||
|
||||
async verifyOwnership({
|
||||
id,
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
}: {
|
||||
id: string;
|
||||
userWorkspaceId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<CalendarChannelEntity> {
|
||||
const calendarChannel = await this.repository.findOne({
|
||||
where: { id, workspaceId },
|
||||
});
|
||||
|
||||
if (!calendarChannel) {
|
||||
throw new CalendarChannelException(
|
||||
`Calendar channel ${id} not found`,
|
||||
CalendarChannelExceptionCode.CALENDAR_CHANNEL_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const userAccountIds =
|
||||
await this.connectedAccountMetadataService.getUserConnectedAccountIds({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!userAccountIds.includes(calendarChannel.connectedAccountId)) {
|
||||
throw new CalendarChannelException(
|
||||
`Calendar channel ${id} does not belong to user workspace ${userWorkspaceId}`,
|
||||
CalendarChannelExceptionCode.CALENDAR_CHANNEL_OWNERSHIP_VIOLATION,
|
||||
);
|
||||
}
|
||||
|
||||
return calendarChannel;
|
||||
}
|
||||
|
||||
async create(
|
||||
data: Partial<CalendarChannelEntity> & {
|
||||
workspaceId: string;
|
||||
@@ -52,11 +153,15 @@ export class CalendarChannelMetadataService {
|
||||
return this.repository.save(entity);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
data: Partial<CalendarChannelEntity>,
|
||||
): Promise<CalendarChannelDTO> {
|
||||
async update({
|
||||
id,
|
||||
workspaceId,
|
||||
data,
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
data: Partial<CalendarChannelEntity>;
|
||||
}): Promise<CalendarChannelDTO> {
|
||||
await this.repository.update(
|
||||
{ id, workspaceId },
|
||||
data as Record<string, unknown>,
|
||||
@@ -65,13 +170,19 @@ export class CalendarChannelMetadataService {
|
||||
return this.repository.findOneOrFail({ where: { id, workspaceId } });
|
||||
}
|
||||
|
||||
async delete(id: string, workspaceId: string): Promise<CalendarChannelDTO> {
|
||||
const entity = await this.repository.findOneOrFail({
|
||||
async delete({
|
||||
id,
|
||||
workspaceId,
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
}): Promise<CalendarChannelDTO> {
|
||||
const calendarChannel = await this.repository.findOneOrFail({
|
||||
where: { id, workspaceId },
|
||||
});
|
||||
|
||||
await this.repository.delete({ id, workspaceId });
|
||||
|
||||
return entity;
|
||||
return calendarChannel;
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -7,6 +7,7 @@ import { CustomException } from 'src/utils/custom-exception';
|
||||
export enum CalendarChannelExceptionCode {
|
||||
CALENDAR_CHANNEL_NOT_FOUND = 'CALENDAR_CHANNEL_NOT_FOUND',
|
||||
INVALID_CALENDAR_CHANNEL_INPUT = 'INVALID_CALENDAR_CHANNEL_INPUT',
|
||||
CALENDAR_CHANNEL_OWNERSHIP_VIOLATION = 'CALENDAR_CHANNEL_OWNERSHIP_VIOLATION',
|
||||
}
|
||||
|
||||
const getCalendarChannelExceptionUserFriendlyMessage = (
|
||||
@@ -17,6 +18,8 @@ const getCalendarChannelExceptionUserFriendlyMessage = (
|
||||
return msg`Calendar channel not found.`;
|
||||
case CalendarChannelExceptionCode.INVALID_CALENDAR_CHANNEL_INPUT:
|
||||
return msg`Invalid calendar channel input.`;
|
||||
case CalendarChannelExceptionCode.CALENDAR_CHANNEL_OWNERSHIP_VIOLATION:
|
||||
return msg`You do not have access to this calendar channel.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
|
||||
+33
-2
@@ -96,10 +96,41 @@ export class CalendarChannelDataAccessService {
|
||||
)
|
||||
: await this.toCoreWhere(workspaceId, where);
|
||||
|
||||
return this.coreRepository.findOne({
|
||||
const requestedRelations =
|
||||
(options.relations as string[] | undefined)?.slice() ?? [];
|
||||
|
||||
const needsConnectedAccount =
|
||||
requestedRelations.includes('connectedAccount');
|
||||
|
||||
const coreRelations = requestedRelations.filter(
|
||||
(r) => r !== 'connectedAccount',
|
||||
);
|
||||
|
||||
const result = await this.coreRepository.findOne({
|
||||
...options,
|
||||
where: coreWhere,
|
||||
} as FindOneOptions<CalendarChannelEntity>) as unknown as Promise<CalendarChannelWorkspaceEntity | null>;
|
||||
relations: coreRelations,
|
||||
} as FindOneOptions<CalendarChannelEntity>);
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workspaceResult =
|
||||
result as unknown as CalendarChannelWorkspaceEntity;
|
||||
|
||||
if (needsConnectedAccount) {
|
||||
const connectedAccount =
|
||||
await this.connectedAccountDataAccessService.findOne(workspaceId, {
|
||||
where: { id: result.connectedAccountId },
|
||||
});
|
||||
|
||||
if (connectedAccount) {
|
||||
workspaceResult.connectedAccount = connectedAccount;
|
||||
}
|
||||
}
|
||||
|
||||
return workspaceResult;
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
+1
-3
@@ -59,9 +59,7 @@ export class CalendarChannelDTO {
|
||||
@Field()
|
||||
isSyncEnabled: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field(() => String, { nullable: true })
|
||||
@HideField()
|
||||
syncCursor: string | null;
|
||||
|
||||
@IsDateString()
|
||||
|
||||
+4
-2
@@ -49,17 +49,19 @@ export class CalendarChannelEntity extends WorkspaceRelatedEntity {
|
||||
})
|
||||
visibility: CalendarChannelVisibility;
|
||||
|
||||
@Column({ type: 'boolean', nullable: false })
|
||||
@Column({ type: 'boolean', nullable: false, default: true })
|
||||
isContactAutoCreationEnabled: boolean;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: CalendarChannelContactAutoCreationPolicy,
|
||||
nullable: false,
|
||||
default:
|
||||
CalendarChannelContactAutoCreationPolicy.AS_PARTICIPANT_AND_ORGANIZER,
|
||||
})
|
||||
contactAutoCreationPolicy: CalendarChannelContactAutoCreationPolicy;
|
||||
|
||||
@Column({ type: 'boolean', nullable: false })
|
||||
@Column({ type: 'boolean', nullable: false, default: true })
|
||||
isSyncEnabled: boolean;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
|
||||
+25
-45
@@ -1,22 +1,21 @@
|
||||
import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import {
|
||||
FeatureFlagGuard,
|
||||
RequireFeatureFlag,
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CalendarChannelMetadataService } from 'src/engine/metadata-modules/calendar-channel/calendar-channel-metadata.service';
|
||||
import { CalendarChannelDTO } from 'src/engine/metadata-modules/calendar-channel/dtos/calendar-channel.dto';
|
||||
import { CreateCalendarChannelInput } from 'src/engine/metadata-modules/calendar-channel/dtos/create-calendar-channel.input';
|
||||
import { UpdateCalendarChannelInput } from 'src/engine/metadata-modules/calendar-channel/dtos/update-calendar-channel.input';
|
||||
import { CalendarChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/calendar-channel/interceptors/calendar-channel-graphql-api-exception.interceptor';
|
||||
|
||||
@@ -29,10 +28,11 @@ export class CalendarChannelResolver {
|
||||
) {}
|
||||
|
||||
@Query(() => [CalendarChannelDTO])
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async calendarChannels(
|
||||
async myCalendarChannels(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
@Args('connectedAccountId', {
|
||||
type: () => UUIDScalarType,
|
||||
nullable: true,
|
||||
@@ -40,59 +40,39 @@ export class CalendarChannelResolver {
|
||||
connectedAccountId?: string,
|
||||
): Promise<CalendarChannelDTO[]> {
|
||||
if (connectedAccountId) {
|
||||
return this.calendarChannelMetadataService.findByConnectedAccountId(
|
||||
connectedAccountId,
|
||||
workspace.id,
|
||||
return this.calendarChannelMetadataService.findByConnectedAccountIdForUser(
|
||||
{
|
||||
connectedAccountId,
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.calendarChannelMetadataService.findAll(workspace.id);
|
||||
}
|
||||
|
||||
@Query(() => CalendarChannelDTO, { nullable: true })
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async calendarChannel(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<CalendarChannelDTO | null> {
|
||||
return this.calendarChannelMetadataService.findById(id, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => CalendarChannelDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async createCalendarChannel(
|
||||
@Args('input') input: CreateCalendarChannelInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<CalendarChannelDTO> {
|
||||
return this.calendarChannelMetadataService.create({
|
||||
...input,
|
||||
return this.calendarChannelMetadataService.findByUserWorkspaceId({
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => CalendarChannelDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async updateCalendarChannel(
|
||||
@Args('input') input: UpdateCalendarChannelInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
): Promise<CalendarChannelDTO> {
|
||||
return this.calendarChannelMetadataService.update(
|
||||
input.id,
|
||||
workspace.id,
|
||||
input.update,
|
||||
);
|
||||
}
|
||||
await this.calendarChannelMetadataService.verifyOwnership({
|
||||
id: input.id,
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
@Mutation(() => CalendarChannelDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async deleteCalendarChannel(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<CalendarChannelDTO> {
|
||||
return this.calendarChannelMetadataService.delete(id, workspace.id);
|
||||
return this.calendarChannelMetadataService.update({
|
||||
id: input.id,
|
||||
workspaceId: workspace.id,
|
||||
data: input.update,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+17
@@ -1,6 +1,7 @@
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
ForbiddenError,
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
@@ -8,6 +9,10 @@ import {
|
||||
CalendarChannelException,
|
||||
CalendarChannelExceptionCode,
|
||||
} from 'src/engine/metadata-modules/calendar-channel/calendar-channel.exception';
|
||||
import {
|
||||
ConnectedAccountException,
|
||||
ConnectedAccountExceptionCode,
|
||||
} from 'src/engine/metadata-modules/connected-account/connected-account.exception';
|
||||
|
||||
export const calendarChannelGraphqlApiExceptionHandler = (error: Error) => {
|
||||
if (error instanceof CalendarChannelException) {
|
||||
@@ -16,11 +21,23 @@ export const calendarChannelGraphqlApiExceptionHandler = (error: Error) => {
|
||||
throw new NotFoundError(error);
|
||||
case CalendarChannelExceptionCode.INVALID_CALENDAR_CHANNEL_INPUT:
|
||||
throw new UserInputError(error);
|
||||
case CalendarChannelExceptionCode.CALENDAR_CHANNEL_OWNERSHIP_VIOLATION:
|
||||
throw new ForbiddenError(error);
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof ConnectedAccountException) {
|
||||
switch (error.code) {
|
||||
case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_OWNERSHIP_VIOLATION:
|
||||
case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND:
|
||||
throw new ForbiddenError(error);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
|
||||
+99
-13
@@ -1,8 +1,12 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
ConnectedAccountException,
|
||||
ConnectedAccountExceptionCode,
|
||||
} from 'src/engine/metadata-modules/connected-account/connected-account.exception';
|
||||
import { ConnectedAccountDTO } from 'src/engine/metadata-modules/connected-account/dtos/connected-account.dto';
|
||||
import { ConnectedAccountEntity } from 'src/engine/metadata-modules/connected-account/entities/connected-account.entity';
|
||||
|
||||
@@ -17,13 +21,85 @@ export class ConnectedAccountMetadataService {
|
||||
return this.repository.find({ where: { workspaceId } });
|
||||
}
|
||||
|
||||
async findById(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
): Promise<ConnectedAccountDTO | null> {
|
||||
async findByUserWorkspaceId({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
}: {
|
||||
userWorkspaceId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<ConnectedAccountDTO[]> {
|
||||
return this.repository.find({
|
||||
where: { userWorkspaceId, workspaceId },
|
||||
});
|
||||
}
|
||||
|
||||
async findById({
|
||||
id,
|
||||
workspaceId,
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
}): Promise<ConnectedAccountDTO | null> {
|
||||
return this.repository.findOne({ where: { id, workspaceId } });
|
||||
}
|
||||
|
||||
async findByIds({
|
||||
ids,
|
||||
workspaceId,
|
||||
}: {
|
||||
ids: string[];
|
||||
workspaceId: string;
|
||||
}): Promise<ConnectedAccountDTO[]> {
|
||||
return this.repository.find({
|
||||
where: { id: In(ids), workspaceId },
|
||||
});
|
||||
}
|
||||
|
||||
async verifyOwnership({
|
||||
id,
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
}: {
|
||||
id: string;
|
||||
userWorkspaceId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<ConnectedAccountEntity> {
|
||||
const connectedAccount = await this.repository.findOne({
|
||||
where: { id, workspaceId },
|
||||
});
|
||||
|
||||
if (!connectedAccount) {
|
||||
throw new ConnectedAccountException(
|
||||
`Connected account ${id} not found`,
|
||||
ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
if (connectedAccount.userWorkspaceId !== userWorkspaceId) {
|
||||
throw new ConnectedAccountException(
|
||||
`Connected account ${id} does not belong to user workspace ${userWorkspaceId}`,
|
||||
ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_OWNERSHIP_VIOLATION,
|
||||
);
|
||||
}
|
||||
|
||||
return connectedAccount;
|
||||
}
|
||||
|
||||
async getUserConnectedAccountIds({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
}: {
|
||||
userWorkspaceId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<string[]> {
|
||||
const accounts = await this.repository.find({
|
||||
where: { userWorkspaceId, workspaceId },
|
||||
select: ['id'],
|
||||
});
|
||||
|
||||
return accounts.map((account) => account.id);
|
||||
}
|
||||
|
||||
async create(
|
||||
data: Partial<ConnectedAccountEntity> & {
|
||||
workspaceId: string;
|
||||
@@ -37,11 +113,15 @@ export class ConnectedAccountMetadataService {
|
||||
return this.repository.save(entity);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
data: Partial<ConnectedAccountEntity>,
|
||||
): Promise<ConnectedAccountDTO> {
|
||||
async update({
|
||||
id,
|
||||
workspaceId,
|
||||
data,
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
data: Partial<ConnectedAccountEntity>;
|
||||
}): Promise<ConnectedAccountDTO> {
|
||||
await this.repository.update(
|
||||
{ id, workspaceId },
|
||||
data as Record<string, unknown>,
|
||||
@@ -50,13 +130,19 @@ export class ConnectedAccountMetadataService {
|
||||
return this.repository.findOneOrFail({ where: { id, workspaceId } });
|
||||
}
|
||||
|
||||
async delete(id: string, workspaceId: string): Promise<ConnectedAccountDTO> {
|
||||
const entity = await this.repository.findOneOrFail({
|
||||
async delete({
|
||||
id,
|
||||
workspaceId,
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
}): Promise<ConnectedAccountDTO> {
|
||||
const connectedAccount = await this.repository.findOneOrFail({
|
||||
where: { id, workspaceId },
|
||||
});
|
||||
|
||||
await this.repository.delete({ id, workspaceId });
|
||||
|
||||
return entity;
|
||||
return connectedAccount;
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -7,6 +7,7 @@ import { CustomException } from 'src/utils/custom-exception';
|
||||
export enum ConnectedAccountExceptionCode {
|
||||
CONNECTED_ACCOUNT_NOT_FOUND = 'CONNECTED_ACCOUNT_NOT_FOUND',
|
||||
INVALID_CONNECTED_ACCOUNT_INPUT = 'INVALID_CONNECTED_ACCOUNT_INPUT',
|
||||
CONNECTED_ACCOUNT_OWNERSHIP_VIOLATION = 'CONNECTED_ACCOUNT_OWNERSHIP_VIOLATION',
|
||||
}
|
||||
|
||||
const getConnectedAccountExceptionUserFriendlyMessage = (
|
||||
@@ -17,6 +18,8 @@ const getConnectedAccountExceptionUserFriendlyMessage = (
|
||||
return msg`Connected account not found.`;
|
||||
case ConnectedAccountExceptionCode.INVALID_CONNECTED_ACCOUNT_INPUT:
|
||||
return msg`Invalid connected account input.`;
|
||||
case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_OWNERSHIP_VIOLATION:
|
||||
return msg`You do not have access to this connected account.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
|
||||
+9
-3
@@ -79,9 +79,15 @@ export class ConnectedAccountDataAccessService {
|
||||
const coreData: Record<string, unknown> = { ...rest };
|
||||
|
||||
if (handleAliases !== undefined) {
|
||||
coreData.handleAliases = isNonEmptyString(handleAliases)
|
||||
? handleAliases.split(',').map((alias: string) => alias.trim())
|
||||
: null;
|
||||
if (Array.isArray(handleAliases)) {
|
||||
coreData.handleAliases = handleAliases;
|
||||
} else if (isNonEmptyString(handleAliases)) {
|
||||
coreData.handleAliases = handleAliases
|
||||
.split(',')
|
||||
.map((alias: string) => alias.trim());
|
||||
} else {
|
||||
coreData.handleAliases = null;
|
||||
}
|
||||
}
|
||||
|
||||
if (accountOwnerId !== undefined) {
|
||||
|
||||
+4
-11
@@ -8,7 +8,6 @@ import {
|
||||
IsString,
|
||||
IsUUID,
|
||||
} from 'class-validator';
|
||||
import GraphQLJSON from 'graphql-type-json';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@@ -29,14 +28,10 @@ export class ConnectedAccountDTO {
|
||||
@Field()
|
||||
provider: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field(() => String, { nullable: true })
|
||||
@HideField()
|
||||
accessToken: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field(() => String, { nullable: true })
|
||||
@HideField()
|
||||
refreshToken: string | null;
|
||||
|
||||
@IsDateString()
|
||||
@@ -59,8 +54,7 @@ export class ConnectedAccountDTO {
|
||||
@Field(() => [String], { nullable: true })
|
||||
scopes: string[] | null;
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@HideField()
|
||||
connectionParameters: Record<string, unknown> | null;
|
||||
|
||||
@IsDateString()
|
||||
@@ -68,8 +62,7 @@ export class ConnectedAccountDTO {
|
||||
@Field(() => Date, { nullable: true })
|
||||
lastSignedInAt: Date | null;
|
||||
|
||||
@IsOptional()
|
||||
@Field(() => GraphQLJSON, { nullable: true })
|
||||
@HideField()
|
||||
oidcTokenClaims: Record<string, unknown> | null;
|
||||
|
||||
@IsUUID()
|
||||
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
import { Field, InputType } from '@nestjs/graphql';
|
||||
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class UpdateConnectedAccountInputUpdates {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
accessToken?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
refreshToken?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@Field(() => [String], { nullable: true })
|
||||
handleAliases?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@Field(() => [String], { nullable: true })
|
||||
scopes?: string[];
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class UpdateConnectedAccountInput {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
@Field(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Type(() => UpdateConnectedAccountInputUpdates)
|
||||
@ValidateNested()
|
||||
@Field(() => UpdateConnectedAccountInputUpdates)
|
||||
update: UpdateConnectedAccountInputUpdates;
|
||||
}
|
||||
+27
-41
@@ -7,17 +7,17 @@ import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import {
|
||||
FeatureFlagGuard,
|
||||
RequireFeatureFlag,
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service';
|
||||
import { ConnectedAccountDTO } from 'src/engine/metadata-modules/connected-account/dtos/connected-account.dto';
|
||||
import { CreateConnectedAccountInput } from 'src/engine/metadata-modules/connected-account/dtos/create-connected-account.input';
|
||||
import { UpdateConnectedAccountInput } from 'src/engine/metadata-modules/connected-account/dtos/update-connected-account.input';
|
||||
import { ConnectedAccountGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/connected-account/interceptors/connected-account-graphql-api-exception.interceptor';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
|
||||
@@ -28,6 +28,19 @@ export class ConnectedAccountResolver {
|
||||
private readonly connectedAccountMetadataService: ConnectedAccountMetadataService,
|
||||
) {}
|
||||
|
||||
@Query(() => [ConnectedAccountDTO])
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async myConnectedAccounts(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
): Promise<ConnectedAccountDTO[]> {
|
||||
return this.connectedAccountMetadataService.findByUserWorkspaceId({
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@Query(() => [ConnectedAccountDTO])
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
@@ -37,50 +50,23 @@ export class ConnectedAccountResolver {
|
||||
return this.connectedAccountMetadataService.findAll(workspace.id);
|
||||
}
|
||||
|
||||
@Query(() => ConnectedAccountDTO, { nullable: true })
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async connectedAccount(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ConnectedAccountDTO | null> {
|
||||
return this.connectedAccountMetadataService.findById(id, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => ConnectedAccountDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async createConnectedAccount(
|
||||
@Args('input') input: CreateConnectedAccountInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ConnectedAccountDTO> {
|
||||
return this.connectedAccountMetadataService.create({
|
||||
...input,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => ConnectedAccountDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async updateConnectedAccount(
|
||||
@Args('input') input: UpdateConnectedAccountInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<ConnectedAccountDTO> {
|
||||
return this.connectedAccountMetadataService.update(
|
||||
input.id,
|
||||
workspace.id,
|
||||
input.update,
|
||||
);
|
||||
}
|
||||
|
||||
@Mutation(() => ConnectedAccountDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async deleteConnectedAccount(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
): Promise<ConnectedAccountDTO> {
|
||||
return this.connectedAccountMetadataService.delete(id, workspace.id);
|
||||
await this.connectedAccountMetadataService.verifyOwnership({
|
||||
id,
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
return this.connectedAccountMetadataService.delete({
|
||||
id,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -1,6 +1,7 @@
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
ForbiddenError,
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
@@ -16,6 +17,8 @@ export const connectedAccountGraphqlApiExceptionHandler = (error: Error) => {
|
||||
throw new NotFoundError(error);
|
||||
case ConnectedAccountExceptionCode.INVALID_CONNECTED_ACCOUNT_INPUT:
|
||||
throw new UserInputError(error);
|
||||
case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_OWNERSHIP_VIOLATION:
|
||||
throw new ForbiddenError(error);
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
|
||||
+47
-2
@@ -96,10 +96,55 @@ export class MessageChannelDataAccessService {
|
||||
)
|
||||
: await this.toCoreWhere(workspaceId, where);
|
||||
|
||||
return this.coreRepository.findOne({
|
||||
const requestedRelations =
|
||||
(options.relations as string[] | undefined)?.slice() ?? [];
|
||||
|
||||
const needsConnectedAccount =
|
||||
requestedRelations.includes('connectedAccount');
|
||||
|
||||
const needsMessageFolders = requestedRelations.includes('messageFolders');
|
||||
|
||||
const coreRelations = requestedRelations.filter(
|
||||
(r) => r !== 'connectedAccount' && r !== 'messageFolders',
|
||||
);
|
||||
|
||||
const result = await this.coreRepository.findOne({
|
||||
...options,
|
||||
where: coreWhere,
|
||||
} as FindOneOptions<MessageChannelEntity>) as unknown as Promise<MessageChannelWorkspaceEntity | null>;
|
||||
relations: coreRelations,
|
||||
} as FindOneOptions<MessageChannelEntity>);
|
||||
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const workspaceResult =
|
||||
result as unknown as MessageChannelWorkspaceEntity;
|
||||
|
||||
if (needsConnectedAccount) {
|
||||
const connectedAccount =
|
||||
await this.connectedAccountDataAccessService.findOne(workspaceId, {
|
||||
where: { id: result.connectedAccountId },
|
||||
});
|
||||
|
||||
if (connectedAccount) {
|
||||
workspaceResult.connectedAccount = connectedAccount;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsMessageFolders) {
|
||||
const workspaceRepository =
|
||||
await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
const workspaceChannel = await workspaceRepository.findOne({
|
||||
where: { id: result.id },
|
||||
relations: ['messageFolders'],
|
||||
});
|
||||
|
||||
workspaceResult.messageFolders = workspaceChannel?.messageFolders ?? [];
|
||||
}
|
||||
|
||||
return workspaceResult;
|
||||
}
|
||||
|
||||
const workspaceRepository = await this.getWorkspaceRepository(workspaceId);
|
||||
|
||||
+1
-3
@@ -75,9 +75,7 @@ export class MessageChannelDTO {
|
||||
@Field()
|
||||
isSyncEnabled: boolean;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field(() => String, { nullable: true })
|
||||
@HideField()
|
||||
syncCursor: string | null;
|
||||
|
||||
@IsDateString()
|
||||
|
||||
+6
-4
@@ -46,13 +46,14 @@ export class MessageChannelEntity extends WorkspaceRelatedEntity {
|
||||
})
|
||||
type: MessageChannelType;
|
||||
|
||||
@Column({ type: 'boolean', nullable: false })
|
||||
@Column({ type: 'boolean', nullable: false, default: true })
|
||||
isContactAutoCreationEnabled: boolean;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: MessageChannelContactAutoCreationPolicy,
|
||||
nullable: false,
|
||||
default: MessageChannelContactAutoCreationPolicy.SENT,
|
||||
})
|
||||
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy;
|
||||
|
||||
@@ -60,13 +61,14 @@ export class MessageChannelEntity extends WorkspaceRelatedEntity {
|
||||
type: 'enum',
|
||||
enum: MessageFolderImportPolicy,
|
||||
nullable: false,
|
||||
default: MessageFolderImportPolicy.ALL_FOLDERS,
|
||||
})
|
||||
messageFolderImportPolicy: MessageFolderImportPolicy;
|
||||
|
||||
@Column({ type: 'boolean', nullable: false })
|
||||
@Column({ type: 'boolean', nullable: false, default: true })
|
||||
excludeNonProfessionalEmails: boolean;
|
||||
|
||||
@Column({ type: 'boolean', nullable: false })
|
||||
@Column({ type: 'boolean', nullable: false, default: true })
|
||||
excludeGroupEmails: boolean;
|
||||
|
||||
@Column({
|
||||
@@ -76,7 +78,7 @@ export class MessageChannelEntity extends WorkspaceRelatedEntity {
|
||||
})
|
||||
pendingGroupEmailsAction: MessageChannelPendingGroupEmailsAction;
|
||||
|
||||
@Column({ type: 'boolean', nullable: false })
|
||||
@Column({ type: 'boolean', nullable: false, default: true })
|
||||
isSyncEnabled: boolean;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
|
||||
+6
@@ -3,11 +3,14 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { ConnectedAccountMetadataModule } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.module';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import { MessageChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/message-channel/interceptors/message-channel-graphql-api-exception.interceptor';
|
||||
import { MessageChannelMetadataService } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.service';
|
||||
import { MessageChannelResolver } from 'src/engine/metadata-modules/message-channel/resolvers/message-channel.resolver';
|
||||
import { MessageFolderDataAccessModule } from 'src/engine/metadata-modules/message-folder/data-access/message-folder-data-access.module';
|
||||
import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module';
|
||||
import { MessagingImportManagerModule } from 'src/modules/messaging/message-import-manager/messaging-import-manager.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -15,6 +18,9 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
AuthModule,
|
||||
PermissionsModule,
|
||||
FeatureFlagModule,
|
||||
ConnectedAccountMetadataModule,
|
||||
MessageFolderDataAccessModule,
|
||||
MessagingImportManagerModule,
|
||||
],
|
||||
providers: [
|
||||
MessageChannelMetadataService,
|
||||
|
||||
+128
-17
@@ -1,7 +1,7 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import {
|
||||
MessageChannelSyncStage,
|
||||
@@ -9,36 +9,137 @@ import {
|
||||
MessageChannelVisibility,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service';
|
||||
import { MessageChannelDTO } from 'src/engine/metadata-modules/message-channel/dtos/message-channel.dto';
|
||||
import { MessageChannelEntity } from 'src/engine/metadata-modules/message-channel/entities/message-channel.entity';
|
||||
import {
|
||||
MessageChannelException,
|
||||
MessageChannelExceptionCode,
|
||||
} from 'src/engine/metadata-modules/message-channel/message-channel.exception';
|
||||
|
||||
@Injectable()
|
||||
export class MessageChannelMetadataService {
|
||||
constructor(
|
||||
@InjectRepository(MessageChannelEntity)
|
||||
private readonly repository: Repository<MessageChannelEntity>,
|
||||
private readonly connectedAccountMetadataService: ConnectedAccountMetadataService,
|
||||
) {}
|
||||
|
||||
async findAll(workspaceId: string): Promise<MessageChannelDTO[]> {
|
||||
return this.repository.find({ where: { workspaceId } });
|
||||
}
|
||||
|
||||
async findByConnectedAccountId(
|
||||
connectedAccountId: string,
|
||||
workspaceId: string,
|
||||
): Promise<MessageChannelDTO[]> {
|
||||
async findByUserWorkspaceId({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
}: {
|
||||
userWorkspaceId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<MessageChannelDTO[]> {
|
||||
const userAccountIds =
|
||||
await this.connectedAccountMetadataService.getUserConnectedAccountIds({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return this.findByConnectedAccountIds({
|
||||
connectedAccountIds: userAccountIds,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
async findByConnectedAccountIdForUser({
|
||||
connectedAccountId,
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
}: {
|
||||
connectedAccountId: string;
|
||||
userWorkspaceId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<MessageChannelDTO[]> {
|
||||
await this.connectedAccountMetadataService.verifyOwnership({
|
||||
id: connectedAccountId,
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return this.findByConnectedAccountId({ connectedAccountId, workspaceId });
|
||||
}
|
||||
|
||||
async findByConnectedAccountId({
|
||||
connectedAccountId,
|
||||
workspaceId,
|
||||
}: {
|
||||
connectedAccountId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<MessageChannelDTO[]> {
|
||||
return this.repository.find({
|
||||
where: { connectedAccountId, workspaceId },
|
||||
});
|
||||
}
|
||||
|
||||
async findById(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
): Promise<MessageChannelDTO | null> {
|
||||
async findByConnectedAccountIds({
|
||||
connectedAccountIds,
|
||||
workspaceId,
|
||||
}: {
|
||||
connectedAccountIds: string[];
|
||||
workspaceId: string;
|
||||
}): Promise<MessageChannelDTO[]> {
|
||||
if (connectedAccountIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.repository.find({
|
||||
where: { connectedAccountId: In(connectedAccountIds), workspaceId },
|
||||
});
|
||||
}
|
||||
|
||||
async findById({
|
||||
id,
|
||||
workspaceId,
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
}): Promise<MessageChannelDTO | null> {
|
||||
return this.repository.findOne({ where: { id, workspaceId } });
|
||||
}
|
||||
|
||||
async verifyOwnership({
|
||||
id,
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
}: {
|
||||
id: string;
|
||||
userWorkspaceId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<MessageChannelEntity> {
|
||||
const messageChannel = await this.repository.findOne({
|
||||
where: { id, workspaceId },
|
||||
});
|
||||
|
||||
if (!messageChannel) {
|
||||
throw new MessageChannelException(
|
||||
`Message channel ${id} not found`,
|
||||
MessageChannelExceptionCode.MESSAGE_CHANNEL_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const userAccountIds =
|
||||
await this.connectedAccountMetadataService.getUserConnectedAccountIds({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (!userAccountIds.includes(messageChannel.connectedAccountId)) {
|
||||
throw new MessageChannelException(
|
||||
`Message channel ${id} does not belong to user workspace ${userWorkspaceId}`,
|
||||
MessageChannelExceptionCode.MESSAGE_CHANNEL_OWNERSHIP_VIOLATION,
|
||||
);
|
||||
}
|
||||
|
||||
return messageChannel;
|
||||
}
|
||||
|
||||
async create(
|
||||
data: Partial<MessageChannelEntity> & {
|
||||
workspaceId: string;
|
||||
@@ -54,11 +155,15 @@ export class MessageChannelMetadataService {
|
||||
return this.repository.save(entity);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
data: Partial<MessageChannelEntity>,
|
||||
): Promise<MessageChannelDTO> {
|
||||
async update({
|
||||
id,
|
||||
workspaceId,
|
||||
data,
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
data: Partial<MessageChannelEntity>;
|
||||
}): Promise<MessageChannelDTO> {
|
||||
await this.repository.update(
|
||||
{ id, workspaceId },
|
||||
data as Record<string, unknown>,
|
||||
@@ -67,13 +172,19 @@ export class MessageChannelMetadataService {
|
||||
return this.repository.findOneOrFail({ where: { id, workspaceId } });
|
||||
}
|
||||
|
||||
async delete(id: string, workspaceId: string): Promise<MessageChannelDTO> {
|
||||
const entity = await this.repository.findOneOrFail({
|
||||
async delete({
|
||||
id,
|
||||
workspaceId,
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
}): Promise<MessageChannelDTO> {
|
||||
const messageChannel = await this.repository.findOneOrFail({
|
||||
where: { id, workspaceId },
|
||||
});
|
||||
|
||||
await this.repository.delete({ id, workspaceId });
|
||||
|
||||
return entity;
|
||||
return messageChannel;
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -7,6 +7,7 @@ import { CustomException } from 'src/utils/custom-exception';
|
||||
export enum MessageChannelExceptionCode {
|
||||
MESSAGE_CHANNEL_NOT_FOUND = 'MESSAGE_CHANNEL_NOT_FOUND',
|
||||
INVALID_MESSAGE_CHANNEL_INPUT = 'INVALID_MESSAGE_CHANNEL_INPUT',
|
||||
MESSAGE_CHANNEL_OWNERSHIP_VIOLATION = 'MESSAGE_CHANNEL_OWNERSHIP_VIOLATION',
|
||||
}
|
||||
|
||||
const getMessageChannelExceptionUserFriendlyMessage = (
|
||||
@@ -17,6 +18,8 @@ const getMessageChannelExceptionUserFriendlyMessage = (
|
||||
return msg`Message channel not found.`;
|
||||
case MessageChannelExceptionCode.INVALID_MESSAGE_CHANNEL_INPUT:
|
||||
return msg`Invalid message channel input.`;
|
||||
case MessageChannelExceptionCode.MESSAGE_CHANNEL_OWNERSHIP_VIOLATION:
|
||||
return msg`You do not have access to this message channel.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
|
||||
+82
-45
@@ -1,24 +1,37 @@
|
||||
import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import {
|
||||
FeatureFlagGuard,
|
||||
RequireFeatureFlag,
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreateMessageChannelInput } from 'src/engine/metadata-modules/message-channel/dtos/create-message-channel.input';
|
||||
import { MessageChannelDTO } from 'src/engine/metadata-modules/message-channel/dtos/message-channel.dto';
|
||||
import { UpdateMessageChannelInput } from 'src/engine/metadata-modules/message-channel/dtos/update-message-channel.input';
|
||||
import {
|
||||
MessageChannelException,
|
||||
MessageChannelExceptionCode,
|
||||
} from 'src/engine/metadata-modules/message-channel/message-channel.exception';
|
||||
import { MessageChannelGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/message-channel/interceptors/message-channel-graphql-api-exception.interceptor';
|
||||
import { MessageChannelMetadataService } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.service';
|
||||
import {
|
||||
MessageChannelPendingGroupEmailsAction,
|
||||
MessageChannelSyncStage,
|
||||
type MessageChannelWorkspaceEntity,
|
||||
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
|
||||
import { MessageFolderPendingSyncAction } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
|
||||
import { MessageFolderDataAccessService } from 'src/engine/metadata-modules/message-folder/data-access/services/message-folder-data-access.service';
|
||||
import { MessagingProcessGroupEmailActionsService } from 'src/modules/messaging/message-import-manager/services/messaging-process-group-email-actions.service';
|
||||
import { Not } from 'typeorm';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard, FeatureFlagGuard)
|
||||
@UseInterceptors(MessageChannelGraphqlApiExceptionInterceptor)
|
||||
@@ -26,13 +39,16 @@ import { MessageChannelMetadataService } from 'src/engine/metadata-modules/messa
|
||||
export class MessageChannelResolver {
|
||||
constructor(
|
||||
private readonly messageChannelMetadataService: MessageChannelMetadataService,
|
||||
private readonly messageFolderDataAccessService: MessageFolderDataAccessService,
|
||||
private readonly messagingProcessGroupEmailActionsService: MessagingProcessGroupEmailActionsService,
|
||||
) {}
|
||||
|
||||
@Query(() => [MessageChannelDTO])
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async messageChannels(
|
||||
async myMessageChannels(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
@Args('connectedAccountId', {
|
||||
type: () => UUIDScalarType,
|
||||
nullable: true,
|
||||
@@ -40,59 +56,80 @@ export class MessageChannelResolver {
|
||||
connectedAccountId?: string,
|
||||
): Promise<MessageChannelDTO[]> {
|
||||
if (connectedAccountId) {
|
||||
return this.messageChannelMetadataService.findByConnectedAccountId(
|
||||
connectedAccountId,
|
||||
workspace.id,
|
||||
return this.messageChannelMetadataService.findByConnectedAccountIdForUser(
|
||||
{
|
||||
connectedAccountId,
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
return this.messageChannelMetadataService.findAll(workspace.id);
|
||||
}
|
||||
|
||||
@Query(() => MessageChannelDTO, { nullable: true })
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async messageChannel(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<MessageChannelDTO | null> {
|
||||
return this.messageChannelMetadataService.findById(id, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => MessageChannelDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async createMessageChannel(
|
||||
@Args('input') input: CreateMessageChannelInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<MessageChannelDTO> {
|
||||
return this.messageChannelMetadataService.create({
|
||||
...input,
|
||||
return this.messageChannelMetadataService.findByUserWorkspaceId({
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => MessageChannelDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async updateMessageChannel(
|
||||
@Args('input') input: UpdateMessageChannelInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
): Promise<MessageChannelDTO> {
|
||||
return this.messageChannelMetadataService.update(
|
||||
input.id,
|
||||
workspace.id,
|
||||
input.update,
|
||||
);
|
||||
}
|
||||
const messageChannel =
|
||||
await this.messageChannelMetadataService.verifyOwnership({
|
||||
id: input.id,
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
@Mutation(() => MessageChannelDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async deleteMessageChannel(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<MessageChannelDTO> {
|
||||
return this.messageChannelMetadataService.delete(id, workspace.id);
|
||||
const isSyncOngoing =
|
||||
messageChannel.syncStage ===
|
||||
MessageChannelSyncStage.MESSAGE_LIST_FETCH_ONGOING;
|
||||
|
||||
const foldersWithPendingAction =
|
||||
await this.messageFolderDataAccessService.find(workspace.id, {
|
||||
messageChannelId: messageChannel.id,
|
||||
pendingSyncAction: Not(MessageFolderPendingSyncAction.NONE),
|
||||
});
|
||||
|
||||
const hasPendingGroupEmailsAction =
|
||||
messageChannel.pendingGroupEmailsAction !==
|
||||
MessageChannelPendingGroupEmailsAction.NONE;
|
||||
|
||||
if (
|
||||
isSyncOngoing &&
|
||||
(foldersWithPendingAction.length > 0 || hasPendingGroupEmailsAction)
|
||||
) {
|
||||
throw new MessageChannelException(
|
||||
'Cannot update message channel while sync is ongoing with pending actions',
|
||||
MessageChannelExceptionCode.INVALID_MESSAGE_CHANNEL_INPUT,
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
messageChannel.syncStage !==
|
||||
MessageChannelSyncStage.PENDING_CONFIGURATION &&
|
||||
isDefined(input.update.excludeGroupEmails) &&
|
||||
input.update.excludeGroupEmails !== messageChannel.excludeGroupEmails
|
||||
) {
|
||||
// Service expects WorkspaceEntity type but only reads .id
|
||||
await this.messagingProcessGroupEmailActionsService.markMessageChannelAsPendingGroupEmailsAction(
|
||||
messageChannel as unknown as MessageChannelWorkspaceEntity,
|
||||
workspace.id,
|
||||
input.update.excludeGroupEmails
|
||||
? MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_DELETION
|
||||
: MessageChannelPendingGroupEmailsAction.GROUP_EMAILS_IMPORT,
|
||||
);
|
||||
}
|
||||
|
||||
return this.messageChannelMetadataService.update({
|
||||
id: input.id,
|
||||
workspaceId: workspace.id,
|
||||
data: input.update,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+17
@@ -1,9 +1,14 @@
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
ForbiddenError,
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
ConnectedAccountException,
|
||||
ConnectedAccountExceptionCode,
|
||||
} from 'src/engine/metadata-modules/connected-account/connected-account.exception';
|
||||
import {
|
||||
MessageChannelException,
|
||||
MessageChannelExceptionCode,
|
||||
@@ -16,11 +21,23 @@ export const messageChannelGraphqlApiExceptionHandler = (error: Error) => {
|
||||
throw new NotFoundError(error);
|
||||
case MessageChannelExceptionCode.INVALID_MESSAGE_CHANNEL_INPUT:
|
||||
throw new UserInputError(error);
|
||||
case MessageChannelExceptionCode.MESSAGE_CHANNEL_OWNERSHIP_VIOLATION:
|
||||
throw new ForbiddenError(error);
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof ConnectedAccountException) {
|
||||
switch (error.code) {
|
||||
case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_OWNERSHIP_VIOLATION:
|
||||
case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND:
|
||||
throw new ForbiddenError(error);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
|
||||
+31
-4
@@ -2,6 +2,7 @@ import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
import { validate as uuidValidate } from 'uuid';
|
||||
import {
|
||||
type FindOneOptions,
|
||||
type FindOptionsWhere,
|
||||
@@ -32,6 +33,31 @@ export class MessageFolderDataAccessService {
|
||||
);
|
||||
}
|
||||
|
||||
// Workspace stores parentFolderId as an externalId (text),
|
||||
// core stores it as a uuid FK. Resolve during dual-write.
|
||||
private async toCore(
|
||||
workspaceId: string,
|
||||
data: Partial<MessageFolderWorkspaceEntity>,
|
||||
): Promise<Record<string, unknown>> {
|
||||
const coreData: Record<string, unknown> = { ...data, workspaceId };
|
||||
const parentFolderId = coreData.parentFolderId as string | null;
|
||||
|
||||
if (parentFolderId && !uuidValidate(parentFolderId)) {
|
||||
const parentFolder = await this.coreRepository.findOne({
|
||||
where: {
|
||||
workspaceId,
|
||||
messageChannelId: coreData.messageChannelId as string,
|
||||
externalId: parentFolderId,
|
||||
},
|
||||
select: ['id'],
|
||||
});
|
||||
|
||||
coreData.parentFolderId = parentFolder?.id ?? null;
|
||||
}
|
||||
|
||||
return coreData;
|
||||
}
|
||||
|
||||
async getWorkspaceRepository(workspaceId: string) {
|
||||
return this.globalWorkspaceOrmManager.getRepository<MessageFolderWorkspaceEntity>(
|
||||
workspaceId,
|
||||
@@ -94,10 +120,11 @@ export class MessageFolderDataAccessService {
|
||||
|
||||
if (await this.isMigrated(workspaceId)) {
|
||||
try {
|
||||
await this.coreRepository.save({
|
||||
...data,
|
||||
workspaceId,
|
||||
} as unknown as MessageFolderEntity);
|
||||
const coreData = await this.toCore(workspaceId, data);
|
||||
|
||||
await this.coreRepository.save(
|
||||
coreData as unknown as MessageFolderEntity,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Failed to dual-write messageFolder to core: ${error}`,
|
||||
|
||||
+1
-3
@@ -25,9 +25,7 @@ export class MessageFolderDTO {
|
||||
@Field(() => String, { nullable: true })
|
||||
name: string | null;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
@Field(() => String, { nullable: true })
|
||||
@HideField()
|
||||
syncCursor: string | null;
|
||||
|
||||
@IsBoolean()
|
||||
|
||||
+13
-18
@@ -3,38 +3,20 @@ import { Field, InputType } from '@nestjs/graphql';
|
||||
import { Type } from 'class-transformer';
|
||||
import {
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsNotEmpty,
|
||||
IsOptional,
|
||||
IsString,
|
||||
IsUUID,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
|
||||
@InputType()
|
||||
export class UpdateMessageFolderInputUpdates {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@Field({ nullable: true })
|
||||
syncCursor?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
@Field({ nullable: true })
|
||||
isSynced?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(MessageFolderPendingSyncAction)
|
||||
@Field(() => MessageFolderPendingSyncAction, { nullable: true })
|
||||
pendingSyncAction?: MessageFolderPendingSyncAction;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
@@ -49,3 +31,16 @@ export class UpdateMessageFolderInput {
|
||||
@Field(() => UpdateMessageFolderInputUpdates)
|
||||
update: UpdateMessageFolderInputUpdates;
|
||||
}
|
||||
|
||||
@InputType()
|
||||
export class UpdateMessageFoldersInput {
|
||||
@IsUUID('4', { each: true })
|
||||
@IsNotEmpty({ each: true })
|
||||
@Field(() => [UUIDScalarType])
|
||||
ids: string[];
|
||||
|
||||
@Type(() => UpdateMessageFolderInputUpdates)
|
||||
@ValidateNested()
|
||||
@Field(() => UpdateMessageFolderInputUpdates)
|
||||
update: UpdateMessageFolderInputUpdates;
|
||||
}
|
||||
|
||||
+1
@@ -41,6 +41,7 @@ export class MessageFolderEntity extends WorkspaceRelatedEntity {
|
||||
type: 'enum',
|
||||
enum: MessageFolderPendingSyncAction,
|
||||
nullable: false,
|
||||
default: MessageFolderPendingSyncAction.NONE,
|
||||
})
|
||||
pendingSyncAction: MessageFolderPendingSyncAction;
|
||||
|
||||
|
||||
+4
@@ -3,6 +3,8 @@ import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
|
||||
import { AuthModule } from 'src/engine/core-modules/auth/auth.module';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
import { ConnectedAccountMetadataModule } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.module';
|
||||
import { MessageChannelMetadataModule } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.module';
|
||||
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
import { MessageFolderGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/message-folder/interceptors/message-folder-graphql-api-exception.interceptor';
|
||||
import { MessageFolderMetadataService } from 'src/engine/metadata-modules/message-folder/message-folder-metadata.service';
|
||||
@@ -15,6 +17,8 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi
|
||||
AuthModule,
|
||||
PermissionsModule,
|
||||
FeatureFlagModule,
|
||||
ConnectedAccountMetadataModule,
|
||||
MessageChannelMetadataModule,
|
||||
],
|
||||
providers: [
|
||||
MessageFolderMetadataService,
|
||||
|
||||
+163
-17
@@ -1,40 +1,159 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
import { In, Repository } from 'typeorm';
|
||||
|
||||
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
|
||||
|
||||
import { ConnectedAccountMetadataService } from 'src/engine/metadata-modules/connected-account/connected-account-metadata.service';
|
||||
import { MessageFolderDTO } from 'src/engine/metadata-modules/message-folder/dtos/message-folder.dto';
|
||||
import { MessageFolderEntity } from 'src/engine/metadata-modules/message-folder/entities/message-folder.entity';
|
||||
import {
|
||||
MessageFolderException,
|
||||
MessageFolderExceptionCode,
|
||||
} from 'src/engine/metadata-modules/message-folder/message-folder.exception';
|
||||
import { MessageChannelMetadataService } from 'src/engine/metadata-modules/message-channel/message-channel-metadata.service';
|
||||
|
||||
@Injectable()
|
||||
export class MessageFolderMetadataService {
|
||||
constructor(
|
||||
@InjectRepository(MessageFolderEntity)
|
||||
private readonly repository: Repository<MessageFolderEntity>,
|
||||
private readonly messageChannelMetadataService: MessageChannelMetadataService,
|
||||
private readonly connectedAccountMetadataService: ConnectedAccountMetadataService,
|
||||
) {}
|
||||
|
||||
async findAll(workspaceId: string): Promise<MessageFolderDTO[]> {
|
||||
return this.repository.find({ where: { workspaceId } });
|
||||
}
|
||||
|
||||
async findByMessageChannelId(
|
||||
messageChannelId: string,
|
||||
workspaceId: string,
|
||||
): Promise<MessageFolderDTO[]> {
|
||||
async findByUserWorkspaceId({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
}: {
|
||||
userWorkspaceId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<MessageFolderDTO[]> {
|
||||
const userAccountIds =
|
||||
await this.connectedAccountMetadataService.getUserConnectedAccountIds({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const userChannels =
|
||||
await this.messageChannelMetadataService.findByConnectedAccountIds({
|
||||
connectedAccountIds: userAccountIds,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const userChannelIds = userChannels.map((channel) => channel.id);
|
||||
|
||||
return this.findByMessageChannelIds({
|
||||
messageChannelIds: userChannelIds,
|
||||
workspaceId,
|
||||
});
|
||||
}
|
||||
|
||||
async findByMessageChannelIdForUser({
|
||||
messageChannelId,
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
}: {
|
||||
messageChannelId: string;
|
||||
userWorkspaceId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<MessageFolderDTO[]> {
|
||||
await this.messageChannelMetadataService.verifyOwnership({
|
||||
id: messageChannelId,
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
return this.findByMessageChannelId({ messageChannelId, workspaceId });
|
||||
}
|
||||
|
||||
async findByMessageChannelId({
|
||||
messageChannelId,
|
||||
workspaceId,
|
||||
}: {
|
||||
messageChannelId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<MessageFolderDTO[]> {
|
||||
return this.repository.find({
|
||||
where: { messageChannelId, workspaceId },
|
||||
});
|
||||
}
|
||||
|
||||
async findById(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
): Promise<MessageFolderDTO | null> {
|
||||
async findByMessageChannelIds({
|
||||
messageChannelIds,
|
||||
workspaceId,
|
||||
}: {
|
||||
messageChannelIds: string[];
|
||||
workspaceId: string;
|
||||
}): Promise<MessageFolderDTO[]> {
|
||||
if (messageChannelIds.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return this.repository.find({
|
||||
where: { messageChannelId: In(messageChannelIds), workspaceId },
|
||||
});
|
||||
}
|
||||
|
||||
async findById({
|
||||
id,
|
||||
workspaceId,
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
}): Promise<MessageFolderDTO | null> {
|
||||
return this.repository.findOne({ where: { id, workspaceId } });
|
||||
}
|
||||
|
||||
async verifyOwnership({
|
||||
id,
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
}: {
|
||||
id: string;
|
||||
userWorkspaceId: string;
|
||||
workspaceId: string;
|
||||
}): Promise<MessageFolderEntity> {
|
||||
const messageFolder = await this.repository.findOne({
|
||||
where: { id, workspaceId },
|
||||
});
|
||||
|
||||
if (!messageFolder) {
|
||||
throw new MessageFolderException(
|
||||
`Message folder ${id} not found`,
|
||||
MessageFolderExceptionCode.MESSAGE_FOLDER_NOT_FOUND,
|
||||
);
|
||||
}
|
||||
|
||||
const userAccountIds =
|
||||
await this.connectedAccountMetadataService.getUserConnectedAccountIds({
|
||||
userWorkspaceId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
const messageChannel = await this.messageChannelMetadataService.findById({
|
||||
id: messageFolder.messageChannelId,
|
||||
workspaceId,
|
||||
});
|
||||
|
||||
if (
|
||||
!messageChannel ||
|
||||
!userAccountIds.includes(messageChannel.connectedAccountId)
|
||||
) {
|
||||
throw new MessageFolderException(
|
||||
`Message folder ${id} does not belong to user workspace ${userWorkspaceId}`,
|
||||
MessageFolderExceptionCode.MESSAGE_FOLDER_OWNERSHIP_VIOLATION,
|
||||
);
|
||||
}
|
||||
|
||||
return messageFolder;
|
||||
}
|
||||
|
||||
async create(
|
||||
data: Partial<MessageFolderEntity> & {
|
||||
workspaceId: string;
|
||||
@@ -47,11 +166,15 @@ export class MessageFolderMetadataService {
|
||||
return this.repository.save(entity);
|
||||
}
|
||||
|
||||
async update(
|
||||
id: string,
|
||||
workspaceId: string,
|
||||
data: Partial<MessageFolderEntity>,
|
||||
): Promise<MessageFolderDTO> {
|
||||
async update({
|
||||
id,
|
||||
workspaceId,
|
||||
data,
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
data: Partial<MessageFolderEntity>;
|
||||
}): Promise<MessageFolderDTO> {
|
||||
await this.repository.update(
|
||||
{ id, workspaceId },
|
||||
data as Record<string, unknown>,
|
||||
@@ -60,13 +183,36 @@ export class MessageFolderMetadataService {
|
||||
return this.repository.findOneOrFail({ where: { id, workspaceId } });
|
||||
}
|
||||
|
||||
async delete(id: string, workspaceId: string): Promise<MessageFolderDTO> {
|
||||
const entity = await this.repository.findOneOrFail({
|
||||
async updateMany({
|
||||
ids,
|
||||
workspaceId,
|
||||
data,
|
||||
}: {
|
||||
ids: string[];
|
||||
workspaceId: string;
|
||||
data: Partial<MessageFolderEntity>;
|
||||
}): Promise<MessageFolderDTO[]> {
|
||||
await this.repository.update(
|
||||
{ id: In(ids), workspaceId },
|
||||
data as Record<string, unknown>,
|
||||
);
|
||||
|
||||
return this.repository.find({ where: { id: In(ids), workspaceId } });
|
||||
}
|
||||
|
||||
async delete({
|
||||
id,
|
||||
workspaceId,
|
||||
}: {
|
||||
id: string;
|
||||
workspaceId: string;
|
||||
}): Promise<MessageFolderDTO> {
|
||||
const messageFolder = await this.repository.findOneOrFail({
|
||||
where: { id, workspaceId },
|
||||
});
|
||||
|
||||
await this.repository.delete({ id, workspaceId });
|
||||
|
||||
return entity;
|
||||
return messageFolder;
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -7,6 +7,7 @@ import { CustomException } from 'src/utils/custom-exception';
|
||||
export enum MessageFolderExceptionCode {
|
||||
MESSAGE_FOLDER_NOT_FOUND = 'MESSAGE_FOLDER_NOT_FOUND',
|
||||
INVALID_MESSAGE_FOLDER_INPUT = 'INVALID_MESSAGE_FOLDER_INPUT',
|
||||
MESSAGE_FOLDER_OWNERSHIP_VIOLATION = 'MESSAGE_FOLDER_OWNERSHIP_VIOLATION',
|
||||
}
|
||||
|
||||
const getMessageFolderExceptionUserFriendlyMessage = (
|
||||
@@ -17,6 +18,8 @@ const getMessageFolderExceptionUserFriendlyMessage = (
|
||||
return msg`Message folder not found.`;
|
||||
case MessageFolderExceptionCode.INVALID_MESSAGE_FOLDER_INPUT:
|
||||
return msg`Invalid message folder input.`;
|
||||
case MessageFolderExceptionCode.MESSAGE_FOLDER_OWNERSHIP_VIOLATION:
|
||||
return msg`You do not have access to this message folder.`;
|
||||
default:
|
||||
assertUnreachable(code);
|
||||
}
|
||||
|
||||
+49
-43
@@ -1,22 +1,24 @@
|
||||
import { UseGuards, UseInterceptors } from '@nestjs/common';
|
||||
import { Args, Mutation, Query } from '@nestjs/graphql';
|
||||
|
||||
import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { WorkspaceEntity } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthUserWorkspaceId } from 'src/engine/decorators/auth/auth-user-workspace-id.decorator';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import {
|
||||
FeatureFlagGuard,
|
||||
RequireFeatureFlag,
|
||||
} from 'src/engine/guards/feature-flag.guard';
|
||||
import { SettingsPermissionGuard } from 'src/engine/guards/settings-permission.guard';
|
||||
import { NoPermissionGuard } from 'src/engine/guards/no-permission.guard';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
import { CreateMessageFolderInput } from 'src/engine/metadata-modules/message-folder/dtos/create-message-folder.input';
|
||||
import { MessageFolderDTO } from 'src/engine/metadata-modules/message-folder/dtos/message-folder.dto';
|
||||
import { UpdateMessageFolderInput } from 'src/engine/metadata-modules/message-folder/dtos/update-message-folder.input';
|
||||
import {
|
||||
UpdateMessageFolderInput,
|
||||
UpdateMessageFoldersInput,
|
||||
} from 'src/engine/metadata-modules/message-folder/dtos/update-message-folder.input';
|
||||
import { MessageFolderGraphqlApiExceptionInterceptor } from 'src/engine/metadata-modules/message-folder/interceptors/message-folder-graphql-api-exception.interceptor';
|
||||
import { MessageFolderMetadataService } from 'src/engine/metadata-modules/message-folder/message-folder-metadata.service';
|
||||
|
||||
@@ -29,10 +31,11 @@ export class MessageFolderResolver {
|
||||
) {}
|
||||
|
||||
@Query(() => [MessageFolderDTO])
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async messageFolders(
|
||||
async myMessageFolders(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
@Args('messageChannelId', {
|
||||
type: () => UUIDScalarType,
|
||||
nullable: true,
|
||||
@@ -40,59 +43,62 @@ export class MessageFolderResolver {
|
||||
messageChannelId?: string,
|
||||
): Promise<MessageFolderDTO[]> {
|
||||
if (messageChannelId) {
|
||||
return this.messageFolderMetadataService.findByMessageChannelId(
|
||||
return this.messageFolderMetadataService.findByMessageChannelIdForUser({
|
||||
messageChannelId,
|
||||
workspace.id,
|
||||
);
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
return this.messageFolderMetadataService.findAll(workspace.id);
|
||||
}
|
||||
|
||||
@Query(() => MessageFolderDTO, { nullable: true })
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async messageFolder(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<MessageFolderDTO | null> {
|
||||
return this.messageFolderMetadataService.findById(id, workspace.id);
|
||||
}
|
||||
|
||||
@Mutation(() => MessageFolderDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async createMessageFolder(
|
||||
@Args('input') input: CreateMessageFolderInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<MessageFolderDTO> {
|
||||
return this.messageFolderMetadataService.create({
|
||||
...input,
|
||||
return this.messageFolderMetadataService.findByUserWorkspaceId({
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => MessageFolderDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async updateMessageFolder(
|
||||
@Args('input') input: UpdateMessageFolderInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
): Promise<MessageFolderDTO> {
|
||||
return this.messageFolderMetadataService.update(
|
||||
input.id,
|
||||
workspace.id,
|
||||
input.update,
|
||||
);
|
||||
await this.messageFolderMetadataService.verifyOwnership({
|
||||
id: input.id,
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
return this.messageFolderMetadataService.update({
|
||||
id: input.id,
|
||||
workspaceId: workspace.id,
|
||||
data: input.update,
|
||||
});
|
||||
}
|
||||
|
||||
@Mutation(() => MessageFolderDTO)
|
||||
@UseGuards(SettingsPermissionGuard(PermissionFlagType.CONNECTED_ACCOUNTS))
|
||||
@Mutation(() => [MessageFolderDTO])
|
||||
@UseGuards(NoPermissionGuard)
|
||||
@RequireFeatureFlag(FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED)
|
||||
async deleteMessageFolder(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
async updateMessageFolders(
|
||||
@Args('input') input: UpdateMessageFoldersInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
): Promise<MessageFolderDTO> {
|
||||
return this.messageFolderMetadataService.delete(id, workspace.id);
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string,
|
||||
): Promise<MessageFolderDTO[]> {
|
||||
await Promise.all(
|
||||
input.ids.map((id) =>
|
||||
this.messageFolderMetadataService.verifyOwnership({
|
||||
id,
|
||||
userWorkspaceId,
|
||||
workspaceId: workspace.id,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
return this.messageFolderMetadataService.updateMany({
|
||||
ids: input.ids,
|
||||
workspaceId: workspace.id,
|
||||
data: input.update,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
+31
@@ -1,9 +1,18 @@
|
||||
import { assertUnreachable } from 'twenty-shared/utils';
|
||||
|
||||
import {
|
||||
ForbiddenError,
|
||||
NotFoundError,
|
||||
UserInputError,
|
||||
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
|
||||
import {
|
||||
ConnectedAccountException,
|
||||
ConnectedAccountExceptionCode,
|
||||
} from 'src/engine/metadata-modules/connected-account/connected-account.exception';
|
||||
import {
|
||||
MessageChannelException,
|
||||
MessageChannelExceptionCode,
|
||||
} from 'src/engine/metadata-modules/message-channel/message-channel.exception';
|
||||
import {
|
||||
MessageFolderException,
|
||||
MessageFolderExceptionCode,
|
||||
@@ -16,11 +25,33 @@ export const messageFolderGraphqlApiExceptionHandler = (error: Error) => {
|
||||
throw new NotFoundError(error);
|
||||
case MessageFolderExceptionCode.INVALID_MESSAGE_FOLDER_INPUT:
|
||||
throw new UserInputError(error);
|
||||
case MessageFolderExceptionCode.MESSAGE_FOLDER_OWNERSHIP_VIOLATION:
|
||||
throw new ForbiddenError(error);
|
||||
default: {
|
||||
return assertUnreachable(error.code);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof MessageChannelException) {
|
||||
switch (error.code) {
|
||||
case MessageChannelExceptionCode.MESSAGE_CHANNEL_OWNERSHIP_VIOLATION:
|
||||
case MessageChannelExceptionCode.MESSAGE_CHANNEL_NOT_FOUND:
|
||||
throw new ForbiddenError(error);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (error instanceof ConnectedAccountException) {
|
||||
switch (error.code) {
|
||||
case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_OWNERSHIP_VIOLATION:
|
||||
case ConnectedAccountExceptionCode.CONNECTED_ACCOUNT_NOT_FOUND:
|
||||
throw new ForbiddenError(error);
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
throw error;
|
||||
};
|
||||
|
||||
+2
-1
@@ -20,7 +20,8 @@ export class MinimalMetadataResolver {
|
||||
@Query(() => MinimalMetadataDTO)
|
||||
async minimalMetadata(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
): Promise<MinimalMetadataDTO> {
|
||||
return this.minimalMetadataService.getMinimalMetadata(
|
||||
workspace.id,
|
||||
|
||||
+8
-4
@@ -44,7 +44,8 @@ export class NavigationMenuItemResolver {
|
||||
@UseGuards(NoPermissionGuard)
|
||||
async navigationMenuItems(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
): Promise<NavigationMenuItemDTO[]> {
|
||||
return await this.navigationMenuItemService.findAll({
|
||||
workspaceId: workspace.id,
|
||||
@@ -69,7 +70,8 @@ export class NavigationMenuItemResolver {
|
||||
async createNavigationMenuItem(
|
||||
@Args('input') input: CreateNavigationMenuItemInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
@AuthApiKey() apiKey: ApiKeyEntity | undefined,
|
||||
@Context() context: { req: { application?: ApplicationEntity } },
|
||||
): Promise<NavigationMenuItemDTO> {
|
||||
@@ -87,7 +89,8 @@ export class NavigationMenuItemResolver {
|
||||
async updateNavigationMenuItem(
|
||||
@Args('input') input: UpdateOneNavigationMenuItemInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
@AuthApiKey() apiKey: ApiKeyEntity | undefined,
|
||||
@Context() context: { req: { application?: ApplicationEntity } },
|
||||
): Promise<NavigationMenuItemDTO> {
|
||||
@@ -105,7 +108,8 @@ export class NavigationMenuItemResolver {
|
||||
async deleteNavigationMenuItem(
|
||||
@Args('id', { type: () => UUIDScalarType }) id: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
@AuthApiKey() apiKey: ApiKeyEntity | undefined,
|
||||
@Context() context: { req: { application?: ApplicationEntity } },
|
||||
): Promise<NavigationMenuItemDTO> {
|
||||
|
||||
+4
-2
@@ -56,7 +56,8 @@ export class ViewController {
|
||||
async findMany(
|
||||
@RequestLocale() locale: keyof typeof APP_LOCALES | undefined,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
@Query('objectMetadataId') objectMetadataId?: string,
|
||||
): Promise<ViewDTO[]> {
|
||||
const views = objectMetadataId
|
||||
@@ -134,7 +135,8 @@ export class ViewController {
|
||||
@Body() input: UpdateViewInput,
|
||||
@RequestLocale() locale: keyof typeof APP_LOCALES | undefined,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
): Promise<ViewDTO> {
|
||||
const updatedView = await this.viewService.updateOne({
|
||||
updateViewInput: {
|
||||
|
||||
@@ -96,7 +96,8 @@ export class ViewResolver {
|
||||
@UseGuards(CustomPermissionGuard)
|
||||
async getViews(
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
@Args('objectMetadataId', { type: () => String, nullable: true })
|
||||
objectMetadataId?: string,
|
||||
@Args('viewTypes', { type: () => [ViewType], nullable: true })
|
||||
@@ -138,7 +139,8 @@ export class ViewResolver {
|
||||
async createView(
|
||||
@Args('input') input: CreateViewInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
): Promise<ViewDTO> {
|
||||
const visibility = input.visibility ?? ViewVisibility.WORKSPACE;
|
||||
|
||||
@@ -157,7 +159,8 @@ export class ViewResolver {
|
||||
@Args('id', { type: () => String }) id: string,
|
||||
@Args('input') input: UpdateViewInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
): Promise<ViewDTO> {
|
||||
return await this.viewService.updateOne({
|
||||
updateViewInput: { ...input, id },
|
||||
|
||||
@@ -58,7 +58,8 @@ export class EventStreamResolver {
|
||||
@Args('eventStreamId') eventStreamId: string,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUser({ allowUndefined: true }) user: AuthContextUser | undefined,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
@AuthApiKey() apiKey: ApiKeyEntity | undefined,
|
||||
) {
|
||||
const eventStreamChannelId = eventStreamIdToChannelId(eventStreamId);
|
||||
@@ -124,7 +125,8 @@ export class EventStreamResolver {
|
||||
@Args('input') input: AddQuerySubscriptionInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUser({ allowUndefined: true }) user: AuthContextUser | undefined,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
@AuthApiKey() apiKey: ApiKeyEntity | undefined,
|
||||
): Promise<boolean> {
|
||||
const eventStreamChannelId = eventStreamIdToChannelId(input.eventStreamId);
|
||||
@@ -168,7 +170,8 @@ export class EventStreamResolver {
|
||||
@Args('input') input: RemoveQueryFromEventStreamInput,
|
||||
@AuthWorkspace() workspace: WorkspaceEntity,
|
||||
@AuthUser({ allowUndefined: true }) user: AuthContextUser | undefined,
|
||||
@AuthUserWorkspaceId() userWorkspaceId: string | undefined,
|
||||
@AuthUserWorkspaceId({ allowUndefined: true })
|
||||
userWorkspaceId: string | undefined,
|
||||
@AuthApiKey() apiKey: ApiKeyEntity | undefined,
|
||||
): Promise<boolean> {
|
||||
const eventStreamChannelId = eventStreamIdToChannelId(input.eventStreamId);
|
||||
|
||||
+3
@@ -12,6 +12,7 @@ import { seedAgents } from 'src/engine/workspace-manager/dev-seeder/core/utils/s
|
||||
import { seedApiKeys } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-api-keys.util';
|
||||
|
||||
import { seedFeatureFlags } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-feature-flags.util';
|
||||
import { seedMetadataEntities } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-metadata-entities.util';
|
||||
import { seedServerId } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-server-id.util';
|
||||
import { seedUserWorkspaces } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
|
||||
import { seedUsers } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-users.util';
|
||||
@@ -93,6 +94,8 @@ export const seedCoreSchema = async ({
|
||||
await seedBillingSubscriptions({ queryRunner, schemaName, workspaceId });
|
||||
}
|
||||
|
||||
await seedMetadataEntities({ queryRunner, schemaName, workspaceId });
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
} catch (error) {
|
||||
await queryRunner.rollbackTransaction();
|
||||
|
||||
+306
@@ -0,0 +1,306 @@
|
||||
import { type QueryRunner } from 'typeorm';
|
||||
|
||||
import {
|
||||
CalendarChannelVisibility,
|
||||
MessageChannelSyncStage,
|
||||
MessageChannelType,
|
||||
MessageChannelVisibility,
|
||||
MessageFolderPendingSyncAction,
|
||||
} from 'twenty-shared/types';
|
||||
|
||||
import {
|
||||
SEED_APPLE_WORKSPACE_ID,
|
||||
SEED_YCOMBINATOR_WORKSPACE_ID,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/core/constants/seeder-workspaces.constant';
|
||||
import { USER_WORKSPACE_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/core/utils/seed-user-workspaces.util';
|
||||
import { CALENDAR_CHANNEL_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/calendar-channel-data-seeds.constant';
|
||||
import { CONNECTED_ACCOUNT_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant';
|
||||
import { MESSAGE_CHANNEL_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/message-channel-data-seeds.constant';
|
||||
import { MESSAGE_FOLDER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/message-folder-data-seeds.constant';
|
||||
|
||||
type SeedMetadataEntitiesArgs = {
|
||||
queryRunner: QueryRunner;
|
||||
schemaName: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export const seedMetadataEntities = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedMetadataEntitiesArgs) => {
|
||||
if (
|
||||
workspaceId !== SEED_APPLE_WORKSPACE_ID &&
|
||||
workspaceId !== SEED_YCOMBINATOR_WORKSPACE_ID
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (workspaceId === SEED_YCOMBINATOR_WORKSPACE_ID) {
|
||||
return;
|
||||
}
|
||||
|
||||
await seedConnectedAccounts({ queryRunner, schemaName, workspaceId });
|
||||
await seedMessageChannels({ queryRunner, schemaName, workspaceId });
|
||||
await seedCalendarChannels({ queryRunner, schemaName, workspaceId });
|
||||
await seedMessageFolders({ queryRunner, schemaName, workspaceId });
|
||||
};
|
||||
|
||||
const seedConnectedAccounts = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedMetadataEntitiesArgs) => {
|
||||
const connectedAccounts = [
|
||||
{
|
||||
id: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM,
|
||||
handle: 'tim@apple.dev',
|
||||
provider: 'google',
|
||||
userWorkspaceId: USER_WORKSPACE_DATA_SEED_IDS.TIM,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY,
|
||||
handle: 'jony.ive@apple.dev',
|
||||
provider: 'google',
|
||||
userWorkspaceId: USER_WORKSPACE_DATA_SEED_IDS.JONY,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: CONNECTED_ACCOUNT_DATA_SEED_IDS.PHIL,
|
||||
handle: 'phil.schiler@apple.dev',
|
||||
provider: 'google',
|
||||
userWorkspaceId: USER_WORKSPACE_DATA_SEED_IDS.PHIL,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE,
|
||||
handle: 'jane.austen@apple.dev',
|
||||
provider: 'google',
|
||||
userWorkspaceId: USER_WORKSPACE_DATA_SEED_IDS.JANE,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE_DELETABLE,
|
||||
handle: 'jane-deletable@apple.dev',
|
||||
provider: 'google',
|
||||
userWorkspaceId: USER_WORKSPACE_DATA_SEED_IDS.JANE,
|
||||
workspaceId,
|
||||
},
|
||||
];
|
||||
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.connectedAccount`, [
|
||||
'id',
|
||||
'handle',
|
||||
'provider',
|
||||
'userWorkspaceId',
|
||||
'workspaceId',
|
||||
])
|
||||
.orIgnore()
|
||||
.values(connectedAccounts)
|
||||
.execute();
|
||||
};
|
||||
|
||||
const seedMessageChannels = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedMetadataEntitiesArgs) => {
|
||||
const messageChannels = [
|
||||
{
|
||||
id: MESSAGE_CHANNEL_DATA_SEED_IDS.TIM,
|
||||
handle: 'tim@apple.dev',
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
type: MessageChannelType.EMAIL,
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
||||
isContactAutoCreationEnabled: true,
|
||||
contactAutoCreationPolicy: 'SENT_AND_RECEIVED',
|
||||
messageFolderImportPolicy: 'ALL_FOLDERS',
|
||||
excludeNonProfessionalEmails: false,
|
||||
excludeGroupEmails: false,
|
||||
pendingGroupEmailsAction: 'NONE',
|
||||
isSyncEnabled: true,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: MESSAGE_CHANNEL_DATA_SEED_IDS.JONY,
|
||||
handle: 'jony.ive@apple.dev',
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
type: MessageChannelType.EMAIL,
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
||||
isContactAutoCreationEnabled: true,
|
||||
contactAutoCreationPolicy: 'SENT_AND_RECEIVED',
|
||||
messageFolderImportPolicy: 'ALL_FOLDERS',
|
||||
excludeNonProfessionalEmails: false,
|
||||
excludeGroupEmails: false,
|
||||
pendingGroupEmailsAction: 'NONE',
|
||||
isSyncEnabled: true,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: MESSAGE_CHANNEL_DATA_SEED_IDS.JANE,
|
||||
handle: 'jane.austen@apple.dev',
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
type: MessageChannelType.EMAIL,
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
||||
isContactAutoCreationEnabled: true,
|
||||
contactAutoCreationPolicy: 'SENT_AND_RECEIVED',
|
||||
messageFolderImportPolicy: 'ALL_FOLDERS',
|
||||
excludeNonProfessionalEmails: false,
|
||||
excludeGroupEmails: false,
|
||||
pendingGroupEmailsAction: 'NONE',
|
||||
isSyncEnabled: true,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE,
|
||||
workspaceId,
|
||||
},
|
||||
];
|
||||
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.messageChannel`, [
|
||||
'id',
|
||||
'handle',
|
||||
'visibility',
|
||||
'type',
|
||||
'syncStage',
|
||||
'isContactAutoCreationEnabled',
|
||||
'contactAutoCreationPolicy',
|
||||
'messageFolderImportPolicy',
|
||||
'excludeNonProfessionalEmails',
|
||||
'excludeGroupEmails',
|
||||
'pendingGroupEmailsAction',
|
||||
'isSyncEnabled',
|
||||
'connectedAccountId',
|
||||
'workspaceId',
|
||||
])
|
||||
.orIgnore()
|
||||
.values(messageChannels)
|
||||
.execute();
|
||||
};
|
||||
|
||||
const seedCalendarChannels = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedMetadataEntitiesArgs) => {
|
||||
const calendarChannels = [
|
||||
{
|
||||
id: CALENDAR_CHANNEL_DATA_SEED_IDS.TIM,
|
||||
handle: 'tim@apple.dev',
|
||||
visibility: CalendarChannelVisibility.METADATA,
|
||||
syncStage: 'CALENDAR_EVENT_LIST_FETCH_PENDING',
|
||||
isContactAutoCreationEnabled: true,
|
||||
contactAutoCreationPolicy: 'NONE',
|
||||
isSyncEnabled: true,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: CALENDAR_CHANNEL_DATA_SEED_IDS.JONY,
|
||||
handle: 'jony@apple.dev',
|
||||
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
syncStage: 'CALENDAR_EVENT_LIST_FETCH_PENDING',
|
||||
isContactAutoCreationEnabled: true,
|
||||
contactAutoCreationPolicy: 'NONE',
|
||||
isSyncEnabled: true,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY,
|
||||
workspaceId,
|
||||
},
|
||||
{
|
||||
id: CALENDAR_CHANNEL_DATA_SEED_IDS.JANE,
|
||||
handle: 'jane.austen@apple.dev',
|
||||
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
syncStage: 'CALENDAR_EVENT_LIST_FETCH_PENDING',
|
||||
isContactAutoCreationEnabled: true,
|
||||
contactAutoCreationPolicy: 'NONE',
|
||||
isSyncEnabled: true,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE,
|
||||
workspaceId,
|
||||
},
|
||||
];
|
||||
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.calendarChannel`, [
|
||||
'id',
|
||||
'handle',
|
||||
'visibility',
|
||||
'syncStage',
|
||||
'isContactAutoCreationEnabled',
|
||||
'contactAutoCreationPolicy',
|
||||
'isSyncEnabled',
|
||||
'connectedAccountId',
|
||||
'workspaceId',
|
||||
])
|
||||
.orIgnore()
|
||||
.values(calendarChannels)
|
||||
.execute();
|
||||
};
|
||||
|
||||
const seedMessageFolders = async ({
|
||||
queryRunner,
|
||||
schemaName,
|
||||
workspaceId,
|
||||
}: SeedMetadataEntitiesArgs) => {
|
||||
const messageFolders = [
|
||||
{
|
||||
id: MESSAGE_FOLDER_DATA_SEED_IDS.TIM_INBOX,
|
||||
name: 'INBOX',
|
||||
isSynced: true,
|
||||
isSentFolder: false,
|
||||
messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.TIM,
|
||||
workspaceId,
|
||||
pendingSyncAction: MessageFolderPendingSyncAction.NONE,
|
||||
},
|
||||
{
|
||||
id: MESSAGE_FOLDER_DATA_SEED_IDS.JONY_INBOX,
|
||||
name: 'INBOX',
|
||||
isSynced: true,
|
||||
isSentFolder: false,
|
||||
messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.JONY,
|
||||
workspaceId,
|
||||
pendingSyncAction: MessageFolderPendingSyncAction.NONE,
|
||||
},
|
||||
{
|
||||
id: MESSAGE_FOLDER_DATA_SEED_IDS.JANE_INBOX,
|
||||
name: 'INBOX',
|
||||
isSynced: true,
|
||||
isSentFolder: false,
|
||||
messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.JANE,
|
||||
workspaceId,
|
||||
pendingSyncAction: MessageFolderPendingSyncAction.NONE,
|
||||
},
|
||||
{
|
||||
id: MESSAGE_FOLDER_DATA_SEED_IDS.JANE_SENT,
|
||||
name: 'Sent',
|
||||
isSynced: true,
|
||||
isSentFolder: true,
|
||||
messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.JANE,
|
||||
workspaceId,
|
||||
pendingSyncAction: MessageFolderPendingSyncAction.NONE,
|
||||
},
|
||||
];
|
||||
|
||||
await queryRunner.manager
|
||||
.createQueryBuilder()
|
||||
.insert()
|
||||
.into(`${schemaName}.messageFolder`, [
|
||||
'id',
|
||||
'name',
|
||||
'isSynced',
|
||||
'isSentFolder',
|
||||
'messageChannelId',
|
||||
'workspaceId',
|
||||
'pendingSyncAction',
|
||||
])
|
||||
.orIgnore()
|
||||
.values(messageFolders)
|
||||
.execute();
|
||||
};
|
||||
+9
@@ -26,6 +26,7 @@ const GENERATE_CALENDAR_CHANNEL_IDS = (): Record<string, string> => {
|
||||
CHANNEL_IDS['TIM'] = '20202020-a40f-4faf-bb9f-c6f9945b8203';
|
||||
CHANNEL_IDS['JONY'] = '20202020-a40f-4faf-bb9f-c6f9945b8204';
|
||||
CHANNEL_IDS['PHIL'] = '20202020-a40f-4faf-bb9f-c6f9945b8205';
|
||||
CHANNEL_IDS['JANE'] = '20202020-a40f-4faf-bb9f-c6f9945b8208';
|
||||
CHANNEL_IDS['COMPANY_MAIN'] = '20202020-a40f-4faf-bb9f-c6f9945b8206';
|
||||
CHANNEL_IDS['TEAM_CALENDAR'] = '20202020-a40f-4faf-bb9f-c6f9945b8207';
|
||||
|
||||
@@ -59,6 +60,14 @@ export const CALENDAR_CHANNEL_DATA_SEEDS: CalendarChannelDataSeed[] = [
|
||||
isContactAutoCreationEnabled: true,
|
||||
isSyncEnabled: true,
|
||||
},
|
||||
{
|
||||
id: CALENDAR_CHANNEL_DATA_SEED_IDS.JANE,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE,
|
||||
handle: 'jane.austen@apple.dev',
|
||||
visibility: CalendarChannelVisibility.SHARE_EVERYTHING,
|
||||
isContactAutoCreationEnabled: true,
|
||||
isSyncEnabled: true,
|
||||
},
|
||||
{
|
||||
id: CALENDAR_CHANNEL_DATA_SEED_IDS.COMPANY_MAIN,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.TIM,
|
||||
|
||||
+26
@@ -31,6 +31,8 @@ export const CONNECTED_ACCOUNT_DATA_SEED_IDS = {
|
||||
TIM: '20202020-9ac0-4390-9a1a-ab4d2c4e1bb7',
|
||||
JONY: '20202020-0cc8-4d60-a3a4-803245698908',
|
||||
PHIL: '20202020-cafc-4323-908d-e5b42ad69fdf',
|
||||
JANE: '20202020-b5c7-46f0-bf5c-3f4e4b3f7c1a',
|
||||
JANE_DELETABLE: '20202020-d1e5-4a8f-9c3b-7f6d5e4c3b2a',
|
||||
};
|
||||
|
||||
export const CONNECTED_ACCOUNT_DATA_SEEDS: ConnectedAccountDataSeed[] = [
|
||||
@@ -70,4 +72,28 @@ export const CONNECTED_ACCOUNT_DATA_SEEDS: ConnectedAccountDataSeed[] = [
|
||||
provider: 'google',
|
||||
handle: 'phil.schiler@apple.dev',
|
||||
},
|
||||
{
|
||||
id: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
lastSyncHistoryId: 'exampleLastSyncHistory',
|
||||
accountOwnerId: WORKSPACE_MEMBER_DATA_SEED_IDS.JANE,
|
||||
refreshToken: 'exampleRefreshToken',
|
||||
accessToken: 'exampleAccessToken',
|
||||
provider: 'google',
|
||||
handle: 'jane.austen@apple.dev',
|
||||
},
|
||||
{
|
||||
id: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE_DELETABLE,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
lastSyncHistoryId: 'exampleLastSyncHistory',
|
||||
accountOwnerId: WORKSPACE_MEMBER_DATA_SEED_IDS.JANE,
|
||||
refreshToken: 'exampleRefreshToken',
|
||||
accessToken: 'exampleAccessToken',
|
||||
provider: 'google',
|
||||
handle: 'jane-deletable@apple.dev',
|
||||
},
|
||||
];
|
||||
|
||||
+14
@@ -40,6 +40,7 @@ const GENERATE_MESSAGE_CHANNEL_IDS = (): Record<string, string> => {
|
||||
CHANNEL_IDS['TIM'] = '20202020-9b80-4c2c-a597-383db48de1d6';
|
||||
CHANNEL_IDS['JONY'] = '20202020-5ffe-4b32-814a-983d5e4911cd';
|
||||
CHANNEL_IDS['PHIL'] = '20202020-e2f1-49b5-85d2-5d3a3386990c';
|
||||
CHANNEL_IDS['JANE'] = '20202020-8c4d-4e71-a672-2e6a8c9f1b3d';
|
||||
CHANNEL_IDS['SUPPORT'] = '20202020-e2f1-49b5-85d2-5d3a3386990d';
|
||||
CHANNEL_IDS['SALES'] = '20202020-e2f1-49b5-85d2-5d3a3386990e';
|
||||
|
||||
@@ -88,6 +89,19 @@ export const MESSAGE_CHANNEL_DATA_SEEDS: MessageChannelDataSeed[] = [
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
||||
},
|
||||
{
|
||||
id: MESSAGE_CHANNEL_DATA_SEED_IDS.JANE,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
isContactAutoCreationEnabled: true,
|
||||
type: MessageChannelType.EMAIL,
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE,
|
||||
handle: 'jane.austen@apple.dev',
|
||||
isSyncEnabled: true,
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
syncStage: MessageChannelSyncStage.MESSAGE_LIST_FETCH_PENDING,
|
||||
},
|
||||
{
|
||||
id: MESSAGE_CHANNEL_DATA_SEED_IDS.SUPPORT,
|
||||
createdAt: new Date(),
|
||||
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
import { MessageFolderPendingSyncAction } from 'twenty-shared/types';
|
||||
|
||||
import { MESSAGE_CHANNEL_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/message-channel-data-seeds.constant';
|
||||
|
||||
type MessageFolderDataSeed = {
|
||||
id: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
deletedAt: Date | null;
|
||||
name: string;
|
||||
isSynced: boolean;
|
||||
isSentFolder: boolean;
|
||||
messageChannelId: string;
|
||||
pendingSyncAction: MessageFolderPendingSyncAction;
|
||||
};
|
||||
|
||||
export const MESSAGE_FOLDER_DATA_SEED_COLUMNS: (keyof MessageFolderDataSeed)[] =
|
||||
[
|
||||
'id',
|
||||
'createdAt',
|
||||
'updatedAt',
|
||||
'deletedAt',
|
||||
'name',
|
||||
'isSynced',
|
||||
'isSentFolder',
|
||||
'messageChannelId',
|
||||
'pendingSyncAction',
|
||||
];
|
||||
|
||||
export const MESSAGE_FOLDER_DATA_SEED_IDS = {
|
||||
TIM_INBOX: '20202020-f1a2-4b3c-8d4e-5f6a7b8c9d0e',
|
||||
TIM_SENT: '20202020-f1a2-4b3c-8d4e-5f6a7b8c9d1e',
|
||||
JONY_INBOX: '20202020-f1a2-4b3c-8d4e-5f6a7b8c9d2e',
|
||||
JANE_INBOX: '20202020-f1a2-4b3c-8d4e-5f6a7b8c9d3e',
|
||||
JANE_SENT: '20202020-f1a2-4b3c-8d4e-5f6a7b8c9d4e',
|
||||
};
|
||||
|
||||
export const MESSAGE_FOLDER_DATA_SEEDS: MessageFolderDataSeed[] = [
|
||||
{
|
||||
id: MESSAGE_FOLDER_DATA_SEED_IDS.TIM_INBOX,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
name: 'INBOX',
|
||||
isSynced: true,
|
||||
isSentFolder: false,
|
||||
messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.TIM,
|
||||
pendingSyncAction: MessageFolderPendingSyncAction.NONE,
|
||||
},
|
||||
{
|
||||
id: MESSAGE_FOLDER_DATA_SEED_IDS.TIM_SENT,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
name: 'Sent',
|
||||
isSynced: true,
|
||||
isSentFolder: true,
|
||||
messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.TIM,
|
||||
pendingSyncAction: MessageFolderPendingSyncAction.NONE,
|
||||
},
|
||||
{
|
||||
id: MESSAGE_FOLDER_DATA_SEED_IDS.JONY_INBOX,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
name: 'INBOX',
|
||||
isSynced: true,
|
||||
isSentFolder: false,
|
||||
messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.JONY,
|
||||
pendingSyncAction: MessageFolderPendingSyncAction.NONE,
|
||||
},
|
||||
{
|
||||
id: MESSAGE_FOLDER_DATA_SEED_IDS.JANE_INBOX,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
name: 'INBOX',
|
||||
isSynced: true,
|
||||
isSentFolder: false,
|
||||
messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.JANE,
|
||||
pendingSyncAction: MessageFolderPendingSyncAction.NONE,
|
||||
},
|
||||
{
|
||||
id: MESSAGE_FOLDER_DATA_SEED_IDS.JANE_SENT,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
deletedAt: null,
|
||||
name: 'Sent',
|
||||
isSynced: true,
|
||||
isSentFolder: true,
|
||||
messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.JANE,
|
||||
pendingSyncAction: MessageFolderPendingSyncAction.NONE,
|
||||
},
|
||||
];
|
||||
+10
-1
@@ -56,6 +56,10 @@ import {
|
||||
MESSAGE_CHANNEL_DATA_SEED_COLUMNS,
|
||||
MESSAGE_CHANNEL_DATA_SEEDS,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/data/constants/message-channel-data-seeds.constant';
|
||||
import {
|
||||
MESSAGE_FOLDER_DATA_SEED_COLUMNS,
|
||||
MESSAGE_FOLDER_DATA_SEEDS,
|
||||
} from 'src/engine/workspace-manager/dev-seeder/data/constants/message-folder-data-seeds.constant';
|
||||
import {
|
||||
MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_DATA_SEED_COLUMNS,
|
||||
MESSAGE_CHANNEL_MESSAGE_ASSOCIATION_DATA_SEEDS,
|
||||
@@ -194,8 +198,13 @@ const getRecordSeedsBatches = (
|
||||
},
|
||||
];
|
||||
|
||||
// Batch 4: Depends on person/company or independent
|
||||
// Batch 4: Depends on person/company/messageChannel or independent
|
||||
const batch4: RecordSeedConfig[] = [
|
||||
{
|
||||
tableName: 'messageFolder',
|
||||
pgColumns: MESSAGE_FOLDER_DATA_SEED_COLUMNS,
|
||||
recordSeeds: MESSAGE_FOLDER_DATA_SEEDS,
|
||||
},
|
||||
{
|
||||
tableName: 'opportunity',
|
||||
pgColumns: OPPORTUNITY_DATA_SEED_COLUMNS,
|
||||
|
||||
+227
@@ -0,0 +1,227 @@
|
||||
import { type CalendarChannelWorkspaceEntity } from 'src/modules/calendar/common/standard-objects/calendar-channel.workspace-entity';
|
||||
import { type ConnectedAccountWorkspaceEntity } from 'src/modules/connected-account/standard-objects/connected-account.workspace-entity';
|
||||
import { CalendarFetchEventsService } from 'src/modules/calendar/calendar-event-import-manager/services/calendar-fetch-events.service';
|
||||
|
||||
const mockCalendarChannelSyncStatusService = {
|
||||
markAsCalendarEventListFetchOngoing: jest.fn(),
|
||||
markAsCalendarEventListFetchPending: jest.fn(),
|
||||
markAsCalendarEventsImportPending: jest.fn(),
|
||||
};
|
||||
|
||||
const mockGetCalendarEventsService = {
|
||||
getCalendarEvents: jest.fn(),
|
||||
};
|
||||
|
||||
const mockCalendarChannelDataAccessService = {
|
||||
update: jest.fn(),
|
||||
};
|
||||
|
||||
const mockCalendarAccountAuthenticationService = {
|
||||
validateAndRefreshConnectedAccountAuthentication: jest
|
||||
.fn()
|
||||
.mockResolvedValue({
|
||||
accessToken: 'fresh-access-token',
|
||||
refreshToken: 'fresh-refresh-token',
|
||||
}),
|
||||
};
|
||||
|
||||
const mockCalendarEventsImportService = {
|
||||
processCalendarEventsImport: jest.fn(),
|
||||
};
|
||||
|
||||
const mockCalendarEventImportErrorHandlerService = {
|
||||
handleDriverException: jest.fn(),
|
||||
};
|
||||
|
||||
const mockCacheStorage = {
|
||||
setAdd: jest.fn(),
|
||||
};
|
||||
|
||||
const mockGlobalWorkspaceOrmManager = {
|
||||
executeInWorkspaceContext: jest.fn(async (callback: () => Promise<void>) => {
|
||||
await callback();
|
||||
}),
|
||||
};
|
||||
|
||||
const workspaceId = 'workspace-123';
|
||||
|
||||
const baseConnectedAccount = {
|
||||
id: 'account-123',
|
||||
provider: 'google',
|
||||
refreshToken: 'refresh-token',
|
||||
accessToken: 'access-token',
|
||||
handle: 'test@example.com',
|
||||
} as unknown as ConnectedAccountWorkspaceEntity;
|
||||
|
||||
const createCalendarChannel = (
|
||||
syncCursor: string | null,
|
||||
): CalendarChannelWorkspaceEntity =>
|
||||
({
|
||||
id: 'channel-123',
|
||||
syncCursor,
|
||||
connectedAccountId: 'account-123',
|
||||
}) as unknown as CalendarChannelWorkspaceEntity;
|
||||
|
||||
describe('CalendarFetchEventsService', () => {
|
||||
let service: CalendarFetchEventsService;
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
|
||||
mockGetCalendarEventsService.getCalendarEvents.mockResolvedValue({
|
||||
fullEvents: true,
|
||||
calendarEvents: [{ id: 'event-1' }],
|
||||
nextSyncCursor: 'new-cursor-abc',
|
||||
});
|
||||
|
||||
service = new CalendarFetchEventsService(
|
||||
mockCacheStorage as any,
|
||||
mockGlobalWorkspaceOrmManager as any,
|
||||
mockCalendarChannelDataAccessService as any,
|
||||
mockCalendarChannelSyncStatusService as any,
|
||||
mockGetCalendarEventsService as any,
|
||||
mockCalendarEventImportErrorHandlerService as any,
|
||||
mockCalendarEventsImportService as any,
|
||||
mockCalendarAccountAuthenticationService as any,
|
||||
);
|
||||
});
|
||||
|
||||
describe('syncCursor handling (backwards compatibility)', () => {
|
||||
it('should perform full sync when syncCursor is null (core schema)', async () => {
|
||||
const calendarChannel = createCalendarChannel(null);
|
||||
|
||||
await service.fetchCalendarEvents(
|
||||
calendarChannel,
|
||||
baseConnectedAccount,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
expect(
|
||||
mockGetCalendarEventsService.getCalendarEvents,
|
||||
).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ handle: 'test@example.com' }),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should perform full sync when syncCursor is empty string (workspace schema)', async () => {
|
||||
const calendarChannel = createCalendarChannel('');
|
||||
|
||||
await service.fetchCalendarEvents(
|
||||
calendarChannel,
|
||||
baseConnectedAccount,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
expect(
|
||||
mockGetCalendarEventsService.getCalendarEvents,
|
||||
).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ handle: 'test@example.com' }),
|
||||
undefined,
|
||||
);
|
||||
});
|
||||
|
||||
it('should use existing syncCursor for incremental sync', async () => {
|
||||
const calendarChannel = createCalendarChannel('cursor-xyz');
|
||||
|
||||
await service.fetchCalendarEvents(
|
||||
calendarChannel,
|
||||
baseConnectedAccount,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
expect(
|
||||
mockGetCalendarEventsService.getCalendarEvents,
|
||||
).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ handle: 'test@example.com' }),
|
||||
'cursor-xyz',
|
||||
);
|
||||
});
|
||||
|
||||
it('should update syncCursor after successful fetch regardless of initial cursor state', async () => {
|
||||
const calendarChannel = createCalendarChannel(null);
|
||||
|
||||
await service.fetchCalendarEvents(
|
||||
calendarChannel,
|
||||
baseConnectedAccount,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
expect(mockCalendarChannelDataAccessService.update).toHaveBeenCalledWith(
|
||||
workspaceId,
|
||||
{ id: 'channel-123' },
|
||||
{ syncCursor: 'new-cursor-abc' },
|
||||
);
|
||||
});
|
||||
|
||||
it('should not throw when feature flag switches from off (workspace empty string) to on (core null)', async () => {
|
||||
// Simulate flag OFF: workspace returns empty string
|
||||
const workspaceChannel = createCalendarChannel('');
|
||||
|
||||
await expect(
|
||||
service.fetchCalendarEvents(
|
||||
workspaceChannel,
|
||||
baseConnectedAccount,
|
||||
workspaceId,
|
||||
),
|
||||
).resolves.not.toThrow();
|
||||
|
||||
jest.clearAllMocks();
|
||||
mockGetCalendarEventsService.getCalendarEvents.mockResolvedValue({
|
||||
fullEvents: true,
|
||||
calendarEvents: [{ id: 'event-2' }],
|
||||
nextSyncCursor: 'new-cursor-def',
|
||||
});
|
||||
|
||||
// Simulate flag ON: core returns null
|
||||
const coreChannel = createCalendarChannel(null);
|
||||
|
||||
await expect(
|
||||
service.fetchCalendarEvents(
|
||||
coreChannel,
|
||||
baseConnectedAccount,
|
||||
workspaceId,
|
||||
),
|
||||
).resolves.not.toThrow();
|
||||
|
||||
expect(
|
||||
mockGetCalendarEventsService.getCalendarEvents,
|
||||
).toHaveBeenCalledWith(expect.anything(), undefined);
|
||||
});
|
||||
|
||||
it('should preserve incremental sync behavior when toggling feature flag with existing cursor', async () => {
|
||||
// Flag OFF: workspace has cursor
|
||||
const workspaceChannel = createCalendarChannel('cursor-from-workspace');
|
||||
|
||||
await service.fetchCalendarEvents(
|
||||
workspaceChannel,
|
||||
baseConnectedAccount,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
expect(
|
||||
mockGetCalendarEventsService.getCalendarEvents,
|
||||
).toHaveBeenCalledWith(expect.anything(), 'cursor-from-workspace');
|
||||
|
||||
jest.clearAllMocks();
|
||||
mockGetCalendarEventsService.getCalendarEvents.mockResolvedValue({
|
||||
fullEvents: true,
|
||||
calendarEvents: [{ id: 'event-3' }],
|
||||
nextSyncCursor: 'newer-cursor',
|
||||
});
|
||||
|
||||
// Flag ON: core has same cursor (dual-written)
|
||||
const coreChannel = createCalendarChannel('cursor-from-workspace');
|
||||
|
||||
await service.fetchCalendarEvents(
|
||||
coreChannel,
|
||||
baseConnectedAccount,
|
||||
workspaceId,
|
||||
);
|
||||
|
||||
expect(
|
||||
mockGetCalendarEventsService.getCalendarEvents,
|
||||
).toHaveBeenCalledWith(expect.anything(), 'cursor-from-workspace');
|
||||
});
|
||||
});
|
||||
});
|
||||
+1
-10
@@ -1,7 +1,5 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
|
||||
import { isDefined } from 'twenty-shared/utils';
|
||||
|
||||
import { InjectCacheStorage } from 'src/engine/core-modules/cache-storage/decorators/cache-storage.decorator';
|
||||
import { CacheStorageService } from 'src/engine/core-modules/cache-storage/services/cache-storage.service';
|
||||
import { CacheStorageNamespace } from 'src/engine/core-modules/cache-storage/types/cache-storage-namespace.enum';
|
||||
@@ -71,17 +69,10 @@ export class CalendarFetchEventsService {
|
||||
refreshToken,
|
||||
};
|
||||
|
||||
if (!isDefined(calendarChannel.syncCursor)) {
|
||||
throw new CalendarEventImportDriverException(
|
||||
'Sync cursor is required',
|
||||
CalendarEventImportDriverExceptionCode.SYNC_CURSOR_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
const getCalendarEventsResponse =
|
||||
await this.getCalendarEventsService.getCalendarEvents(
|
||||
connectedAccountWithFreshTokens,
|
||||
calendarChannel.syncCursor,
|
||||
calendarChannel.syncCursor || undefined,
|
||||
);
|
||||
|
||||
const hasFullEvents = getCalendarEventsResponse.fullEvents;
|
||||
|
||||
+126
@@ -0,0 +1,126 @@
|
||||
import { gql } from 'graphql-tag';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { CALENDAR_CHANNEL_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/calendar-channel-data-seeds.constant';
|
||||
import { CONNECTED_ACCOUNT_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant';
|
||||
|
||||
describe('calendarChannelResolver (e2e)', () => {
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
featureFlag: FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
value: true,
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
featureFlag: FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
value: false,
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe('myCalendarChannels', () => {
|
||||
it('should return only the current user calendar channels', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
query MyCalendarChannels {
|
||||
myCalendarChannels {
|
||||
id
|
||||
handle
|
||||
visibility
|
||||
syncStatus
|
||||
syncStage
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
|
||||
const channels = response.body.data.myCalendarChannels;
|
||||
const channelIds = channels.map(
|
||||
(channel: { id: string }) => channel.id,
|
||||
);
|
||||
|
||||
expect(channelIds).toContain(CALENDAR_CHANNEL_DATA_SEED_IDS.JANE);
|
||||
expect(channelIds).not.toContain(
|
||||
CALENDAR_CHANNEL_DATA_SEED_IDS.JONY,
|
||||
);
|
||||
});
|
||||
|
||||
it('should deny filtering by another user connectedAccountId', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
query MyCalendarChannels($connectedAccountId: UUID) {
|
||||
myCalendarChannels(connectedAccountId: $connectedAccountId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors?.[0]?.extensions?.code).toBe('FORBIDDEN');
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateCalendarChannel', () => {
|
||||
it('should allow updating own channel', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
mutation UpdateCalendarChannel(
|
||||
$input: UpdateCalendarChannelInput!
|
||||
) {
|
||||
updateCalendarChannel(input: $input) {
|
||||
id
|
||||
visibility
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
id: CALENDAR_CHANNEL_DATA_SEED_IDS.JANE,
|
||||
update: { visibility: 'METADATA' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
expect(response.body.data.updateCalendarChannel.visibility).toBe(
|
||||
'METADATA',
|
||||
);
|
||||
});
|
||||
|
||||
it('should deny updating another user channel', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
mutation UpdateCalendarChannel(
|
||||
$input: UpdateCalendarChannelInput!
|
||||
) {
|
||||
updateCalendarChannel(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
id: CALENDAR_CHANNEL_DATA_SEED_IDS.JONY,
|
||||
update: { visibility: 'METADATA' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors?.[0]?.extensions?.code).toBe('FORBIDDEN');
|
||||
});
|
||||
});
|
||||
});
|
||||
+188
@@ -0,0 +1,188 @@
|
||||
import { gql } from 'graphql-tag';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { makeMetadataAPIRequestWithMemberRole } from 'test/integration/metadata/suites/utils/make-metadata-api-request-with-member-role.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { CONNECTED_ACCOUNT_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant';
|
||||
|
||||
describe('connectedAccountResolver (e2e)', () => {
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
featureFlag: FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
value: true,
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
featureFlag: FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
value: false,
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe('myConnectedAccounts', () => {
|
||||
it('should return only the current user connected accounts', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
query MyConnectedAccounts {
|
||||
myConnectedAccounts {
|
||||
id
|
||||
handle
|
||||
provider
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
|
||||
const accounts = response.body.data.myConnectedAccounts;
|
||||
const accountIds = accounts.map(
|
||||
(account: { id: string }) => account.id,
|
||||
);
|
||||
|
||||
expect(accountIds).toContain(CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE);
|
||||
expect(accountIds).not.toContain(
|
||||
CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not return sensitive fields', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
query MyConnectedAccounts {
|
||||
myConnectedAccounts {
|
||||
id
|
||||
handle
|
||||
provider
|
||||
authFailedAt
|
||||
scopes
|
||||
handleAliases
|
||||
lastSignedInAt
|
||||
userWorkspaceId
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
|
||||
const account = response.body.data.myConnectedAccounts.find(
|
||||
(connectedAccount: { id: string }) =>
|
||||
connectedAccount.id === CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE,
|
||||
);
|
||||
|
||||
expect(account).toBeDefined();
|
||||
expect(account.handle).toBe('jane.austen@apple.dev');
|
||||
expect(account).not.toHaveProperty('accessToken');
|
||||
expect(account).not.toHaveProperty('refreshToken');
|
||||
expect(account).not.toHaveProperty('connectionParameters');
|
||||
expect(account).not.toHaveProperty('oidcTokenClaims');
|
||||
});
|
||||
|
||||
it('should reject requesting hidden fields via GraphQL', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
query MyConnectedAccounts {
|
||||
myConnectedAccounts {
|
||||
id
|
||||
accessToken
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors).toBeDefined();
|
||||
expect(response.body.errors.length).toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('connectedAccounts (admin listing)', () => {
|
||||
it('should return all workspace accounts for admin', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
query ConnectedAccounts {
|
||||
connectedAccounts {
|
||||
id
|
||||
handle
|
||||
provider
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
|
||||
const accounts = response.body.data.connectedAccounts;
|
||||
const accountIds = accounts.map(
|
||||
(account: { id: string }) => account.id,
|
||||
);
|
||||
|
||||
expect(accountIds).toContain(CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE);
|
||||
expect(accountIds).toContain(CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY);
|
||||
});
|
||||
|
||||
it('should also be accessible for member role (tool permission)', async () => {
|
||||
const response = await makeMetadataAPIRequestWithMemberRole({
|
||||
query: gql`
|
||||
query ConnectedAccounts {
|
||||
connectedAccounts {
|
||||
id
|
||||
handle
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
expect(response.body.data.connectedAccounts).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteConnectedAccount', () => {
|
||||
it('should allow deleting own account', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
mutation DeleteConnectedAccount($id: UUID!) {
|
||||
deleteConnectedAccount(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { id: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE_DELETABLE },
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
expect(response.body.data.deleteConnectedAccount.id).toBe(
|
||||
CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE_DELETABLE,
|
||||
);
|
||||
});
|
||||
|
||||
it('should deny deleting another user account', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
mutation DeleteConnectedAccount($id: UUID!) {
|
||||
deleteConnectedAccount(id: $id) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { id: CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY },
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors?.[0]?.extensions?.code).toBe('FORBIDDEN');
|
||||
});
|
||||
});
|
||||
});
|
||||
+163
@@ -0,0 +1,163 @@
|
||||
import { gql } from 'graphql-tag';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { CONNECTED_ACCOUNT_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/connected-account-data-seeds.constant';
|
||||
import { MESSAGE_CHANNEL_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/message-channel-data-seeds.constant';
|
||||
|
||||
describe('messageChannelResolver (e2e)', () => {
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
featureFlag: FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
value: true,
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
featureFlag: FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
value: false,
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe('myMessageChannels', () => {
|
||||
it('should return only the current user message channels', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
query MyMessageChannels {
|
||||
myMessageChannels {
|
||||
id
|
||||
handle
|
||||
visibility
|
||||
syncStatus
|
||||
syncStage
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
|
||||
const channels = response.body.data.myMessageChannels;
|
||||
const channelIds = channels.map(
|
||||
(channel: { id: string }) => channel.id,
|
||||
);
|
||||
|
||||
expect(channelIds).toContain(MESSAGE_CHANNEL_DATA_SEED_IDS.JANE);
|
||||
expect(channelIds).not.toContain(MESSAGE_CHANNEL_DATA_SEED_IDS.JONY);
|
||||
});
|
||||
|
||||
it('should filter by connectedAccountId', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
query MyMessageChannels($connectedAccountId: UUID) {
|
||||
myMessageChannels(connectedAccountId: $connectedAccountId) {
|
||||
id
|
||||
handle
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JANE,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
|
||||
const channels = response.body.data.myMessageChannels;
|
||||
const channelIds = channels.map(
|
||||
(channel: { id: string }) => channel.id,
|
||||
);
|
||||
|
||||
expect(channelIds).toContain(MESSAGE_CHANNEL_DATA_SEED_IDS.JANE);
|
||||
expect(channelIds).not.toContain(MESSAGE_CHANNEL_DATA_SEED_IDS.JONY);
|
||||
});
|
||||
|
||||
it('should deny filtering by another user connectedAccountId', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
query MyMessageChannels($connectedAccountId: UUID) {
|
||||
myMessageChannels(connectedAccountId: $connectedAccountId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
connectedAccountId: CONNECTED_ACCOUNT_DATA_SEED_IDS.JONY,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors?.[0]?.extensions?.code).toBe('FORBIDDEN');
|
||||
});
|
||||
|
||||
it('should not return syncCursor', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
query MyMessageChannels {
|
||||
myMessageChannels {
|
||||
id
|
||||
syncCursor
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateMessageChannel', () => {
|
||||
it('should allow updating own channel settings', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
mutation UpdateMessageChannel($input: UpdateMessageChannelInput!) {
|
||||
updateMessageChannel(input: $input) {
|
||||
id
|
||||
visibility
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
id: MESSAGE_CHANNEL_DATA_SEED_IDS.JANE,
|
||||
update: { visibility: 'METADATA' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
expect(response.body.data.updateMessageChannel.visibility).toBe(
|
||||
'METADATA',
|
||||
);
|
||||
});
|
||||
|
||||
it('should deny updating another user channel', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
mutation UpdateMessageChannel($input: UpdateMessageChannelInput!) {
|
||||
updateMessageChannel(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
id: MESSAGE_CHANNEL_DATA_SEED_IDS.JONY,
|
||||
update: { visibility: 'METADATA' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors?.[0]?.extensions?.code).toBe('FORBIDDEN');
|
||||
});
|
||||
});
|
||||
});
|
||||
+161
@@ -0,0 +1,161 @@
|
||||
import { gql } from 'graphql-tag';
|
||||
import { makeMetadataAPIRequest } from 'test/integration/metadata/suites/utils/make-metadata-api-request.util';
|
||||
import { updateFeatureFlag } from 'test/integration/metadata/suites/utils/update-feature-flag.util';
|
||||
import { FeatureFlagKey } from 'twenty-shared/types';
|
||||
|
||||
import { MESSAGE_CHANNEL_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/message-channel-data-seeds.constant';
|
||||
import { MESSAGE_FOLDER_DATA_SEED_IDS } from 'src/engine/workspace-manager/dev-seeder/data/constants/message-folder-data-seeds.constant';
|
||||
|
||||
describe('messageFolderResolver (e2e)', () => {
|
||||
beforeAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
featureFlag: FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
value: true,
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await updateFeatureFlag({
|
||||
featureFlag: FeatureFlagKey.IS_CONNECTED_ACCOUNT_MIGRATED,
|
||||
value: false,
|
||||
expectToFail: false,
|
||||
});
|
||||
});
|
||||
|
||||
describe('myMessageFolders', () => {
|
||||
it('should return only the current user message folders', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
query MyMessageFolders {
|
||||
myMessageFolders {
|
||||
id
|
||||
name
|
||||
isSynced
|
||||
messageChannelId
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
|
||||
const folders = response.body.data.myMessageFolders;
|
||||
const folderIds = folders.map((folder: { id: string }) => folder.id);
|
||||
|
||||
expect(folderIds).toContain(MESSAGE_FOLDER_DATA_SEED_IDS.JANE_INBOX);
|
||||
expect(folderIds).toContain(MESSAGE_FOLDER_DATA_SEED_IDS.JANE_SENT);
|
||||
expect(folderIds).not.toContain(
|
||||
MESSAGE_FOLDER_DATA_SEED_IDS.JONY_INBOX,
|
||||
);
|
||||
});
|
||||
|
||||
it('should filter by messageChannelId', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
query MyMessageFolders($messageChannelId: UUID) {
|
||||
myMessageFolders(messageChannelId: $messageChannelId) {
|
||||
id
|
||||
name
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.JANE,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
|
||||
const folders = response.body.data.myMessageFolders;
|
||||
const folderIds = folders.map((folder: { id: string }) => folder.id);
|
||||
|
||||
expect(folderIds).toContain(MESSAGE_FOLDER_DATA_SEED_IDS.JANE_INBOX);
|
||||
expect(folderIds).not.toContain(
|
||||
MESSAGE_FOLDER_DATA_SEED_IDS.JONY_INBOX,
|
||||
);
|
||||
});
|
||||
|
||||
it('should deny filtering by another user messageChannelId', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
query MyMessageFolders($messageChannelId: UUID) {
|
||||
myMessageFolders(messageChannelId: $messageChannelId) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
messageChannelId: MESSAGE_CHANNEL_DATA_SEED_IDS.JONY,
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors?.[0]?.extensions?.code).toBe('FORBIDDEN');
|
||||
});
|
||||
|
||||
it('should not expose hidden fields', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
query MyMessageFolders {
|
||||
myMessageFolders {
|
||||
id
|
||||
syncCursor
|
||||
}
|
||||
}
|
||||
`,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateMessageFolder', () => {
|
||||
it('should allow updating own folder isSynced', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
mutation UpdateMessageFolder($input: UpdateMessageFolderInput!) {
|
||||
updateMessageFolder(input: $input) {
|
||||
id
|
||||
isSynced
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
id: MESSAGE_FOLDER_DATA_SEED_IDS.JANE_INBOX,
|
||||
update: { isSynced: false },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors).toBeUndefined();
|
||||
expect(response.body.data.updateMessageFolder.isSynced).toBe(false);
|
||||
});
|
||||
|
||||
it('should deny updating another user folder', async () => {
|
||||
const response = await makeMetadataAPIRequest({
|
||||
query: gql`
|
||||
mutation UpdateMessageFolder($input: UpdateMessageFolderInput!) {
|
||||
updateMessageFolder(input: $input) {
|
||||
id
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: {
|
||||
input: {
|
||||
id: MESSAGE_FOLDER_DATA_SEED_IDS.JONY_INBOX,
|
||||
update: { isSynced: false },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).toBe(200);
|
||||
expect(response.body.errors?.[0]?.extensions?.code).toBe('FORBIDDEN');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user