diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts
index b972a5ed51..3c31003a88 100644
--- a/packages/twenty-front/src/generated-metadata/graphql.ts
+++ b/packages/twenty-front/src/generated-metadata/graphql.ts
@@ -1834,6 +1834,7 @@ export type Mutation = {
destroyPageLayoutTab: Scalars['Boolean'];
destroyPageLayoutWidget: Scalars['Boolean'];
disablePostgresProxy: PostgresCredentials;
+ dismissReconnectAccountBanner: Scalars['Boolean'];
duplicateWorkflow: WorkflowVersionDto;
duplicateWorkflowVersionStep: WorkflowVersionStepChanges;
editSSOIdentityProvider: EditSsoOutput;
@@ -2370,6 +2371,11 @@ export type MutationDestroyPageLayoutWidgetArgs = {
};
+export type MutationDismissReconnectAccountBannerArgs = {
+ connectedAccountId: Scalars['UUID'];
+};
+
+
export type MutationDuplicateWorkflowArgs = {
input: DuplicateWorkflowInput;
};
diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts
index 4e1694ad96..21833292ac 100644
--- a/packages/twenty-front/src/generated/graphql.ts
+++ b/packages/twenty-front/src/generated/graphql.ts
@@ -1808,6 +1808,7 @@ export type Mutation = {
destroyPageLayoutTab: Scalars['Boolean'];
destroyPageLayoutWidget: Scalars['Boolean'];
disablePostgresProxy: PostgresCredentials;
+ dismissReconnectAccountBanner: Scalars['Boolean'];
duplicateWorkflow: WorkflowVersionDto;
duplicateWorkflowVersionStep: WorkflowVersionStepChanges;
editSSOIdentityProvider: EditSsoOutput;
@@ -2325,6 +2326,11 @@ export type MutationDestroyPageLayoutWidgetArgs = {
};
+export type MutationDismissReconnectAccountBannerArgs = {
+ connectedAccountId: Scalars['UUID'];
+};
+
+
export type MutationDuplicateWorkflowArgs = {
input: DuplicateWorkflowInput;
};
diff --git a/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx b/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx
index cc66353e21..cdd8aabac9 100644
--- a/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx
+++ b/packages/twenty-front/src/modules/information-banner/components/InformationBanner.tsx
@@ -1,10 +1,15 @@
+import { InformationBannerComponentInstanceContext } from '@/information-banner/states/contexts/InformationBannerComponentInstanceContext';
+import { informationBannerIsOpenComponentState } from '@/information-banner/states/informationBannerIsOpenComponentState';
+import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import styled from '@emotion/styled';
import {
Banner,
type BannerVariant,
type IconComponent,
+ IconX,
} from 'twenty-ui/display';
-import { Button } from 'twenty-ui/input';
+import { Button, IconButton } from 'twenty-ui/input';
+import { GRAY_SCALE_LIGHT } from 'twenty-ui/theme';
const StyledText = styled.div`
overflow: hidden;
@@ -12,6 +17,20 @@ const StyledText = styled.div`
white-space: nowrap;
`;
+const StyledCloseButton = styled(IconButton)`
+ color: ${GRAY_SCALE_LIGHT.gray1};
+ display: flex;
+`;
+
+const StyledContent = styled.div<{ hasCloseButton: boolean }>`
+ align-items: center;
+ justify-content: center;
+ display: flex;
+ flex: 1;
+ margin-left: ${({ hasCloseButton }) => (hasCloseButton ? '24px' : '0')};
+ gap: ${({ theme }) => theme.spacing(3)};
+`;
+
export const InformationBanner = ({
message,
variant = 'default',
@@ -19,6 +38,8 @@ export const InformationBanner = ({
buttonIcon,
buttonOnClick,
isButtonDisabled = false,
+ onClose,
+ componentInstanceId,
}: {
message: string;
variant?: BannerVariant;
@@ -26,21 +47,47 @@ export const InformationBanner = ({
buttonIcon?: IconComponent;
buttonOnClick?: () => void;
isButtonDisabled?: boolean;
+ onClose?: () => void;
+ componentInstanceId: string;
}) => {
+ const informationBannerIsOpenComponent = useRecoilComponentValue(
+ informationBannerIsOpenComponentState,
+ componentInstanceId,
+ );
+
return (
-
- {message}
- {buttonTitle && buttonOnClick && (
-
+
+ {informationBannerIsOpenComponent && (
+
+
+ {message}
+ {buttonTitle && buttonOnClick && (
+
+ )}
+
+ {onClose && (
+
+ )}
+
)}
-
+
);
};
diff --git a/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerBillingSubscriptionPaused.tsx b/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerBillingSubscriptionPaused.tsx
index 9e137bf159..cfeffbde02 100644
--- a/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerBillingSubscriptionPaused.tsx
+++ b/packages/twenty-front/src/modules/information-banner/components/billing/InformationBannerBillingSubscriptionPaused.tsx
@@ -30,6 +30,7 @@ export const InformationBannerBillingSubscriptionPaused = () => {
return (
{
return (
{
return (
{
return (
{
return (
{
const { accountToReconnect } = useAccountToReconnect(
InformationBannerKeys.ACCOUNTS_TO_RECONNECT_EMAIL_ALIASES,
);
const { triggerProviderReconnect } = useTriggerProviderReconnect();
+ const { dismissReconnectAccountBanner } = useDismissReconnectAccountBanner(
+ COMPONENT_INSTANCE_ID,
+ );
if (!accountToReconnect) {
return null;
}
+ const handleDismiss = async () => {
+ await dismissReconnectAccountBanner(accountToReconnect.id);
+ };
+
return (
{
accountToReconnect.id,
)
}
+ onClose={handleDismiss}
/>
);
};
diff --git a/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx b/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx
index d20745779c..b585708a7f 100644
--- a/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx
+++ b/packages/twenty-front/src/modules/information-banner/components/reconnect-account/InformationBannerReconnectAccountInsufficientPermissions.tsx
@@ -1,22 +1,34 @@
import { InformationBanner } from '@/information-banner/components/InformationBanner';
import { useAccountToReconnect } from '@/information-banner/hooks/useAccountToReconnect';
+import { useDismissReconnectAccountBanner } from '@/information-banner/hooks/useDismissReconnectAccountBanner';
import { InformationBannerKeys } from '@/information-banner/types/InformationBannerKeys';
import { useTriggerProviderReconnect } from '@/settings/accounts/hooks/useTriggerProviderReconnect';
import { IconRefresh } from 'twenty-ui/display';
+const COMPONENT_INSTANCE_ID =
+ 'information-banner-reconnect-account-insufficient-permissions';
+
export const InformationBannerReconnectAccountInsufficientPermissions = () => {
const { accountToReconnect } = useAccountToReconnect(
InformationBannerKeys.ACCOUNTS_TO_RECONNECT_INSUFFICIENT_PERMISSIONS,
);
const { triggerProviderReconnect } = useTriggerProviderReconnect();
+ const { dismissReconnectAccountBanner } = useDismissReconnectAccountBanner(
+ COMPONENT_INSTANCE_ID,
+ );
if (!accountToReconnect) {
return null;
}
+ const handleDismiss = async () => {
+ await dismissReconnectAccountBanner(accountToReconnect.id);
+ };
+
return (
{
accountToReconnect.id,
)
}
+ onClose={handleDismiss}
/>
);
};
diff --git a/packages/twenty-front/src/modules/information-banner/graphql/mutations/dismissReconnectAccountBanner.ts b/packages/twenty-front/src/modules/information-banner/graphql/mutations/dismissReconnectAccountBanner.ts
new file mode 100644
index 0000000000..754a3ada77
--- /dev/null
+++ b/packages/twenty-front/src/modules/information-banner/graphql/mutations/dismissReconnectAccountBanner.ts
@@ -0,0 +1,7 @@
+import { gql } from '@apollo/client';
+
+export const DISMISS_RECONNECT_ACCOUNT_BANNER = gql`
+ mutation DismissReconnectAccountBanner($connectedAccountId: UUID!) {
+ dismissReconnectAccountBanner(connectedAccountId: $connectedAccountId)
+ }
+`;
diff --git a/packages/twenty-front/src/modules/information-banner/hooks/useDismissReconnectAccountBanner.ts b/packages/twenty-front/src/modules/information-banner/hooks/useDismissReconnectAccountBanner.ts
new file mode 100644
index 0000000000..dae11ef0ed
--- /dev/null
+++ b/packages/twenty-front/src/modules/information-banner/hooks/useDismissReconnectAccountBanner.ts
@@ -0,0 +1,45 @@
+import { useMutation } from '@apollo/client';
+
+import { DISMISS_RECONNECT_ACCOUNT_BANNER } from '@/information-banner/graphql/mutations/dismissReconnectAccountBanner';
+import { informationBannerIsOpenComponentState } from '@/information-banner/states/informationBannerIsOpenComponentState';
+import { useApolloCoreClient } from '@/object-metadata/hooks/useApolloCoreClient';
+import { useSetRecoilComponentState } from '@/ui/utilities/state/component-state/hooks/useSetRecoilComponentState';
+
+type DismissReconnectAccountBannerMutationVariables = {
+ connectedAccountId: string;
+};
+
+type DismissReconnectAccountBannerMutation = {
+ dismissReconnectAccountBanner: boolean;
+};
+
+export const useDismissReconnectAccountBanner = (
+ componentInstanceId: string,
+) => {
+ const apolloCoreClient = useApolloCoreClient();
+
+ const [mutate] = useMutation<
+ DismissReconnectAccountBannerMutation,
+ DismissReconnectAccountBannerMutationVariables
+ >(DISMISS_RECONNECT_ACCOUNT_BANNER, {
+ client: apolloCoreClient,
+ });
+
+ const setInformationBannerIsOpenComponent = useSetRecoilComponentState(
+ informationBannerIsOpenComponentState,
+ componentInstanceId,
+ );
+
+ const dismissReconnectAccountBanner = async (connectedAccountId: string) => {
+ await mutate({
+ variables: {
+ connectedAccountId,
+ },
+ });
+ setInformationBannerIsOpenComponent(false);
+ };
+
+ return {
+ dismissReconnectAccountBanner,
+ };
+};
diff --git a/packages/twenty-front/src/modules/information-banner/states/contexts/InformationBannerComponentInstanceContext.ts b/packages/twenty-front/src/modules/information-banner/states/contexts/InformationBannerComponentInstanceContext.ts
new file mode 100644
index 0000000000..19e9ca58df
--- /dev/null
+++ b/packages/twenty-front/src/modules/information-banner/states/contexts/InformationBannerComponentInstanceContext.ts
@@ -0,0 +1,4 @@
+import { createComponentInstanceContext } from '@/ui/utilities/state/component-state/utils/createComponentInstanceContext';
+
+export const InformationBannerComponentInstanceContext =
+ createComponentInstanceContext();
diff --git a/packages/twenty-front/src/modules/information-banner/states/informationBannerIsOpenComponentState.ts b/packages/twenty-front/src/modules/information-banner/states/informationBannerIsOpenComponentState.ts
new file mode 100644
index 0000000000..7f35131932
--- /dev/null
+++ b/packages/twenty-front/src/modules/information-banner/states/informationBannerIsOpenComponentState.ts
@@ -0,0 +1,9 @@
+import { InformationBannerComponentInstanceContext } from '@/information-banner/states/contexts/InformationBannerComponentInstanceContext';
+import { createComponentState } from '@/ui/utilities/state/component-state/utils/createComponentState';
+
+export const informationBannerIsOpenComponentState =
+ createComponentState({
+ key: 'informationBannerIsOpenComponentState',
+ defaultValue: true,
+ componentInstanceContext: InformationBannerComponentInstanceContext,
+ });
diff --git a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindOneRecord.ts b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindOneRecord.ts
index 13336faf8a..26d34d59d5 100644
--- a/packages/twenty-front/src/modules/object-record/hooks/useLazyFindOneRecord.ts
+++ b/packages/twenty-front/src/modules/object-record/hooks/useLazyFindOneRecord.ts
@@ -39,8 +39,12 @@ export const useLazyFindOneRecord = ({
withSoftDeleted,
});
- const [findOneRecord, { loading, error, data, called }] =
- useLazyQuery(findOneRecordQuery);
+ const [findOneRecord, { loading, error, data, called }] = useLazyQuery(
+ findOneRecordQuery,
+ {
+ client: apolloCoreClient,
+ },
+ );
return {
findOneRecord: async ({
@@ -50,7 +54,6 @@ export const useLazyFindOneRecord = ({
await findOneRecord({
variables: { objectRecordId },
fetchPolicy,
- client: apolloCoreClient,
onCompleted: (data) => {
const record = getRecordFromRecordNode({
recordNode: data[objectNameSingular],
diff --git a/packages/twenty-server/src/engine/core-modules/messaging/dtos/dismiss-reconnect-account-banner.input.ts b/packages/twenty-server/src/engine/core-modules/messaging/dtos/dismiss-reconnect-account-banner.input.ts
new file mode 100644
index 0000000000..b748fa1a15
--- /dev/null
+++ b/packages/twenty-server/src/engine/core-modules/messaging/dtos/dismiss-reconnect-account-banner.input.ts
@@ -0,0 +1,13 @@
+import { ArgsType, Field } from '@nestjs/graphql';
+
+import { IsNotEmpty, IsUUID } from 'class-validator';
+
+import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
+
+@ArgsType()
+export class DismissReconnectAccountBannerInput {
+ @Field(() => UUIDScalarType)
+ @IsNotEmpty()
+ @IsUUID()
+ connectedAccountId: string;
+}
diff --git a/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.module.ts b/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.module.ts
index f34f1c4e44..66fd54050b 100644
--- a/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.module.ts
+++ b/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.module.ts
@@ -5,8 +5,10 @@ import { TimelineMessagingService } from 'src/engine/core-modules/messaging/serv
import { TimelineMessagingResolver } from 'src/engine/core-modules/messaging/timeline-messaging.resolver';
import { UserModule } from 'src/engine/core-modules/user/user.module';
import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/workspace-datasource.module';
+import { ConnectedAccountModule } from 'src/modules/connected-account/connected-account.module';
+
@Module({
- imports: [WorkspaceDataSourceModule, UserModule],
+ imports: [WorkspaceDataSourceModule, UserModule, ConnectedAccountModule],
exports: [],
providers: [
TimelineMessagingResolver,
diff --git a/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.resolver.ts b/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.resolver.ts
index 08a22d8ac6..efeca838b3 100644
--- a/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.resolver.ts
+++ b/packages/twenty-server/src/engine/core-modules/messaging/timeline-messaging.resolver.ts
@@ -1,10 +1,19 @@
import { UseGuards } from '@nestjs/common';
-import { Args, ArgsType, Field, Int, Query, Resolver } from '@nestjs/graphql';
+import {
+ Args,
+ ArgsType,
+ Field,
+ Int,
+ Mutation,
+ Query,
+ Resolver,
+} from '@nestjs/graphql';
import { Max } from 'class-validator';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
import { TIMELINE_THREADS_MAX_PAGE_SIZE } from 'src/engine/core-modules/messaging/constants/messaging.constants';
+import { DismissReconnectAccountBannerInput } from 'src/engine/core-modules/messaging/dtos/dismiss-reconnect-account-banner.input';
import { TimelineThreadsWithTotalDTO } from 'src/engine/core-modules/messaging/dtos/timeline-threads-with-total.dto';
import { GetMessagesService } from 'src/engine/core-modules/messaging/services/get-messages.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
@@ -15,6 +24,7 @@ import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorat
import { CustomPermissionGuard } from 'src/engine/guards/custom-permission.guard';
import { UserAuthGuard } from 'src/engine/guards/user-auth.guard';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
+import { AccountsToReconnectService } from 'src/modules/connected-account/services/accounts-to-reconnect.service';
@ArgsType()
class GetTimelineThreadsFromPersonIdArgs {
@@ -61,6 +71,7 @@ export class TimelineMessagingResolver {
constructor(
private readonly getMessagesFromPersonIdsService: GetMessagesService,
private readonly userService: UserService,
+ private readonly accountsToReconnectService: AccountsToReconnectService,
) {}
@Query(() => TimelineThreadsWithTotalDTO)
@@ -141,4 +152,19 @@ export class TimelineMessagingResolver {
return timelineThreads;
}
+
+ @Mutation(() => Boolean)
+ async dismissReconnectAccountBanner(
+ @AuthUser() user: UserEntity,
+ @AuthWorkspace() workspace: WorkspaceEntity,
+ @Args() { connectedAccountId }: DismissReconnectAccountBannerInput,
+ ): Promise {
+ await this.accountsToReconnectService.removeAccountToReconnect(
+ user.id,
+ workspace.id,
+ connectedAccountId,
+ );
+
+ return true;
+ }
}