️(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

@@ -170,7 +170,7 @@ test.describe('Document create member', () => {
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 page.getByRole('option', { name: email }).click();
@@ -191,7 +191,22 @@ test.describe('Document create member', () => {
expect(responseCreateInvitation.ok()).toBeTruthy();
await inputSearch.fill(email);
await expect(page.getByText('Loading...')).toBeHidden();
await expect(page.getByRole('option', { name: email })).toBeHidden();
await page.getByRole('option', { name: email }).click();
// 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,
};
const onError = (dataError: APIErrorUser['data']) => {
const messageError =
dataError?.type === OptionType.INVITATION
const onError = (dataError: APIErrorUser) => {
let messageError =
dataError['data']?.type === OptionType.INVITATION
? t(`Failed to create the invitation for {{email}}.`, {
email: dataError?.value,
email: dataError['data']?.value,
})
: 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);
};
@@ -106,11 +115,12 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
setIsPending(false);
setResetKey(resetKey + 1);
setSelectedUsers([]);
settledPromises.forEach((settledPromise) => {
switch (settledPromise.status) {
case 'rejected':
onError((settledPromise.reason as APIErrorUser).data);
onError(settledPromise.reason as APIErrorUser);
break;
case 'fulfilled':
@@ -132,7 +142,7 @@ export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
<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: 80%;">
<Box $css="flex: auto;">
<Box $css="flex: auto;" $width="15rem">
<SearchUsers
key={resetKey + 1}
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 { Options } from 'react-select';
import { InputActionMeta, Options } from 'react-select';
import AsyncSelect from 'react-select/async';
import { useCunninghamTheme } from '@/cunningham';
@@ -42,7 +42,7 @@ export const SearchUsers = ({
const options = data?.results;
useEffect(() => {
const optionsSelect = useMemo(() => {
if (!resolveOptionsRef.current || !options) {
return;
}
@@ -81,6 +81,8 @@ export const SearchUsers = ({
resolveOptionsRef.current(users);
resolveOptionsRef.current = null;
return users;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [options, selectedUsers]);
@@ -91,16 +93,26 @@ export const SearchUsers = ({
};
const timeout = useRef<NodeJS.Timeout | null>(null);
const onInputChangeHandle = useCallback((newValue: string) => {
setInput(newValue);
if (timeout.current) {
clearTimeout(timeout.current);
}
const onInputChangeHandle = useCallback(
(newValue: string, actionMeta: InputActionMeta) => {
if (
actionMeta.action === 'input-blur' ||
actionMeta.action === 'menu-close'
) {
return;
}
timeout.current = setTimeout(() => {
setUserQuery(newValue);
}, 1000);
}, []);
setInput(newValue);
if (timeout.current) {
clearTimeout(timeout.current);
}
timeout.current = setTimeout(() => {
setUserQuery(newValue);
}, 1000);
},
[],
);
return (
<AsyncSelect
@@ -125,10 +137,10 @@ export const SearchUsers = ({
aria-label={t('Find a member to add to the document')}
isMulti
loadOptions={loadOptions}
defaultOptions={[]}
defaultOptions={optionsSelect}
onInputChange={onInputChangeHandle}
inputValue={input}
placeholder={t('Search new members by email')}
placeholder={t('Search by email')}
noOptionsMessage={() =>
input
? t("We didn't find a mail matching, try to be more accurate")