mirror of
https://github.com/suitenumerique/docs.git
synced 2026-05-14 02:46:24 +02:00
wip
This commit is contained in:
@@ -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: {
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -40,7 +40,7 @@ export const DropdownMenu = ({
|
||||
onOpenChange={onOpenChange}
|
||||
button={
|
||||
showArrow ? (
|
||||
<Box>
|
||||
<Box $direction="row" $align="center">
|
||||
<div>{children}</div>
|
||||
<Icon
|
||||
$css={
|
||||
|
||||
28
src/frontend/apps/impress/src/components/LoadMoreText.tsx
Normal file
28
src/frontend/apps/impress/src/components/LoadMoreText.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
48
src/frontend/apps/impress/src/components/SimpleLoader.tsx
Normal file
48
src/frontend/apps/impress/src/components/SimpleLoader.tsx
Normal 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);
|
||||
}
|
||||
}
|
||||
`}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -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>;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
`;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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: {
|
||||
|
||||
@@ -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} />
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -17,5 +17,6 @@ export const useTrans = () => {
|
||||
return translatedRoles[role];
|
||||
},
|
||||
untitledDocument: t('Untitled document'),
|
||||
translatedRoles,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -57,3 +57,8 @@ export interface Doc {
|
||||
versions_retrieve: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export enum DocDownloadFormat {
|
||||
PDF = 'pdf',
|
||||
DOCX = 'docx',
|
||||
}
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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==
|
||||
|
||||
Reference in New Issue
Block a user