From 7dd913df7bffc36bb4a59f7a5135e76594fa2480 Mon Sep 17 00:00:00 2001 From: Adrian Rumpold Date: Wed, 30 Apr 2025 12:04:38 +0200 Subject: [PATCH] Paragraph-level links, preview tooltips --- frontend/__mocks__/zustand.ts | 64 +++++++ frontend/package-lock.json | 61 +++++++ frontend/package.json | 2 + frontend/src/App.tsx | 3 +- .../ArticleSelector/ArticleSelector.test.tsx | 4 - .../ArticleSelector/ArticleSelector.tsx | 5 +- .../CelexSelector/CelexSelector.test.tsx | 53 ++---- .../CelexSelector/CelexSelector.tsx | 6 +- frontend/src/components/Panel/Panel.test.tsx | 4 +- frontend/src/components/Panel/Panel.tsx | 49 ++++-- .../ParagraphPreview/ParagraphPreview.tsx | 34 ++++ frontend/src/components/TOC/TOC.tsx | 4 +- .../src/components/Tooltip/Tooltip.module.css | 10 ++ frontend/src/components/Tooltip/Tooltip.tsx | 163 ++++++++++++++++++ frontend/src/hooks/toc.ts | 4 +- frontend/src/hooks/urlSync.ts | 8 +- frontend/src/hooks/useParagraph.ts | 16 ++ frontend/src/lib/api.ts | 17 +- frontend/src/pages/MainView.tsx | 4 +- frontend/src/store/navStore.ts | 18 +- frontend/src/store/uiStore.ts | 4 +- frontend/src/util/zustand.ts | 18 ++ frontend/tsconfig.app.json | 2 +- src/formex_viewer/formex4.py | 67 +++++-- src/formex_viewer/server.py | 51 ++++-- 25 files changed, 569 insertions(+), 102 deletions(-) create mode 100644 frontend/__mocks__/zustand.ts create mode 100644 frontend/src/components/ParagraphPreview/ParagraphPreview.tsx create mode 100644 frontend/src/components/Tooltip/Tooltip.module.css create mode 100644 frontend/src/components/Tooltip/Tooltip.tsx create mode 100644 frontend/src/hooks/useParagraph.ts create mode 100644 frontend/src/util/zustand.ts diff --git a/frontend/__mocks__/zustand.ts b/frontend/__mocks__/zustand.ts new file mode 100644 index 0000000..81107c0 --- /dev/null +++ b/frontend/__mocks__/zustand.ts @@ -0,0 +1,64 @@ +import { act } from "@testing-library/react"; +import type * as ZustandExportedTypes from "zustand"; +export * from "zustand"; + +const { create: actualCreate, createStore: actualCreateStore } = + jest.requireActual("zustand"); + +// a variable to hold reset functions for all stores declared in the app +export const storeResetFns = new Set<() => void>(); + +const createUncurried = ( + stateCreator: ZustandExportedTypes.StateCreator +) => { + const store = actualCreate(stateCreator); + const initialState = store.getInitialState(); + storeResetFns.add(() => { + store.setState(initialState, true); + }); + return store; +}; + +// when creating a store, we get its initial state, create a reset function and add it in the set +export const create = (( + stateCreator: ZustandExportedTypes.StateCreator +) => { + console.log("zustand create mock"); + + // to support curried version of create + return typeof stateCreator === "function" + ? createUncurried(stateCreator) + : createUncurried; +}) as typeof ZustandExportedTypes.create; + +const createStoreUncurried = ( + stateCreator: ZustandExportedTypes.StateCreator +) => { + const store = actualCreateStore(stateCreator); + const initialState = store.getInitialState(); + storeResetFns.add(() => { + store.setState(initialState, true); + }); + return store; +}; + +// when creating a store, we get its initial state, create a reset function and add it in the set +export const createStore = (( + stateCreator: ZustandExportedTypes.StateCreator +) => { + console.log("zustand createStore mock"); + + // to support curried version of createStore + return typeof stateCreator === "function" + ? createStoreUncurried(stateCreator) + : createStoreUncurried; +}) as typeof ZustandExportedTypes.createStore; + +// reset all stores after each test run +afterEach(() => { + act(() => { + storeResetFns.forEach((resetFn) => { + resetFn(); + }); + }); +}); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 73a6702..0626dcf 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -8,6 +8,7 @@ "name": "frontend", "version": "0.0.0", "dependencies": { + "@floating-ui/react": "^0.27.8", "@tanstack/react-query": "^5.74.4", "@tanstack/react-query-devtools": "^5.74.6", "axios": "^1.9.0", @@ -18,6 +19,7 @@ }, "devDependencies": { "@eslint/js": "^9.22.0", + "@jest/globals": "^29.7.0", "@tanstack/eslint-plugin-query": "^5.73.3", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", @@ -1259,6 +1261,59 @@ "node": "^18.18.0 || ^20.9.0 || >=21.1.0" } }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "license": "MIT", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "license": "MIT", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react": { + "version": "0.27.8", + "resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.8.tgz", + "integrity": "sha512-EQJ4Th328y2wyHR3KzOUOoTW2UKjFk53fmyahfwExnFQ8vnsMYqKc+fFPOkeYtj5tcp1DUMiNJ7BFhed7e9ONw==", + "license": "MIT", + "dependencies": { + "@floating-ui/react-dom": "^2.1.2", + "@floating-ui/utils": "^0.2.9", + "tabbable": "^6.0.0" + }, + "peerDependencies": { + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "license": "MIT", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==", + "license": "MIT" + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -8129,6 +8184,12 @@ "dev": true, "license": "MIT" }, + "node_modules/tabbable": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz", + "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==", + "license": "MIT" + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index 1630621..46a3863 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -11,6 +11,7 @@ "test": "jest" }, "dependencies": { + "@floating-ui/react": "^0.27.8", "@tanstack/react-query": "^5.74.4", "@tanstack/react-query-devtools": "^5.74.6", "axios": "^1.9.0", @@ -21,6 +22,7 @@ }, "devDependencies": { "@eslint/js": "^9.22.0", + "@jest/globals": "^29.7.0", "@tanstack/eslint-plugin-query": "^5.73.3", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 6ad6f7c..46d934c 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -11,7 +11,8 @@ import CelexSelector from "./components/CelexSelector/CelexSelector"; import { useTOC } from "./hooks/toc"; function App() { - const { numPanels, addPanel } = useUIStore(); + const numPanels = useUIStore((state) => state.numPanels); + const addPanel = useUIStore((state) => state.addPanel); const { data: toc, isPending, error } = useTOC(); if (isPending) { diff --git a/frontend/src/components/ArticleSelector/ArticleSelector.test.tsx b/frontend/src/components/ArticleSelector/ArticleSelector.test.tsx index 11f7885..328ae88 100644 --- a/frontend/src/components/ArticleSelector/ArticleSelector.test.tsx +++ b/frontend/src/components/ArticleSelector/ArticleSelector.test.tsx @@ -32,10 +32,6 @@ describe("ArticleSelector", () => { ], }; - beforeEach(() => { - jest.clearAllMocks(); - }); - test("renders a top-level division as an optgroup", () => { const { getByRole } = render(); diff --git a/frontend/src/components/ArticleSelector/ArticleSelector.tsx b/frontend/src/components/ArticleSelector/ArticleSelector.tsx index 9f3d390..b825192 100644 --- a/frontend/src/components/ArticleSelector/ArticleSelector.tsx +++ b/frontend/src/components/ArticleSelector/ArticleSelector.tsx @@ -1,6 +1,6 @@ import { Fragment, JSX } from "react"; import type { Division } from "../../lib/types"; -import useNavState from "../../store/navStore"; +import useNavStore from "../../store/navStore"; import styles from "./ArticleSelector.module.css"; type ArticleSelectorProps = { @@ -41,7 +41,8 @@ function renderDivision(div: Division): JSX.Element { } function ArticleSelector({ toc }: ArticleSelectorProps) { - const { articleId, setArticleId } = useNavState(); + const articleId = useNavStore.use.articleId(); + const setArticleId = useNavStore.use.setArticleId(); return (