This commit is contained in:
Nathan Panchout
2024-11-27 13:18:49 +01:00
parent bc8fd309ed
commit c928a08fc5
27 changed files with 1755 additions and 444 deletions

View File

@@ -12,6 +12,36 @@ const config = {
'info-150': '#E5EEFA',
'greyscale-000': '#fff',
'greyscale-1000': '#161616',
'blue-400': '#7AB1E8',
'blue-500': '#417DC4',
'blue-600': '#3558A2',
'brown-400': '#E6BE92',
'brown-500': '#BD987A',
'brown-600': '#745B47',
'cyan-400': '#34BAB5',
'cyan-500': '#009099',
'cyan-600': '#006A6F',
'gold-400': '#FFCA00',
'gold-500': '#C3992A',
'gold-600': '#695240',
'green-400': '#34CB6A',
'green-500': '#00A95F',
'green-600': '#297254',
'olive-400': '#99C221',
'olive-500': '#68A532',
'olive-600': '#447049',
'orange-400': '#FF732C',
'orange-500': '#E4794A',
'orange-600': '#755348',
'pink-400': '#FFB7AE',
'pink-500': '#E18B76',
'pink-600': '#8D533E',
'purple-400': '#CE70CC',
'purple-500': '#A558A0',
'purple-600': '#6E445A',
'yellow-400': '#D8C634',
'yellow-500': '#B7A73F',
'yellow-600': '#66673D',
},
font: {
sizes: {

View File

@@ -23,6 +23,7 @@
"@openfun/cunningham-react": "2.9.4",
"@sentry/nextjs": "8.40.0",
"@tanstack/react-query": "5.61.3",
"cmdk": "^1.0.4",
"crisp-sdk-web": "1.0.25",
"i18next": "24.0.0",
"i18next-browser-languagedetector": "8.0.0",
@@ -37,6 +38,7 @@
"react-intersection-observer": "9.13.1",
"react-select": "5.8.3",
"styled-components": "6.1.13",
"use-debounce": "10.0.4",
"y-protocols": "1.0.6",
"yjs": "*",
"zustand": "5.0.1"

View File

@@ -40,7 +40,7 @@ export const DropdownMenu = ({
onOpenChange={onOpenChange}
button={
showArrow ? (
<Box>
<Box $direction="row" $align="center">
<div>{children}</div>
<Icon
$css={

View File

@@ -0,0 +1,28 @@
import { useTranslation } from 'react-i18next';
import { Box } from './Box';
import { Icon } from './Icon';
import { Text } from './Text';
export const LoadMoreText = () => {
const { t } = useTranslation();
return (
<Box
$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>
);
};

View File

@@ -0,0 +1,48 @@
import { css } from 'styled-components';
import { Box } from './Box';
type Props = {
size?: 'small' | 'medium' | 'large';
};
export const SimpleLoader = ({ size = 'medium' }: Props) => {
return (
<Box
className={size}
$css={css`
display: inline-block;
border: 3px solid var(--c--theme--colors--primary-300);
border-radius: 50%;
border-top-color: var(--c--theme--colors--primary-600);
animation: spin 1s ease-in-out infinite;
-webkit-animation: spin 1s ease-in-out infinite;
&.small {
width: 24px;
height: 24px;
}
&.medium {
width: 38px;
height: 38px;
}
&.large {
width: 50px;
height: 50px;
}
@keyframes spin {
to {
-webkit-transform: rotate(360deg);
}
}
@-webkit-keyframes spin {
to {
-webkit-transform: rotate(360deg);
}
}
`}
/>
);
};

View File

@@ -0,0 +1,88 @@
import { Command } from 'cmdk';
import { ReactNode, useRef } from 'react';
import { Box } from '../Box';
import { QuickSearchGroup } from './QuickSearchGroup';
import { QuickSearchInput } from './QuickSearchInput';
import { QuickSearchStyle } from './QuickSearchStyle';
export type QuickSearchAction = {
onSelect?: () => void;
content: ReactNode;
};
export type QuickSearchData<T> = {
groupName: string;
elements: T[];
emptyString?: string;
startActions?: QuickSearchAction[];
endActions?: QuickSearchAction[];
showWhenEmpty?: boolean;
};
export type QuickSearchProps<T> = {
data: QuickSearchData<T>[];
onFilter?: (str: string) => void;
renderElement: (element: T) => ReactNode;
onSelect?: (element: T) => void;
inputValue?: string;
inputContent?: ReactNode;
showInput?: boolean;
loading?: boolean;
label?: string;
placeholder?: string;
children?: ReactNode;
};
export const QuickSearch = <T,>({
onSelect,
onFilter,
inputContent,
inputValue,
loading,
showInput = true,
data,
renderElement,
label,
placeholder,
children,
}: QuickSearchProps<T>) => {
const ref = useRef<HTMLDivElement | null>(null);
return (
<>
<QuickSearchStyle />
<div className="quick-search-container">
<Command label={label} shouldFilter={false} ref={ref}>
{showInput && (
<QuickSearchInput
loading={loading}
inputValue={inputValue}
onFilter={onFilter}
placeholder={placeholder}
>
{inputContent}
</QuickSearchInput>
)}
<Command.List>
<Box>
{!loading &&
data.map((group) => {
return (
<QuickSearchGroup
key={group.groupName}
group={group}
onSelect={onSelect}
renderElement={renderElement}
/>
);
})}
{children}
</Box>
</Command.List>
</Command>
</div>
</>
);
};

View File

@@ -0,0 +1,61 @@
import { Command } from 'cmdk';
import { QuickSearchData, QuickSearchProps } from './QuickSearch';
import { QuickSearchItem } from './QuickSearchItem';
type Props<T> = {
group: QuickSearchData<T>;
onSelect?: QuickSearchProps<T>['onSelect'];
renderElement: QuickSearchProps<T>['renderElement'];
};
export const QuickSearchGroup = <T,>({
group,
onSelect,
renderElement,
}: Props<T>) => {
return (
<Command.Group
key={group.groupName}
heading={group.groupName}
forceMount={false}
>
{group.startActions?.map((action, index) => {
return (
<QuickSearchItem
key={`${group.groupName}-action-${index}`}
onSelect={action.onSelect}
>
{action.content}
</QuickSearchItem>
);
})}
{group.elements.map((groupElement, index) => {
return (
<QuickSearchItem
key={`${group.groupName}-element-${index}`}
onSelect={() => {
console.log('onSelect', groupElement);
onSelect?.(groupElement);
}}
>
{renderElement(groupElement)}
</QuickSearchItem>
);
})}
{group.endActions?.map((action, index) => {
return (
<QuickSearchItem
key={`${group.groupName}-action-${index}`}
onSelect={action.onSelect}
>
{action.content}
</QuickSearchItem>
);
})}
{group.emptyString && group.elements.length === 0 && (
<span className="ml-b clr-greyscale-500">{group.emptyString}</span>
)}
</Command.Group>
);
};

View File

@@ -0,0 +1,64 @@
import { Loader } from '@openfun/cunningham-react';
import { Command } from 'cmdk';
import { ReactNode } from 'react';
import { useTranslation } from 'react-i18next';
import { HorizontalSeparator } from '@/components/separators/HorizontalSeparator';
import { useCunninghamTheme } from '@/cunningham';
import { Box } from '../Box';
import { Icon } from '../Icon';
type Props = {
loading?: boolean;
inputValue?: string;
onFilter?: (str: string) => void;
placeholder?: string;
children?: ReactNode;
};
export const QuickSearchInput = ({
loading,
inputValue,
onFilter,
placeholder,
children,
}: Props) => {
const { t } = useTranslation();
const { spacingsTokens } = useCunninghamTheme();
const spacing = spacingsTokens();
if (children) {
return (
<>
{children}
<HorizontalSeparator />
</>
);
}
return (
<>
<Box
$direction="row"
$align="center"
$gap={spacing['2xs']}
$padding={{ horizontal: 'base' }}
>
{!loading && <Icon iconName="search" $variation="400" />}
{loading && (
<div>
<Loader size="small" />
</div>
)}
<Command.Input
/* eslint-disable-next-line jsx-a11y/no-autofocus */
autoFocus={true}
value={inputValue}
placeholder={placeholder ?? t('Search')}
onValueChange={onFilter}
/>
</Box>
<HorizontalSeparator />
</>
);
};

View File

@@ -0,0 +1,12 @@
import { Command } from 'cmdk';
import { PropsWithChildren } from 'react';
type Props = {
onSelect?: (value: string) => void;
};
export const QuickSearchItem = ({
children,
onSelect,
}: PropsWithChildren<Props>) => {
return <Command.Item onSelect={onSelect}>{children}</Command.Item>;
};

View File

@@ -0,0 +1,177 @@
import { createGlobalStyle } from 'styled-components';
export const QuickSearchStyle = createGlobalStyle`
.quick-search-container {
[cmdk-root] {
width: 100%;
background: #ffffff;
border-radius: 12px;
overflow: hidden;
transition: transform 100ms ease;
outline: none;
.dark & {
background: rgba(22, 22, 22, 0.7);
}
}
[cmdk-input] {
border: none;
width: 100%;
font-size: 17px;
padding: 8px;
background: white;
outline: none;
color: var(--c--theme--colors--greyscale-700);
border-radius: 0;
&::placeholder {
color: var(--c--theme--colors--greyscale-300);
}
}
[cmdk-vercel-badge] {
height: 20px;
background: var(--c--theme--colors--greyscale-700);
display: inline-flex;
align-items: center;
padding: 0 8px;
font-size: 12px;
color: var(--c--theme--colors--greyscale-500);
border-radius: 4px;
margin: 4px 0 4px 4px;
user-select: none;
text-transform: capitalize;
font-weight: 500;
}
[cmdk-item] {
content-visibility: auto;
cursor: pointer;
border-radius: var(--c--theme--spacings--xs);
font-size: 14px;
display: flex;
align-items: center;
gap: 8px;
/* padding: var(--c--theme--spacings--2xs) ; */
user-select: none;
will-change: background, color;
transition: all 150ms ease;
transition-property: none;
&[data-selected='true'] {
background: var(--c--theme--colors--greyscale-100);
}
&[data-disabled='true'] {
color: var(--c--theme--colors--greyscale-500);
cursor: not-allowed;
}
& + [cmdk-item] {
margin-top: 4px;
}
}
[cmdk-list] {
height: 500px;
padding: 0 var(--c--theme--spacings--sm) var(--c--theme--spacings--sm)
var(--c--theme--spacings--sm);
max-height: 700px;
overflow-y: auto;
overscroll-behavior: contain;
transition: 100ms ease;
transition-property: height;
}
[cmdk-vercel-shortcuts] {
display: flex;
margin-left: auto;
gap: 8px;
kbd {
font-size: 12px;
min-width: 20px;
padding: 4px;
height: 20px;
border-radius: 4px;
color: white;
background: var(--c--theme--colors--greyscale-500);
display: inline-flex;
align-items: center;
justify-content: center;
text-transform: uppercase;
}
}
[cmdk-separator] {
height: 1px;
width: 100%;
background: var(--c--theme--colors--greyscale-500);
margin: 4px 0;
}
*:not([hidden]) + [cmdk-group] {
margin-top: 8px;
}
[cmdk-group-heading] {
user-select: none;
font-size: var(--c--theme--font--sizes--sm);
color: var(--c--theme--colors--greyscale-500);
font-weight: bold;
display: flex;
align-items: center;
margin: var(--c--theme--spacings--200W) 0;
}
[cmdk-empty] {
}
}
.inputContainer {
display: flex;
padding: 0 10px;
gap: 8px;
align-items: center;
.inputIcon {
color: var(--c--theme--colors--greyscale-400);
}
}
.loading {
display: flex;
justify-content: center;
align-items: center;
}
.c__modal__scroller:has(.quick-search-container),
.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--200W);
margin-bottom: 0;
}
}
`;

View File

@@ -80,6 +80,36 @@ export const tokens = {
'primary-950': '#1B1B35',
'info-150': '#E5EEFA',
'greyscale-1000': '#161616',
'blue-400': '#7AB1E8',
'blue-500': '#417DC4',
'blue-600': '#3558A2',
'brown-400': '#E6BE92',
'brown-500': '#BD987A',
'brown-600': '#745B47',
'cyan-400': '#34BAB5',
'cyan-500': '#009099',
'cyan-600': '#006A6F',
'gold-400': '#FFCA00',
'gold-500': '#C3992A',
'gold-600': '#695240',
'green-400': '#34CB6A',
'green-500': '#00A95F',
'green-600': '#297254',
'olive-400': '#99C221',
'olive-500': '#68A532',
'olive-600': '#447049',
'orange-400': '#FF732C',
'orange-500': '#E4794A',
'orange-600': '#755348',
'pink-400': '#FFB7AE',
'pink-500': '#E18B76',
'pink-600': '#8D533E',
'purple-400': '#CE70CC',
'purple-500': '#A558A0',
'purple-600': '#6E445A',
'yellow-400': '#D8C634',
'yellow-500': '#B7A73F',
'yellow-600': '#66673D',
},
font: {
sizes: {

View File

@@ -1,6 +1,7 @@
import {
Button,
VariantType,
useModal,
useToastProvider,
} from '@openfun/cunningham-react';
import { useState } from 'react';
@@ -28,6 +29,8 @@ import {
import { ModalVersion, Versions } from '@/features/docs/doc-versioning';
import { useResponsiveStore } from '@/stores';
import { DocShareModal } from '../../doc-management/components/share/DocShareModal';
import { ModalPDF } from './ModalExport';
interface DocToolBoxProps {
@@ -46,6 +49,8 @@ export const DocToolBox = ({ doc, versionId }: DocToolBoxProps) => {
const [isModalRemoveOpen, setIsModalRemoveOpen] = useState(false);
const [isModalPDFOpen, setIsModalPDFOpen] = useState(false);
const modalShare = useModal();
const { setIsPanelOpen, setIsPanelTableContentOpen } = usePanelEditorStore();
const [isModalVersionOpen, setIsModalVersionOpen] = useState(false);
const { isSmallMobile } = useResponsiveStore();
@@ -162,7 +167,8 @@ export const DocToolBox = ({ doc, versionId }: DocToolBoxProps) => {
<Button
color="primary-text"
onClick={() => {
setIsModalShareOpen(true);
modalShare.open();
// setIsModalShareOpen(true);
}}
size={isSmallMobile ? 'small' : 'medium'}
>
@@ -201,6 +207,9 @@ export const DocToolBox = ({ doc, versionId }: DocToolBoxProps) => {
{isModalShareOpen && (
<ModalShare onClose={() => setIsModalShareOpen(false)} doc={doc} />
)}
{modalShare.isOpen && (
<DocShareModal doc={doc} onClose={modalShare.onClose} />
)}
{isModalPDFOpen && (
<ModalPDF onClose={() => setIsModalPDFOpen(false)} doc={doc} />
)}

View File

@@ -0,0 +1,54 @@
import { css } from 'styled-components';
import { DropdownMenu, DropdownMenuOption, Text } from '@/components';
import { useTrans } from '../hooks';
import { Role } from '../types';
type Props = {
currentRole: Role;
onSelectRole?: (role: Role) => void;
canUpdate?: boolean;
};
export const DocRoleDropdown = ({
canUpdate = true,
currentRole,
onSelectRole,
}: Props) => {
const { transRole, translatedRoles } = useTrans();
const roles: DropdownMenuOption[] = Object.keys(translatedRoles).map(
(key) => {
return {
label: transRole(key as Role),
callback: () => onSelectRole?.(key as Role),
};
},
);
if (!canUpdate) {
return (
<Text
$variation="600"
$css={css`
font-family: Arial, Helvetica, sans-serif;
`}
>
{transRole(currentRole)}
</Text>
);
}
return (
<DropdownMenu showArrow={true} options={roles}>
<Text
$variation="600"
$css={css`
font-family: Arial, Helvetica, sans-serif;
`}
>
{transRole(currentRole)}
</Text>
</DropdownMenu>
);
};

View File

@@ -0,0 +1,160 @@
import {
Button,
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
import { useState } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { APIError } from '@/api';
import { Box } from '@/components';
import { User } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { Doc, Role } from '@/features/docs';
import { useCreateDocInvitation } from '@/features/docs/members/invitation-list';
import { useCreateDocAccess } from '@/features/docs/members/members-add';
import { OptionType } from '@/features/docs/members/members-add/types';
import { useLanguage } from '@/i18n/hooks/useLanguage';
import { DocRoleDropdown } from '../DocRoleDropdown';
import { DocShareAddMemberListItem } from './DocShareAddMemberListItem';
type APIErrorUser = APIError<{
value: string;
type: OptionType;
}>;
type Props = {
doc: Doc;
selectedUsers: User[];
onRemoveUser?: (user: User) => void;
onSubmit?: (selectedUsers: User[], role: Role) => void;
afterInvite?: () => void;
};
export const DocShareAddMemberList = ({
doc,
selectedUsers,
onRemoveUser,
afterInvite,
}: Props) => {
const { t } = useTranslation();
const { toast } = useToastProvider();
const [isLoading, setIsLoading] = useState(false);
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
const { contentLanguage } = useLanguage();
const [invitationRole, setInvitationRole] = useState<Role>(Role.EDITOR);
const canShare = doc.abilities.accesses_manage;
const spacing = spacingsTokens();
const color = colorsTokens();
const { mutateAsync: createInvitation } = useCreateDocInvitation();
const { mutateAsync: createDocAccess } = useCreateDocAccess();
const onError = (dataError: APIErrorUser) => {
let messageError =
dataError['data']?.type === OptionType.INVITATION
? t(`Failed to create the invitation for {{email}}.`, {
email: dataError['data']?.value,
})
: t(`Failed to add the member in the document.`);
if (
dataError.cause?.[0] ===
'Document invitation with this Email address and Document already exists.'
) {
messageError = t('"{{email}}" is already invited to the document.', {
email: dataError['data']?.value,
});
}
if (
dataError.cause?.[0] ===
'This email is already associated to a registered user.'
) {
messageError = t('"{{email}}" is already member of the document.', {
email: dataError['data']?.value,
});
}
toast(messageError, VariantType.ERROR, {
duration: 4000,
});
};
const onInvite = async () => {
setIsLoading(true);
const promises = selectedUsers.map((user) => {
const isInvitationMode = user.id === user.email;
const payload = {
role: invitationRole,
docId: doc.id,
contentLanguage,
};
return isInvitationMode
? createInvitation({
...payload,
email: user.email,
})
: createDocAccess({
...payload,
memberId: user.id,
});
});
const settledPromises = await Promise.allSettled(promises);
settledPromises.forEach((settledPromise) => {
if (settledPromise.status === 'rejected') {
onError(settledPromise.reason as APIErrorUser);
}
});
afterInvite?.();
setIsLoading(false);
};
return (
<Box
$direction="row"
$padding={spacing.sm}
$align="center"
$background={color['greyscale-050']}
$radius={spacing['3xs']}
$css={css`
border: 1px solid ${color['greyscale-200']};
`}
>
<Box
$direction="row"
$align="center"
$wrap="wrap"
$flex={1}
$gap={spacing.xs}
>
{selectedUsers.map((user) => (
<DocShareAddMemberListItem
key={user.id}
user={user}
onRemoveUser={onRemoveUser}
/>
))}
</Box>
<Box $direction="row" $align="center">
<DocRoleDropdown
canUpdate={canShare}
currentRole={invitationRole}
onSelectRole={setInvitationRole}
/>
<Button
onClick={() => void onInvite()}
size="small"
disabled={isLoading}
>
{t('Invite')}
</Button>
</Box>
</Box>
);
};

View File

@@ -0,0 +1,42 @@
import { Button } from '@openfun/cunningham-react';
import { css } from 'styled-components';
import { Box, Icon, Text } from '@/components';
import { User } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
type Props = {
user: User;
onRemoveUser?: (user: User) => void;
};
export const DocShareAddMemberListItem = ({ user, onRemoveUser }: Props) => {
const { spacingsTokens, colorsTokens, fontSizesTokens } =
useCunninghamTheme();
const spacing = spacingsTokens();
const color = colorsTokens();
const fontSize = fontSizesTokens();
return (
<Box
$radius={spacing['3xs']}
$direction="row"
$height="fit-content"
$justify="center"
$align="center"
$gap={spacing.xs}
$background={color['greyscale-200']}
$padding={{ horizontal: spacing['2xs'], vertical: spacing['3xs'] }}
$css={css`
color: ${color['greyscale-1000']};
font-size: ${fontSize['xs']};
`}
>
<Text $margin={{ top: '-3px' }}>{user.full_name || user.email}</Text>
<Button
color="primary-text"
size="nano"
onClick={() => onRemoveUser(user)}
icon={<Icon $variation="400" $size="sm" iconName="close" />}
/>
</Box>
);
};

View File

@@ -0,0 +1,99 @@
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import {
Box,
DropdownMenu,
DropdownMenuOption,
IconOptions,
} from '@/components';
import { User } from '@/core';
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 { Doc, Role } from '../../types';
import { DocRoleDropdown } from '../DocRoleDropdown';
type Props = {
doc: Doc;
invitation: Invitation;
};
export const DocShareInvitationItem = ({ doc, invitation }: Props) => {
const { t } = useTranslation();
const fakeUser: User = {
id: invitation.email,
full_name: invitation.email,
email: invitation.email,
short_name: invitation.email,
};
const { toast } = useToastProvider();
const canUpdate = invitation.abilities.partial_update;
const { mutate: updateDocInvitation } = useUpdateDocInvitation({
onError: (error) => {
toast(
error?.data?.role?.[0] ?? t('Error during update invitation'),
VariantType.ERROR,
{
duration: 4000,
},
);
},
});
const { mutate: removeDocInvitation } = useDeleteDocInvitation({
onError: (error) => {
toast(
error?.data?.role?.[0] ?? t('Error during delete invitation'),
VariantType.ERROR,
{
duration: 4000,
},
);
},
});
const onUpdate = (newRole: Role) => {
updateDocInvitation({
docId: doc.id,
role: newRole,
invitationId: invitation.id,
});
};
const onRemove = () => {
removeDocInvitation({ invitationId: invitation.id, docId: doc.id });
};
const moreActions: DropdownMenuOption[] = [
{
label: t('Delete'),
icon: 'delete',
callback: onRemove,
disabled: !canUpdate,
},
];
return (
<SearchUserRow
showRightOnHover={false}
user={fakeUser}
right={
<Box $direction="row" $align="center">
<DocRoleDropdown
currentRole={invitation.role}
onSelectRole={onUpdate}
canUpdate={canUpdate}
/>
<DropdownMenu options={moreActions}>
<IconOptions $variation="600" />
</DropdownMenu>
</Box>
}
/>
);
};

View File

@@ -0,0 +1,86 @@
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
import { useTranslation } from 'react-i18next';
import {
Box,
DropdownMenu,
DropdownMenuOption,
IconOptions,
} from '@/components';
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 { Access, Doc, Role } from '../../types';
import { DocRoleDropdown } from '../DocRoleDropdown';
type Props = {
doc: Doc;
access: Access;
};
export const DocShareMemberItem = ({ doc, access }: Props) => {
const { t } = useTranslation();
const { isLastOwner, isOtherOwner } = useWhoAmI(access);
const { toast } = useToastProvider();
const isNotAllowed =
isOtherOwner || isLastOwner || !doc.abilities.accesses_manage;
const { mutate: updateDocAccess } = useUpdateDocAccess({
onError: () => {
toast(t('Error during invitation update'), VariantType.ERROR, {
duration: 4000,
});
},
});
const { mutate: removeDocAccess } = useDeleteDocAccess({
onError: () => {
toast(t('Error while deleting invitation'), VariantType.ERROR, {
duration: 4000,
});
},
});
const onUpdate = (newRole: Role) => {
updateDocAccess({
docId: doc.id,
role: newRole,
accessId: access.id,
});
};
const onRemove = () => {
removeDocAccess({ accessId: access.id, docId: doc.id });
};
const moreActions: DropdownMenuOption[] = [
{
label: t('Delete'),
icon: 'delete',
callback: onRemove,
disabled: isNotAllowed,
},
];
return (
<SearchUserRow
showRightOnHover={false}
user={access.user}
right={
<Box $direction="row" $align="center">
<DocRoleDropdown
currentRole={access.role}
onSelectRole={onUpdate}
canUpdate={!isNotAllowed}
/>
<DropdownMenu options={moreActions}>
<IconOptions $variation="600" />
</DropdownMenu>
</Box>
}
/>
);
};

View File

@@ -0,0 +1,205 @@
import { Modal, ModalSize } from '@openfun/cunningham-react';
import { useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { useDebouncedCallback } from 'use-debounce';
import { Box } from '@/components';
import { LoadMoreText } from '@/components/LoadMoreText';
import {
QuickSearch,
QuickSearchData,
} from '@/components/quick-search/QuickSearch';
import { QuickSearchGroup } from '@/components/quick-search/QuickSearchGroup';
import { User } from '@/core';
import { Access, Doc } from '@/features/docs';
import { useDocInvitationsInfinite } from '@/features/docs/members/invitation-list';
import { Invitation } from '@/features/docs/members/invitation-list/types';
import { KEY_LIST_USER, useUsers } from '@/features/docs/members/members-add';
import { useDocAccessesInfinite } from '@/features/docs/members/members-list';
import { isValidEmail } from '@/utils';
import { DocShareAddMemberList } from './DocShareAddMemberList';
import { DocShareInvitationItem } from './DocShareInvitationItem';
import { DocShareMemberItem } from './DocShareMemberItem';
import { DocShareModalInviteUserRow } from './DocShareModalInviteUserByEmail';
type Props = {
doc: Doc;
onClose: () => void;
};
export const DocShareModal = ({ doc, onClose }: Props) => {
const { t } = useTranslation();
const [selectedUsers, setSelectedUsers] = useState<User[]>([]);
const [userQuery, setUserQuery] = useState('');
const [inputValue, setInputValue] = useState('');
const canShare = doc.abilities.accesses_manage;
const showMemberSection = inputValue === '' && selectedUsers.length === 0;
const onSelect = (user: User) => {
setSelectedUsers((prev) => [...prev, user]);
setUserQuery('');
setInputValue('');
};
const membersQuery = useDocAccessesInfinite({
docId: doc.id,
});
const invitationQuery = useDocInvitationsInfinite({
docId: doc.id,
});
const searchUsersQuery = useUsers(
{ query: userQuery, docId: doc.id },
{
enabled: !!userQuery,
queryKey: [KEY_LIST_USER, { query: userQuery }],
},
);
const membersData: QuickSearchData<Access> = useMemo(() => {
const members =
membersQuery.data?.pages.flatMap((page) => page.results) || [];
return {
groupName: t('Members'),
elements: members,
endActions: membersQuery.hasNextPage
? [
{
content: <LoadMoreText />,
onSelect: () => void membersQuery.fetchNextPage(),
},
]
: undefined,
};
}, [membersQuery, t]);
const invitationsData: QuickSearchData<Invitation> = useMemo(() => {
const invitations =
invitationQuery.data?.pages.flatMap((page) => page.results) || [];
return {
groupName: t('Invitations'),
elements: invitations,
endActions: invitationQuery.hasNextPage
? [
{
content: <LoadMoreText />,
onSelect: () => void invitationQuery.fetchNextPage(),
},
]
: undefined,
};
}, [invitationQuery, t]);
const searchUserData: QuickSearchData<User> = useMemo(() => {
const users = searchUsersQuery.data?.results || [];
const isEmail = isValidEmail(userQuery);
const fakeUser: User = {
id: userQuery,
full_name: '',
email: userQuery,
short_name: '',
};
return {
groupName: t('Users'),
elements: users,
endActions:
isEmail && users.length === 0
? [
{
content: <DocShareModalInviteUserRow user={fakeUser} />,
onSelect: () => void onSelect(fakeUser),
},
]
: undefined,
};
}, [searchUsersQuery.data, t, userQuery]);
const onFilter = useDebouncedCallback((str: string) => {
setUserQuery(str);
}, 300);
const onRemoveUser = (row: User) => {
setSelectedUsers((prevState) => {
const index = prevState.findIndex((value) => value.id === row.id);
if (index < 0) {
return prevState;
}
const newArray = [...prevState];
newArray.splice(index, 1);
return newArray;
});
};
return (
<Modal
isOpen
size={ModalSize.LARGE}
onClose={onClose}
title={
<Box $padding="base" $align="flex-start">
{t('Share the document')}
</Box>
}
>
{canShare && selectedUsers.length > 0 && (
<Box $padding={{ horizontal: 'base' }} $margin={{ vertical: '11px' }}>
<DocShareAddMemberList
doc={doc}
selectedUsers={selectedUsers}
onRemoveUser={onRemoveUser}
afterInvite={() => {
setUserQuery('');
setInputValue('');
setSelectedUsers([]);
}}
/>
</Box>
)}
<QuickSearch
data={[]}
onFilter={(str) => {
setInputValue(str);
onFilter(str);
}}
inputValue={inputValue}
showInput={canShare}
loading={searchUsersQuery.isLoading}
renderElement={(user) => <DocShareModalInviteUserRow user={user} />}
onSelect={onSelect}
placeholder={t('Type a name or email')}
>
{!showMemberSection && inputValue !== '' && (
<QuickSearchGroup
group={searchUserData}
onSelect={onSelect}
renderElement={(user) => <DocShareModalInviteUserRow user={user} />}
/>
)}
{showMemberSection && (
<>
{invitationsData.elements.length > 0 && (
<QuickSearchGroup
group={invitationsData}
renderElement={(invitation) => (
<DocShareInvitationItem doc={doc} invitation={invitation} />
)}
/>
)}
<QuickSearchGroup
group={membersData}
renderElement={(access) => (
<DocShareMemberItem doc={doc} access={access} />
)}
/>
</>
)}
</QuickSearch>
</Modal>
);
};

View File

@@ -0,0 +1,34 @@
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { Box, Icon, Text } from '@/components';
import { User } from '@/core';
import { SearchUserRow } from '@/features/users/components/SearchUserRow';
type Props = {
user: User;
};
export const DocShareModalInviteUserRow = ({ user }: Props) => {
const { t } = useTranslation();
return (
<SearchUserRow
user={user}
right={
<Box
$direction="row"
$align="center"
$css={css`
font-family: Arial, Helvetica, sans-serif;
font-size: var(--c--theme--font--sizes--sm);
color: var(--c--theme--colors--greyscale-400);
`}
>
<Text $theme="primary" $variation="600">
{t('Add')}
</Text>
<Icon $theme="primary" $variation="600" iconName="add" />
</Box>
}
/>
);
};

View File

@@ -17,5 +17,6 @@ export const useTrans = () => {
return translatedRoles[role];
},
untitledDocument: t('Untitled document'),
translatedRoles,
};
};

View File

@@ -57,3 +57,8 @@ export interface Doc {
versions_retrieve: boolean;
};
}
export enum DocDownloadFormat {
PDF = 'pdf',
DOCX = 'docx',
}

View File

@@ -13,6 +13,10 @@ interface DeleteDocInvitationProps {
invitationId: string;
}
interface RemoveDocInvitationError {
role?: string[];
}
export const deleteDocInvitation = async ({
docId,
invitationId,
@@ -34,7 +38,7 @@ export const deleteDocInvitation = async ({
type UseDeleteDocInvitationOptions = UseMutationOptions<
void,
APIError,
APIError<RemoveDocInvitationError>,
DeleteDocInvitationProps
>;
@@ -42,7 +46,11 @@ export const useDeleteDocInvitation = (
options?: UseDeleteDocInvitationOptions,
) => {
const queryClient = useQueryClient();
return useMutation<void, APIError, DeleteDocInvitationProps>({
return useMutation<
void,
APIError<RemoveDocInvitationError>,
DeleteDocInvitationProps
>({
mutationFn: deleteDocInvitation,
...options,
onSuccess: (data, variables, context) => {

View File

@@ -17,6 +17,10 @@ interface UpdateDocInvitationProps {
role: Role;
}
interface UpdateDocInvitationError {
role?: string[];
}
export const updateDocInvitation = async ({
docId,
invitationId,
@@ -43,7 +47,7 @@ type UseUpdateDocInvitation = Partial<Invitation>;
type UseUpdateDocInvitationOptions = UseMutationOptions<
Invitation,
APIError,
APIError<UpdateDocInvitationError>,
UseUpdateDocInvitation
>;
@@ -51,7 +55,11 @@ export const useUpdateDocInvitation = (
options?: UseUpdateDocInvitationOptions,
) => {
const queryClient = useQueryClient();
return useMutation<Invitation, APIError, UpdateDocInvitationProps>({
return useMutation<
Invitation,
APIError<UpdateDocInvitationError>,
UpdateDocInvitationProps
>({
mutationFn: updateDocInvitation,
...options,
onSuccess: (data, variables, context) => {

View File

@@ -0,0 +1,71 @@
import { ReactNode } from 'react';
import { css } from 'styled-components';
import { Box, Text } from '@/components';
import { User } from '@/core';
import { useCunninghamTheme } from '@/cunningham';
import { UserAvatar } from './UserAvatar';
type Props = {
user: User;
showRightOnHover?: boolean;
right?: ReactNode;
};
export const SearchUserRow = ({
user,
right,
showRightOnHover = true,
}: Props) => {
const { spacingsTokens } = useCunninghamTheme();
const spacings = spacingsTokens();
const hasFullName = user.full_name != null && user.full_name !== '';
return (
<Box
$direction="row"
$align="center"
$padding={{ horizontal: '2xs', vertical: '3xs' }}
$justify="space-between"
$width="100%"
$css={css`
.right-user-row {
color: red !important;
display: ${showRightOnHover ? 'none' : 'flex'};
}
[data-cmdk-selected='true'] {
background-color: red !important;
}
&:hover {
.right-user-row {
color: green !important;
display: flex;
}
}
`}
>
<Box $direction="row" $align="center" $gap={spacings['2xs']}>
<UserAvatar user={user} />
<Box $direction="column">
<Text $size="sm" $variation="1000">
{hasFullName ? user.full_name : user.email}
</Text>
{hasFullName && (
<Text $size="xs" $variation="500">
{user.email}
</Text>
)}
</Box>
</Box>
{right && (
<Box className="right-user-row" $direction="row" $align="center">
{right}
</Box>
)}
</Box>
);
};

View File

@@ -0,0 +1,67 @@
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'],
];
type Props = {
user: User;
};
export const UserAvatar = ({ user }: Props) => {
const name = user.full_name || user.email;
const splitName = name.split(' ');
const getColorFromName = () => {
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];
};
return (
<Box
$background={getColorFromName()}
$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>
);
};

View File

@@ -2425,6 +2425,127 @@
"@opentelemetry/instrumentation" "^0.49 || ^0.50 || ^0.51 || ^0.52.0"
"@opentelemetry/sdk-trace-base" "^1.22"
"@radix-ui/primitive@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/primitive/-/primitive-1.1.0.tgz#42ef83b3b56dccad5d703ae8c42919a68798bbe2"
integrity sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==
"@radix-ui/react-compose-refs@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.0.tgz#656432461fc8283d7b591dcf0d79152fae9ecc74"
integrity sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==
"@radix-ui/react-context@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-context/-/react-context-1.1.1.tgz#82074aa83a472353bb22e86f11bcbd1c61c4c71a"
integrity sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==
"@radix-ui/react-dialog@^1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dialog/-/react-dialog-1.1.2.tgz#d9345575211d6f2d13e209e84aec9a8584b54d6c"
integrity sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==
dependencies:
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-context" "1.1.1"
"@radix-ui/react-dismissable-layer" "1.1.1"
"@radix-ui/react-focus-guards" "1.1.1"
"@radix-ui/react-focus-scope" "1.1.0"
"@radix-ui/react-id" "1.1.0"
"@radix-ui/react-portal" "1.1.2"
"@radix-ui/react-presence" "1.1.1"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-slot" "1.1.0"
"@radix-ui/react-use-controllable-state" "1.1.0"
aria-hidden "^1.1.1"
react-remove-scroll "2.6.0"
"@radix-ui/react-dismissable-layer@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.1.tgz#cbdcb739c5403382bdde5f9243042ba643883396"
integrity sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==
dependencies:
"@radix-ui/primitive" "1.1.0"
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown" "1.1.0"
"@radix-ui/react-focus-guards@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz#8635edd346304f8b42cae86b05912b61aef27afe"
integrity sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==
"@radix-ui/react-focus-scope@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.0.tgz#ebe2891a298e0a33ad34daab2aad8dea31caf0b2"
integrity sha512-200UD8zylvEyL8Bx+z76RJnASR2gRMuxlgFCPAe/Q/679a/r0eK3MBVYMb7vZODZcffZBdob1EGnky78xmVvcA==
dependencies:
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-id@1.1.0", "@radix-ui/react-id@^1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-id/-/react-id-1.1.0.tgz#de47339656594ad722eb87f94a6b25f9cffae0ed"
integrity sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==
dependencies:
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-portal@1.1.2":
version "1.1.2"
resolved "https://registry.yarnpkg.com/@radix-ui/react-portal/-/react-portal-1.1.2.tgz#51eb46dae7505074b306ebcb985bf65cc547d74e"
integrity sha512-WeDYLGPxJb/5EGBoedyJbT0MpoULmwnIPMJMSldkuiMsBAv7N1cRdsTWZWht9vpPOiN3qyiGAtbK2is47/uMFg==
dependencies:
"@radix-ui/react-primitive" "2.0.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-presence@1.1.1":
version "1.1.1"
resolved "https://registry.yarnpkg.com/@radix-ui/react-presence/-/react-presence-1.1.1.tgz#98aba423dba5e0c687a782c0669dcd99de17f9b1"
integrity sha512-IeFXVi4YS1K0wVZzXNrbaaUvIJ3qdY+/Ih4eHFhWA9SwGR9UDX7Ck8abvL57C4cv3wwMvUE0OG69Qc3NCcTe/A==
dependencies:
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-use-layout-effect" "1.1.0"
"@radix-ui/react-primitive@2.0.0", "@radix-ui/react-primitive@^2.0.0":
version "2.0.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-primitive/-/react-primitive-2.0.0.tgz#fe05715faa9203a223ccc0be15dc44b9f9822884"
integrity sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==
dependencies:
"@radix-ui/react-slot" "1.1.0"
"@radix-ui/react-slot@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-slot/-/react-slot-1.1.0.tgz#7c5e48c36ef5496d97b08f1357bb26ed7c714b84"
integrity sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==
dependencies:
"@radix-ui/react-compose-refs" "1.1.0"
"@radix-ui/react-use-callback-ref@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz#bce938ca413675bc937944b0d01ef6f4a6dc5bf1"
integrity sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==
"@radix-ui/react-use-controllable-state@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz#1321446857bb786917df54c0d4d084877aab04b0"
integrity sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==
dependencies:
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-escape-keydown@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz#31a5b87c3b726504b74e05dac1edce7437b98754"
integrity sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==
dependencies:
"@radix-ui/react-use-callback-ref" "1.1.0"
"@radix-ui/react-use-layout-effect@1.1.0":
version "1.1.0"
resolved "https://registry.yarnpkg.com/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz#3c2c8ce04827b26a39e442ff4888d9212268bd27"
integrity sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==
"@react-aria/breadcrumbs@^3.5.16", "@react-aria/breadcrumbs@^3.5.19":
version "3.5.19"
resolved "https://registry.yarnpkg.com/@react-aria/breadcrumbs/-/breadcrumbs-3.5.19.tgz#e0a67e0e7017089fa0ee5eadd51a6da505b94cd4"
@@ -5171,6 +5292,13 @@ argparse@^2.0.1:
resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38"
integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
aria-hidden@^1.1.1:
version "1.2.4"
resolved "https://registry.yarnpkg.com/aria-hidden/-/aria-hidden-1.2.4.tgz#b78e383fdbc04d05762c78b4a25a501e736c4522"
integrity sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==
dependencies:
tslib "^2.0.0"
aria-query@5.3.0:
version "5.3.0"
resolved "https://registry.yarnpkg.com/aria-query/-/aria-query-5.3.0.tgz#650c569e41ad90b51b3d7df5e5eed1c7549c103e"
@@ -5742,6 +5870,16 @@ clsx@^2.0.0, clsx@^2.1.1:
resolved "https://registry.yarnpkg.com/clsx/-/clsx-2.1.1.tgz#eed397c9fd8bd882bfb18deab7102049a2f32999"
integrity sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==
cmdk@^1.0.4:
version "1.0.4"
resolved "https://registry.yarnpkg.com/cmdk/-/cmdk-1.0.4.tgz#cbddef6f5ade2378f85c80a0b9ad9a8a712779b5"
integrity sha512-AnsjfHyHpQ/EFeAnG216WY7A5LiYCoZzCSygiLvfXC3H3LFGCprErteUcszaVluGOhuOTbJS3jWHrSDYPBBygg==
dependencies:
"@radix-ui/react-dialog" "^1.1.2"
"@radix-ui/react-id" "^1.1.0"
"@radix-ui/react-primitive" "^2.0.0"
use-sync-external-store "^1.2.2"
co@^4.6.0:
version "4.6.0"
resolved "https://registry.yarnpkg.com/co/-/co-4.6.0.tgz#6ea6bdf3d853ae54ccb8e47bfa0bf3f9031fb184"
@@ -10766,7 +10904,7 @@ react-remove-scroll-bar@^2.3.6:
react-style-singleton "^2.2.1"
tslib "^2.0.0"
react-remove-scroll@^2.6.0:
react-remove-scroll@2.6.0, react-remove-scroll@^2.6.0:
version "2.6.0"
resolved "https://registry.yarnpkg.com/react-remove-scroll/-/react-remove-scroll-2.6.0.tgz#fb03a0845d7768a4f1519a99fdb84983b793dc07"
integrity sha512-I2U4JVEsQenxDAKaVa3VZ/JeJZe0/2DxPWL8Tj8yLKctQJQiZM52pn/GWFpSp8dftjM3pSAHVJZscAnC/y+ySQ==
@@ -12479,6 +12617,11 @@ use-composed-ref@^1.3.0:
resolved "https://registry.yarnpkg.com/use-composed-ref/-/use-composed-ref-1.3.0.tgz#3d8104db34b7b264030a9d916c5e94fbe280dbda"
integrity sha512-GLMG0Jc/jiKov/3Ulid1wbv3r54K9HlMW29IWcDFPEqFkSO2nS0MuefWgMJpeHQ9YJeXDL3ZUF+P3jdXlZX/cQ==
use-debounce@10.0.4:
version "10.0.4"
resolved "https://registry.yarnpkg.com/use-debounce/-/use-debounce-10.0.4.tgz#2135be498ad855416c4495cfd8e0e130bd33bb24"
integrity sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw==
use-isomorphic-layout-effect@^1.1.1, use-isomorphic-layout-effect@^1.1.2:
version "1.1.2"
resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb"
@@ -12499,7 +12642,7 @@ use-sidecar@^1.1.2:
detect-node-es "^1.1.0"
tslib "^2.0.0"
use-sync-external-store@^1, use-sync-external-store@^1.2.0:
use-sync-external-store@^1, use-sync-external-store@^1.2.0, use-sync-external-store@^1.2.2:
version "1.2.2"
resolved "https://registry.yarnpkg.com/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz#c3b6390f3a30eba13200d2302dcdf1e7b57b2ef9"
integrity sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==