️(frontend) improve select share stability

- keep email in search input after unfocus
- keep search in memory after unfocus
- fixed width to reduce flickering
- empty states after validation
This commit is contained in:
Anthony LC
2024-08-06 12:48:42 +02:00
committed by Anthony LC
parent 8d5648005f
commit fb494c8c71
4 changed files with 61 additions and 23 deletions

View File

@@ -16,6 +16,7 @@ and this project adheres to
## Fixed ## Fixed
- 🐛(y-webrtc) fix prob connection #147 - 🐛(y-webrtc) fix prob connection #147
- ⚡️(frontend) improve select share stability #159
## Changed ## Changed

View File

@@ -170,7 +170,7 @@ test.describe('Document create member', () => {
const inputSearch = page.getByLabel(/Find a member to add to the document/); const inputSearch = page.getByLabel(/Find a member to add to the document/);
const email = randomName('test@test.fr', browserName, 1)[0]; const [email] = randomName('test@test.fr', browserName, 1);
await inputSearch.fill(email); await inputSearch.fill(email);
await page.getByRole('option', { name: email }).click(); await page.getByRole('option', { name: email }).click();
@@ -191,7 +191,22 @@ test.describe('Document create member', () => {
expect(responseCreateInvitation.ok()).toBeTruthy(); expect(responseCreateInvitation.ok()).toBeTruthy();
await inputSearch.fill(email); await inputSearch.fill(email);
await expect(page.getByText('Loading...')).toBeHidden(); await page.getByRole('option', { name: email }).click();
await expect(page.getByRole('option', { name: email })).toBeHidden(); // Choose a role
await page.getByRole('combobox', { name: /Choose a role/ }).click();
await page.getByRole('option', { name: 'Owner' }).click();
const responsePromiseCreateInvitationFail = page.waitForResponse(
(response) =>
response.url().includes('/invitations/') && response.status() === 400,
);
await page.getByRole('button', { name: 'Validate' }).click();
await expect(
page.getByText(`"${email}" is already invited to the document.`),
).toBeVisible();
const responseCreateInvitationFail =
await responsePromiseCreateInvitationFail;
expect(responseCreateInvitationFail.ok()).toBeFalsy();
}); });
}); });

View File

@@ -70,14 +70,23 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
duration: 4000, duration: 4000,
}; };
const onError = (dataError: APIErrorUser['data']) => { const onError = (dataError: APIErrorUser) => {
const messageError = let messageError =
dataError?.type === OptionType.INVITATION dataError['data']?.type === OptionType.INVITATION
? t(`Failed to create the invitation for {{email}}.`, { ? t(`Failed to create the invitation for {{email}}.`, {
email: dataError?.value, email: dataError['data']?.value,
}) })
: t(`Failed to add the member in the document.`); : t(`Failed to add the member in the document.`);
if (
dataError.cause?.[0] ===
'Document invitation with this Email address and Document already exists.'
) {
messageError = t('"{{email}}" is already invited to the document.', {
email: dataError['data']?.value,
});
}
toast(messageError, VariantType.ERROR, toastOptions); toast(messageError, VariantType.ERROR, toastOptions);
}; };
@@ -106,11 +115,12 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
setIsPending(false); setIsPending(false);
setResetKey(resetKey + 1); setResetKey(resetKey + 1);
setSelectedUsers([]);
settledPromises.forEach((settledPromise) => { settledPromises.forEach((settledPromise) => {
switch (settledPromise.status) { switch (settledPromise.status) {
case 'rejected': case 'rejected':
onError((settledPromise.reason as APIErrorUser).data); onError(settledPromise.reason as APIErrorUser);
break; break;
case 'fulfilled': case 'fulfilled':
@@ -132,7 +142,7 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
<IconBG iconName="group_add" /> <IconBG iconName="group_add" />
<Box $gap="0.7rem" $direction="row" $wrap="wrap" $css="flex: 70%;"> <Box $gap="0.7rem" $direction="row" $wrap="wrap" $css="flex: 70%;">
<Box $gap="0.7rem" $direction="row" $wrap="wrap" $css="flex: 80%;"> <Box $gap="0.7rem" $direction="row" $wrap="wrap" $css="flex: 80%;">
<Box $css="flex: auto;"> <Box $css="flex: auto;" $width="15rem">
<SearchUsers <SearchUsers
key={resetKey + 1} key={resetKey + 1}
doc={doc} doc={doc}

View File

@@ -1,6 +1,6 @@
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useMemo, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Options } from 'react-select'; import { InputActionMeta, Options } from 'react-select';
import AsyncSelect from 'react-select/async'; import AsyncSelect from 'react-select/async';
import { useCunninghamTheme } from '@/cunningham'; import { useCunninghamTheme } from '@/cunningham';
@@ -42,7 +42,7 @@ export const SearchUsers = ({
const options = data?.results; const options = data?.results;
useEffect(() => { const optionsSelect = useMemo(() => {
if (!resolveOptionsRef.current || !options) { if (!resolveOptionsRef.current || !options) {
return; return;
} }
@@ -81,6 +81,8 @@ export const SearchUsers = ({
resolveOptionsRef.current(users); resolveOptionsRef.current(users);
resolveOptionsRef.current = null; resolveOptionsRef.current = null;
return users;
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, [options, selectedUsers]); }, [options, selectedUsers]);
@@ -91,16 +93,26 @@ export const SearchUsers = ({
}; };
const timeout = useRef<NodeJS.Timeout | null>(null); const timeout = useRef<NodeJS.Timeout | null>(null);
const onInputChangeHandle = useCallback((newValue: string) => { const onInputChangeHandle = useCallback(
setInput(newValue); (newValue: string, actionMeta: InputActionMeta) => {
if (timeout.current) { if (
clearTimeout(timeout.current); actionMeta.action === 'input-blur' ||
} actionMeta.action === 'menu-close'
) {
return;
}
timeout.current = setTimeout(() => { setInput(newValue);
setUserQuery(newValue); if (timeout.current) {
}, 1000); clearTimeout(timeout.current);
}, []); }
timeout.current = setTimeout(() => {
setUserQuery(newValue);
}, 1000);
},
[],
);
return ( return (
<AsyncSelect <AsyncSelect
@@ -125,10 +137,10 @@ export const SearchUsers = ({
aria-label={t('Find a member to add to the document')} aria-label={t('Find a member to add to the document')}
isMulti isMulti
loadOptions={loadOptions} loadOptions={loadOptions}
defaultOptions={[]} defaultOptions={optionsSelect}
onInputChange={onInputChangeHandle} onInputChange={onInputChangeHandle}
inputValue={input} inputValue={input}
placeholder={t('Search new members by email')} placeholder={t('Search by email')}
noOptionsMessage={() => noOptionsMessage={() =>
input input
? t("We didn't find a mail matching, try to be more accurate") ? t("We didn't find a mail matching, try to be more accurate")