Compare commits

...

2 Commits

Author SHA1 Message Date
Cyril d8d655cc3d ️(frontend) structure 5xx error alerts
Use h1/p for 500/502/503; pass status from doc and version views.
2026-03-25 11:59:19 +01:00
Anthony LC fb92a43755 🚸(frontend) hint min char search users
We give a hint to the user about the minimum
number of characters required to perform a search
in the quick search input of the doc share modal.
This is to improve the user experience.
2026-03-25 09:33:14 +01:00
12 changed files with 137 additions and 8 deletions
+5
View File
@@ -6,9 +6,14 @@ and this project adheres to
## [Unreleased]
### Added
- 🚸(frontend) hint min char search users #2064
### Changed
- 💄(frontend) improve comments highlights #1961
- ♿️(frontend) structure correctly 5xx error alerts #2128
## [v4.8.3] - 2026-03-23
@@ -17,6 +17,41 @@ test.describe('Document create member', () => {
await page.goto('/');
});
test('it checks search hints', async ({ page, browserName }) => {
await createDoc(page, 'select-multi-users', browserName, 1);
await page.getByRole('button', { name: 'Share' }).click();
const shareModal = page.getByLabel('Share the document');
await expect(shareModal.getByText('Document owner')).toBeVisible();
const inputSearch = page.getByTestId('quick-search-input');
await inputSearch.fill('u');
await expect(shareModal.getByText('Document owner')).toBeHidden();
await expect(
shareModal.getByText('Type at least 3 characters to display user names'),
).toBeVisible();
await inputSearch.fill('user');
await expect(
shareModal.getByText('Type at least 3 characters to display user names'),
).toBeHidden();
await expect(shareModal.getByText('Choose a user')).toBeVisible();
await inputSearch.fill('anything');
await expect(shareModal.getByText('Choose a user')).toBeHidden();
await expect(
shareModal.getByText(
'No results. Type a full email address to invite someone.',
),
).toBeVisible();
await inputSearch.fill('anything@test.com');
await expect(
shareModal.getByText(
'No results. Type a full email address to invite someone.',
),
).toBeHidden();
await expect(shareModal.getByText('Choose the email')).toBeVisible();
});
test('it selects 2 users and 1 invitation', async ({ page, browserName }) => {
const inputFill = 'user.test';
const responsePromise = page.waitForResponse(
@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next';
import styled from 'styled-components';
import { Box, Text, TextType } from '@/components';
import { useHttpErrorMessages } from '@/hooks';
const AlertStyled = styled(Alert)`
& .c__button--tertiary:hover {
@@ -16,6 +17,7 @@ interface TextErrorsProps extends TextType {
defaultMessage?: string;
icon?: ReactNode;
canClose?: boolean;
status?: number;
}
export const TextErrors = ({
@@ -23,6 +25,7 @@ export const TextErrors = ({
defaultMessage,
icon,
canClose = false,
status,
...textProps
}: TextErrorsProps) => {
return (
@@ -35,6 +38,7 @@ export const TextErrors = ({
<TextOnlyErrors
causes={causes}
defaultMessage={defaultMessage}
status={status}
{...textProps}
/>
</AlertStyled>
@@ -44,9 +48,39 @@ export const TextErrors = ({
export const TextOnlyErrors = ({
causes,
defaultMessage,
status,
...textProps
}: TextErrorsProps) => {
const { t } = useTranslation();
const httpError = useHttpErrorMessages(status);
if (httpError) {
return (
<Box $direction="column" $gap="0.2rem">
<Text
as="h1"
$theme="error"
$textAlign="center"
$margin="0"
$size="1rem"
$weight="unset"
{...textProps}
>
{httpError.title}
</Text>
<Text
as="p"
$theme="error"
$textAlign="center"
$margin="0"
$size="0.875rem"
{...textProps}
>
{httpError.detail}
</Text>
</Box>
);
}
return (
<Box $direction="column" $gap="0.2rem">
@@ -48,7 +48,7 @@ export const QuickSearchInput = ({
$direction="row"
$align="center"
className="quick-search-input"
$gap={spacingsTokens['2xs']}
$gap={spacingsTokens['xxs']}
$padding={{ horizontal: 'base', vertical: 'xxs' }}
>
<Icon iconName="search" $variation="secondary" aria-hidden="true" />
@@ -62,6 +62,7 @@ export const QuickSearchInput = ({
placeholder={placeholder ?? t('Search')}
onValueChange={onFilter}
maxLength={254}
minLength={6}
data-testid="quick-search-input"
/>
</Box>
@@ -18,14 +18,15 @@ export const QuickSearchStyle = createGlobalStyle`
[cmdk-input] {
border: none;
width: 100%;
font-size: 17px;
font-size: 16px;
background: white;
outline: none;
color: var(--c--contextuals--content--semantic--neutral--primary);
border-radius: var(--c--globals--spacings--0);
font-family: var(--c--globals--font--families--base);
&::placeholder {
color: var(--c--globals--colors--gray-500);
color: var(--c--contextuals--content--semantic--neutral--tertiary);
}
}
@@ -124,7 +124,8 @@ export const DocShareAddMemberList = ({
$scope="surface"
$theme="tertiary"
$variation=""
$border="1px solid var(--c--contextuals--border--semantic--contextual--primary)"
$border="1px solid var(--c--contextuals--border--surface--primary)"
$margin={{ bottom: 'sm' }}
>
<Box
$direction="row"
@@ -289,7 +289,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
/>
)}
{showMemberSection && isRootDoc && (
<Box $padding={{ horizontal: 'base' }}>
<Box $padding={{ horizontal: 'base', top: 'base' }}>
<QuickSearchGroupAccessRequest doc={doc} />
<QuickSearchGroupInvitation doc={doc} />
<QuickSearchGroupMember doc={doc} />
@@ -301,6 +301,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
searchUsersRawData={searchUsersQuery.data}
onSelect={onSelect}
userQuery={userQuery}
minLength={API_USERS_SEARCH_QUERY_MIN_LENGTH}
/>
)}
</QuickSearch>
@@ -321,14 +322,35 @@ interface QuickSearchInviteInputSectionProps {
onSelect: (usr: User) => void;
searchUsersRawData: User[] | undefined;
userQuery: string;
minLength: number;
}
const QuickSearchInviteInputSection = ({
onSelect,
searchUsersRawData,
userQuery,
minLength,
}: QuickSearchInviteInputSectionProps) => {
const { t } = useTranslation();
const hint = useMemo(() => {
if (userQuery.length < minLength) {
return t('Type at least {{minLength}} characters to display user names', {
minLength,
});
}
if (isValidEmail(userQuery)) {
return t('Choose the email');
}
if (!searchUsersRawData?.length) {
return t('No results. Type a full email address to invite someone.');
}
return t('Choose a user');
}, [minLength, searchUsersRawData?.length, t, userQuery]);
useEffect(() => {
announce(hint, 'polite');
}, [hint]);
const searchUserData: QuickSearchData<User> = useMemo(() => {
const users = searchUsersRawData || [];
@@ -347,7 +369,7 @@ const QuickSearchInviteInputSection = ({
);
return {
groupName: t('Search user result'),
groupName: hint,
elements: users,
endActions:
isEmail && !hasEmailInUsers
@@ -359,12 +381,12 @@ const QuickSearchInviteInputSection = ({
]
: undefined,
};
}, [onSelect, searchUsersRawData, t, userQuery]);
}, [searchUsersRawData, userQuery, hint, onSelect]);
return (
<Box
aria-label={t('List search user result card')}
$padding={{ horizontal: 'base', bottom: '3xs' }}
$padding={{ horizontal: 'base', bottom: '3xs', top: 'base' }}
>
<QuickSearchGroup
group={searchUserData}
@@ -58,6 +58,7 @@ export const DocVersionEditor = ({
<Box $margin="large" className="--docs--doc-version-editor-error">
<TextErrors
causes={error.cause}
status={error.status}
icon={
error.status === 502 ? (
<Text
@@ -62,6 +62,7 @@ const VersionListState = ({
>
<TextErrors
causes={error.cause}
status={error.status}
icon={
error.status === 502 ? (
<Icon iconName="wifi_off" $theme="danger" />
@@ -1,4 +1,5 @@
export * from './useClipboard';
export * from './useCmdK';
export * from './useDate';
export * from './useHttpErrorMessages';
export * from './useKeyboardAction';
@@ -0,0 +1,26 @@
import { useTranslation } from 'react-i18next';
export const useHttpErrorMessages = (status?: number) => {
const { t } = useTranslation();
const messages: Record<number, { title: string; detail: string }> = {
500: {
title: t('500 - Internal Server Error'),
detail: t('The server met an unexpected condition.'),
},
502: {
title: t('502 - Bad Gateway'),
detail: t(
'The server received an invalid response. Please check your connection and try again.',
),
},
503: {
title: t('503 - Service Unavailable'),
detail: t(
'The service is temporarily unavailable. Please try again later.',
),
},
};
return status ? messages[status] : undefined;
};
@@ -216,6 +216,7 @@ const DocPage = ({ id }: DocProps) => {
<Box $margin="large">
<TextErrors
causes={error.cause}
status={error.status}
icon={
error.status === 502 ? (
<Icon iconName="wifi_off" $theme="danger" $withThemeInherited />