diff --git a/app/package-lock.json b/app/package-lock.json index 91eea3d..7988949 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -15,6 +15,7 @@ "@codemirror/state": "^6.5.2", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.38.6", + "@floating-ui/react": "^0.27.16", "@mantine/core": "^8.3.7", "@mantine/hooks": "^8.3.7", "@mantine/modals": "^8.3.7", @@ -22,9 +23,9 @@ "@react-hook/resize-observer": "^2.0.2", "@tabler/icons-react": "^3.35.0", "codemirror": "^6.0.2", - "react": "^18.3.1", + "react": "^19.2.0", "react-arborist": "^3.4.3", - "react-dom": "^18.3.1", + "react-dom": "^19.2.0", "rehype-highlight": "^7.0.2", "rehype-mathjax": "^7.1.0", "rehype-react": "^8.0.0", @@ -41,8 +42,8 @@ "@testing-library/react": "^16.3.0", "@types/babel__core": "^7.20.5", "@types/node": "^24.10.1", - "@types/react": "^18.3.20", - "@types/react-dom": "^18.3.6", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", "@typescript-eslint/eslint-plugin": "^8.46.4", "@typescript-eslint/parser": "^8.32.1", "@vitejs/plugin-react": "^5.1.1", @@ -2558,32 +2559,24 @@ "undici-types": "~7.16.0" } }, - "node_modules/@types/prop-types": { - "version": "15.7.13", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.13.tgz", - "integrity": "sha512-hCZTSvwbzWGvhqxp/RqVqwU999pBf2vp7hzIjiYOsl8wqOmUxkQ6ddw1cV3l8811+kdUFus/q4d1Y3E3SyEifA==", - "devOptional": true, - "license": "MIT" - }, "node_modules/@types/react": { - "version": "18.3.20", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz", - "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==", + "version": "19.2.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz", + "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==", "devOptional": true, "license": "MIT", "dependencies": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.6", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.6.tgz", - "integrity": "sha512-nf22//wEbKXusP6E9pfOCDwFdHAX4u172eaJI4YkDRQEZiorm6KfYnSC2SWLDMVWUOWPERmJnN0ujeAfTBLvrw==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "dev": true, "license": "MIT", "peerDependencies": { - "@types/react": "^18.0.0" + "@types/react": "^19.2.0" } }, "node_modules/@types/unist": { @@ -5808,9 +5801,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -7426,13 +7419,10 @@ "license": "MIT" }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz", + "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==", "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - }, "engines": { "node": ">=0.10.0" } @@ -7494,16 +7484,15 @@ } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==", "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.2.0" } }, "node_modules/react-is": { @@ -8089,13 +8078,10 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" }, "node_modules/semver": { "version": "6.3.1", @@ -9157,12 +9143,12 @@ } }, "node_modules/use-sync-external-store": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", - "integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", + "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==", "license": "MIT", "peerDependencies": { - "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "node_modules/util-deprecate": { diff --git a/app/package.json b/app/package.json index f254f14..df7c0ba 100644 --- a/app/package.json +++ b/app/package.json @@ -35,6 +35,7 @@ "@codemirror/state": "^6.5.2", "@codemirror/theme-one-dark": "^6.1.3", "@codemirror/view": "^6.38.6", + "@floating-ui/react": "^0.27.16", "@mantine/core": "^8.3.7", "@mantine/hooks": "^8.3.7", "@mantine/modals": "^8.3.7", @@ -42,9 +43,9 @@ "@react-hook/resize-observer": "^2.0.2", "@tabler/icons-react": "^3.35.0", "codemirror": "^6.0.2", - "react": "^18.3.1", + "react": "^19.2.0", "react-arborist": "^3.4.3", - "react-dom": "^18.3.1", + "react-dom": "^19.2.0", "rehype-highlight": "^7.0.2", "rehype-mathjax": "^7.1.0", "rehype-react": "^8.0.0", @@ -61,8 +62,8 @@ "@testing-library/react": "^16.3.0", "@types/babel__core": "^7.20.5", "@types/node": "^24.10.1", - "@types/react": "^18.3.20", - "@types/react-dom": "^18.3.6", + "@types/react": "^19.2.5", + "@types/react-dom": "^19.2.3", "@typescript-eslint/eslint-plugin": "^8.46.4", "@typescript-eslint/parser": "^8.32.1", "@vitejs/plugin-react": "^5.1.1", diff --git a/app/src/components/files/FileTree.test.tsx b/app/src/components/files/FileTree.test.tsx index 5a17311..f3df95b 100644 --- a/app/src/components/files/FileTree.test.tsx +++ b/app/src/components/files/FileTree.test.tsx @@ -69,7 +69,19 @@ vi.mock('react-arborist', () => ({ // Mock resize observer hook vi.mock('@react-hook/resize-observer', () => ({ - default: vi.fn(), + default: vi.fn( + ( + _target: unknown, + callback: (entry: { contentRect: { width: number; height: number } }) => void + ) => { + // Immediately call the callback with a mock entry to provide size + if (callback) { + setTimeout(() => { + callback({ contentRect: { width: 300, height: 600 } }); + }, 0); + } + } + ), })); // Mock contexts @@ -172,7 +184,7 @@ describe('FileTree', () => { vi.clearAllMocks(); }); - it('renders file tree with files', () => { + it('renders file tree with files', async () => { const { getByTestId } = render( { ); - expect(getByTestId('file-tree')).toBeInTheDocument(); + await waitFor(() => { + expect(getByTestId('file-tree')).toBeInTheDocument(); + }); expect(getByTestId('file-node-1')).toBeInTheDocument(); expect(getByTestId('file-node-2')).toBeInTheDocument(); }); @@ -201,6 +215,10 @@ describe('FileTree', () => { ); + await waitFor(() => { + expect(getByTestId('file-node-1')).toBeInTheDocument(); + }); + const fileNode = getByTestId('file-node-1'); fireEvent.click(fileNode); @@ -209,7 +227,7 @@ describe('FileTree', () => { }); }); - it('filters out hidden files when showHiddenFiles is false', () => { + it('filters out hidden files when showHiddenFiles is false', async () => { const { getByTestId, queryByTestId } = render( { ); + await waitFor(() => { + expect(getByTestId('file-node-1')).toBeInTheDocument(); + }); + // Should show regular files expect(getByTestId('file-node-1')).toBeInTheDocument(); expect(getByTestId('file-node-2')).toBeInTheDocument(); @@ -229,7 +251,7 @@ describe('FileTree', () => { expect(queryByTestId('file-node-4')).not.toBeInTheDocument(); }); - it('shows hidden files when showHiddenFiles is true', () => { + it('shows hidden files when showHiddenFiles is true', async () => { const { getByTestId } = render( { ); + await waitFor(() => { + expect(getByTestId('file-node-1')).toBeInTheDocument(); + }); + // Should show all files including hidden expect(getByTestId('file-node-1')).toBeInTheDocument(); expect(getByTestId('file-node-2')).toBeInTheDocument(); expect(getByTestId('file-node-4')).toBeInTheDocument(); }); - it('renders empty tree when no files provided', () => { + it('renders empty tree when no files provided', async () => { const { getByTestId } = render( { ); + await waitFor(() => { + expect(getByTestId('file-tree')).toBeInTheDocument(); + }); + const tree = getByTestId('file-tree'); expect(tree).toBeInTheDocument(); expect(tree.children).toHaveLength(0); @@ -276,6 +306,10 @@ describe('FileTree', () => { ); + await waitFor(() => { + expect(getByTestId('file-node-2')).toBeInTheDocument(); + }); + // Click on folder (has children) const folderNode = getByTestId('file-node-2'); fireEvent.click(folderNode); diff --git a/app/src/components/files/FileTree.tsx b/app/src/components/files/FileTree.tsx index 90c6572..6b3ce1c 100644 --- a/app/src/components/files/FileTree.tsx +++ b/app/src/components/files/FileTree.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useLayoutEffect, useCallback } from 'react'; +import React, { useRef, useState, useCallback } from 'react'; import { Tree, type NodeApi } from 'react-arborist'; import { IconFile, @@ -23,15 +23,11 @@ interface FileTreeProps { loadFileList: () => Promise; } -const useSize = (target: React.RefObject): Size | undefined => { +const useSize = ( + target: React.RefObject +): Size | undefined => { const [size, setSize] = useState(); - useLayoutEffect(() => { - if (target.current) { - setSize(target.current.getBoundingClientRect()); - } - }, [target]); - useResizeObserver(target, (entry) => setSize(entry.contentRect)); return size; }; diff --git a/app/src/components/files/FolderSelector.tsx b/app/src/components/files/FolderSelector.tsx index c841e23..2c19dba 100644 --- a/app/src/components/files/FolderSelector.tsx +++ b/app/src/components/files/FolderSelector.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useLayoutEffect, useState } from 'react'; +import React, { useRef, useState } from 'react'; import { Box } from '@mantine/core'; import { Tree, type NodeApi } from 'react-arborist'; import { @@ -21,15 +21,11 @@ interface Size { height: number; } -const useSize = (target: React.RefObject): Size | undefined => { +const useSize = ( + target: React.RefObject +): Size | undefined => { const [size, setSize] = useState(); - useLayoutEffect(() => { - if (target.current) { - setSize(target.current.getBoundingClientRect()); - } - }, [target]); - useResizeObserver(target, (entry) => setSize(entry.contentRect)); return size; }; @@ -239,7 +235,10 @@ export const FolderSelector: React.FC = ({ }} > {/* Root option */} - onSelect('')} /> + onSelect('')} + /> {/* Folder tree */} {size && folders.length > 0 && ( @@ -255,7 +254,11 @@ export const FolderSelector: React.FC = ({ disableDrop={() => true} > {(props) => ( - + )} )}