diff --git a/src/frontend/apps/impress/src/components/AlertModal.tsx b/src/frontend/apps/impress/src/components/AlertModal.tsx
new file mode 100644
index 00000000..9b591341
--- /dev/null
+++ b/src/frontend/apps/impress/src/components/AlertModal.tsx
@@ -0,0 +1,68 @@
+import { Button, Modal, ModalSize } from '@openfun/cunningham-react';
+import { ReactNode } from 'react';
+import { useTranslation } from 'react-i18next';
+
+import { Box } from './Box';
+import { Text } from './Text';
+
+export type AlertModalProps = {
+ description: ReactNode;
+ isOpen: boolean;
+ onClose: () => void;
+ onConfirm: () => void;
+ title: string;
+ cancelLabel?: string;
+ confirmLabel?: string;
+};
+
+export const AlertModal = ({
+ cancelLabel,
+ confirmLabel,
+ description,
+ isOpen,
+ onClose,
+ onConfirm,
+ title,
+}: AlertModalProps) => {
+ const { t } = useTranslation();
+ return (
+
+ {title}
+
+ }
+ rightActions={
+ <>
+
+
+ >
+ }
+ >
+
+
+ {description}
+
+
+
+ );
+};
diff --git a/src/frontend/apps/impress/src/components/index.ts b/src/frontend/apps/impress/src/components/index.ts
index c1a1314f..a0f256f5 100644
--- a/src/frontend/apps/impress/src/components/index.ts
+++ b/src/frontend/apps/impress/src/components/index.ts
@@ -1,3 +1,4 @@
+export * from './AlertModal';
export * from './Box';
export * from './BoxButton';
export * from './Card';
diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
index 5cbe3d02..762686ff 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocToolBox.tsx
@@ -277,7 +277,11 @@ export const DocToolBox = ({ doc }: DocToolBoxProps) => {
{modalShare.isOpen && (
- modalShare.close()} doc={doc} />
+ modalShare.close()}
+ doc={doc}
+ isRootDoc={treeContext?.root?.id === doc.id}
+ />
)}
{isModalExportOpen && ModalExport && (
setIsModalExportOpen(false)} doc={doc} />
diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalRemoveDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalRemoveDoc.tsx
index f83b1aff..fd7b038a 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalRemoveDoc.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-management/components/ModalRemoveDoc.tsx
@@ -5,14 +5,13 @@ import {
VariantType,
useToastProvider,
} from '@openfun/cunningham-react';
-import { t } from 'i18next';
import { usePathname } from 'next/navigation';
import { useRouter } from 'next/router';
+import { Trans, useTranslation } from 'react-i18next';
import { Box, Text, TextErrors } from '@/components';
import { useRemoveDoc } from '../api/useRemoveDoc';
-import { useTrans } from '../hooks';
import { Doc } from '../types';
interface ModalRemoveDocProps {
@@ -27,10 +26,9 @@ export const ModalRemoveDoc = ({
afterDelete,
}: ModalRemoveDocProps) => {
const { toast } = useToastProvider();
+ const { t } = useTranslation();
const { push } = useRouter();
const pathname = usePathname();
- const { untitledDocument } = useTrans();
-
const {
mutate: removeDoc,
isError,
@@ -82,7 +80,7 @@ export const ModalRemoveDoc = ({
>
}
- size={ModalSize.SMALL}
+ size={ModalSize.MEDIUM}
title={
{!isError && (
-
- {t('Are you sure you want to delete the document "{{title}}"?', {
- title: doc.title ?? untitledDocument,
- })}
-
+ <>
+
+
+ This document and any sub-documents will be
+ permanently deleted. This action is irreversible.
+
+
+ >
)}
{isError && }
diff --git a/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchModal.tsx b/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchModal.tsx
index 28a01767..c8f1dbd1 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchModal.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-search/components/DocSearchModal.tsx
@@ -74,7 +74,10 @@ export const DocSearchModal = ({
loading={loading}
onFilter={handleInputSearch}
>
-
+
{showFilters && (
=> {
+}: DocAccessesParams): Promise => {
let url = `documents/${docId}/accesses/`;
if (ordering) {
@@ -36,7 +33,7 @@ export const getDocAccesses = async ({
export const KEY_LIST_DOC_ACCESSES = 'docs-accesses';
export function useDocAccesses(
- params: DocAccessesAPIParams,
+ params: DocAccessesParams,
queryConfig?: UseQueryOptions,
) {
return useQuery({
diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocInheritedShareContent.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocInheritedShareContent.tsx
index 5d8aef9a..86175eda 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocInheritedShareContent.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocInheritedShareContent.tsx
@@ -1,91 +1,25 @@
-import { Button, Modal, ModalSize, useModal } from '@openfun/cunningham-react';
-import { Fragment, useMemo } from 'react';
+import { Button } from '@openfun/cunningham-react';
+import { Fragment } from 'react';
import { useTranslation } from 'react-i18next';
-import { createGlobalStyle } from 'styled-components';
-import { Box, StyledLink, Text } from '@/components';
+import { Box, HorizontalSeparator, Icon, StyledLink, Text } from '@/components';
import { useCunninghamTheme } from '@/cunningham';
-import {
- Access,
- RoleImportance,
- useDoc,
- useDocStore,
-} from '../../doc-management';
-import SimpleFileIcon from '../../docs-grid/assets/simple-document.svg';
+import { Access, useDocStore } from '../../doc-management';
-import { DocShareMemberItem } from './DocShareMemberItem';
-const ShareModalStyle = createGlobalStyle`
- .c__modal__title {
- padding-bottom: 0 !important;
- }
- .c__modal__scroller {
- padding: 15px 15px !important;
- }
-`;
+import { DocShareMemberItem } from './DocShareMember';
-type Props = {
+type DocInheritedShareContentProps = {
rawAccesses: Access[];
};
-const getMaxRoleBetweenAccesses = (access1: Access, access2: Access) => {
- const role1 = access1.max_role;
- const role2 = access2.max_role;
-
- const roleImportance1 = RoleImportance[role1];
- const roleImportance2 = RoleImportance[role2];
-
- return roleImportance1 > roleImportance2 ? role1 : role2;
-};
-
-export const DocInheritedShareContent = ({ rawAccesses }: Props) => {
+export const DocInheritedShareContent = ({
+ rawAccesses,
+}: DocInheritedShareContentProps) => {
const { t } = useTranslation();
const { spacingsTokens } = useCunninghamTheme();
const { currentDoc } = useDocStore();
- const inheritedData = useMemo(() => {
- if (!currentDoc || rawAccesses.length === 0) {
- return null;
- }
-
- let parentId = null;
- let parentPathLength = 0;
- const members: Access[] = [];
-
- // Find the parent document with the longest path that is different from currentDoc
- for (const access of rawAccesses) {
- const docPath = access.document.path;
-
- // Skip if it's the current document
- if (access.document.id === currentDoc.id) {
- continue;
- }
-
- const findIndex = members.findIndex(
- (member) => member.user.id === access.user.id,
- );
- if (findIndex === -1) {
- members.push(access);
- } else {
- const accessToUpdate = members[findIndex];
- const currentRole = accessToUpdate.max_role;
- const maxRole = getMaxRoleBetweenAccesses(accessToUpdate, access);
-
- if (maxRole !== currentRole) {
- members[findIndex] = access;
- }
- }
-
- // Check if this document has a longer path than our current candidate
- if (docPath && (!parentId || docPath.length > parentPathLength)) {
- parentId = access.document.id;
- parentPathLength = docPath.length;
- }
- }
-
- return { parentId, members };
- }, [currentDoc, rawAccesses]);
-
// Check if accesses map is empty
const hasAccesses = rawAccesses.length > 0;
@@ -94,113 +28,44 @@ export const DocInheritedShareContent = ({ rawAccesses }: Props) => {
}
return (
-
+
+
-
- {t('Inherited share')}
-
-
- {inheritedData && (
-
- )}
+
+
+ {t('People with access via the parent document')}
+
+
+
+
+ }
+ color="tertiary-text"
+ />
+
+
+
+ {rawAccesses.map((access) => (
+
+
+
+ ))}
);
};
-
-type DocInheritedShareContentItemProps = {
- accesses: Access[];
- document_id: string;
-};
-export const DocInheritedShareContentItem = ({
- accesses,
- document_id,
-}: DocInheritedShareContentItemProps) => {
- const { t } = useTranslation();
- const { spacingsTokens } = useCunninghamTheme();
- const { data: doc, error, isLoading } = useDoc({ id: document_id });
- const errorCode = error?.status;
-
- const accessModal = useModal();
- if ((!doc && !isLoading && !error) || (error && errorCode !== 403)) {
- return null;
- }
-
- return (
- <>
-
-
-
-
- {isLoading ? (
-
-
-
-
- ) : (
- <>
-
-
- {error && errorCode === 403
- ? t('You do not have permission to view this document')
- : (doc?.title ?? t('Untitled document'))}
-
-
-
- {t('Members of this page have access')}
-
- >
- )}
-
-
- {!isLoading && (
-
- )}
-
- {accessModal.isOpen && (
-
-
- {t('Access inherited from the parent page')}
-
-
- }
- size={ModalSize.MEDIUM}
- >
-
-
- {accesses.map((access) => (
-
-
-
- ))}
-
-
- )}
- >
- );
-};
diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocRoleDropdown.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocRoleDropdown.tsx
index b62db014..4f03f9c9 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocRoleDropdown.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocRoleDropdown.tsx
@@ -1,16 +1,30 @@
+import { VariantType, useToastProvider } from '@openfun/cunningham-react';
+import { useQueryClient } from '@tanstack/react-query';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
import { DropdownMenu, DropdownMenuOption, Text } from '@/components';
-import { Role, useTrans } from '@/docs/doc-management/';
+import {
+ Access,
+ Doc,
+ KEY_SUB_PAGE,
+ Role,
+ useTrans,
+} from '@/docs/doc-management/';
+
+import { useDeleteDocAccess, useDeleteDocInvitation } from '../api';
+import { Invitation, isInvitation } from '../types';
type DocRoleDropdownProps = {
+ doc?: Doc;
+ access?: Access | Invitation;
canUpdate?: boolean;
currentRole: Role;
message?: string;
onSelectRole: (role: Role) => void;
rolesAllowed?: Role[];
+ isLastOwner?: boolean;
};
export const DocRoleDropdown = ({
@@ -18,10 +32,65 @@ export const DocRoleDropdown = ({
currentRole,
message,
onSelectRole,
+ doc,
rolesAllowed,
+ access,
+ isLastOwner = false,
}: DocRoleDropdownProps) => {
const { t } = useTranslation();
const { transRole, translatedRoles } = useTrans();
+ const queryClient = useQueryClient();
+ const { toast } = useToastProvider();
+
+ const { mutate: removeDocInvitation } = useDeleteDocInvitation({
+ onSuccess: () => {
+ if (!doc) {
+ return;
+ }
+
+ void queryClient.invalidateQueries({
+ queryKey: [KEY_SUB_PAGE, { id: doc.id }],
+ });
+ },
+ onError: (error) => {
+ toast(
+ error?.data?.role?.[0] ?? t('Error during delete invitation'),
+ VariantType.ERROR,
+ {
+ duration: 4000,
+ },
+ );
+ },
+ });
+
+ const { mutate: removeDocAccess } = useDeleteDocAccess({
+ onSuccess: () => {
+ if (!doc) {
+ return;
+ }
+ void queryClient.invalidateQueries({
+ queryKey: [KEY_SUB_PAGE, { id: doc.id }],
+ });
+ },
+ onError: () => {
+ toast(t('Error while deleting invitation'), VariantType.ERROR, {
+ duration: 4000,
+ });
+ },
+ });
+
+ const onRemove = () => {
+ const invitation = isInvitation(access);
+ if (!doc || !access) {
+ return;
+ }
+
+ if (invitation) {
+ removeDocInvitation({ invitationId: access.id, docId: doc.id });
+ } else {
+ removeDocAccess({ accessId: access.id, docId: doc.id });
+ }
+ };
/**
* When there is a higher role, the rolesAllowed are truncated
@@ -44,14 +113,18 @@ export const DocRoleDropdown = ({
}, [canUpdate, rolesAllowed, translatedRoles, message, t]);
const roles: DropdownMenuOption[] = Object.keys(translatedRoles).map(
- (key) => {
+ (key, index) => {
+ const isLast = index === Object.keys(translatedRoles).length - 1;
return {
label: transRole(key as Role),
callback: () => onSelectRole?.(key as Role),
isSelected: currentRole === (key as Role),
+ showSeparator: isLast,
+ disabled: isLastOwner && key !== 'owner',
};
},
);
+
if (!canUpdate) {
return (
@@ -59,15 +132,27 @@ export const DocRoleDropdown = ({
);
}
+
return (
{canUpdate && (
diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMember.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMember.tsx
index da2196eb..c258a3b8 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMember.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMember.tsx
@@ -3,19 +3,14 @@ import { useQueryClient } from '@tanstack/react-query';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
-import {
- Box,
- DropdownMenu,
- DropdownMenuOption,
- IconOptions,
-} from '@/components';
-import { QuickSearchData, QuickSearchGroup } from '@/components/quick-search';
+import { Box } from '@/components';
+import { QuickSearchData } from '@/components/quick-search';
+import { QuickSearchGroup } from '@/components/quick-search/QuickSearchGroup';
import { useCunninghamTheme } from '@/cunningham';
import { Access, Doc, KEY_SUB_PAGE, Role } from '@/docs/doc-management/';
-import { useResponsiveStore } from '@/stores';
-import { useDeleteDocAccess, useDocAccesses, useUpdateDocAccess } from '../api';
-import { useWhoAmI } from '../hooks';
+import { useDocAccesses, useUpdateDocAccess } from '../api';
+import { useWhoAmI } from '../hooks/';
import { DocRoleDropdown } from './DocRoleDropdown';
import { SearchUserRow } from './SearchUserRow';
@@ -35,7 +30,6 @@ export const DocShareMemberItem = ({
const { isLastOwner } = useWhoAmI(access);
const { toast } = useToastProvider();
- const { isDesktop } = useResponsiveStore();
const { spacingsTokens } = useCunninghamTheme();
const message = isLastOwner
@@ -60,22 +54,6 @@ export const DocShareMemberItem = ({
},
});
- const { mutate: removeDocAccess } = useDeleteDocAccess({
- onSuccess: () => {
- if (!doc) {
- return;
- }
- void queryClient.invalidateQueries({
- queryKey: [KEY_SUB_PAGE, { id: doc.id }],
- });
- },
- onError: () => {
- toast(t('Error while deleting the member.'), VariantType.ERROR, {
- duration: 4000,
- });
- },
- });
-
const onUpdate = (newRole: Role) => {
if (!doc) {
return;
@@ -87,22 +65,6 @@ export const DocShareMemberItem = ({
});
};
- const onRemove = () => {
- if (!doc) {
- return;
- }
- removeDocAccess({ accessId: access.id, docId: doc.id });
- };
-
- const moreActions: DropdownMenuOption[] = [
- {
- label: t('Delete'),
- icon: 'delete',
- callback: onRemove,
- disabled: !access.abilities.destroy,
- },
- ];
-
const canUpdate = isInherited ? false : !!doc?.abilities.accesses_manage;
return (
@@ -119,20 +81,13 @@ export const DocShareMemberItem = ({
-
- {isDesktop && canUpdate && (
-
-
-
- )}
}
/>
@@ -170,7 +125,7 @@ export const QuickSearchGroupMember = ({
}, [membersQuery, t]);
return (
-
+
(
diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx
index 5c92705c..eda2b4a5 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModal.tsx
@@ -41,10 +41,11 @@ const ShareModalStyle = createGlobalStyle`
type Props = {
doc: Doc;
+ isRootDoc?: boolean;
onClose: () => void;
};
-export const DocShareModal = ({ doc, onClose }: Props) => {
+export const DocShareModal = ({ doc, onClose, isRootDoc = true }: Props) => {
const { t } = useTranslation();
const selectedUsersRef = useRef(null);
@@ -58,7 +59,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
const [inputValue, setInputValue] = useState('');
const [listHeight, setListHeight] = useState('400px');
- const canShare = doc.abilities.accesses_manage;
+ const canShare = doc.abilities.accesses_manage && isRootDoc;
const canViewAccesses = doc.abilities.accesses_view;
const showMemberSection = inputValue === '' && selectedUsers.length === 0;
const showFooter = selectedUsers.length === 0 && !inputValue;
@@ -114,8 +115,6 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
);
}, [membersQuery, doc.id]);
- const isRootDoc = false;
-
const showInheritedShareContent =
inheritedAccesses.length > 0 && showMemberSection && !isRootDoc;
@@ -149,10 +148,7 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
>
{canShare && selectedUsers.length > 0 && (
-
+
{
/>
)}
- {!canViewAccesses && }
+ {!canViewAccesses && }
@@ -213,13 +209,15 @@ export const DocShareModal = ({ doc, onClose }: Props) => {
}
/>
)}
- {showMemberSection ? (
- <>
+ {showMemberSection && isRootDoc && (
+
- >
- ) : (
+
+ )}
+
+ {!showMemberSection && canShare && (
{
- {showFooter && }
+ {showFooter && (
+
+ )}
diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModalFooter.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModalFooter.tsx
index 6930a68f..587e6f45 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModalFooter.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareModalFooter.tsx
@@ -10,9 +10,14 @@ import { DocVisibility } from './DocVisibility';
type Props = {
doc: Doc;
onClose: () => void;
+ canEditVisibility?: boolean;
};
-export const DocShareModalFooter = ({ doc, onClose }: Props) => {
+export const DocShareModalFooter = ({
+ doc,
+ onClose,
+ canEditVisibility = true,
+}: Props) => {
const copyDocLink = useCopyDocLink(doc.id);
const { t } = useTranslation();
return (
@@ -22,10 +27,10 @@ export const DocShareModalFooter = ({ doc, onClose }: Props) => {
`}
className="--docs--doc-share-modal-footer"
>
-
+
-
-
+
+
{
+export const DocVisibility = ({ doc, canEdit = true }: DocVisibilityProps) => {
const { t } = useTranslation();
const { toast } = useToastProvider();
const { isDesktop } = useResponsiveStore();
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
- const canManage = doc.abilities.accesses_manage;
+ const canManage = doc.abilities.accesses_manage && canEdit;
const [linkReach, setLinkReach] = useState(getDocLinkReach(doc));
const [docLinkRole, setDocLinkRole] = useState(
doc.computed_link_role ?? LinkRole.READER,
diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/types.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/types.tsx
index f79fdb28..aab99806 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-share/types.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-share/types.tsx
@@ -17,6 +17,14 @@ export interface Invitation {
};
}
+/**
+ * Type guard to check if an object is an Invitation
+ * Invitation has unique properties: email, issuer, is_expired, and document as a string
+ */
+export const isInvitation = (obj: unknown): obj is Invitation => {
+ return obj !== null && typeof obj === 'object' && 'issuer' in obj;
+};
+
export enum OptionType {
INVITATION = 'invitation',
NEW_MEMBER = 'new_member',
diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx
index 8fae3131..9fae9746 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocSubPageItem.tsx
@@ -16,6 +16,7 @@ import {
useTrans,
} from '@/features/docs/doc-management';
import { useLeftPanelStore } from '@/features/left-panel';
+import { useResponsiveStore } from '@/stores';
import Logo from './../assets/sub-page-logo.svg';
import { DocTreeItemActions } from './DocTreeItemActions';
@@ -37,7 +38,8 @@ export const DocSubPageItem = (props: Props) => {
const { untitledDocument } = useTrans();
const { node } = props;
const { spacingsTokens } = useCunninghamTheme();
- const [isHover, setIsHover] = useState(false);
+ const { isDesktop } = useResponsiveStore();
+ const [actionsOpen, setActionsOpen] = useState(false);
const router = useRouter();
const { togglePanel } = useLeftPanelStore();
@@ -97,11 +99,22 @@ export const DocSubPageItem = (props: Props) => {
return (
setIsHover(true)}
- onMouseLeave={() => setIsHover(false)}
$css={css`
- &:not(:has(.isSelected)):has(.light-doc-item-actions) {
+ background-color: ${actionsOpen
+ ? 'var(--c--theme--colors--greyscale-100)'
+ : 'var(--c--theme--colors--greyscale-000)'};
+
+ .light-doc-item-actions {
+ display: ${actionsOpen || !isDesktop ? 'flex' : 'none'};
+ }
+
+ &:hover {
background-color: var(--c--theme--colors--greyscale-100);
+ border-radius: 4px;
+
+ .light-doc-item-actions {
+ display: flex;
+ }
}
`}
>
@@ -150,19 +163,19 @@ export const DocSubPageItem = (props: Props) => {
)}
- {isHover && (
-
-
-
- )}
+
+
+
diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTree.tsx b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTree.tsx
index 1d35afb7..fdd520c2 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTree.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTree.tsx
@@ -24,6 +24,7 @@ type DocTreeProps = {
};
export const DocTree = ({ initialTargetId }: DocTreeProps) => {
const { spacingsTokens } = useCunninghamTheme();
+ const [rootActionsOpen, setRootActionsOpen] = useState(false);
const treeContext = useTreeContext();
const { currentDoc } = useDocStore();
const router = useRouter();
@@ -145,11 +146,12 @@ export const DocTree = ({ initialTargetId }: DocTreeProps) => {
`}
>
{
.doc-tree-root-item-actions {
display: 'flex';
- opacity: 0;
+ opacity: ${rootActionsOpen ? '1' : '0'};
&:has(.isOpen) {
opacity: 1;
@@ -201,6 +203,8 @@ export const DocTree = ({ initialTargetId }: DocTreeProps) => {
};
treeContext?.treeData.addChild(null, newDoc);
}}
+ isOpen={rootActionsOpen}
+ onOpenChange={setRootActionsOpen}
/>
diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTreeItemActions.tsx b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTreeItemActions.tsx
index 092d3a2a..add3753c 100644
--- a/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTreeItemActions.tsx
+++ b/src/frontend/apps/impress/src/features/docs/doc-tree/components/DocTreeItemActions.tsx
@@ -5,7 +5,7 @@ import {
} from '@gouvfr-lasuite/ui-kit';
import { useModal } from '@openfun/cunningham-react';
import { useRouter } from 'next/router';
-import { Fragment, useState } from 'react';
+import { Fragment } from 'react';
import { useTranslation } from 'react-i18next';
import { css } from 'styled-components';
@@ -21,14 +21,17 @@ type DocTreeItemActionsProps = {
doc: Doc;
parentId?: string | null;
onCreateSuccess?: (newDoc: Doc) => void;
+ isOpen?: boolean;
+ onOpenChange?: (isOpen: boolean) => void;
};
export const DocTreeItemActions = ({
doc,
parentId,
onCreateSuccess,
+ isOpen,
+ onOpenChange,
}: DocTreeItemActionsProps) => {
- const [isOpen, setIsOpen] = useState(false);
const router = useRouter();
const { t } = useTranslation();
const deleteModal = useModal();
@@ -66,7 +69,7 @@ export const DocTreeItemActions = ({
...(!isCurrentParent
? [
{
- label: t('Convert to doc'),
+ label: t('Move to my docs'),
isDisabled: !doc.abilities.move,
icon: (
{
onCreateSuccess?.(newDoc);
+ void router.push(`/docs/${newDoc.id}`);
},
});
@@ -118,13 +122,13 @@ export const DocTreeItemActions = ({
{
e.stopPropagation();
e.preventDefault();
- setIsOpen(!isOpen);
+ onOpenChange?.(!isOpen);
}}
iconName="more_horiz"
variant="filled"
diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocGridContentList.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocGridContentList.tsx
index 561d16e0..0c47564e 100644
--- a/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocGridContentList.tsx
+++ b/src/frontend/apps/impress/src/features/docs/docs-grid/components/DocGridContentList.tsx
@@ -1,15 +1,22 @@
import { DndContext, DragOverlay, Modifier } from '@dnd-kit/core';
import { getEventCoordinates } from '@dnd-kit/utilities';
import { TreeViewMoveModeEnum } from '@gouvfr-lasuite/ui-kit';
+import { useModal } from '@openfun/cunningham-react';
import { useQueryClient } from '@tanstack/react-query';
-import { useMemo } from 'react';
+import { useMemo, useRef } from 'react';
import { useTranslation } from 'react-i18next';
-import { Box, Text } from '@/components';
+import { AlertModal, Box, Text } from '@/components';
import { Doc, KEY_LIST_DOC } from '@/docs/doc-management';
+import {
+ getDocAccesses,
+ getDocInvitations,
+ useDeleteDocAccess,
+ useDeleteDocInvitation,
+} from '@/docs/doc-share';
import { useMoveDoc } from '@/docs/doc-tree';
-import { useDragAndDrop } from '../hooks/useDragAndDrop';
+import { DocDragEndData, useDragAndDrop } from '../hooks/useDragAndDrop';
import { DocsGridItem } from './DocsGridItem';
import { Draggable } from './Draggable';
@@ -45,23 +52,75 @@ type DocGridContentListProps = {
};
export const DocGridContentList = ({ docs }: DocGridContentListProps) => {
- const { mutate: handleMove, isError } = useMoveDoc();
+ const { mutateAsync: handleMove, isError } = useMoveDoc();
const queryClient = useQueryClient();
- const onDrag = (sourceDocumentId: string, targetDocumentId: string) =>
- handleMove(
- {
+ const modalConfirmation = useModal();
+ const { mutate: handleDeleteInvitation } = useDeleteDocInvitation();
+ const { mutate: handleDeleteAccess } = useDeleteDocAccess();
+ const onDragData = useRef(null);
+
+ const handleMoveDoc = async () => {
+ if (!onDragData.current) {
+ return;
+ }
+
+ const { sourceDocumentId, targetDocumentId } = onDragData.current;
+ modalConfirmation.onClose();
+ if (!sourceDocumentId || !targetDocumentId) {
+ onDragData.current = null;
+
+ return;
+ }
+
+ try {
+ await handleMove({
sourceDocumentId,
targetDocumentId,
position: TreeViewMoveModeEnum.FIRST_CHILD,
- },
- {
- onSuccess: () => {
- void queryClient.invalidateQueries({
- queryKey: [KEY_LIST_DOC],
- });
- },
- },
- );
+ });
+
+ void queryClient.invalidateQueries({
+ queryKey: [KEY_LIST_DOC],
+ });
+ const accesses = await getDocAccesses({
+ docId: sourceDocumentId,
+ });
+
+ const invitationsResponse = await getDocInvitations({
+ docId: sourceDocumentId,
+ page: 1,
+ });
+
+ const invitations = invitationsResponse.results;
+
+ await Promise.all([
+ ...invitations.map((invitation) =>
+ handleDeleteInvitation({
+ docId: sourceDocumentId,
+ invitationId: invitation.id,
+ }),
+ ),
+ ...accesses.map((access) =>
+ handleDeleteAccess({
+ docId: sourceDocumentId,
+ accessId: access.id,
+ }),
+ ),
+ ]);
+ } finally {
+ onDragData.current = null;
+ }
+ };
+
+ const onDrag = (data: DocDragEndData) => {
+ onDragData.current = data;
+ if (data.source.nb_accesses_direct <= 1) {
+ void handleMoveDoc();
+ return;
+ }
+
+ modalConfirmation.open();
+ };
const {
selectedDoc,
@@ -105,37 +164,62 @@ export const DocGridContentList = ({ docs }: DocGridContentListProps) => {
}
return (
-
- {docs.map((doc) => (
-
+
+ {docs.map((doc) => (
+
+ ))}
+
+
+
+ {overlayText}
+
+
+
+
+ {modalConfirmation.isOpen && (
+ {{targetDocumentTitle}}, it will lose its current access rights and inherit the permissions of that document. This access change cannot be undone.',
+ {
+ targetDocumentTitle:
+ onDragData.current?.target.title ?? t('Unnamed document'),
+ },
+ ),
+ }}
+ />
+ }
+ confirmLabel={t('Move')}
+ onConfirm={() => {
+ void handleMoveDoc();
+ }}
/>
- ))}
-
-
-
- {overlayText}
-
-
-
-
+ )}
+ >
);
};
diff --git a/src/frontend/apps/impress/src/features/docs/docs-grid/hooks/useDragAndDrop.tsx b/src/frontend/apps/impress/src/features/docs/docs-grid/hooks/useDragAndDrop.tsx
index f89c3ece..68b84425 100644
--- a/src/frontend/apps/impress/src/features/docs/docs-grid/hooks/useDragAndDrop.tsx
+++ b/src/frontend/apps/impress/src/features/docs/docs-grid/hooks/useDragAndDrop.tsx
@@ -11,13 +11,18 @@ import { useState } from 'react';
import { Doc } from '@/docs/doc-management';
+export type DocDragEndData = {
+ sourceDocumentId: string;
+ targetDocumentId: string;
+ source: Doc;
+ target: Doc;
+};
+
const activationConstraint = {
distance: 20,
};
-export function useDragAndDrop(
- onDrag: (sourceDocumentId: string, targetDocumentId: string) => void,
-) {
+export function useDragAndDrop(onDrag: (data: DocDragEndData) => void) {
const [selectedDoc, setSelectedDoc] = useState();
const [canDrop, setCanDrop] = useState();
@@ -49,7 +54,12 @@ export function useDragAndDrop(
return;
}
- onDrag(active.id as string, over.id as string);
+ onDrag({
+ sourceDocumentId: active.id as string,
+ targetDocumentId: over.id as string,
+ source: active.data.current as Doc,
+ target: over.data.current as Doc,
+ });
};
const updateCanDrop = (docCanDrop: boolean, isOver: boolean) => {
diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeaderButton.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeaderButton.tsx
index ac5bf169..fbf0a937 100644
--- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeaderButton.tsx
+++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeaderButton.tsx
@@ -1,25 +1,13 @@
-import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
import { Button } from '@openfun/cunningham-react';
import { useRouter } from 'next/router';
import { useTranslation } from 'react-i18next';
-import { Doc, useCreateDoc, useDocStore } from '@/docs/doc-management';
-import { isOwnerOrAdmin, useCreateChildrenDoc } from '@/features/docs/doc-tree';
+import { Icon } from '@/components';
+import { useCreateDoc } from '@/features/docs/doc-management';
import { useLeftPanelStore } from '../stores';
export const LeftPanelHeaderButton = () => {
- const router = useRouter();
- const isDoc = router.pathname === '/docs/[id]';
-
- if (isDoc) {
- return ;
- }
-
- return ;
-};
-
-export const LeftPanelHeaderHomeButton = () => {
const router = useRouter();
const { t } = useTranslation();
const { togglePanel } = useLeftPanelStore();
@@ -33,45 +21,10 @@ export const LeftPanelHeaderHomeButton = () => {
);
};
-
-export const LeftPanelHeaderDocButton = () => {
- const router = useRouter();
- const { currentDoc } = useDocStore();
- const { t } = useTranslation();
- const { togglePanel } = useLeftPanelStore();
- const treeContext = useTreeContext();
- const tree = treeContext?.treeData;
- const { mutate: createChildrenDoc, isPending: isDocCreating } =
- useCreateChildrenDoc({
- onSuccess: (doc) => {
- tree?.addRootNode(doc);
- tree?.selectNodeById(doc.id);
- void router.push(`/docs/${doc.id}`);
- togglePanel();
- },
- });
-
- const onCreateDoc = () => {
- if (treeContext && treeContext.root) {
- createChildrenDoc({
- parentId: treeContext.root.id,
- });
- }
- };
-
- return (
-
- );
-};