@ -1,9 +1,13 @@
|
|||||||
import { useState } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import styled from '@emotion/styled';
|
import styled from '@emotion/styled';
|
||||||
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Key } from 'ts-key-enum';
|
import { Key } from 'ts-key-enum';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { Button } from '@/ui/input/button/components/Button';
|
import { Button } from '@/ui/input/button/components/Button';
|
||||||
import { TextInput } from '@/ui/input/components/TextInput';
|
import { TextInput } from '@/ui/input/components/TextInput';
|
||||||
|
import { isDomain } from '~/utils/is-domain';
|
||||||
|
|
||||||
const StyledContainer = styled.div`
|
const StyledContainer = styled.div`
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -19,45 +23,75 @@ type SettingsAccountsEmailsBlocklistInputProps = {
|
|||||||
updateBlockedEmailList: (email: string) => void;
|
updateBlockedEmailList: (email: string) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const validationSchema = z
|
||||||
|
.object({
|
||||||
|
emailOrDomain: z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.email('Invalid email or domain')
|
||||||
|
.or(
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.refine(
|
||||||
|
(value) => value.startsWith('@') && isDomain(value.slice(1)),
|
||||||
|
'Invalid email or domain',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
})
|
||||||
|
.required();
|
||||||
|
|
||||||
|
type FormInput = z.infer<typeof validationSchema>;
|
||||||
|
|
||||||
export const SettingsAccountsEmailsBlocklistInput = ({
|
export const SettingsAccountsEmailsBlocklistInput = ({
|
||||||
updateBlockedEmailList,
|
updateBlockedEmailList,
|
||||||
}: SettingsAccountsEmailsBlocklistInputProps) => {
|
}: SettingsAccountsEmailsBlocklistInputProps) => {
|
||||||
const [formValues, setFormValues] = useState<{
|
const { reset, handleSubmit, control, formState } = useForm<FormInput>({
|
||||||
email: string;
|
mode: 'onSubmit',
|
||||||
}>({
|
resolver: zodResolver(validationSchema),
|
||||||
email: '',
|
defaultValues: {
|
||||||
|
emailOrDomain: '',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const submit = handleSubmit((data) => {
|
||||||
|
updateBlockedEmailList(data.emailOrDomain);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
if (e.key === Key.Enter) {
|
if (e.key === Key.Enter) {
|
||||||
updateBlockedEmailList(formValues.email);
|
submit();
|
||||||
setFormValues({ email: '' });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { isSubmitSuccessful } = formState;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSubmitSuccessful) {
|
||||||
|
reset();
|
||||||
|
}
|
||||||
|
}, [isSubmitSuccessful, reset]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<StyledContainer>
|
<form onSubmit={submit}>
|
||||||
<StyledLinkContainer>
|
<StyledContainer>
|
||||||
<TextInput
|
<StyledLinkContainer>
|
||||||
placeholder="eddy@gmail.com, @apple.com"
|
<Controller
|
||||||
value={formValues.email}
|
name="emailOrDomain"
|
||||||
onChange={(value) => {
|
control={control}
|
||||||
setFormValues((prevState) => ({
|
render={({ field: { value, onChange }, fieldState: { error } }) => (
|
||||||
...prevState,
|
<TextInput
|
||||||
email: value,
|
placeholder="eddy@gmail.com, @apple.com"
|
||||||
}));
|
value={value}
|
||||||
}}
|
onChange={onChange}
|
||||||
fullWidth
|
error={error?.message}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
/>
|
fullWidth
|
||||||
</StyledLinkContainer>
|
/>
|
||||||
<Button
|
)}
|
||||||
title="Add to blocklist"
|
/>
|
||||||
onClick={() => {
|
</StyledLinkContainer>
|
||||||
updateBlockedEmailList(formValues.email);
|
<Button title="Add to blocklist" type="submit" />
|
||||||
setFormValues({ email: '' });
|
</StyledContainer>
|
||||||
}}
|
</form>
|
||||||
/>
|
|
||||||
</StyledContainer>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -22,7 +22,7 @@ export type ButtonProps = {
|
|||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
focus?: boolean;
|
focus?: boolean;
|
||||||
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
};
|
} & React.ComponentProps<'button'>;
|
||||||
|
|
||||||
const StyledButton = styled.button<
|
const StyledButton = styled.button<
|
||||||
Pick<
|
Pick<
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { MessageFindOnePreQueryHook } from 'src/modules/messaging/query-hooks/me
|
|||||||
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/types/workspace-query-hook.type';
|
import { WorkspaceQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/types/workspace-query-hook.type';
|
||||||
import { CalendarEventFindManyPreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook';
|
import { CalendarEventFindManyPreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-many.pre-query.hook';
|
||||||
import { CalendarEventFindOnePreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook';
|
import { CalendarEventFindOnePreQueryHook } from 'src/modules/calendar/query-hooks/calendar-event/calendar-event-find-one.pre-query-hook';
|
||||||
|
import { BlocklistCreateManyPreQueryHook } from 'src/modules/connected-account/query-hooks/blocklist/blocklist-create-many.pre-query.hook';
|
||||||
|
|
||||||
// TODO: move to a decorator
|
// TODO: move to a decorator
|
||||||
export const workspacePreQueryHooks: WorkspaceQueryHook = {
|
export const workspacePreQueryHooks: WorkspaceQueryHook = {
|
||||||
@ -14,4 +15,7 @@ export const workspacePreQueryHooks: WorkspaceQueryHook = {
|
|||||||
findOne: [CalendarEventFindOnePreQueryHook.name],
|
findOne: [CalendarEventFindOnePreQueryHook.name],
|
||||||
findMany: [CalendarEventFindManyPreQueryHook.name],
|
findMany: [CalendarEventFindManyPreQueryHook.name],
|
||||||
},
|
},
|
||||||
|
blocklist: {
|
||||||
|
createMany: [BlocklistCreateManyPreQueryHook.name],
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,9 +3,14 @@ import { Module } from '@nestjs/common';
|
|||||||
import { MessagingQueryHookModule } from 'src/modules/messaging/query-hooks/messaging-query-hook.module';
|
import { MessagingQueryHookModule } from 'src/modules/messaging/query-hooks/messaging-query-hook.module';
|
||||||
import { WorkspacePreQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.service';
|
import { WorkspacePreQueryHookService } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/workspace-pre-query-hook.service';
|
||||||
import { CalendarQueryHookModule } from 'src/modules/calendar/query-hooks/calendar-query-hook.module';
|
import { CalendarQueryHookModule } from 'src/modules/calendar/query-hooks/calendar-query-hook.module';
|
||||||
|
import { ConnectedAccountQueryHookModule } from 'src/modules/connected-account/query-hooks/connected-account-query-hook.module';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [MessagingQueryHookModule, CalendarQueryHookModule],
|
imports: [
|
||||||
|
MessagingQueryHookModule,
|
||||||
|
CalendarQueryHookModule,
|
||||||
|
ConnectedAccountQueryHookModule,
|
||||||
|
],
|
||||||
providers: [WorkspacePreQueryHookService],
|
providers: [WorkspacePreQueryHookService],
|
||||||
exports: [WorkspacePreQueryHookService],
|
exports: [WorkspacePreQueryHookService],
|
||||||
})
|
})
|
||||||
|
|||||||
@ -227,6 +227,14 @@ export class WorkspaceQueryRunnerService {
|
|||||||
options,
|
options,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
await this.workspacePreQueryHookService.executePreHooks(
|
||||||
|
userId,
|
||||||
|
workspaceId,
|
||||||
|
objectMetadataItem.nameSingular,
|
||||||
|
'createMany',
|
||||||
|
args,
|
||||||
|
);
|
||||||
|
|
||||||
const query = await this.workspaceQueryBuilderFactory.createMany(
|
const query = await this.workspaceQueryBuilderFactory.createMany(
|
||||||
computedArgs,
|
computedArgs,
|
||||||
options,
|
options,
|
||||||
|
|||||||
5
packages/twenty-server/src/engine/utils/is-domain.ts
Normal file
5
packages/twenty-server/src/engine/utils/is-domain.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const isDomain = (url: string | undefined | null) =>
|
||||||
|
!!url &&
|
||||||
|
/^((?!-))(xn--)?[a-z0-9][a-z0-9-_]{0,61}[a-z0-9]{0,1}\.(xn--)?([a-z0-9-]{1,61}|[a-z0-9-]{1,30}\.[a-z]{2,})$/.test(
|
||||||
|
url,
|
||||||
|
);
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
|
||||||
|
import z from 'zod';
|
||||||
|
|
||||||
|
import { WorkspacePreQueryHook } from 'src/engine/api/graphql/workspace-query-runner/workspace-pre-query-hook/interfaces/workspace-pre-query-hook.interface';
|
||||||
|
import { CreateManyResolverArgs } from 'src/engine/api/graphql/workspace-resolver-builder/interfaces/workspace-resolvers-builder.interface';
|
||||||
|
|
||||||
|
import { isDomain } from 'src/engine/utils/is-domain';
|
||||||
|
import { BlocklistObjectMetadata } from 'src/modules/connected-account/standard-objects/blocklist.object-metadata';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class BlocklistCreateManyPreQueryHook implements WorkspacePreQueryHook {
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
async execute(
|
||||||
|
userId: string,
|
||||||
|
workspaceId: string,
|
||||||
|
payload: CreateManyResolverArgs<
|
||||||
|
Omit<BlocklistObjectMetadata, 'createdAt' | 'updatedAt'> & {
|
||||||
|
createdAt: string;
|
||||||
|
updatedAt: string;
|
||||||
|
}
|
||||||
|
>,
|
||||||
|
): Promise<void> {
|
||||||
|
const emailOrDomainSchema = z
|
||||||
|
.string()
|
||||||
|
.trim()
|
||||||
|
.email('Invalid email or domain')
|
||||||
|
.or(
|
||||||
|
z
|
||||||
|
.string()
|
||||||
|
.refine(
|
||||||
|
(value) => value.startsWith('@') && isDomain(value.slice(1)),
|
||||||
|
'Invalid email or domain',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const { handle } of payload.data) {
|
||||||
|
if (!handle) {
|
||||||
|
throw new Error('Handle is required');
|
||||||
|
}
|
||||||
|
|
||||||
|
emailOrDomainSchema.parse(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
|
||||||
|
import { BlocklistCreateManyPreQueryHook } from 'src/modules/connected-account/query-hooks/blocklist/blocklist-create-many.pre-query.hook';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: BlocklistCreateManyPreQueryHook.name,
|
||||||
|
useClass: BlocklistCreateManyPreQueryHook,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
export class ConnectedAccountQueryHookModule {}
|
||||||
Reference in New Issue
Block a user