diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistInput.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistInput.tsx index 4854f742012..84d4fb93558 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistInput.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistInput.tsx @@ -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({ 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) => { @@ -78,6 +104,7 @@ export const SettingsAccountsBlocklistInput = ({ return; } if (e.key === Key.Enter) { + e.preventDefault(); submit(); } }; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistSection.tsx b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistSection.tsx index c6d09682f59..406f4b376fe 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistSection.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/SettingsAccountsBlocklistSection.tsx @@ -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({ + const { createManyRecords: createBlocklistItems } = + useCreateManyRecords({ 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, + }; + }), }); }; diff --git a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistInput.stories.tsx b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistInput.stories.tsx index 1a40bb87140..bb5cdb4eca0 100644 --- a/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistInput.stories.tsx +++ b/packages/twenty-front/src/modules/settings/accounts/components/__stories__/SettingsAccountsBlocklistInput.stories.tsx @@ -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', - ); + ]); }, };