mirror of
https://github.com/twentyhq/twenty.git
synced 2026-06-11 09:26:53 -04:00
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:
committed by
GitHub
parent
49828d9379
commit
d3a7ea0790
@@ -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();
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}),
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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',
|
||||
);
|
||||
]);
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user