From 496e14f024c525a1d2d47fc796719ae1a063d024 Mon Sep 17 00:00:00 2001 From: Aaron Leopold <36278431+aaronleopold@users.noreply.github.com> Date: Sat, 31 Aug 2024 17:34:14 -0700 Subject: [PATCH 1/4] :bug: Fix emailer form validation Resolves #429 --- .github/actions/setup-dav1d/action.yml | 1 - packages/browser/babel.config.json | 14 + packages/browser/jest.config.ts | 10 + packages/browser/jest.setup.ts | 1 + packages/browser/package.json | 11 + .../browser/src/__mocks__/resizeObserver.ts | 8 + .../server/email/EmailSettingsRouter.tsx | 3 +- .../scenes/settings/server/email/context.ts | 2 + .../emailers/CreateOrUpdateEmailerForm.tsx | 94 +- .../email/emailers/EmailerActionMenu.tsx | 16 +- .../server/email/emailers/EmailerListItem.tsx | 15 +- .../CreateOrUpdateEmailerForm.test.tsx | 92 + .../email/emailers/__tests__/schema.test.ts | 122 + .../settings/server/email/emailers/schema.ts | 47 + packages/browser/src/utils/form.ts | 5 + packages/browser/tsconfig.json | 2 +- packages/client/src/queries/emailers.ts | 22 + packages/client/src/queries/index.ts | 1 + packages/components/src/input/Input.tsx | 1 + .../components/src/input/raw/RawCheckBox.tsx | 1 + .../components/src/select/NativeSelect.tsx | 1 + yarn.lock | 2017 ++++++++++++++++- 22 files changed, 2392 insertions(+), 94 deletions(-) create mode 100644 packages/browser/babel.config.json create mode 100644 packages/browser/jest.config.ts create mode 100644 packages/browser/jest.setup.ts create mode 100644 packages/browser/src/__mocks__/resizeObserver.ts create mode 100644 packages/browser/src/scenes/settings/server/email/emailers/__tests__/CreateOrUpdateEmailerForm.test.tsx create mode 100644 packages/browser/src/scenes/settings/server/email/emailers/__tests__/schema.test.ts create mode 100644 packages/browser/src/scenes/settings/server/email/emailers/schema.ts create mode 100644 packages/browser/src/utils/form.ts diff --git a/.github/actions/setup-dav1d/action.yml b/.github/actions/setup-dav1d/action.yml index 10a8f12d4..1064d0653 100644 --- a/.github/actions/setup-dav1d/action.yml +++ b/.github/actions/setup-dav1d/action.yml @@ -84,4 +84,3 @@ runs: meson build -Dprefix=C:\build -Denable_tools=false -Denable_examples=false --buildtype release ninja -C build ninja -C build install - diff --git a/packages/browser/babel.config.json b/packages/browser/babel.config.json new file mode 100644 index 000000000..33bf03ce8 --- /dev/null +++ b/packages/browser/babel.config.json @@ -0,0 +1,14 @@ +{ + "presets": [ + "@babel/preset-env", + [ + "@babel/preset-react", + { + // Note: This is to allow React to automatically import the JSX runtime, i.e. + // we don't need to have `import React from 'react'` in every file. + "runtime": "automatic" + } + ], + "@babel/preset-typescript" + ] +} diff --git a/packages/browser/jest.config.ts b/packages/browser/jest.config.ts new file mode 100644 index 000000000..3348ed706 --- /dev/null +++ b/packages/browser/jest.config.ts @@ -0,0 +1,10 @@ +export default { + moduleNameMapper: { + '^@/(.*)$': '/src/$1', + }, + setupFilesAfterEnv: ['/jest.setup.ts'], + testEnvironment: 'jsdom', + transform: { + '^.+\\.tsx?$': 'babel-jest', + }, +} diff --git a/packages/browser/jest.setup.ts b/packages/browser/jest.setup.ts new file mode 100644 index 000000000..c44951a68 --- /dev/null +++ b/packages/browser/jest.setup.ts @@ -0,0 +1 @@ +import '@testing-library/jest-dom' diff --git a/packages/browser/package.json b/packages/browser/package.json index 0f3428c49..5f8eb8c79 100644 --- a/packages/browser/package.json +++ b/packages/browser/package.json @@ -7,6 +7,7 @@ "main": "src/index.ts", "scripts": { "check-types": "tsc --build tsconfig.json", + "test": "jest", "lint": "eslint --ext .ts,.tsx,.cts,.mts,.js,.jsx,.cjs,.mjs --fix --report-unused-disable-directives --no-error-on-unmatched-pattern --exit-on-fatal-error --ignore-path ../../.gitignore ." }, "exports": { @@ -67,6 +68,13 @@ "zustand": "^4.5.2" }, "devDependencies": { + "@babel/preset-env": "^7.25.4", + "@babel/preset-react": "^7.24.7", + "@babel/preset-typescript": "^7.24.7", + "@testing-library/jest-dom": "^6.5.0", + "@testing-library/react": "^16.0.1", + "@testing-library/user-event": "^14.5.2", + "@types/jest": "^29.5.12", "@types/lodash.groupby": "^4.6.9", "@types/lodash.isequal": "^4.5.8", "@types/lodash.sortby": "^4.7.9", @@ -79,6 +87,9 @@ "@types/react-helmet": "^6.1.11", "@types/react-router-dom": "^5.3.3", "@vitejs/plugin-react": "^4.2.1", + "jest": "^29.7.0", + "jest-environment-jsdom": "^29.7.0", + "ts-node": "^10.9.2", "typescript": "^5.4.5", "vite": "^5.2.8" }, diff --git a/packages/browser/src/__mocks__/resizeObserver.ts b/packages/browser/src/__mocks__/resizeObserver.ts new file mode 100644 index 000000000..0af16862a --- /dev/null +++ b/packages/browser/src/__mocks__/resizeObserver.ts @@ -0,0 +1,8 @@ +/* eslint-disable @typescript-eslint/no-empty-function */ +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// components will require this mock. +global.ResizeObserver = class FakeResizeObserver { + observe() {} + unobserve() {} + disconnect() {} +} diff --git a/packages/browser/src/scenes/settings/server/email/EmailSettingsRouter.tsx b/packages/browser/src/scenes/settings/server/email/EmailSettingsRouter.tsx index 9578265f3..8e81ab797 100644 --- a/packages/browser/src/scenes/settings/server/email/EmailSettingsRouter.tsx +++ b/packages/browser/src/scenes/settings/server/email/EmailSettingsRouter.tsx @@ -28,7 +28,8 @@ export default function EmailSettingsRouter() { return ( diff --git a/packages/browser/src/scenes/settings/server/email/context.ts b/packages/browser/src/scenes/settings/server/email/context.ts index 916248ba8..37a92c4ed 100644 --- a/packages/browser/src/scenes/settings/server/email/context.ts +++ b/packages/browser/src/scenes/settings/server/email/context.ts @@ -3,10 +3,12 @@ import { createContext, useContext } from 'react' export type IEmailerSettingsContext = { canCreateEmailer: boolean canEditEmailer: boolean + canDeleteEmailer: boolean } export const EmailerSettingsContext = createContext({ canCreateEmailer: false, + canDeleteEmailer: false, canEditEmailer: false, }) diff --git a/packages/browser/src/scenes/settings/server/email/emailers/CreateOrUpdateEmailerForm.tsx b/packages/browser/src/scenes/settings/server/email/emailers/CreateOrUpdateEmailerForm.tsx index 2959865cd..4a8e84df5 100644 --- a/packages/browser/src/scenes/settings/server/email/emailers/CreateOrUpdateEmailerForm.tsx +++ b/packages/browser/src/scenes/settings/server/email/emailers/CreateOrUpdateEmailerForm.tsx @@ -12,16 +12,16 @@ import { } from '@stump/components' import { useLocaleContext } from '@stump/i18n' import { SMTPEmailer } from '@stump/types' -import React, { useMemo } from 'react' -import { useForm } from 'react-hook-form' -import { z } from 'zod' +import React, { useCallback, useMemo } from 'react' +import { useForm, useFormState } from 'react-hook-form' +import { CreateOrUpdateEmailerSchema, createSchema, formDefaults } from './schema' import { commonHosts, getCommonHost } from './utils' type Props = { emailer?: SMTPEmailer existingNames: string[] - onSubmit: (values: FormValues) => void + onSubmit: (values: CreateOrUpdateEmailerSchema) => void } // TODO: Some of the descriptions are LONG. Use tooltips where necessary, instead of inline descriptions. @@ -37,30 +37,18 @@ export default function CreateOrUpdateEmailerForm({ emailer, existingNames, onSu ), [t, emailer, existingNames], ) - const form = useForm({ - defaultValues: emailer - ? { - is_primary: emailer.is_primary, - max_attachment_size_bytes: emailer.config.max_attachment_size_bytes ?? undefined, - name: emailer.name, - sender_display_name: emailer.config.sender_display_name, - sender_email: emailer.config.sender_email, - smtp_host: emailer.config.smtp_host, - smtp_port: emailer.config.smtp_port, - tls_enabled: emailer.config.tls_enabled, - username: emailer.config.username, - } - : undefined, + const form = useForm({ + defaultValues: formDefaults(emailer), resolver: zodResolver(schema), }) - - const errors = useMemo(() => form.formState.errors, [form.formState.errors]) + const { errors } = useFormState({ control: form.control }) const [currentHost, tlsEnabled] = form.watch(['smtp_host', 'tls_enabled']) + const presetValue = useMemo(() => getCommonHost(currentHost)?.name.toLowerCase(), [currentHost]) - const numericChangeHandler = - (key: keyof FormValues) => (e: React.ChangeEvent) => { + const numericChangeHandler = useCallback( + (key: keyof CreateOrUpdateEmailerSchema) => (e: React.ChangeEvent) => { const { value } = e.target if (value === '' || value == undefined) { @@ -71,17 +59,29 @@ export default function CreateOrUpdateEmailerForm({ emailer, existingNames, onSu form.setValue(key, parsed) } } - } - const numericRegister = (key: keyof FormValues) => { - return { - ...form.register(key), - onChange: numericChangeHandler(key), - } - } + }, + [form], + ) + + const numericRegister = useCallback( + (key: keyof CreateOrUpdateEmailerSchema) => { + return { + ...form.register(key, { valueAsNumber: true }), + onChange: numericChangeHandler(key), + } + }, + [form, numericChangeHandler], + ) return ( -
+ string, isCreating: boolean) => - z.object({ - is_primary: z.boolean().default(existingNames.length === 0), - max_attachment_size_bytes: z.number().optional(), - name: z.string().refine( - (name) => { - if (existingNames.includes(name)) { - return _t(`${LOCALE_BASE}.nameAlreadyExists`) - } else if (FORBIDDEN_NAMES.includes(name)) { - return _t(`${LOCALE_BASE}.nameIsForbidden`) - } - return true - }, - { message: _t(`${LOCALE_BASE}.validation.nameAlreadyExists`) }, - ), - password: isCreating ? z.string() : z.string().optional(), - sender_display_name: z.string(), - sender_email: z.string().email(), - smtp_host: z.string(), - smtp_port: z.number(), - tls_enabled: z.boolean().default(false), - username: z.string(), - }) -export type FormValues = z.infer> diff --git a/packages/browser/src/scenes/settings/server/email/emailers/EmailerActionMenu.tsx b/packages/browser/src/scenes/settings/server/email/emailers/EmailerActionMenu.tsx index 5a05d7d21..14fd2c3bc 100644 --- a/packages/browser/src/scenes/settings/server/email/emailers/EmailerActionMenu.tsx +++ b/packages/browser/src/scenes/settings/server/email/emailers/EmailerActionMenu.tsx @@ -4,7 +4,7 @@ import React from 'react' type Props = { onEdit: () => void - onDelete: () => void + onDelete?: () => void } export default function EmailerActionMenu({ onEdit, onDelete }: Props) { return ( @@ -17,11 +17,15 @@ export default function EmailerActionMenu({ onEdit, onDelete }: Props) { leftIcon: , onClick: onEdit, }, - { - label: 'Delete', - leftIcon: , - onClick: onDelete, - }, + ...(onDelete + ? [ + { + label: 'Delete', + leftIcon: , + onClick: onDelete, + }, + ] + : []), ], }, ]} diff --git a/packages/browser/src/scenes/settings/server/email/emailers/EmailerListItem.tsx b/packages/browser/src/scenes/settings/server/email/emailers/EmailerListItem.tsx index ed63d4b76..2411c464f 100644 --- a/packages/browser/src/scenes/settings/server/email/emailers/EmailerListItem.tsx +++ b/packages/browser/src/scenes/settings/server/email/emailers/EmailerListItem.tsx @@ -1,9 +1,9 @@ -import { prefetchEmailerSendHistory } from '@stump/client' +import { prefetchEmailerSendHistory, useDeleteEmailer } from '@stump/client' import { Badge, Card, Text, ToolTip } from '@stump/components' import { SMTPEmailer } from '@stump/types' import dayjs from 'dayjs' import { Sparkles } from 'lucide-react' -import React, { Suspense, useMemo } from 'react' +import React, { Suspense, useCallback, useMemo } from 'react' import { useNavigate } from 'react-router' import paths from '@/paths' @@ -26,6 +26,8 @@ export default function EmailerListItem({ emailer }: Props) { last_used_at, } = emailer + const { deleteEmailer } = useDeleteEmailer() + const displayedHost = useMemo( () => getCommonHost(smtp_host) ?? { name: smtp_host, smtp_host: smtp_host }, [smtp_host], @@ -43,6 +45,12 @@ export default function EmailerListItem({ emailer }: Props) { } } + const handleDeleteEmailer = useCallback(() => { + if (canEditEmailer) { + deleteEmailer(emailer.id) + } + }, [canEditEmailer, deleteEmailer]) + return ( navigate(paths.editEmailer(emailer.id))} - // TODO: implement delete - onDelete={() => {}} + onDelete={canEditEmailer ? handleDeleteEmailer : undefined} /> )} diff --git a/packages/browser/src/scenes/settings/server/email/emailers/__tests__/CreateOrUpdateEmailerForm.test.tsx b/packages/browser/src/scenes/settings/server/email/emailers/__tests__/CreateOrUpdateEmailerForm.test.tsx new file mode 100644 index 000000000..8410312f4 --- /dev/null +++ b/packages/browser/src/scenes/settings/server/email/emailers/__tests__/CreateOrUpdateEmailerForm.test.tsx @@ -0,0 +1,92 @@ +import '@/__mocks__/resizeObserver' + +import { act, fireEvent, render, screen } from '@testing-library/react' +import { ComponentProps } from 'react' + +import CreateOrUpdateEmailerForm from '../CreateOrUpdateEmailerForm' +import { CreateOrUpdateEmailerSchema } from '../schema' + +jest.mock('@stump/i18n', () => ({ + useLocaleContext: () => ({ t: (s: string) => s }), +})) + +const validEmailer: CreateOrUpdateEmailerSchema = { + is_primary: true, + max_attachment_size_bytes: null, + name: 'newName', + password: 'password', + sender_display_name: 'sender_display_name', + sender_email: 'sender_email@gmail.com', + smtp_host: 'smtp_host', + smtp_port: 123, + tls_enabled: false, + username: 'username', +} + +const inputEmailer = async (overrides: Partial = {}) => { + const emailer = { ...validEmailer, ...overrides } + + fireEvent.input(screen.getByTestId('name'), { target: { value: emailer.name } }) + fireEvent.input(screen.getByTestId('password'), { target: { value: emailer.password } }) + fireEvent.input(screen.getByTestId('sender_display_name'), { + target: { value: emailer.sender_display_name }, + }) + fireEvent.input(screen.getByTestId('sender_email'), { target: { value: emailer.sender_email } }) + fireEvent.input(screen.getByTestId('smtp_host'), { target: { value: emailer.smtp_host } }) + fireEvent.input(screen.getByTestId('smtp_port'), { target: { value: emailer.smtp_port } }) + fireEvent.input(screen.getByTestId('username'), { target: { value: emailer.username } }) + + if (emailer.tls_enabled) { + fireEvent.click(screen.getByTestId('tls_enabled')) + } + + if (emailer.max_attachment_size_bytes != null) { + fireEvent.input(screen.getByTestId('max_attachment_size_bytes'), { + target: { value: emailer.max_attachment_size_bytes }, + }) + } +} + +const onSubmit = jest.fn() + +type SubjectProps = Omit>, 'onSubmit'> +const Subject = ({ existingNames = [], ...props }: SubjectProps) => ( + +) + +describe('CreateOrUpdateEmailerForm', () => { + // TODO: fix the select component emitting a warning about defaultValue vs value + const originalError = console.error.bind(console.error) + beforeAll(() => { + console.error = jest.fn() + }) + afterAll(() => { + console.error = originalError + }) + + beforeEach(() => { + jest.clearAllMocks() + }) + + test('renders properly', async () => { + const { container } = render() + expect(container).not.toBeEmptyDOMElement() + }) + + test('should submit with valid data', async () => { + render() + + inputEmailer() + + await act(async () => { + fireEvent.click(screen.getByRole('button', { name: /submit/i })) + }) + + expect(onSubmit).toHaveBeenCalledWith( + validEmailer, + expect.any(Object), // Submit event + ) + }) + + // TODO: more tests +}) diff --git a/packages/browser/src/scenes/settings/server/email/emailers/__tests__/schema.test.ts b/packages/browser/src/scenes/settings/server/email/emailers/__tests__/schema.test.ts new file mode 100644 index 000000000..60da711d0 --- /dev/null +++ b/packages/browser/src/scenes/settings/server/email/emailers/__tests__/schema.test.ts @@ -0,0 +1,122 @@ +import { FORBIDDEN_ENTITY_NAMES } from '@/utils/form' + +import { CreateOrUpdateEmailerSchema, createSchema } from '../schema' + +const translateFn = jest.fn() + +const validEmailer: CreateOrUpdateEmailerSchema = { + is_primary: false, + name: 'newName', + password: 'password', + sender_display_name: 'sender_display_name', + sender_email: 'sender_email@gmail.com', + smtp_host: 'smtp_host', + smtp_port: 123, + tls_enabled: false, + username: 'username', +} + +const createEmailer = ( + overrides: Partial = {}, +): CreateOrUpdateEmailerSchema => ({ + ...validEmailer, + ...overrides, +}) + +describe('CreateOrUpdateEmailerSchema', () => { + describe('formDefaults', () => { + it('should default is_primary to true when no existing emailers', () => { + const schema = createSchema([], translateFn, true) + expect(schema.parse(createEmailer({ is_primary: undefined })).is_primary).toBe(true) + }) + + it('should default is_primary to false when existing emailers', () => { + const schema = createSchema(['existingName'], translateFn, true) + expect(schema.parse(createEmailer({ is_primary: undefined })).is_primary).toBe(false) + }) + + it('should build form defaults from an emailer', () => { + const schema = createSchema([], translateFn, true) + const emailer = createEmailer() + expect(schema.parse(emailer)).toEqual(emailer) + }) + }) + + describe('validation', () => { + it('should allow optional max_attachment_size_bytes', () => { + const schema = createSchema([], translateFn, true) + expect( + schema.safeParse(createEmailer({ max_attachment_size_bytes: undefined })).success, + ).toBe(true) + expect(schema.safeParse(createEmailer({ max_attachment_size_bytes: 123 })).success).toBe(true) + }) + + it('should not allow existing names', () => { + const schema = createSchema(['existingName'], translateFn, true) + expect(schema.safeParse(createEmailer({ name: 'existingName' })).success).toBe(false) + expect(schema.safeParse(createEmailer({ name: 'newName' })).success).toBe(true) + }) + + it('should not allow forbidden names', () => { + const schema = createSchema([], translateFn, true) + for (const name of FORBIDDEN_ENTITY_NAMES) { + expect(schema.safeParse(createEmailer({ name })).success).toBe(false) + } + }) + + it('should require a password when creating', () => { + const schema = createSchema([], translateFn, true) + expect(schema.safeParse(createEmailer({ password: '' })).success).toBe(false) + expect(schema.safeParse(createEmailer({ password: undefined })).success).toBe(false) + expect(schema.safeParse(createEmailer()).success).toBe(true) + }) + + it('should not require a password when updating', () => { + const schema = createSchema([], translateFn, false) + expect(schema.safeParse(createEmailer({ password: '' })).success).toBe(true) + expect(schema.safeParse(createEmailer({ password: undefined })).success).toBe(true) + expect(schema.safeParse(createEmailer()).success).toBe(true) + }) + + it('should require a valid email address for sender_email', () => { + const schema = createSchema([], translateFn, true) + expect(schema.safeParse(createEmailer({ sender_email: 'invalid' })).success).toBe(false) + expect(schema.safeParse(createEmailer({ sender_email: 'valid@gmail.com' })).success).toBe( + true, + ) + }) + + it('should require a non-empty string for sender_display_name', () => { + const schema = createSchema([], translateFn, true) + expect(schema.safeParse(createEmailer({ sender_display_name: '' })).success).toBe(false) + expect(schema.safeParse(createEmailer({ sender_display_name: 'valid' })).success).toBe(true) + }) + + it('should require a non-empty string for smtp_host', () => { + const schema = createSchema([], translateFn, true) + expect(schema.safeParse(createEmailer({ smtp_host: '' })).success).toBe(false) + expect(schema.safeParse(createEmailer({ smtp_host: 'valid' })).success).toBe(true) + }) + + it('should require a number for smtp_port', () => { + const schema = createSchema([], translateFn, true) + expect(schema.safeParse(createEmailer({ smtp_port: NaN })).success).toBe(false) + // @ts-expect-error: smtp_port is a number + expect(schema.safeParse(createEmailer({ smtp_port: '3' })).success).toBe(false) + expect(schema.safeParse(createEmailer({ smtp_port: 123 })).success).toBe(true) + }) + + it('should require a boolean for tls_enabled', () => { + const schema = createSchema([], translateFn, true) + // @ts-expect-error: tls_enabled is a boolean + expect(schema.safeParse(createEmailer({ tls_enabled: 'true' })).success).toBe(false) + expect(schema.safeParse(createEmailer({ tls_enabled: true })).success).toBe(true) + }) + + it('should require a non-empty string for username', () => { + const schema = createSchema([], translateFn, true) + expect(schema.safeParse(createEmailer({ username: '' })).success).toBe(false) + expect(schema.safeParse(createEmailer({ username: 'valid' })).success).toBe(true) + }) + }) +}) diff --git a/packages/browser/src/scenes/settings/server/email/emailers/schema.ts b/packages/browser/src/scenes/settings/server/email/emailers/schema.ts new file mode 100644 index 000000000..47aa1bdb9 --- /dev/null +++ b/packages/browser/src/scenes/settings/server/email/emailers/schema.ts @@ -0,0 +1,47 @@ +import { SMTPEmailer } from '@stump/types' +import { z } from 'zod' + +import { FORBIDDEN_ENTITY_NAMES } from '@/utils/form' + +const LOCALE_BASE = 'settingsScene.server/email.createOrUpdateForm' + +export const createSchema = ( + existingNames: string[], + t: (key: string) => string, + isCreating: boolean, +) => + z.object({ + is_primary: z.boolean().default(existingNames.length === 0), + max_attachment_size_bytes: z.number().optional().nullable(), + name: z.string().refine( + (name) => { + if (existingNames.includes(name)) { + return t(`${LOCALE_BASE}.nameAlreadyExists`) + } else if (FORBIDDEN_ENTITY_NAMES.includes(name)) { + return t(`${LOCALE_BASE}.nameIsForbidden`) + } + return true + }, + { message: t(`${LOCALE_BASE}.validation.nameAlreadyExists`) }, + ), + password: isCreating ? z.string().min(1) : z.string().optional(), + sender_display_name: z.string().min(1), + sender_email: z.string().email(), + smtp_host: z.string().min(1), + smtp_port: z.number(), + tls_enabled: z.boolean().default(false), + username: z.string().min(1), + }) +export type CreateOrUpdateEmailerSchema = z.infer> + +export const formDefaults = (emailer?: SMTPEmailer) => ({ + is_primary: emailer?.is_primary || true, + max_attachment_size_bytes: emailer?.config.max_attachment_size_bytes ?? null, + name: emailer?.name, + sender_display_name: emailer?.config.sender_display_name, + sender_email: emailer?.config.sender_email, + smtp_host: emailer?.config.smtp_host, + smtp_port: emailer?.config.smtp_port, + tls_enabled: emailer?.config.tls_enabled || false, + username: emailer?.config.username, +}) diff --git a/packages/browser/src/utils/form.ts b/packages/browser/src/utils/form.ts new file mode 100644 index 000000000..fbcf62b38 --- /dev/null +++ b/packages/browser/src/utils/form.ts @@ -0,0 +1,5 @@ +/** + * A list of entity names that are forbidden to be used as entity names, as they are reserved for + * and/or describe special actions + */ +export const FORBIDDEN_ENTITY_NAMES = ['new', 'create', 'update', 'edit', 'delete', 'remove'] diff --git a/packages/browser/tsconfig.json b/packages/browser/tsconfig.json index cace3b2e9..57f4f667e 100644 --- a/packages/browser/tsconfig.json +++ b/packages/browser/tsconfig.json @@ -1,7 +1,7 @@ { "extends": "../../tsconfig.json", "compilerOptions": { - "types": ["vite/client", "node"], + "types": ["vite/client", "node", "jest", "@testing-library/jest-dom"], "skipLibCheck": true, "allowImportingTsExtensions": true, "jsx": "preserve", diff --git a/packages/client/src/queries/emailers.ts b/packages/client/src/queries/emailers.ts index b1d1cf535..4045290c9 100644 --- a/packages/client/src/queries/emailers.ts +++ b/packages/client/src/queries/emailers.ts @@ -72,6 +72,28 @@ export function useUpdateEmailer({ id, ...options }: UseCreateEmailerOptions) { } } +export function useDeleteEmailer() { + const { + mutate: deleteEmailer, + mutateAsync: deleteEmailerAsync, + ...restReturn + } = useMutation([emailerQueryKeys.deleteEmailer], emailerApi.deleteEmailer, { + onSuccess: () => + queryClient.invalidateQueries({ + predicate: (query) => + query.queryKey.some( + (key) => typeof key === 'string' && key.includes(emailerQueryKeys.getEmailers), + ), + }), + }) + + return { + deleteEmailer, + deleteEmailerAsync, + ...restReturn, + } +} + type UseEmailerSendHistoryQueryOptions = { emailerId: number params?: EmailerSendRecordIncludeParams diff --git a/packages/client/src/queries/index.ts b/packages/client/src/queries/index.ts index 6ebc433f2..49eeb6f6c 100644 --- a/packages/client/src/queries/index.ts +++ b/packages/client/src/queries/index.ts @@ -12,6 +12,7 @@ export { prefetchEmailerSendHistory, useCreateEmailDevice, useDeleteEmailDevice, + useDeleteEmailer, useEmailDevicesQuery, useEmailerQuery, useEmailerSendHistoryQuery, diff --git a/packages/components/src/input/Input.tsx b/packages/components/src/input/Input.tsx index 73560fc11..06fd3a6b9 100644 --- a/packages/components/src/input/Input.tsx +++ b/packages/components/src/input/Input.tsx @@ -144,6 +144,7 @@ export const Input = React.forwardRef( }, className, )} + data-testid={props.id} /> {renderRightDecoration()} diff --git a/packages/components/src/input/raw/RawCheckBox.tsx b/packages/components/src/input/raw/RawCheckBox.tsx index f4056ae0a..f67713a72 100644 --- a/packages/components/src/input/raw/RawCheckBox.tsx +++ b/packages/components/src/input/raw/RawCheckBox.tsx @@ -45,6 +45,7 @@ export const RawCheckBox = React.forwardRef( ref={ref} className={cn(checkboxVariants({ className, rounded, size, variant }))} {...props} + data-testid={props.id} > diff --git a/packages/components/src/select/NativeSelect.tsx b/packages/components/src/select/NativeSelect.tsx index 29af49921..9fd13af55 100644 --- a/packages/components/src/select/NativeSelect.tsx +++ b/packages/components/src/select/NativeSelect.tsx @@ -42,6 +42,7 @@ export const NativeSelect = forwardRef( className, )} {...props} + data-testid={props.id} > {emptyOption && (