Ses outbound followup (#20610)
This pull request unifies outbound with inbound under the new feature and the new email groups feature. These are workspace level shared inboxes that are shared between all workspace members. outbound sending with SES works, we only listen for tenant status events, rest is managed by AWS PR refactors old code and webhook to be split for outbound and inbound for proper separation | Area | Change | |---|---| | AWS SES driver | Split into `AwsSesRegisterDomainService` (tenant + identity + DKIM + MAIL FROM + configuration-set + EventBridge dest + contact list) and `AwsSesSendEmailService` (SendEmail). | | Reputation webhook | New `/webhooks/messaging/ses/outbound` route. SES → EventBridge (`Sending Status Enabled/Disabled` on default bus) → SNS → router → `SesOutboundSendingStateHandlerService` updates `emailing_domain.tenantStatus`. | | Inbound webhook | Refactored into `SesInboundWebhookRouterService` + `SesInboundMailHandlerService`. Shared `SnsSignatureVerifierService` + `SnsSubscriptionConfirmerService` across both routes. | | Global uniqueness | New migration + instance command: `emailing_domain.domain` is now globally unique (one tenant per domain across workspaces). | | Tenant status | New `emailing_domain.tenantStatus` column (`ACTIVE` / `PAUSED`) + `EmailingDomainTenantStatusService`. | | Send-email mutation | New `sendEmailViaDomain` GraphQL mutation + DTOs. | | Cleanup | `EmailingDomainWorkspaceCleanupJob` wired into `WorkspaceService.deleteWorkspace` — tears down SES tenant association + identity on workspace delete. | | Settings UI | Rewritten around reusable `SettingsTableListSection`. "Email Group" → "Email Handle" rename. New cells for status/source/forwarding. Outbound domains surfaced on workspace settings page. | ### Env vars (new) All in `config-variables.ts`, group `AWS_SES_SETTINGS`, all optional: - `AWS_SES_REGION` — `@IsAWSRegion`, consumed by `AwsSesClientProvider` + driver factory - `AWS_SES_ACCOUNT_ID` — used for ARN construction in driver factory - `SES_SNS_TOPIC_ARN_ALLOWLIST` — **shared** by inbound + outbound webhook routers, comma-separated list of accepted SNS topic ARNs (verified via `sns-payload-validator`) ### Migrations - `1778862608620-add-emailing-domain-tenant-status` (fast) — adds `tenantStatus` column. - `1778865501791-unique-emailing-domain-globally` (slow, idempotent) — enforces global uniqueness on `domain`. - Instance commands bumped to `2.5`. ### Infra dependency Two coupled twenty-infra PRs: - `ses-inbound-email` — receipt-rule + inbound SNS topic + S3 bucket policy + KMS grant + `email_group_*` outputs. - `ses-outbound-tf` — EventBridge rule + outbound SNS topic + SES IAM policy + outbound `webhook_url` subscription. **Based on `ses-inbound-email`.** Merge order: inbound first, then outbound. Outbound PR's chart edit owns the comma-joined `SES_SNS_TOPIC_ARN_ALLOWLIST` value (both ARNs). Features lives under `/settings/general` <img width="1496" height="845" alt="SCR-20260519-ofhi-2" src="https://github.com/user-attachments/assets/a025485a-09f7-4131-91cd-0067690ff18d" /> --------- Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: Félix Malfait <FelixMalfait@users.noreply.github.com>
This commit is contained in:
+5
-5
@@ -39,14 +39,14 @@ export const SettingsAccountsNewEmailGroupChannel = () => {
|
||||
}
|
||||
} catch {
|
||||
enqueueErrorSnackBar({
|
||||
message: t`Failed to create email group channel. Email group may not be configured on this server.`,
|
||||
message: t`Failed to create email handle. Email handles may not be configured on this server.`,
|
||||
});
|
||||
}
|
||||
}, [createEmailGroupChannel, handle, navigate, enqueueErrorSnackBar, t]);
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={t`New Email Group`}
|
||||
title={t`New Email Handle`}
|
||||
links={[
|
||||
{
|
||||
children: t`Workspace`,
|
||||
@@ -56,7 +56,7 @@ export const SettingsAccountsNewEmailGroupChannel = () => {
|
||||
children: t`General`,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{ children: t`New Email Group` },
|
||||
{ children: t`New Email Handle` },
|
||||
]}
|
||||
actionButton={
|
||||
<SaveAndCancelButtons
|
||||
@@ -72,10 +72,10 @@ export const SettingsAccountsNewEmailGroupChannel = () => {
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Email Address`}
|
||||
description={t`Enter the email address you want to forward emails from (e.g. support@mycompany.com).`}
|
||||
description={t`The address your workspace will send and receive email from (e.g. support@mycompany.com). Outbound sending requires the domain to be verified in Outbound Domains.`}
|
||||
/>
|
||||
<SettingsTextInput
|
||||
instanceId="email-group-handle"
|
||||
instanceId="email-group-source"
|
||||
label={t`Source Email Address`}
|
||||
placeholder="support@mycompany.com"
|
||||
value={handle}
|
||||
|
||||
@@ -91,6 +91,7 @@ export const SettingsDnsRecordsTable = ({
|
||||
<TableCell>{record.type}</TableCell>
|
||||
<StyledTableCellFontWrapper>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
onClick={() => {
|
||||
copyToClipboard(record.key || '');
|
||||
}}
|
||||
@@ -101,6 +102,7 @@ export const SettingsDnsRecordsTable = ({
|
||||
|
||||
<StyledTableCellFontWrapper>
|
||||
<TableCell
|
||||
overflow="hidden"
|
||||
onClick={() => {
|
||||
copyToClipboard(record.value);
|
||||
}}
|
||||
@@ -111,17 +113,17 @@ export const SettingsDnsRecordsTable = ({
|
||||
|
||||
{hasPriorityRecords && (
|
||||
<StyledTableCellFontWrapper>
|
||||
<TableCell>{record.priority}</TableCell>
|
||||
<TableCell overflow="hidden">{record.priority}</TableCell>
|
||||
</StyledTableCellFontWrapper>
|
||||
)}
|
||||
{hasTtlRecords && (
|
||||
<StyledTableCellFontWrapper>
|
||||
<TableCell>{record.ttl}</TableCell>
|
||||
<TableCell overflow="hidden">{record.ttl}</TableCell>
|
||||
</StyledTableCellFontWrapper>
|
||||
)}
|
||||
{hasStatusRecords && (
|
||||
<StyledTableCellFontWrapper>
|
||||
<TableCell>
|
||||
<TableCell overflow="hidden">
|
||||
{'status' in record ? (
|
||||
<Status
|
||||
color={record.statusColor}
|
||||
|
||||
@@ -0,0 +1,114 @@
|
||||
import { type ComponentType } from 'react';
|
||||
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
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 { H2Title, IconPlus } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledTableRows = styled.div`
|
||||
padding-bottom: ${themeCssVariables.spacing[2]};
|
||||
padding-top: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledRowWrapper = styled.div<{ clickable: boolean }>`
|
||||
${({ clickable }) =>
|
||||
clickable
|
||||
? `
|
||||
> * {
|
||||
&:hover {
|
||||
background-color: ${themeCssVariables.background.transparent.light};
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
`
|
||||
: ''}
|
||||
`;
|
||||
|
||||
const StyledFooter = styled.div`
|
||||
border-top: 1px solid ${themeCssVariables.border.color.light};
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const HEADER_PADDING = `0 ${themeCssVariables.spacing[2]} 0 ${themeCssVariables.spacing[2]}`;
|
||||
|
||||
export type SettingsTableListSectionColumn<Item> = {
|
||||
label: string;
|
||||
align?: 'left' | 'right';
|
||||
Cell: ComponentType<{ item: Item }>;
|
||||
};
|
||||
|
||||
type SettingsTableListSectionProps<Item extends { id: string }> = {
|
||||
title: string;
|
||||
description: string;
|
||||
items: Item[];
|
||||
columns: SettingsTableListSectionColumn<Item>[];
|
||||
gridAutoColumns: string;
|
||||
onRowClick?: (item: Item) => void;
|
||||
footerButtonLabel: string;
|
||||
onFooterButtonClick: () => void;
|
||||
};
|
||||
|
||||
export const SettingsTableListSection = <
|
||||
Item extends { id: string } = { id: string },
|
||||
>({
|
||||
title,
|
||||
description,
|
||||
items,
|
||||
columns,
|
||||
gridAutoColumns,
|
||||
onRowClick,
|
||||
footerButtonLabel,
|
||||
onFooterButtonClick,
|
||||
}: SettingsTableListSectionProps<Item>) => (
|
||||
<Section>
|
||||
<H2Title title={title} description={description} />
|
||||
{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}
|
||||
/>
|
||||
</StyledFooter>
|
||||
</Section>
|
||||
);
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import { type GetEmailingDomainsQuery } from '~/generated-metadata/graphql';
|
||||
import { IconMail, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledNameCell = styled.div`
|
||||
align-items: center;
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
display: flex;
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
type SettingsEmailingDomainNameCellProps = {
|
||||
item: GetEmailingDomainsQuery['getEmailingDomains'][0];
|
||||
};
|
||||
|
||||
export const SettingsEmailingDomainNameCell = ({
|
||||
item,
|
||||
}: SettingsEmailingDomainNameCellProps) => (
|
||||
<StyledNameCell>
|
||||
<IconMail size={16} />
|
||||
<OverflowingTextWithTooltip text={item.domain} />
|
||||
</StyledNameCell>
|
||||
);
|
||||
-83
@@ -1,83 +0,0 @@
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { CombinedGraphQLErrors } from '@apollo/client/errors';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconDotsVertical, IconTrash } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import { useMutation, useQuery } from '@apollo/client/react';
|
||||
import {
|
||||
type GetEmailingDomainsQuery,
|
||||
DeleteEmailingDomainDocument,
|
||||
GetEmailingDomainsDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type SettingsEmailingDomainRowDropdownMenuProps = {
|
||||
emailingDomain: GetEmailingDomainsQuery['getEmailingDomains'][0];
|
||||
};
|
||||
|
||||
export const SettingsEmailingDomainRowDropdownMenu = ({
|
||||
emailingDomain,
|
||||
}: SettingsEmailingDomainRowDropdownMenuProps) => {
|
||||
const dropdownId = `settings-emailing-domain-row-${emailingDomain.id}`;
|
||||
|
||||
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const { refetch: refetchEmailingDomains } = useQuery(
|
||||
GetEmailingDomainsDocument,
|
||||
);
|
||||
|
||||
const [deleteEmailingDomainMutation] = useMutation(
|
||||
DeleteEmailingDomainDocument,
|
||||
);
|
||||
|
||||
const handleDeleteEmailingDomain = async () => {
|
||||
try {
|
||||
await deleteEmailingDomainMutation({
|
||||
variables: {
|
||||
id: emailingDomain.id,
|
||||
},
|
||||
});
|
||||
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Emailing domain deleted successfully`,
|
||||
});
|
||||
|
||||
await refetchEmailingDomains();
|
||||
} catch (error) {
|
||||
enqueueErrorSnackBar({
|
||||
...(CombinedGraphQLErrors.is(error) ? { apolloError: error } : {}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownPlacement="right-start"
|
||||
clickableComponent={
|
||||
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
accent="danger"
|
||||
LeftIcon={IconTrash}
|
||||
text={t`Delete`}
|
||||
onClick={() => {
|
||||
handleDeleteEmailingDomain();
|
||||
closeDropdown(dropdownId);
|
||||
}}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
import { type GetEmailingDomainsQuery } from '~/generated-metadata/graphql';
|
||||
import { getColorByEmailingDomainStatus } from '~/pages/settings/emailing-domains/utils/getEmailingDomainStatusColor';
|
||||
import { getTextByEmailingDomainStatus } from '~/pages/settings/emailing-domains/utils/getEmailingDomainStatusText';
|
||||
import { Status } from 'twenty-ui/display';
|
||||
|
||||
type SettingsEmailingDomainStatusCellProps = {
|
||||
item: GetEmailingDomainsQuery['getEmailingDomains'][0];
|
||||
};
|
||||
|
||||
export const SettingsEmailingDomainStatusCell = ({
|
||||
item,
|
||||
}: SettingsEmailingDomainStatusCellProps) => (
|
||||
<Status
|
||||
color={getColorByEmailingDomainStatus(item.status)}
|
||||
text={getTextByEmailingDomainStatus(item.status)}
|
||||
/>
|
||||
);
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
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>
|
||||
);
|
||||
+27
-119
@@ -1,65 +1,13 @@
|
||||
import { styled } from '@linaria/react';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
|
||||
import { type MessageChannel } from '@/accounts/types/MessageChannel';
|
||||
import { useMyMessageChannels } from '@/settings/accounts/hooks/useMyMessageChannels';
|
||||
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 { SettingsTableListSection } from '@/settings/components/SettingsTableListSection';
|
||||
import { SettingsWorkspaceEmailGroupForwardingCell } from '@/settings/workspace/components/SettingsWorkspaceEmailGroupForwardingCell';
|
||||
import { SettingsWorkspaceEmailGroupSourceCell } from '@/settings/workspace/components/SettingsWorkspaceEmailGroupSourceCell';
|
||||
import { MessageChannelType, SettingsPath } from 'twenty-shared/types';
|
||||
import { H2Title, IconMail, IconPlus } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
|
||||
const GRID_AUTO_COLUMNS = '1fr 1fr';
|
||||
|
||||
const StyledTableRows = styled.div`
|
||||
padding-bottom: ${themeCssVariables.spacing[2]};
|
||||
padding-top: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
const StyledClickableRow = styled.div`
|
||||
> * {
|
||||
&:hover {
|
||||
background-color: ${themeCssVariables.background.transparent.light};
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledNameCell = styled.div`
|
||||
align-items: center;
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
display: flex;
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
const StyledHandle = styled.span`
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
`;
|
||||
|
||||
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;
|
||||
`;
|
||||
|
||||
const StyledFooter = styled.div`
|
||||
border-top: 1px solid ${themeCssVariables.border.color.light};
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
padding-top: ${themeCssVariables.spacing[2]};
|
||||
`;
|
||||
|
||||
export const SettingsWorkspaceEmailGroupSection = () => {
|
||||
const { t } = useLingui();
|
||||
const navigateSettings = useNavigateSettings();
|
||||
@@ -70,67 +18,27 @@ export const SettingsWorkspaceEmailGroupSection = () => {
|
||||
);
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Email Groups`}
|
||||
description={t`Workspace-level shared addresses that receive forwarded mail.`}
|
||||
/>
|
||||
{emailGroupChannels.length > 0 && (
|
||||
<Table>
|
||||
<TableRow gridAutoColumns={GRID_AUTO_COLUMNS}>
|
||||
<TableHeader
|
||||
padding={`0 ${themeCssVariables.spacing[2]} 0 ${themeCssVariables.spacing[2]}`}
|
||||
>
|
||||
<Trans>Source</Trans>
|
||||
</TableHeader>
|
||||
<TableHeader
|
||||
padding={`0 ${themeCssVariables.spacing[2]} 0 ${themeCssVariables.spacing[2]}`}
|
||||
>
|
||||
<Trans>Forwarding address</Trans>
|
||||
</TableHeader>
|
||||
</TableRow>
|
||||
<StyledTableRows>
|
||||
{emailGroupChannels.map((channel) => {
|
||||
const sourceHandle =
|
||||
channel.connectedAccount?.handle ?? channel.handle;
|
||||
|
||||
return (
|
||||
<StyledClickableRow key={channel.id}>
|
||||
<TableRow
|
||||
gridAutoColumns={GRID_AUTO_COLUMNS}
|
||||
onClick={() =>
|
||||
navigateSettings(SettingsPath.EmailGroupChannelDetail, {
|
||||
messageChannelId: channel.id,
|
||||
})
|
||||
}
|
||||
>
|
||||
<TableCell>
|
||||
<StyledNameCell>
|
||||
<IconMail size={16} />
|
||||
<StyledHandle>{sourceHandle}</StyledHandle>
|
||||
</StyledNameCell>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StyledForwardingCell>
|
||||
{channel.handle}
|
||||
</StyledForwardingCell>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</StyledClickableRow>
|
||||
);
|
||||
})}
|
||||
</StyledTableRows>
|
||||
</Table>
|
||||
)}
|
||||
<StyledFooter>
|
||||
<Button
|
||||
Icon={IconPlus}
|
||||
title={t`Add email group`}
|
||||
variant="secondary"
|
||||
size="small"
|
||||
onClick={() => navigateSettings(SettingsPath.NewEmailGroupChannel)}
|
||||
/>
|
||||
</StyledFooter>
|
||||
</Section>
|
||||
<SettingsTableListSection<MessageChannel>
|
||||
title={t`Email Handles`}
|
||||
description={t`Shared addresses your workspace uses to send and receive email.`}
|
||||
items={emailGroupChannels}
|
||||
columns={[
|
||||
{ label: t`Source`, Cell: SettingsWorkspaceEmailGroupSourceCell },
|
||||
{
|
||||
label: t`Forwarding address`,
|
||||
Cell: SettingsWorkspaceEmailGroupForwardingCell,
|
||||
},
|
||||
]}
|
||||
gridAutoColumns="1fr 1fr"
|
||||
onRowClick={(channel) =>
|
||||
navigateSettings(SettingsPath.EmailGroupChannelDetail, {
|
||||
messageChannelId: channel.id,
|
||||
})
|
||||
}
|
||||
footerButtonLabel={t`Add email handle`}
|
||||
onFooterButtonClick={() =>
|
||||
navigateSettings(SettingsPath.NewEmailGroupChannel)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
import { styled } from '@linaria/react';
|
||||
|
||||
import { type MessageChannel } from '@/accounts/types/MessageChannel';
|
||||
import { IconMail, OverflowingTextWithTooltip } from 'twenty-ui/display';
|
||||
import { themeCssVariables } from 'twenty-ui/theme-constants';
|
||||
|
||||
const StyledNameCell = styled.div`
|
||||
align-items: center;
|
||||
color: ${themeCssVariables.font.color.primary};
|
||||
display: flex;
|
||||
font-weight: ${themeCssVariables.font.weight.medium};
|
||||
gap: ${themeCssVariables.spacing[2]};
|
||||
min-width: 0;
|
||||
`;
|
||||
|
||||
type SettingsWorkspaceEmailGroupSourceCellProps = {
|
||||
item: MessageChannel;
|
||||
};
|
||||
|
||||
export const SettingsWorkspaceEmailGroupSourceCell = ({
|
||||
item,
|
||||
}: SettingsWorkspaceEmailGroupSourceCellProps) => {
|
||||
const sourceHandle = item.connectedAccount?.handle;
|
||||
|
||||
return (
|
||||
<StyledNameCell>
|
||||
<IconMail size={16} />
|
||||
<OverflowingTextWithTooltip text={sourceHandle ?? '—'} />
|
||||
</StyledNameCell>
|
||||
);
|
||||
};
|
||||
+48
@@ -0,0 +1,48 @@
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useQuery } from '@apollo/client/react';
|
||||
|
||||
import { SettingsTableListSection } from '@/settings/components/SettingsTableListSection';
|
||||
import { SettingsEmailingDomainNameCell } from '@/settings/emailing-domains/components/SettingsEmailingDomainNameCell';
|
||||
import { SettingsEmailingDomainStatusCell } from '@/settings/emailing-domains/components/SettingsEmailingDomainStatusCell';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import {
|
||||
type GetEmailingDomainsQuery,
|
||||
GetEmailingDomainsDocument,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
|
||||
type EmailingDomain = GetEmailingDomainsQuery['getEmailingDomains'][0];
|
||||
|
||||
export const SettingsWorkspaceEmailingDomainsSection = () => {
|
||||
const { t } = useLingui();
|
||||
const navigateSettings = useNavigateSettings();
|
||||
|
||||
const { data } = useQuery(GetEmailingDomainsDocument);
|
||||
const emailingDomains = data?.getEmailingDomains ?? [];
|
||||
|
||||
return (
|
||||
<SettingsTableListSection<EmailingDomain>
|
||||
title={t`Emailing Domains`}
|
||||
description={t`Verify domains so the workspace can send outbound email through them.`}
|
||||
items={emailingDomains}
|
||||
columns={[
|
||||
{ label: t`Domain`, Cell: SettingsEmailingDomainNameCell },
|
||||
{
|
||||
label: t`Status`,
|
||||
align: 'right',
|
||||
Cell: SettingsEmailingDomainStatusCell,
|
||||
},
|
||||
]}
|
||||
gridAutoColumns="1fr 1fr"
|
||||
onRowClick={(emailingDomain) =>
|
||||
navigateSettings(SettingsPath.EmailingDomainDetail, {
|
||||
domainId: emailingDomain.id,
|
||||
})
|
||||
}
|
||||
footerButtonLabel={t`Add emailing domain`}
|
||||
onFooterButtonClick={() =>
|
||||
navigateSettings(SettingsPath.NewEmailingDomain)
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user