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.
This commit is contained in:
Ludy87 2025-09-24 19:44:30 +02:00
parent 885d0d4404
commit c6cf5bccf0
No known key found for this signature in database
GPG Key ID: 92696155E0220F94
12 changed files with 125 additions and 92 deletions

View File

@ -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"

View File

@ -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",

View File

@ -24,10 +24,10 @@ const ToolChain: React.FC<ToolChainProps> = ({
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) => {

View File

@ -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<TooltipProps> = ({
}
}, []);
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<TooltipProps> = ({
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

View File

@ -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

View File

@ -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<TParams = unknown>(
createStep: (title: string, props: any, children?: React.ReactNode) => React.ReactElement,
props: ReviewToolStepProps<TParams>
): React.ReactElement {
const { t } = useTranslation();
const title = i18n.t("review", { defaultValue: "Review" });
return createStep(
t("review", "Review"),
title,
{
isVisible: props.isVisible,
isCollapsed: props.isCollapsed,

View File

@ -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 (

View File

@ -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<SplitMethod, TooltipContent> = {
[SPLIT_METHODS.BY_PAGES]: {
@ -132,3 +136,9 @@ export const useSplitSettingsTips = (method: SplitMethod | ''): TooltipContent |
return tooltipMap[method];
};
export const useSplitSettingsTips = (method: SplitMethod | ''): TooltipContent | null => {
const { t } = useTranslation();
return useMemo(() => getSplitSettingsTips(t, method), [t, method]);
};

View File

@ -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);
};

View File

@ -34,16 +34,19 @@ import { IndexedDBProvider, useIndexedDB } from './IndexedDBContext';
const DEBUG = process.env.NODE_ENV === 'development';
type IndexedDBApi = ReturnType<typeof useIndexedDB>;
// 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<Map<FileId, File>>(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<StirlingFile[]> => {
@ -88,7 +91,7 @@ function FileContextInner({
}
return stirlingFiles;
}, [enablePersistence]);
}, [enablePersistence, lifecycleManager, selectFiles]);
const addStirlingFileStubsAction = useCallback(async (stirlingFileStubs: StirlingFileStub[], options?: { insertAfterPageId?: string; selectFiles?: boolean }): Promise<StirlingFile[]> => {
// 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<void> => {
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 <FileContextContent {...props} indexedDB={indexedDB} />;
}
// 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 (
<IndexedDBProvider>
<FileContextInner
enableUrlSync={enableUrlSync}
enablePersistence={enablePersistence}
>
{children}
</FileContextInner>
<FileContextWithPersistence {...sharedProps} />
</IndexedDBProvider>
);
} else {
return (
<FileContextInner
enableUrlSync={enableUrlSync}
enablePersistence={enablePersistence}
>
{children}
</FileContextInner>
<FileContextContent
{...sharedProps}
indexedDB={null}
/>
);
}
}

View File

@ -15,8 +15,12 @@ export interface BaseParametersConfig<T> {
validateFn?: (params: T) => boolean;
}
export function useBaseParameters<T>(config: BaseParametersConfig<T>): BaseParametersHook<T> {
const [parameters, setParameters] = useState<T>(config.defaultParameters);
export function useBaseParameters<T>({
defaultParameters,
endpointName,
validateFn
}: BaseParametersConfig<T>): BaseParametersHook<T> {
const [parameters, setParameters] = useState<T>(defaultParameters);
const updateParameter = useCallback(<K extends keyof T>(parameter: K, value: T[K]) => {
setParameters(prev => ({
@ -26,24 +30,19 @@ export function useBaseParameters<T>(config: BaseParametersConfig<T>): 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,

View File

@ -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 = () => {