This commit is contained in:
Nathan Panchout
2025-02-27 08:49:56 +01:00
parent 8bc56f0000
commit a571f9bc6e
14 changed files with 196 additions and 69 deletions
+1
View File
@@ -45,6 +45,7 @@
"react-dom": "*",
"react-i18next": "15.4.0",
"react-intersection-observer": "9.15.1",
"react-resizable-panels": "^2.1.7",
"react-select": "5.10.0",
"sass": "1.83.4",
"styled-components": "6.1.15",
@@ -325,7 +325,7 @@ export const TreeViewNode = <T,>({
) : (
<>
{isLoading ? (
<Box $padding={{ horizontal: '4px' }}>
<Box>
<Loader />
</Box>
) : (
@@ -6,6 +6,7 @@ import { TreeViewDataType } from '@/features/docs/doc-tree/types/tree';
interface TreeStore<T> {
treeData: TreeViewDataType<T>[];
selectedNode: TreeViewDataType<T> | null;
rootId: string | undefined;
setSelectedNode: (node: TreeViewDataType<T> | null) => void;
setTreeData: (data: TreeViewDataType<T>[]) => void;
updateNode: (nodeId: string, newData: Partial<TreeViewDataType<T>>) => void;
@@ -14,6 +15,8 @@ interface TreeStore<T> {
addChildNode: (parentId: string, newNode: TreeViewDataType<T>) => void;
refreshNode: (nodeId: string) => void;
findNode: (nodeId: string) => TreeViewDataType<T> | null;
setRootId: (id?: string) => void;
reset: () => void;
}
export const createTreeStore = <T>(
@@ -22,12 +25,16 @@ export const createTreeStore = <T>(
create<TreeStore<T>>((set, get) => ({
treeData: [],
selectedNode: null,
rootId: undefined,
setSelectedNode: (node) => {
set({ selectedNode: node });
},
setTreeData: (data) => {
set({ treeData: data });
},
setRootId: (id) => {
set({ rootId: id });
},
refreshNode: (nodeId) => {
set((state) => {
const node = state.findNode(nodeId);
@@ -149,6 +156,9 @@ export const createTreeStore = <T>(
return findNodeInTree(get().treeData);
},
reset: () => {
set({ treeData: [], selectedNode: null, rootId: undefined });
},
}));
// Créer une instance du store
@@ -36,11 +36,11 @@ export const ModalRenameDoc = ({ onClose, doc }: ModalRenameDocProps) => {
{t('Cancel')}
</Button>
<Button
aria-label={t('Confirm deletion')}
aria-label={t('Confirm rename')}
fullWidth
onClick={() => console.log('rename', title)}
>
{t('Delete')}
{t('Rename')}
</Button>
</>
}
@@ -6,7 +6,7 @@ import { DocsResponse } from '../../doc-management';
export type DocsChildrenParams = {
docId: string;
page: number;
page?: number;
page_size?: number;
};
@@ -0,0 +1,37 @@
import { useMutation } from '@tanstack/react-query';
import { APIError, errorCauses, fetchAPI } from '@/api';
import { TreeViewMoveModeEnum } from '../types/tree';
export type MoveDocParam = {
sourceDocumentId: string;
targetDocumentId: string;
position: TreeViewMoveModeEnum;
};
export const moveDoc = async ({
sourceDocumentId,
targetDocumentId,
position,
}: MoveDocParam): Promise<void> => {
const response = await fetchAPI(`documents/${sourceDocumentId}/move/`, {
method: 'POST',
body: JSON.stringify({
target_document_id: targetDocumentId,
position,
}),
});
if (!response.ok) {
throw new APIError('Failed to move the doc', await errorCauses(response));
}
return response.json() as Promise<void>;
};
export function useMoveDoc() {
return useMutation<void, APIError, MoveDocParam>({
mutationFn: moveDoc,
});
}
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { OpenMap } from 'react-arborist/dist/module/state/open-slice';
import { css } from 'styled-components';
import { fetchAPI } from '@/api';
import { Box, SeparatedSection, StyledLink } from '@/components';
import { TreeView } from '@/components/common/tree/TreeView';
import { useTreeStore } from '@/components/common/tree/treeStore';
@@ -11,6 +10,7 @@ import { useCunninghamTheme } from '@/cunningham';
import { Doc } from '../../doc-management';
import { SimpleDocItem } from '../../docs-grid';
import { useDocTree } from '../api/useDocTree';
import { useMoveDoc } from '../api/useMove';
import { TreeViewDataType, TreeViewMoveResult } from '../types/tree';
import { DocTreeItem } from './DocTreeItem';
@@ -24,6 +24,7 @@ export const DocTree = ({ docId }: Props) => {
const [rootNode, setRootNode] = useState<Doc | null>(null);
const { spacingsTokens } = useCunninghamTheme();
const spacing = spacingsTokens();
const moveDoc = useMoveDoc();
const [initialOpenState, setInitialOpenState] = useState<OpenMap | undefined>(
undefined,
@@ -35,6 +36,7 @@ export const DocTree = ({ docId }: Props) => {
refreshNode,
setTreeData: setTreeDataStore,
treeData: treeDataStore,
setRootId,
} = useTreeStore();
const { data, isLoading, isFetching, isRefetching } = useDocTree({
@@ -43,24 +45,27 @@ export const DocTree = ({ docId }: Props) => {
page: 1,
});
const afterMove = async (
const afterMove = (
result: TreeViewMoveResult,
newTreeData: TreeViewDataType<Doc>[],
) => {
const { targetNodeId, mode: position, sourceNodeId, oldParentId } = result;
await fetchAPI(`documents/${sourceNodeId}/move/`, {
method: 'POST',
body: JSON.stringify({
target_document_id: targetNodeId,
moveDoc.mutate(
{
sourceDocumentId: sourceNodeId,
targetDocumentId: targetNodeId,
position,
}),
});
setTreeDataStore(newTreeData);
if (oldParentId) {
refreshNode(oldParentId);
}
refreshNode(targetNodeId);
},
{
onSuccess: () => {
setTreeDataStore(newTreeData);
if (oldParentId) {
refreshNode(oldParentId);
}
refreshNode(targetNodeId);
},
},
);
};
useEffect(() => {
@@ -103,11 +108,12 @@ export const DocTree = ({ docId }: Props) => {
};
root.children = serialize(root.children ?? [], docId);
console.log('initialOpenState', initialOpenState);
setInitialOpenState(initialOpenState);
setRootNode(root);
setRootId(root.id);
setTreeDataStore(root.children ?? []);
}, [data, setTreeDataStore, docId, setSelectedNode, rootNode]);
}, [data, setTreeDataStore, docId, setSelectedNode, rootNode, setRootId]);
const isRootNodeSelected = !selectedNode
? true
@@ -2,7 +2,6 @@ import { useModal } from '@openfun/cunningham-react';
import { useRouter } from 'next/navigation';
import { Fragment, useState } from 'react';
import { NodeRendererProps } from 'react-arborist';
import { createPortal } from 'react-dom';
import { useTranslation } from 'react-i18next';
import { Box, BoxButton, DropdownMenu, Icon } from '@/components';
@@ -25,7 +24,6 @@ type DocTreeItemProps = NodeRendererProps<TreeViewDataType<DocTreeDataType>>;
export const DocTreeItem = ({ node, ...props }: DocTreeItemProps) => {
const data = node.data;
const { push } = useRouter();
const deleteModal = useModal();
@@ -38,7 +36,7 @@ export const DocTreeItem = ({ node, ...props }: DocTreeItemProps) => {
const { refetch } = useDocChildren(
{
docId: data.id,
page: 1,
page_size: 999,
},
{ enabled: false },
);
@@ -80,7 +78,7 @@ export const DocTreeItem = ({ node, ...props }: DocTreeItemProps) => {
const loadChildren = async () => {
const data = await refetch();
console.log(data, node);
const childs = data.data?.results ?? [];
const newChilds: TreeViewDataType<DocTreeDataType>[] = childs.map(
(child) => ({
@@ -96,13 +94,12 @@ export const DocTreeItem = ({ node, ...props }: DocTreeItemProps) => {
};
const afterDelete = () => {
console.log('afterDelete', node.data);
removeNode(node.data.id);
if (node.data.parentId) {
router.push(`/docs/${node.data.parentId}`);
refreshNode(node.data.parentId);
setSelectedNode(node.data);
}
removeNode(node.data.id);
};
const options = [
@@ -170,11 +167,9 @@ export const DocTreeItem = ({ node, ...props }: DocTreeItemProps) => {
{shareModal.isOpen && (
<DocShareModal doc={node.data} onClose={shareModal.close} />
)}
{renameModal.isOpen &&
createPortal(
<ModalRenameDoc onClose={renameModal.onClose} doc={node.data} />,
document.getElementById('modals') as HTMLElement,
)}
{renameModal.isOpen && (
<ModalRenameDoc onClose={renameModal.onClose} doc={node.data} />
)}
</Fragment>
);
};
@@ -45,7 +45,7 @@ export const LeftPanel = () => {
data-testid="left-panel-desktop"
$css={`
height: calc(100vh - ${HEADER_HEIGHT}px);
width: 300px;
width: 100%;
min-width: 300px;
overflow: hidden;
border-right: 1px solid ${colors['greyscale-200']};
@@ -1,11 +1,23 @@
import { useEffect } from 'react';
import { Box } from '@/components';
import { useTreeStore } from '@/components/common/tree/treeStore';
import { useDocStore } from '@/features/docs/doc-management';
import { DocTree } from '@/features/docs/doc-tree/components/DocTree';
import { useDocRootTreeStore } from '@/features/docs/doc-tree/stores/useDocRootTree';
export const LeftPanelDocContent = () => {
const { rootId } = useDocRootTreeStore();
const { currentDoc } = useDocStore();
const { currentDoc, setCurrentDoc } = useDocStore();
const { reset } = useTreeStore();
useEffect(() => {
return () => {
setCurrentDoc(undefined);
reset();
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (!currentDoc) {
return null;
@@ -33,9 +33,14 @@ export const LeftPanelHeader = ({ children }: Props) => {
const { mutate: createChildrenDoc } = useCreateChildrenDoc({
onSuccess: (doc) => {
treeStore.addRootNode(doc);
treeStore.setSelectedNode(doc);
router.push(`/docs/${doc.id}`);
if (treeStore.rootId === currentDoc?.id) {
treeStore.addRootNode(doc);
} else if (currentDoc) {
treeStore.addChildNode(currentDoc.id, doc);
} else {
treeStore.addRootNode(doc);
}
togglePanel();
},
});
@@ -1,4 +1,5 @@
import { PropsWithChildren } from 'react';
import { PropsWithChildren, useEffect, useState } from 'react';
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { css } from 'styled-components';
import { Box } from '@/components';
@@ -11,14 +12,58 @@ import { useResponsiveStore } from '@/stores';
type MainLayoutProps = {
backgroundColor?: 'white' | 'grey';
enableResize?: boolean;
};
const calculateDefaultSize = (targetWidth: number, isDesktop: boolean) => {
if (!isDesktop) {
return 0;
}
const windowWidth = window.innerWidth;
console.log('windowWidth', windowWidth);
return (targetWidth / windowWidth) * 100;
// On limite le pourcentage entre 20 et 40 pour éviter des panneaux trop petits ou trop grands
};
export function MainLayout({
children,
backgroundColor = 'white',
enableResize = false,
}: PropsWithChildren<MainLayoutProps>) {
const windowWidth = window.innerWidth;
const { isDesktop } = useResponsiveStore();
const { colorsTokens } = useCunninghamTheme();
const [minPanelSize, setMinPanelSize] = useState(
calculateDefaultSize(300, isDesktop),
);
const [maxPanelSize, setMaxPanelSize] = useState(
calculateDefaultSize(450, isDesktop),
);
useEffect(() => {
const updatePanelSize = () => {
const min = calculateDefaultSize(300, isDesktop);
const max = Math.min(calculateDefaultSize(450, isDesktop), 40);
setMinPanelSize(isDesktop ? min : 0);
if (enableResize) {
setMaxPanelSize(max);
} else {
setMaxPanelSize(min);
}
};
updatePanelSize();
window.addEventListener('resize', () => {
console.log('resize');
updatePanelSize();
});
return () => {
window.removeEventListener('resize', updatePanelSize);
};
}, [isDesktop, enableResize]);
const colors = colorsTokens();
const currentBackgroundColor = !isDesktop ? 'white' : backgroundColor;
@@ -30,29 +75,40 @@ export function MainLayout({
$margin={{ top: `${HEADER_HEIGHT}px` }}
$width="100%"
>
<LeftPanel />
<Box
as="main"
id={MAIN_LAYOUT_ID}
$align="center"
$flex={1}
$width="100%"
$height={`calc(100dvh - ${HEADER_HEIGHT}px)`}
$padding={{
all: isDesktop ? 'base' : '0',
}}
$background={
currentBackgroundColor === 'white'
? colors['greyscale-000']
: colors['greyscale-050']
}
$css={css`
overflow-y: auto;
overflow-x: clip;
`}
>
{children}
</Box>
<PanelGroup direction="horizontal">
<Panel
defaultSize={minPanelSize}
minSize={minPanelSize}
maxSize={maxPanelSize}
>
<LeftPanel />
</Panel>
<PanelResizeHandle />
<Panel>
<Box
as="main"
id={MAIN_LAYOUT_ID}
$align="center"
$flex={1}
$width="100%"
$height={`calc(100dvh - ${HEADER_HEIGHT}px)`}
$padding={{
all: isDesktop ? 'base' : '0',
}}
$background={
backgroundColor === 'white'
? colors['greyscale-000']
: colors['greyscale-050']
}
$css={css`
overflow-y: auto;
overflow-x: clip;
`}
>
{children}
</Box>
</Panel>
</PanelGroup>
</Box>
</div>
);
@@ -34,7 +34,7 @@ export function DocLayout() {
<meta name="robots" content="noindex" />
</Head>
<MainLayout>
<MainLayout enableResize={true}>
<DocPage id={id} />
</MainLayout>
</>
@@ -77,10 +77,10 @@ const DocPage = ({ id }: DocProps) => {
useEffect(() => {
return () => {
console.log('exit');
setRootId(undefined);
setCurrentDoc(undefined);
};
}, [setRootId]);
}, [setRootId, setCurrentDoc]);
useEffect(() => {
if (!docQuery || isFetching) {
+11 -6
View File
@@ -5207,7 +5207,7 @@
dependencies:
"@types/node" "*"
"@types/node@*", "@types/[email protected]", "@types/node@^22.7.5":
"@types/node@*", "@types/node@^22.7.5":
version "22.13.1"
resolved "https://registry.yarnpkg.com/@types/node/-/node-22.13.1.tgz#a2a3fefbdeb7ba6b89f40371842162fac0934f33"
integrity sha512-jK8uzQlrvXqEU91UxiK5J7pKHyzgnI1Qnl0QDHIgVGuolJhRb9EEl28Cj9b3rGR8B2lhFCtvIm5os8lFnO/1Ew==
@@ -5269,7 +5269,7 @@
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
"@types/react-dom@*", "@types/[email protected]":
"@types/react-dom@*":
version "18.3.1"
resolved "https://registry.yarnpkg.com/@types/react-dom/-/react-dom-18.3.1.tgz#1e4654c08a9cdcfb6594c780ac59b55aad42fe07"
integrity sha512-qW1Mfv8taImTthu4KoXgDfLuk4bydU6Q/TkADnDWWHwi4NX4BR+LWfTp2sVmTqRrsHvyDDTelgelxJ+SsejKKQ==
@@ -5414,7 +5414,7 @@
dependencies:
"@types/yargs-parser" "*"
"@typescript-eslint/eslint-plugin@*", "@typescript-eslint/[email protected]", "@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
"@typescript-eslint/eslint-plugin@*", "@typescript-eslint/eslint-plugin@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
version "8.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.23.0.tgz#7745f4e3e4a7ae5f6f73fefcd856fd6a074189b7"
integrity sha512-vBz65tJgRrA1Q5gWlRfvoH+w943dq9K1p1yDBY2pc+a1nbBLZp7fB9+Hk8DaALUbzjqlMfgaqlVPT1REJdkt/w==
@@ -5429,7 +5429,7 @@
natural-compare "^1.4.0"
ts-api-utils "^2.0.1"
"@typescript-eslint/parser@*", "@typescript-eslint/[email protected]", "@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
"@typescript-eslint/parser@*", "@typescript-eslint/parser@^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0":
version "8.23.0"
resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-8.23.0.tgz#57acb3b65fce48d12b70d119436e145842a30081"
integrity sha512-h2lUByouOXFAlMec2mILeELUbME5SZRN/7R9Cw2RD2lRQQY08MWMM+PmVVKKJNK1aIwqTo9t/0CvOxwPbRIE2Q==
@@ -7671,7 +7671,7 @@ eslint-visitor-keys@^4.2.0:
resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz#687bacb2af884fcdda8a6e7d65c606f46a14cd45"
integrity sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==
eslint@*, [email protected]:
eslint@*:
version "8.57.0"
resolved "https://registry.yarnpkg.com/eslint/-/eslint-8.57.0.tgz#c786a6fd0e0b68941aaf624596fb987089195668"
integrity sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==
@@ -12142,6 +12142,11 @@ react-remove-scroll@^2.6.2:
use-callback-ref "^1.3.3"
use-sidecar "^1.1.3"
react-resizable-panels@^2.1.7:
version "2.1.7"
resolved "https://registry.yarnpkg.com/react-resizable-panels/-/react-resizable-panels-2.1.7.tgz#afd29d8a3d708786a9f95183a38803c89f13c2e7"
integrity sha512-JtT6gI+nURzhMYQYsx8DKkx6bSoOGFp7A3CwMrOb8y5jFHFyqwo9m68UhmXRw57fRVJksFn1TSlm3ywEQ9vMgA==
[email protected]:
version "5.10.0"
resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.10.0.tgz#9b5f4544cfecdfc744184b87651468ee0fb6e172"
@@ -13855,7 +13860,7 @@ typed-array-length@^1.0.7:
possible-typed-array-names "^1.0.0"
reflect.getprototypeof "^1.0.6"
typescript@*, [email protected], typescript@^5.0.4:
typescript@*, typescript@^5.0.4:
version "5.7.3"
resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e"
integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==