Message Folders Redesign and Import Policy (#14714)

This commit is contained in:
neo773
2025-10-01 18:18:46 +05:30
committed by GitHub
parent a68aa303aa
commit 62a1de221b
20 changed files with 439 additions and 121 deletions
@@ -9,6 +9,11 @@ export enum MessageChannelContactAutoCreationPolicy {
NONE = 'NONE',
}
export enum MessageFolderImportPolicy {
ALL_FOLDERS = 'ALL_FOLDERS',
SELECTED_FOLDERS = 'SELECTED_FOLDERS',
}
export enum MessageChannelSyncStatus {
NOT_SYNCED = 'NOT_SYNCED',
ONGOING = 'ONGOING',
@@ -38,6 +43,7 @@ export type MessageChannel = {
isSyncEnabled: boolean;
messageFolders: MessageFolder[];
visibility: MessageChannelVisibility;
messageFolderImportPolicy: MessageFolderImportPolicy;
syncStatus: MessageChannelSyncStatus;
syncStage: MessageChannelSyncStage;
syncCursor: string;
@@ -3,11 +3,12 @@ import styled from '@emotion/styled';
import {
type MessageChannel,
type MessageChannelContactAutoCreationPolicy,
type MessageFolderImportPolicy,
} from '@/accounts/types/MessageChannel';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { SettingsAccountsMessageAutoCreationCard } from '@/settings/accounts/components/SettingsAccountsMessageAutoCreationCard';
import { SettingsAccountsMessageFoldersCard } from '@/settings/accounts/components/message-folders/SettingsAccountsMessageFoldersCard';
import { SettingsAccountsMessageFolderCard } from '@/settings/accounts/components/SettingsAccountsMessageFolderCard';
import { SettingsAccountsMessageVisibilityCard } from '@/settings/accounts/components/SettingsAccountsMessageVisibilityCard';
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
@@ -29,6 +30,7 @@ type SettingsAccountsMessageChannelDetailsProps = {
| 'excludeGroupEmails'
| 'isSyncEnabled'
| 'messageFolders'
| 'messageFolderImportPolicy'
>;
};
@@ -87,17 +89,26 @@ export const SettingsAccountsMessageChannelDetails = ({
});
};
const handleMessageFolderImportPolicyChange = (
value: MessageFolderImportPolicy,
) => {
updateOneRecord({
idToUpdate: messageChannel.id,
updateOneRecordInput: { messageFolderImportPolicy: value },
});
};
return (
<StyledDetailsContainer>
{isFolderControlEnabled && messageChannel.messageFolders && (
<Section>
<H2Title
title={t`Folder Management`}
description={t`Control which folders are synced`}
title={t`Import`}
description={t`Emails from the blocklist will be ignored. Manage blocklist on the “Accounts” setting page.`}
/>
<SettingsAccountsMessageFoldersCard
messageChannelId={messageChannel.id}
messageFolders={messageChannel.messageFolders}
<SettingsAccountsMessageFolderCard
onChange={handleMessageFolderImportPolicyChange}
value={messageChannel.messageFolderImportPolicy}
/>
</Section>
)}
@@ -1,5 +1,5 @@
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
import { type MessageChannel } from '@/accounts/types/MessageChannel';
@@ -11,10 +11,12 @@ import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { SettingsAccountsMessageChannelDetails } from '@/settings/accounts/components/SettingsAccountsMessageChannelDetails';
import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection';
import { SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID } from '@/settings/accounts/constants/SettingsAccountMessageChannelsTabListComponentId';
import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { activeTabIdComponentState } from '@/ui/layout/tab-list/states/activeTabIdComponentState';
import { useRecoilComponentValue } from '@/ui/utilities/state/component-state/hooks/useRecoilComponentValue';
import React from 'react';
import { isDefined } from 'twenty-shared/utils';
const StyledMessageContainer = styled.div`
padding-bottom: ${({ theme }) => theme.spacing(6)};
@@ -26,6 +28,9 @@ export const SettingsAccountsMessageChannelsContainer = () => {
SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID,
);
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
const setSelectedMessageChannel = useSetRecoilState(
settingsAccountsSelectedMessageChannelState,
);
const messageChannelObjectMetadataItem = useObjectMetadataItem({
objectNameSingular: CoreObjectNameSingular.MessageChannel,
@@ -57,6 +62,9 @@ export const SettingsAccountsMessageChannelsContainer = () => {
recordGqlFields: generateDepthOneRecordGqlFields(
messageChannelObjectMetadataItem,
),
onCompleted: (data) => {
setSelectedMessageChannel(data[0]);
},
skip: !accounts.length,
});
@@ -69,6 +77,15 @@ export const SettingsAccountsMessageChannelsContainer = () => {
return <SettingsNewAccountSection />;
}
const handleTabChange = (tabId: string) => {
const selectedMessageChannel = messageChannels.find(
(channel) => channel.id === tabId,
);
if (isDefined(selectedMessageChannel)) {
setSelectedMessageChannel(selectedMessageChannel);
}
};
return (
<>
{tabs.length > 1 && (
@@ -78,6 +95,7 @@ export const SettingsAccountsMessageChannelsContainer = () => {
componentInstanceId={
SETTINGS_ACCOUNT_MESSAGE_CHANNELS_TAB_LIST_COMPONENT_ID
}
onChangeTab={handleTabChange}
/>
</StyledMessageContainer>
)}
@@ -0,0 +1,46 @@
import { MessageFolderImportPolicy } from '@/accounts/types/MessageChannel';
import { SettingsAccountsMessageFoldersCard } from '@/settings/accounts/components/message-folders/SettingsAccountsMessageFoldersCard';
import { SettingsAccountsMessageFolderIcon } from '@/settings/accounts/components/SettingsAccountsMessageFolderIcon';
import { SettingsAccountsRadioSettingsCard } from '@/settings/accounts/components/SettingsAccountsRadioSettingsCard';
import { msg } from '@lingui/core/macro';
type SettingsAccountsMessageFolderCardProps = {
onChange: (nextValue: MessageFolderImportPolicy) => void;
value?: MessageFolderImportPolicy;
};
const INBOX_SETTINGS_VISIBILITY_OPTIONS = [
{
title: msg`Everything`,
description: msg`Import all emails`,
value: MessageFolderImportPolicy.ALL_FOLDERS,
cardMedia: (
<SettingsAccountsMessageFolderIcon
value={MessageFolderImportPolicy.ALL_FOLDERS}
/>
),
},
{
title: msg`Some folders`,
description: msg`Import only selected folders/labels`,
value: MessageFolderImportPolicy.SELECTED_FOLDERS,
cardMedia: (
<SettingsAccountsMessageFolderIcon
value={MessageFolderImportPolicy.SELECTED_FOLDERS}
/>
),
cardContentExpanded: <SettingsAccountsMessageFoldersCard />,
},
];
export const SettingsAccountsMessageFolderCard = ({
onChange,
value = MessageFolderImportPolicy.SELECTED_FOLDERS,
}: SettingsAccountsMessageFolderCardProps) => (
<SettingsAccountsRadioSettingsCard
name="message-folder-import-policy"
options={INBOX_SETTINGS_VISIBILITY_OPTIONS}
value={value}
onChange={onChange}
/>
);
@@ -0,0 +1,69 @@
import styled from '@emotion/styled';
import { MessageFolderImportPolicy } from '@/accounts/types/MessageChannel';
import { SettingsAccountsCardMedia } from '@/settings/accounts/components/SettingsAccountsCardMedia';
type SettingsAccountsMessageFolderIconProps = {
className?: string;
value?: MessageFolderImportPolicy;
};
const StyledCardMedia = styled(SettingsAccountsCardMedia)`
align-items: stretch;
flex-direction: column;
`;
const StyledFolderRow = styled.div`
align-items: center;
display: flex;
gap: 2px;
`;
const StyledFolderIcon = styled.div<{ isDisabled?: boolean }>`
background-color: ${({ isDisabled, theme }) =>
isDisabled ? theme.background.quaternary : theme.accent.accent4060};
border-radius: 1px;
height: 5px;
width: 5px;
`;
const StyledFolderLabel = styled.div<{ isDisabled?: boolean }>`
background-color: ${({ isDisabled, theme }) =>
isDisabled ? theme.background.quaternary : theme.accent.accent4060};
border-radius: 1px;
flex: 1;
height: 5px;
`;
export const SettingsAccountsMessageFolderIcon = ({
className,
value,
}: SettingsAccountsMessageFolderIconProps) => {
const isSelectedFolders =
value === MessageFolderImportPolicy.SELECTED_FOLDERS;
return (
<StyledCardMedia className={className}>
<StyledFolderRow>
<StyledFolderIcon isDisabled={isSelectedFolders} />
<StyledFolderLabel isDisabled={isSelectedFolders} />
</StyledFolderRow>
<StyledFolderRow>
<StyledFolderIcon isDisabled={isSelectedFolders} />
<StyledFolderLabel isDisabled={isSelectedFolders} />
</StyledFolderRow>
<StyledFolderRow>
<StyledFolderIcon />
<StyledFolderLabel />
</StyledFolderRow>
<StyledFolderRow>
<StyledFolderIcon isDisabled={isSelectedFolders} />
<StyledFolderLabel isDisabled={isSelectedFolders} />
</StyledFolderRow>
<StyledFolderRow>
<StyledFolderIcon />
<StyledFolderLabel />
</StyledFolderRow>
</StyledCardMedia>
);
};
@@ -2,8 +2,8 @@ import styled from '@emotion/styled';
import { type MessageDescriptor } from '@lingui/core';
import { Trans } from '@lingui/react';
import { type ReactNode } from 'react';
import { Card, CardContent } from 'twenty-ui/layout';
import { Radio } from 'twenty-ui/input';
import { Card, CardContent } from 'twenty-ui/layout';
type SettingsAccountsRadioSettingsCardProps<Option extends { value: string }> =
{
@@ -14,9 +14,6 @@ type SettingsAccountsRadioSettingsCardProps<Option extends { value: string }> =
};
const StyledCardContent = styled(CardContent)`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(4)};
cursor: pointer;
&:hover {
@@ -24,6 +21,12 @@ const StyledCardContent = styled(CardContent)`
}
`;
const StyledOptionHeader = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(4)};
`;
const StyledTitle = styled.div`
color: ${({ theme }) => theme.font.color.primary};
font-weight: ${({ theme }) => theme.font.weight.medium};
@@ -39,12 +42,17 @@ const StyledRadio = styled(Radio)`
margin-left: auto;
`;
const StyledExpandedContent = styled.div`
margin-top: ${({ theme }) => theme.spacing(4)};
`;
export const SettingsAccountsRadioSettingsCard = <
Option extends {
cardMedia: ReactNode;
description: MessageDescriptor;
title: MessageDescriptor;
value: string;
cardContentExpanded?: ReactNode;
},
>({
onChange,
@@ -59,21 +67,28 @@ export const SettingsAccountsRadioSettingsCard = <
divider={index < options.length - 1}
onClick={() => onChange(option.value)}
>
{option.cardMedia}
<div>
<StyledTitle>
<Trans id={option.title.id} />
</StyledTitle>
<StyledDescription>
<Trans id={option.description.id} />
</StyledDescription>
</div>
<StyledRadio
name={name}
value={option.value}
onCheckedChange={() => onChange(option.value)}
checked={value === option.value}
/>
<StyledOptionHeader>
{option.cardMedia}
<div>
<StyledTitle>
<Trans id={option.title.id} />
</StyledTitle>
<StyledDescription>
<Trans id={option.description.id} />
</StyledDescription>
</div>
<StyledRadio
name={name}
value={option.value}
onCheckedChange={() => onChange(option.value)}
checked={value === option.value}
/>
</StyledOptionHeader>
{option.cardContentExpanded && value === option.value && (
<StyledExpandedContent>
{option.cardContentExpanded}
</StyledExpandedContent>
)}
</StyledCardContent>
))}
</Card>
@@ -1,6 +1,9 @@
import { type Meta, type StoryObj } from '@storybook/react';
import { MessageChannelContactAutoCreationPolicy } from '@/accounts/types/MessageChannel';
import {
MessageChannelContactAutoCreationPolicy,
MessageFolderImportPolicy,
} from '@/accounts/types/MessageChannel';
import { SettingsAccountsMessageChannelDetails } from '@/settings/accounts/components/SettingsAccountsMessageChannelDetails';
import { ComponentDecorator } from 'twenty-ui/testing';
import { MessageChannelVisibility } from '~/generated/graphql';
@@ -27,6 +30,7 @@ const meta: Meta<typeof SettingsAccountsMessageChannelDetails> = {
isSyncEnabled: true,
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
messageFolders: [],
messageFolderImportPolicy: MessageFolderImportPolicy.ALL_FOLDERS,
},
},
argTypes: {
@@ -0,0 +1,24 @@
import { type MessageFolder } from '@/accounts/types/MessageFolder';
import { useTheme } from '@emotion/react';
import { IconFolder, IconInbox, IconSend } from 'twenty-ui/display';
type SettingsAccountsMessageFolderIconProps = {
folder: MessageFolder;
};
export const SettingsAccountsMessageFolderIcon = ({
folder,
}: SettingsAccountsMessageFolderIconProps) => {
const theme = useTheme();
if (folder.isSentFolder) {
return <IconSend size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />;
}
if (folder.name.toLowerCase().includes('inbox')) {
return (
<IconInbox size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
);
}
return <IconFolder size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />;
};
@@ -1,24 +1,113 @@
import { type MessageChannel } from '@/accounts/types/MessageChannel';
import { type MessageFolder } from '@/accounts/types/MessageFolder';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useFindOneRecord } from '@/object-record/hooks/useFindOneRecord';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { SettingsMessageFoldersEmptyStateCard } from '@/settings/accounts/components/message-folders/SettingsMessageFoldersEmptyStateCard';
import { SettingsMessageFoldersTableHeader } from '@/settings/accounts/components/message-folders/SettingsMessageFoldersTableHeader';
import { SettingsMessageFoldersTableRow } from '@/settings/accounts/components/message-folders/SettingsMessageFoldersTableRow';
import { settingsAccountsSelectedMessageChannelState } from '@/settings/accounts/states/settingsAccountsSelectedMessageChannelState';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useMemo, useState } from 'react';
import { useRecoilValue } from 'recoil';
import { Label } from 'twenty-ui/display';
import { Checkbox, CheckboxSize } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
type SettingsAccountsMessageFoldersCardProps = {
messageChannelId: string;
messageFolders: MessageFolder[];
};
const StyledTableRows = styled.div`
max-height: 300px;
overflow-y: auto;
padding-bottom: ${({ theme }) => theme.spacing(2)};
padding-top: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsAccountsMessageFoldersCard = ({
messageFolders,
}: SettingsAccountsMessageFoldersCardProps) => {
const StyledSearchInput = styled(SettingsTextInput)`
margin-bottom: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledCheckboxCell = styled(TableCell)`
align-items: center;
display: flex;
padding-right: ${({ theme }) => theme.spacing(1)};
justify-content: flex-end;
`;
const StyledSectionHeader = styled.div`
align-items: center;
background-color: ${({ theme }) => theme.background.transparent.lighter};
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
cursor: pointer;
display: flex;
height: ${({ theme }) => theme.spacing(6)};
justify-content: space-between;
padding: 0 ${({ theme }) => theme.spacing(1)};
text-align: left;
`;
const StyledLabel = styled(Label)`
color: ${({ theme }) => theme.font.color.tertiary};
margin-bottom: ${({ theme }) => theme.spacing(2)};
margin-top: ${({ theme }) => theme.spacing(2)};
`;
export const SettingsAccountsMessageFoldersCard = () => {
const { t } = useLingui();
const [search, setSearch] = useState('');
const settingsAccountsSelectedMessageChannel = useRecoilValue(
settingsAccountsSelectedMessageChannelState,
);
const { updateOneRecord } = useUpdateOneRecord<MessageFolder>({
objectNameSingular: CoreObjectNameSingular.MessageFolder,
});
const { record: messageChannel } = useFindOneRecord<MessageChannel>({
objectNameSingular: CoreObjectNameSingular.MessageChannel,
objectRecordId: settingsAccountsSelectedMessageChannel?.id,
});
const { messageFolders = [] } = messageChannel ?? {};
const filteredMessageFolders = useMemo(() => {
return messageFolders.filter((folder) =>
folder.name.toLowerCase().includes(search.toLowerCase()),
);
}, [messageFolders, search]);
const allFoldersToggled = useMemo(() => {
return filteredMessageFolders.every((folder) => folder.isSynced);
}, [filteredMessageFolders]);
const handleToggleAllFolders = async (
messageFoldersToToggle: MessageFolder[],
) => {
if (messageFoldersToToggle.length === 0) return;
const allSynced = messageFoldersToToggle.every((folder) => folder.isSynced);
const targetSyncState = !allSynced;
for (const folder of messageFoldersToToggle) {
await updateOneRecord({
idToUpdate: folder.id,
updateOneRecordInput: { isSynced: targetSyncState },
});
}
};
const handleToggleFolder = async (messageFoldersToToggle: MessageFolder) => {
await updateOneRecord({
idToUpdate: messageFoldersToToggle.id,
updateOneRecordInput: {
isSynced: !messageFoldersToToggle.isSynced,
},
});
};
if (!messageFolders || messageFolders.length === 0) {
return <SettingsMessageFoldersEmptyStateCard />;
}
@@ -26,10 +115,32 @@ export const SettingsAccountsMessageFoldersCard = ({
return (
<Section>
<Table>
<SettingsMessageFoldersTableHeader />
<StyledSearchInput
placeholder={t`Search folders...`}
value={search}
onChange={setSearch}
instanceId={'message-folders-search'}
/>
<StyledLabel>{t`Folders`}</StyledLabel>
<StyledSectionHeader>
<Label>{t`Toggle all folders`}</Label>
<StyledCheckboxCell>
<Checkbox
checked={allFoldersToggled}
onChange={() => handleToggleAllFolders(messageFolders)}
size={CheckboxSize.Small}
/>
</StyledCheckboxCell>
</StyledSectionHeader>
<StyledTableRows>
{messageFolders.map((folder) => (
<SettingsMessageFoldersTableRow key={folder.id} folder={folder} />
{filteredMessageFolders?.map((folder) => (
<SettingsMessageFoldersTableRow
key={folder.id}
folder={folder}
onSyncToggle={() => handleToggleFolder(folder)}
/>
))}
</StyledTableRows>
</Table>
@@ -1,19 +0,0 @@
import { Table } from '@/ui/layout/table/components/Table';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { Trans } from '@lingui/react/macro';
export const SettingsMessageFoldersTableHeader = () => {
return (
<Table>
<TableRow gridAutoColumns="1fr 120px">
<TableHeader>
<Trans>Folder</Trans>
</TableHeader>
<TableHeader align="center">
<Trans>Sync</Trans>
</TableHeader>
</TableRow>
</Table>
);
};
@@ -1,75 +1,50 @@
import { type MessageFolder } from '@/accounts/types/MessageFolder';
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
import { SettingsAccountsMessageFolderIcon } from '@/settings/accounts/components/message-folders/SettingsAccountsMessageFolderIcon';
import { formatFolderName } from '@/settings/accounts/components/message-folders/utils/formatFolderName.util';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { IconFolder, IconSend } from 'twenty-ui/display';
import { Toggle } from 'twenty-ui/input';
import { Checkbox, CheckboxSize } from 'twenty-ui/input';
const StyledFolderNameCell = styled.div`
align-items: center;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
gap: ${({ theme }) => theme.spacing(1)};
color: ${({ theme }) => theme.font.color.primary};
`;
const StyledTableRow = styled(TableRow)`
&:hover {
background: ${({ theme }) => theme.background.transparent.light};
}
const StyledTableRow = styled(TableRow)``;
const StyledCheckboxCell = styled(TableCell)`
display: flex;
justify-content: flex-end;
`;
type SettingsMessageFoldersTableRowProps = {
folder: MessageFolder;
onSyncToggle: () => void;
};
export const SettingsMessageFoldersTableRow = ({
folder,
onSyncToggle,
}: SettingsMessageFoldersTableRowProps) => {
const theme = useTheme();
const { updateOneRecord } = useUpdateOneRecord<MessageFolder>({
objectNameSingular: CoreObjectNameSingular.MessageFolder,
});
const handleSyncToggle = (value: boolean) => {
updateOneRecord({
idToUpdate: folder.id,
updateOneRecordInput: {
isSynced: value,
},
});
};
const formatName = (name: string) => {
return name
.split(' ')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
};
return (
<StyledTableRow gridAutoColumns="1fr 120px 70px">
<StyledTableRow gridAutoColumns="1fr 120px">
<TableCell>
<StyledFolderNameCell>
{folder.isSentFolder ? (
<IconSend size={theme.icon.size.md} stroke={theme.icon.stroke.sm} />
) : (
<IconFolder
size={theme.icon.size.md}
stroke={theme.icon.stroke.sm}
/>
)}
{formatName(folder.name)}
<SettingsAccountsMessageFolderIcon folder={folder} />
{formatFolderName(folder.name)}
</StyledFolderNameCell>
</TableCell>
<TableCell align="center">
<Toggle
value={folder.isSynced}
onChange={handleSyncToggle}
toggleSize="small"
<StyledCheckboxCell>
<Checkbox
checked={folder.isSynced}
onChange={onSyncToggle}
size={CheckboxSize.Small}
/>
</TableCell>
</StyledCheckboxCell>
</StyledTableRow>
);
};
@@ -0,0 +1,6 @@
export const formatFolderName = (name: string) => {
return name
.split(' ')
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
.join(' ');
};
@@ -0,0 +1,9 @@
import { atom } from 'recoil';
import { type MessageChannel } from '@/accounts/types/MessageChannel';
export const settingsAccountsSelectedMessageChannelState =
atom<MessageChannel | null>({
key: 'settingsAccountsSelectedMessageChannelState',
default: null,
});
@@ -66,6 +66,7 @@ export const TabList = ({
isInRightDrawer,
className,
componentInstanceId,
onChangeTab,
onAddTab,
}: TabListProps) => {
const visibleTabs = tabs.filter((tab) => !tab.hide);
@@ -115,24 +116,27 @@ export const TabList = ({
useEffect(() => {
setActiveTabId(initialActiveTabId);
}, [initialActiveTabId, setActiveTabId]);
onChangeTab?.(initialActiveTabId || '');
}, [initialActiveTabId, setActiveTabId, onChangeTab]);
const handleTabSelect = useCallback(
(tabId: string) => {
setActiveTabId(tabId);
onChangeTab?.(tabId);
},
[setActiveTabId],
[setActiveTabId, onChangeTab],
);
const handleTabSelectFromDropdown = useCallback(
(tabId: string) => {
if (behaveAsLinks) {
navigate(`#${tabId}`);
onChangeTab?.(tabId);
} else {
handleTabSelect(tabId);
}
},
[behaveAsLinks, handleTabSelect, navigate],
[behaveAsLinks, handleTabSelect, navigate, onChangeTab],
);
const handleTabWidthChange = useCallback(
@@ -240,7 +244,9 @@ export const TabList = ({
pill={tab.pill}
to={behaveAsLinks ? `#${tab.id}` : undefined}
onClick={
behaveAsLinks ? undefined : () => handleTabSelect(tab.id)
behaveAsLinks
? () => onChangeTab?.(tab.id)
: () => handleTabSelect(tab.id)
}
/>
))}
@@ -7,5 +7,6 @@ export type TabListProps = {
className?: string;
isInRightDrawer?: boolean;
componentInstanceId: string;
onChangeTab?: (tabId: string) => void;
onAddTab?: () => void;
};
@@ -244,6 +244,7 @@ export const MESSAGE_CHANNEL_STANDARD_FIELD_IDS = {
excludeGroupEmails: '20202020-45a0-4be4-9164-5820a6a109fb',
messageChannelMessageAssociations: '20202020-49b8-4766-88fd-75f1e21b3d5f',
messageFolders: '20202020-cc39-4432-9fe8-ec8ab8bbed94',
messageFolderImportPolicy: '20202020-cc39-4432-9fe8-ec8ab8bbed95',
isSyncEnabled: '20202020-d9a6-48e9-990b-b97fdf22e8dd',
syncCursor: '20202020-79d1-41cf-b738-bcf5ed61e256',
syncedAt: '20202020-263d-4c6b-ad51-137ada56f7d4',
@@ -59,6 +59,11 @@ export enum MessageChannelContactAutoCreationPolicy {
NONE = 'NONE',
}
export enum MessageFolderImportPolicy {
ALL_FOLDERS = 'ALL_FOLDERS',
SELECTED_FOLDERS = 'SELECTED_FOLDERS',
}
registerEnumType(MessageChannelVisibility, {
name: 'MessageChannelVisibility',
});
@@ -79,6 +84,10 @@ registerEnumType(MessageChannelContactAutoCreationPolicy, {
name: 'MessageChannelContactAutoCreationPolicy',
});
registerEnumType(MessageFolderImportPolicy, {
name: 'MessageFolderImportPolicy',
});
@WorkspaceEntity({
standardId: STANDARD_OBJECT_IDS.messageChannel,
namePlural: 'messageChannels',
@@ -195,6 +204,30 @@ export class MessageChannelWorkspaceEntity extends BaseWorkspaceEntity {
})
contactAutoCreationPolicy: MessageChannelContactAutoCreationPolicy;
@WorkspaceField({
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.messageFolderImportPolicy,
type: FieldMetadataType.SELECT,
label: msg`Message folder import policy`,
description: msg`Message folder import policy`,
icon: 'IconFolder',
options: [
{
value: MessageFolderImportPolicy.ALL_FOLDERS,
label: 'All folders',
position: 0,
color: 'green',
},
{
value: MessageFolderImportPolicy.SELECTED_FOLDERS,
label: 'Selected folders',
position: 1,
color: 'blue',
},
],
defaultValue: `'${MessageFolderImportPolicy.SELECTED_FOLDERS}'`,
})
messageFolderImportPolicy: MessageFolderImportPolicy;
@WorkspaceField({
standardId: MESSAGE_CHANNEL_STANDARD_FIELD_IDS.excludeNonProfessionalEmails,
type: FieldMetadataType.BOOLEAN,
@@ -33,15 +33,12 @@ export class ImapGetMessageListService {
messageFolders,
}: GetMessageListsArgs): Promise<GetMessageListsResponse> {
let client: ImapFlow | null = null;
const syncedMessageFolders = messageFolders.filter(
(folder) => folder.isSynced,
);
try {
client = await this.imapClientProvider.getClient(connectedAccount);
const result: GetMessageListsResponse = [];
for (const folder of syncedMessageFolders) {
for (const folder of messageFolders) {
this.logger.log(`Processing folder: ${folder.name}`);
try {
@@ -79,7 +76,7 @@ export class ImapGetMessageListService {
this.imapHandleErrorService.handleImapMessageListFetchError(error);
return syncedMessageFolders.map((folder) => ({
return messageFolders.map((folder) => ({
messageExternalIds: [],
nextSyncCursor: folder.syncCursor || '',
previousSyncCursor: folder.syncCursor,
@@ -47,11 +47,7 @@ export class MicrosoftGetMessageListService {
);
}
const syncedMessageFolders = messageFolders.filter(
(folder) => folder.isSynced,
);
for (const folder of syncedMessageFolders) {
for (const folder of messageFolders) {
const response = await this.getMessageList(connectedAccount, folder);
result.push({
@@ -2,7 +2,10 @@ import { Injectable } from '@nestjs/common';
import { ConnectedAccountProvider } from 'twenty-shared/types';
import { type MessageChannelWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import {
MessageFolderImportPolicy,
type MessageChannelWorkspaceEntity,
} from 'src/modules/messaging/common/standard-objects/message-channel.workspace-entity';
import { MessageFolderWorkspaceEntity } from 'src/modules/messaging/common/standard-objects/message-folder.workspace-entity';
import { GmailGetMessageListService } from 'src/modules/messaging/message-import-manager/drivers/gmail/services/gmail-get-message-list.service';
import { ImapGetMessageListService } from 'src/modules/messaging/message-import-manager/drivers/imap/services/imap-get-message-list.service';
@@ -28,8 +31,14 @@ export class MessagingGetMessageListService {
public async getMessageLists(
messageChannel: MessageChannelWorkspaceEntity,
messageFoldersToSync: MessageFolder[],
messageFolders: MessageFolder[],
): Promise<GetMessageListsResponse> {
const messageFoldersToSync: MessageFolder[] =
messageChannel.messageFolderImportPolicy ===
MessageFolderImportPolicy.ALL_FOLDERS
? messageFolders
: messageFolders.filter((folder) => folder.isSynced);
switch (messageChannel.connectedAccount.provider) {
case ConnectedAccountProvider.GOOGLE:
return await this.gmailGetMessageListService.getMessageLists({