⚡️(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:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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")
|
||||||
|
|||||||
Reference in New Issue
Block a user