This commit is contained in:
Nathan Panchout
2025-03-24 17:15:14 +01:00
parent 615ab564cc
commit 2b2a579d9f
18 changed files with 301 additions and 222 deletions
+2 -1
View File
@@ -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",
@@ -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 (
<Text
$isMaterialIcon={!isFilled}
{...textProps}
className={
isFilled
? `material-icons-filled ${textProps.className}`
: textProps.className
}
>
<Text $isMaterialIcon={textProps.$isMaterialIcon ?? true} {...textProps}>
{iconName}
</Text>
);
@@ -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)<TextProps>`
const Text = forwardRef<HTMLElement, ComponentPropsWithRef<typeof TextStyled>>(
({ className, $isMaterialIcon, ...props }, ref) => {
const isFilled = $isMaterialIcon === 'filled';
const isMaterialIcon =
typeof $isMaterialIcon === 'boolean' && $isMaterialIcon;
return (
<TextStyled
ref={ref}
as="span"
$theme="greyscale"
$variation="text"
className={`${className || ''}${$isMaterialIcon ? ' material-icons' : ''}`}
className={clsx(className || '', {
'material-icons': isMaterialIcon,
'material-icons-filled': isFilled,
})}
{...props}
/>
);
@@ -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,
);
},
});
@@ -6,6 +6,7 @@ import { Doc } from '../types';
export type DocParams = {
id: string;
isTree?: boolean;
};
export const getDoc = async ({ id }: DocParams): Promise<Doc> => {
@@ -19,14 +20,15 @@ export const getDoc = async ({ id }: DocParams): Promise<Doc> => {
};
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<Doc, APIError, Doc>,
queryConfig?: Omit<UseQueryOptions<Doc, APIError, Doc>, 'queryFn'>,
) {
return useQuery<Doc, APIError, Doc>({
queryKey: [KEY_DOC, param],
queryKey: queryConfig?.queryKey ?? [KEY_DOC, param],
queryFn: () => getDoc(param),
...queryConfig,
});
@@ -97,7 +97,7 @@ export const ModalRemoveDoc = ({
{!isError && (
<Text $size="sm" $variation="600">
{t('Are you sure you want to delete the document "{{title}}"?', {
title: doc.title,
title: doc.title ?? t('Untitled document'),
})}
</Text>
)}
@@ -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<DocSearchFiltersValues>(
@@ -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?.();
@@ -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>(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 }],
});
},
},
);
@@ -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(
@@ -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, {
@@ -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<Doc> & {
treeData: ReturnType<typeof useTree<Doc>>;
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) => {
<TreeViewItem
{...props}
loadChildren={() =>
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}
</Text>
{doc.nb_accesses_direct > 1 && (
<Icon isFilled iconName="group" $size="16px" $variation="400" />
<Icon
$isMaterialIcon="filled"
iconName="group"
$size="16px"
$variation="400"
/>
)}
</Box>
@@ -134,6 +167,7 @@ export const DocSubPageItem = ({ doc, setSelectedNode, ...props }: Props) => {
>
<DocTreeItemActions
doc={doc}
treeData={treeData}
parentId={node.data.parentKey}
onCreateSuccess={afterCreate}
/>
@@ -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<OpenMap | undefined>(
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 && (
<StyledLink
$css={css`
width: 100%;
`}
href={`/docs/${treeData.root.id}`}
href={`/docs/${store.root.id}`}
onClick={(e) => {
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}`);
}}
>
<Box $direction="row" $align="center" $width="100%">
<SimpleDocItem doc={treeData.root} showAccesses={true} />
<SimpleDocItem doc={store.root} showAccesses={true} />
<div className="doc-tree-root-item-actions">
<DocTreeItemActions
doc={treeData.root}
doc={store.root}
treeData={treeData}
onCreateSuccess={(createdDoc) => {
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);
}}
/>
</div>
@@ -145,27 +180,54 @@ export const DocTree = ({ initialTargetId }: DocTreeProps) => {
</Box>
</Box>
</SeparatedSection>
<button
onClick={() => {
const children = data?.children ?? [];
const newDoc = {
id: '1',
children: [],
childrenCount: 0,
title: 'TITI',
creator: 'test',
is_favorite: false,
};
{initialOpenState && (
// Ajout des propriétés manquantes pour correspondre au type Doc
const completeDoc = {
...newDoc,
link_reach: LinkReach.PUBLIC,
link_role: LinkRole.EDITOR,
user_roles: [],
// Ajoutez ici les autres propriétés requises par le type Doc
};
children.push(completeDoc);
buildDocTree(data);
}}
>
Refresh
</button>
{initialOpenState && treeData.nodes.length > 0 && (
<TreeView
handleMove={handleMove}
initialOpenState={initialOpenState}
selectedNodeId={
treeData.tree.selectedNode?.id ??
treeData.initialTargetId ??
undefined
treeData.selectedNode?.id ?? store.initialTargetId ?? undefined
}
treeData={treeData.tree.nodes ?? []}
rootNodeId={treeData.root?.id ?? ''}
treeData={treeData.nodes ?? []}
rootNodeId={store.root?.id ?? ''}
renderNode={(props) => {
if (treeData === undefined) {
return null;
}
return (
<DocSubPageItem
{...props}
treeData={treeData}
doc={props.node.data.value as Doc}
loadChildren={(node) =>
treeData.tree.handleLoadChildren(node.id)
}
setSelectedNode={treeData.tree.setSelectedNode}
loadChildren={(node) => treeData.handleLoadChildren(node.id)}
setSelectedNode={(node) => treeData.setSelectedNode(node)}
/>
);
}}
@@ -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<typeof useTree<Doc>>;
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"
>
<Icon $variation="800" $theme="primary" isFilled iconName="add_box" />
<Icon
$variation="800"
$theme="primary"
$isMaterialIcon="filled"
iconName="add_box"
/>
</BoxButton>
</Box>
{deleteModal.isOpen && (
@@ -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<typeof useTree<Doc>>;
root: Doc | null;
initialTargetId?: string | null;
initialRootId?: string | null;
setRoot: (doc: Doc | null) => void;
setInitialTargetId: (id: string) => void;
refreshRoot: () => Promise<void>;
}
// Création du contexte avec une valeur par défaut
const DocTreeContext = createContext<DocTreeContextType | undefined>(undefined);
// Props pour le provider
interface DocTreeProviderProps {
children: ReactNode;
initialData?: TreeViewDataType<Doc>[];
initialRootId?: string;
initialTargetId?: string;
}
// Provider qui expose les fonctionnalités de l'arbre
export const DocTreeProvider: React.FC<DocTreeProviderProps> = ({
children,
initialData = [],
initialRootId,
initialTargetId: targetId,
}) => {
const [root, setRoot] = useState<Doc | null>(null);
const [initialTargetId, setInitialTargetId] = useState<string | null>(
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 (
<DocTreeContext.Provider value={value}>{children}</DocTreeContext.Provider>
);
};
// 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<Doc>[] => {
export const subPageToTree = (
children: Doc[],
callback?: (doc: Doc) => void,
): TreeViewDataType<Doc>[] => {
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<DocTreeStore>((set) => ({
root: null,
selectedNode: null,
initialTargetId: undefined,
initialRootId: undefined,
setRoot: (doc) => set({ root: doc }),
setInitialTargetId: (id) => set({ initialTargetId: id }),
setSelectedNode: (node) => set({ selectedNode: node }),
}));
@@ -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 && (
<DocTree initialTargetId={tree.initialTargetId} />
{treeStore.initialTargetId && (
<DocTree initialTargetId={treeStore.initialTargetId} />
)}
</Box>
);
@@ -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();
@@ -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() {
<meta name="robots" content="noindex" />
</Head>
<DocTreeProvider initialTargetId={id}>
<MainLayout>
<DocPage id={id} />
</MainLayout>
</DocTreeProvider>
<MainLayout>
{treeStore.initialTargetId && <DocPage id={id} />}
</MainLayout>
</>
);
}
@@ -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
+1 -3
View File
@@ -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"