✨(frontend) enhance dropdown components and add new LoadMoreText feature
- Updated DropButton and DropdownMenu components to include new props for accessibility and improved layout. - Introduced LoadMoreText component for better user experience in loading additional content. - Added SearchUserRow and UserAvatar components for improved user search functionality. - Cleaned up unused imports and adjusted styles for better consistency across components.
This commit is contained in:
committed by
Anthony LC
parent
eb35fdc7a9
commit
8456f47260
@@ -1,7 +1,7 @@
|
||||
import { PropsWithChildren, useState } from 'react';
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box, BoxButton, BoxProps, DropButton, Icon } from '@/components';
|
||||
import { Box, BoxButton, BoxProps, DropButton, Icon, Text } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
export type DropdownMenuOption = {
|
||||
@@ -10,6 +10,7 @@ export type DropdownMenuOption = {
|
||||
testId?: string;
|
||||
callback?: () => void | Promise<unknown>;
|
||||
danger?: boolean;
|
||||
isSelected?: boolean;
|
||||
disabled?: boolean;
|
||||
show?: boolean;
|
||||
};
|
||||
@@ -17,7 +18,9 @@ export type DropdownMenuOption = {
|
||||
export type DropdownMenuProps = {
|
||||
options: DropdownMenuOption[];
|
||||
showArrow?: boolean;
|
||||
label?: string;
|
||||
arrowCss?: BoxProps['$css'];
|
||||
topMessage?: string;
|
||||
};
|
||||
|
||||
export const DropdownMenu = ({
|
||||
@@ -25,6 +28,8 @@ export const DropdownMenu = ({
|
||||
children,
|
||||
showArrow = false,
|
||||
arrowCss,
|
||||
label,
|
||||
topMessage,
|
||||
}: PropsWithChildren<DropdownMenuProps>) => {
|
||||
const theme = useCunninghamTheme();
|
||||
const spacings = theme.spacingsTokens();
|
||||
@@ -39,9 +44,10 @@ export const DropdownMenu = ({
|
||||
<DropButton
|
||||
isOpen={isOpen}
|
||||
onOpenChange={onOpenChange}
|
||||
label={label}
|
||||
button={
|
||||
showArrow ? (
|
||||
<Box>
|
||||
<Box $direction="row" $align="center">
|
||||
<div>{children}</div>
|
||||
<Icon
|
||||
$css={
|
||||
@@ -58,15 +64,26 @@ export const DropdownMenu = ({
|
||||
)
|
||||
}
|
||||
>
|
||||
<Box>
|
||||
{options.map((option, index) => {
|
||||
<Box $maxWidth="320px">
|
||||
{topMessage && (
|
||||
<Text
|
||||
$variation="1000"
|
||||
$wrap="wrap"
|
||||
$size="xs"
|
||||
$weight="bold"
|
||||
$padding={{ vertical: 'xs', horizontal: 'base' }}
|
||||
>
|
||||
{topMessage}
|
||||
</Text>
|
||||
)}
|
||||
{options.map((option) => {
|
||||
if (option.show !== undefined && !option.show) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isDisabled = option.disabled !== undefined && option.disabled;
|
||||
return (
|
||||
<BoxButton
|
||||
aria-label={option.label}
|
||||
data-testid={option.testId}
|
||||
$direction="row"
|
||||
disabled={isDisabled}
|
||||
@@ -78,6 +95,7 @@ export const DropdownMenu = ({
|
||||
}}
|
||||
key={option.label}
|
||||
$align="center"
|
||||
$justify="space-between"
|
||||
$background={colors['greyscale-000']}
|
||||
$color={colors['primary-600']}
|
||||
$padding={{ vertical: 'xs', horizontal: 'base' }}
|
||||
@@ -86,28 +104,35 @@ export const DropdownMenu = ({
|
||||
$css={css`
|
||||
border: none;
|
||||
font-size: var(--c--theme--font--sizes--sm);
|
||||
color: var(--c--theme--colors--primary-600);
|
||||
color: var(--c--theme--colors--greyscale-1000);
|
||||
font-weight: 500;
|
||||
cursor: ${isDisabled ? 'not-allowed' : 'pointer'};
|
||||
user-select: none;
|
||||
border-bottom: ${index !== options.length - 1
|
||||
? `1px solid var(--c--theme--colors--greyscale-200)`
|
||||
: 'none'};
|
||||
|
||||
&:hover {
|
||||
background-color: var(--c--theme--colors--greyscale-050);
|
||||
}
|
||||
`}
|
||||
>
|
||||
{option.icon && (
|
||||
<Icon
|
||||
$size="20px"
|
||||
$theme={!isDisabled ? 'primary' : 'greyscale'}
|
||||
$variation={!isDisabled ? '600' : '400'}
|
||||
iconName={option.icon}
|
||||
/>
|
||||
<Box $direction="row" $align="center" $gap={spacings['base']}>
|
||||
{option.icon && (
|
||||
<Icon
|
||||
$size="20px"
|
||||
$theme="greyscale"
|
||||
$variation={isDisabled ? '400' : '1000'}
|
||||
iconName={option.icon}
|
||||
/>
|
||||
)}
|
||||
<Text
|
||||
$margin={{ top: '-3px' }}
|
||||
$variation={isDisabled ? '400' : '1000'}
|
||||
>
|
||||
{option.label}
|
||||
</Text>
|
||||
</Box>
|
||||
{option.isSelected && (
|
||||
<Icon iconName="check" $size="20px" $theme="greyscale" />
|
||||
)}
|
||||
{option.label}
|
||||
</BoxButton>
|
||||
);
|
||||
})}
|
||||
|
||||
35
src/frontend/apps/impress/src/components/LoadMoreText.tsx
Normal file
35
src/frontend/apps/impress/src/components/LoadMoreText.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box } from './Box';
|
||||
import { Icon } from './Icon';
|
||||
import { Text } from './Text';
|
||||
|
||||
type LoadMoreTextProps = {
|
||||
['data-testid']?: string;
|
||||
};
|
||||
|
||||
export const LoadMoreText = ({
|
||||
'data-testid': dataTestId,
|
||||
}: LoadMoreTextProps) => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return (
|
||||
<Box
|
||||
data-testid={dataTestId}
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$gap="0.4rem"
|
||||
$padding={{ horizontal: '2xs', vertical: 'sm' }}
|
||||
>
|
||||
<Icon
|
||||
$theme="primary"
|
||||
$variation="800"
|
||||
iconName="arrow_downward"
|
||||
$size="md"
|
||||
/>
|
||||
<Text $theme="primary" $variation="800">
|
||||
{t('Load more')}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -22,7 +22,8 @@ import { BlockNoteToolbar } from './BlockNoteToolbar';
|
||||
|
||||
const cssEditor = (readonly: boolean) => `
|
||||
&, & > .bn-container, & .ProseMirror {
|
||||
height:100%
|
||||
height:100%;
|
||||
|
||||
};
|
||||
|
||||
& .bn-inline-content code {
|
||||
@@ -32,8 +33,7 @@ const cssEditor = (readonly: boolean) => `
|
||||
}
|
||||
@media screen and (width <= 560px) {
|
||||
& .bn-editor {
|
||||
padding-left: 40px;
|
||||
padding-right: 10px;
|
||||
|
||||
${readonly && `padding-left: 10px;`}
|
||||
};
|
||||
.bn-side-menu[data-block-type=heading][data-level="1"] {
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { Alert, Loader, VariantType } from '@openfun/cunningham-react';
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { css } from 'styled-components';
|
||||
import * as Y from 'yjs';
|
||||
|
||||
@@ -25,7 +24,6 @@ interface DocEditorProps {
|
||||
}
|
||||
|
||||
export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { isDesktop } = useResponsiveStore();
|
||||
|
||||
const isVersion = !!versionId && typeof versionId === 'string';
|
||||
@@ -60,14 +58,6 @@ export const DocEditor = ({ doc, versionId }: DocEditorProps) => {
|
||||
)}
|
||||
</Box>
|
||||
|
||||
{!doc.abilities.partial_update && (
|
||||
<Box $width="100%" $margin={{ all: 'small', top: 'none' }}>
|
||||
<Alert type={VariantType.WARNING}>
|
||||
{t(`Read only, you cannot edit this document.`)}
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
<Box
|
||||
$background={colorsTokens()['primary-bg']}
|
||||
$direction="row"
|
||||
|
||||
@@ -92,7 +92,7 @@ export const DocHeader = ({ doc }: DocHeaderProps) => {
|
||||
<DocToolBox doc={doc} />
|
||||
</Box>
|
||||
</Box>
|
||||
<HorizontalSeparator $withPadding={false} />
|
||||
<HorizontalSeparator />
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -21,14 +21,12 @@ import {
|
||||
useEditorStore,
|
||||
usePanelEditorStore,
|
||||
} from '@/features/docs/doc-editor/';
|
||||
import {
|
||||
Doc,
|
||||
ModalRemoveDoc,
|
||||
ModalShare,
|
||||
} from '@/features/docs/doc-management';
|
||||
import { Doc, ModalRemoveDoc } from '@/features/docs/doc-management';
|
||||
import { ModalSelectVersion } from '@/features/docs/doc-versioning';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { DocShareModal } from '../../doc-share/component/DocShareModal';
|
||||
|
||||
import { ModalPDF } from './ModalExport';
|
||||
|
||||
interface DocToolBoxProps {
|
||||
@@ -42,10 +40,10 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
const spacings = spacingsTokens();
|
||||
const colors = colorsTokens();
|
||||
|
||||
const [isModalShareOpen, setIsModalShareOpen] = useState(false);
|
||||
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
|
||||
const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
|
||||
const selectHistoryModal = useModal();
|
||||
const modalShare = useModal();
|
||||
const { setIsPanelOpen, setIsPanelTableContentOpen } = usePanelEditorStore();
|
||||
|
||||
const { isSmallMobile, isDesktop } = useResponsiveStore();
|
||||
@@ -60,7 +58,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
label: t('Share'),
|
||||
icon: 'upload',
|
||||
callback: () => {
|
||||
setIsModalShareOpen(true);
|
||||
modalShare.open();
|
||||
},
|
||||
},
|
||||
{
|
||||
@@ -72,6 +70,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
{
|
||||
label: t('Version history'),
|
||||
icon: 'history',
|
||||
@@ -149,7 +148,7 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
<Button
|
||||
color="primary-text"
|
||||
onClick={() => {
|
||||
setIsModalShareOpen(true);
|
||||
modalShare.open();
|
||||
}}
|
||||
size={isSmallMobile ? 'small' : 'medium'}
|
||||
>
|
||||
@@ -185,8 +184,9 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
|
||||
/>
|
||||
</DropdownMenu>
|
||||
</Box>
|
||||
{isModalShareOpen && (
|
||||
<ModalShare onClose={() => setIsModalShareOpen(false)} doc={doc} />
|
||||
|
||||
{modalShare.isOpen && (
|
||||
<DocShareModal onClose={() => modalShare.close()} doc={doc} />
|
||||
)}
|
||||
{isModalPDFOpen && (
|
||||
<ModalPDF onClose={() => setIsModalPDFOpen(false)} doc={doc} />
|
||||
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
} from '@/components';
|
||||
import { User } from '@/core';
|
||||
import { Doc, Role } from '@/features/docs/doc-management';
|
||||
import { SearchUserRow } from '@/features/docs/doc-share/component/SearchUserRow';
|
||||
import {
|
||||
useDeleteDocInvitation,
|
||||
useUpdateDocInvitation,
|
||||
} from '@/features/docs/members/invitation-list';
|
||||
import { Invitation } from '@/features/docs/members/invitation-list/types';
|
||||
import { SearchUserRow } from '@/features/users/components/SearchUserRow';
|
||||
|
||||
import { DocRoleDropdown } from './DocRoleDropdown';
|
||||
|
||||
|
||||
@@ -7,12 +7,12 @@ import {
|
||||
DropdownMenuOption,
|
||||
IconOptions,
|
||||
} from '@/components';
|
||||
import { SearchUserRow } from '@/features/docs/doc-share/component/SearchUserRow';
|
||||
import {
|
||||
useDeleteDocAccess,
|
||||
useUpdateDocAccess,
|
||||
} from '@/features/docs/members/members-list';
|
||||
import { useWhoAmI } from '@/features/docs/members/members-list/hooks/useWhoAmI';
|
||||
import { SearchUserRow } from '@/features/users/components/SearchUserRow';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { Access, Doc, Role } from '../../doc-management/types';
|
||||
|
||||
@@ -3,7 +3,7 @@ import { css } from 'styled-components';
|
||||
|
||||
import { Box, Icon, Text } from '@/components';
|
||||
import { User } from '@/core';
|
||||
import { SearchUserRow } from '@/features/users/components/SearchUserRow';
|
||||
import { SearchUserRow } from '@/features/docs/doc-share/component/SearchUserRow';
|
||||
|
||||
type Props = {
|
||||
user: User;
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { Box, Text } from '@/components';
|
||||
import {
|
||||
QuickSearchItemContent,
|
||||
QuickSearchItemContentProps,
|
||||
} from '@/components/quick-search/QuickSearchItemContent';
|
||||
import { User } from '@/core';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { UserAvatar } from './UserAvatar';
|
||||
|
||||
type Props = {
|
||||
user: User;
|
||||
alwaysShowRight?: boolean;
|
||||
right?: QuickSearchItemContentProps['right'];
|
||||
};
|
||||
|
||||
export const SearchUserRow = ({
|
||||
user,
|
||||
right,
|
||||
alwaysShowRight = false,
|
||||
}: Props) => {
|
||||
const hasFullName = user.full_name != null && user.full_name !== '';
|
||||
const { spacingsTokens } = useCunninghamTheme();
|
||||
const spacings = spacingsTokens();
|
||||
|
||||
return (
|
||||
<QuickSearchItemContent
|
||||
right={right}
|
||||
alwaysShowRight={alwaysShowRight}
|
||||
left={
|
||||
<Box $direction="row" $align="center" $gap={spacings['2xs']}>
|
||||
<UserAvatar user={user} />
|
||||
<Box $direction="column">
|
||||
<Text $size="sm" $weight="500" $variation="1000">
|
||||
{hasFullName ? user.full_name : user.email}
|
||||
</Text>
|
||||
{hasFullName && (
|
||||
<Text $size="xs" $variation="600">
|
||||
{user.email}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { css } from 'styled-components';
|
||||
|
||||
import { Box } from '@/components';
|
||||
import { User } from '@/core';
|
||||
import { tokens } from '@/cunningham';
|
||||
|
||||
const colors = tokens.themes.default.theme.colors;
|
||||
|
||||
const avatarsColors = [
|
||||
colors['blue-500'],
|
||||
colors['brown-500'],
|
||||
colors['cyan-500'],
|
||||
colors['gold-500'],
|
||||
colors['green-500'],
|
||||
colors['olive-500'],
|
||||
colors['orange-500'],
|
||||
colors['pink-500'],
|
||||
colors['purple-500'],
|
||||
colors['yellow-500'],
|
||||
];
|
||||
|
||||
const getColorFromName = (name: string) => {
|
||||
let hash = 0;
|
||||
for (let i = 0; i < name.length; i++) {
|
||||
hash = name.charCodeAt(i) + ((hash << 5) - hash);
|
||||
}
|
||||
return avatarsColors[Math.abs(hash) % avatarsColors.length];
|
||||
};
|
||||
|
||||
type Props = {
|
||||
user: User;
|
||||
};
|
||||
|
||||
export const UserAvatar = ({ user }: Props) => {
|
||||
const name = user.full_name || user.email || '?';
|
||||
const splitName = name?.split(' ');
|
||||
|
||||
return (
|
||||
<Box
|
||||
$background={getColorFromName(name)}
|
||||
$width="24px"
|
||||
$height="24px"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$justify="center"
|
||||
$radius="50%"
|
||||
$css={css`
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
border: 1px solid rgba(255, 255, 255, 0.5);
|
||||
`}
|
||||
>
|
||||
<Box
|
||||
$direction="row"
|
||||
$css={css`
|
||||
text-align: center;
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
font-family: Arial, Helvetica, sans-serif; // Can't use marianne font because it's impossible to center with this font
|
||||
font-size: 10px;
|
||||
text-transform: uppercase;
|
||||
`}
|
||||
>
|
||||
{splitName[0]?.charAt(0)}
|
||||
{splitName?.[1]?.charAt(0)}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user