🔥(frontend) remove unused document management components
- Deleted `DocVisibility`, `ModalShare`, `InvitationList`, `MemberList`, and related components to streamline the document management feature. - Updated component exports to reflect the removal of these components. - Cleaned up associated assets and styles to improve code maintainability.
This commit is contained in:
committed by
Anthony LC
parent
8456f47260
commit
a5f6cb542d
@@ -1,144 +0,0 @@
|
||||
import {
|
||||
Radio,
|
||||
RadioGroup,
|
||||
Select,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, Card, IconBG } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
|
||||
import { KEY_DOC, KEY_LIST_DOC, useUpdateDocLink } from '../api';
|
||||
import { Doc, LinkReach, LinkRole } from '../types';
|
||||
|
||||
interface DocVisibilityProps {
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const DocVisibility = ({ doc }: DocVisibilityProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { toast } = useToastProvider();
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const api = useUpdateDocLink({
|
||||
onSuccess: () => {
|
||||
toast(
|
||||
t('The document visibility has been updated.'),
|
||||
VariantType.SUCCESS,
|
||||
{
|
||||
duration: 4000,
|
||||
},
|
||||
);
|
||||
},
|
||||
listInvalideQueries: [KEY_LIST_DOC, KEY_DOC],
|
||||
});
|
||||
|
||||
const transLinkReach = {
|
||||
[LinkReach.RESTRICTED]: {
|
||||
label: t('Restricted'),
|
||||
description: t('Only for people with access'),
|
||||
},
|
||||
[LinkReach.AUTHENTICATED]: {
|
||||
label: t('Authenticated'),
|
||||
description: t('Only for authenticated users'),
|
||||
},
|
||||
[LinkReach.PUBLIC]: {
|
||||
label: t('Public'),
|
||||
description: t('Anyone on the internet with the link can view'),
|
||||
},
|
||||
};
|
||||
|
||||
const linkRoleList = [
|
||||
{
|
||||
label: t('Read only'),
|
||||
value: LinkRole.READER,
|
||||
},
|
||||
{
|
||||
label: t('Can read and edit'),
|
||||
value: LinkRole.EDITOR,
|
||||
},
|
||||
];
|
||||
|
||||
const showLinkRoleOptions = doc.link_reach !== LinkReach.RESTRICTED;
|
||||
|
||||
return (
|
||||
<Card
|
||||
$margin="tiny"
|
||||
$padding={{ horizontal: 'small', vertical: 'tiny' }}
|
||||
aria-label={t('Doc visibility card')}
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$justify="space-between"
|
||||
$gap="1rem"
|
||||
$wrap="wrap"
|
||||
>
|
||||
<IconBG iconName="public" />
|
||||
<Box
|
||||
$wrap="wrap"
|
||||
$gap="1rem"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$flex="1"
|
||||
$css={`
|
||||
& .c__field__footer .c__field__text {
|
||||
${!doc.abilities.link_configuration && `color: ${colorsTokens()['greyscale-400']};`};
|
||||
}
|
||||
`}
|
||||
>
|
||||
<Box $shrink="0" $flex="auto" $maxWidth="20rem">
|
||||
<Select
|
||||
label={t('Visibility')}
|
||||
options={Object.values(LinkReach).map((linkReach) => ({
|
||||
label: transLinkReach[linkReach].label,
|
||||
value: linkReach,
|
||||
}))}
|
||||
onChange={(evt) =>
|
||||
api.mutate({
|
||||
link_reach: evt.target.value as LinkReach,
|
||||
id: doc.id,
|
||||
})
|
||||
}
|
||||
value={doc.link_reach}
|
||||
clearable={false}
|
||||
text={transLinkReach[doc.link_reach].description}
|
||||
disabled={!doc.abilities.link_configuration}
|
||||
/>
|
||||
</Box>
|
||||
{showLinkRoleOptions && (
|
||||
<Box
|
||||
$css={`
|
||||
& .c__checkbox{
|
||||
padding: 0.15rem 0.25rem;
|
||||
}
|
||||
`}
|
||||
>
|
||||
<RadioGroup
|
||||
compact
|
||||
style={{
|
||||
display: 'flex',
|
||||
}}
|
||||
text={t('How people can interact with the document')}
|
||||
>
|
||||
{linkRoleList.map((radio) => (
|
||||
<Radio
|
||||
key={radio.value}
|
||||
label={radio.label}
|
||||
value={radio.value}
|
||||
onChange={() =>
|
||||
api.mutate({
|
||||
link_role: radio.value,
|
||||
id: doc.id,
|
||||
})
|
||||
}
|
||||
checked={doc.link_role === radio.value}
|
||||
disabled={!doc.abilities.link_configuration}
|
||||
/>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -1,138 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { createGlobalStyle } from 'styled-components';
|
||||
|
||||
import { Box, Card, IconBG, SideModal, Text } from '@/components';
|
||||
import { InvitationList } from '@/features/docs/members/invitation-list';
|
||||
import { AddMembers } from '@/features/docs/members/members-add';
|
||||
import { MemberList } from '@/features/docs/members/members-list';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { Doc } from '../types';
|
||||
import { currentDocRole } from '../utils';
|
||||
|
||||
import { DocVisibility } from './DocVisibility';
|
||||
|
||||
const ModalShareStyle = createGlobalStyle`
|
||||
& .c__modal__scroller{
|
||||
background: #FAFAFA;
|
||||
padding: 1.5rem .5rem;
|
||||
|
||||
.c__modal__title{
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.c__modal__close{
|
||||
margin-right: 1rem;
|
||||
|
||||
button{
|
||||
border-bottom: 1px solid #E0E0E0;
|
||||
border-left: 1px solid #E0E0E0;
|
||||
}
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
interface ModalShareProps {
|
||||
onClose: () => void;
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const ModalShare = ({ onClose, doc }: ModalShareProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { isMobile, isSmallMobile } = useResponsiveStore();
|
||||
const width = isSmallMobile ? '100vw' : isMobile ? '90vw' : '70vw';
|
||||
const { toast } = useToastProvider();
|
||||
|
||||
return (
|
||||
<>
|
||||
<ModalShareStyle />
|
||||
<SideModal
|
||||
isOpen
|
||||
closeOnClickOutside
|
||||
hideCloseButton={!isSmallMobile}
|
||||
onClose={onClose}
|
||||
width={width}
|
||||
$css="min-width: 320px;max-width: 777px;"
|
||||
>
|
||||
<Box aria-label={t('Share modal')} $margin={{ bottom: 'small' }}>
|
||||
<Box $shrink="0">
|
||||
<Card
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$margin={{ horizontal: 'tiny', top: 'none', bottom: 'big' }}
|
||||
$padding="tiny"
|
||||
$gap="1rem"
|
||||
>
|
||||
<IconBG
|
||||
$isMaterialIcon
|
||||
$size="48px"
|
||||
iconName="share"
|
||||
$margin="none"
|
||||
/>
|
||||
<Box
|
||||
$justify="space-between"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$width="100%"
|
||||
$gap="1rem"
|
||||
$wrap="wrap"
|
||||
>
|
||||
<Box $align="flex-start">
|
||||
<Text as="h3" $size="26px" $margin="none">
|
||||
{t('Share')}
|
||||
</Text>
|
||||
<Text $size="small" $weight="normal" $textAlign="left">
|
||||
{doc.title}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box $margin={{ right: '1.5rem' }} $shrink="0">
|
||||
<Button
|
||||
onClick={() => {
|
||||
navigator.clipboard
|
||||
.writeText(window.location.href)
|
||||
.then(() => {
|
||||
toast(t('Link Copied !'), VariantType.SUCCESS, {
|
||||
duration: 3000,
|
||||
});
|
||||
})
|
||||
.catch(() => {
|
||||
toast(t('Failed to copy link'), VariantType.ERROR, {
|
||||
duration: 3000,
|
||||
});
|
||||
});
|
||||
}}
|
||||
color="primary"
|
||||
icon={<span className="material-icons">copy</span>}
|
||||
>
|
||||
{t('Copy link')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
<DocVisibility doc={doc} />
|
||||
{doc.abilities.accesses_manage && (
|
||||
<AddMembers
|
||||
doc={doc}
|
||||
currentRole={currentDocRole(doc.abilities)}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
<Box $minHeight="0">
|
||||
{doc.abilities.accesses_view && (
|
||||
<>
|
||||
<InvitationList doc={doc} />
|
||||
<MemberList doc={doc} />
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</SideModal>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +1 @@
|
||||
export * from './ModalRemoveDoc';
|
||||
export * from './ModalShare';
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
export * from './useDeleteDocAccess';
|
||||
export * from './useDocAccesses';
|
||||
export * from './useUpdateDocAccess';
|
||||
export * from './useCreateDocAccess';
|
||||
export * from './useUsers';
|
||||
export * from './useCreateDocInvitation';
|
||||
export * from './useDeleteDocInvitation';
|
||||
export * from './useDocInvitations';
|
||||
export * from './useUpdateDocInvitation';
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
KEY_LIST_DOC,
|
||||
Role,
|
||||
} from '@/features/docs/doc-management';
|
||||
import { KEY_LIST_DOC_ACCESSES } from '@/features/docs/members/members-list';
|
||||
import { KEY_LIST_DOC_ACCESSES } from '@/features/docs/doc-share';
|
||||
import { ContentLanguage } from '@/i18n/types';
|
||||
import { useBroadcastStore } from '@/stores';
|
||||
|
||||
@@ -3,11 +3,9 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
import { User } from '@/core/auth';
|
||||
import { Doc, Role } from '@/features/docs/doc-management';
|
||||
import { OptionType } from '@/features/docs/members/members-add/types';
|
||||
import { Invitation, OptionType } from '@/features/docs/doc-share/types';
|
||||
import { ContentLanguage } from '@/i18n/types';
|
||||
|
||||
import { Invitation } from '../types';
|
||||
|
||||
import { KEY_LIST_DOC_INVITATIONS } from './useDocInvitations';
|
||||
|
||||
interface CreateDocInvitationParams {
|
||||
@@ -6,7 +6,7 @@ import {
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
import { KEY_DOC, KEY_LIST_DOC } from '@/features/docs/doc-management';
|
||||
import { KEY_LIST_USER } from '@/features/docs/members/members-add';
|
||||
import { KEY_LIST_USER } from '@/features/docs/doc-share';
|
||||
import { useBroadcastStore } from '@/stores';
|
||||
|
||||
import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses';
|
||||
@@ -8,8 +8,7 @@ import {
|
||||
} from '@tanstack/react-query';
|
||||
|
||||
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
|
||||
|
||||
import { Invitation } from '../types';
|
||||
import { Invitation } from '@/features/docs/doc-share/types';
|
||||
|
||||
export type DocInvitationsParams = {
|
||||
docId: string;
|
||||
@@ -6,8 +6,7 @@ import {
|
||||
|
||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||
import { Role } from '@/features/docs/doc-management';
|
||||
|
||||
import { Invitation } from '../types';
|
||||
import { Invitation } from '@/features/docs/doc-share/types';
|
||||
|
||||
import { KEY_LIST_DOC_INVITATIONS } from './useDocInvitations';
|
||||
|
||||
@@ -12,9 +12,11 @@ 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 {
|
||||
useCreateDocAccess,
|
||||
useCreateDocInvitation,
|
||||
} from '@/features/docs/doc-share';
|
||||
import { OptionType } from '@/features/docs/doc-share/types';
|
||||
import { useLanguage } from '@/i18n/hooks/useLanguage';
|
||||
|
||||
import { DocRoleDropdown } from './DocRoleDropdown';
|
||||
|
||||
@@ -9,12 +9,12 @@ import {
|
||||
} from '@/components';
|
||||
import { User } from '@/core';
|
||||
import { Doc, Role } from '@/features/docs/doc-management';
|
||||
import { SearchUserRow } from '@/features/docs/doc-share/component/SearchUserRow';
|
||||
import {
|
||||
useDeleteDocInvitation,
|
||||
useUpdateDocInvitation,
|
||||
} from '@/features/docs/members/invitation-list';
|
||||
import { Invitation } from '@/features/docs/members/invitation-list/types';
|
||||
} from '@/features/docs/doc-share';
|
||||
import { SearchUserRow } from '@/features/docs/doc-share/component/SearchUserRow';
|
||||
import { Invitation } from '@/features/docs/doc-share/types';
|
||||
|
||||
import { DocRoleDropdown } from './DocRoleDropdown';
|
||||
|
||||
|
||||
@@ -8,14 +8,11 @@ import {
|
||||
IconOptions,
|
||||
} from '@/components';
|
||||
import { SearchUserRow } from '@/features/docs/doc-share/component/SearchUserRow';
|
||||
import {
|
||||
useDeleteDocAccess,
|
||||
useUpdateDocAccess,
|
||||
} from '@/features/docs/members/members-list';
|
||||
import { useWhoAmI } from '@/features/docs/members/members-list/hooks/useWhoAmI';
|
||||
import { useWhoAmI } from '@/features/docs/doc-share/hooks/useWhoAmI';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { Access, Doc, Role } from '../../doc-management/types';
|
||||
import { useDeleteDocAccess, useUpdateDocAccess } from '../index';
|
||||
|
||||
import { DocRoleDropdown } from './DocRoleDropdown';
|
||||
|
||||
|
||||
@@ -13,10 +13,13 @@ import {
|
||||
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 {
|
||||
KEY_LIST_USER,
|
||||
useDocAccessesInfinite,
|
||||
useDocInvitationsInfinite,
|
||||
useUsers,
|
||||
} from '@/features/docs/doc-share';
|
||||
import { Invitation } from '@/features/docs/doc-share/types';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
import { isValidEmail } from '@/utils';
|
||||
|
||||
|
||||
@@ -11,15 +11,16 @@ import {
|
||||
Text,
|
||||
} from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import {
|
||||
Doc,
|
||||
KEY_DOC,
|
||||
KEY_LIST_DOC,
|
||||
LinkReach,
|
||||
LinkRole,
|
||||
useUpdateDocLink,
|
||||
} from '../../doc-management/api';
|
||||
import { useTranslatedShareSettings } from '../hooks/useTranslatedShareSettings';
|
||||
import { Doc, LinkReach, LinkRole } from '../../doc-management/types';
|
||||
} from '@/features/docs';
|
||||
import { useTranslatedShareSettings } from '@/features/docs/doc-share';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
interface DocVisibilityProps {
|
||||
doc: Doc;
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './useTranslatedShareSettings';
|
||||
export * from './useWhoAmI';
|
||||
@@ -0,0 +1,2 @@
|
||||
export * from './api';
|
||||
export * from './hooks';
|
||||
@@ -1,4 +1,21 @@
|
||||
import { User } from '@/core/auth';
|
||||
import { Role } from '@/features/docs';
|
||||
|
||||
export interface Invitation {
|
||||
id: string;
|
||||
role: Role;
|
||||
document: string;
|
||||
created_at: string;
|
||||
is_expired: boolean;
|
||||
issuer: string;
|
||||
email: string;
|
||||
abilities: {
|
||||
destroy: boolean;
|
||||
retrieve: boolean;
|
||||
partial_update: boolean;
|
||||
update: boolean;
|
||||
};
|
||||
}
|
||||
|
||||
export enum OptionType {
|
||||
INVITATION = 'invitation',
|
||||
@@ -1,4 +0,0 @@
|
||||
export * from './useCreateDocInvitation';
|
||||
export * from './useDeleteDocInvitation';
|
||||
export * from './useDocInvitations';
|
||||
export * from './useUpdateDocInvitation';
|
||||
@@ -1,146 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
Loader,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, IconBG, Text, TextErrors } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc, Role } from '@/features/docs/doc-management';
|
||||
import { ChooseRole } from '@/features/docs/members/members-add/';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useDeleteDocInvitation, useUpdateDocInvitation } from '../api';
|
||||
import { Invitation } from '../types';
|
||||
|
||||
interface InvitationItemProps {
|
||||
role: Role;
|
||||
currentRole: Role;
|
||||
invitation: Invitation;
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const InvitationItem = ({
|
||||
doc,
|
||||
role,
|
||||
invitation,
|
||||
currentRole,
|
||||
}: InvitationItemProps) => {
|
||||
const canDelete = invitation.abilities.destroy;
|
||||
const canUpdate = invitation.abilities.partial_update;
|
||||
const { t } = useTranslation();
|
||||
const { isSmallMobile, screenWidth } = useResponsiveStore();
|
||||
const [localRole, setLocalRole] = useState(role);
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { toast } = useToastProvider();
|
||||
const { mutate: updateDocInvitation, error: errorUpdate } =
|
||||
useUpdateDocInvitation({
|
||||
onSuccess: () => {
|
||||
toast(t('The role has been updated.'), VariantType.SUCCESS, {
|
||||
duration: 4000,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: removeDocInvitation, error: errorDelete } =
|
||||
useDeleteDocInvitation({
|
||||
onSuccess: () => {
|
||||
toast(t('The invitation has been removed.'), VariantType.SUCCESS, {
|
||||
duration: 4000,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
if (!invitation.email) {
|
||||
return (
|
||||
<Box className="m-auto">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box $width="100%" $gap="0.7rem">
|
||||
<Box $direction="row" $gap="1rem" $wrap="wrap">
|
||||
<Box
|
||||
$align="center"
|
||||
$direction="row"
|
||||
$gap="1rem"
|
||||
$justify="space-between"
|
||||
$width="100%"
|
||||
$wrap="wrap"
|
||||
$css={`flex: ${isSmallMobile ? '100%' : '70%'};`}
|
||||
>
|
||||
<IconBG iconName="account_circle" $size="2rem" />
|
||||
<Box $css="flex:1;">
|
||||
<Text
|
||||
$size="t"
|
||||
$background={colorsTokens()['info-600']}
|
||||
$color="white"
|
||||
$radius="2px"
|
||||
$padding="xtiny"
|
||||
$css="align-self: flex-start;"
|
||||
>
|
||||
{t('Invited')}
|
||||
</Text>
|
||||
<Text $justify="center">{invitation.email}</Text>
|
||||
</Box>
|
||||
<Box
|
||||
$direction="row"
|
||||
$gap="1rem"
|
||||
$align="center"
|
||||
$justify="space-between"
|
||||
$css="flex:1;"
|
||||
$wrap={screenWidth < 400 ? 'wrap' : 'nowrap'}
|
||||
>
|
||||
<Box $minWidth="13rem" $css={isSmallMobile ? 'flex:1;' : ''}>
|
||||
<ChooseRole
|
||||
label={t('Role')}
|
||||
defaultRole={localRole}
|
||||
currentRole={currentRole}
|
||||
disabled={!canUpdate}
|
||||
setRole={(role) => {
|
||||
setLocalRole(role);
|
||||
updateDocInvitation({
|
||||
docId: doc.id,
|
||||
invitationId: invitation.id,
|
||||
role,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{doc.abilities.accesses_manage && (
|
||||
<Box $margin={isSmallMobile ? 'auto' : ''}>
|
||||
<Button
|
||||
color="tertiary-text"
|
||||
icon={
|
||||
<Text
|
||||
$isMaterialIcon
|
||||
$theme={!canDelete ? 'greyscale' : 'primary'}
|
||||
$variation={!canDelete ? '500' : 'text'}
|
||||
>
|
||||
delete
|
||||
</Text>
|
||||
}
|
||||
disabled={!canDelete}
|
||||
onClick={() =>
|
||||
removeDocInvitation({
|
||||
docId: doc.id,
|
||||
invitationId: invitation.id,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
{(errorUpdate || errorDelete) && (
|
||||
<TextErrors causes={errorUpdate?.cause || errorDelete?.cause} />
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,142 +0,0 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { APIError } from '@/api';
|
||||
import { Box, Card, InfiniteScroll, TextErrors } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc, currentDocRole } from '@/features/docs/doc-management';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useDocInvitationsInfinite } from '../api';
|
||||
import { Invitation } from '../types';
|
||||
|
||||
import { InvitationItem } from './InvitationItem';
|
||||
|
||||
interface InvitationListStateProps {
|
||||
isLoading: boolean;
|
||||
error: APIError | null;
|
||||
invitations?: Invitation[];
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
const InvitationListState = ({
|
||||
invitations,
|
||||
error,
|
||||
isLoading,
|
||||
doc,
|
||||
}: InvitationListStateProps) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { isSmallMobile } = useResponsiveStore();
|
||||
|
||||
if (error) {
|
||||
return <TextErrors causes={error.cause} />;
|
||||
}
|
||||
|
||||
if (isLoading || !invitations) {
|
||||
return (
|
||||
<Box $align="center" className="m-l">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return invitations?.map((invitation, index) => {
|
||||
if (!invitation.email) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={`${invitation.id}-${index}`}
|
||||
$background={!(index % 2) ? 'white' : colorsTokens()['greyscale-000']}
|
||||
$direction="row"
|
||||
$padding={isSmallMobile ? 'tiny' : 'small'}
|
||||
$align="center"
|
||||
$gap="1rem"
|
||||
$radius="4px"
|
||||
as="li"
|
||||
>
|
||||
<InvitationItem
|
||||
invitation={invitation}
|
||||
role={invitation.role}
|
||||
doc={doc}
|
||||
currentRole={currentDocRole(doc.abilities)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
interface InvitationListProps {
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const InvitationList = ({ doc }: InvitationListProps) => {
|
||||
const { t } = useTranslation();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [, setRefInitialized] = useState(false);
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
error,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useDocInvitationsInfinite({
|
||||
docId: doc.id,
|
||||
});
|
||||
|
||||
const invitations = useMemo(() => {
|
||||
return data?.pages.reduce((acc, page) => {
|
||||
return acc.concat(page.results);
|
||||
}, [] as Invitation[]);
|
||||
}, [data?.pages]);
|
||||
|
||||
/**
|
||||
* The "return null;" statement below blocks a necessary rerender
|
||||
* for the InfiniteScroll component to work properly.
|
||||
* This useEffect is a workaround to force the rerender.
|
||||
*/
|
||||
useEffect(() => {
|
||||
if (containerRef.current) {
|
||||
setRefInitialized(true);
|
||||
}
|
||||
}, [invitations?.length]);
|
||||
|
||||
if (!invitations?.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Card
|
||||
$margin="tiny"
|
||||
$overflow="auto"
|
||||
$maxHeight="50vh"
|
||||
$padding="tiny"
|
||||
aria-label={t('List invitation card')}
|
||||
>
|
||||
<Box ref={containerRef} $overflow="auto">
|
||||
<InfiniteScroll
|
||||
hasMore={hasNextPage}
|
||||
isLoading={isFetchingNextPage}
|
||||
next={() => {
|
||||
void fetchNextPage();
|
||||
}}
|
||||
scrollContainer={containerRef.current}
|
||||
as="ul"
|
||||
role="listbox"
|
||||
$padding="none"
|
||||
$margin="none"
|
||||
>
|
||||
<InvitationListState
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
invitations={invitations}
|
||||
doc={doc}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './InvitationList';
|
||||
@@ -1 +0,0 @@
|
||||
export const PAGE_SIZE = 20;
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './api';
|
||||
export * from './components';
|
||||
@@ -1,17 +0,0 @@
|
||||
import { Role } from '@/features/docs/doc-management';
|
||||
|
||||
export interface Invitation {
|
||||
id: string;
|
||||
role: Role;
|
||||
document: string;
|
||||
created_at: string;
|
||||
is_expired: boolean;
|
||||
issuer: string;
|
||||
email: string;
|
||||
abilities: {
|
||||
destroy: boolean;
|
||||
retrieve: boolean;
|
||||
partial_update: boolean;
|
||||
update: boolean;
|
||||
};
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './useCreateDocAccess';
|
||||
export * from './useUsers';
|
||||
@@ -1,14 +0,0 @@
|
||||
<svg viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path
|
||||
d="M21.34 26.04C20.9 26.02 20.46 26 20 26C15.16 26 10.64 27.34 6.78 29.64C5.02 30.68 4 32.64 4 34.7V40H22.52C20.94 37.74 20 34.98 20 32C20 29.86 20.5 27.86 21.34 26.04Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M20 24C24.4183 24 28 20.4183 28 16C28 11.5817 24.4183 8 20 8C15.5817 8 12 11.5817 12 16C12 20.4183 15.5817 24 20 24Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
<path
|
||||
d="M33 24C28.032 24 24 28.032 24 33C24 37.968 28.032 42 33 42C37.968 42 42 37.968 42 33C42 28.032 37.968 24 33 24ZM36.6 33.9H33.9V36.6C33.9 37.095 33.495 37.5 33 37.5C32.505 37.5 32.1 37.095 32.1 36.6V33.9H29.4C28.905 33.9 28.5 33.495 28.5 33C28.5 32.505 28.905 32.1 29.4 32.1H32.1V29.4C32.1 28.905 32.505 28.5 33 28.5C33.495 28.5 33.9 28.905 33.9 29.4V32.1H36.6C37.095 32.1 37.5 32.505 37.5 33C37.5 33.495 37.095 33.9 36.6 33.9Z"
|
||||
fill="currentColor"
|
||||
/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 925 B |
@@ -1,203 +0,0 @@
|
||||
import {
|
||||
Button,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { APIError } from '@/api';
|
||||
import { Box, Card, IconBG } from '@/components';
|
||||
import { Doc, Role } from '@/features/docs/doc-management';
|
||||
import { useCreateDocInvitation } from '@/features/docs/members/invitation-list/';
|
||||
import { useLanguage } from '@/i18n/hooks/useLanguage';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useCreateDocAccess } from '../api';
|
||||
import {
|
||||
OptionInvitation,
|
||||
OptionNewMember,
|
||||
OptionSelect,
|
||||
OptionType,
|
||||
isOptionNewMember,
|
||||
} from '../types';
|
||||
|
||||
import { ChooseRole } from './ChooseRole';
|
||||
import { OptionsSelect, SearchUsers } from './SearchUsers';
|
||||
|
||||
type APIErrorUser = APIError<{
|
||||
value: string;
|
||||
type: OptionType;
|
||||
}>;
|
||||
|
||||
interface ModalAddMembersProps {
|
||||
currentRole: Role;
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const AddMembers = ({ currentRole, doc }: ModalAddMembersProps) => {
|
||||
const { contentLanguage } = useLanguage();
|
||||
const { t } = useTranslation();
|
||||
const { isSmallMobile } = useResponsiveStore();
|
||||
const [selectedUsers, setSelectedUsers] = useState<OptionsSelect>([]);
|
||||
const [selectedRole, setSelectedRole] = useState<Role>();
|
||||
const { toast } = useToastProvider();
|
||||
const { mutateAsync: createInvitation } = useCreateDocInvitation();
|
||||
const { mutateAsync: createDocAccess } = useCreateDocAccess();
|
||||
const [resetKey, setResetKey] = useState(1);
|
||||
|
||||
const [isPending, setIsPending] = useState<boolean>(false);
|
||||
|
||||
const switchActions = (selectedUsers: OptionsSelect, selectedRole: Role) =>
|
||||
selectedUsers.map(async (selectedUser) => {
|
||||
switch (selectedUser.type) {
|
||||
case OptionType.INVITATION:
|
||||
await createInvitation({
|
||||
email: selectedUser.value.email,
|
||||
role: selectedRole,
|
||||
docId: doc.id,
|
||||
contentLanguage,
|
||||
});
|
||||
break;
|
||||
|
||||
case OptionType.NEW_MEMBER:
|
||||
await createDocAccess({
|
||||
role: selectedRole,
|
||||
docId: doc.id,
|
||||
memberId: selectedUser.value.id,
|
||||
contentLanguage,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
return selectedUser;
|
||||
});
|
||||
|
||||
const toastOptions = {
|
||||
duration: 4000,
|
||||
};
|
||||
|
||||
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, toastOptions);
|
||||
};
|
||||
|
||||
const onSuccess = (option: OptionSelect) => {
|
||||
const message = !isOptionNewMember(option)
|
||||
? t('Invitation sent to {{email}}.', {
|
||||
email: option.value.email,
|
||||
})
|
||||
: t('User {{email}} added to the document.', {
|
||||
email: option.value.email,
|
||||
});
|
||||
|
||||
toast(message, VariantType.SUCCESS, toastOptions);
|
||||
};
|
||||
|
||||
const handleValidate = async () => {
|
||||
setIsPending(true);
|
||||
|
||||
if (!selectedRole) {
|
||||
return;
|
||||
}
|
||||
|
||||
const settledPromises = await Promise.allSettled<
|
||||
OptionInvitation | OptionNewMember
|
||||
>(switchActions(selectedUsers, selectedRole));
|
||||
|
||||
setIsPending(false);
|
||||
setResetKey(resetKey + 1);
|
||||
setSelectedUsers([]);
|
||||
setSelectedRole(undefined);
|
||||
|
||||
settledPromises.forEach((settledPromise) => {
|
||||
switch (settledPromise.status) {
|
||||
case 'rejected':
|
||||
onError(settledPromise.reason as APIErrorUser);
|
||||
break;
|
||||
|
||||
case 'fulfilled':
|
||||
onSuccess(settledPromise.value);
|
||||
break;
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card
|
||||
$gap="1rem"
|
||||
$padding={{ horizontal: 'small', vertical: 'tiny' }}
|
||||
$margin="tiny"
|
||||
$direction="row"
|
||||
$align="center"
|
||||
$wrap="wrap"
|
||||
>
|
||||
<IconBG iconName="group_add" />
|
||||
<Box
|
||||
$gap="0.7rem"
|
||||
$direction="row"
|
||||
$wrap={isSmallMobile ? 'wrap' : 'nowrap'}
|
||||
$css="flex: 70%;"
|
||||
>
|
||||
<Box $gap="0.7rem" $direction="row" $wrap="wrap" $css="flex: 80%;">
|
||||
<Box $css="flex: auto;" $width="15rem">
|
||||
<SearchUsers
|
||||
key={resetKey}
|
||||
doc={doc}
|
||||
setSelectedUsers={setSelectedUsers}
|
||||
selectedUsers={selectedUsers}
|
||||
disabled={isPending || !doc.abilities.accesses_manage}
|
||||
/>
|
||||
</Box>
|
||||
<Box $css="flex: auto;">
|
||||
<ChooseRole
|
||||
key={resetKey}
|
||||
currentRole={currentRole}
|
||||
disabled={isPending || !doc.abilities.accesses_manage}
|
||||
setRole={setSelectedRole}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
<Box $align="center" $justify="center" $css="flex: auto;">
|
||||
<Button
|
||||
color="primary"
|
||||
disabled={
|
||||
!selectedUsers.length ||
|
||||
isPending ||
|
||||
!selectedRole ||
|
||||
!doc.abilities.accesses_manage
|
||||
}
|
||||
onClick={() => void handleValidate()}
|
||||
style={{ height: '100%', maxHeight: '55px' }}
|
||||
>
|
||||
{t('Validate')}
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -1,37 +0,0 @@
|
||||
import { Select } from '@openfun/cunningham-react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Role, useTrans } from '@/features/docs/doc-management';
|
||||
|
||||
interface ChooseRoleProps {
|
||||
currentRole: Role;
|
||||
disabled: boolean;
|
||||
defaultRole?: Role;
|
||||
setRole: (role: Role) => void;
|
||||
label?: string;
|
||||
}
|
||||
|
||||
export const ChooseRole = ({
|
||||
defaultRole,
|
||||
disabled,
|
||||
currentRole,
|
||||
setRole,
|
||||
label,
|
||||
}: ChooseRoleProps) => {
|
||||
const { t } = useTranslation();
|
||||
const { transRole } = useTrans();
|
||||
|
||||
return (
|
||||
<Select
|
||||
label={label || t('Choose a role')}
|
||||
options={Object.values(Role).map((role) => ({
|
||||
label: transRole(role),
|
||||
value: role,
|
||||
disabled: currentRole !== Role.OWNER && role === Role.OWNER,
|
||||
}))}
|
||||
onChange={(evt) => setRole(evt.target.value as Role)}
|
||||
disabled={disabled}
|
||||
value={defaultRole}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,161 +0,0 @@
|
||||
import { useCallback, useMemo, useRef, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { InputActionMeta, Options } from 'react-select';
|
||||
import AsyncSelect from 'react-select/async';
|
||||
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Doc } from '@/features/docs/doc-management';
|
||||
import { isValidEmail } from '@/utils';
|
||||
|
||||
import { KEY_LIST_USER, useUsers } from '../api/useUsers';
|
||||
import { OptionSelect, OptionType } from '../types';
|
||||
|
||||
export type OptionsSelect = Options<OptionSelect>;
|
||||
|
||||
interface SearchUsersProps {
|
||||
doc: Doc;
|
||||
selectedUsers: OptionsSelect;
|
||||
setSelectedUsers: (value: OptionsSelect) => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export const SearchUsers = ({
|
||||
doc,
|
||||
selectedUsers,
|
||||
setSelectedUsers,
|
||||
disabled,
|
||||
}: SearchUsersProps) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { t } = useTranslation();
|
||||
const [input, setInput] = useState('');
|
||||
const [userQuery, setUserQuery] = useState('');
|
||||
const resolveOptionsRef = useRef<((value: OptionsSelect) => void) | null>(
|
||||
null,
|
||||
);
|
||||
const { data } = useUsers(
|
||||
{ query: userQuery, docId: doc.id },
|
||||
{
|
||||
enabled: !!userQuery,
|
||||
queryKey: [KEY_LIST_USER, { query: userQuery }],
|
||||
},
|
||||
);
|
||||
|
||||
const options = data?.results;
|
||||
|
||||
const optionsSelect = useMemo(() => {
|
||||
if (!resolveOptionsRef.current || !options) {
|
||||
return;
|
||||
}
|
||||
|
||||
const optionsFiltered = options.filter(
|
||||
(user) =>
|
||||
!selectedUsers?.find(
|
||||
(selectedUser) => selectedUser.value.email === user.email,
|
||||
),
|
||||
);
|
||||
|
||||
let users: OptionsSelect = optionsFiltered.map((user) => ({
|
||||
value: user,
|
||||
label: user.email,
|
||||
type: OptionType.NEW_MEMBER,
|
||||
}));
|
||||
|
||||
if (userQuery && isValidEmail(userQuery)) {
|
||||
const isFoundUser = !!optionsFiltered.find(
|
||||
(user) => user.email === userQuery,
|
||||
);
|
||||
const isFoundEmail = !!selectedUsers.find(
|
||||
(selectedUser) => selectedUser.value.email === userQuery,
|
||||
);
|
||||
|
||||
if (!isFoundUser && !isFoundEmail) {
|
||||
users = [
|
||||
...users,
|
||||
{
|
||||
value: { email: userQuery },
|
||||
label: userQuery,
|
||||
type: OptionType.INVITATION,
|
||||
},
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
resolveOptionsRef.current(users);
|
||||
resolveOptionsRef.current = null;
|
||||
|
||||
return users;
|
||||
}, [options, selectedUsers, userQuery]);
|
||||
|
||||
const loadOptions = (): Promise<OptionsSelect> => {
|
||||
return new Promise<OptionsSelect>((resolve) => {
|
||||
resolveOptionsRef.current = resolve;
|
||||
});
|
||||
};
|
||||
|
||||
const timeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const onInputChangeHandle = useCallback(
|
||||
(newValue: string, actionMeta: InputActionMeta) => {
|
||||
if (
|
||||
actionMeta.action === 'input-blur' ||
|
||||
actionMeta.action === 'menu-close'
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
setInput(newValue);
|
||||
if (timeout.current) {
|
||||
clearTimeout(timeout.current);
|
||||
}
|
||||
|
||||
timeout.current = setTimeout(() => {
|
||||
setUserQuery(newValue);
|
||||
}, 1000);
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<AsyncSelect
|
||||
styles={{
|
||||
placeholder: (base) => ({
|
||||
...base,
|
||||
fontSize: '14px',
|
||||
color: disabled
|
||||
? colorsTokens()['greyscale-300']
|
||||
: colorsTokens()['primary-600'],
|
||||
}),
|
||||
control: (base) => ({
|
||||
...base,
|
||||
minHeight: '45px',
|
||||
borderColor: disabled
|
||||
? colorsTokens()['greyscale-300']
|
||||
: colorsTokens()['primary-600'],
|
||||
backgroundColor: 'white',
|
||||
}),
|
||||
input: (base) => ({
|
||||
...base,
|
||||
minHeight: '45px',
|
||||
fontSize: '14px',
|
||||
}),
|
||||
}}
|
||||
isDisabled={disabled}
|
||||
aria-label={t('Find a member to add to the document')}
|
||||
isMulti
|
||||
loadOptions={loadOptions}
|
||||
defaultOptions={optionsSelect}
|
||||
onInputChange={onInputChangeHandle}
|
||||
inputValue={input}
|
||||
placeholder={t('Search by email')}
|
||||
noOptionsMessage={() =>
|
||||
input
|
||||
? t("We didn't find a mail matching, try to be more accurate")
|
||||
: t('Invite new members to {{title}}', { title: doc.title })
|
||||
}
|
||||
onChange={(value) => {
|
||||
setInput('');
|
||||
setUserQuery('');
|
||||
setSelectedUsers(value);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './AddMembers';
|
||||
export * from './ChooseRole';
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './api';
|
||||
export * from './components';
|
||||
@@ -1,3 +0,0 @@
|
||||
export * from './useDeleteDocAccess';
|
||||
export * from './useDocAccesses';
|
||||
export * from './useUpdateDocAccess';
|
||||
@@ -1,166 +0,0 @@
|
||||
import {
|
||||
Alert,
|
||||
Button,
|
||||
Loader,
|
||||
VariantType,
|
||||
useToastProvider,
|
||||
} from '@openfun/cunningham-react';
|
||||
import { useRouter } from 'next/router';
|
||||
import React, { useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, IconBG, Text, TextErrors } from '@/components';
|
||||
import { Access, Doc, Role } from '@/features/docs/doc-management';
|
||||
import { ChooseRole } from '@/features/docs/members/members-add/';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useDeleteDocAccess, useUpdateDocAccess } from '../api';
|
||||
import { useWhoAmI } from '../hooks/useWhoAmI';
|
||||
|
||||
interface MemberItemProps {
|
||||
role: Role;
|
||||
currentRole: Role;
|
||||
access: Access;
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const MemberItem = ({
|
||||
doc,
|
||||
role,
|
||||
access,
|
||||
currentRole,
|
||||
}: MemberItemProps) => {
|
||||
const { isMyself, isLastOwner, isOtherOwner } = useWhoAmI(access);
|
||||
const { t } = useTranslation();
|
||||
const { isSmallMobile, screenWidth } = useResponsiveStore();
|
||||
const [localRole, setLocalRole] = useState(role);
|
||||
const { toast } = useToastProvider();
|
||||
const { push } = useRouter();
|
||||
const { mutate: updateDocAccess, error: errorUpdate } = useUpdateDocAccess({
|
||||
onSuccess: () => {
|
||||
toast(t('The role has been updated'), VariantType.SUCCESS, {
|
||||
duration: 4000,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const { mutate: removeDocAccess, error: errorDelete } = useDeleteDocAccess({
|
||||
onSuccess: () => {
|
||||
toast(
|
||||
t('The member has been removed from the document'),
|
||||
VariantType.SUCCESS,
|
||||
{
|
||||
duration: 4000,
|
||||
},
|
||||
);
|
||||
|
||||
if (isMyself) {
|
||||
void push('/');
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const isNotAllowed =
|
||||
isOtherOwner || isLastOwner || !doc.abilities.accesses_manage;
|
||||
|
||||
if (!access.user) {
|
||||
return (
|
||||
<Box className="m-auto">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Box $width="100%">
|
||||
<Box $direction="row" $gap="1rem" $wrap="wrap">
|
||||
<Box
|
||||
$align="center"
|
||||
$direction="row"
|
||||
$gap="1rem"
|
||||
$justify="space-between"
|
||||
$width="100%"
|
||||
$wrap="wrap"
|
||||
$css={`flex: ${isSmallMobile ? '100%' : '70%'};`}
|
||||
>
|
||||
<IconBG iconName="account_circle" $size="2rem" />
|
||||
<Box $justify="center" $css="flex:1;">
|
||||
{access.user.full_name && <Text>{access.user.full_name}</Text>}
|
||||
<Text>{access.user.email}</Text>
|
||||
</Box>
|
||||
<Box
|
||||
$direction="row"
|
||||
$gap="1rem"
|
||||
$align="center"
|
||||
$justify="space-between"
|
||||
$css="flex:1;"
|
||||
$wrap={screenWidth < 400 ? 'wrap' : 'nowrap'}
|
||||
>
|
||||
<Box $minWidth="13rem" $css={isSmallMobile ? 'flex:1;' : ''}>
|
||||
<ChooseRole
|
||||
label={t('Role')}
|
||||
defaultRole={localRole}
|
||||
currentRole={currentRole}
|
||||
disabled={isNotAllowed}
|
||||
setRole={(role) => {
|
||||
setLocalRole(role);
|
||||
updateDocAccess({
|
||||
docId: doc.id,
|
||||
accessId: access.id,
|
||||
role,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
{doc.abilities.accesses_manage && (
|
||||
<Box $margin={isSmallMobile ? 'auto' : ''}>
|
||||
<Button
|
||||
color="tertiary-text"
|
||||
icon={
|
||||
<Text $isMaterialIcon $color="inherit">
|
||||
delete
|
||||
</Text>
|
||||
}
|
||||
disabled={isNotAllowed}
|
||||
onClick={() =>
|
||||
removeDocAccess({ docId: doc.id, accessId: access.id })
|
||||
}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
</Box>
|
||||
{(errorUpdate || errorDelete) && (
|
||||
<Box $margin={{ top: 'tiny' }}>
|
||||
<TextErrors causes={errorUpdate?.cause || errorDelete?.cause} />
|
||||
</Box>
|
||||
)}
|
||||
{(isLastOwner || isOtherOwner) && doc.abilities.accesses_manage && (
|
||||
<Box $margin={{ top: 'tiny' }}>
|
||||
<Alert
|
||||
canClose={false}
|
||||
type={VariantType.WARNING}
|
||||
icon={
|
||||
<Text className="material-icons" $theme="warning">
|
||||
warning
|
||||
</Text>
|
||||
}
|
||||
>
|
||||
{isLastOwner && (
|
||||
<Box $direction="column" $gap="0.2rem">
|
||||
<Text $theme="warning">
|
||||
{t(
|
||||
'You are the sole owner of this group, make another member the group owner before you can change your own role or be removed from your document.',
|
||||
)}
|
||||
</Text>
|
||||
</Box>
|
||||
)}
|
||||
{isOtherOwner &&
|
||||
t('You cannot update the role or remove other owner.')}
|
||||
</Alert>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,125 +0,0 @@
|
||||
import { Loader } from '@openfun/cunningham-react';
|
||||
import React, { useMemo, useRef } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { APIError } from '@/api';
|
||||
import { Box, Card, InfiniteScroll, TextErrors } from '@/components';
|
||||
import { useCunninghamTheme } from '@/cunningham';
|
||||
import { Access, Doc, currentDocRole } from '@/features/docs/doc-management';
|
||||
import { useResponsiveStore } from '@/stores';
|
||||
|
||||
import { useDocAccessesInfinite } from '../api';
|
||||
|
||||
import { MemberItem } from './MemberItem';
|
||||
|
||||
interface MemberListStateProps {
|
||||
isLoading: boolean;
|
||||
error: APIError | null;
|
||||
accesses?: Access[];
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
const MemberListState = ({
|
||||
accesses,
|
||||
error,
|
||||
isLoading,
|
||||
doc,
|
||||
}: MemberListStateProps) => {
|
||||
const { colorsTokens } = useCunninghamTheme();
|
||||
const { isSmallMobile } = useResponsiveStore();
|
||||
|
||||
if (error) {
|
||||
return <TextErrors causes={error.cause} />;
|
||||
}
|
||||
|
||||
if (isLoading || !accesses) {
|
||||
return (
|
||||
<Box $align="center" className="m-l">
|
||||
<Loader />
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return accesses?.map((access, index) => {
|
||||
if (!access.user) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box
|
||||
key={`${access.id}-${index}`}
|
||||
$background={!(index % 2) ? 'white' : colorsTokens()['greyscale-000']}
|
||||
$direction="row"
|
||||
$padding={isSmallMobile ? 'tiny' : 'small'}
|
||||
$align="center"
|
||||
$gap="1rem"
|
||||
$radius="4px"
|
||||
as="li"
|
||||
>
|
||||
<MemberItem
|
||||
access={access}
|
||||
role={access.role}
|
||||
doc={doc}
|
||||
currentRole={currentDocRole(doc.abilities)}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
interface MemberListProps {
|
||||
doc: Doc;
|
||||
}
|
||||
|
||||
export const MemberList = ({ doc }: MemberListProps) => {
|
||||
const { t } = useTranslation();
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const {
|
||||
data,
|
||||
isLoading,
|
||||
error,
|
||||
fetchNextPage,
|
||||
hasNextPage,
|
||||
isFetchingNextPage,
|
||||
} = useDocAccessesInfinite({
|
||||
docId: doc.id,
|
||||
});
|
||||
|
||||
const accesses = useMemo(() => {
|
||||
return data?.pages.reduce((acc, page) => {
|
||||
return acc.concat(page.results);
|
||||
}, [] as Access[]);
|
||||
}, [data?.pages]);
|
||||
|
||||
return (
|
||||
<Card
|
||||
$margin="tiny"
|
||||
$overflow="auto"
|
||||
$maxHeight="80vh"
|
||||
$padding="tiny"
|
||||
aria-label={t('List members card')}
|
||||
>
|
||||
<Box ref={containerRef} $overflow="auto">
|
||||
<InfiniteScroll
|
||||
hasMore={hasNextPage}
|
||||
isLoading={isFetchingNextPage}
|
||||
next={() => {
|
||||
void fetchNextPage();
|
||||
}}
|
||||
scrollContainer={containerRef.current}
|
||||
as="ul"
|
||||
$padding="none"
|
||||
$margin="none"
|
||||
role="listbox"
|
||||
>
|
||||
<MemberListState
|
||||
isLoading={isLoading}
|
||||
error={error}
|
||||
accesses={accesses}
|
||||
doc={doc}
|
||||
/>
|
||||
</InfiniteScroll>
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
@@ -1 +0,0 @@
|
||||
export * from './MemberList';
|
||||
@@ -1 +0,0 @@
|
||||
export const PAGE_SIZE = 20;
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from './api';
|
||||
export * from './components';
|
||||
Reference in New Issue
Block a user