From c6cf5bccf063be651db3827538965ab577d779c2 Mon Sep 17 00:00:00 2001 From: Ludy87 Date: Wed, 24 Sep 2025 19:44:30 +0200 Subject: [PATCH] Refactor tooltips, context, and update dependencies Refactored tooltip and context usage for improved flexibility and error handling. Updated dependency versions for posthog-js and react-router-dom. Improved i18n usage in tool step components, restructured FileContext for better persistence management, and enhanced hooks for parameter handling. Minor code cleanups and optimizations throughout. --- frontend/package-lock.json | 38 +++++------ frontend/package.json | 6 +- frontend/src/components/shared/ToolChain.tsx | 4 +- frontend/src/components/shared/Tooltip.tsx | 15 +++-- .../components/tools/shared/FilesToolStep.tsx | 6 +- .../tools/shared/ReviewToolStep.tsx | 5 +- .../src/components/tools/shared/ToolStep.tsx | 4 +- .../tooltips/useSplitSettingsTips.ts | 22 +++++-- frontend/src/constants/app.ts | 11 +++- frontend/src/contexts/FileContext.tsx | 66 +++++++++++-------- .../hooks/tools/shared/useBaseParameters.ts | 31 +++++---- frontend/src/tools/Split.tsx | 9 +-- 12 files changed, 125 insertions(+), 92 deletions(-) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index dd7b67a08..9bc90879c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -49,11 +49,11 @@ "license-report": "^6.8.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^5.4.149", - "posthog-js": "^1.268.3", + "posthog-js": "^1.268.4", "react": "^19.1.1", "react-dom": "^19.1.1", "react-i18next": "^15.7.3", - "react-router-dom": "^7.9.1", + "react-router-dom": "^7.9.2", "tailwindcss": "^4.1.13", "web-vitals": "^5.1.0" }, @@ -2460,9 +2460,9 @@ } }, "node_modules/@posthog/core": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.2.0.tgz", - "integrity": "sha512-JuVWgEiOeEfjNtxKg3Kxvt/YWbZCilZUnezUCSAUPQoKxM0VAzMYklh6u/+wxwWybSQ9NaStcNhkxnSU4OxqWA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.2.1.tgz", + "integrity": "sha512-zNw96BipqM5/Tf161Q8/K5zpwGY3ezfb2wz+Yc3fIT5OQHW8eEzkQldPgtFKMUkqImc73ukEa2IdUpS6vEGH7w==", "license": "MIT" }, "node_modules/@rolldown/pluginutils": { @@ -5481,9 +5481,9 @@ } }, "node_modules/detect-libc": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.0.tgz", - "integrity": "sha512-vEtk+OcP7VBRtQZ1EJ3bdgzSfBjgnEalLTp5zjJrS+2Z1w2KZly4SBdac/WDU3hhsNAZ9E8SC96ME4Ey8MZ7cg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.1.tgz", + "integrity": "sha512-ecqj/sy1jcK1uWrwpR67UhYrIFQ+5WlGxth34WquCbamhFA6hkkwiu37o6J5xCHdo1oixJRfVRw+ywV+Hq/0Aw==", "license": "Apache-2.0", "engines": { "node": ">=8" @@ -10042,12 +10042,12 @@ } }, "node_modules/posthog-js": { - "version": "1.268.3", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.268.3.tgz", - "integrity": "sha512-1F5MA9YPNKHkCodPi9VOv83dSDRf7xQpeOIjP0ww9isE+hlK7ogqoC/ZgO3bks4Du5yYazr4Tu7tlZDbkDCXfg==", + "version": "1.268.4", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.268.4.tgz", + "integrity": "sha512-kbE8SeH4Hi6uETEzO4EVANULz1ncw+PXC/SMfDdByf4Qt0a/AKoxjlGCZwHuZuflQmBfTwwQcjHeQxnmIxti1A==", "license": "SEE LICENSE IN LICENSE", "dependencies": { - "@posthog/core": "1.2.0", + "@posthog/core": "1.2.1", "core-js": "^3.38.1", "fflate": "^0.4.8", "preact": "^10.19.3", @@ -10465,9 +10465,9 @@ "license": "0BSD" }, "node_modules/react-router": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.1.tgz", - "integrity": "sha512-pfAByjcTpX55mqSDGwGnY9vDCpxqBLASg0BMNAuMmpSGESo/TaOUG6BllhAtAkCGx8Rnohik/XtaqiYUJtgW2g==", + "version": "7.9.2", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-7.9.2.tgz", + "integrity": "sha512-i2TPp4dgaqrOqiRGLZmqh2WXmbdFknUyiCRmSKs0hf6fWXkTKg5h56b+9F22NbGRAMxjYfqQnpi63egzD2SuZA==", "license": "MIT", "dependencies": { "cookie": "^1.0.1", @@ -10487,12 +10487,12 @@ } }, "node_modules/react-router-dom": { - "version": "7.9.1", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.1.tgz", - "integrity": "sha512-U9WBQssBE9B1vmRjo9qTM7YRzfZ3lUxESIZnsf4VjR/lXYz9MHjvOxHzr/aUm4efpktbVOrF09rL/y4VHa8RMw==", + "version": "7.9.2", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-7.9.2.tgz", + "integrity": "sha512-pagqpVJnjZOfb+vIM23eTp7Sp/AAJjOgaowhP1f1TWOdk5/W8Uk8d/M/0wfleqx7SgjitjNPPsKeCZE1hTSp3w==", "license": "MIT", "dependencies": { - "react-router": "7.9.1" + "react-router": "7.9.2" }, "engines": { "node": ">=20.0.0" diff --git a/frontend/package.json b/frontend/package.json index fbc78743d..aa03d343f 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -45,11 +45,11 @@ "license-report": "^6.8.0", "pdf-lib": "^1.17.1", "pdfjs-dist": "^5.4.149", - "posthog-js": "^1.268.3", + "posthog-js": "^1.268.4", "react": "^19.1.1", "react-dom": "^19.1.1", "react-i18next": "^15.7.3", - "react-router-dom": "^7.9.1", + "react-router-dom": "^7.9.2", "tailwindcss": "^4.1.13", "web-vitals": "^5.1.0" }, @@ -97,7 +97,6 @@ ] }, "devDependencies": { - "eslint-plugin-react": "^7.37.5", "@eslint/js": "^9.36.0", "@iconify-json/material-symbols": "^1.2.38", "@iconify/utils": "^3.0.2", @@ -114,6 +113,7 @@ "@vitejs/plugin-react-swc": "^4.1.0", "@vitest/coverage-v8": "^3.2.4", "eslint": "^9.36.0", + "eslint-plugin-react": "^7.37.5", "eslint-plugin-react-hooks": "^5.2.0", "globals": "^16.4.0", "jsdom": "^27.0.0", diff --git a/frontend/src/components/shared/ToolChain.tsx b/frontend/src/components/shared/ToolChain.tsx index b87b891a2..6873ac6a1 100644 --- a/frontend/src/components/shared/ToolChain.tsx +++ b/frontend/src/components/shared/ToolChain.tsx @@ -24,10 +24,10 @@ const ToolChain: React.FC = ({ size = 'xs', color = 'var(--mantine-color-blue-7)' }) => { - if (!toolChain || toolChain.length === 0) return null; - const { t } = useTranslation(); + if (!toolChain || toolChain.length === 0) return null; + const toolIds = toolChain.map(tool => tool.toolId); const getToolName = (toolId: ToolId) => { diff --git a/frontend/src/components/shared/Tooltip.tsx b/frontend/src/components/shared/Tooltip.tsx index 160857a50..f777ae37e 100644 --- a/frontend/src/components/shared/Tooltip.tsx +++ b/frontend/src/components/shared/Tooltip.tsx @@ -5,7 +5,7 @@ import { addEventListenerWithCleanup } from '../../utils/genericUtils'; import { useTooltipPosition } from '../../hooks/useTooltipPosition'; import { TooltipTip } from '../../types/tips'; import { TooltipContent } from './tooltip/TooltipContent'; -import { useSidebarContext } from '../../contexts/SidebarContext'; +import { useOptionalSidebarContext } from '../../contexts/SidebarContext'; import styles from './tooltip/Tooltip.module.css'; export interface TooltipProps { @@ -64,7 +64,14 @@ export const Tooltip: React.FC = ({ } }, []); - const sidebarContext = sidebarTooltip ? useSidebarContext() : null; + const sidebarContext = useOptionalSidebarContext(); + const effectiveSidebarContext = sidebarTooltip ? sidebarContext : undefined; + + useEffect(() => { + if (sidebarTooltip && !sidebarContext) { + console.warn('Sidebar tooltip requested without SidebarProvider context.'); + } + }, [sidebarTooltip, sidebarContext]); const isControlled = controlledOpen !== undefined; const open = isControlled ? !!controlledOpen : internalOpen; @@ -86,8 +93,8 @@ export const Tooltip: React.FC = ({ gap, triggerRef, tooltipRef, - sidebarRefs: sidebarContext?.sidebarRefs, - sidebarState: sidebarContext?.sidebarState, + sidebarRefs: effectiveSidebarContext?.sidebarRefs, + sidebarState: effectiveSidebarContext?.sidebarState, }); // Close on outside click: pinned → close; not pinned → optionally close diff --git a/frontend/src/components/tools/shared/FilesToolStep.tsx b/frontend/src/components/tools/shared/FilesToolStep.tsx index 5ce118bbe..17c2ad88c 100644 --- a/frontend/src/components/tools/shared/FilesToolStep.tsx +++ b/frontend/src/components/tools/shared/FilesToolStep.tsx @@ -1,5 +1,5 @@ import React from 'react'; -import { useTranslation } from 'react-i18next'; +import i18n from '../../../i18n'; import FileStatusIndicator from './FileStatusIndicator'; import { StirlingFile } from '../../../types/fileContext'; @@ -14,9 +14,9 @@ export function createFilesToolStep( createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement, props: FilesToolStepProps ): React.ReactElement { - const { t } = useTranslation(); + const title = i18n.t('files.title', { defaultValue: 'Files' }); - return createStep(t("files.title", "Files"), { + return createStep(title, { isVisible: true, isCollapsed: props.isCollapsed, onCollapsedClick: props.onCollapsedClick diff --git a/frontend/src/components/tools/shared/ReviewToolStep.tsx b/frontend/src/components/tools/shared/ReviewToolStep.tsx index 243e787b5..39c292468 100644 --- a/frontend/src/components/tools/shared/ReviewToolStep.tsx +++ b/frontend/src/components/tools/shared/ReviewToolStep.tsx @@ -1,6 +1,7 @@ import React, { useEffect, useRef } from "react"; import { Button, Stack } from "@mantine/core"; import { useTranslation } from "react-i18next"; +import i18n from "../../../i18n"; import DownloadIcon from "@mui/icons-material/Download"; import UndoIcon from "@mui/icons-material/Undo"; import ErrorNotification from "./ErrorNotification"; @@ -107,10 +108,10 @@ export function createReviewToolStep( createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement, props: ReviewToolStepProps ): React.ReactElement { - const { t } = useTranslation(); + const title = i18n.t("review", { defaultValue: "Review" }); return createStep( - t("review", "Review"), + title, { isVisible: props.isVisible, isCollapsed: props.isCollapsed, diff --git a/frontend/src/components/tools/shared/ToolStep.tsx b/frontend/src/components/tools/shared/ToolStep.tsx index 4d9c5a187..73a582a4b 100644 --- a/frontend/src/components/tools/shared/ToolStep.tsx +++ b/frontend/src/components/tools/shared/ToolStep.tsx @@ -80,8 +80,6 @@ const ToolStep = ({ alwaysShowTooltip = false, tooltip }: ToolStepProps) => { - if (!isVisible) return null; - const parent = useContext(ToolStepContext); // Auto-detect if we should show numbers based on sibling count or force option @@ -91,6 +89,8 @@ const ToolStep = ({ return parent ? parent.visibleStepCount >= 3 : false; // Auto-detect }, [showNumber, parent]); + if (!isVisible) return null; + const stepNumber = _stepNumber; return ( diff --git a/frontend/src/components/tooltips/useSplitSettingsTips.ts b/frontend/src/components/tooltips/useSplitSettingsTips.ts index bfc80a7d9..e5d00a3cb 100644 --- a/frontend/src/components/tooltips/useSplitSettingsTips.ts +++ b/frontend/src/components/tooltips/useSplitSettingsTips.ts @@ -1,11 +1,15 @@ -import { useTranslation } from 'react-i18next'; +import { useMemo } from 'react'; +import { useTranslation, type TFunction } from 'react-i18next'; import { TooltipContent } from '../../types/tips'; import { SPLIT_METHODS, type SplitMethod } from '../../constants/splitConstants'; -export const useSplitSettingsTips = (method: SplitMethod | ''): TooltipContent | null => { - const { t } = useTranslation(); - - if (!method) return null; +export const getSplitSettingsTips = ( + t: TFunction, + method: SplitMethod | '' +): TooltipContent | null => { + if (!method) { + return null; + } const tooltipMap: Record = { [SPLIT_METHODS.BY_PAGES]: { @@ -131,4 +135,10 @@ export const useSplitSettingsTips = (method: SplitMethod | ''): TooltipContent | }; return tooltipMap[method]; -}; \ No newline at end of file +}; + +export const useSplitSettingsTips = (method: SplitMethod | ''): TooltipContent | null => { + const { t } = useTranslation(); + + return useMemo(() => getSplitSettingsTips(t, method), [t, method]); +}; diff --git a/frontend/src/constants/app.ts b/frontend/src/constants/app.ts index 3b8b765df..61353689b 100644 --- a/frontend/src/constants/app.ts +++ b/frontend/src/constants/app.ts @@ -1,7 +1,12 @@ -import { useAppConfig } from '../hooks/useAppConfig'; +import { useAppConfig, type AppConfig } from '../hooks/useAppConfig'; // Get base URL from app config with fallback -export const getBaseUrl = (): string => { +export const DEFAULT_BASE_URL = 'https://stirling.com'; + +export const getBaseUrlFromConfig = (config?: AppConfig | null): string => + config?.baseUrl || DEFAULT_BASE_URL; +// Hook to access the base URL within React components +export const useBaseUrl = (): string => { const { config } = useAppConfig(); - return config?.baseUrl || 'https://stirling.com'; + return getBaseUrlFromConfig(config); }; diff --git a/frontend/src/contexts/FileContext.tsx b/frontend/src/contexts/FileContext.tsx index 921c88333..5b4e256f5 100644 --- a/frontend/src/contexts/FileContext.tsx +++ b/frontend/src/contexts/FileContext.tsx @@ -34,16 +34,19 @@ import { IndexedDBProvider, useIndexedDB } from './IndexedDBContext'; const DEBUG = process.env.NODE_ENV === 'development'; +type IndexedDBApi = ReturnType; + // Inner provider component that has access to IndexedDB -function FileContextInner({ +function FileContextContent({ children, - enablePersistence = true -}: FileContextProviderProps) { + enablePersistence = true, + indexedDB +}: FileContextProviderProps & { indexedDB: IndexedDBApi | null }) { const [state, dispatch] = useReducer(fileContextReducer, initialFileContextState); // IndexedDB context for persistence - const indexedDB = enablePersistence ? useIndexedDB() : null; + const persistenceApi = enablePersistence ? indexedDB : null; // File ref map - stores File objects outside React state const filesRef = useRef>(new Map()); @@ -72,11 +75,11 @@ function FileContextInner({ dispatch({ type: 'SET_UNSAVED_CHANGES', payload: { hasChanges } }); }, []); - const selectFiles = (stirlingFiles: StirlingFile[]) => { + const selectFiles = useCallback((stirlingFiles: StirlingFile[]) => { const currentSelection = stateRef.current.ui.selectedFileIds; const newFileIds = stirlingFiles.map(stirlingFile => stirlingFile.fileId); dispatch({ type: 'SET_SELECTED_FILES', payload: { fileIds: [...currentSelection, ...newFileIds] } }); - } + }, [dispatch]); // File operations using unified addFiles helper with persistence const addRawFiles = useCallback(async (files: File[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise => { @@ -88,7 +91,7 @@ function FileContextInner({ } return stirlingFiles; - }, [enablePersistence]); + }, [enablePersistence, lifecycleManager, selectFiles]); const addStirlingFileStubsAction = useCallback(async (stirlingFileStubs: StirlingFileStub[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise => { // StirlingFileStubs preserve all metadata - perfect for FileManager use case! @@ -100,7 +103,7 @@ function FileContextInner({ } return result; - }, []); + }, [lifecycleManager, selectFiles]); // Action creators @@ -112,8 +115,8 @@ function FileContextInner({ }, []); const undoConsumeFilesWrapper = useCallback(async (inputFiles: File[], inputStirlingFileStubs: StirlingFileStub[], outputFileIds: FileId[]): Promise => { - return undoConsumeFiles(inputFiles, inputStirlingFileStubs, outputFileIds, filesRef, dispatch, indexedDB); - }, [indexedDB]); + return undoConsumeFiles(inputFiles, inputStirlingFileStubs, outputFileIds, filesRef, dispatch, persistenceApi); + }, [persistenceApi]); // File pinning functions - use StirlingFile directly const pinFileWrapper = useCallback((file: StirlingFile) => { @@ -134,9 +137,9 @@ function FileContextInner({ lifecycleManager.removeFiles(fileIds, stateRef); // Remove from IndexedDB if enabled - if (indexedDB && enablePersistence && deleteFromStorage !== false) { + if (persistenceApi && enablePersistence && deleteFromStorage !== false) { try { - await indexedDB.deleteMultiple(fileIds); + await persistenceApi.deleteMultiple(fileIds); } catch (error) { console.error('Failed to delete files from IndexedDB:', error); } @@ -162,9 +165,9 @@ function FileContextInner({ dispatch({ type: 'RESET_CONTEXT' }); // Then clear IndexedDB storage - if (indexedDB && enablePersistence) { + if (persistenceApi && enablePersistence) { try { - await indexedDB.clearAll(); + await persistenceApi.clearAll(); } catch (error) { console.error('Failed to clear IndexedDB:', error); } @@ -190,7 +193,7 @@ function FileContextInner({ undoConsumeFilesWrapper, pinFileWrapper, unpinFileWrapper, - indexedDB, + persistenceApi, enablePersistence ]); @@ -229,31 +232,38 @@ function FileContextInner({ ); } +function FileContextWithPersistence(props: FileContextProviderProps) { + const indexedDB = useIndexedDB(); + return ; +} + + // Outer provider component that wraps with IndexedDBProvider export function FileContextProvider({ children, enableUrlSync = true, - enablePersistence = true + enablePersistence = true, + ...rest }: FileContextProviderProps) { + const sharedProps = { + children, + enableUrlSync, + enablePersistence, + ...rest + }; + if (enablePersistence) { return ( - - {children} - + ); } else { return ( - - {children} - + ); } } diff --git a/frontend/src/hooks/tools/shared/useBaseParameters.ts b/frontend/src/hooks/tools/shared/useBaseParameters.ts index 941f8cdd2..572be953b 100644 --- a/frontend/src/hooks/tools/shared/useBaseParameters.ts +++ b/frontend/src/hooks/tools/shared/useBaseParameters.ts @@ -15,8 +15,12 @@ export interface BaseParametersConfig { validateFn?: (params: T) => boolean; } -export function useBaseParameters(config: BaseParametersConfig): BaseParametersHook { - const [parameters, setParameters] = useState(config.defaultParameters); +export function useBaseParameters({ + defaultParameters, + endpointName, + validateFn +}: BaseParametersConfig): BaseParametersHook { + const [parameters, setParameters] = useState(defaultParameters); const updateParameter = useCallback((parameter: K, value: T[K]) => { setParameters(prev => ({ @@ -26,24 +30,19 @@ export function useBaseParameters(config: BaseParametersConfig): BaseParam }, []); const resetParameters = useCallback(() => { - setParameters(config.defaultParameters); - }, [config.defaultParameters]); + setParameters(defaultParameters); + }, [defaultParameters]); const validateParameters = useCallback(() => { - return config.validateFn ? config.validateFn(parameters) : true; - }, [parameters, config.validateFn]); - - const endpointName = config.endpointName; - let getEndpointName: () => string; - if (typeof endpointName === "string") { - getEndpointName = useCallback(() => { + return validateFn ? validateFn(parameters) : true; + }, [parameters, validateFn]); + const getEndpointName = useCallback(() => { + if (typeof endpointName === "string") { return endpointName; - }, []); - } else { - getEndpointName = useCallback(() => { + } else { return endpointName(parameters); - }, [parameters]); - } + } + }, [endpointName, parameters]); return { parameters, diff --git a/frontend/src/tools/Split.tsx b/frontend/src/tools/Split.tsx index 96b335f5f..57688efea 100644 --- a/frontend/src/tools/Split.tsx +++ b/frontend/src/tools/Split.tsx @@ -1,3 +1,4 @@ +import { useCallback } from "react"; import { useTranslation } from "react-i18next"; import { createToolFlow } from "../components/tools/shared/createToolFlow"; import CardSelector from "../components/shared/CardSelector"; @@ -6,7 +7,7 @@ import { useSplitParameters } from "../hooks/tools/split/useSplitParameters"; import { useSplitOperation } from "../hooks/tools/split/useSplitOperation"; import { useBaseTool } from "../hooks/tools/shared/useBaseTool"; import { useSplitMethodTips } from "../components/tooltips/useSplitMethodTips"; -import { useSplitSettingsTips } from "../components/tooltips/useSplitSettingsTips"; +import { useSplitSettingsTips, getSplitSettingsTips } from "../components/tooltips/useSplitSettingsTips"; import { BaseToolProps, ToolComponent } from "../types/tool"; import { type SplitMethod, METHOD_OPTIONS, type MethodOption } from "../constants/splitConstants"; @@ -24,10 +25,10 @@ const Split = (props: BaseToolProps) => { const settingsTips = useSplitSettingsTips(base.params.parameters.method); // Get tooltip content for a specific method - const getMethodTooltip = (option: MethodOption) => { - const tooltipContent = useSplitSettingsTips(option.value); + const getMethodTooltip = useCallback((option: MethodOption) => { + const tooltipContent = getSplitSettingsTips(t, option.value); return tooltipContent?.tips || []; - }; + }, [t]); // Get the method name for the settings step title const getSettingsTitle = () => {