🩹(frontend) disable submission button while a request is pending

If any request is taking longer than expected, the user could interact with
the frontend, thinking the previous submission was not taken into account,
and would re-submit a request.

It happened to me while sending an invitation. Replaying request can either
lead to inconsistencies in db for the user, or to errors in requests' response.

I propose to disable all interactive button while submitting a request.
It's good enough for this actual sprint, these types of interactivity issues
could be improved later on.
This commit is contained in:
Lebaud Antoine
2024-03-26 21:50:12 +01:00
committed by aleb_the_flash
parent df15b41a87
commit 5ed05b96a5
4 changed files with 30 additions and 6 deletions

View File

@@ -57,6 +57,8 @@ export const ModalAddMembers = ({
const { mutateAsync: createInvitation } = useCreateInvitation(); const { mutateAsync: createInvitation } = useCreateInvitation();
const { mutateAsync: createTeamAccess } = useCreateTeamAccess(); const { mutateAsync: createTeamAccess } = useCreateTeamAccess();
const [isPending, setIsPending] = useState<boolean>(false);
const switchActions = (selectedMembers: OptionsSelect) => const switchActions = (selectedMembers: OptionsSelect) =>
selectedMembers.map(async (selectedMember) => { selectedMembers.map(async (selectedMember) => {
switch (selectedMember.type) { switch (selectedMember.type) {
@@ -111,11 +113,15 @@ export const ModalAddMembers = ({
}; };
const handleValidate = async () => { const handleValidate = async () => {
setIsPending(true);
const settledPromises = await Promise.allSettled< const settledPromises = await Promise.allSettled<
OptionInvitation | OptionNewMember OptionInvitation | OptionNewMember
>(switchActions(selectedMembers)); >(switchActions(selectedMembers));
onClose(); onClose();
setIsPending(false);
settledPromises.forEach((settledPromise) => { settledPromises.forEach((settledPromise) => {
switch (settledPromise.status) { switch (settledPromise.status) {
case 'rejected': case 'rejected':
@@ -133,7 +139,12 @@ export const ModalAddMembers = ({
<Modal <Modal
isOpen isOpen
leftActions={ leftActions={
<Button color="secondary" fullWidth onClick={onClose}> <Button
color="secondary"
fullWidth
onClick={onClose}
disabled={isPending}
>
{t('Cancel')} {t('Cancel')}
</Button> </Button>
} }
@@ -144,7 +155,7 @@ export const ModalAddMembers = ({
<Button <Button
color="primary" color="primary"
fullWidth fullWidth
disabled={!selectedMembers.length} disabled={!selectedMembers.length || isPending}
onClick={() => void handleValidate()} onClick={() => void handleValidate()}
> >
{t('Validate')} {t('Validate')}
@@ -166,6 +177,7 @@ export const ModalAddMembers = ({
team={team} team={team}
setSelectedMembers={setSelectedMembers} setSelectedMembers={setSelectedMembers}
selectedMembers={selectedMembers} selectedMembers={selectedMembers}
disabled={isPending}
/> />
{selectedMembers.length > 0 && ( {selectedMembers.length > 0 && (
<Box className="mt-s"> <Box className="mt-s">
@@ -174,7 +186,7 @@ export const ModalAddMembers = ({
</Text> </Text>
<ChooseRole <ChooseRole
currentRole={currentRole} currentRole={currentRole}
disabled={false} disabled={isPending}
defaultRole={Role.MEMBER} defaultRole={Role.MEMBER}
setRole={setSelectedRole} setRole={setSelectedRole}
/> />

View File

@@ -15,12 +15,14 @@ interface SearchMembersProps {
team: Team; team: Team;
selectedMembers: OptionsSelect; selectedMembers: OptionsSelect;
setSelectedMembers: (value: OptionsSelect) => void; setSelectedMembers: (value: OptionsSelect) => void;
disabled?: boolean;
} }
export const SearchMembers = ({ export const SearchMembers = ({
team, team,
selectedMembers, selectedMembers,
setSelectedMembers, setSelectedMembers,
disabled,
}: SearchMembersProps) => { }: SearchMembersProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [input, setInput] = useState(''); const [input, setInput] = useState('');
@@ -100,6 +102,7 @@ export const SearchMembers = ({
return ( return (
<AsyncSelect <AsyncSelect
isDisabled={disabled}
aria-label={t('Find a member to add to the team')} aria-label={t('Find a member to add to the team')}
isMulti isMulti
loadOptions={loadOptions} loadOptions={loadOptions}

View File

@@ -37,6 +37,7 @@ export const ModalRole = ({
mutate: updateTeamAccess, mutate: updateTeamAccess,
error: errorUpdate, error: errorUpdate,
isError: isErrorUpdate, isError: isErrorUpdate,
isPending,
} = useUpdateTeamAccess({ } = useUpdateTeamAccess({
onSuccess: () => { onSuccess: () => {
toast(t('The role has been updated'), VariantType.SUCCESS, { toast(t('The role has been updated'), VariantType.SUCCESS, {
@@ -53,7 +54,12 @@ export const ModalRole = ({
<Modal <Modal
isOpen isOpen
leftActions={ leftActions={
<Button color="secondary" fullWidth onClick={() => onClose()}> <Button
color="secondary"
fullWidth
onClick={() => onClose()}
disabled={isPending}
>
{t('Cancel')} {t('Cancel')}
</Button> </Button>
} }
@@ -71,7 +77,7 @@ export const ModalRole = ({
accessId: access.id, accessId: access.id,
}); });
}} }}
disabled={isNotAllowed} disabled={isNotAllowed || isPending}
> >
{t('Validate')} {t('Validate')}
</Button> </Button>

View File

@@ -57,7 +57,10 @@ export const CardCreateTeam = () => {
<StyledLink href="/"> <StyledLink href="/">
<Button color="secondary">{t('Cancel')}</Button> <Button color="secondary">{t('Cancel')}</Button>
</StyledLink> </StyledLink>
<Button onClick={() => createTeam(teamName)} disabled={!teamName}> <Button
onClick={() => createTeam(teamName)}
disabled={!teamName || isPending}
>
{t('Create the team')} {t('Create the team')}
</Button> </Button>
</Box> </Box>