💄(frontend) refactor document sharing and grid components

Improvements:
- Added disabled state for dropdown menus in share settings
- Updated document grid layout and responsiveness
- Simplified sharing and access count logic
- Improved tooltips and visibility of shared documents
- Created a new responsive doc grid hook
This commit is contained in:
Nathan Panchout
2025-01-27 11:38:24 +01:00
parent ee41d156c7
commit 55ddfe9181
16 changed files with 255 additions and 219 deletions

View File

@@ -20,12 +20,14 @@ export type DropdownMenuProps = {
showArrow?: boolean; showArrow?: boolean;
label?: string; label?: string;
arrowCss?: BoxProps['$css']; arrowCss?: BoxProps['$css'];
disabled?: boolean;
topMessage?: string; topMessage?: string;
}; };
export const DropdownMenu = ({ export const DropdownMenu = ({
options, options,
children, children,
disabled = false,
showArrow = false, showArrow = false,
arrowCss, arrowCss,
label, label,
@@ -40,6 +42,10 @@ export const DropdownMenu = ({
setIsOpen(isOpen); setIsOpen(isOpen);
}; };
if (disabled) {
return children;
}
return ( return (
<DropButton <DropButton
isOpen={isOpen} isOpen={isOpen}

View File

@@ -1,8 +1,8 @@
import Link from 'next/link'; import Link from 'next/link';
import styled from 'styled-components'; import styled, { RuleSet } from 'styled-components';
export interface LinkProps { export interface LinkProps {
$css?: string; $css?: string | RuleSet<object>;
} }
export const StyledLink = styled(Link)<LinkProps>` export const StyledLink = styled(Link)<LinkProps>`
@@ -12,5 +12,5 @@ export const StyledLink = styled(Link)<LinkProps>`
color: #ffffff; color: #ffffff;
} }
display: flex; display: flex;
${({ $css }) => $css && `${$css};`} ${({ $css }) => $css && (typeof $css === 'string' ? `${$css};` : $css)}
`; `;

View File

@@ -42,7 +42,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
<Box <Box
aria-label={t('Public document')} aria-label={t('Public document')}
$color={colors['primary-800']} $color={colors['primary-800']}
$background={colors['primary-100']} $background={colors['primary-050']}
$radius={spacings['3xs']} $radius={spacings['3xs']}
$direction="row" $direction="row"
$padding="xs" $padding="xs"

View File

@@ -18,11 +18,7 @@ import {
} from '@/components'; } from '@/components';
import { useCunninghamTheme } from '@/cunningham'; import { useCunninghamTheme } from '@/cunningham';
import { useEditorStore } from '@/features/docs/doc-editor/'; import { useEditorStore } from '@/features/docs/doc-editor/';
import { import { Doc, ModalRemoveDoc } from '@/features/docs/doc-management';
Doc,
ModalRemoveDoc,
useCopyDocLink,
} from '@/features/docs/doc-management';
import { DocShareModal } from '@/features/docs/doc-share'; import { DocShareModal } from '@/features/docs/doc-share';
import { import {
KEY_LIST_DOC_VERSIONS, KEY_LIST_DOC_VERSIONS,
@@ -41,8 +37,6 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const hasAccesses = doc.nb_accesses > 1; const hasAccesses = doc.nb_accesses > 1;
const queryClient = useQueryClient(); const queryClient = useQueryClient();
const copyDocLink = useCopyDocLink(doc.id);
const { spacingsTokens, colorsTokens } = useCunninghamTheme(); const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const spacings = spacingsTokens(); const spacings = spacingsTokens();
@@ -55,24 +49,15 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
const { isSmallMobile, isDesktop } = useResponsiveStore(); const { isSmallMobile, isDesktop } = useResponsiveStore();
const { editor } = useEditorStore(); const { editor } = useEditorStore();
const { toast } = useToastProvider(); const { toast } = useToastProvider();
const canViewAccesses = doc.abilities.accesses_view;
const options: DropdownMenuOption[] = [ const options: DropdownMenuOption[] = [
...(isSmallMobile ...(isSmallMobile
? [ ? [
{ {
label: canViewAccesses ? t('Share') : t('Copy link'), label: t('Share'),
icon: canViewAccesses ? 'group' : 'link', icon: 'group',
callback: modalShare.open,
callback: () => {
if (canViewAccesses) {
modalShare.open();
return;
}
copyDocLink();
},
}, },
{ {
label: t('Export'), label: t('Export'),
@@ -165,7 +150,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
$margin={{ left: 'auto' }} $margin={{ left: 'auto' }}
$gap={spacings['2xs']} $gap={spacings['2xs']}
> >
{canViewAccesses && !isSmallMobile && ( {!isSmallMobile && (
<> <>
{!hasAccesses && ( {!hasAccesses && (
<Button <Button
@@ -199,23 +184,13 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
}} }}
size={isSmallMobile ? 'small' : 'medium'} size={isSmallMobile ? 'small' : 'medium'}
> >
{doc.nb_accesses - 1} {doc.nb_accesses}
</Button> </Button>
</Box> </Box>
)} )}
</> </>
)} )}
{!canViewAccesses && !isSmallMobile && (
<Button
color="tertiary-text"
onClick={() => {
copyDocLink();
}}
size={isSmallMobile ? 'small' : 'medium'}
>
{t('Copy link')}
</Button>
)}
{!isSmallMobile && ( {!isSmallMobile && (
<Button <Button
color="tertiary-text" color="tertiary-text"

View File

@@ -4,7 +4,7 @@ import { useTranslation } from 'react-i18next';
import { createGlobalStyle, css } from 'styled-components'; import { createGlobalStyle, css } from 'styled-components';
import { useDebouncedCallback } from 'use-debounce'; import { useDebouncedCallback } from 'use-debounce';
import { Box, LoadMoreText } from '@/components'; import { Box, HorizontalSeparator, LoadMoreText, Text } from '@/components';
import { import {
QuickSearch, QuickSearch,
QuickSearchData, QuickSearchData,
@@ -56,7 +56,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
const [userQuery, setUserQuery] = useState(''); const [userQuery, setUserQuery] = useState('');
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const [listHeight, setListHeight] = useState<string>('0px'); const [listHeight, setListHeight] = useState<string>('400px');
const canShare = doc.abilities.accesses_manage; const canShare = doc.abilities.accesses_manage;
const canViewAccesses = doc.abilities.accesses_view; const canViewAccesses = doc.abilities.accesses_view;
const showMemberSection = inputValue === '' && selectedUsers.length === 0; const showMemberSection = inputValue === '' && selectedUsers.length === 0;
@@ -95,7 +95,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
count === 1 count === 1
? t('Document owner') ? t('Document owner')
: t('Share with {{count}} users', { : t('Share with {{count}} users', {
count: count - 1, count: count,
}), }),
elements: members, elements: members,
endActions: membersQuery.hasNextPage endActions: membersQuery.hasNextPage
@@ -169,10 +169,6 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
}; };
const handleRef = (node: HTMLDivElement) => { const handleRef = (node: HTMLDivElement) => {
if (!canViewAccesses) {
setListHeight('0px');
return;
}
const inputHeight = canShare ? 70 : 0; const inputHeight = canShare ? 70 : 0;
const marginTop = 11; const marginTop = 11;
const footerHeight = node?.clientHeight ?? 0; const footerHeight = node?.clientHeight ?? 0;
@@ -198,6 +194,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
aria-label={t('Share modal')} aria-label={t('Share modal')}
$height={canViewAccesses ? modalContentHeight : 'auto'} $height={canViewAccesses ? modalContentHeight : 'auto'}
$overflow="hidden" $overflow="hidden"
className="noPadding"
$justify="space-between" $justify="space-between"
> >
<Box <Box
@@ -209,7 +206,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
} }
`} `}
> >
<div ref={selectedUsersRef}> <Box ref={selectedUsersRef}>
{canShare && selectedUsers.length > 0 && ( {canShare && selectedUsers.length > 0 && (
<Box <Box
$padding={{ horizontal: 'base' }} $padding={{ horizontal: 'base' }}
@@ -227,59 +224,76 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
/> />
</Box> </Box>
)} )}
</div> {!canViewAccesses && <HorizontalSeparator />}
</Box>
<Box data-testid="doc-share-quick-search"> <Box data-testid="doc-share-quick-search">
<QuickSearch {!canViewAccesses && (
onFilter={(str) => { <Box $height={listHeight} $align="center" $justify="center">
setInputValue(str); <Text
onFilter(str); $maxWidth="320px"
}} $textAlign="center"
inputValue={inputValue} $variation="600"
showInput={canShare} $size="sm"
loading={searchUsersQuery.isLoading} >
placeholder={t('Type a name or email')} {t(
> 'You do not have permission to view users sharing this document or modify link settings.',
{canViewAccesses && (
<>
{!showMemberSection && inputValue !== '' && (
<QuickSearchGroup
group={searchUserData}
onSelect={onSelect}
renderElement={(user) => (
<DocShareModalInviteUserRow user={user} />
)}
/>
)} )}
{showMemberSection && ( </Text>
<> </Box>
{invitationsData.elements.length > 0 && ( )}
<Box aria-label={t('List invitation card')}> {canViewAccesses && (
<QuickSearch
onFilter={(str) => {
setInputValue(str);
onFilter(str);
}}
inputValue={inputValue}
showInput={canShare}
loading={searchUsersQuery.isLoading}
placeholder={t('Type a name or email')}
>
{canViewAccesses && (
<>
{!showMemberSection && inputValue !== '' && (
<QuickSearchGroup
group={searchUserData}
onSelect={onSelect}
renderElement={(user) => (
<DocShareModalInviteUserRow user={user} />
)}
/>
)}
{showMemberSection && (
<>
{invitationsData.elements.length > 0 && (
<Box aria-label={t('List invitation card')}>
<QuickSearchGroup
group={invitationsData}
renderElement={(invitation) => (
<DocShareInvitationItem
doc={doc}
invitation={invitation}
/>
)}
/>
</Box>
)}
<Box aria-label={t('List members card')}>
<QuickSearchGroup <QuickSearchGroup
group={invitationsData} group={membersData}
renderElement={(invitation) => ( renderElement={(access) => (
<DocShareInvitationItem <DocShareMemberItem doc={doc} access={access} />
doc={doc}
invitation={invitation}
/>
)} )}
/> />
</Box> </Box>
)} </>
)}
<Box aria-label={t('List members card')}> </>
<QuickSearchGroup )}
group={membersData} </QuickSearch>
renderElement={(access) => ( )}
<DocShareMemberItem doc={doc} access={access} />
)}
/>
</Box>
</>
)}
</>
)}
</QuickSearch>
</Box> </Box>
</Box> </Box>

View File

@@ -13,8 +13,6 @@ type Props = {
}; };
export const DocShareModalFooter = ({ doc, onClose }: Props) => { export const DocShareModalFooter = ({ doc, onClose }: Props) => {
const canShare = doc.abilities.accesses_manage;
const copyDocLink = useCopyDocLink(doc.id); const copyDocLink = useCopyDocLink(doc.id);
const { t } = useTranslation(); const { t } = useTranslation();
return ( return (
@@ -24,12 +22,10 @@ export const DocShareModalFooter = ({ doc, onClose }: Props) => {
`} `}
> >
<HorizontalSeparator $withPadding={true} /> <HorizontalSeparator $withPadding={true} />
{canShare && (
<> <DocVisibility doc={doc} />
<DocVisibility doc={doc} /> <HorizontalSeparator />
<HorizontalSeparator />
</>
)}
<Box <Box
$direction="row" $direction="row"
$justify="space-between" $justify="space-between"

View File

@@ -26,10 +26,10 @@ export const DocShareModalInviteUserRow = ({ user }: Props) => {
color: var(--c--theme--colors--greyscale-400); color: var(--c--theme--colors--greyscale-400);
`} `}
> >
<Text $theme="primary" $variation="600"> <Text $theme="primary" $variation="800">
{t('Add')} {t('Add')}
</Text> </Text>
<Icon $theme="primary" $variation="600" iconName="add" /> <Icon $theme="primary" $variation="800" iconName="add" />
</Box> </Box>
} }
/> />

View File

@@ -34,6 +34,7 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
const { spacingsTokens, colorsTokens } = useCunninghamTheme(); const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const spacing = spacingsTokens(); const spacing = spacingsTokens();
const colors = colorsTokens(); const colors = colorsTokens();
const canManage = doc.abilities.accesses_manage;
const [linkReach, setLinkReach] = useState<LinkReach>(doc.link_reach); const [linkReach, setLinkReach] = useState<LinkReach>(doc.link_reach);
const [docLinkRole, setDocLinkRole] = useState<LinkRole>(doc.link_role); const [docLinkRole, setDocLinkRole] = useState<LinkRole>(doc.link_role);
const { linkModeTranslations, linkReachChoices, linkReachTranslations } = const { linkModeTranslations, linkReachChoices, linkReachTranslations } =
@@ -106,29 +107,35 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
$direction="row" $direction="row"
$align={isDesktop ? 'center' : undefined} $align={isDesktop ? 'center' : undefined}
$padding={{ horizontal: '2xs' }} $padding={{ horizontal: '2xs' }}
$gap={spacing['3xs']} $gap={canManage ? spacing['3xs'] : spacing['base']}
> >
<DropdownMenu <DropdownMenu
label={t('Visibility')} label={t('Visibility')}
arrowCss={css` arrowCss={css`
color: ${colors['primary-800']} !important; color: ${colors['primary-800']} !important;
`} `}
disabled={!doc.abilities.accesses_manage}
showArrow={true} showArrow={true}
options={linkReachOptions} options={linkReachOptions}
> >
<Box $direction="row" $align="center" $gap={spacing['3xs']}> <Box $direction="row" $align="center" $gap={spacing['3xs']}>
<Icon <Icon
$theme="primary" $theme={canManage ? 'primary' : 'greyscale'}
$variation="800" $variation={canManage ? '800' : '600'}
iconName={linkReachChoices[linkReach].icon} iconName={linkReachChoices[linkReach].icon}
/> />
<Text $theme="primary" $variation="800"> <Text
$theme={canManage ? 'primary' : 'greyscale'}
$variation={canManage ? '800' : '600'}
$weight="500"
$size="md"
>
{linkReachChoices[linkReach].label} {linkReachChoices[linkReach].label}
</Text> </Text>
</Box> </Box>
</DropdownMenu> </DropdownMenu>
{isDesktop && ( {isDesktop && (
<Text $size="xs" $variation="600"> <Text $size="xs" $variation="600" $weight="400">
{description} {description}
</Text> </Text>
)} )}
@@ -137,6 +144,7 @@ export const DocVisibility = ({ doc }: DocVisibilityProps) => {
<Box $direction="row" $align="center" $gap={spacing['3xs']}> <Box $direction="row" $align="center" $gap={spacing['3xs']}>
{linkReach !== LinkReach.RESTRICTED && ( {linkReach !== LinkReach.RESTRICTED && (
<DropdownMenu <DropdownMenu
disabled={!canManage}
showArrow={true} showArrow={true}
options={linkMode} options={linkMode}
label={t('Visibility mode')} label={t('Visibility mode')}

View File

@@ -26,7 +26,7 @@ export const useTranslatedShareSettings = () => {
}, },
[LinkReach.AUTHENTICATED]: { [LinkReach.AUTHENTICATED]: {
label: linkReachTranslations[LinkReach.AUTHENTICATED], label: linkReachTranslations[LinkReach.AUTHENTICATED],
icon: 'corporate_fare', icon: 'vpn_lock',
value: LinkReach.AUTHENTICATED, value: LinkReach.AUTHENTICATED,
descriptionReadOnly: t( descriptionReadOnly: t(
'Anyone with the link can view the document if they are logged in', 'Anyone with the link can view the document if they are logged in',

View File

@@ -10,6 +10,8 @@ import {
} from '@/features/docs/doc-management'; } from '@/features/docs/doc-management';
import { useResponsiveStore } from '@/stores'; import { useResponsiveStore } from '@/stores';
import { useResponsiveDocGrid } from '../hooks/useResponsiveDocGrid';
import { DocsGridItem } from './DocsGridItem'; import { DocsGridItem } from './DocsGridItem';
import { DocsGridLoader } from './DocsGridLoader'; import { DocsGridLoader } from './DocsGridLoader';
@@ -22,6 +24,7 @@ export const DocsGrid = ({
const { t } = useTranslation(); const { t } = useTranslation();
const { isDesktop } = useResponsiveStore(); const { isDesktop } = useResponsiveStore();
const { flexLeft, flexRight } = useResponsiveDocGrid();
const { const {
data, data,
@@ -101,23 +104,21 @@ export const DocsGrid = ({
<Box <Box
$direction="row" $direction="row"
$padding={{ horizontal: 'xs' }} $padding={{ horizontal: 'xs' }}
$gap="20px" $gap="10px"
data-testid="docs-grid-header" data-testid="docs-grid-header"
> >
<Box $flex={6} $padding="3xs"> <Box $flex={flexLeft} $padding="3xs">
<Text $size="xs" $variation="600" $weight="500"> <Text $size="xs" $variation="600" $weight="500">
{t('Name')} {t('Name')}
</Text> </Text>
</Box> </Box>
{isDesktop && ( {isDesktop && (
<Box $flex={2} $padding="3xs"> <Box $flex={flexRight} $padding={{ vertical: '3xs' }}>
<Text $size="xs" $weight="500" $variation="600"> <Text $size="xs" $weight="500" $variation="600">
{t('Updated at')} {t('Updated at')}
</Text> </Text>
</Box> </Box>
)} )}
<Box $flex={1.15} $align="flex-end" $padding="3xs" />
</Box> </Box>
{/* Body */} {/* Body */}

View File

@@ -6,7 +6,6 @@ import {
Doc, Doc,
KEY_LIST_DOC, KEY_LIST_DOC,
ModalRemoveDoc, ModalRemoveDoc,
useCopyDocLink,
useCreateFavoriteDoc, useCreateFavoriteDoc,
useDeleteFavoriteDoc, useDeleteFavoriteDoc,
} from '@/features/docs/doc-management'; } from '@/features/docs/doc-management';
@@ -22,10 +21,6 @@ export const DocsGridActions = ({
}: DocsGridActionsProps) => { }: DocsGridActionsProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const copyDocLink = useCopyDocLink(doc.id);
const canViewAccesses = doc.abilities.accesses_view;
const deleteModal = useModal(); const deleteModal = useModal();
const removeFavoriteDoc = useDeleteFavoriteDoc({ const removeFavoriteDoc = useDeleteFavoriteDoc({
@@ -49,14 +44,10 @@ export const DocsGridActions = ({
testId: `docs-grid-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`, testId: `docs-grid-actions-${doc.is_favorite ? 'unpin' : 'pin'}-${doc.id}`,
}, },
{ {
label: canViewAccesses ? t('Share') : t('Copy link'), label: t('Share'),
icon: canViewAccesses ? 'group' : 'link', icon: 'group',
callback: () => { callback: () => {
if (canViewAccesses) { openShareModal?.();
openShareModal?.();
return;
}
copyDocLink();
}, },
testId: `docs-grid-actions-share-${doc.id}`, testId: `docs-grid-actions-share-${doc.id}`,

View File

@@ -1,23 +1,33 @@
import { useModal } from '@openfun/cunningham-react'; import { Tooltip, useModal } from '@openfun/cunningham-react';
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components'; import { css } from 'styled-components';
import { Box, StyledLink, Text } from '@/components'; import { Box, Icon, StyledLink, Text } from '@/components';
import { Doc } from '@/features/docs/doc-management'; import { useCunninghamTheme } from '@/cunningham';
import { Doc, LinkReach } from '@/features/docs/doc-management';
import { DocShareModal } from '@/features/docs/doc-share'; import { DocShareModal } from '@/features/docs/doc-share';
import { useResponsiveStore } from '@/stores'; import { useResponsiveStore } from '@/stores';
import { useResponsiveDocGrid } from '../hooks/useResponsiveDocGrid';
import { DocsGridActions } from './DocsGridActions'; import { DocsGridActions } from './DocsGridActions';
import { DocsGridItemSharedButton } from './DocsGridItemSharedButton'; import { DocsGridItemSharedButton } from './DocsGridItemSharedButton';
import { SimpleDocItem } from './SimpleDocItem'; import { SimpleDocItem } from './SimpleDocItem';
type DocsGridItemProps = { type DocsGridItemProps = {
doc: Doc; doc: Doc;
}; };
export const DocsGridItem = ({ doc }: DocsGridItemProps) => { export const DocsGridItem = ({ doc }: DocsGridItemProps) => {
const { t } = useTranslation();
const { isDesktop } = useResponsiveStore(); const { isDesktop } = useResponsiveStore();
const { flexLeft, flexRight } = useResponsiveDocGrid();
const { spacingsTokens } = useCunninghamTheme();
const spacings = spacingsTokens();
const shareModal = useModal(); const shareModal = useModal();
const isPublic = doc.link_reach === LinkReach.PUBLIC;
const isAuthenticated = doc.link_reach === LinkReach.AUTHENTICATED;
const showAccesses = isPublic || isAuthenticated;
const handleShareClick = () => { const handleShareClick = () => {
shareModal.open(); shareModal.open();
@@ -29,8 +39,8 @@ export const DocsGridItem = ({ doc }: DocsGridItemProps) => {
$direction="row" $direction="row"
$width="100%" $width="100%"
$align="center" $align="center"
$gap="20px"
role="row" role="row"
$gap="20px"
$padding={{ vertical: '2xs', horizontal: isDesktop ? 'base' : 'xs' }} $padding={{ vertical: '2xs', horizontal: isDesktop ? 'base' : 'xs' }}
$css={css` $css={css`
cursor: pointer; cursor: pointer;
@@ -41,39 +51,71 @@ export const DocsGridItem = ({ doc }: DocsGridItemProps) => {
`} `}
> >
<StyledLink <StyledLink
$css="flex: 8; align-items: center;" $css={css`
flex: ${flexLeft};
align-items: center;
`}
href={`/docs/${doc.id}`} href={`/docs/${doc.id}`}
> >
<Box <Box
data-testid={`docs-grid-name-${doc.id}`} data-testid={`docs-grid-name-${doc.id}`}
$flex={6} $direction="row"
$padding={{ right: 'base' }} $align="center"
$gap={spacings.xs}
$flex={flexLeft}
$padding={{ right: 'md' }}
> >
<SimpleDocItem isPinned={doc.is_favorite} doc={doc} /> <SimpleDocItem isPinned={doc.is_favorite} doc={doc} />
{showAccesses && isDesktop && (
<>
<Tooltip
content={
<Text $textAlign="center" $variation="000">
{isPublic
? t('Accessible to anyone')
: t('Accessible to authenticated users')}
</Text>
}
placement="top"
>
<div>
<Icon
$theme="greyscale"
$variation="600"
$size="14px"
iconName={isPublic ? 'public' : 'vpn_lock'}
/>
</div>
</Tooltip>
</>
)}
</Box> </Box>
{isDesktop && (
<Box $flex={2}>
<Text $variation="600" $size="xs">
{DateTime.fromISO(doc.updated_at).toRelative()}
</Text>
</Box>
)}
</StyledLink> </StyledLink>
<Box <Box
$flex={1.15} $flex={flexRight}
$direction="row" $direction="row"
$align="center" $align="center"
$justify="flex-end" $justify={isDesktop ? 'space-between' : 'flex-end'}
$gap="32px" $gap="32px"
> >
{isDesktop && ( {isDesktop && (
<DocsGridItemSharedButton <StyledLink href={`/docs/${doc.id}`}>
doc={doc} <Text $variation="600" $size="xs">
handleClick={handleShareClick} {DateTime.fromISO(doc.updated_at).toRelative()}
/> </Text>
</StyledLink>
)} )}
<DocsGridActions doc={doc} openShareModal={handleShareClick} /> <Box $direction="row" $align="center" $gap={spacings.lg}>
{isDesktop && (
<DocsGridItemSharedButton
doc={doc}
handleClick={handleShareClick}
/>
)}
<DocsGridActions doc={doc} openShareModal={handleShareClick} />
</Box>
</Box> </Box>
</Box> </Box>
{shareModal.isOpen && ( {shareModal.isOpen && (

View File

@@ -1,66 +1,45 @@
import { Button } from '@openfun/cunningham-react'; import { Button, Tooltip } from '@openfun/cunningham-react';
import { useMemo } from 'react'; import { useTranslation } from 'react-i18next';
import { Box, Icon } from '@/components'; import { Box, Icon, Text } from '@/components';
import { Doc, LinkReach } from '../../doc-management'; import { Doc } from '../../doc-management';
type Props = { type Props = {
doc: Doc; doc: Doc;
handleClick: () => void; handleClick: () => void;
}; };
export const DocsGridItemSharedButton = ({ doc, handleClick }: Props) => { export const DocsGridItemSharedButton = ({ doc, handleClick }: Props) => {
const isPublic = doc.link_reach === LinkReach.PUBLIC; const { t } = useTranslation();
const isAuthenticated = doc.link_reach === LinkReach.AUTHENTICATED; const sharedCount = doc.nb_accesses;
const isRestricted = doc.link_reach === LinkReach.RESTRICTED; const isShared = sharedCount - 1 > 0;
const sharedCount = doc.nb_accesses - 1;
const isShared = sharedCount > 0;
const icon = useMemo(() => { if (!isShared) {
if (isPublic) { return <Box $minWidth="50px">&nbsp;</Box>;
return 'public';
}
if (isAuthenticated) {
return 'corporate_fare';
}
if (isRestricted) {
return 'group';
}
return undefined;
}, [isPublic, isAuthenticated, isRestricted]);
if (!icon) {
return null;
}
if (!doc.abilities.accesses_view) {
return (
<Box $align="center" $width="100%">
<Icon $variation="800" $theme="primary" iconName={icon} />
</Box>
);
} }
return ( return (
<Button <Tooltip
onClick={(event) => { content={
event.preventDefault(); <Text $textAlign="center" $variation="000">
event.stopPropagation(); {t('Shared with {{count}} users', { count: sharedCount })}
handleClick(); </Text>
}}
fullWidth
color={isRestricted ? 'tertiary' : 'primary'}
size="nano"
icon={
<Icon
$variation={isRestricted ? '800' : '000'}
$theme={isRestricted ? 'primary' : 'greyscale'}
iconName={icon}
/>
} }
placement="top"
> >
{isShared ? sharedCount : undefined} <Button
</Button> style={{ minWidth: '50px', justifyContent: 'center' }}
onClick={(event) => {
event.preventDefault();
event.stopPropagation();
handleClick();
}}
color="tertiary"
size="nano"
icon={<Icon $variation="800" $theme="primary" iconName="group" />}
>
{sharedCount}
</Button>
</Tooltip>
); );
}; };

View File

@@ -1,9 +1,9 @@
import { DateTime } from 'luxon'; import { DateTime } from 'luxon';
import { css } from 'styled-components'; import { css } from 'styled-components';
import { Box, Icon, Text } from '@/components'; import { Box, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham'; import { useCunninghamTheme } from '@/cunningham';
import { Doc, LinkReach } from '@/features/docs/doc-management'; import { Doc } from '@/features/docs/doc-management';
import { useResponsiveStore } from '@/stores'; import { useResponsiveStore } from '@/stores';
import PinnedDocumentIcon from '../assets/pinned-document.svg'; import PinnedDocumentIcon from '../assets/pinned-document.svg';
@@ -34,11 +34,6 @@ export const SimpleDocItem = ({
const { isDesktop } = useResponsiveStore(); const { isDesktop } = useResponsiveStore();
const spacings = spacingsTokens(); const spacings = spacingsTokens();
const isPublic = doc?.link_reach === LinkReach.PUBLIC;
const isShared = !isPublic && doc.nb_accesses > 1;
const accessCount = doc.nb_accesses - 1;
const isSharedOrPublic = isShared || isPublic;
return ( return (
<Box $direction="row" $gap={spacings.sm}> <Box $direction="row" $gap={spacings.sm}>
<Box <Box
@@ -69,22 +64,6 @@ export const SimpleDocItem = ({
$gap={spacings['3xs']} $gap={spacings['3xs']}
$margin={{ top: '-2px' }} $margin={{ top: '-2px' }}
> >
{isPublic && (
<Icon iconName="public" $size="16px" $variation="600" />
)}
{isShared && (
<Icon iconName="group" $size="16px" $variation="600" />
)}
{isSharedOrPublic && accessCount > 0 && (
<Text $size="12px" $weight="bold" $variation="600">
{accessCount}
</Text>
)}
{isSharedOrPublic && (
<Text $size="12px" $variation="600">
·
</Text>
)}
<Text $variation="600" $size="xs"> <Text $variation="600" $size="xs">
{DateTime.fromISO(doc.updated_at).toRelative()} {DateTime.fromISO(doc.updated_at).toRelative()}
</Text> </Text>

View File

@@ -0,0 +1,29 @@
import { useMemo } from 'react';
import { useResponsiveStore } from '@/stores';
export const useResponsiveDocGrid = () => {
const { isDesktop, screenWidth } = useResponsiveStore();
const flexLeft = useMemo(() => {
if (!isDesktop) {
return 1;
} else if (screenWidth <= 1100) {
return 6;
} else if (screenWidth < 1200) {
return 8;
}
return 8;
}, [isDesktop, screenWidth]);
const flexRight = useMemo(() => {
if (!isDesktop) {
return undefined;
} else if (screenWidth <= 1200) {
return 5;
}
return 4;
}, [isDesktop, screenWidth]);
return { flexLeft, flexRight };
};

View File

@@ -41,3 +41,19 @@ main ::-webkit-scrollbar-thumb:hover,
cursor: pointer; cursor: pointer;
outline: inherit; outline: inherit;
} }
.c__modal__scroller:has(.noPadding) {
padding: 0 !important;
.c__modal__close .c__button {
right: 5px;
top: 5px;
padding: 1.5rem 1rem;
}
.c__modal__title {
font-size: var(--c--theme--font--sizes--xs);
padding: var(--c--theme--spacings--base);
margin-bottom: 0;
}
}