⚡️(frontend) improve tree stability
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.
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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<Doc>();
|
||||
|
||||
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);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -19,7 +19,6 @@ export const getDoc = async ({ id }: DocParams): Promise<Doc> => {
|
||||
};
|
||||
|
||||
export const KEY_DOC = 'doc';
|
||||
export const KEY_SUB_PAGE = 'sub-page';
|
||||
export const KEY_DOC_VISIBILITY = 'doc-visibility';
|
||||
|
||||
export function useDoc(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>(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);
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -9,11 +9,7 @@ export type DocsTreeParams = {
|
||||
};
|
||||
|
||||
export const getDocTree = async ({ docId }: DocsTreeParams): Promise<Doc> => {
|
||||
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<Doc, APIError, Doc>,
|
||||
'queryKey' | 'queryFn'
|
||||
>,
|
||||
queryConfig?: UseQueryOptions<Doc, APIError, Doc>,
|
||||
) {
|
||||
return useQuery<Doc, APIError, Doc>({
|
||||
queryKey: [KEY_DOC_TREE, params],
|
||||
|
||||
@@ -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<Doc>;
|
||||
export const DocSubPageItem = (props: Props) => {
|
||||
export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
|
||||
const doc = props.node.data.value as Doc;
|
||||
const treeContext = useTreeContext<Doc>();
|
||||
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 ?? [];
|
||||
|
||||
|
||||
@@ -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<Doc>();
|
||||
const { currentDoc } = useDocStore();
|
||||
const treeContext = useTreeContext<Doc | null>();
|
||||
const router = useRouter();
|
||||
|
||||
const previousDocId = useRef<string | null>(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<OpenMap | undefined>(
|
||||
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 (
|
||||
<Box
|
||||
data-testid="doc-tree"
|
||||
@@ -178,42 +195,38 @@ export const DocTree = ({ initialTargetId }: DocTreeProps) => {
|
||||
}
|
||||
`}
|
||||
>
|
||||
{treeContext.root !== null && rootNode && (
|
||||
<StyledLink
|
||||
$css={css`
|
||||
width: 100%;
|
||||
`}
|
||||
href={`/docs/${treeContext.root.id}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
treeContext.treeData.setSelectedNode(
|
||||
treeContext.root ?? undefined,
|
||||
);
|
||||
router.push(`/docs/${treeContext?.root?.id}`);
|
||||
}}
|
||||
>
|
||||
<Box $direction="row" $align="center" $width="100%">
|
||||
<SimpleDocItem doc={rootNode} showAccesses={true} />
|
||||
<div className="doc-tree-root-item-actions">
|
||||
<DocTreeItemActions
|
||||
doc={rootNode}
|
||||
onCreateSuccess={(createdDoc) => {
|
||||
const newDoc = {
|
||||
...createdDoc,
|
||||
children: [],
|
||||
childrenCount: 0,
|
||||
parentId: treeContext.root?.id ?? undefined,
|
||||
};
|
||||
treeContext?.treeData.addChild(null, newDoc);
|
||||
}}
|
||||
isOpen={rootActionsOpen}
|
||||
onOpenChange={setRootActionsOpen}
|
||||
/>
|
||||
</div>
|
||||
</Box>
|
||||
</StyledLink>
|
||||
)}
|
||||
<StyledLink
|
||||
$css={css`
|
||||
width: 100%;
|
||||
`}
|
||||
href={`/docs/${treeContext.root.id}`}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
treeContext.treeData.setSelectedNode(
|
||||
treeContext.root ?? undefined,
|
||||
);
|
||||
router.push(`/docs/${treeContext?.root?.id}`);
|
||||
}}
|
||||
>
|
||||
<Box $direction="row" $align="center" $width="100%">
|
||||
<SimpleDocItem doc={treeContext.root} showAccesses={true} />
|
||||
<DocTreeItemActions
|
||||
doc={treeContext.root}
|
||||
onCreateSuccess={(createdDoc) => {
|
||||
const newDoc = {
|
||||
...createdDoc,
|
||||
children: [],
|
||||
childrenCount: 0,
|
||||
parentId: treeContext.root?.id ?? undefined,
|
||||
};
|
||||
treeContext?.treeData.addChild(null, newDoc);
|
||||
}}
|
||||
isOpen={rootActionsOpen}
|
||||
onOpenChange={setRootActionsOpen}
|
||||
/>
|
||||
</Box>
|
||||
</StyledLink>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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<Doc>();
|
||||
const treeContext = useTreeContext<Doc | null>();
|
||||
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 (
|
||||
<Fragment>
|
||||
<Box className="doc-tree-root-item-actions">
|
||||
<Box
|
||||
$direction="row"
|
||||
$align="center"
|
||||
@@ -187,6 +197,6 @@ export const DocTreeItemActions = ({
|
||||
afterDelete={afterDelete}
|
||||
/>
|
||||
)}
|
||||
</Fragment>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export * from './DocTree';
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './api';
|
||||
export * from './components';
|
||||
export * from './hooks';
|
||||
export * from './utils';
|
||||
|
||||
@@ -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<Doc>[] => {
|
||||
});
|
||||
return children;
|
||||
};
|
||||
|
||||
export const findIndexInTree = (
|
||||
nodes: TreeDataItem<TreeViewDataType<Doc>>[],
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -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 && (
|
||||
<DocTree initialTargetId={tree.initialTargetId} />
|
||||
)}
|
||||
<DocTree currentDoc={currentDoc} />
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user