⚡️(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
|
- ♻️(frontend) redirect to doc after duplicate #1175
|
||||||
- 🔧(project) change env.d system by using local files #1200
|
- 🔧(project) change env.d system by using local files #1200
|
||||||
|
- ⚡️(frontend) improve tree stability #1207
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
/* eslint-disable jsx-a11y/click-events-have-key-events */
|
||||||
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
/* eslint-disable jsx-a11y/no-noninteractive-element-interactions */
|
||||||
|
import { useTreeContext } from '@gouvfr-lasuite/ui-kit';
|
||||||
import { Tooltip } from '@openfun/cunningham-react';
|
import { Tooltip } from '@openfun/cunningham-react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
Doc,
|
Doc,
|
||||||
KEY_DOC,
|
KEY_DOC,
|
||||||
KEY_LIST_DOC,
|
KEY_LIST_DOC,
|
||||||
KEY_SUB_PAGE,
|
|
||||||
useDocStore,
|
useDocStore,
|
||||||
useTrans,
|
useTrans,
|
||||||
useUpdateDoc,
|
useUpdateDoc,
|
||||||
@@ -50,10 +49,10 @@ export const DocTitleText = () => {
|
|||||||
|
|
||||||
const DocTitleInput = ({ doc }: DocTitleProps) => {
|
const DocTitleInput = ({ doc }: DocTitleProps) => {
|
||||||
const { isDesktop } = useResponsiveStore();
|
const { isDesktop } = useResponsiveStore();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { colorsTokens } = useCunninghamTheme();
|
const { colorsTokens } = useCunninghamTheme();
|
||||||
const [titleDisplay, setTitleDisplay] = useState(doc.title);
|
const [titleDisplay, setTitleDisplay] = useState(doc.title);
|
||||||
|
const treeContext = useTreeContext<Doc>();
|
||||||
|
|
||||||
const { untitledDocument } = useTrans();
|
const { untitledDocument } = useTrans();
|
||||||
|
|
||||||
@@ -64,10 +63,16 @@ const DocTitleInput = ({ doc }: DocTitleProps) => {
|
|||||||
onSuccess(updatedDoc) {
|
onSuccess(updatedDoc) {
|
||||||
// Broadcast to every user connected to the document
|
// Broadcast to every user connected to the document
|
||||||
broadcast(`${KEY_DOC}-${updatedDoc.id}`);
|
broadcast(`${KEY_DOC}-${updatedDoc.id}`);
|
||||||
queryClient.setQueryData(
|
|
||||||
[KEY_SUB_PAGE, { id: updatedDoc.id }],
|
if (!treeContext) {
|
||||||
updatedDoc,
|
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_DOC = 'doc';
|
||||||
export const KEY_SUB_PAGE = 'sub-page';
|
|
||||||
export const KEY_DOC_VISIBILITY = 'doc-visibility';
|
export const KEY_DOC_VISIBILITY = 'doc-visibility';
|
||||||
|
|
||||||
export function useDoc(
|
export function useDoc(
|
||||||
|
|||||||
@@ -1,17 +1,10 @@
|
|||||||
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
|
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
import { DropdownMenu, DropdownMenuOption, Text } from '@/components';
|
import { DropdownMenu, DropdownMenuOption, Text } from '@/components';
|
||||||
import {
|
import { Access, Doc, Role, useTrans } from '@/docs/doc-management/';
|
||||||
Access,
|
|
||||||
Doc,
|
|
||||||
KEY_SUB_PAGE,
|
|
||||||
Role,
|
|
||||||
useTrans,
|
|
||||||
} from '@/docs/doc-management/';
|
|
||||||
|
|
||||||
import { useDeleteDocAccess, useDeleteDocInvitation } from '../api';
|
import { useDeleteDocAccess, useDeleteDocInvitation } from '../api';
|
||||||
import { Invitation, isInvitation } from '../types';
|
import { Invitation, isInvitation } from '../types';
|
||||||
@@ -39,19 +32,9 @@ export const DocRoleDropdown = ({
|
|||||||
}: DocRoleDropdownProps) => {
|
}: DocRoleDropdownProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { transRole, translatedRoles } = useTrans();
|
const { transRole, translatedRoles } = useTrans();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { toast } = useToastProvider();
|
const { toast } = useToastProvider();
|
||||||
|
|
||||||
const { mutate: removeDocInvitation } = useDeleteDocInvitation({
|
const { mutate: removeDocInvitation } = useDeleteDocInvitation({
|
||||||
onSuccess: () => {
|
|
||||||
if (!doc) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
void queryClient.invalidateQueries({
|
|
||||||
queryKey: [KEY_SUB_PAGE, { id: doc.id }],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast(
|
toast(
|
||||||
error?.data?.role?.[0] ?? t('Error during delete invitation'),
|
error?.data?.role?.[0] ?? t('Error during delete invitation'),
|
||||||
@@ -64,14 +47,6 @@ export const DocRoleDropdown = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { mutate: removeDocAccess } = useDeleteDocAccess({
|
const { mutate: removeDocAccess } = useDeleteDocAccess({
|
||||||
onSuccess: () => {
|
|
||||||
if (!doc) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
void queryClient.invalidateQueries({
|
|
||||||
queryKey: [KEY_SUB_PAGE, { id: doc.id }],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast(t('Error while deleting invitation'), VariantType.ERROR, {
|
toast(t('Error while deleting invitation'), VariantType.ERROR, {
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import {
|
|||||||
VariantType,
|
VariantType,
|
||||||
useToastProvider,
|
useToastProvider,
|
||||||
} from '@openfun/cunningham-react';
|
} from '@openfun/cunningham-react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
@@ -11,7 +10,7 @@ import { css } from 'styled-components';
|
|||||||
import { APIError } from '@/api';
|
import { APIError } from '@/api';
|
||||||
import { Box } from '@/components';
|
import { Box } from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
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 { User } from '@/features/auth';
|
||||||
|
|
||||||
import { useCreateDocAccess, useCreateDocInvitation } from '../api';
|
import { useCreateDocAccess, useCreateDocInvitation } from '../api';
|
||||||
@@ -45,7 +44,6 @@ export const DocShareAddMemberList = ({
|
|||||||
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
const { spacingsTokens, colorsTokens } = useCunninghamTheme();
|
||||||
const [invitationRole, setInvitationRole] = useState<Role>(Role.EDITOR);
|
const [invitationRole, setInvitationRole] = useState<Role>(Role.EDITOR);
|
||||||
const canShare = doc.abilities.accesses_manage;
|
const canShare = doc.abilities.accesses_manage;
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { mutateAsync: createInvitation } = useCreateDocInvitation();
|
const { mutateAsync: createInvitation } = useCreateDocInvitation();
|
||||||
const { mutateAsync: createDocAccess } = useCreateDocAccess();
|
const { mutateAsync: createDocAccess } = useCreateDocAccess();
|
||||||
|
|
||||||
@@ -91,32 +89,14 @@ export const DocShareAddMemberList = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
return isInvitationMode
|
return isInvitationMode
|
||||||
? createInvitation(
|
? createInvitation({
|
||||||
{
|
...payload,
|
||||||
...payload,
|
email: user.email,
|
||||||
email: user.email,
|
})
|
||||||
},
|
: createDocAccess({
|
||||||
{
|
...payload,
|
||||||
onSuccess: () => {
|
memberId: user.id,
|
||||||
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 }],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const settledPromises = await Promise.allSettled(promises);
|
const settledPromises = await Promise.allSettled(promises);
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
|
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
@@ -15,7 +14,7 @@ import {
|
|||||||
} from '@/components';
|
} from '@/components';
|
||||||
import { QuickSearchData, QuickSearchGroup } from '@/components/quick-search';
|
import { QuickSearchData, QuickSearchGroup } from '@/components/quick-search';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
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 { User } from '@/features/auth';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -38,7 +37,6 @@ export const DocShareInvitationItem = ({
|
|||||||
invitation,
|
invitation,
|
||||||
}: DocShareInvitationItemProps) => {
|
}: DocShareInvitationItemProps) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { spacingsTokens } = useCunninghamTheme();
|
const { spacingsTokens } = useCunninghamTheme();
|
||||||
const invitedUser: User = {
|
const invitedUser: User = {
|
||||||
id: invitation.email,
|
id: invitation.email,
|
||||||
@@ -52,11 +50,6 @@ export const DocShareInvitationItem = ({
|
|||||||
const canUpdate = doc.abilities.accesses_manage;
|
const canUpdate = doc.abilities.accesses_manage;
|
||||||
|
|
||||||
const { mutate: updateDocInvitation } = useUpdateDocInvitation({
|
const { mutate: updateDocInvitation } = useUpdateDocInvitation({
|
||||||
onSuccess: () => {
|
|
||||||
void queryClient.invalidateQueries({
|
|
||||||
queryKey: [KEY_SUB_PAGE, { id: doc.id }],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast(
|
toast(
|
||||||
error?.data?.role?.[0] ?? t('Error during update invitation'),
|
error?.data?.role?.[0] ?? t('Error during update invitation'),
|
||||||
@@ -69,11 +62,6 @@ export const DocShareInvitationItem = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { mutate: removeDocInvitation } = useDeleteDocInvitation({
|
const { mutate: removeDocInvitation } = useDeleteDocInvitation({
|
||||||
onSuccess: () => {
|
|
||||||
void queryClient.invalidateQueries({
|
|
||||||
queryKey: [KEY_SUB_PAGE, { id: doc.id }],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
toast(
|
toast(
|
||||||
error?.data?.role?.[0] ?? t('Error during delete invitation'),
|
error?.data?.role?.[0] ?? t('Error during delete invitation'),
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
|
import { VariantType, useToastProvider } from '@openfun/cunningham-react';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
@@ -7,7 +6,7 @@ import { Box } from '@/components';
|
|||||||
import { QuickSearchData } from '@/components/quick-search';
|
import { QuickSearchData } from '@/components/quick-search';
|
||||||
import { QuickSearchGroup } from '@/components/quick-search/QuickSearchGroup';
|
import { QuickSearchGroup } from '@/components/quick-search/QuickSearchGroup';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
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 { useDocAccesses, useUpdateDocAccess } from '../api';
|
||||||
import { useWhoAmI } from '../hooks/';
|
import { useWhoAmI } from '../hooks/';
|
||||||
@@ -26,7 +25,6 @@ export const DocShareMemberItem = ({
|
|||||||
isInherited = false,
|
isInherited = false,
|
||||||
}: Props) => {
|
}: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const queryClient = useQueryClient();
|
|
||||||
const { isLastOwner } = useWhoAmI(access);
|
const { isLastOwner } = useWhoAmI(access);
|
||||||
const { toast } = useToastProvider();
|
const { toast } = useToastProvider();
|
||||||
|
|
||||||
@@ -39,14 +37,6 @@ export const DocShareMemberItem = ({
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const { mutate: updateDocAccess } = useUpdateDocAccess({
|
const { mutate: updateDocAccess } = useUpdateDocAccess({
|
||||||
onSuccess: () => {
|
|
||||||
if (!doc) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
void queryClient.invalidateQueries({
|
|
||||||
queryKey: [KEY_SUB_PAGE, { id: doc.id }],
|
|
||||||
});
|
|
||||||
},
|
|
||||||
onError: () => {
|
onError: () => {
|
||||||
toast(t('Error while updating the member role.'), VariantType.ERROR, {
|
toast(t('Error while updating the member role.'), VariantType.ERROR, {
|
||||||
duration: 4000,
|
duration: 4000,
|
||||||
|
|||||||
@@ -9,11 +9,7 @@ export type DocsTreeParams = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getDocTree = async ({ docId }: DocsTreeParams): Promise<Doc> => {
|
export const getDocTree = async ({ docId }: DocsTreeParams): Promise<Doc> => {
|
||||||
const searchParams = new URLSearchParams();
|
const response = await fetchAPI(`documents/${docId}/tree/`);
|
||||||
|
|
||||||
const response = await fetchAPI(
|
|
||||||
`documents/${docId}/tree/?${searchParams.toString()}`,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new APIError(
|
throw new APIError(
|
||||||
@@ -29,10 +25,7 @@ export const KEY_DOC_TREE = 'doc-tree';
|
|||||||
|
|
||||||
export function useDocTree(
|
export function useDocTree(
|
||||||
params: DocsTreeParams,
|
params: DocsTreeParams,
|
||||||
queryConfig?: Omit<
|
queryConfig?: UseQueryOptions<Doc, APIError, Doc>,
|
||||||
UseQueryOptions<Doc, APIError, Doc>,
|
|
||||||
'queryKey' | 'queryFn'
|
|
||||||
>,
|
|
||||||
) {
|
) {
|
||||||
return useQuery<Doc, APIError, Doc>({
|
return useQuery<Doc, APIError, Doc>({
|
||||||
queryKey: [KEY_DOC_TREE, params],
|
queryKey: [KEY_DOC_TREE, params],
|
||||||
|
|||||||
@@ -4,17 +4,12 @@ import {
|
|||||||
useTreeContext,
|
useTreeContext,
|
||||||
} from '@gouvfr-lasuite/ui-kit';
|
} from '@gouvfr-lasuite/ui-kit';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
import { Box, Icon, Text } from '@/components';
|
import { Box, Icon, Text } from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
import {
|
import { Doc, useTrans } from '@/features/docs/doc-management';
|
||||||
Doc,
|
|
||||||
KEY_SUB_PAGE,
|
|
||||||
useDoc,
|
|
||||||
useTrans,
|
|
||||||
} from '@/features/docs/doc-management';
|
|
||||||
import { useLeftPanelStore } from '@/features/left-panel';
|
import { useLeftPanelStore } from '@/features/left-panel';
|
||||||
import { useResponsiveStore } from '@/stores';
|
import { useResponsiveStore } from '@/stores';
|
||||||
|
|
||||||
@@ -31,8 +26,7 @@ const ItemTextCss = css`
|
|||||||
-webkit-box-orient: vertical;
|
-webkit-box-orient: vertical;
|
||||||
`;
|
`;
|
||||||
|
|
||||||
type Props = TreeViewNodeProps<Doc>;
|
export const DocSubPageItem = (props: TreeViewNodeProps<Doc>) => {
|
||||||
export const DocSubPageItem = (props: Props) => {
|
|
||||||
const doc = props.node.data.value as Doc;
|
const doc = props.node.data.value as Doc;
|
||||||
const treeContext = useTreeContext<Doc>();
|
const treeContext = useTreeContext<Doc>();
|
||||||
const { untitledDocument } = useTrans();
|
const { untitledDocument } = useTrans();
|
||||||
@@ -44,28 +38,6 @@ export const DocSubPageItem = (props: Props) => {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { togglePanel } = useLeftPanelStore();
|
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 afterCreate = (createdDoc: Doc) => {
|
||||||
const actualChildren = node.data.children ?? [];
|
const actualChildren = node.data.children ?? [];
|
||||||
|
|
||||||
|
|||||||
@@ -5,52 +5,46 @@ import {
|
|||||||
useTreeContext,
|
useTreeContext,
|
||||||
} from '@gouvfr-lasuite/ui-kit';
|
} from '@gouvfr-lasuite/ui-kit';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useState } from 'react';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
import { Box, StyledLink } from '@/components';
|
import { Box, StyledLink } from '@/components';
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
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 { SimpleDocItem } from '@/docs/docs-grid';
|
||||||
|
|
||||||
import { useDocTree } from '../api/useDocTree';
|
import { KEY_DOC_TREE, useDocTree } from '../api/useDocTree';
|
||||||
import { useMoveDoc } from '../api/useMove';
|
import { useMoveDoc } from '../api/useMove';
|
||||||
|
import { findIndexInTree } from '../utils';
|
||||||
|
|
||||||
import { DocSubPageItem } from './DocSubPageItem';
|
import { DocSubPageItem } from './DocSubPageItem';
|
||||||
import { DocTreeItemActions } from './DocTreeItemActions';
|
import { DocTreeItemActions } from './DocTreeItemActions';
|
||||||
|
|
||||||
type DocTreeProps = {
|
type DocTreeProps = {
|
||||||
initialTargetId: string;
|
currentDoc: Doc;
|
||||||
};
|
};
|
||||||
export const DocTree = ({ initialTargetId }: DocTreeProps) => {
|
|
||||||
|
export const DocTree = ({ currentDoc }: DocTreeProps) => {
|
||||||
const { spacingsTokens } = useCunninghamTheme();
|
const { spacingsTokens } = useCunninghamTheme();
|
||||||
const [rootActionsOpen, setRootActionsOpen] = useState(false);
|
const [rootActionsOpen, setRootActionsOpen] = useState(false);
|
||||||
const treeContext = useTreeContext<Doc>();
|
const treeContext = useTreeContext<Doc | null>();
|
||||||
const { currentDoc } = useDocStore();
|
|
||||||
const router = useRouter();
|
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>(
|
const [initialOpenState, setInitialOpenState] = useState<OpenMap | undefined>(
|
||||||
undefined,
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
const { mutate: moveDoc } = useMoveDoc();
|
const { mutate: moveDoc } = useMoveDoc();
|
||||||
|
|
||||||
const { data } = useDocTree({
|
const { data: tree, isFetching } = useDocTree(
|
||||||
docId: initialTargetId,
|
{
|
||||||
});
|
docId: currentDoc.id,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
enabled: !!!treeContext?.root?.id,
|
||||||
|
queryKey: [KEY_DOC_TREE, { id: currentDoc.id }],
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const handleMove = (result: TreeViewMoveResult) => {
|
const handleMove = (result: TreeViewMoveResult) => {
|
||||||
moveDoc({
|
moveDoc({
|
||||||
@@ -61,12 +55,55 @@ export const DocTree = ({ initialTargetId }: DocTreeProps) => {
|
|||||||
treeContext?.treeData.handleMove(result);
|
treeContext?.treeData.handleMove(result);
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
/**
|
||||||
if (!data) {
|
* This function resets the tree states.
|
||||||
|
*/
|
||||||
|
const resetStateTree = useCallback(() => {
|
||||||
|
if (!treeContext?.root?.id) {
|
||||||
return;
|
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 ?? [];
|
const children = rootChildren ?? [];
|
||||||
treeContext?.setRoot(root);
|
treeContext?.setRoot(root);
|
||||||
const initialOpenState: OpenMap = {};
|
const initialOpenState: OpenMap = {};
|
||||||
@@ -84,50 +121,30 @@ export const DocTree = ({ initialTargetId }: DocTreeProps) => {
|
|||||||
|
|
||||||
treeContext?.treeData.resetTree(children);
|
treeContext?.treeData.resetTree(children);
|
||||||
setInitialOpenState(initialOpenState);
|
setInitialOpenState(initialOpenState);
|
||||||
if (initialTargetId === root.id) {
|
}, [tree, treeContext, isFetching]);
|
||||||
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]);
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This effect is used to select the current document in the tree
|
||||||
|
*/
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (!treeContext || !treeContext.root?.id) {
|
||||||
!currentDoc ||
|
|
||||||
(previousDocId.current && previousDocId.current === currentDoc.id)
|
|
||||||
) {
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const item = treeContext?.treeData.getNode(currentDoc?.id ?? '');
|
if (currentDoc.id === treeContext?.root?.id) {
|
||||||
if (!item && currentDoc.id !== rootNode?.id) {
|
treeContext?.treeData.setSelectedNode(treeContext?.root);
|
||||||
treeContext?.treeData.resetTree([]);
|
} else {
|
||||||
treeContext?.setRoot(currentDoc);
|
treeContext?.treeData.selectNodeById(currentDoc.id);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
}, [currentDoc, treeContext]);
|
||||||
|
|
||||||
treeContext?.treeData.setSelectedNode(currentDoc);
|
if (!treeContext || !treeContext.root) {
|
||||||
}, [currentDoc, rootNode?.id, treeContext]);
|
|
||||||
|
|
||||||
const rootIsSelected =
|
|
||||||
treeContext?.treeData.selectedNode?.id === treeContext?.root?.id;
|
|
||||||
|
|
||||||
if (!initialTargetId || !treeContext) {
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const rootIsSelected =
|
||||||
|
treeContext.treeData.selectedNode?.id === treeContext.root.id;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
data-testid="doc-tree"
|
data-testid="doc-tree"
|
||||||
@@ -178,42 +195,38 @@ export const DocTree = ({ initialTargetId }: DocTreeProps) => {
|
|||||||
}
|
}
|
||||||
`}
|
`}
|
||||||
>
|
>
|
||||||
{treeContext.root !== null && rootNode && (
|
<StyledLink
|
||||||
<StyledLink
|
$css={css`
|
||||||
$css={css`
|
width: 100%;
|
||||||
width: 100%;
|
`}
|
||||||
`}
|
href={`/docs/${treeContext.root.id}`}
|
||||||
href={`/docs/${treeContext.root.id}`}
|
onClick={(e) => {
|
||||||
onClick={(e) => {
|
e.stopPropagation();
|
||||||
e.stopPropagation();
|
e.preventDefault();
|
||||||
e.preventDefault();
|
treeContext.treeData.setSelectedNode(
|
||||||
treeContext.treeData.setSelectedNode(
|
treeContext.root ?? undefined,
|
||||||
treeContext.root ?? undefined,
|
);
|
||||||
);
|
router.push(`/docs/${treeContext?.root?.id}`);
|
||||||
router.push(`/docs/${treeContext?.root?.id}`);
|
}}
|
||||||
}}
|
>
|
||||||
>
|
<Box $direction="row" $align="center" $width="100%">
|
||||||
<Box $direction="row" $align="center" $width="100%">
|
<SimpleDocItem doc={treeContext.root} showAccesses={true} />
|
||||||
<SimpleDocItem doc={rootNode} showAccesses={true} />
|
<DocTreeItemActions
|
||||||
<div className="doc-tree-root-item-actions">
|
doc={treeContext.root}
|
||||||
<DocTreeItemActions
|
onCreateSuccess={(createdDoc) => {
|
||||||
doc={rootNode}
|
const newDoc = {
|
||||||
onCreateSuccess={(createdDoc) => {
|
...createdDoc,
|
||||||
const newDoc = {
|
children: [],
|
||||||
...createdDoc,
|
childrenCount: 0,
|
||||||
children: [],
|
parentId: treeContext.root?.id ?? undefined,
|
||||||
childrenCount: 0,
|
};
|
||||||
parentId: treeContext.root?.id ?? undefined,
|
treeContext?.treeData.addChild(null, newDoc);
|
||||||
};
|
}}
|
||||||
treeContext?.treeData.addChild(null, newDoc);
|
isOpen={rootActionsOpen}
|
||||||
}}
|
onOpenChange={setRootActionsOpen}
|
||||||
isOpen={rootActionsOpen}
|
/>
|
||||||
onOpenChange={setRootActionsOpen}
|
</Box>
|
||||||
/>
|
</StyledLink>
|
||||||
</div>
|
|
||||||
</Box>
|
|
||||||
</StyledLink>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
@@ -227,20 +240,17 @@ export const DocTree = ({ initialTargetId }: DocTreeProps) => {
|
|||||||
undefined
|
undefined
|
||||||
}
|
}
|
||||||
canDrop={({ parentNode }) => {
|
canDrop={({ parentNode }) => {
|
||||||
if (!rootNode) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const parentDoc = parentNode?.data.value as Doc;
|
const parentDoc = parentNode?.data.value as Doc;
|
||||||
if (!parentDoc) {
|
if (!parentDoc) {
|
||||||
return rootNode?.abilities.move;
|
return currentDoc.abilities.move;
|
||||||
}
|
}
|
||||||
return parentDoc?.abilities.move;
|
return parentDoc.abilities.move;
|
||||||
}}
|
}}
|
||||||
canDrag={(node) => {
|
canDrag={(node) => {
|
||||||
const doc = node.value as Doc;
|
const doc = node.value as Doc;
|
||||||
return doc.abilities.move;
|
return doc.abilities.move;
|
||||||
}}
|
}}
|
||||||
rootNodeId={treeContext.root?.id ?? ''}
|
rootNodeId={treeContext.root.id}
|
||||||
renderNode={DocSubPageItem}
|
renderNode={DocSubPageItem}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
} from '@gouvfr-lasuite/ui-kit';
|
} from '@gouvfr-lasuite/ui-kit';
|
||||||
import { useModal } from '@openfun/cunningham-react';
|
import { useModal } from '@openfun/cunningham-react';
|
||||||
import { useRouter } from 'next/router';
|
import { useRouter } from 'next/router';
|
||||||
import { Fragment } from 'react';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { css } from 'styled-components';
|
import { css } from 'styled-components';
|
||||||
|
|
||||||
@@ -25,9 +24,9 @@ import { useTreeUtils } from '../hooks';
|
|||||||
|
|
||||||
type DocTreeItemActionsProps = {
|
type DocTreeItemActionsProps = {
|
||||||
doc: Doc;
|
doc: Doc;
|
||||||
|
isOpen?: boolean;
|
||||||
parentId?: string | null;
|
parentId?: string | null;
|
||||||
onCreateSuccess?: (newDoc: Doc) => void;
|
onCreateSuccess?: (newDoc: Doc) => void;
|
||||||
isOpen?: boolean;
|
|
||||||
onOpenChange?: (isOpen: boolean) => void;
|
onOpenChange?: (isOpen: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,10 +44,12 @@ export const DocTreeItemActions = ({
|
|||||||
const copyLink = useCopyDocLink(doc.id);
|
const copyLink = useCopyDocLink(doc.id);
|
||||||
const { isCurrentParent } = useTreeUtils(doc);
|
const { isCurrentParent } = useTreeUtils(doc);
|
||||||
const { mutate: detachDoc } = useDetachDoc();
|
const { mutate: detachDoc } = useDetachDoc();
|
||||||
const treeContext = useTreeContext<Doc>();
|
const treeContext = useTreeContext<Doc | null>();
|
||||||
const { mutate: duplicateDoc } = useDuplicateDoc({
|
const { mutate: duplicateDoc } = useDuplicateDoc({
|
||||||
onSuccess: (data) => {
|
onSuccess: (duplicatedDoc) => {
|
||||||
void router.push(`/docs/${data.id}`);
|
// 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 },
|
{ documentId: doc.id, rootId: treeContext.root.id },
|
||||||
{
|
{
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
treeContext.treeData.deleteNode(doc.id);
|
|
||||||
if (treeContext.root) {
|
if (treeContext.root) {
|
||||||
treeContext.treeData.setSelectedNode(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 = () => {
|
const afterDelete = () => {
|
||||||
if (parentId) {
|
if (parentId) {
|
||||||
treeContext?.treeData.deleteNode(doc.id);
|
void router.push(`/docs/${parentId}`).then(() => {
|
||||||
void router.push(`/docs/${parentId}`);
|
setTimeout(() => {
|
||||||
|
treeContext?.treeData.deleteNode(doc.id);
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
} else if (doc.id === treeContext?.root?.id && !parentId) {
|
} else if (doc.id === treeContext?.root?.id && !parentId) {
|
||||||
void router.push(`/docs/`);
|
void router.push(`/docs/`);
|
||||||
} else if (treeContext && treeContext.root) {
|
} else if (treeContext && treeContext.root) {
|
||||||
treeContext?.treeData.deleteNode(doc.id);
|
void router.push(`/docs/${treeContext.root.id}`).then(() => {
|
||||||
void router.push(`/docs/${treeContext.root.id}`);
|
setTimeout(() => {
|
||||||
|
treeContext?.treeData.deleteNode(doc.id);
|
||||||
|
}, 100);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Fragment>
|
<Box className="doc-tree-root-item-actions">
|
||||||
<Box
|
<Box
|
||||||
$direction="row"
|
$direction="row"
|
||||||
$align="center"
|
$align="center"
|
||||||
@@ -187,6 +197,6 @@ export const DocTreeItemActions = ({
|
|||||||
afterDelete={afterDelete}
|
afterDelete={afterDelete}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</Fragment>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1 @@
|
|||||||
|
export * from './DocTree';
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
export * from './api';
|
export * from './api';
|
||||||
|
export * from './components';
|
||||||
export * from './hooks';
|
export * from './hooks';
|
||||||
export * from './utils';
|
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';
|
import { Doc } from '../doc-management';
|
||||||
|
|
||||||
@@ -9,3 +9,24 @@ export const subPageToTree = (children: Doc[]): TreeViewDataType<Doc>[] => {
|
|||||||
});
|
});
|
||||||
return children;
|
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 { Box } from '@/components';
|
||||||
import { Doc, useDocStore } from '@/docs/doc-management';
|
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 = () => {
|
export const LeftPanelDocContent = () => {
|
||||||
const { currentDoc } = useDocStore();
|
const { currentDoc } = useDocStore();
|
||||||
@@ -20,9 +20,7 @@ export const LeftPanelDocContent = () => {
|
|||||||
$css="width: 100%; overflow-y: auto; overflow-x: hidden;"
|
$css="width: 100%; overflow-y: auto; overflow-x: hidden;"
|
||||||
className="--docs--left-panel-doc-content"
|
className="--docs--left-panel-doc-content"
|
||||||
>
|
>
|
||||||
{tree.initialTargetId && (
|
<DocTree currentDoc={currentDoc} />
|
||||||
<DocTree initialTargetId={tree.initialTargetId} />
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user