🔥(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 './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,
|
KEY_LIST_DOC,
|
||||||
Role,
|
Role,
|
||||||
} from '@/features/docs/doc-management';
|
} 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 { ContentLanguage } from '@/i18n/types';
|
||||||
import { useBroadcastStore } from '@/stores';
|
import { useBroadcastStore } from '@/stores';
|
||||||
|
|
||||||
@@ -3,11 +3,9 @@ import { useMutation, useQueryClient } from '@tanstack/react-query';
|
|||||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||||
import { User } from '@/core/auth';
|
import { User } from '@/core/auth';
|
||||||
import { Doc, Role } from '@/features/docs/doc-management';
|
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 { ContentLanguage } from '@/i18n/types';
|
||||||
|
|
||||||
import { Invitation } from '../types';
|
|
||||||
|
|
||||||
import { KEY_LIST_DOC_INVITATIONS } from './useDocInvitations';
|
import { KEY_LIST_DOC_INVITATIONS } from './useDocInvitations';
|
||||||
|
|
||||||
interface CreateDocInvitationParams {
|
interface CreateDocInvitationParams {
|
||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
|
|
||||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||||
import { KEY_DOC, KEY_LIST_DOC } from '@/features/docs/doc-management';
|
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 { useBroadcastStore } from '@/stores';
|
||||||
|
|
||||||
import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses';
|
import { KEY_LIST_DOC_ACCESSES } from './useDocAccesses';
|
||||||
@@ -8,8 +8,7 @@ import {
|
|||||||
} from '@tanstack/react-query';
|
} from '@tanstack/react-query';
|
||||||
|
|
||||||
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
|
import { APIError, APIList, errorCauses, fetchAPI } from '@/api';
|
||||||
|
import { Invitation } from '@/features/docs/doc-share/types';
|
||||||
import { Invitation } from '../types';
|
|
||||||
|
|
||||||
export type DocInvitationsParams = {
|
export type DocInvitationsParams = {
|
||||||
docId: string;
|
docId: string;
|
||||||
@@ -6,8 +6,7 @@ import {
|
|||||||
|
|
||||||
import { APIError, errorCauses, fetchAPI } from '@/api';
|
import { APIError, errorCauses, fetchAPI } from '@/api';
|
||||||
import { Role } from '@/features/docs/doc-management';
|
import { Role } from '@/features/docs/doc-management';
|
||||||
|
import { Invitation } from '@/features/docs/doc-share/types';
|
||||||
import { Invitation } from '../types';
|
|
||||||
|
|
||||||
import { KEY_LIST_DOC_INVITATIONS } from './useDocInvitations';
|
import { KEY_LIST_DOC_INVITATIONS } from './useDocInvitations';
|
||||||
|
|
||||||
@@ -12,9 +12,11 @@ import { Box } from '@/components';
|
|||||||
import { User } from '@/core';
|
import { User } from '@/core';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
import { Doc, Role } from '@/features/docs';
|
import { Doc, Role } from '@/features/docs';
|
||||||
import { useCreateDocInvitation } from '@/features/docs/members/invitation-list';
|
import {
|
||||||
import { useCreateDocAccess } from '@/features/docs/members/members-add';
|
useCreateDocAccess,
|
||||||
import { OptionType } from '@/features/docs/members/members-add/types';
|
useCreateDocInvitation,
|
||||||
|
} from '@/features/docs/doc-share';
|
||||||
|
import { OptionType } from '@/features/docs/doc-share/types';
|
||||||
import { useLanguage } from '@/i18n/hooks/useLanguage';
|
import { useLanguage } from '@/i18n/hooks/useLanguage';
|
||||||
|
|
||||||
import { DocRoleDropdown } from './DocRoleDropdown';
|
import { DocRoleDropdown } from './DocRoleDropdown';
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import {
|
|||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { User } from '@/core';
|
import { User } from '@/core';
|
||||||
import { Doc, Role } from '@/features/docs/doc-management';
|
import { Doc, Role } from '@/features/docs/doc-management';
|
||||||
import { SearchUserRow } from '@/features/docs/doc-share/component/SearchUserRow';
|
|
||||||
import {
|
import {
|
||||||
useDeleteDocInvitation,
|
useDeleteDocInvitation,
|
||||||
useUpdateDocInvitation,
|
useUpdateDocInvitation,
|
||||||
} from '@/features/docs/members/invitation-list';
|
} from '@/features/docs/doc-share';
|
||||||
import { Invitation } from '@/features/docs/members/invitation-list/types';
|
import { SearchUserRow } from '@/features/docs/doc-share/component/SearchUserRow';
|
||||||
|
import { Invitation } from '@/features/docs/doc-share/types';
|
||||||
|
|
||||||
import { DocRoleDropdown } from './DocRoleDropdown';
|
import { DocRoleDropdown } from './DocRoleDropdown';
|
||||||
|
|
||||||
|
|||||||
@@ -8,14 +8,11 @@ import {
|
|||||||
IconOptions,
|
IconOptions,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { SearchUserRow } from '@/features/docs/doc-share/component/SearchUserRow';
|
import { SearchUserRow } from '@/features/docs/doc-share/component/SearchUserRow';
|
||||||
import {
|
import { useWhoAmI } from '@/features/docs/doc-share/hooks/useWhoAmI';
|
||||||
useDeleteDocAccess,
|
|
||||||
useUpdateDocAccess,
|
|
||||||
} from '@/features/docs/members/members-list';
|
|
||||||
import { useWhoAmI } from '@/features/docs/members/members-list/hooks/useWhoAmI';
|
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
import { Access, Doc, Role } from '../../doc-management/types';
|
import { Access, Doc, Role } from '../../doc-management/types';
|
||||||
|
import { useDeleteDocAccess, useUpdateDocAccess } from '../index';
|
||||||
|
|
||||||
import { DocRoleDropdown } from './DocRoleDropdown';
|
import { DocRoleDropdown } from './DocRoleDropdown';
|
||||||
|
|
||||||
|
|||||||
@@ -13,10 +13,13 @@ import {
|
|||||||
import { QuickSearchGroup } from '@/components/quick-search/QuickSearchGroup';
|
import { QuickSearchGroup } from '@/components/quick-search/QuickSearchGroup';
|
||||||
import { User } from '@/core';
|
import { User } from '@/core';
|
||||||
import { Access, Doc } from '@/features/docs';
|
import { Access, Doc } from '@/features/docs';
|
||||||
import { useDocInvitationsInfinite } from '@/features/docs/members/invitation-list';
|
import {
|
||||||
import { Invitation } from '@/features/docs/members/invitation-list/types';
|
KEY_LIST_USER,
|
||||||
import { KEY_LIST_USER, useUsers } from '@/features/docs/members/members-add';
|
useDocAccessesInfinite,
|
||||||
import { useDocAccessesInfinite } from '@/features/docs/members/members-list';
|
useDocInvitationsInfinite,
|
||||||
|
useUsers,
|
||||||
|
} from '@/features/docs/doc-share';
|
||||||
|
import { Invitation } from '@/features/docs/doc-share/types';
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
import { isValidEmail } from '@/utils';
|
import { isValidEmail } from '@/utils';
|
||||||
|
|
||||||
|
|||||||
@@ -11,15 +11,16 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
import { useResponsiveStore } from '@/stores';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
Doc,
|
||||||
KEY_DOC,
|
KEY_DOC,
|
||||||
KEY_LIST_DOC,
|
KEY_LIST_DOC,
|
||||||
|
LinkReach,
|
||||||
|
LinkRole,
|
||||||
useUpdateDocLink,
|
useUpdateDocLink,
|
||||||
} from '../../doc-management/api';
|
} from '@/features/docs';
|
||||||
import { useTranslatedShareSettings } from '../hooks/useTranslatedShareSettings';
|
import { useTranslatedShareSettings } from '@/features/docs/doc-share';
|
||||||
import { Doc, LinkReach, LinkRole } from '../../doc-management/types';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
interface DocVisibilityProps {
|
interface DocVisibilityProps {
|
||||||
doc: Doc;
|
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 { 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 {
|
export enum OptionType {
|
||||||
INVITATION = 'invitation',
|
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