From 2b2a579d9fef00f32a412df6b1bad0aff4f66e50 Mon Sep 17 00:00:00 2001 From: Nathan Panchout Date: Mon, 24 Mar 2025 17:15:14 +0100 Subject: [PATCH] wip --- src/frontend/apps/impress/package.json | 3 +- .../apps/impress/src/components/Icon.tsx | 13 +- .../apps/impress/src/components/Text.tsx | 12 +- .../docs/doc-header/components/DocTitle.tsx | 20 ++- .../docs/doc-management/api/useDoc.tsx | 6 +- .../components/ModalRemoveDoc.tsx | 2 +- .../doc-search/components/DocSearchModal.tsx | 16 +- .../components/DocShareAddMemberList.tsx | 14 +- .../components/DocShareInvitationItem.tsx | 14 +- .../components/DocShareMemberItem.tsx | 15 +- .../doc-tree/components/DocSubPageItem.tsx | 56 +++++-- .../docs/doc-tree/components/DocTree.tsx | 144 +++++++++++++----- .../components/DocTreeItemActions.tsx | 41 +++-- .../docs/doc-tree/context/DocTreeContext.tsx | 117 ++++---------- .../components/LeftPanelDocContent.tsx | 11 +- .../left-panel/components/LeftPanelHeader.tsx | 14 +- .../impress/src/pages/docs/[id]/index.tsx | 21 ++- src/frontend/yarn.lock | 4 +- 18 files changed, 301 insertions(+), 222 deletions(-) diff --git a/src/frontend/apps/impress/package.json b/src/frontend/apps/impress/package.json index 9c83d49e..7c7c7bab 100644 --- a/src/frontend/apps/impress/package.json +++ b/src/frontend/apps/impress/package.json @@ -25,13 +25,14 @@ "@dnd-kit/modifiers": "9.0.0", "@fontsource/material-icons": "5.2.5", "@gouvfr-lasuite/integration": "1.0.2", - "@gouvfr-lasuite/ui-kit": "0.1.3", + "@gouvfr-lasuite/ui-kit": "/Users/melde/Documents/societes/melde/clients/dinum/design-system", "@hocuspocus/provider": "2.15.2", "@openfun/cunningham-react": "3.0.0", "@react-pdf/renderer": "4.1.6", "@sentry/nextjs": "9.3.0", "@tanstack/react-query": "5.67.1", "canvg": "4.0.3", + "clsx": "2.1.1", "cmdk": "1.0.4", "crisp-sdk-web": "1.0.25", "docx": "9.1.1", diff --git a/src/frontend/apps/impress/src/components/Icon.tsx b/src/frontend/apps/impress/src/components/Icon.tsx index b3444661..513fee22 100644 --- a/src/frontend/apps/impress/src/components/Icon.tsx +++ b/src/frontend/apps/impress/src/components/Icon.tsx @@ -5,19 +5,10 @@ import { useCunninghamTheme } from '@/cunningham'; type IconProps = TextType & { iconName: string; - isFilled?: boolean; }; -export const Icon = ({ iconName, isFilled, ...textProps }: IconProps) => { +export const Icon = ({ iconName, ...textProps }: IconProps) => { return ( - + {iconName} ); diff --git a/src/frontend/apps/impress/src/components/Text.tsx b/src/frontend/apps/impress/src/components/Text.tsx index bc7dec46..a00b2c4a 100644 --- a/src/frontend/apps/impress/src/components/Text.tsx +++ b/src/frontend/apps/impress/src/components/Text.tsx @@ -1,3 +1,4 @@ +import clsx from 'clsx'; import { CSSProperties, ComponentPropsWithRef, forwardRef } from 'react'; import styled from 'styled-components'; @@ -11,7 +12,7 @@ type TextSizes = keyof typeof sizes; export interface TextProps extends BoxProps { as?: 'p' | 'span' | 'div' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'; $elipsis?: boolean; - $isMaterialIcon?: boolean; + $isMaterialIcon?: boolean | 'filled'; $weight?: CSSProperties['fontWeight']; $textAlign?: CSSProperties['textAlign']; $size?: TextSizes | (string & {}); @@ -58,13 +59,20 @@ export const TextStyled = styled(Box)` const Text = forwardRef>( ({ className, $isMaterialIcon, ...props }, ref) => { + const isFilled = $isMaterialIcon === 'filled'; + const isMaterialIcon = + typeof $isMaterialIcon === 'boolean' && $isMaterialIcon; + return ( ); 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 ee3caf00..295efb2b 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,6 +1,7 @@ /* eslint-disable jsx-a11y/click-events-have-key-events */ /* eslint-disable jsx-a11y/no-noninteractive-element-interactions */ 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'; @@ -11,13 +12,13 @@ import { Doc, KEY_DOC, KEY_LIST_DOC, + KEY_SUB_DOC, useTrans, useUpdateDoc, } from '@/docs/doc-management'; import { useBroadcastStore, useResponsiveStore } from '@/stores'; -import { useDocTreeData } from '../../doc-tree/context/DocTreeContext'; - +import { useDocTreeStore } from '../../doc-tree/context/DocTreeContext'; interface DocTitleProps { doc: Doc; } @@ -53,22 +54,27 @@ export const DocTitleText = ({ title }: DocTitleTextProps) => { const DocTitleInput = ({ doc }: DocTitleProps) => { const { isDesktop } = useResponsiveStore(); const { t } = useTranslation(); + const queryClient = useQueryClient(); const { colorsTokens } = useCunninghamTheme(); + const treeStore = useDocTreeStore(); const [titleDisplay, setTitleDisplay] = useState(doc.title); - const data = useDocTreeData(); + const { untitledDocument } = useTrans(); const { broadcast } = useBroadcastStore(); const { mutate: updateDoc } = useUpdateDoc({ - listInvalideQueries: [KEY_DOC, KEY_LIST_DOC], + listInvalideQueries: [KEY_LIST_DOC], onSuccess(updatedDoc) { // Broadcast to every user connected to the document broadcast(`${KEY_DOC}-${updatedDoc.id}`); - data?.tree?.updateNode(updatedDoc.id, { title: updatedDoc.title }); - if (updatedDoc.id === data?.root?.id) { - void data?.refreshRoot(); + if (updatedDoc.id === treeStore.root?.id) { + treeStore.setRoot(updatedDoc); } + queryClient.setQueryData( + [KEY_SUB_DOC, { id: 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 ebbb1d54..13f86a5a 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 @@ -6,6 +6,7 @@ import { Doc } from '../types'; export type DocParams = { id: string; + isTree?: boolean; }; export const getDoc = async ({ id }: DocParams): Promise => { @@ -19,14 +20,15 @@ export const getDoc = async ({ id }: DocParams): Promise => { }; export const KEY_DOC = 'doc'; +export const KEY_SUB_DOC = 'sub-doc'; export const KEY_DOC_VISIBILITY = 'doc-visibility'; export function useDoc( param: DocParams, - queryConfig?: UseQueryOptions, + queryConfig?: Omit, 'queryFn'>, ) { return useQuery({ - queryKey: [KEY_DOC, param], + queryKey: queryConfig?.queryKey ?? [KEY_DOC, param], queryFn: () => getDoc(param), ...queryConfig, }); 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 53115c9d..5af99d97 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 @@ -97,7 +97,7 @@ export const ModalRemoveDoc = ({ {!isError && ( {t('Are you sure you want to delete the document "{{title}}"?', { - title: doc.title, + title: doc.title ?? t('Untitled document'), })} )} 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 b9ebc320..98b32a29 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 @@ -15,7 +15,7 @@ import { import { Doc, useInfiniteDocs } from '@/docs/doc-management'; import { useResponsiveStore } from '@/stores'; -import { useDocTreeData } from '../../doc-tree/context/DocTreeContext'; +import { useDocTreeStore } from '../../doc-tree/context/DocTreeContext'; import EmptySearchIcon from '../assets/illustration-docs-empty.png'; import { @@ -36,8 +36,9 @@ export const DocSearchModal = ({ ...modalProps }: DocSearchModalProps) => { const { t } = useTranslation(); - const tree = useDocTreeData(); + const router = useRouter(); + const treeStore = useDocTreeStore(); const [search, setSearch] = useState(''); const [filters, setFilters] = useState( @@ -56,17 +57,16 @@ export const DocSearchModal = ({ page: 1, title: search, ...filters, - parent_id: tree?.root?.id, + parent_id: treeStore?.root?.id, }); const loading = isFetching || isRefetching || isLoading; const handleInputSearch = useDebouncedCallback(setSearch, 300); const handleSelect = (doc: Doc) => { - if (tree?.initialRootId !== doc.id) { - tree?.tree.resetTree([]); - tree?.tree.setSelectedNode(doc); - tree?.setRoot(doc); - tree?.setInitialTargetId(doc.id); + if (treeStore?.initialRootId !== doc.id) { + treeStore.setSelectedNode(doc); + treeStore.setRoot(doc); + treeStore.setInitialTargetId(doc.id); } router.push(`/docs/${doc.id}`); modalProps.onClose?.(); 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 53c2294c..6668bb44 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,6 +3,7 @@ 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,9 +12,8 @@ import { APIError } from '@/api'; import { Box } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; import { User } from '@/features/auth'; -import { Doc, Role } from '@/features/docs'; +import { Doc, KEY_SUB_DOC, Role } from '@/features/docs'; -import { useDocTreeData } from '../../doc-tree/context/DocTreeContext'; import { useCreateDocAccess, useCreateDocInvitation } from '../api'; import { OptionType } from '../types'; @@ -40,7 +40,7 @@ export const DocShareAddMemberList = ({ }: Props) => { const { t } = useTranslation(); const { toast } = useToastProvider(); - const treeData = useDocTreeData(); + const queryClient = useQueryClient(); const [isLoading, setIsLoading] = useState(false); const { spacingsTokens, colorsTokens } = useCunninghamTheme(); const [invitationRole, setInvitationRole] = useState(Role.EDITOR); @@ -100,7 +100,9 @@ export const DocShareAddMemberList = ({ }, { onSuccess: () => { - void treeData?.tree.refreshNode(doc.id); + void queryClient.invalidateQueries({ + queryKey: [KEY_SUB_DOC, { id: doc.id }], + }); }, }, ) @@ -111,7 +113,9 @@ export const DocShareAddMemberList = ({ }, { onSuccess: () => { - void treeData?.tree.refreshNode(doc.id); + void queryClient.invalidateQueries({ + queryKey: [KEY_SUB_DOC, { id: doc.id }], + }); }, }, ); diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitationItem.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitationItem.tsx index 56974a30..bdf334e6 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitationItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareInvitationItem.tsx @@ -1,4 +1,5 @@ import { VariantType, useToastProvider } from '@openfun/cunningham-react'; +import { useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { @@ -8,10 +9,9 @@ import { IconOptions, } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; -import { Doc, Role } from '@/docs/doc-management'; +import { Doc, KEY_SUB_DOC, Role } from '@/docs/doc-management'; import { User } from '@/features/auth'; -import { useDocTreeData } from '../../doc-tree/context/DocTreeContext'; import { useDeleteDocInvitation, useUpdateDocInvitation } from '../api'; import { Invitation } from '../types'; @@ -24,9 +24,9 @@ type Props = { }; export const DocShareInvitationItem = ({ doc, invitation }: Props) => { const { t } = useTranslation(); + const queryClient = useQueryClient(); const { spacingsTokens } = useCunninghamTheme(); const spacing = spacingsTokens(); - const treeData = useDocTreeData(); const fakeUser: User = { id: invitation.email, full_name: invitation.email, @@ -40,7 +40,9 @@ export const DocShareInvitationItem = ({ doc, invitation }: Props) => { const { mutate: updateDocInvitation } = useUpdateDocInvitation({ onSuccess: () => { - void treeData?.tree.refreshNode(doc.id); + void queryClient.invalidateQueries({ + queryKey: [KEY_SUB_DOC, { id: doc.id }], + }); }, onError: (error) => { toast( @@ -55,7 +57,9 @@ export const DocShareInvitationItem = ({ doc, invitation }: Props) => { const { mutate: removeDocInvitation } = useDeleteDocInvitation({ onSuccess: () => { - void treeData?.tree.refreshNode(doc.id); + void queryClient.invalidateQueries({ + queryKey: [KEY_SUB_DOC, { id: doc.id }], + }); }, onError: (error) => { toast( diff --git a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMemberItem.tsx b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMemberItem.tsx index a9f3c195..0c2fd22a 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMemberItem.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-share/components/DocShareMemberItem.tsx @@ -1,4 +1,5 @@ import { VariantType, useToastProvider } from '@openfun/cunningham-react'; +import { useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { @@ -8,10 +9,9 @@ import { IconOptions, } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; -import { Access, Doc, Role } from '@/docs/doc-management/'; +import { Access, Doc, KEY_SUB_DOC, Role } from '@/docs/doc-management/'; import { useResponsiveStore } from '@/stores'; -import { useDocTreeData } from '../../doc-tree/context/DocTreeContext'; import { useDeleteDocAccess, useUpdateDocAccess } from '../api'; import { useWhoAmI } from '../hooks/'; @@ -26,16 +26,19 @@ export const DocShareMemberItem = ({ doc, access }: Props) => { const { t } = useTranslation(); const { isLastOwner, isOtherOwner } = useWhoAmI(access); const { toast } = useToastProvider(); - const treeData = useDocTreeData(); + const { isDesktop } = useResponsiveStore(); const { spacingsTokens } = useCunninghamTheme(); + const queryClient = useQueryClient(); const spacing = spacingsTokens(); const isNotAllowed = isOtherOwner || !!isLastOwner || !doc.abilities.accesses_manage; const { mutate: updateDocAccess } = useUpdateDocAccess({ onSuccess: () => { - void treeData?.tree.refreshNode(doc.id); + void queryClient.invalidateQueries({ + queryKey: [KEY_SUB_DOC, { id: doc.id }], + }); }, onError: () => { toast(t('Error during invitation update'), VariantType.ERROR, { @@ -46,7 +49,9 @@ export const DocShareMemberItem = ({ doc, access }: Props) => { const { mutate: removeDocAccess } = useDeleteDocAccess({ onSuccess: () => { - void treeData?.tree.refreshNode(doc.id); + void queryClient.invalidateQueries({ + queryKey: [KEY_SUB_DOC, { id: doc.id }], + }); }, onError: () => { toast(t('Error while deleting invitation'), VariantType.ERROR, { 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 c66f1d4f..a66591ca 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 @@ -1,14 +1,17 @@ -import { TreeViewItem, TreeViewNodeProps } from '@gouvfr-lasuite/ui-kit'; +import { + TreeViewItem, + TreeViewNodeProps, + useTree, +} from '@gouvfr-lasuite/ui-kit'; import { useRouter } from 'next/navigation'; +import { useEffect, useRef } from 'react'; import { css } from 'styled-components'; import { Box, Icon, Text } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; -import { Doc } from '@/features/docs/doc-management'; +import { Doc, KEY_SUB_DOC, useDoc } from '@/features/docs/doc-management'; import { useLeftPanelStore } from '@/features/left-panel'; -import { useDocTreeData } from '../context/DocTreeContext'; - import Logo from './../assets/sub-page-logo.svg'; import { DocTreeItemActions } from './DocTreeItemActions'; @@ -24,18 +27,43 @@ const ItemTextCss = css` `; type Props = TreeViewNodeProps & { + treeData: ReturnType>; doc: Doc; setSelectedNode: (node: Doc) => void; }; -export const DocSubPageItem = ({ doc, setSelectedNode, ...props }: Props) => { +export const DocSubPageItem = ({ + doc, + setSelectedNode, + treeData, + ...props +}: Props) => { const { loadChildren, node } = props; - + const isInitialLoad = useRef(false); const { spacingsTokens } = useCunninghamTheme(); const spacing = spacingsTokens(); const router = useRouter(); const { togglePanel } = useLeftPanelStore(); - const treeData = useDocTreeData(); + + const { data: docQuery } = useDoc( + { isTree: true, id: doc.id }, + { + initialData: doc, + queryKey: [KEY_SUB_DOC, { id: doc.id }], + refetchOnMount: false, + refetchOnWindowFocus: false, + }, + ); + + useEffect(() => { + if (docQuery && isInitialLoad.current === true) { + console.log('docQuery', docQuery); + treeData?.updateNode(docQuery.id, docQuery); + } + if (docQuery) { + isInitialLoad.current = true; + } + }, [docQuery, treeData]); const afterCreate = (createdDoc: Doc) => { const actualChildren = node.data.children ?? []; @@ -46,7 +74,7 @@ export const DocSubPageItem = ({ doc, setSelectedNode, ...props }: Props) => { node.open(); router.push(`/docs/${doc.id}`); - treeData?.tree.setChildren(node.data.value.id, allChildren); + treeData?.setChildren(node.data.value.id, allChildren); togglePanel(); }) .catch(console.error); @@ -57,7 +85,7 @@ export const DocSubPageItem = ({ doc, setSelectedNode, ...props }: Props) => { childrenCount: 0, parentId: node.id, }; - treeData?.tree.addChild(node.data.value.id, newDoc); + treeData?.addChild(node.data.value.id, newDoc); node.open(); router.push(`/docs/${createdDoc.id}`); togglePanel(); @@ -72,7 +100,7 @@ export const DocSubPageItem = ({ doc, setSelectedNode, ...props }: Props) => { - treeData?.tree.handleLoadChildren(props.node.data.value.id) + treeData?.handleLoadChildren(props.node.data.value.id) } onClick={() => { setSelectedNode(props.node.data.value as Doc); @@ -122,7 +150,12 @@ export const DocSubPageItem = ({ doc, setSelectedNode, ...props }: Props) => { {doc.title} {doc.nb_accesses_direct > 1 && ( - + )} @@ -134,6 +167,7 @@ export const DocSubPageItem = ({ doc, setSelectedNode, ...props }: Props) => { > 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 94f64f7e..fee28478 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 @@ -1,4 +1,10 @@ -import { OpenMap, TreeView, TreeViewMoveResult } from '@gouvfr-lasuite/ui-kit'; +import { + OpenMap, + TreeView, + TreeViewMoveResult, + useTree, +} from '@gouvfr-lasuite/ui-kit'; +import { useQueryClient } from '@tanstack/react-query'; import { useRouter } from 'next/navigation'; import { useEffect, useState } from 'react'; import { css } from 'styled-components'; @@ -6,11 +12,18 @@ import { css } from 'styled-components'; import { Box, SeparatedSection, StyledLink } from '@/components'; import { useCunninghamTheme } from '@/cunningham'; -import { Doc } from '../../doc-management'; +import { + Doc, + KEY_DOC, + LinkReach, + LinkRole, + getDoc, +} from '../../doc-management'; import { SimpleDocItem } from '../../docs-grid'; +import { getDocChildren } from '../api/useDocChildren'; import { useDocTree } from '../api/useDocTree'; import { useMoveDoc } from '../api/useMove'; -import { useDocTreeData } from '../context/DocTreeContext'; +import { subPageToTree, useDocTreeStore } from '../context/DocTreeContext'; import { DocSubPageItem } from './DocSubPageItem'; import { DocTreeItemActions } from './DocTreeItemActions'; @@ -20,8 +33,24 @@ type DocTreeProps = { }; export const DocTree = ({ initialTargetId }: DocTreeProps) => { const { spacingsTokens } = useCunninghamTheme(); + const queryClient = useQueryClient(); + const store = useDocTreeStore(); + const spacing = spacingsTokens(); - const treeData = useDocTreeData(); + + const treeData = useTree( + [], + async (docId) => { + const doc = await getDoc({ id: docId }); + const newDoc = { ...doc, childrenCount: doc.numchild }; + void queryClient.setQueryData([KEY_DOC, { id: docId }], newDoc); + return newDoc; + }, + async (docId) => { + const doc = await getDocChildren({ docId: docId }); + return subPageToTree(doc.results ?? []); + }, + ); const router = useRouter(); const [initialOpenState, setInitialOpenState] = useState( undefined, @@ -39,42 +68,47 @@ export const DocTree = ({ initialTargetId }: DocTreeProps) => { targetDocumentId: result.targetModeId, position: result.mode, }); - treeData?.tree.handleMove(result); + treeData?.handleMove(result); }; - useEffect(() => { + + const buildDocTree = (data?: Doc) => { if (!data) { return; } const { children: rootChildren, ...root } = data; const children = rootChildren ?? []; - treeData?.setRoot(root); + store.setRoot(root); const initialOpenState: OpenMap = {}; initialOpenState[root.id] = true; - const serialize = (children: Doc[]) => { - children.forEach((child) => { - child.childrenCount = child.numchild ?? 0; - if (child?.children?.length && child?.children?.length > 0) { - initialOpenState[child.id] = true; - } - serialize(child.children ?? []); - }); - }; - serialize(children); - console.log(children); + subPageToTree(children, (child) => { + if (child?.children?.length && child?.children?.length > 0) { + initialOpenState[child.id] = true; + } + }); - treeData?.tree.resetTree(children); + treeData.resetTree(children); setInitialOpenState(initialOpenState); if (initialTargetId === root.id) { - treeData?.tree.setSelectedNode(root); + treeData?.setSelectedNode(root); } else { - treeData?.tree.selectNodeById(initialTargetId); + treeData?.selectNodeById(initialTargetId); } + }; + + useEffect(() => { + if (treeData?.selectedNode?.id !== store.selectedNode?.id) { + store.setSelectedNode(treeData?.selectedNode ?? null); + } + }, [store, treeData?.selectedNode]); + + useEffect(() => { + buildDocTree(data); // eslint-disable-next-line react-hooks/exhaustive-deps }, [data]); - const rootIsSelected = treeData?.tree.selectedNode?.id === treeData?.root?.id; + const rootIsSelected = treeData?.selectedNode?.id === store.root?.id; - if (!initialTargetId || !treeData) { + if (!initialTargetId) { return null; } @@ -110,32 +144,33 @@ export const DocTree = ({ initialTargetId }: DocTreeProps) => { } `} > - {treeData.root !== null && ( + {store.root !== null && ( { e.stopPropagation(); e.preventDefault(); - treeData.tree.setSelectedNode(treeData.root ?? undefined); - router.push(`/docs/${treeData?.root?.id}`); + treeData?.setSelectedNode(store.root ?? undefined); + router.push(`/docs/${store.root?.id}`); }} > - +
{ const newDoc = { ...createdDoc, children: [], childrenCount: 0, - parentId: treeData.root?.id ?? undefined, + parentId: store.root?.id ?? undefined, }; - treeData?.tree.addChild(null, newDoc); + treeData?.addChild(null, newDoc); }} />
@@ -145,27 +180,54 @@ export const DocTree = ({ initialTargetId }: DocTreeProps) => {
+ + + {initialOpenState && treeData.nodes.length > 0 && ( { + if (treeData === undefined) { + return null; + } return ( - treeData.tree.handleLoadChildren(node.id) - } - setSelectedNode={treeData.tree.setSelectedNode} + loadChildren={(node) => treeData.handleLoadChildren(node.id)} + setSelectedNode={(node) => treeData.setSelectedNode(node)} /> ); }} 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 1a19a3ab..b9208399 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 @@ -1,4 +1,8 @@ -import { DropdownMenu, DropdownMenuOption } from '@gouvfr-lasuite/ui-kit'; +import { + DropdownMenu, + DropdownMenuOption, + useTree, +} from '@gouvfr-lasuite/ui-kit'; import { useModal } from '@openfun/cunningham-react'; import { useRouter } from 'next/navigation'; import { Fragment, useState } from 'react'; @@ -10,11 +14,12 @@ import { useLeftPanelStore } from '@/features/left-panel'; import { Doc, ModalRemoveDoc } from '../../doc-management'; import { useCreateChildrenDoc } from '../api/useCreateChildren'; -import { useDocTreeData } from '../context/DocTreeContext'; +import { useDocTreeStore } from '../context/DocTreeContext'; type DocTreeItemActionsProps = { doc: Doc; parentId?: string | null; + treeData: ReturnType>; onCreateSuccess?: (newDoc: Doc) => void; }; @@ -22,14 +27,16 @@ export const DocTreeItemActions = ({ doc, parentId, onCreateSuccess, + treeData, }: DocTreeItemActionsProps) => { const [isOpen, setIsOpen] = useState(false); const router = useRouter(); + + const treeStore = useDocTreeStore(); const { t } = useTranslation(); const deleteModal = useModal(); const { togglePanel } = useLeftPanelStore(); - const treeData = useDocTreeData(); const options: DropdownMenuOption[] = [ { label: t('Delete'), @@ -43,22 +50,23 @@ export const DocTreeItemActions = ({ onCreateSuccess?.(doc); togglePanel(); - treeData?.tree.setSelectedNode(doc); + treeData.setSelectedNode(doc); + router.push(`/docs/${doc.id}`); }, }); const afterDelete = () => { if (parentId) { router.push(`/docs/${parentId}`); - treeData?.tree.selectNodeById(parentId); - treeData?.tree.deleteNode(doc.id); - void treeData?.tree.refreshNode(parentId); - } else if (doc.id === treeData?.root?.id && !parentId) { + treeData?.selectNodeById(parentId); + treeData?.deleteNode(doc.id); + void treeData?.refreshNode(parentId); + } else if (doc.id === treeStore.root?.id && !parentId) { router.push(`/docs/`); - } else if (treeData && treeData.root) { - router.push(`/docs/${treeData.root.id}`); - treeData?.tree.deleteNode(doc.id); - treeData?.tree.setSelectedNode(treeData.root); + } else if (treeStore.root) { + router.push(`/docs/${treeStore.root.id}`); + treeData?.deleteNode(doc.id); + treeData?.setSelectedNode(treeStore.root); } }; @@ -84,7 +92,7 @@ export const DocTreeItemActions = ({ setIsOpen(!isOpen); }} iconName="more_horiz" - isFilled + $isMaterialIcon="filled" $theme="primary" $variation="600" /> @@ -101,7 +109,12 @@ export const DocTreeItemActions = ({ }} color="primary" > - + {deleteModal.isOpen && ( diff --git a/src/frontend/apps/impress/src/features/docs/doc-tree/context/DocTreeContext.tsx b/src/frontend/apps/impress/src/features/docs/doc-tree/context/DocTreeContext.tsx index 82904ce5..dc6c198c 100644 --- a/src/frontend/apps/impress/src/features/docs/doc-tree/context/DocTreeContext.tsx +++ b/src/frontend/apps/impress/src/features/docs/doc-tree/context/DocTreeContext.tsx @@ -1,94 +1,37 @@ -import { TreeViewDataType, useTree } from '@gouvfr-lasuite/ui-kit'; -import React, { ReactNode, createContext, useContext, useState } from 'react'; +import { TreeViewDataType } from '@gouvfr-lasuite/ui-kit'; +import { create } from 'zustand'; -import { Doc, getDoc } from '../../doc-management'; -import { getDocChildren } from '../api/useDocChildren'; +import { Doc } from '../../doc-management'; -// import { useTree2 } from './useTree2'; - -// Interface pour le contexte de l'arbre de documents -interface DocTreeContextType { - tree: ReturnType>; - root: Doc | null; - initialTargetId?: string | null; - initialRootId?: string | null; - setRoot: (doc: Doc | null) => void; - setInitialTargetId: (id: string) => void; - refreshRoot: () => Promise; -} - -// Création du contexte avec une valeur par défaut -const DocTreeContext = createContext(undefined); - -// Props pour le provider -interface DocTreeProviderProps { - children: ReactNode; - initialData?: TreeViewDataType[]; - initialRootId?: string; - initialTargetId?: string; -} - -// Provider qui expose les fonctionnalités de l'arbre -export const DocTreeProvider: React.FC = ({ - children, - initialData = [], - initialRootId, - initialTargetId: targetId, -}) => { - const [root, setRoot] = useState(null); - const [initialTargetId, setInitialTargetId] = useState( - targetId ?? null, - ); - - const tree = useTree( - initialData, - async (docId) => { - const doc = await getDoc({ id: docId }); - - return { ...doc, childrenCount: doc.numchild }; - }, - async (docId) => { - const doc = await getDocChildren({ docId: docId }); - return subPageToTree(doc.results ?? []); - }, - ); - - const refreshRoot = async () => { - if (!root) { - return; - } - const doc = await getDoc({ id: root.id }); - setRoot(doc); - }; - - const value: DocTreeContextType = { - tree, - root, - setRoot, - initialTargetId, - initialRootId, - setInitialTargetId, - refreshRoot, - }; - - return ( - {children} - ); -}; - -// Hook personnalisé pour utiliser le contexte -export const useDocTreeData = (): DocTreeContextType | undefined => { - const context = useContext(DocTreeContext); - if (context === undefined) { - return; - } - return context; -}; - -export const subPageToTree = (children: Doc[]): TreeViewDataType[] => { +export const subPageToTree = ( + children: Doc[], + callback?: (doc: Doc) => void, +): TreeViewDataType[] => { children.forEach((child) => { child.childrenCount = child.numchild ?? 0; - subPageToTree(child.children ?? []); + callback?.(child); + subPageToTree(child.children ?? [], callback); }); return children; }; + +interface DocTreeStore { + initialTargetId?: string | null; + initialRootId?: string | null; + setRoot: (doc: Doc | null) => void; + root: Doc | null; + setInitialTargetId: (id: string) => void; + setSelectedNode: (node: Doc | null) => void; + selectedNode: Doc | null; +} + +export const useDocTreeStore = create((set) => ({ + root: null, + selectedNode: null, + initialTargetId: undefined, + initialRootId: undefined, + + setRoot: (doc) => set({ root: doc }), + setInitialTargetId: (id) => set({ initialTargetId: id }), + setSelectedNode: (node) => set({ selectedNode: node }), +})); 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 339096e6..10594a04 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 @@ -1,13 +1,14 @@ import { Box } from '@/components'; import { useDocStore } from '@/docs/doc-management'; import { DocTree } from '@/features/docs/doc-tree/components/DocTree'; -import { useDocTreeData } from '@/features/docs/doc-tree/context/DocTreeContext'; +import { useDocTreeStore } from '@/features/docs/doc-tree/context/DocTreeContext'; export const LeftPanelDocContent = () => { const { currentDoc } = useDocStore(); - const tree = useDocTreeData(); - if (!currentDoc || !tree) { + const treeStore = useDocTreeStore(); + + if (!currentDoc || !treeStore.initialTargetId) { return null; } @@ -17,8 +18,8 @@ export const LeftPanelDocContent = () => { $width="100%" $css="width: 100%; overflow-y: auto; overflow-x: hidden;" > - {tree.initialTargetId && ( - + {treeStore.initialTargetId && ( + )} ); diff --git a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx index eb160d6f..fbf6b5aa 100644 --- a/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx +++ b/src/frontend/apps/impress/src/features/left-panel/components/LeftPanelHeader.tsx @@ -9,7 +9,7 @@ import { useAuth } from '@/features/auth'; import { useCreateDoc, useDocStore } from '@/features/docs/doc-management'; import { DocSearchTarget } from '@/features/docs/doc-search/components/DocSearchFilters'; import { useCreateChildrenDoc } from '@/features/docs/doc-tree/api/useCreateChildren'; -import { useDocTreeData } from '@/features/docs/doc-tree/context/DocTreeContext'; +import { useDocTreeStore } from '@/features/docs/doc-tree/context/DocTreeContext'; import { useCmdK } from '@/hook/useCmdK'; import { useLeftPanelStore } from '../stores'; @@ -18,8 +18,8 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => { const router = useRouter(); const searchModal = useModal(); const { authenticated } = useAuth(); - const docTreeData = useDocTreeData(); - const tree = docTreeData?.tree; + const treeStore = useDocTreeStore(); + const { currentDoc } = useDocStore(); const isDoc = router.pathname === '/docs/[id]'; @@ -43,8 +43,8 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => { const { mutate: createChildrenDoc } = useCreateChildrenDoc({ onSuccess: (doc) => { - tree?.addRootNode(doc); - tree?.selectNodeById(doc.id); + treeStore.treeData?.addRootNode(doc); + treeStore.treeData?.selectNodeById(doc.id); void router.push(`/docs/${doc.id}`); togglePanel(); }, @@ -56,10 +56,10 @@ export const LeftPanelHeader = ({ children }: PropsWithChildren) => { }; const createNewDoc = () => { - if (docTreeData && docTreeData.root && isDoc) { + if (treeStore.root && isDoc) { createChildrenDoc({ title: t('Untitled page'), - parentId: docTreeData.root.id, + parentId: treeStore.root.id, }); } else { createDoc(); diff --git a/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx b/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx index 6998c7f7..c941d936 100644 --- a/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx +++ b/src/frontend/apps/impress/src/pages/docs/[id]/index.tsx @@ -15,7 +15,7 @@ import { useDoc, useDocStore, } from '@/features/docs/doc-management/'; -import { DocTreeProvider } from '@/features/docs/doc-tree/context/DocTreeContext'; +import { useDocTreeStore } from '@/features/docs/doc-tree/context/DocTreeContext'; import { MainLayout } from '@/layouts'; import { useBroadcastStore } from '@/stores'; import { NextPageWithLayout } from '@/types/next'; @@ -25,6 +25,14 @@ export function DocLayout() { query: { id }, } = useRouter(); + const treeStore = useDocTreeStore(); + + useEffect(() => { + if (typeof id === 'string' && !treeStore.initialTargetId) { + treeStore.setInitialTargetId(id); + } + }, [id, treeStore]); + if (typeof id !== 'string') { return null; } @@ -35,11 +43,9 @@ export function DocLayout() { - - - - - + + {treeStore.initialTargetId && } + ); } @@ -59,6 +65,8 @@ const DocPage = ({ id }: DocProps) => { { staleTime: 0, queryKey: [KEY_DOC, { id }], + refetchOnMount: false, + refetchOnWindowFocus: false, }, ); @@ -89,7 +97,6 @@ const DocPage = ({ id }: DocProps) => { useEffect(() => { return () => { - console.log('unmount'); setCurrentDoc(undefined); }; // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/frontend/yarn.lock b/src/frontend/yarn.lock index 909a533a..ea353edc 100644 --- a/src/frontend/yarn.lock +++ b/src/frontend/yarn.lock @@ -1640,10 +1640,8 @@ resolved "https://registry.yarnpkg.com/@gouvfr-lasuite/integration/-/integration-1.0.2.tgz#ed0000f4b738c5a19bb60f5b80a9a2f5d9414234" integrity sha512-npOotZQSyu6SffHiPP+jQVOkJ3qW2KE2cANhEK92sNLX9uZqQaCqljO5GhzsBmh0lB76fiXnrr9i8SIpnDUSZg== -"@gouvfr-lasuite/ui-kit@0.1.3": +"@gouvfr-lasuite/ui-kit@file:../../../design-system": version "0.1.3" - resolved "https://registry.yarnpkg.com/@gouvfr-lasuite/ui-kit/-/ui-kit-0.1.3.tgz#1be7f1bdf12e7428e630d6ce11fbc77c4c9b9b21" - integrity sha512-ba3ZrAIhX84cofa2IwiWhgE0wzz85+ySbOTvB1lP9jeWYvWn/N5HsnxphA9bEMIrx1Yi91upzmYLvjHRoDq1Ww== dependencies: "@dnd-kit/core" "6.3.1" "@dnd-kit/modifiers" "9.0.0"