Posthog, scarf and url navigation overhaul (#4318)

Added post hog project - always enabled
Added scarf pixel - Always enabled 
Reworked Url navigation 
Forward and back now works without reloading page

---------

Co-authored-by: Connor Yoh <[email protected]>
This commit is contained in:
ConnorYoh
2025-08-28 15:42:33 +01:00
committed by GitHub
co-authored by Connor Yoh
parent 5b20f11e20
commit a7d5c80188
31 changed files with 945 additions and 882 deletions
@@ -2,29 +2,29 @@ import { useState, useEffect, useCallback } from 'react';
import { useTranslation } from 'react-i18next';
import { AutomationTool, AutomationConfig, AutomationMode } from '../../../types/automation';
import { AUTOMATION_CONSTANTS } from '../../../constants/automation';
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
import { ToolRegistry } from '../../../data/toolsTaxonomy';
interface UseAutomationFormProps {
mode: AutomationMode;
existingAutomation?: AutomationConfig;
toolRegistry: Record<string, ToolRegistryEntry>;
toolRegistry: ToolRegistry;
}
export function useAutomationForm({ mode, existingAutomation, toolRegistry }: UseAutomationFormProps) {
const { t } = useTranslation();
const [automationName, setAutomationName] = useState('');
const [automationDescription, setAutomationDescription] = useState('');
const [automationIcon, setAutomationIcon] = useState<string>('');
const [selectedTools, setSelectedTools] = useState<AutomationTool[]>([]);
const getToolName = useCallback((operation: string) => {
const tool = toolRegistry?.[operation] as any;
const tool = toolRegistry?.[operation as keyof ToolRegistry] as any;
return tool?.name || t(`tools.${operation}.name`, operation);
}, [toolRegistry, t]);
const getToolDefaultParameters = useCallback((operation: string): Record<string, any> => {
const config = toolRegistry[operation]?.operationConfig;
const config = toolRegistry[operation as keyof ToolRegistry]?.operationConfig;
if (config?.defaultParameters) {
return { ...config.defaultParameters };
}
@@ -119,4 +119,4 @@ export function useAutomationForm({ mode, existingAutomation, toolRegistry }: Us
getToolName,
getToolDefaultParameters
};
}
}
+7 -6
View File
@@ -1,5 +1,6 @@
import { useMemo } from 'react';
import { useNavigationActions, useNavigationState } from '../contexts/NavigationContext';
import { ToolId } from '../types/toolId';
// Material UI Icons
import CompressIcon from '@mui/icons-material/Compress';
@@ -9,7 +10,7 @@ import CropIcon from '@mui/icons-material/Crop';
import TextFieldsIcon from '@mui/icons-material/TextFields';
export interface SuggestedTool {
id: string /* FIX ME: Should be ToolId */;
id: ToolId;
title: string;
icon: React.ComponentType<any>;
navigate: () => void;
@@ -32,7 +33,7 @@ const ALL_SUGGESTED_TOOLS: Omit<SuggestedTool, 'navigate'>[] = [
icon: CleaningServicesIcon
},
{
id: 'splitPdf',
id: 'split',
title: 'Split',
icon: CropIcon
},
@@ -45,16 +46,16 @@ const ALL_SUGGESTED_TOOLS: Omit<SuggestedTool, 'navigate'>[] = [
export function useSuggestedTools(): SuggestedTool[] {
const { actions } = useNavigationActions();
const { selectedToolKey } = useNavigationState();
const { selectedTool } = useNavigationState();
return useMemo(() => {
// Filter out the current tool
const filteredTools = ALL_SUGGESTED_TOOLS.filter(tool => tool.id !== selectedToolKey);
const filteredTools = ALL_SUGGESTED_TOOLS.filter(tool => tool.id !== selectedTool);
// Add navigation function to each tool
return filteredTools.map(tool => ({
...tool,
navigate: () => actions.handleToolSelect(tool.id)
navigate: () => actions.setSelectedTool(tool.id)
}));
}, [selectedToolKey, actions]);
}, [selectedTool, actions]);
}
+1 -1
View File
@@ -35,7 +35,7 @@ export const useToolManagement = (): ToolManagementResult => {
const isToolAvailable = useCallback((toolKey: string): boolean => {
if (endpointsLoading) return true;
const endpoints = baseRegistry[toolKey]?.endpoints || [];
const endpoints = baseRegistry[toolKey as keyof typeof baseRegistry]?.endpoints || [];
return endpoints.length === 0 || endpoints.some((endpoint: string) => endpointStatus[endpoint] === true);
}, [endpointsLoading, endpointStatus, baseRegistry]);
-129
View File
@@ -1,129 +0,0 @@
// src/hooks/useToolUrlRouting.ts
// Focused hook for URL <-> tool-key mapping and browser history sync.
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
export interface UseToolUrlRoutingOpts {
/** Currently selected tool key (from context). */
selectedToolKey: string | null;
/** Registry of available tools (key -> tool metadata). */
toolRegistry: Record<string, any> | null | undefined;
/** Select a tool (no extra side-effects). */
selectTool: (toolKey: string) => void;
/** Clear selection. */
clearToolSelection: () => void;
/** Called once during initialization if URL contains a tool; may trigger UI changes. */
onInitSelect?: (toolKey: string) => void;
/** Called when navigating via back/forward (popstate). Defaults to selectTool. */
onPopStateSelect?: (toolKey: string) => void;
/** Optional base path if the app isn't served at "/" (no trailing slash). Default: "" (root). */
basePath?: string;
}
export function useToolUrlRouting(opts: UseToolUrlRoutingOpts) {
const {
selectedToolKey,
toolRegistry,
selectTool,
clearToolSelection,
onInitSelect,
onPopStateSelect,
basePath = '',
} = opts;
// Central slug map; keep here to co-locate routing policy.
const urlMap = useMemo(
() =>
new Map<string, string>([
['compress', 'compress-pdf'],
['split', 'split-pdf'],
['convert', 'convert-pdf'],
['ocr', 'ocr-pdf'],
['merge', 'merge-pdf'],
['rotate', 'rotate-pdf'],
]),
[]
);
const getToolUrlSlug = useCallback(
(toolKey: string) => urlMap.get(toolKey) ?? toolKey,
[urlMap]
);
const getToolKeyFromSlug = useCallback(
(slug: string) => {
for (const [key, value] of urlMap) {
if (value === slug) return key;
}
return slug; // fall back to raw key
},
[urlMap]
);
// Internal flag to avoid clearing URL on initial mount.
const [hasInitialized, setHasInitialized] = useState(false);
// Normalize a pathname by stripping basePath and leading slash.
const normalizePath = useCallback(
(fullPath: string) => {
let p = fullPath;
if (basePath && p.startsWith(basePath)) {
p = p.slice(basePath.length);
}
if (p.startsWith('/')) p = p.slice(1);
return p;
},
[basePath]
);
// Update URL when tool changes (but not on first paint before any selection happens).
useEffect(() => {
if (selectedToolKey) {
const slug = getToolUrlSlug(selectedToolKey);
const newUrl = `${basePath}/${slug}`.replace(/\/+/, '/');
window.history.replaceState({}, '', newUrl);
setHasInitialized(true);
} else if (hasInitialized) {
const rootUrl = basePath || '/';
window.history.replaceState({}, '', rootUrl);
}
}, [selectedToolKey, getToolUrlSlug, hasInitialized, basePath]);
// Initialize from URL when the registry is ready and nothing is selected yet.
useEffect(() => {
if (!toolRegistry || Object.keys(toolRegistry).length === 0) return;
if (selectedToolKey) return; // don't override explicit selection
const currentPath = normalizePath(window.location.pathname);
if (currentPath) {
const toolKey = getToolKeyFromSlug(currentPath);
if (toolRegistry[toolKey]) {
(onInitSelect ?? selectTool)(toolKey);
}
}
}, [toolRegistry, selectedToolKey, getToolKeyFromSlug, selectTool, onInitSelect, normalizePath]);
// Handle browser back/forward. NOTE: useRef needs an initial value in TS.
const popHandlerRef = useRef<((this: Window, ev: PopStateEvent) => any) | null>(null);
useEffect(() => {
popHandlerRef.current = () => {
const path = normalizePath(window.location.pathname);
if (path) {
const toolKey = getToolKeyFromSlug(path);
if (toolRegistry && toolRegistry[toolKey]) {
(onPopStateSelect ?? selectTool)(toolKey);
return;
}
}
clearToolSelection();
};
const handler = (e: PopStateEvent) => popHandlerRef.current?.call(window, e);
window.addEventListener('popstate', handler);
return () => window.removeEventListener('popstate', handler);
}, [toolRegistry, selectTool, clearToolSelection, getToolKeyFromSlug, onPopStateSelect, normalizePath]);
// Expose pure helpers if you want them elsewhere (optional).
return { getToolUrlSlug, getToolKeyFromSlug };
}
+77 -79
View File
@@ -1,120 +1,107 @@
/**
* URL synchronization hooks for tool routing
* URL synchronization hooks for tool routing with registry support
*/
import { useEffect, useCallback } from 'react';
import { ModeType } from '../types/navigation';
import { useEffect, useCallback, useRef } from 'react';
import { ToolId } from '../types/toolId';
import { parseToolRoute, updateToolRoute, clearToolRoute } from '../utils/urlRouting';
import { ToolRegistry } from '../data/toolsTaxonomy';
import { firePixel } from '../utils/scarfTracking';
/**
* Hook to sync navigation mode with URL
* Hook to sync workbench and tool with URL using registry
*/
export function useNavigationUrlSync(
currentMode: ModeType,
setMode: (mode: ModeType) => void,
selectedTool: ToolId | null,
handleToolSelect: (toolId: string) => void,
clearToolSelection: () => void,
registry: ToolRegistry,
enableSync: boolean = true
) {
// Initialize mode from URL on mount
const hasInitialized = useRef(false);
const prevSelectedTool = useRef<ToolId | null>(null);
// Initialize workbench and tool from URL on mount
useEffect(() => {
if (!enableSync) return;
const route = parseToolRoute();
if (route.mode !== currentMode) {
setMode(route.mode);
// Fire pixel for initial page load
const currentPath = window.location.pathname;
firePixel(currentPath);
const route = parseToolRoute(registry);
if (route.toolId !== selectedTool) {
if (route.toolId) {
handleToolSelect(route.toolId);
} else if (selectedTool !== null) {
// Only clear selection if we actually had a tool selected
// Don't clear on initial load when selectedTool starts as null
clearToolSelection();
}
}
hasInitialized.current = true;
}, []); // Only run on mount
// Update URL when mode changes
// Update URL when tool or workbench changes
useEffect(() => {
if (!enableSync) return;
if (currentMode === 'pageEditor') {
clearToolRoute();
} else {
updateToolRoute(currentMode, currentMode);
if (selectedTool) {
updateToolRoute(selectedTool, registry, false); // Use pushState for user navigation
} else if (prevSelectedTool.current !== null) {
// Only clear URL if we had a tool before (user navigated away)
// Don't clear on initial load when both current and previous are null
if (window.location.pathname !== '/') {
clearToolRoute(false); // Use pushState for user navigation
}
}
}, [currentMode, enableSync]);
prevSelectedTool.current = selectedTool;
}, [selectedTool, registry, enableSync]);
// Handle browser back/forward navigation
useEffect(() => {
if (!enableSync) return;
const handlePopState = () => {
const route = parseToolRoute();
if (route.mode !== currentMode) {
setMode(route.mode);
const route = parseToolRoute(registry);
if (route.toolId !== selectedTool) {
// Fire pixel for back/forward navigation
const currentPath = window.location.pathname;
firePixel(currentPath);
if (route.toolId) {
handleToolSelect(route.toolId);
} else {
clearToolSelection();
}
}
};
window.addEventListener('popstate', handlePopState);
return () => window.removeEventListener('popstate', handlePopState);
}, [currentMode, setMode, enableSync]);
}, [selectedTool, handleToolSelect, clearToolSelection, registry, enableSync]);
}
/**
* Hook to sync tool workflow with URL
* Hook to programmatically navigate to tools with registry support
*/
export function useToolWorkflowUrlSync(
selectedToolKey: string | null,
selectTool: (toolKey: string) => void,
clearTool: () => void,
enableSync: boolean = true
) {
// Initialize tool from URL on mount
useEffect(() => {
if (!enableSync) return;
const route = parseToolRoute();
if (route.toolKey && route.toolKey !== selectedToolKey) {
selectTool(route.toolKey);
} else if (!route.toolKey && selectedToolKey) {
clearTool();
}
}, []); // Only run on mount
export function useToolNavigation(registry: ToolRegistry) {
const navigateToTool = useCallback((toolId: ToolId) => {
updateToolRoute(toolId, registry);
// Update URL when tool changes
useEffect(() => {
if (!enableSync) return;
if (selectedToolKey) {
const route = parseToolRoute();
if (route.toolKey !== selectedToolKey) {
updateToolRoute(selectedToolKey as ModeType, selectedToolKey);
}
}
}, [selectedToolKey, enableSync]);
}
/**
* Hook to get current URL route information
*/
export function useCurrentRoute() {
const getCurrentRoute = useCallback(() => {
return parseToolRoute();
}, []);
return getCurrentRoute;
}
/**
* Hook to programmatically navigate to tools
*/
export function useToolNavigation() {
const navigateToTool = useCallback((toolKey: string) => {
updateToolRoute(toolKey as ModeType, toolKey);
// Dispatch a custom event to notify other components
window.dispatchEvent(new CustomEvent('toolNavigation', {
detail: { toolKey }
window.dispatchEvent(new CustomEvent('toolNavigation', {
detail: { toolId }
}));
}, []);
}, [registry]);
const navigateToHome = useCallback(() => {
clearToolRoute();
// Dispatch a custom event to notify other components
window.dispatchEvent(new CustomEvent('toolNavigation', {
detail: { toolKey: null }
window.dispatchEvent(new CustomEvent('toolNavigation', {
detail: { toolId: null }
}));
}, []);
@@ -122,4 +109,15 @@ export function useToolNavigation() {
navigateToTool,
navigateToHome
};
}
}
/**
* Hook to get current URL route information with registry support
*/
export function useCurrentRoute(registry: ToolRegistry) {
const getCurrentRoute = useCallback(() => {
return parseToolRoute(registry);
}, [registry]);
return getCurrentRoute;
}