(app-desk) create mailbox for a mail domain

Add form to create a mailbox for a mail domain. It sends a http
POST request mail-domains/<mail-domain-id>/mailboxes/ on form
submit. The form appears inside a modal.
Installs react-hook-form, zod, and @hookform/resolvers for form
manipulation and field validation.
This commit is contained in:
daproclaima
2024-05-24 22:53:49 +02:00
committed by Sebastien Nobour
parent 37d32888f5
commit 6981ef17df
9 changed files with 512 additions and 27 deletions

View File

@@ -16,6 +16,7 @@
},
"dependencies": {
"@gouvfr-lasuite/integration": "0.1.3",
"@hookform/resolvers": "3.4.2",
"@openfun/cunningham-react": "2.9.0",
"@tanstack/react-query": "5.36.0",
"i18next": "23.11.4",
@@ -25,12 +26,15 @@
"react": "*",
"react-aria-components": "1.2.0",
"react-dom": "*",
"react-hook-form": "7.51.5",
"react-i18next": "14.1.1",
"react-select": "5.8.0",
"styled-components": "6.1.11",
"zod": "3.23.8",
"zustand": "4.5.2"
},
"devDependencies": {
"@hookform/devtools": "4.3.1",
"@svgr/webpack": "8.1.0",
"@tanstack/react-query-devtools": "5.36.0",
"@testing-library/jest-dom": "6.4.5",

View File

@@ -0,0 +1,64 @@
import { UUID } from 'crypto';
import {
UseMutationOptions,
useMutation,
useQueryClient,
} from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { KEY_LIST_MAILBOX } from './useMailboxes';
export interface CreateMailboxParams {
first_name: string;
last_name: string;
local_part: string;
secondary_email: string;
phone_number: string;
mailDomainId: UUID;
}
export const createMailbox = async ({
mailDomainId,
...data
}: CreateMailboxParams): Promise<void> => {
const response = await fetchAPI(`mail-domains/${mailDomainId}/mailboxes/`, {
method: 'POST',
body: JSON.stringify(data),
});
if (!response.ok) {
// TODO: extend errorCauses to return the name of the invalid field names to highlight in the form?
throw new APIError(
'Failed to create the mailbox',
await errorCauses(response),
);
}
};
type UseCreateMailboxParams = { domainId: UUID } & UseMutationOptions<
void,
APIError,
CreateMailboxParams
>;
export function useCreateMailbox(options: UseCreateMailboxParams) {
const queryClient = useQueryClient();
return useMutation<void, APIError, CreateMailboxParams>({
mutationFn: createMailbox,
onSuccess: (data, variables, context) => {
void queryClient.invalidateQueries({
queryKey: [KEY_LIST_MAILBOX, { id: variables.mailDomainId }],
});
if (options?.onSuccess) {
options.onSuccess(data, variables, context);
}
},
onError: (error, variables, context) => {
if (options?.onError) {
options.onError(error, variables, context);
}
},
});
}

View File

@@ -35,7 +35,7 @@ export const getMailDomainMailboxes = async ({
return response.json() as Promise<MailDomainMailboxesResponse>;
};
const KEY_LIST_MAILBOX = 'mailboxes';
export const KEY_LIST_MAILBOX = 'mailboxes';
export function useMailDomainMailboxes(
param: MailDomainMailboxesParams,

View File

@@ -0,0 +1,14 @@
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path
d="M21.34 26.04C20.9 26.02 20.46 26 20 26C15.16 26 10.64 27.34 6.78 29.64C5.02 30.68 4 32.64 4 34.7V40H22.52C20.94 37.74 20 34.98 20 32C20 29.86 20.5 27.86 21.34 26.04Z"
fill="currentColor"
/>
<path
d="M20 24C24.4183 24 28 20.4183 28 16C28 11.5817 24.4183 8 20 8C15.5817 8 12 11.5817 12 16C12 20.4183 15.5817 24 20 24Z"
fill="currentColor"
/>
<path
d="M33 24C28.032 24 24 28.032 24 33C24 37.968 28.032 42 33 42C37.968 42 42 37.968 42 33C42 28.032 37.968 24 33 24ZM36.6 33.9H33.9V36.6C33.9 37.095 33.495 37.5 33 37.5C32.505 37.5 32.1 37.095 32.1 36.6V33.9H29.4C28.905 33.9 28.5 33.495 28.5 33C28.5 32.505 28.905 32.1 29.4 32.1H32.1V29.4C32.1 28.905 32.505 28.5 33 28.5C33.495 28.5 33.9 28.905 33.9 29.4V32.1H36.6C37.095 32.1 37.5 32.505 37.5 33C37.5 33.495 37.095 33.9 36.6 33.9Z"
fill="currentColor"
/>
</svg>

After

Width:  |  Height:  |  Size: 925 B

View File

@@ -1,4 +1,5 @@
import {
Button,
DataGrid,
Loader,
SortModel,
@@ -8,11 +9,13 @@ import React, { useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Box, Card, Text, TextErrors } from '@/components';
import { MailDomain } from '@/features/mail-domains';
import { useMailDomainMailboxes } from '@/features/mail-domains/api/useMailDomainMailboxes';
import { PAGE_SIZE } from '@/features/mail-domains/conf';
import { useMailboxes } from '../api/useMailboxes';
import { default as MailDomainsLogo } from '../assets/mail-domains-logo.svg';
import { PAGE_SIZE } from '../conf';
import { MailDomain, MailDomainMailbox } from '../types';
import { CreateMailboxForm } from './forms/CreateMailboxForm';
export type ViewMailbox = { email: string; id: string };
@@ -44,6 +47,9 @@ function formatSortModel(
export function MailDomainsContent({ mailDomain }: { mailDomain: MailDomain }) {
const [sortModel, setSortModel] = useState<SortModel>([]);
const { t } = useTranslation();
const [isCreateMailboxFormVisible, setIsCreateMailboxFormVisible] =
useState(false);
const pagination = usePagination({
defaultPage: 1,
pageSize: PAGE_SIZE,
@@ -52,7 +58,7 @@ export function MailDomainsContent({ mailDomain }: { mailDomain: MailDomain }) {
const { page, pageSize, setPagesCount } = pagination;
const ordering = sortModel.length ? formatSortModel(sortModel[0]) : undefined;
const { data, isLoading, error } = useMailDomainMailboxes({
const { data, isLoading, error } = useMailboxes({
id: mailDomain.id,
page,
ordering,
@@ -60,7 +66,7 @@ export function MailDomainsContent({ mailDomain }: { mailDomain: MailDomain }) {
const viewMailboxes: ViewMailbox[] =
mailDomain && data?.results?.length
? data.results.map((mailbox) => ({
? data.results.map((mailbox: MailDomainMailbox) => ({
email: `${mailbox.local_part}@${mailDomain.name}`,
id: mailbox.id,
}))
@@ -75,7 +81,16 @@ export function MailDomainsContent({ mailDomain }: { mailDomain: MailDomain }) {
</Box>
) : (
<>
<TopBanner name={mailDomain.name} />
{isCreateMailboxFormVisible && mailDomain ? (
<CreateMailboxForm
mailDomain={mailDomain}
setIsFormVisible={setIsCreateMailboxFormVisible}
/>
) : null}
<TopBanner
name={mailDomain.name}
setIsFormVisible={setIsCreateMailboxFormVisible}
/>
<Card
$padding={{ bottom: 'small' }}
$margin={{ all: 'big', top: 'none' }}
@@ -104,20 +119,36 @@ export function MailDomainsContent({ mailDomain }: { mailDomain: MailDomain }) {
);
}
const TopBanner = ({ name }: { name: string }) => {
const TopBanner = ({
name,
setIsFormVisible,
}: {
name: string;
setIsFormVisible: (value: boolean) => void;
}) => {
const { t } = useTranslation();
return (
<Box
$direction="row"
$align="center"
$margin={{ all: 'big', vertical: 'xbig' }}
$gap="2.25rem"
>
<MailDomainsLogo aria-label={t('Mail Domains icon')} />
<Text $margin="none" as="h3" $size="h3">
{name}
</Text>
</Box>
<>
<Box
$direction="row"
$align="center"
$margin={{ all: 'big', vertical: 'xbig' }}
$gap="2.25rem"
>
<MailDomainsLogo aria-label={t('Mail Domains icon')} />
<Text $margin="none" as="h3" $size="h3">
{name}
</Text>
</Box>
<Box $margin={{ all: 'big', bottom: 'small' }} $align="flex-end">
<Button
aria-label={t(`Create a mailbox in {{name}} domain`, { name })}
onClick={() => setIsFormVisible(true)}
>
{t('Create a mailbox')}
</Button>
</Box>
</>
);
};

View File

@@ -0,0 +1,276 @@
import { zodResolver } from '@hookform/resolvers/zod';
import {
Button,
Input,
Modal,
ModalSize,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
import React from 'react';
import {
Controller,
FormProvider,
UseFormReturn,
useForm,
} from 'react-hook-form';
import { useTranslation } from 'react-i18next';
import { z } from 'zod';
import { Box, Text, TextErrors } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { CreateMailboxParams, useCreateMailbox } from '../../api';
import IconCreateMailbox from '../../assets/create-mailbox.svg';
import { MailDomain } from '../../types';
const FORM_ID: string = 'form-create-mailbox';
const createMailboxValidationSchema = z.object({
first_name: z.string().min(1),
last_name: z.string().min(1),
local_part: z.string().min(1),
secondary_email: z.string().min(1),
phone_number: z.string().min(1),
});
export const CreateMailboxForm = ({
mailDomain,
setIsFormVisible,
}: {
mailDomain: MailDomain;
setIsFormVisible: (value: boolean) => void;
}) => {
const { t } = useTranslation();
const { toast } = useToastProvider();
const { colorsTokens } = useCunninghamTheme();
const methods = useForm<CreateMailboxParams>({
delayError: 0,
defaultValues: {
first_name: '',
last_name: '',
local_part: '',
secondary_email: '',
phone_number: '',
},
mode: 'onChange',
reValidateMode: 'onChange',
resolver: zodResolver(createMailboxValidationSchema),
});
const { mutate: createMailbox, ...queryState } = useCreateMailbox({
domainId: mailDomain.id,
onSuccess: () => {
toast(t('Mailbox created!'), VariantType.SUCCESS, {
duration: 4000,
});
setIsFormVisible(false);
},
});
const closeModal = () => setIsFormVisible(false);
const onSubmitCallback = (event: React.FormEvent) => {
event.preventDefault();
void methods.handleSubmit((data) =>
createMailbox({ ...data, mailDomainId: mailDomain.id }),
)();
};
return (
<FormProvider {...methods}>
<Modal
isOpen
leftActions={
<Button
color="secondary"
fullWidth
onClick={closeModal}
disabled={methods.formState.isSubmitting}
>
{t('Cancel')}
</Button>
}
onClose={closeModal}
closeOnClickOutside
hideCloseButton
rightActions={
<Button
color="primary"
fullWidth
type="submit"
form={FORM_ID}
disabled={methods.formState.isSubmitting}
>
{t('Submit')}
</Button>
}
size={ModalSize.MEDIUM}
title={
<Box $align="center" $gap="1rem">
<IconCreateMailbox
width={48}
color={colorsTokens()['primary-text']}
title={t('Mailbox creation form')}
/>
<Text $size="h3" $margin="none" role="heading" aria-level={3}>
{t('Create a mailbox')}
</Text>
</Box>
}
>
<Box $width="100%" $margin={{ top: 'large', bottom: 'xl' }}>
{queryState.isError && (
<TextErrors className="mb-s" causes={queryState.error.cause} />
)}
{methods ? (
<Form
methods={methods}
mailDomain={mailDomain}
onSubmitCallback={onSubmitCallback}
/>
) : null}
</Box>
</Modal>
</FormProvider>
);
};
const Form = ({
methods,
mailDomain,
onSubmitCallback,
}: {
methods: UseFormReturn<CreateMailboxParams>;
mailDomain: MailDomain;
onSubmitCallback: (event: React.FormEvent) => void;
}) => {
const { t } = useTranslation();
return (
<form onSubmit={onSubmitCallback} id={FORM_ID}>
<Box $direction="column" $width="100%" $gap="2rem" $margin="auto">
<Box $margin={{ horizontal: 'none' }}>
<Controller
control={methods.control}
name="first_name"
render={({ fieldState }) => (
<Input
aria-invalid={!!fieldState.error}
label={t('First name')}
state={fieldState.error ? 'error' : 'default'}
text={
fieldState.error
? t('Please enter your first name')
: undefined
}
{...methods.register('first_name')}
/>
)}
/>
</Box>
<Box $margin={{ horizontal: 'none' }}>
<Controller
control={methods.control}
name="last_name"
render={({ fieldState }) => (
<Input
aria-invalid={!!fieldState.error}
label={t('Last name')}
state={fieldState.error ? 'error' : 'default'}
text={
fieldState.error
? t('Please enter your last name')
: undefined
}
{...methods.register('last_name')}
/>
)}
/>
</Box>
<Box $margin={{ horizontal: 'none' }} $direction="row">
<Box $width="65%">
<Controller
control={methods.control}
name="local_part"
render={({ fieldState }) => (
<Input
aria-invalid={!!fieldState.error}
label={t('Main email address')}
state={fieldState.error ? 'error' : 'default'}
text={
fieldState.error
? t(
'Please enter the first part of the email address, without including "@" in it',
)
: undefined
}
{...methods.register('local_part')}
/>
)}
/>
</Box>
<Text
as="span"
$theme="primary"
$size="1rem"
$display="inline-block"
$css={`
position: relative;
display: inline-block;
left: 1rem;
top: 1rem;
`}
>
@{mailDomain.name}
</Text>
</Box>
<Box $margin={{ horizontal: 'none' }}>
<Controller
control={methods.control}
name="secondary_email"
render={({ fieldState }) => (
<Input
aria-invalid={!!fieldState.error}
label={t('Secondary email address')}
state={fieldState.error ? 'error' : 'default'}
text={
fieldState.error
? t('Please enter your secondary email address')
: undefined
}
{...methods.register('secondary_email')}
/>
)}
/>
</Box>
<Box $margin={{ horizontal: 'none' }}>
<Controller
control={methods.control}
name="phone_number"
render={({ fieldState }) => (
<Input
aria-invalid={!!fieldState.error}
label={t('Phone number')}
state={fieldState.error ? 'error' : 'default'}
text={
fieldState.error
? t('Please enter your phone number')
: undefined
}
{...methods.register('phone_number')}
/>
)}
/>
</Box>
</Box>
</form>
);
};

View File

@@ -1,3 +1,4 @@
export * from './components/';
export * from './components';
export * from './types';
export * from './api';
export * from './store';

View File

@@ -0,0 +1 @@
export * from './useMailDomainsStore';

View File

@@ -1120,7 +1120,7 @@
resolved "https://registry.yarnpkg.com/@emotion/hash/-/hash-0.9.1.tgz#4ffb0055f7ef676ebc3a5a91fb621393294e2f43"
integrity sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==
"@emotion/is-prop-valid@1.2.2":
"@emotion/is-prop-valid@1.2.2", "@emotion/is-prop-valid@^1.2.2":
version "1.2.2"
resolved "https://registry.yarnpkg.com/@emotion/is-prop-valid/-/is-prop-valid-1.2.2.tgz#d4175076679c6a26faa92b03bb786f9e52612337"
integrity sha512-uNsoYd37AFmaCdXlg6EYD1KaPOaRWRByMCYzbKUX4+hhMfrxdVSelShywL4JVaAeM/eHUOSprYBQls+/neX3pw==
@@ -1132,7 +1132,7 @@
resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.1.tgz#c1ddb040429c6d21d38cc945fe75c818cfb68e17"
integrity sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==
"@emotion/react@^11.8.1":
"@emotion/react@^11.1.5", "@emotion/react@^11.8.1":
version "11.11.4"
resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.11.4.tgz#3a829cac25c1f00e126408fab7f891f00ecc3c1d"
integrity sha512-t8AjMlF0gHpvvxk5mAtCqR4vmxiGHCeJBaQO6gncUSdklELOgtwjerNY2yuJNfwnc6vi16U/+uMF+afIawJ9iw==
@@ -1146,7 +1146,7 @@
"@emotion/weak-memoize" "^0.3.1"
hoist-non-react-statics "^3.3.1"
"@emotion/serialize@^1.1.2", "@emotion/serialize@^1.1.3":
"@emotion/serialize@^1.1.2", "@emotion/serialize@^1.1.3", "@emotion/serialize@^1.1.4":
version "1.1.4"
resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.4.tgz#fc8f6d80c492cfa08801d544a05331d1cc7cd451"
integrity sha512-RIN04MBT8g+FnDwgvIUi8czvr1LU1alUMI05LekWB5DGyTm8cCBMCRpq3GqaiyEDRptEXOyXnvZ58GZYu4kBxQ==
@@ -1162,6 +1162,18 @@
resolved "https://registry.yarnpkg.com/@emotion/sheet/-/sheet-1.2.2.tgz#d58e788ee27267a14342303e1abb3d508b6d0fec"
integrity sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==
"@emotion/styled@^11.3.0":
version "11.11.5"
resolved "https://registry.yarnpkg.com/@emotion/styled/-/styled-11.11.5.tgz#0c5c8febef9d86e8a926e663b2e5488705545dfb"
integrity sha512-/ZjjnaNKvuMPxcIiUkf/9SHoG4Q196DRl1w82hQ3WCsjo1IUR8uaGWrC6a87CrYAW0Kb/pK7hk8BnLgLRi9KoQ==
dependencies:
"@babel/runtime" "^7.18.3"
"@emotion/babel-plugin" "^11.11.0"
"@emotion/is-prop-valid" "^1.2.2"
"@emotion/serialize" "^1.1.4"
"@emotion/use-insertion-effect-with-fallbacks" "^1.0.1"
"@emotion/utils" "^1.2.1"
"@emotion/unitless@0.8.1", "@emotion/unitless@^0.8.1":
version "0.8.1"
resolved "https://registry.yarnpkg.com/@emotion/unitless/-/unitless-0.8.1.tgz#182b5a4704ef8ad91bde93f7a860a88fd92c79a3"
@@ -1403,6 +1415,25 @@
resolved "https://registry.yarnpkg.com/@gouvfr-lasuite/integration/-/integration-0.1.3.tgz#cbf44473cd2a5b9497814faff459c3d88bc1ddce"
integrity sha512-WvAaMyEcNZkNX88Rbi6xo1rxFIGjsg3w8Gxi5NKVyNY0Ph2dXcGkP63ybCKD8JAsWHfxwAvs82wqXFZk5RCJ1g==
"@hookform/devtools@4.3.1":
version "4.3.1"
resolved "https://registry.yarnpkg.com/@hookform/devtools/-/devtools-4.3.1.tgz#5df1b77ea12b4f1c220da3d2dba737f81cbb12bc"
integrity sha512-CrWxEoHQZaOXJZVQ8KBgOuAa8p2LI8M0DAN5GTRTmdCieRwFVjVDEmuTAVazWVRRkpEQSgSt3KYp7VmmqXdEnw==
dependencies:
"@emotion/react" "^11.1.5"
"@emotion/styled" "^11.3.0"
"@types/lodash" "^4.14.168"
little-state-machine "^4.1.0"
lodash "^4.17.21"
react-simple-animate "^3.3.12"
use-deep-compare-effect "^1.8.1"
uuid "^8.3.2"
"@hookform/resolvers@3.4.2":
version "3.4.2"
resolved "https://registry.yarnpkg.com/@hookform/resolvers/-/resolvers-3.4.2.tgz#b69525248c2a9a1b2546411251ea25029915841a"
integrity sha512-1m9uAVIO8wVf7VCDAGsuGA0t6Z3m6jVGAN50HkV9vYLl0yixKK/Z1lr01vaRvYCkIKGoy1noVRxMzQYb4y/j1Q==
"@humanwhocodes/config-array@^0.11.14":
version "0.11.14"
resolved "https://registry.yarnpkg.com/@humanwhocodes/config-array/-/config-array-0.11.14.tgz#d78e481a039f7566ecc9660b4ea7fe6b1fec442b"
@@ -3393,6 +3424,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.1.tgz#0fabfcf2f2127ef73b119d98452bd317c4a17eb8"
integrity sha512-X+2qazGS3jxLAIz5JDXDzglAF3KpijdhFxlf/V1+hEsOUc+HnWi81L/uv/EvGuV90WY+7mPGFCUDGfQC3Gj95Q==
"@types/lodash@^4.14.168":
version "4.17.4"
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.17.4.tgz#0303b64958ee070059e3a7184048a55159fe20b7"
integrity sha512-wYCP26ZLxaT3R39kiN2+HcJ4kTd3U1waI/cY7ivWYqFP6pW3ZNpvi6Wd6PHZx7T/t8z0vlkXMg3QYLa7DZ/IJQ==
"@types/luxon@3.4.2":
version "3.4.2"
resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.4.2.tgz#e4fc7214a420173cea47739c33cdf10874694db7"
@@ -4704,7 +4740,7 @@ delayed-stream@~1.0.0:
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
dequal@^2.0.3:
dequal@^2.0.2, dequal@^2.0.3:
version "2.0.3"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be"
integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==
@@ -6991,6 +7027,11 @@ lines-and-columns@^1.1.6:
resolved "https://registry.yarnpkg.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz#eca284f75d2965079309dc0ad9255abb2ebc1632"
integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
little-state-machine@^4.1.0:
version "4.8.0"
resolved "https://registry.yarnpkg.com/little-state-machine/-/little-state-machine-4.8.0.tgz#4853d01f71dc7e15fec00193692f845020a57686"
integrity sha512-xfi5+iDxTLhu0hbnNubUs+qoQQqxhtEZeObP5ELjUlHnl74bbasY7mOonsGQrAouyrbag3ebNLSse5xX1T7buQ==
locate-path@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
@@ -7885,6 +7926,11 @@ react-dom@*, react-dom@18.2.0, react-dom@18.3.1:
loose-envify "^1.1.0"
scheduler "^0.23.0"
react-hook-form@7.51.5:
version "7.51.5"
resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.51.5.tgz#4afbfb819312db9fea23e8237a3a0d097e128b43"
integrity sha512-J2ILT5gWx1XUIJRETiA7M19iXHlG74+6O3KApzvqB/w8S5NQR7AbU8HVZrMALdmDgWpRPYiZJl0zx8Z4L2mP6Q==
react-i18next@14.1.1:
version "14.1.1"
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-14.1.1.tgz#3d942a99866555ae3552c40f9fddfa061e29d7f3"
@@ -7938,6 +7984,11 @@ react-select@5.8.0:
react-transition-group "^4.3.0"
use-isomorphic-layout-effect "^1.1.2"
react-simple-animate@^3.3.12:
version "3.5.2"
resolved "https://registry.yarnpkg.com/react-simple-animate/-/react-simple-animate-3.5.2.tgz#ab08865c8bd47872b92bd1e25902326bf7c695b3"
integrity sha512-xLE65euP920QMTOmv5haPlml+hmOPDkbIr5WeF7ADIXWBYt5kW/vwpNfWg8EKMab8aeDxIZ6QjffVh8v2dUyhg==
react-stately@3.30.1:
version "3.30.1"
resolved "https://registry.yarnpkg.com/react-stately/-/react-stately-3.30.1.tgz#7d87649c69f1bcf42c68a732f121ff23393f5abb"
@@ -8463,7 +8514,16 @@ string-length@^4.0.1:
char-regex "^1.0.2"
strip-ansi "^6.0.0"
"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
"string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
dependencies:
emoji-regex "^8.0.0"
is-fullwidth-code-point "^3.0.0"
strip-ansi "^6.0.1"
string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3:
version "4.2.3"
resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -8541,7 +8601,14 @@ string_decoder@~1.1.1:
dependencies:
safe-buffer "~5.1.0"
"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1:
"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
dependencies:
ansi-regex "^5.0.1"
strip-ansi@^6.0.0, strip-ansi@^6.0.1:
version "6.0.1"
resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -9106,6 +9173,14 @@ url-parse@^1.5.3:
querystringify "^2.1.1"
requires-port "^1.0.0"
use-deep-compare-effect@^1.8.1:
version "1.8.1"
resolved "https://registry.yarnpkg.com/use-deep-compare-effect/-/use-deep-compare-effect-1.8.1.tgz#ef0ce3b3271edb801da1ec23bf0754ef4189d0c6"
integrity sha512-kbeNVZ9Zkc0RFGpfMN3MNfaKNvcLNyxOAAd9O4CBZ+kCBXXscn9s/4I+8ytUER4RDpEYs5+O6Rs4PqiZ+rHr5Q==
dependencies:
"@babel/runtime" "^7.12.5"
dequal "^2.0.2"
use-isomorphic-layout-effect@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
@@ -9126,6 +9201,11 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2, util-deprecate@~1.0.1:
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
uuid@^8.3.2:
version "8.3.2"
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
v8-compile-cache-lib@^3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf"
@@ -9369,7 +9449,16 @@ word-wrap@^1.2.5:
resolved "https://registry.yarnpkg.com/word-wrap/-/word-wrap-1.2.5.tgz#d2c45c6dd4fbce621a66f136cbe328afd0410b34"
integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
dependencies:
ansi-styles "^4.0.0"
string-width "^4.1.0"
strip-ansi "^6.0.0"
wrap-ansi@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
@@ -9471,6 +9560,11 @@ yocto-queue@^0.1.0:
resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b"
integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==
zod@3.23.8:
version "3.23.8"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.8.tgz#e37b957b5d52079769fb8097099b592f0ef4067d"
integrity sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==
zustand@4.5.2:
version "4.5.2"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-4.5.2.tgz#fddbe7cac1e71d45413b3682cdb47b48034c3848"