Message Folders Redesign and Import Policy (#14714)
This commit is contained in:
@@ -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;
|
||||
|
||||
+17
-6
@@ -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>
|
||||
)}
|
||||
|
||||
+19
-1
@@ -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>
|
||||
)}
|
||||
|
||||
+46
@@ -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}
|
||||
/>
|
||||
);
|
||||
+69
@@ -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>
|
||||
);
|
||||
};
|
||||
+34
-19
@@ -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>
|
||||
|
||||
+5
-1
@@ -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: {
|
||||
|
||||
+24
@@ -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} />;
|
||||
};
|
||||
+123
-12
@@ -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>
|
||||
|
||||
-19
@@ -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>
|
||||
);
|
||||
};
|
||||
+22
-47
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
export const formatFolderName = (name: string) => {
|
||||
return name
|
||||
.split(' ')
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
|
||||
.join(' ');
|
||||
};
|
||||
+9
@@ -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;
|
||||
};
|
||||
|
||||
+1
@@ -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',
|
||||
|
||||
+33
@@ -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,
|
||||
|
||||
+2
-5
@@ -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,
|
||||
|
||||
+1
-5
@@ -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({
|
||||
|
||||
+11
-2
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user