🛂(frontend) fix cannot manage member on small screen

We can now manage document members on small
screens (mobile and tablet). We improved the
overall responsive design of the doc share modal.
This commit is contained in:
Anthony LC 2026-04-17 15:42:31 +02:00
parent 5a687799d5
commit 599b909318
No known key found for this signature in database
10 changed files with 90 additions and 30 deletions

View file

@ -6,16 +6,17 @@ and this project adheres to
## [Unreleased]
### Changed
- ♿️(frontend) structure correctly 5xx error alerts #2128
- ♿️(frontend) make doc search result labels uniquely identifiable #2212
### Fixed
- 🚸(frontend) redirect on current url tab after 401 #2197
- 🐛(frontend) abort check media status unmount #2194
- ✨(backend) order pinned documents by last updated at #2028
### Changed
- ♿️(frontend) structure correctly 5xx error alerts #2128
- ♿️(frontend) make doc search result labels uniquely identifiable #2212
- 🛂(frontend) fix cannot manage member on small screen #2226
## [v4.8.6] - 2026-04-08

View file

@ -1,7 +1,6 @@
import { ReactNode } from 'react';
import { useCunninghamTheme } from '@/cunningham';
import { useResponsiveStore } from '@/stores';
import { Box } from '../Box';
@ -18,29 +17,29 @@ export const QuickSearchItemContent = ({
}: QuickSearchItemContentProps) => {
const { spacingsTokens } = useCunninghamTheme();
const { isDesktop } = useResponsiveStore();
return (
<Box
className="--docs--quick-search-item-content"
$direction="row"
$align="center"
$padding={{ horizontal: '2xs', vertical: '4xs' }}
$justify="space-between"
$minHeight="34px"
$width="100%"
$gap="sm"
>
<Box
className="--docs--quick-search-item-content-left"
$direction="row"
$align="center"
$gap={spacingsTokens['2xs']}
$width="100%"
>
{left}
</Box>
{isDesktop && right && (
{right && (
<Box
className={!alwaysShowRight ? 'show-right-on-focus' : ''}
className={`--docs--quick-search-item-content-right ${!alwaysShowRight ? 'show-right-on-focus' : ''}`}
$direction="row"
$align="center"
>

View file

@ -6,7 +6,7 @@ import {
} from '@gouvfr-lasuite/cunningham-react';
import { MouseEventHandler, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { createGlobalStyle } from 'styled-components';
import { createGlobalStyle, css } from 'styled-components';
import {
Box,
@ -20,6 +20,7 @@ import { QuickSearchData, QuickSearchGroup } from '@/components/quick-search';
import { useCunninghamTheme } from '@/cunningham';
import { AccessRequest, Doc, Role } from '@/docs/doc-management/';
import { useAuth } from '@/features/auth';
import { useResponsiveStore } from '@/stores';
import {
useAcceptDocAccessRequest,
@ -33,8 +34,12 @@ import { DocRoleDropdown } from './DocRoleDropdown';
import { SearchUserRow } from './SearchUserRow';
const QuickSearchGroupAccessRequestStyle = createGlobalStyle`
.--docs--share-access-request [cmdk-item][data-selected='true'] {
background: inherit
.quick-search-container .--docs--share-access-request [cmdk-item]:hover,
.quick-search-container .--docs--share-access-request [cmdk-item][data-selected='true'] {
background: inherit;
}
.--docs--doc-share-access-request-item:hover {
background: var(--c--contextuals--background--semantic--contextual--primary);
}
`;
@ -45,6 +50,7 @@ type Props = {
const DocShareAccessRequestItem = ({ doc, accessRequest }: Props) => {
const { t } = useTranslation();
const { isSmallMobile } = useResponsiveStore();
const { toast } = useToastProvider();
const { spacingsTokens } = useCunninghamTheme();
const { mutate: acceptDocAccessRequests } = useAcceptDocAccessRequest();
@ -67,6 +73,15 @@ const DocShareAccessRequestItem = ({ doc, accessRequest }: Props) => {
$width="100%"
data-testid={`doc-share-access-request-row-${accessRequest.user.email}`}
className="--docs--doc-share-access-request-item"
$css={css`
& .--docs--quick-search-item-content {
flex-wrap: wrap;
.--docs--quick-search-item-content-right {
margin-left: auto;
}
}
`}
>
<SearchUserRow
alwaysShowRight={true}
@ -84,7 +99,7 @@ const DocShareAccessRequestItem = ({ doc, accessRequest }: Props) => {
/>
<Button
color="brand"
variant="tertiary"
variant="secondary"
onClick={() =>
acceptDocAccessRequests({
docId: doc.id,
@ -92,7 +107,7 @@ const DocShareAccessRequestItem = ({ doc, accessRequest }: Props) => {
role,
})
}
size="small"
size={isSmallMobile ? 'nano' : 'small'}
>
{t('Approve')}
</Button>
@ -153,6 +168,7 @@ export const QuickSearchGroupAccessRequest = ({
<Box
aria-label={t('List request access card')}
className="--docs--share-access-request"
$padding={{ horizontal: 'base' }}
>
<QuickSearchGroupAccessRequestStyle />
<QuickSearchGroup

View file

@ -11,6 +11,7 @@ import { Box, Card } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
import { Doc, Role } from '@/docs/doc-management';
import { User } from '@/features/auth';
import { useResponsiveStore } from '@/stores';
import { useCreateDocAccess, useCreateDocInvitation } from '../api';
import { OptionType } from '../types';
@ -38,7 +39,7 @@ export const DocShareAddMemberList = ({
}: Props) => {
const { t } = useTranslation();
const { toast } = useToastProvider();
const { isSmallMobile } = useResponsiveStore();
const [isLoading, setIsLoading] = useState(false);
const { spacingsTokens } = useCunninghamTheme();
const [invitationRole, setInvitationRole] = useState<Role>(Role.EDITOR);
@ -118,14 +119,15 @@ export const DocShareAddMemberList = ({
<Card
className="--docs--doc-share-add-member-list"
data-testid="doc-share-add-member-list"
$direction="row"
$align="center"
$direction={isSmallMobile ? 'column' : 'row'}
$align={isSmallMobile ? 'stretch' : 'center'}
$padding={spacingsTokens.sm}
$scope="surface"
$theme="tertiary"
$variation=""
$border="1px solid var(--c--contextuals--border--surface--primary)"
$margin={{ bottom: 'sm' }}
$gap={spacingsTokens.xs}
>
<Box
$direction="row"
@ -142,7 +144,12 @@ export const DocShareAddMemberList = ({
/>
))}
</Box>
<Box $direction="row" $align="center" $gap={spacingsTokens.xs}>
<Box
$direction="row"
$align="center"
$gap={spacingsTokens.xs}
$margin={{ left: isSmallMobile ? 'auto' : '' }}
>
<DocRoleDropdown
canUpdate={canShare}
currentRole={invitationRole}
@ -154,6 +161,7 @@ export const DocShareAddMemberList = ({
disabled={isLoading}
aria-label={inviteLabel}
data-testid="doc-share-invite-button"
size={isSmallMobile ? 'small' : 'medium'}
>
{t('Invite')}
</Button>

View file

@ -1,4 +1,5 @@
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, BoxButton, Icon, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
@ -31,7 +32,13 @@ export const DocShareAddMemberListItem = ({ user, onRemoveUser }: Props) => {
$theme="neutral"
$variation="secondary"
>
<Text $withThemeInherited $size="xs">
<Text
$withThemeInherited
$size="xs"
$css={css`
line-break: anywhere;
`}
>
{user.full_name || user.email}
</Text>
<BoxButton

View file

@ -162,7 +162,10 @@ export const QuickSearchGroupInvitation = ({
}
return (
<Box aria-label={t('List invitation card')}>
<Box
aria-label={t('List invitation card')}
$padding={{ horizontal: 'base' }}
>
<QuickSearchGroup
group={invitationsData}
renderElement={(invitation) => (

View file

@ -30,7 +30,6 @@ export const DocShareMemberItem = ({
const { t } = useTranslation();
const { isLastOwner } = useWhoAmI(access);
const { toast } = useToastProvider();
const { spacingsTokens } = useCunninghamTheme();
const message = isLastOwner
@ -121,7 +120,10 @@ export const QuickSearchGroupMember = ({
}, [membersQuery.data, t]);
return (
<Box aria-label={t('List members card')} $padding={{ bottom: '3xs' }}>
<Box
aria-label={t('List members card')}
$padding={{ horizontal: 'base', bottom: '3xs' }}
>
<QuickSearchGroup
group={membersData}
renderElement={(access) => (

View file

@ -63,7 +63,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
const API_USERS_SEARCH_QUERY_MIN_LENGTH =
config?.API_USERS_SEARCH_QUERY_MIN_LENGTH || 5;
const { isDesktop } = useResponsiveStore();
const { isLargeScreen } = useResponsiveStore();
/**
* The modal content height is calculated based on the viewport height.
@ -75,7 +75,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
* - 690px is the height of the content in desktop
* This ensures that the modal content is always visible and does not overflow.
*/
const modalContentHeight = isDesktop
const modalContentHeight = isLargeScreen
? 'min(690px, calc(100dvh - 2em - 12px - 34px))'
: `calc(100dvh - 34px)`;
const [selectedUsers, setSelectedUsers] = useState<User[]>([]);
@ -181,7 +181,7 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
closeOnClickOutside
data-testid="doc-share-modal"
aria-label={t('Share the document')}
size={isDesktop ? ModalSize.LARGE : ModalSize.FULL}
size={isLargeScreen ? ModalSize.LARGE : ModalSize.FULL}
aria-modal="true"
onClose={onClose}
title={
@ -289,9 +289,11 @@ export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
/>
)}
{showMemberSection && isRootDoc && (
<Box $padding={{ horizontal: 'base', top: 'base' }}>
<Box $padding={{ top: 'base' }}>
<QuickSearchGroupAccessRequest doc={doc} />
<HorizontalSeparator $margin={{ vertical: 'sm' }} />
<QuickSearchGroupInvitation doc={doc} />
<HorizontalSeparator $margin={{ vertical: 'sm' }} />
<QuickSearchGroupMember doc={doc} />
</Box>
)}

View file

@ -1,3 +1,5 @@
import { css } from 'styled-components';
import { Box, Text } from '@/components';
import {
QuickSearchItemContent,
@ -38,11 +40,24 @@ export const SearchUserRow = ({
background={isInvitation ? colorsTokens['gray-400'] : undefined}
/>
<Box $direction="column">
<Text $size="sm" $weight="500">
<Text
$size="sm"
$weight="500"
$css={css`
line-break: anywhere;
`}
>
{hasFullName ? user.full_name : user.email}
</Text>
{hasFullName && (
<Text $size="xs" $margin={{ top: '-2px' }} $variation="secondary">
<Text
$size="xs"
$margin={{ top: '-2px' }}
$variation="secondary"
$css={css`
line-break: anywhere;
`}
>
{user.email}
</Text>
)}

View file

@ -10,10 +10,12 @@ export interface UseResponsiveStore {
screenWidth: number;
setScreenSize: (size: ScreenSize) => void;
isDesktop: boolean;
isLargeScreen: boolean;
initializeResizeListener: () => () => void;
}
const initialState = {
isLargeScreen: false,
isMobile: false,
isSmallMobile: false,
isTablet: false,
@ -24,6 +26,7 @@ const initialState = {
export const useResponsiveStore = create<UseResponsiveStore>((set) => ({
isDesktop: initialState.isDesktop,
isLargeScreen: initialState.isLargeScreen,
isMobile: initialState.isMobile,
isSmallMobile: initialState.isSmallMobile,
isTablet: initialState.isTablet,
@ -40,6 +43,7 @@ export const useResponsiveStore = create<UseResponsiveStore>((set) => ({
isMobile: true,
isTablet: true,
isSmallMobile: true,
isLargeScreen: false,
});
} else if (width < 768) {
set({
@ -48,6 +52,7 @@ export const useResponsiveStore = create<UseResponsiveStore>((set) => ({
isTablet: true,
isMobile: true,
isSmallMobile: false,
isLargeScreen: false,
});
} else if (width >= 768 && width < 1024) {
set({
@ -56,6 +61,7 @@ export const useResponsiveStore = create<UseResponsiveStore>((set) => ({
isTablet: true,
isMobile: false,
isSmallMobile: false,
isLargeScreen: true,
});
} else {
set({
@ -64,6 +70,7 @@ export const useResponsiveStore = create<UseResponsiveStore>((set) => ({
isTablet: false,
isMobile: false,
isSmallMobile: false,
isLargeScreen: true,
});
}