message campaign redesign (#22508)

<!-- This is an auto-generated description by cubic. -->
<a
href="https://cubic.dev/pr/twentyhq/twenty/pull/22508?utm_source=github"
target="_blank" rel="noopener noreferrer"
data-no-image-dialog="true"><picture><source
media="(prefers-color-scheme: dark)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"><source
media="(prefers-color-scheme: light)"
srcset="https://www.cubic.dev/buttons/review-in-cubic-light.svg"><img
alt="Review in cubic"
src="https://www.cubic.dev/buttons/review-in-cubic-dark.svg"></picture></a>
<!-- End of auto-generated description by cubic. -->
This commit is contained in:
neo773
2026-07-05 17:14:34 +05:30
committed by GitHub
parent e90ab56c7a
commit 904957ea1e
19 changed files with 423 additions and 357 deletions
@@ -182,22 +182,24 @@ const SettingsLegalDpaNew = lazy(() =>
})),
);
const SettingsWorkspaceEmail = lazy(() =>
import('~/pages/settings/email/SettingsWorkspaceEmail').then((module) => ({
default: module.SettingsWorkspaceEmail,
})),
const SettingsWorkspaceCommunications = lazy(() =>
import('~/pages/settings/communications/SettingsWorkspaceCommunications').then(
(module) => ({
default: module.SettingsWorkspaceCommunications,
}),
),
);
const SettingsWorkspaceEmailGroupChannelDetail = lazy(() =>
import('~/pages/settings/email/SettingsWorkspaceEmailGroupChannelDetail').then(
const SettingsWorkspaceCommunicationGroupChannelDetail = lazy(() =>
import('~/pages/settings/communications/SettingsWorkspaceCommunicationGroupChannelDetail').then(
(module) => ({
default: module.SettingsWorkspaceEmailGroupChannelDetail,
default: module.SettingsWorkspaceCommunicationGroupChannelDetail,
}),
),
);
const SettingsWorkspaceNewUnsubscribeTopic = lazy(() =>
import('~/pages/settings/email/SettingsWorkspaceNewUnsubscribeTopic').then(
import('~/pages/settings/communications/SettingsWorkspaceNewUnsubscribeTopic').then(
(module) => ({
default: module.SettingsWorkspaceNewUnsubscribeTopic,
}),
@@ -205,7 +207,7 @@ const SettingsWorkspaceNewUnsubscribeTopic = lazy(() =>
);
const SettingsWorkspaceUnsubscribeTopicDetail = lazy(() =>
import('~/pages/settings/email/SettingsWorkspaceUnsubscribeTopicDetail').then(
import('~/pages/settings/communications/SettingsWorkspaceUnsubscribeTopicDetail').then(
(module) => ({
default: module.SettingsWorkspaceUnsubscribeTopicDetail,
}),
@@ -703,8 +705,8 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
>
<Route path={SettingsPath.General} element={<SettingsGeneral />} />
<Route
path={SettingsPath.WorkspaceEmail}
element={<SettingsWorkspaceEmail />}
path={SettingsPath.WorkspaceCommunications}
element={<SettingsWorkspaceCommunications />}
/>
<Route
path={SettingsPath.NewEmailGroupChannel}
@@ -712,7 +714,7 @@ export const SettingsRoutes = ({ isAdminPageEnabled }: SettingsRoutesProps) => (
/>
<Route
path={SettingsPath.EmailGroupChannelDetail}
element={<SettingsWorkspaceEmailGroupChannelDetail />}
element={<SettingsWorkspaceCommunicationGroupChannelDetail />}
/>
<Route
path={SettingsPath.NewUnsubscribeTopic}
@@ -53,8 +53,8 @@ export const SettingsAccountsNewEmailGroupChannel = () => {
href: getSettingsPath(SettingsPath.General),
},
{
children: t`Email`,
href: getSettingsPath(SettingsPath.WorkspaceEmail),
children: t`Communications`,
href: getSettingsPath(SettingsPath.WorkspaceCommunications),
},
{ children: t`New Email Channel` },
]}
@@ -63,7 +63,7 @@ export const SettingsAccountsNewEmailGroupChannel = () => {
isSaveDisabled={!canSave}
isCancelDisabled={loading}
isLoading={loading}
onCancel={() => navigate(SettingsPath.WorkspaceEmail)}
onCancel={() => navigate(SettingsPath.WorkspaceCommunications)}
onSave={handleSave}
/>
}
@@ -80,6 +80,11 @@ export const SettingsAccountsNewEmailGroupChannel = () => {
placeholder="support@mycompany.com"
value={handle}
onChange={setHandle}
onInputEnter={() => {
if (canSave) {
handleSave();
}
}}
disabled={loading}
/>
</Section>
@@ -1,3 +1,4 @@
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
@@ -6,40 +7,41 @@ import { styled } from '@linaria/react';
import { t } from '@lingui/core/macro';
import { capitalize, isDefined } from 'twenty-shared/utils';
import { Status } from 'twenty-ui/data-display';
import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces';
import { type ThemeColor } from 'twenty-ui/theme';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
type RecordStatus = {
status: string;
statusColor: ThemeColor;
};
type DnsRecordBase = {
type DnsRecord = {
type: string;
key: string;
value: string;
priority?: number | null;
ttl?: string;
status?: string;
statusColor?: ThemeColor;
};
type DnsRecord = DnsRecordBase | (DnsRecordBase & RecordStatus);
type SettingsDnsRecordsTableProps = {
records: DnsRecord[];
};
const StyledTableRowContainer = styled.div`
> * > * {
max-width: 100%;
min-width: 0;
overflow: hidden;
}
const StyledRecordTableRow = styled(TableRow)`
margin-top: ${themeCssVariables.spacing[2]};
`;
const StyledTableCellFontWrapper = styled.div`
display: contents;
font-family: monospace;
const StyledCopyableCell = styled.div`
min-width: 0;
width: 100%;
& input {
cursor: pointer;
pointer-events: none;
}
&:hover input {
background-color: ${themeCssVariables.background.transparent.light};
border-color: ${themeCssVariables.border.color.strong};
}
`;
export const SettingsDnsRecordsTable = ({
@@ -51,91 +53,101 @@ export const SettingsDnsRecordsTable = ({
return null;
}
const hasTtlRecords = records.some((record) => isDefined(record.ttl));
const hasStatusRecords = records.some((record) => 'status' in record);
const hasPriorityRecords = records.some((record) =>
const hasPriorityColumn = records.some((record) =>
isDefined(record.priority),
);
const hasTtlColumn = records.some((record) => isDefined(record.ttl));
const hasStatusColumn = records.some((record) => isDefined(record.status));
const buildGridColumns = () => {
const baseColumns = ['max-content', '1fr', '1fr'];
if (hasPriorityRecords) baseColumns.push('max-content');
if (hasTtlRecords) baseColumns.push('max-content');
if (hasStatusRecords) baseColumns.push('max-content');
return baseColumns.join(' ');
};
const gridAutoColumns = buildGridColumns();
const gridAutoColumns = [
'100px',
'minmax(0, 1fr)',
'minmax(0, 1.5fr)',
...(hasPriorityColumn ? ['max-content'] : []),
...(hasTtlColumn ? ['max-content'] : []),
...(hasStatusColumn ? ['max-content'] : []),
].join(' ');
return (
<Table>
<StyledTableRowContainer>
<TableRow gridAutoColumns={gridAutoColumns}>
<TableHeader align="center">{t`Type`}</TableHeader>
<TableHeader align="center">{t`Key`}</TableHeader>
<TableHeader align="center">{t`Value`}</TableHeader>
{hasPriorityRecords && (
<TableHeader align="center">{t`Priority`}</TableHeader>
<TableRow gridAutoColumns={gridAutoColumns}>
<TableHeader>{t`Type`}</TableHeader>
<TableHeader>{t`Name`}</TableHeader>
<TableHeader>{t`Value`}</TableHeader>
{hasPriorityColumn && (
<TableHeader align="center">{t`Priority`}</TableHeader>
)}
{hasTtlColumn && <TableHeader align="center">{t`TTL`}</TableHeader>}
{hasStatusColumn && (
<TableHeader align="center">{t`Status`}</TableHeader>
)}
</TableRow>
{records.map((record, index) => (
<StyledRecordTableRow
key={`${index}-${record.type}-${record.key}`}
gridAutoColumns={gridAutoColumns}
>
<TableCell>
<StyledCopyableCell
onClick={() =>
copyToClipboard(record.type, t`Copied to clipboard`)
}
>
<SettingsTextInput
instanceId={`dns-record-type-${index}`}
value={record.type}
sizeVariant="sm"
disabled
fullWidth
/>
</StyledCopyableCell>
</TableCell>
<TableCell>
<StyledCopyableCell
onClick={() =>
copyToClipboard(record.key, t`Copied to clipboard`)
}
>
<SettingsTextInput
instanceId={`dns-record-name-${index}`}
value={record.key}
sizeVariant="sm"
disabled
fullWidth
/>
</StyledCopyableCell>
</TableCell>
<TableCell>
<StyledCopyableCell
onClick={() =>
copyToClipboard(record.value, t`Copied to clipboard`)
}
>
<SettingsTextInput
instanceId={`dns-record-value-${index}`}
value={record.value}
sizeVariant="sm"
disabled
fullWidth
/>
</StyledCopyableCell>
</TableCell>
{hasPriorityColumn && (
<TableCell align="center">{record.priority}</TableCell>
)}
{hasTtlRecords && <TableHeader align="center">{t`TTL`}</TableHeader>}
{hasStatusRecords && (
<TableHeader align="center">{t`Status`}</TableHeader>
{hasTtlColumn && <TableCell align="center">{record.ttl}</TableCell>}
{hasStatusColumn && (
<TableCell align="center">
{isDefined(record.status) && isDefined(record.statusColor) && (
<Status
color={record.statusColor}
text={capitalize(record.status)}
/>
)}
</TableCell>
)}
</TableRow>
</StyledTableRowContainer>
{records.map((record) => (
<StyledTableRowContainer key={record.value}>
<TableRow gridAutoColumns={gridAutoColumns}>
<TableCell>{record.type}</TableCell>
<StyledTableCellFontWrapper>
<TableCell
overflow="hidden"
onClick={() => {
copyToClipboard(record.key || '');
}}
>
<OverflowingTextWithTooltip text={record.key} />
</TableCell>
</StyledTableCellFontWrapper>
<StyledTableCellFontWrapper>
<TableCell
overflow="hidden"
onClick={() => {
copyToClipboard(record.value);
}}
>
<OverflowingTextWithTooltip text={record.value} />
</TableCell>
</StyledTableCellFontWrapper>
{hasPriorityRecords && (
<StyledTableCellFontWrapper>
<TableCell overflow="hidden">{record.priority}</TableCell>
</StyledTableCellFontWrapper>
)}
{hasTtlRecords && (
<StyledTableCellFontWrapper>
<TableCell overflow="hidden">{record.ttl}</TableCell>
</StyledTableCellFontWrapper>
)}
{hasStatusRecords && (
<StyledTableCellFontWrapper>
<TableCell overflow="hidden">
{'status' in record ? (
<Status
color={record.statusColor}
text={capitalize(record.status)}
/>
) : null}
</TableCell>
</StyledTableCellFontWrapper>
)}
</TableRow>
</StyledTableRowContainer>
</StyledRecordTableRow>
))}
</Table>
);
@@ -6,7 +6,7 @@ import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { IconPlus } from 'twenty-ui/icon';
import { IconChevronRight, IconPlus } from 'twenty-ui/icon';
import { H2Title } from 'twenty-ui/typography';
import { Button } from 'twenty-ui/input';
import { Section } from 'twenty-ui/layout';
@@ -53,6 +53,7 @@ type SettingsTableListSectionProps<Item extends { id: string }> = {
items: Item[];
columns: SettingsTableListSectionColumn<Item>[];
gridAutoColumns: string;
showRowChevron?: boolean;
onRowClick?: (item: Item) => void;
footerButtonLabel: string;
onFooterButtonClick: () => void;
@@ -67,55 +68,71 @@ export const SettingsTableListSection = <
items,
columns,
gridAutoColumns,
showRowChevron = false,
onRowClick,
footerButtonLabel,
onFooterButtonClick,
}: SettingsTableListSectionProps<Item>) => (
<Section>
<H2Title
title={title}
description={description}
adornment={headerAdornment}
/>
{items.length > 0 && (
<Table>
<TableRow gridAutoColumns={gridAutoColumns}>
{columns.map((column) => (
<TableHeader
key={column.label}
align={column.align}
padding={HEADER_PADDING}
>
{column.label}
</TableHeader>
))}
</TableRow>
<StyledTableRows>
{items.map((item) => (
<StyledRowWrapper key={item.id} clickable={Boolean(onRowClick)}>
<TableRow
gridAutoColumns={gridAutoColumns}
onClick={onRowClick ? () => onRowClick(item) : undefined}
>
{columns.map((column) => (
<TableCell key={column.label} align={column.align}>
<column.Cell item={item} />
</TableCell>
))}
</TableRow>
</StyledRowWrapper>
))}
</StyledTableRows>
</Table>
)}
<StyledFooter>
<Button
Icon={IconPlus}
title={footerButtonLabel}
variant="secondary"
size="small"
onClick={onFooterButtonClick}
}: SettingsTableListSectionProps<Item>) => {
const resolvedGridAutoColumns = showRowChevron
? `${gridAutoColumns} auto`
: gridAutoColumns;
return (
<Section>
<H2Title
title={title}
description={description}
adornment={headerAdornment}
/>
</StyledFooter>
</Section>
);
{items.length > 0 && (
<Table>
<TableRow gridAutoColumns={resolvedGridAutoColumns}>
{columns.map((column) => (
<TableHeader
key={column.label}
align={column.align}
padding={HEADER_PADDING}
>
{column.label}
</TableHeader>
))}
{showRowChevron && <TableHeader padding={HEADER_PADDING} />}
</TableRow>
<StyledTableRows>
{items.map((item) => (
<StyledRowWrapper key={item.id} clickable={Boolean(onRowClick)}>
<TableRow
gridAutoColumns={resolvedGridAutoColumns}
onClick={onRowClick ? () => onRowClick(item) : undefined}
>
{columns.map((column) => (
<TableCell key={column.label} align={column.align}>
<column.Cell item={item} />
</TableCell>
))}
{showRowChevron && (
<TableCell
align="right"
color={themeCssVariables.font.color.light}
>
<IconChevronRight size={16} />
</TableCell>
)}
</TableRow>
</StyledRowWrapper>
))}
</StyledTableRows>
</Table>
)}
<StyledFooter>
<Button
Icon={IconPlus}
title={footerButtonLabel}
variant="secondary"
size="small"
onClick={onFooterButtonClick}
/>
</StyledFooter>
</Section>
);
};
@@ -28,6 +28,7 @@ import {
IconLayout,
IconMail,
IconMessage,
IconMessageCircle,
IconPlug,
IconServer,
IconSettings,
@@ -173,9 +174,9 @@ const useSettingsNavigationItems = (): SettingsNavigationSection[] => {
isHidden: !permissionMap[PermissionFlagType.AI_SETTINGS],
},
{
label: t`Email`,
path: SettingsPath.WorkspaceEmail,
Icon: IconMail,
label: t`Communications`,
path: SettingsPath.WorkspaceCommunications,
Icon: IconMessageCircle,
isHidden:
!isEmailGroupFeatureEnabled ||
!permissionMap[PermissionFlagType.WORKSPACE],
@@ -1,18 +1,14 @@
import { useLazyQuery } from '@apollo/client/react';
import { useLingui } from '@lingui/react/macro';
import { useUnsubscribeTopics } from '@/activities/emails/hooks/useUnsubscribeTopics';
import { SettingsTableListSection } from '@/settings/components/SettingsTableListSection';
import { GET_UNSUBSCRIBE_PAGE_PREVIEW_URL } from '@/settings/unsubscribe-topics/graphql/queries/getUnsubscribePagePreviewUrl';
import { SettingsPath } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import {
type UnsubscribeTopicsQuery,
UnsubscribeTopicVisibility,
} from '~/generated-metadata/graphql';
import { Status } from 'twenty-ui/data-display';
import { IconExternalLink } from 'twenty-ui/icon';
import { Button } from 'twenty-ui/input';
import { Pill, Status } from 'twenty-ui/data-display';
import { IconLock } from 'twenty-ui/icon';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
type UnsubscribeTopic = UnsubscribeTopicsQuery['unsubscribeTopics'][number];
@@ -21,49 +17,25 @@ export const SettingsWorkspaceUnsubscribeTopicSection = () => {
const { t } = useLingui();
const navigateSettings = useNavigateSettings();
const { unsubscribeTopics } = useUnsubscribeTopics();
const [getPreviewUrl] = useLazyQuery<{
unsubscribePagePreviewUrl: string;
}>(GET_UNSUBSCRIBE_PAGE_PREVIEW_URL);
// Open the tab synchronously on click (so it isn't popup-blocked), then point
// it at the freshly minted preview URL once the query resolves.
const handlePreview = () => {
const previewWindow = window.open('', '_blank');
void getPreviewUrl()
.then(({ data }) => {
const url = data?.unsubscribePagePreviewUrl;
if (isDefined(previewWindow) && isDefined(url)) {
previewWindow.location.href = url;
} else {
previewWindow?.close();
}
})
.catch(() => previewWindow?.close());
};
const title = t`Unsubscribe topics`;
const description = t`Email categories recipients can opt out of`;
const organizationPill = <Pill Icon={IconLock} label={t`Organization`} />;
return (
<SettingsTableListSection<UnsubscribeTopic>
title={t`Unsubscribe Topics`}
description={t`Email categories recipients can opt out of.`}
headerAdornment={
<Button
title={t`Preview`}
variant="secondary"
size="small"
Icon={IconExternalLink}
onClick={handlePreview}
/>
}
title={title}
description={description}
headerAdornment={organizationPill}
items={unsubscribeTopics}
columns={[
{
label: t`Name`,
label: t`Topic`,
Cell: ({ item }) => <>{item.name ?? t`Untitled topic`}</>,
},
{
label: t`Visibility`,
align: 'right',
Cell: ({ item }) =>
item.visibility === UnsubscribeTopicVisibility.PUBLIC ? (
<Status color="blue" text={t`Public`} />
@@ -73,12 +45,13 @@ export const SettingsWorkspaceUnsubscribeTopicSection = () => {
},
]}
gridAutoColumns="1fr 1fr"
showRowChevron
onRowClick={(topic) =>
navigateSettings(SettingsPath.UnsubscribeTopicDetail, {
unsubscribeTopicId: topic.id,
})
}
footerButtonLabel={t`Add unsubscribe topic`}
footerButtonLabel={t`Add topic`}
onFooterButtonClick={() =>
navigateSettings(SettingsPath.NewUnsubscribeTopic)
}
@@ -1,23 +0,0 @@
import { styled } from '@linaria/react';
import { type MessageChannel } from '@/accounts/types/MessageChannel';
import { themeCssVariables } from 'twenty-ui/theme-constants';
const StyledForwardingCell = styled.div`
color: ${themeCssVariables.font.color.tertiary};
font-family: monospace;
font-size: ${themeCssVariables.font.size.sm};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
type SettingsWorkspaceEmailGroupForwardingCellProps = {
item: MessageChannel;
};
export const SettingsWorkspaceEmailGroupForwardingCell = ({
item,
}: SettingsWorkspaceEmailGroupForwardingCellProps) => (
<StyledForwardingCell>{item.handle}</StyledForwardingCell>
);
@@ -4,9 +4,10 @@ import { type MessageChannel } from '@/accounts/types/MessageChannel';
import { useMyMessageChannels } from '@/settings/accounts/hooks/useMyMessageChannels';
import { SettingsTableListSection } from '@/settings/components/SettingsTableListSection';
import { SettingsWorkspaceEmailChannelDomainStatusCell } from '@/settings/workspace/components/SettingsWorkspaceEmailChannelDomainStatusCell';
import { SettingsWorkspaceEmailGroupForwardingCell } from '@/settings/workspace/components/SettingsWorkspaceEmailGroupForwardingCell';
import { SettingsWorkspaceEmailGroupSourceCell } from '@/settings/workspace/components/SettingsWorkspaceEmailGroupSourceCell';
import { MessageChannelType, SettingsPath } from 'twenty-shared/types';
import { Pill } from 'twenty-ui/data-display';
import { IconLock } from 'twenty-ui/icon';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
export const SettingsWorkspaceEmailGroupSection = () => {
@@ -20,22 +21,20 @@ export const SettingsWorkspaceEmailGroupSection = () => {
return (
<SettingsTableListSection<MessageChannel>
title={t`Email Channels`}
description={t`Shared addresses your workspace uses to send and receive email.`}
title={t`Channels`}
description={t`Addresses your workspace uses to send and receive email from shared inboxes`}
headerAdornment={<Pill Icon={IconLock} label={t`Organization`} />}
items={emailGroupChannels}
columns={[
{ label: t`Source`, Cell: SettingsWorkspaceEmailGroupSourceCell },
{
label: t`Forwarding address`,
Cell: SettingsWorkspaceEmailGroupForwardingCell,
},
{ label: t`Email`, Cell: SettingsWorkspaceEmailGroupSourceCell },
{
label: t`Domain`,
align: 'right',
Cell: SettingsWorkspaceEmailChannelDomainStatusCell,
},
]}
gridAutoColumns="1fr 1fr 1fr"
gridAutoColumns="1fr 1fr"
showRowChevron
onRowClick={(channel) =>
navigateSettings(SettingsPath.EmailGroupChannelDetail, {
messageChannelId: channel.id,
@@ -1,7 +1,6 @@
import { styled } from '@linaria/react';
import { type MessageChannel } from '@/accounts/types/MessageChannel';
import { IconMail } from 'twenty-ui/icon';
import { OverflowingTextWithTooltip } from 'twenty-ui/surfaces';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -10,7 +9,6 @@ const StyledNameCell = styled.div`
color: ${themeCssVariables.font.color.primary};
display: flex;
font-weight: ${themeCssVariables.font.weight.medium};
gap: ${themeCssVariables.spacing[2]};
min-width: 0;
`;
@@ -25,7 +23,6 @@ export const SettingsWorkspaceEmailGroupSourceCell = ({
return (
<StyledNameCell>
<IconMail size={16} />
<OverflowingTextWithTooltip text={sourceHandle ?? '—'} />
</StyledNameCell>
);
@@ -3,11 +3,12 @@ import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { useParams } from 'react-router-dom';
import { SettingsAccountsMessageChannelDetails } from '@/settings/accounts/components/SettingsAccountsMessageChannelDetails';
import { useDeleteEmailGroupChannel } from '@/settings/accounts/hooks/useDeleteEmailGroupChannel';
import { useMyMessageChannels } from '@/settings/accounts/hooks/useMyMessageChannels';
import { getEmailChannelDomain } from '@/settings/accounts/utils/getEmailChannelDomain';
import { SettingsDnsRecordsTable } from '@/settings/components/SettingsDnsRecordsTable';
import { SettingsEmailingDomainVerifyButton } from '@/settings/emailing-domains/components/SettingsEmailingDomainVerifyButton';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
@@ -16,6 +17,7 @@ import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { ConfirmationModal } from '@/ui/layout/modal/components/ConfirmationModal';
import { useModal } from '@/ui/layout/modal/hooks/useModal';
import { getDocumentationUrl } from '@/support/utils/getDocumentationUrl';
import { MessageChannelType, SettingsPath } from 'twenty-shared/types';
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
import { GetEmailingDomainsDocument } from '~/generated-metadata/graphql';
@@ -23,7 +25,9 @@ import { Status } from 'twenty-ui/data-display';
import { IconCopy, IconTrash } from 'twenty-ui/icon';
import { H2Title } from 'twenty-ui/typography';
import { Button } from 'twenty-ui/input';
import { InlineBanner } from 'twenty-ui/feedback';
import { Section } from 'twenty-ui/layout';
import { Card } from 'twenty-ui/surfaces';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { NotFound } from '~/pages/not-found/NotFound';
import { getColorByEmailingDomainStatus } from '~/pages/settings/emailing-domains/utils/getEmailingDomainStatusColor';
@@ -43,7 +47,22 @@ const StyledForwardingInputContainer = styled.div`
margin-right: ${themeCssVariables.spacing[2]};
`;
export const SettingsWorkspaceEmailGroupChannelDetail = () => {
const StyledDomainStatusRow = styled.div`
align-items: center;
display: flex;
gap: ${themeCssVariables.spacing[2]};
justify-content: space-between;
padding: ${themeCssVariables.spacing[2]} ${themeCssVariables.spacing[3]};
`;
const StyledDomainName = styled.div`
color: ${themeCssVariables.font.color.secondary};
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
`;
export const SettingsWorkspaceCommunicationGroupChannelDetail = () => {
const { t } = useLingui();
const navigateSettings = useNavigateSettings();
const { messageChannelId } = useParams<{ messageChannelId: string }>();
@@ -80,7 +99,7 @@ export const SettingsWorkspaceEmailGroupChannelDetail = () => {
const handleDelete = async () => {
try {
await deleteEmailGroupChannel(channel.id);
navigateSettings(SettingsPath.WorkspaceEmail);
navigateSettings(SettingsPath.WorkspaceCommunications);
} catch {
enqueueErrorSnackBar({
message: t`Failed to delete email channel.`,
@@ -97,10 +116,9 @@ export const SettingsWorkspaceEmailGroupChannelDetail = () => {
href: getSettingsPath(SettingsPath.General),
},
{
children: t`Email`,
href: getSettingsPath(SettingsPath.WorkspaceEmail),
children: t`Communications`,
href: getSettingsPath(SettingsPath.WorkspaceCommunications),
},
{ children: sourceHandle },
]}
actionButton={
<Button
@@ -115,32 +133,22 @@ export const SettingsWorkspaceEmailGroupChannelDetail = () => {
}
>
<SettingsPageContainer>
{isDefined(emailingDomain) && (
<Section>
<H2Title
title={t`Sending domain`}
description={t`Outbound mail from this channel is sent through this domain. It must be verified before email can be delivered.`}
adornment={
<SettingsEmailingDomainVerifyButton
emailingDomainId={emailingDomain.id}
/>
}
/>
<Status
color={getColorByEmailingDomainStatus(emailingDomain.status)}
text={getTextByEmailingDomainStatus(emailingDomain.status)}
/>
{isDefined(emailingDomain.verificationRecords) && (
<SettingsDnsRecordsTable
records={emailingDomain.verificationRecords}
/>
)}
</Section>
)}
<InlineBanner
message={t`Need help to configure your shared mailbox?`}
button={{
title: t`Go to documentation`,
onClick: () =>
window.open(
getDocumentationUrl({}),
'_blank',
'noopener,noreferrer',
),
}}
/>
<Section>
<H2Title
title={t`Source address`}
description={t`The address your workspace sends and receives email from.`}
title={t`Shared email`}
description={t`The shared email you want to use.`}
/>
<SettingsTextInput
instanceId="email-group-source"
@@ -175,7 +183,33 @@ export const SettingsWorkspaceEmailGroupChannelDetail = () => {
/>
</StyledForwardingRow>
</Section>
<SettingsAccountsMessageChannelDetails messageChannel={channel} />
{isDefined(emailingDomain) && (
<Section>
<H2Title
title={t`Sending domain`}
description={t`Outbound mail from this channel is sent through this domain. It must be verified before email can be delivered.`}
adornment={
<SettingsEmailingDomainVerifyButton
emailingDomainId={emailingDomain.id}
/>
}
/>
{isDefined(emailingDomain.verificationRecords) && (
<SettingsDnsRecordsTable
records={emailingDomain.verificationRecords}
/>
)}
<Card rounded>
<StyledDomainStatusRow>
<StyledDomainName>{emailingDomain.domain}</StyledDomainName>
<Status
color={getColorByEmailingDomainStatus(emailingDomain.status)}
text={getTextByEmailingDomainStatus(emailingDomain.status)}
/>
</StyledDomainStatusRow>
</Card>
</Section>
)}
</SettingsPageContainer>
<ConfirmationModal
modalInstanceId={DELETE_EMAIL_GROUP_MODAL_ID}
@@ -0,0 +1,127 @@
import { useApolloClient, useQuery } from '@apollo/client/react';
import { styled } from '@linaria/react';
import { useLingui } from '@lingui/react/macro';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsDiscoveryHeroCard } from '@/settings/components/SettingsDiscoveryHeroCard';
import { GET_UNSUBSCRIBE_PAGE_PREVIEW_URL } from '@/settings/unsubscribe-topics/graphql/queries/getUnsubscribePagePreviewUrl';
import { SettingsWorkspaceUnsubscribeTopicSection } from '@/settings/unsubscribe-topics/components/SettingsWorkspaceUnsubscribeTopicSection';
import { SettingsWorkspaceEmailGroupSection } from '@/settings/workspace/components/SettingsWorkspaceEmailGroupSection';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey, SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import {
IconBrandWhatsapp,
IconMail,
IconMailX,
IconPhone,
} from 'twenty-ui/icon';
import { H2Title } from 'twenty-ui/typography';
import { Section } from 'twenty-ui/layout';
import coverDark from '~/pages/settings/communications/assets/cover-dark.png';
import coverLight from '~/pages/settings/communications/assets/cover-light.png';
import { SettingsCard } from '@/settings/components/SettingsCard';
import { useContext } from 'react';
import { ThemeContext } from 'twenty-ui/theme-constants';
const COMMUNICATIONS_TABS_INSTANCE_ID = 'settings-communications-tabs';
const StyledCardLink = styled.a`
display: block;
min-width: 0;
text-decoration: none;
`;
export const SettingsWorkspaceCommunications = () => {
const { theme } = useContext(ThemeContext);
const { t } = useLingui();
const isEmailGroupFeatureEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_EMAIL_GROUP_ENABLED,
);
const apolloClient = useApolloClient();
const { data: unsubscribePreviewData } = useQuery<{
unsubscribePagePreviewUrl: string;
}>(GET_UNSUBSCRIBE_PAGE_PREVIEW_URL, {
client: apolloClient,
skip: !isEmailGroupFeatureEnabled,
});
const unsubscribePageUrl = unsubscribePreviewData?.unsubscribePagePreviewUrl;
if (!isEmailGroupFeatureEnabled) {
return null;
}
return (
<SettingsPageLayout
title={t`Communications`}
links={[
{
children: t`Workspace`,
href: getSettingsPath(SettingsPath.General),
},
{ children: t`Communications` },
]}
>
<SettingsPageContainer>
<TabList
componentInstanceId={COMMUNICATIONS_TABS_INSTANCE_ID}
tabs={[
{ id: 'emails', title: t`Emails`, Icon: IconMail },
{
id: 'whatsapp',
title: t`Whatsapp`,
Icon: IconBrandWhatsapp,
disabled: true,
pill: t`Soon`,
},
{
id: 'calls',
title: t`Calls`,
Icon: IconPhone,
disabled: true,
pill: t`Soon`,
},
]}
/>
<Section>
<SettingsDiscoveryHeroCard
lightSrc={coverLight}
darkSrc={coverDark}
instanceIdPrefix="settings-communications-hero"
tabs={[]}
/>
</Section>
<SettingsWorkspaceEmailGroupSection />
<SettingsWorkspaceUnsubscribeTopicSection />
<Section>
<H2Title
title={t`Unsubscribe`}
description={t`The page your users will get redirected to to unsubscribe from your emails`}
/>
<StyledCardLink
href={unsubscribePageUrl}
target="_blank"
rel="noopener noreferrer"
>
<SettingsCard
Icon={
<IconMailX
size={theme.icon.size.lg}
stroke={theme.icon.stroke.md}
/>
}
title={t`See unsubscribe page`}
/>
</StyledCardLink>
</Section>
</SettingsPageContainer>
</SettingsPageLayout>
);
};
@@ -50,7 +50,7 @@ export const SettingsWorkspaceNewUnsubscribeTopic = () => {
unsubscribeTopicId,
});
} else {
navigate(SettingsPath.WorkspaceEmail);
navigate(SettingsPath.WorkspaceCommunications);
}
} catch {
enqueueErrorSnackBar({
@@ -80,8 +80,8 @@ export const SettingsWorkspaceNewUnsubscribeTopic = () => {
href: getSettingsPath(SettingsPath.General),
},
{
children: t`Email`,
href: getSettingsPath(SettingsPath.WorkspaceEmail),
children: t`Communications`,
href: getSettingsPath(SettingsPath.WorkspaceCommunications),
},
{ children: t`New Unsubscribe Topic` },
]}
@@ -90,7 +90,7 @@ export const SettingsWorkspaceNewUnsubscribeTopic = () => {
isSaveDisabled={!canSave}
isCancelDisabled={loading}
isLoading={loading}
onCancel={() => navigate(SettingsPath.WorkspaceEmail)}
onCancel={() => navigate(SettingsPath.WorkspaceCommunications)}
onSave={handleSave}
/>
}
@@ -104,7 +104,7 @@ export const SettingsWorkspaceUnsubscribeTopicDetail = () => {
const handleDelete = async () => {
try {
await deleteUnsubscribeTopic(unsubscribeTopic.id);
navigateSettings(SettingsPath.WorkspaceEmail);
navigateSettings(SettingsPath.WorkspaceCommunications);
} catch {
enqueueErrorSnackBar({
message: t`Failed to delete unsubscribe topic.`,
@@ -123,8 +123,8 @@ export const SettingsWorkspaceUnsubscribeTopicDetail = () => {
href: getSettingsPath(SettingsPath.General),
},
{
children: t`Email`,
href: getSettingsPath(SettingsPath.WorkspaceEmail),
children: t`Communications`,
href: getSettingsPath(SettingsPath.WorkspaceCommunications),
},
{ children: topicName },
]}
Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

@@ -1,82 +0,0 @@
import { useLingui } from '@lingui/react/macro';
import { billingState } from '@/client-config/states/billingState';
import { isEmailingDomainInDemoModeState } from '@/client-config/states/isEmailingDomainInDemoModeState';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsOptionCardContentButton } from '@/settings/components/SettingsOptions/SettingsOptionCardContentButton';
import { SettingsWorkspaceUnsubscribeTopicSection } from '@/settings/unsubscribe-topics/components/SettingsWorkspaceUnsubscribeTopicSection';
import { SettingsWorkspaceEmailGroupSection } from '@/settings/workspace/components/SettingsWorkspaceEmailGroupSection';
import { SettingsPageLayout } from '@/settings/components/layout/SettingsPageLayout';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey, SettingsPath } from 'twenty-shared/types';
import { getSettingsPath } from 'twenty-shared/utils';
import { IconArrowUp, IconLock } from 'twenty-ui/icon';
import { Button } from 'twenty-ui/input';
import { Card } from 'twenty-ui/surfaces';
import { themeCssVariables } from 'twenty-ui/theme-constants';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
export const SettingsWorkspaceEmail = () => {
const { t } = useLingui();
const navigateSettings = useNavigateSettings();
const isEmailingDomainInDemoMode = useAtomStateValue(
isEmailingDomainInDemoModeState,
);
const billing = useAtomStateValue(billingState);
const isBillingEnabled = billing?.isBillingEnabled ?? false;
const isEmailGroupFeatureEnabled = useIsFeatureEnabled(
FeatureFlagKey.IS_EMAIL_GROUP_ENABLED,
);
if (!isEmailGroupFeatureEnabled) {
return null;
}
return (
<SettingsPageLayout
title={t`Email`}
links={[
{
children: t`Workspace`,
href: getSettingsPath(SettingsPath.General),
},
{ children: t`Email` },
]}
>
<SettingsPageContainer>
{isEmailingDomainInDemoMode && (
<Card
rounded
backgroundColor={themeCssVariables.background.secondary}
>
<SettingsOptionCardContentButton
Icon={IconLock}
title={t`Emailing is in demo mode`}
description={t`Emails are logged, not sent. Sending requires the AWS SES driver with an Enterprise license, or Twenty Cloud.`}
Button={
<Button
title={t`Upgrade`}
variant="primary"
accent="blue"
size="small"
Icon={IconArrowUp}
onClick={() =>
navigateSettings(
isBillingEnabled
? SettingsPath.BillingPlans
: SettingsPath.AdminPanelEnterprise,
)
}
/>
}
/>
</Card>
)}
<SettingsWorkspaceEmailGroupSection />
<SettingsWorkspaceUnsubscribeTopicSection />
</SettingsPageContainer>
</SettingsPageLayout>
);
};
@@ -28,7 +28,7 @@ export enum SettingsPath {
General = 'general',
Subdomain = 'general/subdomain',
CustomDomain = 'general/custom-domain',
WorkspaceEmail = 'email',
WorkspaceCommunications = 'communications',
EmailGroupChannelDetail = 'email/email-group/:messageChannelId',
NewEmailGroupChannel = 'email/new-email-group',
NewUnsubscribeTopic = 'email/new-unsubscribe-topic',
@@ -53,6 +53,7 @@ export {
IconBrandLinkedin,
IconBrandNpm,
IconBrandOpenai,
IconBrandWhatsapp,
IconBrandX,
IconBriefcase,
IconBroadcast,
@@ -101,6 +102,7 @@ export {
IconColumnInsertRight,
IconColumns,
IconCommand,
IconMessageCircle,
IconMessageCircle as IconComment,
IconCopy,
IconCopyPlus,
+2
View File
@@ -108,6 +108,7 @@ export {
IconBrandLinkedin,
IconBrandNpm,
IconBrandOpenai,
IconBrandWhatsapp,
IconBrandX,
IconBriefcase,
IconBroadcast,
@@ -335,6 +336,7 @@ export {
IconMathXy,
IconMaximize,
IconMessage,
IconMessageCircle,
IconMessageCirclePlus,
IconMinus,
IconMoneybag,