💄(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:
@@ -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}
|
||||||
|
|||||||
@@ -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)}
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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')}
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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 */}
|
||||||
|
|||||||
@@ -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}`,
|
||||||
|
|||||||
@@ -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 && (
|
||||||
|
|||||||
@@ -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"> </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>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 };
|
||||||
|
};
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user