5 Commits

Author SHA1 Message Date
d9f1a16d94 Merge pull request #96 from lordmathis/chore/update-deps
Bump React and TypeScript types
2025-11-15 15:21:49 +01:00
0999fa9315 Fix FileTree tests with async rendering and mock resize observer 2025-11-15 15:16:19 +01:00
cc8c8fd414 Fix typescript issues 2025-11-15 14:56:48 +01:00
9ba37b3342 Update react dependencies 2025-11-15 14:47:13 +01:00
140ccd6879 Bump React and TypeScript types 2025-11-15 01:19:01 +01:00
5 changed files with 93 additions and 73 deletions

76
app/package-lock.json generated
View File

@@ -15,6 +15,7 @@
"@codemirror/state": "^6.5.2", "@codemirror/state": "^6.5.2",
"@codemirror/theme-one-dark": "^6.1.3", "@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.38.6", "@codemirror/view": "^6.38.6",
"@floating-ui/react": "^0.27.16",
"@mantine/core": "^8.3.7", "@mantine/core": "^8.3.7",
"@mantine/hooks": "^8.3.7", "@mantine/hooks": "^8.3.7",
"@mantine/modals": "^8.3.7", "@mantine/modals": "^8.3.7",
@@ -22,9 +23,9 @@
"@react-hook/resize-observer": "^2.0.2", "@react-hook/resize-observer": "^2.0.2",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"codemirror": "^6.0.2", "codemirror": "^6.0.2",
"react": "^18.3.1", "react": "^19.2.0",
"react-arborist": "^3.4.3", "react-arborist": "^3.4.3",
"react-dom": "^18.3.1", "react-dom": "^19.2.0",
"rehype-highlight": "^7.0.2", "rehype-highlight": "^7.0.2",
"rehype-mathjax": "^7.1.0", "rehype-mathjax": "^7.1.0",
"rehype-react": "^8.0.0", "rehype-react": "^8.0.0",
@@ -41,8 +42,8 @@
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@types/babel__core": "^7.20.5", "@types/babel__core": "^7.20.5",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/react": "^18.3.20", "@types/react": "^19.2.5",
"@types/react-dom": "^18.3.6", "@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.46.4", "@typescript-eslint/eslint-plugin": "^8.46.4",
"@typescript-eslint/parser": "^8.32.1", "@typescript-eslint/parser": "^8.32.1",
"@vitejs/plugin-react": "^5.1.1", "@vitejs/plugin-react": "^5.1.1",
@@ -2558,32 +2559,24 @@
"undici-types": "~7.16.0" "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": { "node_modules/@types/react": {
"version": "18.3.20", "version": "19.2.5",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz", "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.5.tgz",
"integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==", "integrity": "sha512-keKxkZMqnDicuvFoJbzrhbtdLSPhj/rZThDlKWCDbgXmUg0rEUFtRssDXKYmtXluZlIqiC5VqkCgRwzuyLHKHw==",
"devOptional": true, "devOptional": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2" "csstype": "^3.0.2"
} }
}, },
"node_modules/@types/react-dom": { "node_modules/@types/react-dom": {
"version": "18.3.6", "version": "19.2.3",
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.6.tgz", "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-nf22//wEbKXusP6E9pfOCDwFdHAX4u172eaJI4YkDRQEZiorm6KfYnSC2SWLDMVWUOWPERmJnN0ujeAfTBLvrw==", "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"@types/react": "^18.0.0" "@types/react": "^19.2.0"
} }
}, },
"node_modules/@types/unist": { "node_modules/@types/unist": {
@@ -5808,9 +5801,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -7426,13 +7419,10 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/react": { "node_modules/react": {
"version": "18.3.1", "version": "19.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react/-/react-19.2.0.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", "integrity": "sha512-tmbWg6W31tQLeB5cdIBOicJDJRR2KzXsV7uSK9iNfLWQ5bIZfxuPEHp7M8wiHyHnn0DD1i7w3Zmin0FtkrwoCQ==",
"license": "MIT", "license": "MIT",
"dependencies": {
"loose-envify": "^1.1.0"
},
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -7494,16 +7484,15 @@
} }
}, },
"node_modules/react-dom": { "node_modules/react-dom": {
"version": "18.3.1", "version": "19.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.0.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", "integrity": "sha512-UlbRu4cAiGaIewkPyiRGJk0imDN2T3JjieT6spoL2UeSf5od4n5LB/mQ4ejmxhCFT1tYe8IvaFulzynWovsEFQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"loose-envify": "^1.1.0", "scheduler": "^0.27.0"
"scheduler": "^0.23.2"
}, },
"peerDependencies": { "peerDependencies": {
"react": "^18.3.1" "react": "^19.2.0"
} }
}, },
"node_modules/react-is": { "node_modules/react-is": {
@@ -8089,13 +8078,10 @@
} }
}, },
"node_modules/scheduler": { "node_modules/scheduler": {
"version": "0.23.2", "version": "0.27.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
"integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
"license": "MIT", "license": "MIT"
"dependencies": {
"loose-envify": "^1.1.0"
}
}, },
"node_modules/semver": { "node_modules/semver": {
"version": "6.3.1", "version": "6.3.1",
@@ -9157,12 +9143,12 @@
} }
}, },
"node_modules/use-sync-external-store": { "node_modules/use-sync-external-store": {
"version": "1.2.2", "version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.2.2.tgz", "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-PElTlVMwpblvbNqQ82d2n6RjStvdSoNe9FG28kNfz3WiXilJm4DdNkEzRhCZuIDwY8U08WVihhGR5iRqAwfDiw==", "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "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": { "node_modules/util-deprecate": {

View File

@@ -35,6 +35,7 @@
"@codemirror/state": "^6.5.2", "@codemirror/state": "^6.5.2",
"@codemirror/theme-one-dark": "^6.1.3", "@codemirror/theme-one-dark": "^6.1.3",
"@codemirror/view": "^6.38.6", "@codemirror/view": "^6.38.6",
"@floating-ui/react": "^0.27.16",
"@mantine/core": "^8.3.7", "@mantine/core": "^8.3.7",
"@mantine/hooks": "^8.3.7", "@mantine/hooks": "^8.3.7",
"@mantine/modals": "^8.3.7", "@mantine/modals": "^8.3.7",
@@ -42,9 +43,9 @@
"@react-hook/resize-observer": "^2.0.2", "@react-hook/resize-observer": "^2.0.2",
"@tabler/icons-react": "^3.35.0", "@tabler/icons-react": "^3.35.0",
"codemirror": "^6.0.2", "codemirror": "^6.0.2",
"react": "^18.3.1", "react": "^19.2.0",
"react-arborist": "^3.4.3", "react-arborist": "^3.4.3",
"react-dom": "^18.3.1", "react-dom": "^19.2.0",
"rehype-highlight": "^7.0.2", "rehype-highlight": "^7.0.2",
"rehype-mathjax": "^7.1.0", "rehype-mathjax": "^7.1.0",
"rehype-react": "^8.0.0", "rehype-react": "^8.0.0",
@@ -61,8 +62,8 @@
"@testing-library/react": "^16.3.0", "@testing-library/react": "^16.3.0",
"@types/babel__core": "^7.20.5", "@types/babel__core": "^7.20.5",
"@types/node": "^24.10.1", "@types/node": "^24.10.1",
"@types/react": "^18.3.20", "@types/react": "^19.2.5",
"@types/react-dom": "^18.3.6", "@types/react-dom": "^19.2.3",
"@typescript-eslint/eslint-plugin": "^8.46.4", "@typescript-eslint/eslint-plugin": "^8.46.4",
"@typescript-eslint/parser": "^8.32.1", "@typescript-eslint/parser": "^8.32.1",
"@vitejs/plugin-react": "^5.1.1", "@vitejs/plugin-react": "^5.1.1",

View File

@@ -69,7 +69,19 @@ vi.mock('react-arborist', () => ({
// Mock resize observer hook // Mock resize observer hook
vi.mock('@react-hook/resize-observer', () => ({ 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 // Mock contexts
@@ -172,7 +184,7 @@ describe('FileTree', () => {
vi.clearAllMocks(); vi.clearAllMocks();
}); });
it('renders file tree with files', () => { it('renders file tree with files', async () => {
const { getByTestId } = render( const { getByTestId } = render(
<TestWrapper> <TestWrapper>
<FileTree <FileTree
@@ -184,7 +196,9 @@ describe('FileTree', () => {
</TestWrapper> </TestWrapper>
); );
expect(getByTestId('file-tree')).toBeInTheDocument(); await waitFor(() => {
expect(getByTestId('file-tree')).toBeInTheDocument();
});
expect(getByTestId('file-node-1')).toBeInTheDocument(); expect(getByTestId('file-node-1')).toBeInTheDocument();
expect(getByTestId('file-node-2')).toBeInTheDocument(); expect(getByTestId('file-node-2')).toBeInTheDocument();
}); });
@@ -201,6 +215,10 @@ describe('FileTree', () => {
</TestWrapper> </TestWrapper>
); );
await waitFor(() => {
expect(getByTestId('file-node-1')).toBeInTheDocument();
});
const fileNode = getByTestId('file-node-1'); const fileNode = getByTestId('file-node-1');
fireEvent.click(fileNode); 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( const { getByTestId, queryByTestId } = render(
<TestWrapper> <TestWrapper>
<FileTree <FileTree
@@ -221,6 +239,10 @@ describe('FileTree', () => {
</TestWrapper> </TestWrapper>
); );
await waitFor(() => {
expect(getByTestId('file-node-1')).toBeInTheDocument();
});
// Should show regular files // Should show regular files
expect(getByTestId('file-node-1')).toBeInTheDocument(); expect(getByTestId('file-node-1')).toBeInTheDocument();
expect(getByTestId('file-node-2')).toBeInTheDocument(); expect(getByTestId('file-node-2')).toBeInTheDocument();
@@ -229,7 +251,7 @@ describe('FileTree', () => {
expect(queryByTestId('file-node-4')).not.toBeInTheDocument(); 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( const { getByTestId } = render(
<TestWrapper> <TestWrapper>
<FileTree <FileTree
@@ -241,13 +263,17 @@ describe('FileTree', () => {
</TestWrapper> </TestWrapper>
); );
await waitFor(() => {
expect(getByTestId('file-node-1')).toBeInTheDocument();
});
// Should show all files including hidden // Should show all files including hidden
expect(getByTestId('file-node-1')).toBeInTheDocument(); expect(getByTestId('file-node-1')).toBeInTheDocument();
expect(getByTestId('file-node-2')).toBeInTheDocument(); expect(getByTestId('file-node-2')).toBeInTheDocument();
expect(getByTestId('file-node-4')).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( const { getByTestId } = render(
<TestWrapper> <TestWrapper>
<FileTree <FileTree
@@ -259,6 +285,10 @@ describe('FileTree', () => {
</TestWrapper> </TestWrapper>
); );
await waitFor(() => {
expect(getByTestId('file-tree')).toBeInTheDocument();
});
const tree = getByTestId('file-tree'); const tree = getByTestId('file-tree');
expect(tree).toBeInTheDocument(); expect(tree).toBeInTheDocument();
expect(tree.children).toHaveLength(0); expect(tree.children).toHaveLength(0);
@@ -276,6 +306,10 @@ describe('FileTree', () => {
</TestWrapper> </TestWrapper>
); );
await waitFor(() => {
expect(getByTestId('file-node-2')).toBeInTheDocument();
});
// Click on folder (has children) // Click on folder (has children)
const folderNode = getByTestId('file-node-2'); const folderNode = getByTestId('file-node-2');
fireEvent.click(folderNode); fireEvent.click(folderNode);

View File

@@ -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 { Tree, type NodeApi } from 'react-arborist';
import { import {
IconFile, IconFile,
@@ -23,15 +23,11 @@ interface FileTreeProps {
loadFileList: () => Promise<void>; loadFileList: () => Promise<void>;
} }
const useSize = (target: React.RefObject<HTMLElement>): Size | undefined => { const useSize = (
target: React.RefObject<HTMLElement | null>
): Size | undefined => {
const [size, setSize] = useState<Size>(); const [size, setSize] = useState<Size>();
useLayoutEffect(() => {
if (target.current) {
setSize(target.current.getBoundingClientRect());
}
}, [target]);
useResizeObserver(target, (entry) => setSize(entry.contentRect)); useResizeObserver(target, (entry) => setSize(entry.contentRect));
return size; return size;
}; };

View File

@@ -1,4 +1,4 @@
import React, { useRef, useLayoutEffect, useState } from 'react'; import React, { useRef, useState } from 'react';
import { Box } from '@mantine/core'; import { Box } from '@mantine/core';
import { Tree, type NodeApi } from 'react-arborist'; import { Tree, type NodeApi } from 'react-arborist';
import { import {
@@ -21,15 +21,11 @@ interface Size {
height: number; height: number;
} }
const useSize = (target: React.RefObject<HTMLElement>): Size | undefined => { const useSize = (
target: React.RefObject<HTMLElement | null>
): Size | undefined => {
const [size, setSize] = useState<Size>(); const [size, setSize] = useState<Size>();
useLayoutEffect(() => {
if (target.current) {
setSize(target.current.getBoundingClientRect());
}
}, [target]);
useResizeObserver(target, (entry) => setSize(entry.contentRect)); useResizeObserver(target, (entry) => setSize(entry.contentRect));
return size; return size;
}; };
@@ -239,7 +235,10 @@ export const FolderSelector: React.FC<FolderSelectorProps> = ({
}} }}
> >
{/* Root option */} {/* Root option */}
<RootNode isSelected={selectedPath === ''} onSelect={() => onSelect('')} /> <RootNode
isSelected={selectedPath === ''}
onSelect={() => onSelect('')}
/>
{/* Folder tree */} {/* Folder tree */}
{size && folders.length > 0 && ( {size && folders.length > 0 && (
@@ -255,7 +254,11 @@ export const FolderSelector: React.FC<FolderSelectorProps> = ({
disableDrop={() => true} disableDrop={() => true}
> >
{(props) => ( {(props) => (
<FolderNode {...props} selectedPath={selectedPath} onSelect={onSelect} /> <FolderNode
{...props}
selectedPath={selectedPath}
onSelect={onSelect}
/>
)} )}
</Tree> </Tree>
)} )}