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 | 214 +++++++++--------- .../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, 191 insertions(+), 247 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 8d16acc8..66e13b4a 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 4d53835a..b54e710d 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 5365ad4d..4fd6e07f 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 4f03f9c9..fb69453c 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 2e1df3aa..351dca4a 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 3e959f61..c51e138e 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 c258a3b8..fa208827 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 c5501cf5..083bccc2 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 8c72e31b..273db720 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 bee2f485..ec6817b5 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); }; - useEffect(() => { - if (!data) { + /** + * This function resets the tree states. + */ + const resetStateTree = useCallback(() => { + if (!treeContext?.root?.id) { return; } - const { children: rootChildren, ...root } = data; + 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 (!tree || treeContext?.root?.id || isFetching) { + return; + } + + 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 1d8d8a92..0589f9de 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 00000000..29f15332 --- /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 608f00da..ec8b4043 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 789e9211..510baa90 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 928af5ec..6e67f8a9 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 && ( - - )} + ); };