Fix: Not able to add multiple handles to Blocklist in settings/accounts (#21049)

Fixes: #21031

### Root cause:
This feature was never fully implemented even though the placeholder
text suggested it was supported. It could only update one handle at a
time.

### Fix
Fixed the zod validation to validate each handle separately.
Used `useCreateManyRecords` to update multiple handles at the same time.

### Before:
<img width="2032" height="1162" alt="Screenshot 2026-05-29 at 3 25
12 PM"
src="https://github.com/user-attachments/assets/ae6b6ae3-ed38-4410-801e-11f514773681"
/>

### After:


https://github.com/user-attachments/assets/69354129-422a-41de-baf7-fa5a28f01f3f

---------

Co-authored-by: cubic-dev-ai[bot] <191113872+cubic-dev-ai[bot]@users.noreply.github.com>
Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
This commit is contained in:
Priyanshu Bartwal
2026-06-03 23:04:51 +05:30
committed by GitHub
parent 49828d9379
commit d3a7ea0790
3 changed files with 71 additions and 39 deletions

View File

@@ -1,5 +1,5 @@
import { styled } from '@linaria/react';
import { zodResolver } from '@hookform/resolvers/zod';
import { styled } from '@linaria/react';
import { useEffect } from 'react';
import { Controller, useForm } from 'react-hook-form';
import { Key } from 'ts-key-enum';
@@ -7,7 +7,7 @@ import { z } from 'zod';
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
import { useLingui } from '@lingui/react/macro';
import { isValidHostname } from 'twenty-shared/utils';
import { isNonEmptyArray, isValidHostname } from 'twenty-shared/utils';
import { Button } from 'twenty-ui/input';
import { themeCssVariables } from 'twenty-ui/theme-constants';
@@ -21,8 +21,14 @@ const StyledLinkContainer = styled.div`
margin-right: ${themeCssVariables.spacing[2]};
`;
const parseHandles = (value: string): string[] =>
value
.split(',')
.map((handle) => handle.trim())
.filter((handle) => isNonEmptyString(handle));
type SettingsAccountsBlocklistInputProps = {
updateBlockedEmailList: (email: string) => void;
updateBlockedEmailList: (emails: string[]) => void;
blockedEmailOrDomainList: string[];
};
@@ -37,29 +43,46 @@ export const SettingsAccountsBlocklistInput = ({
const { t } = useLingui();
const validationSchema = (blockedEmailOrDomainList: string[]) =>
z
.object({
emailOrDomain: z
.string()
.trim()
.pipe(z.email({ error: t`Invalid email or domain` }))
.or(
z.string().refine(
(value) =>
value.startsWith('@') &&
isValidHostname(value.slice(1), {
allowIp: false,
allowLocalhost: false,
}),
t`Invalid email or domain`,
),
)
.refine(
(value) => !blockedEmailOrDomainList.includes(value),
t`Email or domain is already in blocklist`,
),
})
.required();
z.object({
emailOrDomain: z
.string()
.trim()
.refine(
(value) => {
const handles = parseHandles(value);
return (
isNonEmptyArray(handles) &&
handles.every((handle) => {
const isEmail = z.email().safeParse(handle).success;
const isDomain =
handle.startsWith('@') &&
isValidHostname(handle.slice(1), {
allowIp: false,
allowLocalhost: false,
});
return isEmail || isDomain;
})
);
},
t`Invalid email or domain`,
)
.refine(
(value) => {
const handles = parseHandles(value);
return (
isNonEmptyArray(handles) &&
handles.every(
(handle) => !blockedEmailOrDomainList.includes(handle),
)
);
},
t`Email or domain is already in blocklist`,
),
});
const { reset, handleSubmit, control, formState } = useForm<FormInput>({
mode: 'onSubmit',
@@ -70,7 +93,10 @@ export const SettingsAccountsBlocklistInput = ({
});
const submit = handleSubmit((data) => {
updateBlockedEmailList(data.emailOrDomain);
const handles = parseHandles(data.emailOrDomain);
if (isNonEmptyArray(handles)) {
updateBlockedEmailList(handles);
}
});
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
@@ -78,6 +104,7 @@ export const SettingsAccountsBlocklistInput = ({
return;
}
if (e.key === Key.Enter) {
e.preventDefault();
submit();
}
};

View File

@@ -1,14 +1,14 @@
import { type BlocklistItem } from '@/accounts/types/BlocklistItem';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { useCreateOneRecord } from '@/object-record/hooks/useCreateOneRecord';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useCreateManyRecords } from '@/object-record/hooks/useCreateManyRecords';
import { useDeleteOneRecord } from '@/object-record/hooks/useDeleteOneRecord';
import { useFindManyRecords } from '@/object-record/hooks/useFindManyRecords';
import { SettingsAccountsBlocklistInput } from '@/settings/accounts/components/SettingsAccountsBlocklistInput';
import { SettingsAccountsBlocklistTable } from '@/settings/accounts/components/SettingsAccountsBlocklistTable';
import { useAtomStateValue } from '@/ui/utilities/state/jotai/hooks/useAtomStateValue';
import { useLingui } from '@lingui/react/macro';
import { CoreObjectNameSingular } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { H2Title } from 'twenty-ui/display';
import { Section } from 'twenty-ui/layout';
@@ -34,8 +34,8 @@ export const SettingsAccountsBlocklistSection = () => {
skip: !isDefined(currentWorkspaceMember),
});
const { createOneRecord: createBlocklistItem } =
useCreateOneRecord<BlocklistItem>({
const { createManyRecords: createBlocklistItems } =
useCreateManyRecords<BlocklistItem>({
objectNameSingular: CoreObjectNameSingular.Blocklist,
});
@@ -47,10 +47,15 @@ export const SettingsAccountsBlocklistSection = () => {
deleteBlocklistItem(id);
};
const updateBlockedEmailList = (handle: string) => {
createBlocklistItem({
handle,
workspaceMemberId: currentWorkspaceMember?.id,
const updateBlockedEmailList = (handles: string[]) => {
if (!isDefined(currentWorkspaceMember)) return;
createBlocklistItems({
recordsToCreate: [...new Set(handles)].map((handle) => {
return {
handle,
workspaceMemberId: currentWorkspaceMember.id,
};
}),
});
};

View File

@@ -55,8 +55,8 @@ export const AddToBlocklist: Story = {
await userEvent.click(addToBlocklistButton);
expect(updateBlockedEmailListJestFn).toHaveBeenCalledTimes(1);
expect(updateBlockedEmailListJestFn).toHaveBeenCalledWith(
expect(updateBlockedEmailListJestFn).toHaveBeenCalledWith([
'test@twenty.com',
);
]);
},
};