From bbf48f088f6cc6f3af644376bdde3d5b6024cfe0 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Tue, 22 Jul 2025 17:53:41 +0200 Subject: [PATCH] =?UTF-8?q?=E2=9A=A1=EF=B8=8F(frontend)=20improve=20tree?= =?UTF-8?q?=20stability?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Improve tree stability by limiting the requests, we now only load the tree request one time then we let the treeContext handle the state without mutating it directly. We do not do the doc subpage request anymore, the treeContext has already the data we need, we just need to update the tree node when needed. --- CHANGELOG.md | 1 + .../docs/doc-header/components/DocTitle.tsx | 19 +- .../docs/doc-management/api/useDoc.tsx | 1 - .../doc-share/components/DocRoleDropdown.tsx | 27 +-- .../components/DocShareAddMemberList.tsx | 38 +--- .../components/DocShareInvitation.tsx | 14 +- .../doc-share/components/DocShareMember.tsx | 12 +- .../features/docs/doc-tree/api/useDocTree.tsx | 11 +- .../doc-tree/components/DocSubPageItem.tsx | 34 +-- .../docs/doc-tree/components/DocTree.tsx | 212 +++++++++--------- .../components/DocTreeItemActions.tsx | 36 +-- .../docs/doc-tree/components/index.ts | 1 + .../src/features/docs/doc-tree/index.ts | 1 + .../src/features/docs/doc-tree/utils.ts | 23 +- .../components/LeftPanelDocContent.tsx | 6 +- 15 files changed, 190 insertions(+), 246 deletions(-) create mode 100644 src/frontend/apps/impress/src/features/docs/doc-tree/components/index.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d16acc8a4..66e13b4a47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to - ♻️(frontend) redirect to doc after duplicate #1175 - 🔧(project) change env.d system by using local files #1200 +- ⚡️(frontend) improve tree stability #1207 ### Fixed diff --git a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx index 4d53835a6d..b54e710d2b 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-header/components/DocTitle.tsx @@ -1,7 +1,7 @@ /* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ +import { useTreeContext } from '@gouvfr-lasuite/ui-kit'; import { Tooltip } from '@openfun/cunningham-react'; -import { useQueryClient } from '@tanstack/react-query'; import React, { useCallback, useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; @@ -12,7 +12,6 @@ import { Doc, KEY_DOC, KEY_LIST_DOC, - KEY_SUB_PAGE, useDocStore, useTrans, useUpdateDoc, @@ -50,10 +49,10 @@ export const DocTitleText = () => { const DocTitleInput = ({ doc }: DocTitleProps) => { const { isDesktop } = useResponsiveStore(); - const queryClient = useQueryClient(); const { t } = useTranslation(); const { colorsTokens } = useCunninghamTheme(); const [titleDisplay, setTitleDisplay] = useState(doc.title); + const treeContext = useTreeContext(); const { untitledDocument } = useTrans(); @@ -64,10 +63,16 @@ const DocTitleInput = ({ doc }: DocTitleProps) => { onSuccess(updatedDoc) { // Broadcast to every user connected to the document broadcast(`${KEY_DOC}-${updatedDoc.id}`); - queryClient.setQueryData( - [KEY_SUB_PAGE, { id: updatedDoc.id }], - updatedDoc, - ); + + if (!treeContext) { + return; + } + + if (treeContext.root?.id === updatedDoc.id) { + treeContext?.setRoot(updatedDoc); + } else { + treeContext?.treeData.updateNode(updatedDoc.id, updatedDoc); + } }, }); diff --git a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDoc.tsx b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDoc.tsx index 5365ad4d90..4fd6e07ffc 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-management/api/useDoc.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-management/api/useDoc.tsx @@ -19,7 +19,6 @@ export const getDoc = async ({ id }: DocParams): Promise => { }; export const KEY_DOC = 'doc'; -export const KEY_SUB_PAGE = 'sub-page'; export const KEY_DOC_VISIBILITY = 'doc-visibility'; export function useDoc( 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 4f03f9c95b..fb69453cdc 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,17 +1,10 @@ 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 { - Access, - Doc, - KEY_SUB_PAGE, - Role, - useTrans, -} from '@/docs/doc-management/'; +import { Access, Doc, Role, useTrans } from '@/docs/doc-management/'; import { useDeleteDocAccess, useDeleteDocInvitation } from '../api'; import { Invitation, isInvitation } from '../types'; @@ -39,19 +32,9 @@ export const DocRoleDropdown = ({ }: 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'), @@ -64,14 +47,6 @@ export const DocRoleDropdown = ({ }); 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, diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareAddMemberList.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareAddMemberList.tsx index 2e1df3aaeb..351dca4a59 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareAddMemberList.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareAddMemberList.tsx @@ -3,7 +3,6 @@ import { VariantType, useToastProvider, } from '@openfun/cunningham-react'; -import { useQueryClient } from '@tanstack/react-query'; import { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; @@ -11,7 +10,7 @@ import { css } from 'styled-components'; import { APIError } from '@/api'; import { Box } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; -import { Doc, KEY_SUB_PAGE, Role } from '@/docs/doc-management'; +import { Doc, Role } from '@/docs/doc-management'; import { User } from '@/features/auth'; import { useCreateDocAccess, useCreateDocInvitation } from '../api'; @@ -45,7 +44,6 @@ export const DocShareAddMemberList = ({ const { spacingsTokens, colorsTokens } = useCunninghamTheme(); const [invitationRole, setInvitationRole] = useState(Role.EDITOR); const canShare = doc.abilities.accesses_manage; - const queryClient = useQueryClient(); const { mutateAsync: createInvitation } = useCreateDocInvitation(); const { mutateAsync: createDocAccess } = useCreateDocAccess(); @@ -91,32 +89,14 @@ export const DocShareAddMemberList = ({ }; return isInvitationMode - ? createInvitation( - { - ...payload, - email: user.email, - }, - { - onSuccess: () => { - void queryClient.invalidateQueries({ - queryKey: [KEY_SUB_PAGE, { id: doc.id }], - }); - }, - }, - ) - : createDocAccess( - { - ...payload, - memberId: user.id, - }, - { - onSuccess: () => { - void queryClient.invalidateQueries({ - queryKey: [KEY_SUB_PAGE, { id: doc.id }], - }); - }, - }, - ); + ? createInvitation({ + ...payload, + email: user.email, + }) + : createDocAccess({ + ...payload, + memberId: user.id, + }); }); const settledPromises = await Promise.allSettled(promises); diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitation.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitation.tsx index 3e959f61f4..c51e138e72 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitation.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitation.tsx @@ -1,5 +1,4 @@ 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'; @@ -15,7 +14,7 @@ import { } from '@/components'; import { QuickSearchData, QuickSearchGroup } from '@/components/quick-search'; import { useCunninghamTheme } from '@/cunningham'; -import { Doc, KEY_SUB_PAGE, Role } from '@/docs/doc-management'; +import { Doc, Role } from '@/docs/doc-management'; import { User } from '@/features/auth'; import { @@ -38,7 +37,6 @@ export const DocShareInvitationItem = ({ invitation, }: DocShareInvitationItemProps) => { const { t } = useTranslation(); - const queryClient = useQueryClient(); const { spacingsTokens } = useCunninghamTheme(); const invitedUser: User = { id: invitation.email, @@ -52,11 +50,6 @@ export const DocShareInvitationItem = ({ const canUpdate = doc.abilities.accesses_manage; const { mutate: updateDocInvitation } = useUpdateDocInvitation({ - onSuccess: () => { - void queryClient.invalidateQueries({ - queryKey: [KEY_SUB_PAGE, { id: doc.id }], - }); - }, onError: (error) => { toast( error?.data?.role?.[0] ?? t('Error during update invitation'), @@ -69,11 +62,6 @@ export const DocShareInvitationItem = ({ }); const { mutate: removeDocInvitation } = useDeleteDocInvitation({ - onSuccess: () => { - void queryClient.invalidateQueries({ - queryKey: [KEY_SUB_PAGE, { id: doc.id }], - }); - }, onError: (error) => { toast( error?.data?.role?.[0] ?? t('Error during delete invitation'), 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 c258a3b877..fa20882772 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 @@ -1,5 +1,4 @@ import { VariantType, useToastProvider } from '@openfun/cunningham-react'; -import { useQueryClient } from '@tanstack/react-query'; import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -7,7 +6,7 @@ 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 { Access, Doc, Role } from '@/docs/doc-management/'; import { useDocAccesses, useUpdateDocAccess } from '../api'; import { useWhoAmI } from '../hooks/'; @@ -26,7 +25,6 @@ export const DocShareMemberItem = ({ isInherited = false, }: Props) => { const { t } = useTranslation(); - const queryClient = useQueryClient(); const { isLastOwner } = useWhoAmI(access); const { toast } = useToastProvider(); @@ -39,14 +37,6 @@ export const DocShareMemberItem = ({ : undefined; const { mutate: updateDocAccess } = useUpdateDocAccess({ - onSuccess: () => { - if (!doc) { - return; - } - void queryClient.invalidateQueries({ - queryKey: [KEY_SUB_PAGE, { id: doc.id }], - }); - }, onError: () => { toast(t('Error while updating the member role.'), VariantType.ERROR, { duration: 4000, diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/api/useDocTree.tsx b/src/frontend/apps/impress/src/features/docs/doc-tree/api/useDocTree.tsx index c5501cf528..083bccc274 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-tree/api/useDocTree.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/api/useDocTree.tsx @@ -9,11 +9,7 @@ export type DocsTreeParams = { }; export const getDocTree = async ({ docId }: DocsTreeParams): Promise => { - const searchParams = new URLSearchParams(); - - const response = await fetchAPI( - `documents/${docId}/tree/?${searchParams.toString()}`, - ); + const response = await fetchAPI(`documents/${docId}/tree/`); if (!response.ok) { throw new APIError( @@ -29,10 +25,7 @@ export const KEY_DOC_TREE = 'doc-tree'; export function useDocTree( params: DocsTreeParams, - queryConfig?: Omit< - UseQueryOptions, - 'queryKey' | 'queryFn' - >, + queryConfig?: UseQueryOptions, ) { return useQuery({ queryKey: [KEY_DOC_TREE, params], 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 8c72e31bc7..273db720ab 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 @@ -4,17 +4,12 @@ import { useTreeContext, } from '@gouvfr-lasuite/ui-kit'; import { useRouter } from 'next/navigation'; -import { useEffect, useRef, useState } from 'react'; +import { useState } from 'react'; import { css } from 'styled-components'; import { Box, Icon, Text } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; -import { - Doc, - KEY_SUB_PAGE, - useDoc, - useTrans, -} from '@/features/docs/doc-management'; +import { Doc, useTrans } from '@/features/docs/doc-management'; import { useLeftPanelStore } from '@/features/left-panel'; import { useResponsiveStore } from '@/stores'; @@ -31,8 +26,7 @@ const ItemTextCss = css` -webkit-box-orient: vertical; `; -type Props = TreeViewNodeProps; -export const DocSubPageItem = (props: Props) => { +export const DocSubPageItem = (props: TreeViewNodeProps) => { const doc = props.node.data.value as Doc; const treeContext = useTreeContext(); const { untitledDocument } = useTrans(); @@ -44,28 +38,6 @@ export const DocSubPageItem = (props: Props) => { const router = useRouter(); const { togglePanel } = useLeftPanelStore(); - const isInitialLoad = useRef(false); - const { data: docQuery } = useDoc( - { id: doc.id }, - { - initialData: doc, - queryKey: [KEY_SUB_PAGE, { id: doc.id }], - refetchOnMount: false, - refetchOnWindowFocus: false, - }, - ); - - useEffect(() => { - if (docQuery && isInitialLoad.current === true) { - treeContext?.treeData.updateNode(docQuery.id, docQuery); - } - - if (docQuery) { - isInitialLoad.current = true; - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [docQuery]); - const afterCreate = (createdDoc: Doc) => { const actualChildren = node.data.children ?? []; 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 bee2f48533..ec6817b58c 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 @@ -5,52 +5,46 @@ import { useTreeContext, } from '@gouvfr-lasuite/ui-kit'; import { useRouter } from 'next/navigation'; -import { useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useState } from 'react'; import { css } from 'styled-components'; import { Box, StyledLink } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; -import { Doc, KEY_SUB_PAGE, useDoc, useDocStore } from '@/docs/doc-management'; +import { Doc } from '@/docs/doc-management'; import { SimpleDocItem } from '@/docs/docs-grid'; -import { useDocTree } from '../api/useDocTree'; +import { KEY_DOC_TREE, useDocTree } from '../api/useDocTree'; import { useMoveDoc } from '../api/useMove'; +import { findIndexInTree } from '../utils'; import { DocSubPageItem } from './DocSubPageItem'; import { DocTreeItemActions } from './DocTreeItemActions'; type DocTreeProps = { - initialTargetId: string; + currentDoc: Doc; }; -export const DocTree = ({ initialTargetId }: DocTreeProps) => { + +export const DocTree = ({ currentDoc }: DocTreeProps) => { const { spacingsTokens } = useCunninghamTheme(); const [rootActionsOpen, setRootActionsOpen] = useState(false); - const treeContext = useTreeContext(); - const { currentDoc } = useDocStore(); + const treeContext = useTreeContext(); const router = useRouter(); - const previousDocId = useRef(initialTargetId); - - const { data: rootNode } = useDoc( - { id: treeContext?.root?.id ?? '' }, - { - enabled: !!treeContext?.root?.id, - initialData: treeContext?.root ?? undefined, - queryKey: [KEY_SUB_PAGE, { id: treeContext?.root?.id ?? '' }], - refetchOnMount: false, - refetchOnWindowFocus: false, - }, - ); - const [initialOpenState, setInitialOpenState] = useState( undefined, ); const { mutate: moveDoc } = useMoveDoc(); - const { data } = useDocTree({ - docId: initialTargetId, - }); + const { data: tree, isFetching } = useDocTree( + { + docId: currentDoc.id, + }, + { + enabled: !!!treeContext?.root?.id, + queryKey: [KEY_DOC_TREE, { id: currentDoc.id }], + }, + ); const handleMove = (result: TreeViewMoveResult) => { moveDoc({ @@ -61,12 +55,55 @@ export const DocTree = ({ initialTargetId }: DocTreeProps) => { treeContext?.treeData.handleMove(result); }; + /** + * This function resets the tree states. + */ + const resetStateTree = useCallback(() => { + if (!treeContext?.root?.id) { + return; + } + + treeContext?.setRoot(null); + setInitialOpenState(undefined); + }, [treeContext]); + + /** + * This effect is used to reset the tree when a new document + * that is not part of the current tree is loaded. + */ + useEffect(() => { + if (!treeContext?.root?.id) { + return; + } + + const index = findIndexInTree(treeContext.treeData.nodes, currentDoc.id); + if (index === -1 && currentDoc.id !== treeContext.root?.id) { + resetStateTree(); + return; + } + }, [currentDoc, resetStateTree, treeContext]); + + /** + * This effect is used to reset the tree when the component is unmounted. + */ + useEffect(() => { + return () => { + resetStateTree(); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + /** + * This effect is used to set the initial open state of the tree when the tree is loaded. + * If the treeContext is already set, we do not need to set it again. + */ useEffect(() => { - if (!data) { + if (!tree || treeContext?.root?.id || isFetching) { return; } - const { children: rootChildren, ...root } = data; + const { children: rootChildren, ...root } = tree; const children = rootChildren ?? []; treeContext?.setRoot(root); const initialOpenState: OpenMap = {}; @@ -84,50 +121,30 @@ export const DocTree = ({ initialTargetId }: DocTreeProps) => { treeContext?.treeData.resetTree(children); setInitialOpenState(initialOpenState); - if (initialTargetId === root.id) { - treeContext?.treeData.setSelectedNode(root); - } else { - treeContext?.treeData.selectNodeById(initialTargetId); - } - - // Because treeData change in the treeContext, we have a infinite loop - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [data, initialTargetId]); + }, [tree, treeContext, isFetching]); + /** + * This effect is used to select the current document in the tree + */ useEffect(() => { - if ( - !currentDoc || - (previousDocId.current && previousDocId.current === currentDoc.id) - ) { + if (!treeContext || !treeContext.root?.id) { return; } - const item = treeContext?.treeData.getNode(currentDoc?.id ?? ''); - if (!item && currentDoc.id !== rootNode?.id) { - treeContext?.treeData.resetTree([]); - treeContext?.setRoot(currentDoc); - treeContext?.setInitialTargetId(currentDoc.id); - } else if (item) { - const { children: _children, ...leftDoc } = currentDoc; - treeContext?.treeData.updateNode(currentDoc.id, { - ...leftDoc, - childrenCount: leftDoc.numchild, - }); - } - if (currentDoc?.id && currentDoc?.id !== previousDocId.current) { - previousDocId.current = currentDoc?.id; + if (currentDoc.id === treeContext?.root?.id) { + treeContext?.treeData.setSelectedNode(treeContext?.root); + } else { + treeContext?.treeData.selectNodeById(currentDoc.id); } + }, [currentDoc, treeContext]); - treeContext?.treeData.setSelectedNode(currentDoc); - }, [currentDoc, rootNode?.id, treeContext]); - - const rootIsSelected = - treeContext?.treeData.selectedNode?.id === treeContext?.root?.id; - - if (!initialTargetId || !treeContext) { + if (!treeContext || !treeContext.root) { return null; } + const rootIsSelected = + treeContext.treeData.selectedNode?.id === treeContext.root.id; + return ( { } `} > - {treeContext.root !== null && rootNode && ( - { - e.stopPropagation(); - e.preventDefault(); - treeContext.treeData.setSelectedNode( - treeContext.root ?? undefined, - ); - router.push(`/docs/${treeContext?.root?.id}`); - }} - > - - -
- { - const newDoc = { - ...createdDoc, - children: [], - childrenCount: 0, - parentId: treeContext.root?.id ?? undefined, - }; - treeContext?.treeData.addChild(null, newDoc); - }} - isOpen={rootActionsOpen} - onOpenChange={setRootActionsOpen} - /> -
-
-
- )} + { + e.stopPropagation(); + e.preventDefault(); + treeContext.treeData.setSelectedNode( + treeContext.root ?? undefined, + ); + router.push(`/docs/${treeContext?.root?.id}`); + }} + > + + + { + const newDoc = { + ...createdDoc, + children: [], + childrenCount: 0, + parentId: treeContext.root?.id ?? undefined, + }; + treeContext?.treeData.addChild(null, newDoc); + }} + isOpen={rootActionsOpen} + onOpenChange={setRootActionsOpen} + /> + +
@@ -227,20 +240,17 @@ export const DocTree = ({ initialTargetId }: DocTreeProps) => { undefined } canDrop={({ parentNode }) => { - if (!rootNode) { - return false; - } const parentDoc = parentNode?.data.value as Doc; if (!parentDoc) { - return rootNode?.abilities.move; + return currentDoc.abilities.move; } - return parentDoc?.abilities.move; + return parentDoc.abilities.move; }} canDrag={(node) => { const doc = node.value as Doc; return doc.abilities.move; }} - rootNodeId={treeContext.root?.id ?? ''} + rootNodeId={treeContext.root.id} renderNode={DocSubPageItem} /> )} 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 1d8d8a9248..0589f9de30 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,6 @@ import { } from '@gouvfr-lasuite/ui-kit'; import { useModal } from '@openfun/cunningham-react'; import { useRouter } from 'next/router'; -import { Fragment } from 'react'; import { useTranslation } from 'react-i18next'; import { css } from 'styled-components'; @@ -25,9 +24,9 @@ import { useTreeUtils } from '../hooks'; type DocTreeItemActionsProps = { doc: Doc; + isOpen?: boolean; parentId?: string | null; onCreateSuccess?: (newDoc: Doc) => void; - isOpen?: boolean; onOpenChange?: (isOpen: boolean) => void; }; @@ -45,10 +44,12 @@ export const DocTreeItemActions = ({ const copyLink = useCopyDocLink(doc.id); const { isCurrentParent } = useTreeUtils(doc); const { mutate: detachDoc } = useDetachDoc(); - const treeContext = useTreeContext(); + const treeContext = useTreeContext(); const { mutate: duplicateDoc } = useDuplicateDoc({ - onSuccess: (data) => { - void router.push(`/docs/${data.id}`); + onSuccess: (duplicatedDoc) => { + // Reset the tree context root will reset the full tree view. + treeContext?.setRoot(null); + void router.push(`/docs/${duplicatedDoc.id}`); }, }); @@ -61,10 +62,13 @@ export const DocTreeItemActions = ({ { documentId: doc.id, rootId: treeContext.root.id }, { onSuccess: () => { - treeContext.treeData.deleteNode(doc.id); if (treeContext.root) { treeContext.treeData.setSelectedNode(treeContext.root); - void router.push(`/docs/${treeContext.root.id}`); + void router.push(`/docs/${treeContext.root.id}`).then(() => { + setTimeout(() => { + treeContext?.treeData.deleteNode(doc.id); + }, 100); + }); } }, }, @@ -124,18 +128,24 @@ export const DocTreeItemActions = ({ const afterDelete = () => { if (parentId) { - treeContext?.treeData.deleteNode(doc.id); - void router.push(`/docs/${parentId}`); + void router.push(`/docs/${parentId}`).then(() => { + setTimeout(() => { + treeContext?.treeData.deleteNode(doc.id); + }, 100); + }); } else if (doc.id === treeContext?.root?.id && !parentId) { void router.push(`/docs/`); } else if (treeContext && treeContext.root) { - treeContext?.treeData.deleteNode(doc.id); - void router.push(`/docs/${treeContext.root.id}`); + void router.push(`/docs/${treeContext.root.id}`).then(() => { + setTimeout(() => { + treeContext?.treeData.deleteNode(doc.id); + }, 100); + }); } }; return ( - + )} - + ); }; diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/components/index.ts b/src/frontend/apps/impress/src/features/docs/doc-tree/components/index.ts new file mode 100644 index 0000000000..29f1533289 --- /dev/null +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/components/index.ts @@ -0,0 +1 @@ +export * from './DocTree'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/index.ts b/src/frontend/apps/impress/src/features/docs/doc-tree/index.ts index 608f00da51..ec8b404339 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-tree/index.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/index.ts @@ -1,3 +1,4 @@ export * from './api'; +export * from './components'; export * from './hooks'; export * from './utils'; diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/utils.ts b/src/frontend/apps/impress/src/features/docs/doc-tree/utils.ts index 789e9211f5..510baa90bb 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-tree/utils.ts +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/utils.ts @@ -1,4 +1,4 @@ -import { TreeViewDataType } from '@gouvfr-lasuite/ui-kit'; +import { TreeDataItem, TreeViewDataType } from '@gouvfr-lasuite/ui-kit'; import { Doc } from '../doc-management'; @@ -9,3 +9,24 @@ export const subPageToTree = (children: Doc[]): TreeViewDataType[] => { }); return children; }; + +export const findIndexInTree = ( + nodes: TreeDataItem>[], + key: string, +) => { + for (let i = 0; i < nodes.length; i++) { + if (nodes[i].key === key) { + return i; + } + if (nodes[i].children?.length ?? 0 > 0) { + const childIndex: number = nodes[i].children + ? findIndexInTree(nodes[i].children ?? [], key) + : -1; + + if (childIndex !== -1) { + return childIndex; + } + } + } + return -1; +}; diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelDocContent.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelDocContent.tsx index 928af5ec37..6e67f8a987 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelDocContent.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelDocContent.tsx @@ -2,7 +2,7 @@ import { useTreeContext } from '@gouvfr-lasuite/ui-kit'; import { Box } from '@/components'; import { Doc, useDocStore } from '@/docs/doc-management'; -import { DocTree } from '@/features/docs/doc-tree/components/DocTree'; +import { DocTree } from '@/docs/doc-tree/'; export const LeftPanelDocContent = () => { const { currentDoc } = useDocStore(); @@ -20,9 +20,7 @@ export const LeftPanelDocContent = () => { $css="width: 100%; overflow-y: auto; overflow-x: hidden;" className="--docs--left-panel-doc-content" > - {tree.initialTargetId && ( - - )} + ); };