feat: message folders control (#14144)
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { type ImapSmtpCaldavAccount } from '@/accounts/types/ImapSmtpCaldavAccount';
|
||||
import { type MessageFolder } from '@/accounts/types/MessageFolder';
|
||||
import { type ConnectedAccountProvider } from 'twenty-shared/types';
|
||||
import { type MessageChannelVisibility } from '~/generated/graphql';
|
||||
|
||||
@@ -35,6 +36,7 @@ export type MessageChannel = {
|
||||
excludeNonProfessionalEmails: boolean;
|
||||
excludeGroupEmails: boolean;
|
||||
isSyncEnabled: boolean;
|
||||
messageFolders: MessageFolder[];
|
||||
visibility: MessageChannelVisibility;
|
||||
syncStatus: MessageChannelSyncStatus;
|
||||
syncStage: MessageChannelSyncStage;
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export type MessageFolder = {
|
||||
id: string;
|
||||
name: string;
|
||||
syncCursor: string;
|
||||
isSentFolder: boolean;
|
||||
isSynced: boolean;
|
||||
messageChannelId: string;
|
||||
__typename: 'MessageFolder';
|
||||
};
|
||||
+9
@@ -47,6 +47,15 @@ export const triggerAttachRelationOptimisticEffect = ({
|
||||
}
|
||||
|
||||
if (fieldValueIsObjectRecordConnectionWithRefs) {
|
||||
const recordAlreadyExists = targetRecordFieldValue.edges.some(
|
||||
(edge: RecordGqlRefEdge) =>
|
||||
edge.node.__ref === sourceRecordReference.__ref,
|
||||
);
|
||||
|
||||
if (recordAlreadyExists) {
|
||||
return targetRecordFieldValue;
|
||||
}
|
||||
|
||||
const nextEdges: RecordGqlRefEdge[] = [
|
||||
...targetRecordFieldValue.edges,
|
||||
{
|
||||
|
||||
@@ -15,6 +15,7 @@ export enum CoreObjectNameSingular {
|
||||
Message = 'message',
|
||||
MessageChannel = 'messageChannel',
|
||||
MessageParticipant = 'messageParticipant',
|
||||
MessageFolder = 'messageFolder',
|
||||
MessageThread = 'messageThread',
|
||||
Note = 'note',
|
||||
NoteTarget = 'noteTarget',
|
||||
|
||||
+24
-2
@@ -7,12 +7,17 @@ import {
|
||||
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 { SettingsAccountsMessageVisibilityCard } from '@/settings/accounts/components/SettingsAccountsMessageVisibilityCard';
|
||||
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
|
||||
import { type MessageChannelVisibility } from '~/generated-metadata/graphql';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { Card, Section } from 'twenty-ui/layout';
|
||||
import { H2Title, IconBriefcase, IconUsers } from 'twenty-ui/display';
|
||||
import { Card, Section } from 'twenty-ui/layout';
|
||||
import {
|
||||
FeatureFlagKey,
|
||||
type MessageChannelVisibility,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type SettingsAccountsMessageChannelDetailsProps = {
|
||||
messageChannel: Pick<
|
||||
@@ -23,6 +28,7 @@ type SettingsAccountsMessageChannelDetailsProps = {
|
||||
| 'excludeNonProfessionalEmails'
|
||||
| 'excludeGroupEmails'
|
||||
| 'isSyncEnabled'
|
||||
| 'messageFolders'
|
||||
>;
|
||||
};
|
||||
|
||||
@@ -39,6 +45,10 @@ export const SettingsAccountsMessageChannelDetails = ({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||
});
|
||||
|
||||
const isFolderControlEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_MESSAGE_FOLDER_CONTROL_ENABLED,
|
||||
);
|
||||
|
||||
const handleVisibilityChange = (value: MessageChannelVisibility) => {
|
||||
updateOneRecord({
|
||||
idToUpdate: messageChannel.id,
|
||||
@@ -79,6 +89,18 @@ export const SettingsAccountsMessageChannelDetails = ({
|
||||
|
||||
return (
|
||||
<StyledDetailsContainer>
|
||||
{isFolderControlEnabled && messageChannel.messageFolders && (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Folder Management`}
|
||||
description={t`Control which folders are synced`}
|
||||
/>
|
||||
<SettingsAccountsMessageFoldersCard
|
||||
messageChannelId={messageChannel.id}
|
||||
messageFolders={messageChannel.messageFolders}
|
||||
/>
|
||||
</Section>
|
||||
)}
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Visibility`}
|
||||
|
||||
+13
-6
@@ -4,7 +4,9 @@ import { useRecoilValue } from 'recoil';
|
||||
import { type ConnectedAccount } from '@/accounts/types/ConnectedAccount';
|
||||
import { type MessageChannel } from '@/accounts/types/MessageChannel';
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { useObjectMetadataItem } from '@/object-metadata/hooks/useObjectMetadataItem';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { generateDepthOneRecordGqlFields } from '@/object-record/graphql/utils/generateDepthOneRecordGqlFields';
|
||||
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
|
||||
import { SettingsAccountsMessageChannelDetails } from '@/settings/accounts/components/SettingsAccountsMessageChannelDetails';
|
||||
import { SettingsNewAccountSection } from '@/settings/accounts/components/SettingsNewAccountSection';
|
||||
@@ -25,6 +27,10 @@ export const SettingsAccountsMessageChannelsContainer = () => {
|
||||
);
|
||||
const currentWorkspaceMember = useRecoilValue(currentWorkspaceMemberState);
|
||||
|
||||
const messageChannelObjectMetadataItem = useObjectMetadataItem({
|
||||
objectNameSingular: CoreObjectNameSingular.MessageChannel,
|
||||
});
|
||||
|
||||
const { records: accounts } = useFindManyRecords<ConnectedAccount>({
|
||||
objectNameSingular: CoreObjectNameSingular.ConnectedAccount,
|
||||
filter: {
|
||||
@@ -48,15 +54,16 @@ export const SettingsAccountsMessageChannelsContainer = () => {
|
||||
eq: true,
|
||||
},
|
||||
},
|
||||
recordGqlFields: generateDepthOneRecordGqlFields(
|
||||
messageChannelObjectMetadataItem,
|
||||
),
|
||||
skip: !accounts.length,
|
||||
});
|
||||
|
||||
const tabs = [
|
||||
...messageChannels.map((messageChannel) => ({
|
||||
id: messageChannel.id,
|
||||
title: messageChannel.handle,
|
||||
})),
|
||||
];
|
||||
const tabs = messageChannels.map((messageChannel) => ({
|
||||
id: messageChannel.id,
|
||||
title: messageChannel.handle,
|
||||
}));
|
||||
|
||||
if (!messageChannels.length) {
|
||||
return <SettingsNewAccountSection />;
|
||||
|
||||
+2
-1
@@ -2,11 +2,11 @@ import { type Meta, type StoryObj } from '@storybook/react';
|
||||
|
||||
import { MessageChannelContactAutoCreationPolicy } from '@/accounts/types/MessageChannel';
|
||||
import { SettingsAccountsMessageChannelDetails } from '@/settings/accounts/components/SettingsAccountsMessageChannelDetails';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
import { MessageChannelVisibility } from '~/generated/graphql';
|
||||
import { I18nFrontDecorator } from '~/testing/decorators/I18nFrontDecorator';
|
||||
import { ObjectMetadataItemsDecorator } from '~/testing/decorators/ObjectMetadataItemsDecorator';
|
||||
import { SnackBarDecorator } from '~/testing/decorators/SnackBarDecorator';
|
||||
import { ComponentDecorator } from 'twenty-ui/testing';
|
||||
|
||||
const meta: Meta<typeof SettingsAccountsMessageChannelDetails> = {
|
||||
title:
|
||||
@@ -26,6 +26,7 @@ const meta: Meta<typeof SettingsAccountsMessageChannelDetails> = {
|
||||
excludeGroupEmails: false,
|
||||
isSyncEnabled: true,
|
||||
visibility: MessageChannelVisibility.SHARE_EVERYTHING,
|
||||
messageFolders: [],
|
||||
},
|
||||
},
|
||||
argTypes: {
|
||||
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
import { type MessageFolder } from '@/accounts/types/MessageFolder';
|
||||
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 { Table } from '@/ui/layout/table/components/Table';
|
||||
import styled from '@emotion/styled';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
|
||||
type SettingsAccountsMessageFoldersCardProps = {
|
||||
messageChannelId: string;
|
||||
messageFolders: MessageFolder[];
|
||||
};
|
||||
|
||||
const StyledTableRows = styled.div`
|
||||
padding-bottom: ${({ theme }) => theme.spacing(2)};
|
||||
padding-top: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
export const SettingsAccountsMessageFoldersCard = ({
|
||||
messageFolders,
|
||||
}: SettingsAccountsMessageFoldersCardProps) => {
|
||||
if (!messageFolders || messageFolders.length === 0) {
|
||||
return <SettingsMessageFoldersEmptyStateCard />;
|
||||
}
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<Table>
|
||||
<SettingsMessageFoldersTableHeader />
|
||||
<StyledTableRows>
|
||||
{messageFolders.map((folder) => (
|
||||
<SettingsMessageFoldersTableRow key={folder.id} folder={folder} />
|
||||
))}
|
||||
</StyledTableRows>
|
||||
</Table>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
import { useTheme } from '@emotion/react';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconFolder } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
|
||||
const StyledEmptyState = styled.div`
|
||||
align-items: center;
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
padding: ${({ theme }) => theme.spacing(8)};
|
||||
text-align: center;
|
||||
`;
|
||||
|
||||
export const SettingsMessageFoldersEmptyStateCard = () => {
|
||||
const theme = useTheme();
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<StyledEmptyState>
|
||||
<IconFolder size={theme.icon.size.md} />
|
||||
<div>{t`No folders found for this account`}</div>
|
||||
</StyledEmptyState>
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
+19
@@ -0,0 +1,19 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
import { type MessageFolder } from '@/accounts/types/MessageFolder';
|
||||
import { CoreObjectNameSingular } from '@/object-metadata/types/CoreObjectNameSingular';
|
||||
import { useUpdateOneRecord } from '@/object-record/hooks/useUpdateOneRecord';
|
||||
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';
|
||||
|
||||
const StyledFolderNameCell = styled.div`
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: ${({ theme }) => theme.spacing(2)};
|
||||
`;
|
||||
|
||||
const StyledTableRow = styled(TableRow)`
|
||||
&:hover {
|
||||
background: ${({ theme }) => theme.background.transparent.light};
|
||||
}
|
||||
`;
|
||||
|
||||
type SettingsMessageFoldersTableRowProps = {
|
||||
folder: MessageFolder;
|
||||
};
|
||||
|
||||
export const SettingsMessageFoldersTableRow = ({
|
||||
folder,
|
||||
}: 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">
|
||||
<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)}
|
||||
</StyledFolderNameCell>
|
||||
</TableCell>
|
||||
<TableCell align="center">
|
||||
<Toggle
|
||||
value={folder.isSynced}
|
||||
onChange={handleSyncToggle}
|
||||
toggleSize="small"
|
||||
/>
|
||||
</TableCell>
|
||||
</StyledTableRow>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user