diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 817f7b17e..7e6d23d50 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -32,6 +32,7 @@ "jszip": "^3.10.1", "pdf-lib": "^1.17.1", "pdfjs-dist": "^3.11.174", + "posthog-js": "^1.261.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.5.2", @@ -1774,6 +1775,12 @@ "url": "https://opencollective.com/popperjs" } }, + "node_modules/@posthog/core": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@posthog/core/-/core-1.0.2.tgz", + "integrity": "sha512-hWk3rUtJl2crQK0WNmwg13n82hnTwB99BT99/XI5gZSvIlYZ1TPmMZE8H2dhJJ98J/rm9vYJ/UXNzw3RV5HTpQ==", + "license": "MIT" + }, "node_modules/@rolldown/pluginutils": { "version": "1.0.0-beta.9", "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.9.tgz", @@ -3813,6 +3820,17 @@ "node": ">=18" } }, + "node_modules/core-js": { + "version": "3.45.1", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz", + "integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==", + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, "node_modules/core-util-is": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", @@ -4614,6 +4632,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.4.8.tgz", + "integrity": "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA==", + "license": "MIT" + }, "node_modules/file-selector": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-2.1.2.tgz", @@ -7388,6 +7412,47 @@ "postcss": "^8.2.9" } }, + "node_modules/posthog-js": { + "version": "1.261.0", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.261.0.tgz", + "integrity": "sha512-jyiXqyrCU+VlpbNNVRA6OQYAVut0XZMYNELCZH+XvTd981VqbE4jXn4XCBreo7XCL2gdPgDVxUVOuzNvEuKcmw==", + "license": "SEE LICENSE IN LICENSE", + "dependencies": { + "@posthog/core": "1.0.2", + "core-js": "^3.38.1", + "fflate": "^0.4.8", + "preact": "^10.19.3", + "web-vitals": "^4.2.4" + }, + "peerDependencies": { + "@rrweb/types": "2.0.0-alpha.17", + "rrweb-snapshot": "2.0.0-alpha.17" + }, + "peerDependenciesMeta": { + "@rrweb/types": { + "optional": true + }, + "rrweb-snapshot": { + "optional": true + } + } + }, + "node_modules/posthog-js/node_modules/web-vitals": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", + "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "license": "Apache-2.0" + }, + "node_modules/preact": { + "version": "10.27.1", + "resolved": "https://registry.npmjs.org/preact/-/preact-10.27.1.tgz", + "integrity": "sha512-V79raXEWch/rbqoNc7nT9E4ep7lu+mI3+sBmfRD4i1M73R3WLYcCtdI0ibxGVf4eQL8ZIz2nFacqEC+rmnOORQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/preact" + } + }, "node_modules/precinct": { "version": "12.2.0", "resolved": "https://registry.npmjs.org/precinct/-/precinct-12.2.0.tgz", diff --git a/frontend/package.json b/frontend/package.json index eaa5f20d4..13c795fc2 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -28,6 +28,7 @@ "jszip": "^3.10.1", "pdf-lib": "^1.17.1", "pdfjs-dist": "^3.11.174", + "posthog-js": "^1.261.0", "react": "^19.1.0", "react-dom": "^19.1.0", "react-i18next": "^15.5.2", diff --git a/frontend/scripts/generate-licenses.js b/frontend/scripts/generate-licenses.js index cfd5f675a..aaac69800 100644 --- a/frontend/scripts/generate-licenses.js +++ b/frontend/scripts/generate-licenses.js @@ -30,11 +30,11 @@ try { } // Generate license report using license-checker (more reliable) - const licenseReport = execSync('npx license-checker --production --json', { + const licenseReport = execSync('npx license-checker --production --json', { encoding: 'utf8', cwd: path.dirname(PACKAGE_JSON) }); - + let licenseData; try { licenseData = JSON.parse(licenseReport); @@ -43,16 +43,16 @@ try { console.error('Raw output:', licenseReport.substring(0, 500) + '...'); process.exit(1); } - + if (!licenseData || typeof licenseData !== 'object') { console.error('āŒ Invalid license data structure'); process.exit(1); } - + // Convert license-checker format to array const licenseArray = Object.entries(licenseData).map(([key, value]) => { let name, version; - + // Handle scoped packages like @mantine/core@1.0.0 if (key.startsWith('@')) { const parts = key.split('@'); @@ -64,30 +64,30 @@ try { name = key.substring(0, lastAtIndex); version = key.substring(lastAtIndex + 1); } - + // Normalize license types for edge cases let licenseType = value.licenses; - + // Handle missing or null licenses if (!licenseType || licenseType === null || licenseType === undefined) { licenseType = 'Unknown'; } - + // Handle empty string licenses if (licenseType === '') { licenseType = 'Unknown'; } - + // Handle array licenses (rare but possible) if (Array.isArray(licenseType)) { licenseType = licenseType.join(' AND '); } - + // Handle object licenses (fallback) if (typeof licenseType === 'object' && licenseType !== null) { licenseType = 'Unknown'; } - + return { name: name, version: version || value.version || 'unknown', @@ -97,13 +97,13 @@ try { link: value.licenseUrl }; }); - + // Transform to match Java backend format const transformedData = { dependencies: licenseArray.map(dep => { const licenseType = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : (dep.licenseType || 'Unknown'); const licenseUrl = dep.link || getLicenseUrl(licenseType); - + return { moduleName: dep.name, moduleUrl: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}`, @@ -113,29 +113,29 @@ try { }; }) }; - + // Log summary of license types found const licenseSummary = licenseArray.reduce((acc, dep) => { const license = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : (dep.licenseType || 'Unknown'); acc[license] = (acc[license] || 0) + 1; return acc; }, {}); - + console.log('šŸ“Š License types found:'); Object.entries(licenseSummary).forEach(([license, count]) => { console.log(` ${license}: ${count} packages`); }); - + // Log any complex or unusual license formats for debugging - const complexLicenses = licenseArray.filter(dep => + const complexLicenses = licenseArray.filter(dep => dep.licenseType && ( - dep.licenseType.includes('AND') || - dep.licenseType.includes('OR') || + dep.licenseType.includes('AND') || + dep.licenseType.includes('OR') || dep.licenseType === 'Unknown' || dep.licenseType.includes('SEE LICENSE') ) ); - + if (complexLicenses.length > 0) { console.log('\nšŸ” Complex/Edge case licenses detected:'); complexLicenses.forEach(dep => { @@ -150,7 +150,7 @@ try { problematicLicenses.forEach(warning => { console.log(` ${warning.message}`); }); - + // Write license warnings to a separate file for CI/CD const warningsFile = path.join(__dirname, '..', 'src', 'assets', 'license-warnings.json'); fs.writeFileSync(warningsFile, JSON.stringify({ @@ -164,11 +164,11 @@ try { // Write to file fs.writeFileSync(OUTPUT_FILE, JSON.stringify(transformedData, null, 4)); - + console.log(`āœ… License report generated successfully!`); console.log(`šŸ“„ Found ${transformedData.dependencies.length} dependencies`); console.log(`šŸ’¾ Saved to: ${OUTPUT_FILE}`); - + } catch (error) { console.error('āŒ Error generating license report:', error.message); process.exit(1); @@ -179,9 +179,10 @@ try { */ function getLicenseUrl(licenseType) { if (!licenseType || licenseType === 'Unknown') return ''; - + const licenseUrls = { 'MIT': 'https://opensource.org/licenses/MIT', + 'MIT*': 'https://opensource.org/licenses/MIT', 'Apache-2.0': 'https://www.apache.org/licenses/LICENSE-2.0', 'Apache License 2.0': 'https://www.apache.org/licenses/LICENSE-2.0', 'BSD-3-Clause': 'https://opensource.org/licenses/BSD-3-Clause', @@ -206,12 +207,12 @@ function getLicenseUrl(licenseType) { 'Public Domain': 'https://creativecommons.org/publicdomain/zero/1.0/', 'UNLICENSED': '' }; - + // Try exact match first if (licenseUrls[licenseType]) { return licenseUrls[licenseType]; } - + // Try case-insensitive match const lowerType = licenseType.toLowerCase(); for (const [key, url] of Object.entries(licenseUrls)) { @@ -219,7 +220,7 @@ function getLicenseUrl(licenseType) { return url; } } - + // Handle complex SPDX expressions like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)" if (licenseType.includes('AND') || licenseType.includes('OR')) { // Extract the first license from compound expressions for URL @@ -228,7 +229,7 @@ function getLicenseUrl(licenseType) { return licenseUrls[match[1]]; } } - + // For non-standard licenses, return empty string (will use package link if available) return ''; } @@ -238,7 +239,7 @@ function getLicenseUrl(licenseType) { */ function checkLicenseCompatibility(licenseSummary, licenseArray) { const warnings = []; - + // Define problematic license patterns const problematicLicenses = { // Copyleft licenses @@ -248,7 +249,7 @@ function checkLicenseCompatibility(licenseSummary, licenseArray) { 'LGPL-3.0': 'Weak copyleft license - may require source disclosure for modifications', 'AGPL-3.0': 'Network copyleft license - requires source disclosure for network use', 'AGPL-1.0': 'Network copyleft license - requires source disclosure for network use', - + // Other potentially problematic licenses 'WTFPL': 'Potentially problematic license - legal uncertainty', 'CC-BY-SA-4.0': 'ShareAlike license - requires derivative works to use same license', @@ -267,47 +268,47 @@ function checkLicenseCompatibility(licenseSummary, licenseArray) { 'UNLICENSED': 'No license specified - usage rights unclear', 'Unknown': 'License not detected - manual review required' }; - + // Known good licenses (no warnings needed) const goodLicenses = new Set([ - 'MIT', 'Apache-2.0', 'Apache License 2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'BSD', + 'MIT', 'MIT*', 'Apache-2.0', 'Apache License 2.0', 'BSD-2-Clause', 'BSD-3-Clause', 'BSD', 'ISC', 'CC0-1.0', 'Public Domain', 'Unlicense', '0BSD', 'BlueOak-1.0.0', 'Zlib', 'Artistic-2.0', 'Python-2.0', 'Ruby', 'MPL-2.0', 'CC-BY-4.0', 'SEE LICENSE IN https://raw.githubusercontent.com/Stirling-Tools/Stirling-PDF/refs/heads/main/proprietary/LICENSE' ]); - + // Helper function to normalize license names for comparison function normalizeLicense(license) { return license .replace(/-or-later$/, '') // Remove -or-later suffix - .replace(/\+$/, '') // Remove + suffix + .replace(/\+$/, '') // Remove + suffix .trim(); } - + // Check each license type Object.entries(licenseSummary).forEach(([license, count]) => { // Skip known good licenses if (goodLicenses.has(license)) { return; } - + // Check if this license only affects our own packages const affectedPackages = licenseArray.filter(dep => { const depLicense = Array.isArray(dep.licenseType) ? dep.licenseType.join(', ') : dep.licenseType; return depLicense === license; }); - - const isOnlyOurPackages = affectedPackages.every(dep => - dep.name === 'frontend' || + + const isOnlyOurPackages = affectedPackages.every(dep => + dep.name === 'frontend' || dep.name.toLowerCase().includes('stirling-pdf') || dep.name.toLowerCase().includes('stirling_pdf') || dep.name.toLowerCase().includes('stirlingpdf') ); - + if (isOnlyOurPackages && (license === 'UNLICENSED' || license.startsWith('SEE LICENSE IN'))) { return; // Skip warnings for our own Stirling-PDF packages } - + // Check for compound licenses like "(MIT AND Zlib)" or "(MIT OR CC0-1.0)" if (license.includes('AND') || license.includes('OR')) { // For OR licenses, check if there's at least one acceptable license option @@ -317,23 +318,23 @@ function checkLicenseCompatibility(licenseSummary, licenseArray) { .replace(/[()]/g, '') // Remove parentheses .split(' OR ') .map(component => component.trim()); - + // Check if any component is in the goodLicenses set (with normalization) const hasGoodLicense = orComponents.some(component => { const normalized = normalizeLicense(component); return goodLicenses.has(component) || goodLicenses.has(normalized); }); - + if (hasGoodLicense) { return; // Skip warning - can use the good license option } } - + // For AND licenses or OR licenses with no good options, check for problematic components - const hasProblematicComponent = Object.keys(problematicLicenses).some(problematic => + const hasProblematicComponent = Object.keys(problematicLicenses).some(problematic => license.includes(problematic) ); - + if (hasProblematicComponent) { const affectedPackages = licenseArray .filter(dep => { @@ -345,12 +346,12 @@ function checkLicenseCompatibility(licenseSummary, licenseArray) { version: dep.version, url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}` })); - + const licenseType = license.includes('AND') ? 'AND' : 'OR'; - const reason = licenseType === 'AND' + const reason = licenseType === 'AND' ? 'Compound license with AND requirement - all components must be compatible' : 'Compound license with potentially problematic components and no good fallback options'; - + warnings.push({ message: `šŸ“‹ This PR contains ${count} package${count > 1 ? 's' : ''} with compound license "${license}" - manual review recommended`, licenseType: license, @@ -362,7 +363,7 @@ function checkLicenseCompatibility(licenseSummary, licenseArray) { } return; } - + // Check for exact matches with problematic licenses if (problematicLicenses[license]) { const affectedPackages = licenseArray @@ -375,10 +376,10 @@ function checkLicenseCompatibility(licenseSummary, licenseArray) { version: dep.version, url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}` })); - + const packageList = affectedPackages.map(pkg => pkg.name).slice(0, 5).join(', ') + (affectedPackages.length > 5 ? `, and ${affectedPackages.length - 5} more` : ''); const licenseUrl = getLicenseUrl(license) || 'https://opensource.org/licenses'; - + warnings.push({ message: `āš ļø This PR contains ${count} package${count > 1 ? 's' : ''} with license type [${license}](${licenseUrl}) - ${problematicLicenses[license]}. Affected packages: ${packageList}`, licenseType: license, @@ -399,7 +400,7 @@ function checkLicenseCompatibility(licenseSummary, licenseArray) { version: dep.version, url: dep.repository || dep.url || `https://www.npmjs.com/package/${dep.name}` })); - + warnings.push({ message: `ā“ This PR contains ${count} package${count > 1 ? 's' : ''} with unknown license type "${license}" - manual review required`, licenseType: license, @@ -410,6 +411,6 @@ function checkLicenseCompatibility(licenseSummary, licenseArray) { }); } }); - + return warnings; -} \ No newline at end of file +} diff --git a/frontend/src/components/fileEditor/FileEditor.tsx b/frontend/src/components/fileEditor/FileEditor.tsx index d531db8a3..b2dd334c2 100644 --- a/frontend/src/components/fileEditor/FileEditor.tsx +++ b/frontend/src/components/fileEditor/FileEditor.tsx @@ -412,9 +412,9 @@ const FileEditor = ({ if (record) { // Set the file as selected in context and switch to viewer for preview setSelectedFiles([fileId]); - navActions.setMode('viewer'); + navActions.setWorkbench('viewer'); } - }, [activeFileRecords, setSelectedFiles, navActions.setMode]); + }, [activeFileRecords, setSelectedFiles, navActions.setWorkbench]); const handleMergeFromHere = useCallback((fileId: FileId) => { const startIndex = activeFileRecords.findIndex(r => r.id === fileId); diff --git a/frontend/src/components/layout/Workbench.tsx b/frontend/src/components/layout/Workbench.tsx index 3884fdaf5..a77067d99 100644 --- a/frontend/src/components/layout/Workbench.tsx +++ b/frontend/src/components/layout/Workbench.tsx @@ -6,13 +6,13 @@ import { useToolWorkflow } from '../../contexts/ToolWorkflowContext'; import { useFileHandler } from '../../hooks/useFileHandler'; import { useFileState, useFileActions } from '../../contexts/FileContext'; import { useNavigationState, useNavigationActions } from '../../contexts/NavigationContext'; +import { useToolManagement } from '../../hooks/useToolManagement'; import TopControls from '../shared/TopControls'; import FileEditor from '../fileEditor/FileEditor'; import PageEditor from '../pageEditor/PageEditor'; import PageEditorControls from '../pageEditor/PageEditorControls'; import Viewer from '../viewer/Viewer'; -import ToolRenderer from '../tools/ToolRenderer'; import LandingPage from '../shared/LandingPage'; // No props needed - component uses contexts directly @@ -23,9 +23,9 @@ export default function Workbench() { // Use context-based hooks to eliminate all prop drilling const { state } = useFileState(); const { actions } = useFileActions(); - const { currentMode: currentView } = useNavigationState(); + const { workbench: currentView } = useNavigationState(); const { actions: navActions } = useNavigationActions(); - const setCurrentView = navActions.setMode; + const setCurrentView = navActions.setWorkbench; const activeFiles = state.files.ids; const { previewFile, @@ -36,7 +36,14 @@ export default function Workbench() { setSidebarsVisible } = useToolWorkflow(); - const { selectedToolKey, selectedTool, handleToolSelect } = useToolWorkflow(); + const { handleToolSelect } = useToolWorkflow(); + + // Get navigation state - this is the source of truth + const { selectedTool: selectedToolId } = useNavigationState(); + + // Get tool registry to look up selected tool + const { toolRegistry } = useToolManagement(); + const selectedTool = selectedToolId ? toolRegistry[selectedToolId] : null; const { addToActiveFiles } = useFileHandler(); const handlePreviewClose = () => { @@ -69,11 +76,11 @@ export default function Workbench() { case "fileEditor": return ( { setCurrentView("pageEditor"); }, @@ -127,14 +134,6 @@ export default function Workbench() { ); default: - // Check if it's a tool view - if (selectedToolKey && selectedTool) { - return ( - - ); - } return ( ); @@ -154,7 +153,7 @@ export default function Workbench() { {/* Main content area */} diff --git a/frontend/src/components/pageEditor/PageEditor.tsx b/frontend/src/components/pageEditor/PageEditor.tsx index ec007f327..f337eb605 100644 --- a/frontend/src/components/pageEditor/PageEditor.tsx +++ b/frontend/src/components/pageEditor/PageEditor.tsx @@ -6,7 +6,6 @@ import { } from "@mantine/core"; import { useTranslation } from "react-i18next"; import { useFileState, useFileActions, useCurrentFile, useFileSelection } from "../../contexts/FileContext"; -import { ModeType } from "../../contexts/NavigationContext"; import { PDFDocument, PDFPage, PageEditorFunctions } from "../../types/pageEditor"; import { ProcessedFile as EnhancedProcessedFile } from "../../types/processing"; import { pdfExportService } from "../../services/pdfExportService"; diff --git a/frontend/src/components/shared/RightRail.tsx b/frontend/src/components/shared/RightRail.tsx index 614a60cf3..b141ec493 100644 --- a/frontend/src/components/shared/RightRail.tsx +++ b/frontend/src/components/shared/RightRail.tsx @@ -25,7 +25,7 @@ export default function RightRail() { const [csvInput, setCsvInput] = useState(""); // Navigation view - const { currentMode: currentView } = useNavigationState(); + const { workbench: currentView } = useNavigationState(); // File state and selection const { state, selectors } = useFileState(); diff --git a/frontend/src/components/shared/TopControls.tsx b/frontend/src/components/shared/TopControls.tsx index 1a9dbbd9f..489468eb2 100644 --- a/frontend/src/components/shared/TopControls.tsx +++ b/frontend/src/components/shared/TopControls.tsx @@ -5,7 +5,7 @@ import rainbowStyles from '../../styles/rainbow.module.css'; import VisibilityIcon from "@mui/icons-material/Visibility"; import EditNoteIcon from "@mui/icons-material/EditNote"; import FolderIcon from "@mui/icons-material/Folder"; -import { ModeType, isValidMode } from '../../contexts/NavigationContext'; +import { WorkbenchType, isValidWorkbench } from '../../types/workbench'; import { Tooltip } from "./Tooltip"; const viewOptionStyle = { @@ -19,7 +19,7 @@ const viewOptionStyle = { // Build view options showing text only for current view; others icon-only with tooltip -const createViewOptions = (currentView: ModeType, switchingTo: ModeType | null) => [ +const createViewOptions = (currentView: WorkbenchType, switchingTo: WorkbenchType | null) => [ { label: (
@@ -70,8 +70,8 @@ const createViewOptions = (currentView: ModeType, switchingTo: ModeType | null) ]; interface TopControlsProps { - currentView: ModeType; - setCurrentView: (view: ModeType) => void; + currentView: WorkbenchType; + setCurrentView: (view: WorkbenchType) => void; selectedToolKey?: string | null; } @@ -81,25 +81,25 @@ const TopControls = ({ selectedToolKey, }: TopControlsProps) => { const { isRainbowMode } = useRainbowThemeContext(); - const [switchingTo, setSwitchingTo] = useState(null); + const [switchingTo, setSwitchingTo] = useState(null); const isToolSelected = selectedToolKey !== null; const handleViewChange = useCallback((view: string) => { - if (!isValidMode(view)) { - // Ignore invalid values defensively + if (!isValidWorkbench(view)) { return; } - const mode = view as ModeType; + + const workbench = view; // Show immediate feedback - setSwitchingTo(mode as ModeType); + setSwitchingTo(workbench); // Defer the heavy view change to next frame so spinner can render requestAnimationFrame(() => { // Give the spinner one more frame to show requestAnimationFrame(() => { - setCurrentView(mode as ModeType); + setCurrentView(workbench); // Clear the loading state after view change completes setTimeout(() => setSwitchingTo(null), 300); diff --git a/frontend/src/components/tools/automate/AutomationCreation.tsx b/frontend/src/components/tools/automate/AutomationCreation.tsx index 49e9f6f1b..5dfdc0468 100644 --- a/frontend/src/components/tools/automate/AutomationCreation.tsx +++ b/frontend/src/components/tools/automate/AutomationCreation.tsx @@ -11,7 +11,7 @@ import { Modal } from '@mantine/core'; import CheckIcon from '@mui/icons-material/Check'; -import { ToolRegistryEntry } from '../../../data/toolsTaxonomy'; +import { ToolRegistry } from '../../../data/toolsTaxonomy'; import ToolConfigurationModal from './ToolConfigurationModal'; import ToolList from './ToolList'; import IconSelector from './IconSelector'; @@ -24,7 +24,7 @@ interface AutomationCreationProps { existingAutomation?: AutomationConfig; onBack: () => void; onComplete: (automation: AutomationConfig) => void; - toolRegistry: Record; + toolRegistry: ToolRegistry; } export default function AutomationCreation({ mode, existingAutomation, onBack, onComplete, toolRegistry }: AutomationCreationProps) { diff --git a/frontend/src/components/tools/automate/AutomationRun.tsx b/frontend/src/components/tools/automate/AutomationRun.tsx index 640f802f6..1a3cb1263 100644 --- a/frontend/src/components/tools/automate/AutomationRun.tsx +++ b/frontend/src/components/tools/automate/AutomationRun.tsx @@ -33,7 +33,7 @@ export default function AutomationRun({ automation, onComplete, automateOperatio React.useEffect(() => { if (automation?.operations) { const steps = automation.operations.map((op: any, index: number) => { - const tool = toolRegistry[op.operation]; + const tool = toolRegistry[op.operation as keyof typeof toolRegistry]; return { id: `${op.operation}-${index}`, operation: op.operation, diff --git a/frontend/src/components/tools/automate/ToolConfigurationModal.tsx b/frontend/src/components/tools/automate/ToolConfigurationModal.tsx index d97819fb8..b2aa1c975 100644 --- a/frontend/src/components/tools/automate/ToolConfigurationModal.tsx +++ b/frontend/src/components/tools/automate/ToolConfigurationModal.tsx @@ -35,7 +35,7 @@ export default function ToolConfigurationModal({ opened, tool, onSave, onCancel, const [isValid, setIsValid] = useState(true); // Get tool info from registry - const toolInfo = toolRegistry[tool.operation]; + const toolInfo = toolRegistry[tool.operation as keyof ToolRegistry]; const SettingsComponent = toolInfo?.settingsComponent; // Initialize parameters from tool (which should contain defaults from registry) diff --git a/frontend/src/contexts/NavigationContext.tsx b/frontend/src/contexts/NavigationContext.tsx index 532eb20bb..3b65f01d1 100644 --- a/frontend/src/contexts/NavigationContext.tsx +++ b/frontend/src/contexts/NavigationContext.tsx @@ -1,84 +1,94 @@ import React, { createContext, useContext, useReducer, useCallback } from 'react'; -import { useNavigationUrlSync } from '../hooks/useUrlSync'; -import { ModeType, isValidMode, getDefaultMode } from '../types/navigation'; +import { WorkbenchType, getDefaultWorkbench } from '../types/workbench'; +import { ToolId, isValidToolId } from '../types/toolId'; +import { useFlatToolRegistry } from '../data/useTranslatedToolRegistry'; /** * NavigationContext - Complete navigation management system - * + * * Handles navigation modes, navigation guards for unsaved changes, - * and breadcrumb/history navigation. Separated from FileContext to + * and breadcrumb/history navigation. Separated from FileContext to * maintain clear separation of concerns. */ // Navigation state -interface NavigationState { - currentMode: ModeType; +interface NavigationContextState { + workbench: WorkbenchType; + selectedTool: ToolId | null; hasUnsavedChanges: boolean; pendingNavigation: (() => void) | null; showNavigationWarning: boolean; - selectedToolKey: string | null; // Add tool selection to navigation state } // Navigation actions type NavigationAction = - | { type: 'SET_MODE'; payload: { mode: ModeType } } + | { type: 'SET_WORKBENCH'; payload: { workbench: WorkbenchType } } + | { type: 'SET_SELECTED_TOOL'; payload: { toolId: ToolId | null } } + | { type: 'SET_TOOL_AND_WORKBENCH'; payload: { toolId: ToolId | null; workbench: WorkbenchType } } | { type: 'SET_UNSAVED_CHANGES'; payload: { hasChanges: boolean } } | { type: 'SET_PENDING_NAVIGATION'; payload: { navigationFn: (() => void) | null } } - | { type: 'SHOW_NAVIGATION_WARNING'; payload: { show: boolean } } - | { type: 'SET_SELECTED_TOOL'; payload: { toolKey: string | null } }; + | { type: 'SHOW_NAVIGATION_WARNING'; payload: { show: boolean } }; // Navigation reducer -const navigationReducer = (state: NavigationState, action: NavigationAction): NavigationState => { +const navigationReducer = (state: NavigationContextState, action: NavigationAction): NavigationContextState => { switch (action.type) { - case 'SET_MODE': - return { ...state, currentMode: action.payload.mode }; - + case 'SET_WORKBENCH': + return { ...state, workbench: action.payload.workbench }; + + case 'SET_SELECTED_TOOL': + return { ...state, selectedTool: action.payload.toolId }; + + case 'SET_TOOL_AND_WORKBENCH': + return { + ...state, + selectedTool: action.payload.toolId, + workbench: action.payload.workbench + }; + case 'SET_UNSAVED_CHANGES': return { ...state, hasUnsavedChanges: action.payload.hasChanges }; - + case 'SET_PENDING_NAVIGATION': return { ...state, pendingNavigation: action.payload.navigationFn }; - + case 'SHOW_NAVIGATION_WARNING': return { ...state, showNavigationWarning: action.payload.show }; - - case 'SET_SELECTED_TOOL': - return { ...state, selectedToolKey: action.payload.toolKey }; - + default: return state; } }; // Initial state -const initialState: NavigationState = { - currentMode: getDefaultMode(), +const initialState: NavigationContextState = { + workbench: getDefaultWorkbench(), + selectedTool: null, hasUnsavedChanges: false, pendingNavigation: null, - showNavigationWarning: false, - selectedToolKey: null + showNavigationWarning: false }; // Navigation context actions interface export interface NavigationContextActions { - setMode: (mode: ModeType) => void; + setWorkbench: (workbench: WorkbenchType) => void; + setSelectedTool: (toolId: ToolId | null) => void; + setToolAndWorkbench: (toolId: ToolId | null, workbench: WorkbenchType) => void; setHasUnsavedChanges: (hasChanges: boolean) => void; showNavigationWarning: (show: boolean) => void; requestNavigation: (navigationFn: () => void) => void; confirmNavigation: () => void; cancelNavigation: () => void; - selectTool: (toolKey: string) => void; clearToolSelection: () => void; handleToolSelect: (toolId: string) => void; } -// Split context values +// Context state values export interface NavigationContextStateValue { - currentMode: ModeType; + workbench: WorkbenchType; + selectedTool: ToolId | null; hasUnsavedChanges: boolean; pendingNavigation: (() => void) | null; showNavigationWarning: boolean; - selectedToolKey: string | null; } export interface NavigationContextActionsValue { @@ -90,15 +100,24 @@ const NavigationStateContext = createContext(undefined); // Provider component -export const NavigationProvider: React.FC<{ +export const NavigationProvider: React.FC<{ children: React.ReactNode; enableUrlSync?: boolean; }> = ({ children, enableUrlSync = true }) => { const [state, dispatch] = useReducer(navigationReducer, initialState); + const toolRegistry = useFlatToolRegistry(); const actions: NavigationContextActions = { - setMode: useCallback((mode: ModeType) => { - dispatch({ type: 'SET_MODE', payload: { mode } }); + setWorkbench: useCallback((workbench: WorkbenchType) => { + dispatch({ type: 'SET_WORKBENCH', payload: { workbench } }); + }, []), + + setSelectedTool: useCallback((toolId: ToolId | null) => { + dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolId } }); + }, []), + + setToolAndWorkbench: useCallback((toolId: ToolId | null, workbench: WorkbenchType) => { + dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId, workbench } }); }, []), setHasUnsavedChanges: useCallback((hasChanges: boolean) => { @@ -110,75 +129,67 @@ export const NavigationProvider: React.FC<{ }, []), requestNavigation: useCallback((navigationFn: () => void) => { - // If no unsaved changes, navigate immediately if (!state.hasUnsavedChanges) { navigationFn(); return; } - // Otherwise, store the navigation and show warning dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn } }); dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: true } }); }, [state.hasUnsavedChanges]), confirmNavigation: useCallback(() => { - // Execute pending navigation if (state.pendingNavigation) { state.pendingNavigation(); } - - // Clear navigation state + dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } }); dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } }); }, [state.pendingNavigation]), cancelNavigation: useCallback(() => { - // Clear navigation without executing dispatch({ type: 'SET_PENDING_NAVIGATION', payload: { navigationFn: null } }); dispatch({ type: 'SHOW_NAVIGATION_WARNING', payload: { show: false } }); }, []), - selectTool: useCallback((toolKey: string) => { - dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey } }); - }, []), - clearToolSelection: useCallback(() => { - dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } }); + dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: getDefaultWorkbench() } }); }, []), handleToolSelect: useCallback((toolId: string) => { - // Handle special cases if (toolId === 'allTools') { - dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } }); + dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: getDefaultWorkbench() } }); return; } - // Special-case: if tool is a dedicated reader tool, enter reader mode if (toolId === 'read' || toolId === 'view-pdf') { - dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: null } }); + dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: null, workbench: 'viewer' } }); return; } - dispatch({ type: 'SET_SELECTED_TOOL', payload: { toolKey: toolId } }); - dispatch({ type: 'SET_MODE', payload: { mode: 'fileEditor' as ModeType } }); - }, []) + // Look up the tool in the registry to get its proper workbench + + const tool = isValidToolId(toolId)? toolRegistry[toolId] : null; + const workbench = tool ? (tool.workbench || getDefaultWorkbench()) : getDefaultWorkbench(); + + // Validate toolId and convert to ToolId type + const validToolId = isValidToolId(toolId) ? toolId : null; + dispatch({ type: 'SET_TOOL_AND_WORKBENCH', payload: { toolId: validToolId, workbench } }); + }, [toolRegistry]) }; const stateValue: NavigationContextStateValue = { - currentMode: state.currentMode, + workbench: state.workbench, + selectedTool: state.selectedTool, hasUnsavedChanges: state.hasUnsavedChanges, pendingNavigation: state.pendingNavigation, - showNavigationWarning: state.showNavigationWarning, - selectedToolKey: state.selectedToolKey + showNavigationWarning: state.showNavigationWarning }; const actionsValue: NavigationContextActionsValue = { actions }; - // Enable URL synchronization - useNavigationUrlSync(state.currentMode, actions.setMode, enableUrlSync); - return ( @@ -216,7 +227,7 @@ export const useNavigation = () => { export const useNavigationGuard = () => { const state = useNavigationState(); const { actions } = useNavigationActions(); - + return { pendingNavigation: state.pendingNavigation, showNavigationWarning: state.showNavigationWarning, @@ -228,13 +239,3 @@ export const useNavigationGuard = () => { setShowNavigationWarning: actions.showNavigationWarning }; }; - -// Re-export utility functions from types for backward compatibility -export { isValidMode, getDefaultMode, type ModeType } from '../types/navigation'; - -// TODO: This will be expanded for URL-based routing system -// - URL parsing utilities -// - Route definitions -// - Navigation hooks with URL sync -// - History management -// - Breadcrumb restoration from URL params \ No newline at end of file diff --git a/frontend/src/contexts/ToolWorkflowContext.tsx b/frontend/src/contexts/ToolWorkflowContext.tsx index 2ab92572b..4077bac60 100644 --- a/frontend/src/contexts/ToolWorkflowContext.tsx +++ b/frontend/src/contexts/ToolWorkflowContext.tsx @@ -6,9 +6,11 @@ import React, { createContext, useContext, useReducer, useCallback, useMemo } from 'react'; import { useToolManagement } from '../hooks/useToolManagement'; import { PageEditorFunctions } from '../types/pageEditor'; -import { ToolRegistryEntry } from '../data/toolsTaxonomy'; -import { useToolWorkflowUrlSync } from '../hooks/useUrlSync'; +import { ToolRegistryEntry, ToolRegistry } from '../data/toolsTaxonomy'; import { useNavigationActions, useNavigationState } from './NavigationContext'; +import { ToolId, isValidToolId } from '../types/toolId'; +import { useNavigationUrlSync } from '../hooks/useUrlSync'; +import { getDefaultWorkbench } from '../types/workbench'; // State interface interface ToolWorkflowState { @@ -83,7 +85,7 @@ interface ToolWorkflowContextValue extends ToolWorkflowState { setSearchQuery: (query: string) => void; // Tool Actions - selectTool: (toolId: string) => void; + selectTool: (toolId: ToolId | null) => void; clearToolSelection: () => void; // Tool Reset Actions @@ -124,7 +126,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { } = useToolManagement(); // Get selected tool from navigation context - const selectedTool = getSelectedTool(navigationState.selectedToolKey); + const selectedTool = getSelectedTool(navigationState.selectedTool); // UI Action creators const setSidebarsVisible = useCallback((visible: boolean) => { @@ -142,7 +144,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { const setPreviewFile = useCallback((file: File | null) => { dispatch({ type: 'SET_PREVIEW_FILE', payload: file }); if (file) { - actions.setMode('viewer'); + actions.setWorkbench('viewer'); } }, [actions]); @@ -172,7 +174,17 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { // Workflow actions (compound actions that coordinate multiple state changes) const handleToolSelect = useCallback((toolId: string) => { - actions.handleToolSelect(toolId); + // Set the selected tool and determine the appropriate workbench + const validToolId = isValidToolId(toolId) ? toolId : null; + actions.setSelectedTool(validToolId); + + // Get the tool from registry to determine workbench + const tool = getSelectedTool(toolId); + if (tool && tool.workbench) { + actions.setWorkbench(tool.workbench); + } else { + actions.setWorkbench(getDefaultWorkbench()); + } // Clear search query when selecting a tool setSearchQuery(''); @@ -189,13 +201,13 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { setLeftPanelView('toolContent'); setReaderMode(false); // Disable read mode when selecting tools } - }, [actions, setLeftPanelView, setReaderMode, setSearchQuery]); + }, [actions, getSelectedTool, setLeftPanelView, setReaderMode, setSearchQuery]); const handleBackToTools = useCallback(() => { setLeftPanelView('toolPicker'); setReaderMode(false); - actions.clearToolSelection(); - }, [setLeftPanelView, setReaderMode, actions]); + actions.setSelectedTool(null); + }, [setLeftPanelView, setReaderMode, actions.setSelectedTool]); const handleReaderToggle = useCallback(() => { setReaderMode(true); @@ -214,14 +226,20 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { [state.sidebarsVisible, state.readerMode] ); - // Enable URL synchronization for tool selection - useToolWorkflowUrlSync(navigationState.selectedToolKey, actions.selectTool, actions.clearToolSelection, true); + // URL sync for proper tool navigation + useNavigationUrlSync( + navigationState.selectedTool, + handleToolSelect, + handleBackToTools, + toolRegistry as ToolRegistry, + true + ); // Properly memoized context value const contextValue = useMemo((): ToolWorkflowContextValue => ({ // State ...state, - selectedToolKey: navigationState.selectedToolKey, + selectedToolKey: navigationState.selectedTool, selectedTool, toolRegistry, @@ -232,8 +250,8 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { setPreviewFile, setPageEditorFunctions, setSearchQuery, - selectTool: actions.selectTool, - clearToolSelection: actions.clearToolSelection, + selectTool: actions.setSelectedTool, + clearToolSelection: () => actions.setSelectedTool(null), // Tool Reset Actions registerToolReset, @@ -249,7 +267,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { isPanelVisible, }), [ state, - navigationState.selectedToolKey, + navigationState.selectedTool, selectedTool, toolRegistry, setSidebarsVisible, @@ -258,8 +276,7 @@ export function ToolWorkflowProvider({ children }: ToolWorkflowProviderProps) { setPreviewFile, setPageEditorFunctions, setSearchQuery, - actions.selectTool, - actions.clearToolSelection, + actions.setSelectedTool, registerToolReset, resetTool, handleToolSelect, diff --git a/frontend/src/data/toolsTaxonomy.ts b/frontend/src/data/toolsTaxonomy.ts index 0892b8b6c..6d2590481 100644 --- a/frontend/src/data/toolsTaxonomy.ts +++ b/frontend/src/data/toolsTaxonomy.ts @@ -1,8 +1,9 @@ import { type TFunction } from 'i18next'; import React from 'react'; -import { ToolOperationHook, ToolOperationConfig } from '../hooks/tools/shared/useToolOperation'; +import { ToolOperationConfig } from '../hooks/tools/shared/useToolOperation'; import { BaseToolProps } from '../types/tool'; -import { BaseParameters } from '../types/parameters'; +import { WorkbenchType } from '../types/workbench'; +import { ToolId } from '../types/toolId'; export enum SubcategoryId { SIGNING = 'signing', @@ -28,7 +29,6 @@ export type ToolRegistryEntry = { icon: React.ReactNode; name: string; component: React.ComponentType | null; - view: 'sign' | 'security' | 'format' | 'extract' | 'view' | 'merge' | 'pageEditor' | 'convert' | 'redact' | 'split' | 'convert' | 'remove' | 'compress' | 'external'; description: string; categoryId: ToolCategoryId; subcategoryId: SubcategoryId; @@ -37,13 +37,17 @@ export type ToolRegistryEntry = { endpoints?: string[]; link?: string; type?: string; + // URL path for routing (e.g., '/split-pdfs', '/compress-pdf') + urlPath?: string; + // Workbench type for navigation + workbench?: WorkbenchType; // Operation configuration for automation operationConfig?: ToolOperationConfig; // Settings component for automation configuration settingsComponent?: React.ComponentType; } -export type ToolRegistry = Record; +export type ToolRegistry = Record; export const SUBCATEGORY_ORDER: SubcategoryId[] = [ SubcategoryId.SIGNING, @@ -107,3 +111,30 @@ export const getAllApplicationEndpoints = ( const convEp = extensionToEndpoint ? getConversionEndpoints(extensionToEndpoint) : []; return Array.from(new Set([...toolEp, ...convEp])); }; + +/** + * Default workbench for tools that don't specify one + * Returns null to trigger the default case in Workbench component (ToolRenderer) + */ +export const getDefaultToolWorkbench = (): WorkbenchType => 'fileEditor'; + +/** + * Get workbench type for a tool + */ +export const getToolWorkbench = (tool: ToolRegistryEntry): WorkbenchType => { + return tool.workbench || getDefaultToolWorkbench(); +}; + +/** + * Get URL path for a tool + */ +export const getToolUrlPath = (toolId: string, tool: ToolRegistryEntry): string => { + return tool.urlPath || `/${toolId.replace(/([A-Z])/g, '-$1').toLowerCase()}`; +}; + +/** + * Check if a tool ID exists in the registry + */ +export const isValidToolId = (toolId: string, registry: ToolRegistry): boolean => { + return toolId in registry; +}; diff --git a/frontend/src/data/useTranslatedToolRegistry.tsx b/frontend/src/data/useTranslatedToolRegistry.tsx index 4e441f647..68883fe92 100644 --- a/frontend/src/data/useTranslatedToolRegistry.tsx +++ b/frontend/src/data/useTranslatedToolRegistry.tsx @@ -1,66 +1,128 @@ -import React, { useMemo } from 'react'; -import LocalIcon from '../components/shared/LocalIcon'; -import { useTranslation } from 'react-i18next'; +import React, { useMemo } from "react"; +import LocalIcon from "../components/shared/LocalIcon"; +import { useTranslation } from "react-i18next"; import SplitPdfPanel from "../tools/Split"; import CompressPdfPanel from "../tools/Compress"; -import OCRPanel from '../tools/OCR'; -import ConvertPanel from '../tools/Convert'; -import Sanitize from '../tools/Sanitize'; -import AddPassword from '../tools/AddPassword'; -import ChangePermissions from '../tools/ChangePermissions'; -import RemovePassword from '../tools/RemovePassword'; -import { SubcategoryId, ToolCategoryId, ToolRegistry } from './toolsTaxonomy'; -import AddWatermark from '../tools/AddWatermark'; -import Repair from '../tools/Repair'; -import SingleLargePage from '../tools/SingleLargePage'; -import UnlockPdfForms from '../tools/UnlockPdfForms'; -import RemoveCertificateSign from '../tools/RemoveCertificateSign'; -import { compressOperationConfig } from '../hooks/tools/compress/useCompressOperation'; -import { splitOperationConfig } from '../hooks/tools/split/useSplitOperation'; -import { addPasswordOperationConfig } from '../hooks/tools/addPassword/useAddPasswordOperation'; -import { removePasswordOperationConfig } from '../hooks/tools/removePassword/useRemovePasswordOperation'; -import { sanitizeOperationConfig } from '../hooks/tools/sanitize/useSanitizeOperation'; -import { repairOperationConfig } from '../hooks/tools/repair/useRepairOperation'; -import { addWatermarkOperationConfig } from '../hooks/tools/addWatermark/useAddWatermarkOperation'; -import { unlockPdfFormsOperationConfig } from '../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation'; -import { singleLargePageOperationConfig } from '../hooks/tools/singleLargePage/useSingleLargePageOperation'; -import { ocrOperationConfig } from '../hooks/tools/ocr/useOCROperation'; -import { convertOperationConfig } from '../hooks/tools/convert/useConvertOperation'; -import { removeCertificateSignOperationConfig } from '../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation'; -import { changePermissionsOperationConfig } from '../hooks/tools/changePermissions/useChangePermissionsOperation'; -import CompressSettings from '../components/tools/compress/CompressSettings'; -import SplitSettings from '../components/tools/split/SplitSettings'; -import AddPasswordSettings from '../components/tools/addPassword/AddPasswordSettings'; -import RemovePasswordSettings from '../components/tools/removePassword/RemovePasswordSettings'; -import SanitizeSettings from '../components/tools/sanitize/SanitizeSettings'; -import RepairSettings from '../components/tools/repair/RepairSettings'; -import UnlockPdfFormsSettings from '../components/tools/unlockPdfForms/UnlockPdfFormsSettings'; -import AddWatermarkSingleStepSettings from '../components/tools/addWatermark/AddWatermarkSingleStepSettings'; -import OCRSettings from '../components/tools/ocr/OCRSettings'; -import ConvertSettings from '../components/tools/convert/ConvertSettings'; -import ChangePermissionsSettings from '../components/tools/changePermissions/ChangePermissionsSettings'; +import OCRPanel from "../tools/OCR"; +import ConvertPanel from "../tools/Convert"; +import Sanitize from "../tools/Sanitize"; +import AddPassword from "../tools/AddPassword"; +import ChangePermissions from "../tools/ChangePermissions"; +import RemovePassword from "../tools/RemovePassword"; +import { SubcategoryId, ToolCategoryId, ToolRegistry } from "./toolsTaxonomy"; +import AddWatermark from "../tools/AddWatermark"; +import Repair from "../tools/Repair"; +import SingleLargePage from "../tools/SingleLargePage"; +import UnlockPdfForms from "../tools/UnlockPdfForms"; +import RemoveCertificateSign from "../tools/RemoveCertificateSign"; +import { compressOperationConfig } from "../hooks/tools/compress/useCompressOperation"; +import { splitOperationConfig } from "../hooks/tools/split/useSplitOperation"; +import { addPasswordOperationConfig } from "../hooks/tools/addPassword/useAddPasswordOperation"; +import { removePasswordOperationConfig } from "../hooks/tools/removePassword/useRemovePasswordOperation"; +import { sanitizeOperationConfig } from "../hooks/tools/sanitize/useSanitizeOperation"; +import { repairOperationConfig } from "../hooks/tools/repair/useRepairOperation"; +import { addWatermarkOperationConfig } from "../hooks/tools/addWatermark/useAddWatermarkOperation"; +import { unlockPdfFormsOperationConfig } from "../hooks/tools/unlockPdfForms/useUnlockPdfFormsOperation"; +import { singleLargePageOperationConfig } from "../hooks/tools/singleLargePage/useSingleLargePageOperation"; +import { ocrOperationConfig } from "../hooks/tools/ocr/useOCROperation"; +import { convertOperationConfig } from "../hooks/tools/convert/useConvertOperation"; +import { removeCertificateSignOperationConfig } from "../hooks/tools/removeCertificateSign/useRemoveCertificateSignOperation"; +import { changePermissionsOperationConfig } from "../hooks/tools/changePermissions/useChangePermissionsOperation"; +import CompressSettings from "../components/tools/compress/CompressSettings"; +import SplitSettings from "../components/tools/split/SplitSettings"; +import AddPasswordSettings from "../components/tools/addPassword/AddPasswordSettings"; +import RemovePasswordSettings from "../components/tools/removePassword/RemovePasswordSettings"; +import SanitizeSettings from "../components/tools/sanitize/SanitizeSettings"; +import RepairSettings from "../components/tools/repair/RepairSettings"; +import UnlockPdfFormsSettings from "../components/tools/unlockPdfForms/UnlockPdfFormsSettings"; +import AddWatermarkSingleStepSettings from "../components/tools/addWatermark/AddWatermarkSingleStepSettings"; +import OCRSettings from "../components/tools/ocr/OCRSettings"; +import ConvertSettings from "../components/tools/convert/ConvertSettings"; +import ChangePermissionsSettings from "../components/tools/changePermissions/ChangePermissionsSettings"; +import { ToolId } from "../types/toolId"; const showPlaceholderTools = true; // Show all tools; grey out unavailable ones in UI // Convert tool supported file formats export const CONVERT_SUPPORTED_FORMATS = [ - // Microsoft Office - "doc", "docx", "dot", "dotx", "csv", "xls", "xlsx", "xlt", "xltx", "slk", "dif", "ppt", "pptx", - // OpenDocument - "odt", "ott", "ods", "ots", "odp", "otp", "odg", "otg", - // Text formats - "txt", "text", "xml", "rtf", "html", "lwp", "md", - // Images - "bmp", "gif", "jpeg", "jpg", "png", "tif", "tiff", "pbm", "pgm", "ppm", "ras", "xbm", "xpm", "svg", "svm", "wmf", "webp", - // StarOffice - "sda", "sdc", "sdd", "sdw", "stc", "std", "sti", "stw", "sxd", "sxg", "sxi", "sxw", - // Email formats - "eml", - // Archive formats - "zip", - // Other - "dbf", "fods", "vsd", "vor", "vor3", "vor4", "uop", "pct", "ps", "pdf" - ]; + // Microsoft Office + "doc", + "docx", + "dot", + "dotx", + "csv", + "xls", + "xlsx", + "xlt", + "xltx", + "slk", + "dif", + "ppt", + "pptx", + // OpenDocument + "odt", + "ott", + "ods", + "ots", + "odp", + "otp", + "odg", + "otg", + // Text formats + "txt", + "text", + "xml", + "rtf", + "html", + "lwp", + "md", + // Images + "bmp", + "gif", + "jpeg", + "jpg", + "png", + "tif", + "tiff", + "pbm", + "pgm", + "ppm", + "ras", + "xbm", + "xpm", + "svg", + "svm", + "wmf", + "webp", + // StarOffice + "sda", + "sdc", + "sdd", + "sdw", + "stc", + "std", + "sti", + "stw", + "sxd", + "sxg", + "sxi", + "sxw", + // Email formats + "eml", + // Archive formats + "zip", + // Other + "dbf", + "fods", + "vsd", + "vor", + "vor3", + "vor4", + "uop", + "pct", + "ps", + "pdf", +]; // Hook to get the translated tool registry export function useFlatToolRegistry(): ToolRegistry { @@ -68,619 +130,577 @@ export function useFlatToolRegistry(): ToolRegistry { return useMemo(() => { const allTools: ToolRegistry = { - // Signing + // Signing - "certSign": { + certSign: { icon: , name: t("home.certSign.title", "Sign with Certificate"), component: null, - view: "sign", description: t("home.certSign.desc", "Signs a PDF with a Certificate/Key (PEM/P12)"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.SIGNING - }, - "sign": { + subcategoryId: SubcategoryId.SIGNING, + }, + sign: { icon: , name: t("home.sign.title", "Sign"), component: null, - view: "sign", description: t("home.sign.desc", "Adds signature to PDF by drawing, text or image"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.SIGNING - }, + subcategoryId: SubcategoryId.SIGNING, + }, + // Document Security - // Document Security - - "addPassword": { + addPassword: { icon: , name: t("home.addPassword.title", "Add Password"), component: AddPassword, - view: "security", description: t("home.addPassword.desc", "Add password protection and restrictions to PDF files"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["add-password"], operationConfig: addPasswordOperationConfig, - settingsComponent: AddPasswordSettings - }, - "watermark": { + settingsComponent: AddPasswordSettings, + }, + addWatermark: { icon: , name: t("home.watermark.title", "Add Watermark"), component: AddWatermark, - view: "format", maxFiles: -1, description: t("home.watermark.desc", "Add a custom watermark to your PDF document."), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.DOCUMENT_SECURITY, endpoints: ["add-watermark"], operationConfig: addWatermarkOperationConfig, - settingsComponent: AddWatermarkSingleStepSettings - }, - "add-stamp": { + settingsComponent: AddWatermarkSingleStepSettings, + }, + "add-stamp": { icon: , name: t("home.AddStampRequest.title", "Add Stamp to PDF"), component: null, - view: "format", description: t("home.AddStampRequest.desc", "Add text or add image stamps at set locations"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.DOCUMENT_SECURITY - }, - "sanitize": { + subcategoryId: SubcategoryId.DOCUMENT_SECURITY, + }, + sanitize: { icon: , name: t("home.sanitize.title", "Sanitize"), component: Sanitize, - view: "security", maxFiles: -1, categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.DOCUMENT_SECURITY, description: t("home.sanitize.desc", "Remove potentially harmful elements from PDF files"), endpoints: ["sanitize-pdf"], operationConfig: sanitizeOperationConfig, - settingsComponent: SanitizeSettings - }, - "flatten": { + settingsComponent: SanitizeSettings, + }, + flatten: { icon: , name: t("home.flatten.title", "Flatten"), component: null, - view: "format", description: t("home.flatten.desc", "Remove all interactive elements and forms from a PDF"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.DOCUMENT_SECURITY - }, - "unlock-pdf-forms": { + subcategoryId: SubcategoryId.DOCUMENT_SECURITY, + }, + "unlock-pdf-forms": { icon: , name: t("home.unlockPDFForms.title", "Unlock PDF Forms"), component: UnlockPdfForms, - view: "security", description: t("home.unlockPDFForms.desc", "Remove read-only property of form fields in a PDF document."), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["unlock-pdf-forms"], operationConfig: unlockPdfFormsOperationConfig, - settingsComponent: UnlockPdfFormsSettings - }, - "manage-certificates": { + settingsComponent: UnlockPdfFormsSettings, + }, + "manage-certificates": { icon: , name: t("home.manageCertificates.title", "Manage Certificates"), component: null, - view: "security", - description: t("home.manageCertificates.desc", "Import, export, or delete digital certificate files used for signing PDFs."), + description: t( + "home.manageCertificates.desc", + "Import, export, or delete digital certificate files used for signing PDFs." + ), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.DOCUMENT_SECURITY - }, - "change-permissions": { + subcategoryId: SubcategoryId.DOCUMENT_SECURITY, + }, + "change-permissions": { icon: , name: t("home.changePermissions.title", "Change Permissions"), component: ChangePermissions, - view: "security", description: t("home.changePermissions.desc", "Change document restrictions and permissions"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.DOCUMENT_SECURITY, maxFiles: -1, endpoints: ["add-password"], operationConfig: changePermissionsOperationConfig, - settingsComponent: ChangePermissionsSettings - }, - // Verification + settingsComponent: ChangePermissionsSettings, + }, + // Verification - "get-all-info-on-pdf": { + "get-all-info-on-pdf": { icon: , name: t("home.getPdfInfo.title", "Get ALL Info on PDF"), component: null, - view: "extract", description: t("home.getPdfInfo.desc", "Grabs any and all information possible on PDFs"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.VERIFICATION - }, - "validate-pdf-signature": { + subcategoryId: SubcategoryId.VERIFICATION, + }, + "validate-pdf-signature": { icon: , name: t("home.validateSignature.title", "Validate PDF Signature"), component: null, - view: "security", description: t("home.validateSignature.desc", "Verify digital signatures and certificates in PDF documents"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.VERIFICATION - }, + subcategoryId: SubcategoryId.VERIFICATION, + }, + // Document Review - // Document Review - - "read": { + read: { icon: , name: t("home.read.title", "Read"), component: null, - view: "view", - description: t("home.read.desc", "View and annotate PDFs. Highlight text, draw, or insert comments for review and collaboration."), + workbench: "viewer", + description: t( + "home.read.desc", + "View and annotate PDFs. Highlight text, draw, or insert comments for review and collaboration." + ), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.DOCUMENT_REVIEW - }, - "change-metadata": { + subcategoryId: SubcategoryId.DOCUMENT_REVIEW, + }, + "change-metadata": { icon: , name: t("home.changeMetadata.title", "Change Metadata"), component: null, - view: "format", description: t("home.changeMetadata.desc", "Change/Remove/Add metadata from a PDF document"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.DOCUMENT_REVIEW - }, - // Page Formatting + subcategoryId: SubcategoryId.DOCUMENT_REVIEW, + }, + // Page Formatting - "cropPdf": { + cropPdf: { icon: , name: t("home.crop.title", "Crop PDF"), component: null, - view: "format", description: t("home.crop.desc", "Crop a PDF to reduce its size (maintains text!)"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.PAGE_FORMATTING - }, - "rotate": { + subcategoryId: SubcategoryId.PAGE_FORMATTING, + }, + rotate: { icon: , name: t("home.rotate.title", "Rotate"), component: null, - view: "format", description: t("home.rotate.desc", "Easily rotate your PDFs."), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.PAGE_FORMATTING - }, - "splitPdf": { + subcategoryId: SubcategoryId.PAGE_FORMATTING, + }, + split: { icon: , name: t("home.split.title", "Split"), component: SplitPdfPanel, - view: "split", description: t("home.split.desc", "Split PDFs into multiple documents"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.PAGE_FORMATTING, operationConfig: splitOperationConfig, - settingsComponent: SplitSettings - }, - "reorganize-pages": { + settingsComponent: SplitSettings, + }, + "reorganize-pages": { icon: , name: t("home.reorganizePages.title", "Reorganize Pages"), component: null, - view: "pageEditor", - description: t("home.reorganizePages.desc", "Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control."), + workbench: "pageEditor", + description: t( + "home.reorganizePages.desc", + "Rearrange, duplicate, or delete PDF pages with visual drag-and-drop control." + ), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.PAGE_FORMATTING - }, - "adjust-page-size-scale": { + subcategoryId: SubcategoryId.PAGE_FORMATTING, + }, + "adjust-page-size-scale": { icon: , name: t("home.scalePages.title", "Adjust page size/scale"), component: null, - view: "format", + description: t("home.scalePages.desc", "Change the size/scale of a page and/or its contents."), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.PAGE_FORMATTING - }, - "addPageNumbers": { + subcategoryId: SubcategoryId.PAGE_FORMATTING, + }, + addPageNumbers: { icon: , name: t("home.addPageNumbers.title", "Add Page Numbers"), component: null, - view: "format", + description: t("home.addPageNumbers.desc", "Add Page numbers throughout a document in a set location"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.PAGE_FORMATTING - }, - "multi-page-layout": { + subcategoryId: SubcategoryId.PAGE_FORMATTING, + }, + "multi-page-layout": { icon: , name: t("home.pageLayout.title", "Multi-Page Layout"), component: null, - view: "format", + description: t("home.pageLayout.desc", "Merge multiple pages of a PDF document into a single page"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.PAGE_FORMATTING - }, - "single-large-page": { + subcategoryId: SubcategoryId.PAGE_FORMATTING, + }, + "single-large-page": { icon: , name: t("home.pdfToSinglePage.title", "PDF to Single Large Page"), component: SingleLargePage, - view: "format", + description: t("home.pdfToSinglePage.desc", "Merges all PDF pages into one large single page"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.PAGE_FORMATTING, maxFiles: -1, endpoints: ["pdf-to-single-page"], - operationConfig: singleLargePageOperationConfig - }, - "add-attachments": { + operationConfig: singleLargePageOperationConfig, + }, + "add-attachments": { icon: , name: t("home.attachments.title", "Add Attachments"), component: null, - view: "format", + description: t("home.attachments.desc", "Add or remove embedded files (attachments) to/from a PDF"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.PAGE_FORMATTING, - }, + }, + // Extraction - // Extraction - - "extractPages": { + "extract-page": { icon: , name: t("home.extractPages.title", "Extract Pages"), component: null, - view: "extract", description: t("home.extractPages.desc", "Extract specific pages from a PDF document"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.EXTRACTION - }, - "extract-images": { + subcategoryId: SubcategoryId.EXTRACTION, + }, + "extract-images": { icon: , name: t("home.extractImages.title", "Extract Images"), component: null, - view: "extract", description: t("home.extractImages.desc", "Extract images from PDF documents"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.EXTRACTION - }, + subcategoryId: SubcategoryId.EXTRACTION, + }, + // Removal - // Removal - - "removePages": { + removePages: { icon: , name: t("home.removePages.title", "Remove Pages"), component: null, - view: "remove", description: t("home.removePages.desc", "Remove specific pages from a PDF document"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.REMOVAL - }, - "remove-blank-pages": { + subcategoryId: SubcategoryId.REMOVAL, + }, + "remove-blank-pages": { icon: , name: t("home.removeBlanks.title", "Remove Blank Pages"), component: null, - view: "remove", description: t("home.removeBlanks.desc", "Remove blank pages from PDF documents"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.REMOVAL - }, - "remove-annotations": { + subcategoryId: SubcategoryId.REMOVAL, + }, + "remove-annotations": { icon: , name: t("home.removeAnnotations.title", "Remove Annotations"), component: null, - view: "remove", description: t("home.removeAnnotations.desc", "Remove annotations and comments from PDF documents"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.REMOVAL - }, - "remove-image": { + subcategoryId: SubcategoryId.REMOVAL, + }, + "remove-image": { icon: , name: t("home.removeImagePdf.title", "Remove Image"), component: null, - view: "format", description: t("home.removeImagePdf.desc", "Remove images from PDF documents"), categoryId: ToolCategoryId.STANDARD_TOOLS, - subcategoryId: SubcategoryId.REMOVAL - }, - "remove-password": { + subcategoryId: SubcategoryId.REMOVAL, + }, + "remove-password": { icon: , name: t("home.removePassword.title", "Remove Password"), component: RemovePassword, - view: "security", description: t("home.removePassword.desc", "Remove password protection from PDF documents"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.REMOVAL, endpoints: ["remove-password"], maxFiles: -1, operationConfig: removePasswordOperationConfig, - settingsComponent: RemovePasswordSettings - }, - "remove-certificate-sign": { + settingsComponent: RemovePasswordSettings, + }, + "remove-certificate-sign": { icon: , name: t("home.removeCertSign.title", "Remove Certificate Sign"), component: RemoveCertificateSign, - view: "security", description: t("home.removeCertSign.desc", "Remove digital signature from PDF documents"), categoryId: ToolCategoryId.STANDARD_TOOLS, subcategoryId: SubcategoryId.REMOVAL, maxFiles: -1, endpoints: ["remove-certificate-sign"], - operationConfig: removeCertificateSignOperationConfig - }, + operationConfig: removeCertificateSignOperationConfig, + }, + // Automation - // Automation - - "automate": { + automate: { icon: , name: t("home.automate.title", "Automate"), - component: React.lazy(() => import('../tools/Automate')), - view: "format", - description: t("home.automate.desc", "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks."), + component: React.lazy(() => import("../tools/Automate")), + description: t( + "home.automate.desc", + "Build multi-step workflows by chaining together PDF actions. Ideal for recurring tasks." + ), categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.AUTOMATION, maxFiles: -1, supportedFormats: CONVERT_SUPPORTED_FORMATS, - endpoints: ["handleData"] - }, - "auto-rename-pdf-file": { + endpoints: ["handleData"], + }, + "auto-rename-pdf-file": { icon: , name: t("home.auto-rename.title", "Auto Rename PDF File"), component: null, - view: "format", description: t("home.auto-rename.desc", "Automatically rename PDF files based on their content"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.AUTOMATION - }, - "auto-split-pages": { + subcategoryId: SubcategoryId.AUTOMATION, + }, + "auto-split-pages": { icon: , name: t("home.autoSplitPDF.title", "Auto Split Pages"), component: null, - view: "format", description: t("home.autoSplitPDF.desc", "Automatically split PDF pages based on content detection"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.AUTOMATION - }, - "auto-split-by-size-count": { + subcategoryId: SubcategoryId.AUTOMATION, + }, + "auto-split-by-size-count": { icon: , name: t("home.autoSizeSplitPDF.title", "Auto Split by Size/Count"), component: null, - view: "format", description: t("home.autoSizeSplitPDF.desc", "Automatically split PDFs by file size or page count"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.AUTOMATION - }, + subcategoryId: SubcategoryId.AUTOMATION, + }, + // Advanced Formatting - // Advanced Formatting - - "adjustContrast": { + "adjust-contrast": { icon: , name: t("home.adjustContrast.title", "Adjust Colors/Contrast"), component: null, - view: "format", description: t("home.adjustContrast.desc", "Adjust colors and contrast of PDF documents"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.ADVANCED_FORMATTING - }, - "repair": { + subcategoryId: SubcategoryId.ADVANCED_FORMATTING, + }, + repair: { icon: , name: t("home.repair.title", "Repair"), component: Repair, - view: "format", description: t("home.repair.desc", "Repair corrupted or damaged PDF files"), categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.ADVANCED_FORMATTING, maxFiles: -1, endpoints: ["repair"], operationConfig: repairOperationConfig, - settingsComponent: RepairSettings - }, - "detect-split-scanned-photos": { + settingsComponent: RepairSettings, + }, + "detect-split-scanned-photos": { icon: , name: t("home.ScannerImageSplit.title", "Detect & Split Scanned Photos"), component: null, - view: "format", description: t("home.ScannerImageSplit.desc", "Detect and split scanned photos into separate pages"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.ADVANCED_FORMATTING - }, - "overlay-pdfs": { + subcategoryId: SubcategoryId.ADVANCED_FORMATTING, + }, + "overlay-pdfs": { icon: , name: t("home.overlay-pdfs.title", "Overlay PDFs"), component: null, - view: "format", description: t("home.overlay-pdfs.desc", "Overlay one PDF on top of another"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.ADVANCED_FORMATTING - }, - "replace-and-invert-color": { + subcategoryId: SubcategoryId.ADVANCED_FORMATTING, + }, + "replace-and-invert-color": { icon: , name: t("home.replaceColorPdf.title", "Replace & Invert Color"), component: null, - view: "format", description: t("home.replaceColorPdf.desc", "Replace or invert colors in PDF documents"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.ADVANCED_FORMATTING - }, - "add-image": { + subcategoryId: SubcategoryId.ADVANCED_FORMATTING, + }, + "add-image": { icon: , name: t("home.addImage.title", "Add Image"), component: null, - view: "format", description: t("home.addImage.desc", "Add images to PDF documents"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.ADVANCED_FORMATTING - }, - "edit-table-of-contents": { + subcategoryId: SubcategoryId.ADVANCED_FORMATTING, + }, + "edit-table-of-contents": { icon: , name: t("home.editTableOfContents.title", "Edit Table of Contents"), component: null, - view: "format", description: t("home.editTableOfContents.desc", "Add or edit bookmarks and table of contents in PDF documents"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.ADVANCED_FORMATTING - }, - "scanner-effect": { + subcategoryId: SubcategoryId.ADVANCED_FORMATTING, + }, + "scanner-effect": { icon: , name: t("home.fakeScan.title", "Scanner Effect"), component: null, - view: "format", description: t("home.fakeScan.desc", "Create a PDF that looks like it was scanned"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.ADVANCED_FORMATTING - }, + subcategoryId: SubcategoryId.ADVANCED_FORMATTING, + }, + // Developer Tools - // Developer Tools - - "show-javascript": { + "show-javascript": { icon: , name: t("home.showJS.title", "Show JavaScript"), component: null, - view: "extract", description: t("home.showJS.desc", "Extract and display JavaScript code from PDF documents"), categoryId: ToolCategoryId.ADVANCED_TOOLS, - subcategoryId: SubcategoryId.DEVELOPER_TOOLS - }, - "dev-api": { - icon: , + subcategoryId: SubcategoryId.DEVELOPER_TOOLS, + }, + "dev-api": { + icon: , name: t("home.devApi.title", "API"), component: null, - view: "external", description: t("home.devApi.desc", "Link to API documentation"), categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS, - link: "https://stirlingpdf.io/swagger-ui/5.21.0/index.html" - }, - "dev-folder-scanning": { - icon: , + link: "https://stirlingpdf.io/swagger-ui/5.21.0/index.html", + }, + "dev-folder-scanning": { + icon: , name: t("home.devFolderScanning.title", "Automated Folder Scanning"), component: null, - view: "external", description: t("home.devFolderScanning.desc", "Link to automated folder scanning guide"), categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS, - link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/" - }, - "dev-sso-guide": { - icon: , + link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Folder%20Scanning/", + }, + "dev-sso-guide": { + icon: , name: t("home.devSsoGuide.title", "SSO Guide"), component: null, - view: "external", description: t("home.devSsoGuide.desc", "Link to SSO guide"), categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS, link: "https://docs.stirlingpdf.com/Advanced%20Configuration/Single%20Sign-On%20Configuration", - }, - "dev-airgapped": { - icon: , + }, + "dev-airgapped": { + icon: , name: t("home.devAirgapped.title", "Air-gapped Setup"), component: null, - view: "external", description: t("home.devAirgapped.desc", "Link to air-gapped setup guide"), categoryId: ToolCategoryId.ADVANCED_TOOLS, subcategoryId: SubcategoryId.DEVELOPER_TOOLS, - link: "https://docs.stirlingpdf.com/Pro/#activation" - }, + link: "https://docs.stirlingpdf.com/Pro/#activation", + }, - - // Recommended Tools - "compare": { + // Recommended Tools + compare: { icon: , name: t("home.compare.title", "Compare"), component: null, - view: "format", description: t("home.compare.desc", "Compare two PDF documents and highlight differences"), categoryId: ToolCategoryId.RECOMMENDED_TOOLS, - subcategoryId: SubcategoryId.GENERAL - }, - "compress": { + subcategoryId: SubcategoryId.GENERAL, + }, + compress: { icon: , name: t("home.compress.title", "Compress"), component: CompressPdfPanel, - view: "compress", description: t("home.compress.desc", "Compress PDFs to reduce their file size."), categoryId: ToolCategoryId.RECOMMENDED_TOOLS, subcategoryId: SubcategoryId.GENERAL, maxFiles: -1, operationConfig: compressOperationConfig, - settingsComponent: CompressSettings - }, - "convert": { + settingsComponent: CompressSettings, + }, + convert: { icon: , name: t("home.convert.title", "Convert"), component: ConvertPanel, - view: "convert", description: t("home.convert.desc", "Convert files to and from PDF format"), categoryId: ToolCategoryId.RECOMMENDED_TOOLS, subcategoryId: SubcategoryId.GENERAL, maxFiles: -1, supportedFormats: CONVERT_SUPPORTED_FORMATS, endpoints: [ - "pdf-to-img", - "img-to-pdf", - "pdf-to-word", - "pdf-to-presentation", - "pdf-to-text", - "pdf-to-html", - "pdf-to-xml", - "html-to-pdf", - "markdown-to-pdf", - "file-to-pdf", - "pdf-to-csv", - "pdf-to-markdown", - "pdf-to-pdfa", - "eml-to-pdf" + "pdf-to-img", + "img-to-pdf", + "pdf-to-word", + "pdf-to-presentation", + "pdf-to-text", + "pdf-to-html", + "pdf-to-xml", + "html-to-pdf", + "markdown-to-pdf", + "file-to-pdf", + "pdf-to-csv", + "pdf-to-markdown", + "pdf-to-pdfa", + "eml-to-pdf", ], operationConfig: convertOperationConfig, - settingsComponent: ConvertSettings - }, - "mergePdfs": { + settingsComponent: ConvertSettings, + }, + mergePdfs: { icon: , name: t("home.merge.title", "Merge"), component: null, - view: "merge", + description: t("home.merge.desc", "Merge multiple PDFs into a single document"), categoryId: ToolCategoryId.RECOMMENDED_TOOLS, subcategoryId: SubcategoryId.GENERAL, - maxFiles: -1 - }, - "multi-tool": { + maxFiles: -1, + }, + "multi-tool": { icon: , name: t("home.multiTool.title", "Multi-Tool"), component: null, - view: "pageEditor", + workbench: "pageEditor", description: t("home.multiTool.desc", "Use multiple tools on a single PDF document"), categoryId: ToolCategoryId.RECOMMENDED_TOOLS, subcategoryId: SubcategoryId.GENERAL, - maxFiles: -1 - }, - "ocr": { + maxFiles: -1, + }, + ocr: { icon: , name: t("home.ocr.title", "OCR"), component: OCRPanel, - view: "convert", description: t("home.ocr.desc", "Extract text from scanned PDFs using Optical Character Recognition"), categoryId: ToolCategoryId.RECOMMENDED_TOOLS, subcategoryId: SubcategoryId.GENERAL, maxFiles: -1, operationConfig: ocrOperationConfig, - settingsComponent: OCRSettings - }, - "redact": { + settingsComponent: OCRSettings, + }, + redact: { icon: , name: t("home.redact.title", "Redact"), component: null, - view: "redact", description: t("home.redact.desc", "Permanently remove sensitive information from PDF documents"), categoryId: ToolCategoryId.RECOMMENDED_TOOLS, - subcategoryId: SubcategoryId.GENERAL - }, - }; + subcategoryId: SubcategoryId.GENERAL, + }, + }; if (showPlaceholderTools) { return allTools; } const filteredTools = Object.keys(allTools) - .filter(key => allTools[key].component !== null || allTools[key].link) + .filter((key) => allTools[key as ToolId].component !== null || allTools[key as ToolId].link) .reduce((obj, key) => { - obj[key] = allTools[key]; + obj[key as ToolId] = allTools[key as ToolId]; return obj; }, {} as ToolRegistry); return filteredTools; diff --git a/frontend/src/hooks/tools/automate/useAutomationForm.ts b/frontend/src/hooks/tools/automate/useAutomationForm.ts index deb5fbd2b..6ad8afe81 100644 --- a/frontend/src/hooks/tools/automate/useAutomationForm.ts +++ b/frontend/src/hooks/tools/automate/useAutomationForm.ts @@ -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; + toolRegistry: ToolRegistry; } export function useAutomationForm({ mode, existingAutomation, toolRegistry }: UseAutomationFormProps) { const { t } = useTranslation(); - + const [automationName, setAutomationName] = useState(''); const [automationDescription, setAutomationDescription] = useState(''); const [automationIcon, setAutomationIcon] = useState(''); const [selectedTools, setSelectedTools] = useState([]); 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 => { - 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 }; -} \ No newline at end of file +} diff --git a/frontend/src/hooks/useSuggestedTools.ts b/frontend/src/hooks/useSuggestedTools.ts index f3f04ae34..8478bbc6b 100644 --- a/frontend/src/hooks/useSuggestedTools.ts +++ b/frontend/src/hooks/useSuggestedTools.ts @@ -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; navigate: () => void; @@ -32,7 +33,7 @@ const ALL_SUGGESTED_TOOLS: Omit[] = [ icon: CleaningServicesIcon }, { - id: 'splitPdf', + id: 'split', title: 'Split', icon: CropIcon }, @@ -45,16 +46,16 @@ const ALL_SUGGESTED_TOOLS: Omit[] = [ 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]); } diff --git a/frontend/src/hooks/useToolManagement.tsx b/frontend/src/hooks/useToolManagement.tsx index 37d29278a..f73f458db 100644 --- a/frontend/src/hooks/useToolManagement.tsx +++ b/frontend/src/hooks/useToolManagement.tsx @@ -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]); diff --git a/frontend/src/hooks/useToolUrlRouting.ts b/frontend/src/hooks/useToolUrlRouting.ts deleted file mode 100644 index 57e61d9e0..000000000 --- a/frontend/src/hooks/useToolUrlRouting.ts +++ /dev/null @@ -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 | 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([ - ['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 }; -} diff --git a/frontend/src/hooks/useUrlSync.ts b/frontend/src/hooks/useUrlSync.ts index 4a50a36ba..e65e5500f 100644 --- a/frontend/src/hooks/useUrlSync.ts +++ b/frontend/src/hooks/useUrlSync.ts @@ -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(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 }; -} \ No newline at end of file +} + +/** + * Hook to get current URL route information with registry support + */ +export function useCurrentRoute(registry: ToolRegistry) { + const getCurrentRoute = useCallback(() => { + return parseToolRoute(registry); + }, [registry]); + + return getCurrentRoute; +} diff --git a/frontend/src/index.tsx b/frontend/src/index.tsx index 740eec3dc..9443f0be6 100644 --- a/frontend/src/index.tsx +++ b/frontend/src/index.tsx @@ -1,4 +1,5 @@ import '@mantine/core/styles.css'; +import '../vite-env.d.ts'; import './index.css'; // Import Tailwind CSS import React from 'react'; import ReactDOM from 'react-dom/client'; @@ -6,6 +7,7 @@ import { ColorSchemeScript } from '@mantine/core'; import { BrowserRouter } from 'react-router-dom'; import App from './App'; import './i18n'; // Initialize i18next +import { PostHogProvider } from 'posthog-js/react'; // Compute initial color scheme function getInitialScheme(): 'light' | 'dark' { @@ -27,9 +29,18 @@ const root = ReactDOM.createRoot(container); // Finds the root DOM element root.render( - - - + + + + + ); - diff --git a/frontend/src/tools/Automate.tsx b/frontend/src/tools/Automate.tsx index d5cea69c8..15e5ea3fa 100644 --- a/frontend/src/tools/Automate.tsx +++ b/frontend/src/tools/Automate.tsx @@ -2,7 +2,7 @@ import React, { useState, useMemo, useEffect } from "react"; import { useTranslation } from "react-i18next"; import { useFileContext } from "../contexts/FileContext"; import { useFileSelection } from "../contexts/FileContext"; -import { useNavigation } from "../contexts/NavigationContext"; +import { useNavigationActions } from "../contexts/NavigationContext"; import { useToolWorkflow } from "../contexts/ToolWorkflowContext"; import { createToolFlow } from "../components/tools/shared/createToolFlow"; @@ -21,7 +21,7 @@ import { AUTOMATION_STEPS } from "../constants/automation"; const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const { t } = useTranslation(); const { selectedFiles } = useFileSelection(); - const { setMode } = useNavigation(); + const { actions } = useNavigationActions(); const { registerToolReset } = useToolWorkflow(); const [currentStep, setCurrentStep] = useState(AUTOMATION_STEPS.SELECTION); @@ -161,7 +161,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { const filesPlaceholder = useMemo(() => { if (currentStep === AUTOMATION_STEPS.RUN && stepData.automation?.operations?.length) { const firstOperation = stepData.automation.operations[0]; - const toolConfig = toolRegistry[firstOperation.operation]; + const toolConfig = toolRegistry[firstOperation.operation as keyof typeof toolRegistry]; // Check if the tool has supportedFormats that include non-PDF formats if (toolConfig?.supportedFormats && toolConfig.supportedFormats.length > 1) { @@ -223,7 +223,7 @@ const Automate = ({ onPreviewFile, onComplete, onError }: BaseToolProps) => { title: t('automate.reviewTitle', 'Automation Results'), onFileClick: (file: File) => { onPreviewFile?.(file); - setMode('viewer'); + actions.setWorkbench('viewer'); } } }); diff --git a/frontend/src/types/navigation.ts b/frontend/src/types/navigation.ts index 70d108c9a..b66b3276d 100644 --- a/frontend/src/types/navigation.ts +++ b/frontend/src/types/navigation.ts @@ -1,42 +1,19 @@ /** - * Shared navigation types to avoid circular dependencies + * Navigation types for workbench and tool separation */ -// Navigation mode types - complete list to match contexts -export type ModeType = - | 'viewer' - | 'pageEditor' - | 'fileEditor' - | 'merge' - | 'split' - | 'compress' - | 'ocr' - | 'convert' - | 'sanitize' - | 'addPassword' - | 'changePermissions' - | 'addWatermark' - | 'removePassword' - | 'single-large-page' - | 'repair' - | 'unlockPdfForms' - | 'removeCertificateSign'; +import { WorkbenchType } from './workbench'; +import { ToolId } from './toolId'; -// Utility functions for mode handling -export const isValidMode = (mode: string): mode is ModeType => { - const validModes: ModeType[] = [ - 'viewer', 'pageEditor', 'fileEditor', 'merge', 'split', - 'compress', 'ocr', 'convert', 'addPassword', 'changePermissions', - 'sanitize', 'addWatermark', 'removePassword', 'single-large-page', - 'repair', 'unlockPdfForms', 'removeCertificateSign' - ]; - return validModes.includes(mode as ModeType); -}; +// Navigation state +export interface NavigationState { + workbench: WorkbenchType; + selectedTool: ToolId | null; +} -export const getDefaultMode = (): ModeType => 'fileEditor'; // Route parsing result export interface ToolRoute { - mode: ModeType; - toolKey: string | null; -} \ No newline at end of file + workbench: WorkbenchType; + toolId: ToolId | null; +} diff --git a/frontend/src/types/navigationActions.ts b/frontend/src/types/navigationActions.ts index b227dac9d..d200ffdbc 100644 --- a/frontend/src/types/navigationActions.ts +++ b/frontend/src/types/navigationActions.ts @@ -2,10 +2,12 @@ * Navigation action interfaces to break circular dependencies */ -import { ModeType } from './navigation'; +import { WorkbenchType } from './workbench'; +import { ToolId } from './toolId'; export interface NavigationActions { - setMode: (mode: ModeType) => void; + setWorkbench: (workbench: WorkbenchType) => void; + setSelectedTool: (toolId: ToolId | null) => void; setHasUnsavedChanges: (hasChanges: boolean) => void; showNavigationWarning: (show: boolean) => void; requestNavigation: (navigationFn: () => void) => void; @@ -14,7 +16,8 @@ export interface NavigationActions { } export interface NavigationState { - currentMode: ModeType; + workbench: WorkbenchType; + selectedTool: ToolId | null; hasUnsavedChanges: boolean; pendingNavigation: (() => void) | null; showNavigationWarning: boolean; diff --git a/frontend/src/types/toolId.ts b/frontend/src/types/toolId.ts new file mode 100644 index 000000000..be38bdf37 --- /dev/null +++ b/frontend/src/types/toolId.ts @@ -0,0 +1,25 @@ +// Define all possible tool IDs as source of truth +const TOOL_IDS = [ + 'certSign', 'sign', 'addPassword', 'remove-password', 'removePages', 'remove-blank-pages', 'remove-annotations', 'remove-image', + 'change-permissions', 'addWatermark', + 'sanitize', 'auto-split-pages', 'auto-split-by-size-count', 'split', 'mergePdfs', + 'convert', 'ocr', 'add-image', 'rotate', + 'detect-split-scanned-photos', + 'edit-table-of-contents', + 'scanner-effect', + 'auto-rename-pdf-file', 'multi-page-layout', 'adjust-page-size-scale', 'adjust-contrast', 'cropPdf', 'single-large-page', 'multi-tool', + 'repair', 'compare', 'addPageNumbers', 'redact', + 'flatten', 'remove-certificate-sign', + 'unlock-pdf-forms', 'compress', 'extract-page', 'reorganize-pages', 'extract-images', + 'add-stamp', 'add-attachments', 'change-metadata', 'overlay-pdfs', + 'manage-certificates', 'get-all-info-on-pdf', 'validate-pdf-signature', 'read', 'automate', 'replace-and-invert-color', + 'show-javascript', 'dev-api', 'dev-folder-scanning', 'dev-sso-guide', 'dev-airgapped' +] as const; + +// Tool identity - what PDF operation we're performing (type-safe) +export type ToolId = typeof TOOL_IDS[number]; + +// Type guard using the same source of truth +export const isValidToolId = (value: string): value is ToolId => { + return TOOL_IDS.includes(value as ToolId); +}; diff --git a/frontend/src/types/workbench.ts b/frontend/src/types/workbench.ts new file mode 100644 index 000000000..8943638f2 --- /dev/null +++ b/frontend/src/types/workbench.ts @@ -0,0 +1,12 @@ +// Define workbench values once as source of truth +const WORKBENCH_TYPES = ['viewer', 'pageEditor', 'fileEditor'] as const; + +// Workbench types - how the user interacts with content +export type WorkbenchType = typeof WORKBENCH_TYPES[number]; + +export const getDefaultWorkbench = (): WorkbenchType => 'fileEditor'; + +// Type guard using the same source of truth +export const isValidWorkbench = (value: string): value is WorkbenchType => { + return WORKBENCH_TYPES.includes(value as WorkbenchType); +}; \ No newline at end of file diff --git a/frontend/src/utils/automationExecutor.ts b/frontend/src/utils/automationExecutor.ts index 0eb052e70..124f065a7 100644 --- a/frontend/src/utils/automationExecutor.ts +++ b/frontend/src/utils/automationExecutor.ts @@ -31,7 +31,7 @@ export const executeToolOperationWithPrefix = async ( ): Promise => { console.log(`šŸ”§ Executing tool: ${operationName}`, { parameters, fileCount: files.length }); - const config = toolRegistry[operationName]?.operationConfig; + const config = toolRegistry[operationName as keyof ToolRegistry]?.operationConfig; if (!config) { console.error(`āŒ Tool operation not supported: ${operationName}`); throw new Error(`Tool operation not supported: ${operationName}`); diff --git a/frontend/src/utils/scarfTracking.ts b/frontend/src/utils/scarfTracking.ts new file mode 100644 index 000000000..27c16b238 --- /dev/null +++ b/frontend/src/utils/scarfTracking.ts @@ -0,0 +1,28 @@ +let lastFiredPathname: string | null = null; +let lastFiredTime = 0; + +/** + * Fire scarf pixel for analytics tracking + * Only fires if pathname is different from last call or enough time has passed + */ +export function firePixel(pathname: string): void { + const now = Date.now(); + + // Only fire if pathname changed or it's been at least 1 second since last fire + if (pathname === lastFiredPathname && now - lastFiredTime < 250) { + return; + } + + lastFiredPathname = pathname; + lastFiredTime = now; + + const url = 'https://static.scarf.sh/a.png?x-pxid=3c1d68de-8945-4e9f-873f-65320b6fabf7' + + '&path=' + encodeURIComponent(pathname) + + const img = new Image(); + img.referrerPolicy = "no-referrer-when-downgrade"; + img.src = url; + + console.log("ScarfPixel: Fire to... " + pathname); +} + diff --git a/frontend/src/utils/urlMapping.ts b/frontend/src/utils/urlMapping.ts new file mode 100644 index 000000000..909924ca6 --- /dev/null +++ b/frontend/src/utils/urlMapping.ts @@ -0,0 +1,33 @@ +import { ToolId } from '../types/toolId'; + +// Map URL paths to tool keys (multiple URLs can map to same tool) +export const URL_TO_TOOL_MAP: Record = { + '/split-pdfs': 'split', + '/split': 'split', + '/merge-pdfs': 'mergePdfs', + '/compress-pdf': 'compress', + '/convert': 'convert', + '/convert-pdf': 'convert', + '/file-to-pdf': 'convert', + '/eml-to-pdf': 'convert', + '/html-to-pdf': 'convert', + '/markdown-to-pdf': 'convert', + '/pdf-to-csv': 'convert', + '/pdf-to-img': 'convert', + '/pdf-to-markdown': 'convert', + '/pdf-to-pdfa': 'convert', + '/pdf-to-word': 'convert', + '/pdf-to-xml': 'convert', + '/add-password': 'addPassword', + '/change-permissions': 'change-permissions', + '/sanitize-pdf': 'sanitize', + '/ocr': 'ocr', + '/ocr-pdf': 'ocr', + '/add-watermark': 'addWatermark', + '/remove-password': 'remove-password', + '/single-large-page': 'single-large-page', + '/repair': 'repair', + '/unlock-pdf-forms': 'unlock-pdf-forms', + '/remove-certificate-sign': 'remove-certificate-sign', + '/remove-cert-sign': 'remove-certificate-sign' +}; diff --git a/frontend/src/utils/urlRouting.ts b/frontend/src/utils/urlRouting.ts index 05493423a..3ca35e9a7 100644 --- a/frontend/src/utils/urlRouting.ts +++ b/frontend/src/utils/urlRouting.ts @@ -1,167 +1,127 @@ /** - * URL routing utilities for tool navigation - * Provides clean URL routing for the V2 tool system + * URL routing utilities for tool navigation with registry support */ -import { ModeType, isValidMode as isValidModeType, getDefaultMode, ToolRoute } from '../types/navigation'; +import { ToolRoute } from '../types/navigation'; +import { ToolId, isValidToolId } from '../types/toolId'; +import { getDefaultWorkbench } from '../types/workbench'; +import { ToolRegistry, getToolWorkbench, getToolUrlPath } from '../data/toolsTaxonomy'; +import { firePixel } from './scarfTracking'; +import { URL_TO_TOOL_MAP } from './urlMapping'; /** * Parse the current URL to extract tool routing information */ -export function parseToolRoute(): ToolRoute { +export function parseToolRoute(registry: ToolRegistry): ToolRoute { const path = window.location.pathname; const searchParams = new URLSearchParams(window.location.search); - - // Extract tool from URL path (e.g., /split-pdf -> split) - const toolMatch = path.match(/\/([a-zA-Z-]+)(?:-pdf)?$/); - if (toolMatch) { - const toolKey = toolMatch[1].toLowerCase(); - - // Map URL paths to tool keys and modes (excluding internal UI modes) - const toolMappings: Record = { - 'split': { mode: 'split', toolKey: 'split' }, - 'merge': { mode: 'merge', toolKey: 'merge' }, - 'compress': { mode: 'compress', toolKey: 'compress' }, - 'convert': { mode: 'convert', toolKey: 'convert' }, - 'add-password': { mode: 'addPassword', toolKey: 'addPassword' }, - 'change-permissions': { mode: 'changePermissions', toolKey: 'changePermissions' }, - 'sanitize': { mode: 'sanitize', toolKey: 'sanitize' }, - 'ocr': { mode: 'ocr', toolKey: 'ocr' } + + // First, check URL mapping for multiple URL aliases + const mappedToolId = URL_TO_TOOL_MAP[path]; + if (mappedToolId && registry[mappedToolId]) { + const tool = registry[mappedToolId]; + return { + workbench: getToolWorkbench(tool), + toolId: mappedToolId }; - - const mapping = toolMappings[toolKey]; - if (mapping) { + } + + // Fallback: Try to find tool by primary URL path in registry + for (const [toolId, tool] of Object.entries(registry)) { + const toolUrlPath = getToolUrlPath(toolId, tool); + if (path === toolUrlPath && isValidToolId(toolId)) { return { - mode: mapping.mode, - toolKey: mapping.toolKey + workbench: getToolWorkbench(tool), + toolId }; } } - + // Check for query parameter fallback (e.g., ?tool=split) const toolParam = searchParams.get('tool'); - if (toolParam && isValidModeType(toolParam)) { + if (toolParam && isValidToolId(toolParam) && registry[toolParam]) { + const tool = registry[toolParam]; return { - mode: toolParam as ModeType, - toolKey: toolParam + workbench: getToolWorkbench(tool), + toolId: toolParam }; } - - // Default to page editor for home page + + // Default to fileEditor workbench for home page return { - mode: getDefaultMode(), - toolKey: null + workbench: getDefaultWorkbench(), + toolId: null }; } /** - * Update the URL to reflect the current tool selection - * Internal UI modes (viewer, fileEditor, pageEditor) don't get URLs + * Update URL and fire analytics pixel */ -export function updateToolRoute(mode: ModeType, toolKey?: string): void { +function updateUrl(newPath: string, searchParams: URLSearchParams, replace: boolean = false): void { const currentPath = window.location.pathname; - const searchParams = new URLSearchParams(window.location.search); - - // Don't create URLs for internal UI modes - if (mode === 'viewer' || mode === 'fileEditor' || mode === 'pageEditor') { - // If we're switching to an internal mode, clear any existing tool URL - if (currentPath !== '/') { - clearToolRoute(); - } - return; - } - - let newPath = '/'; - - // Map modes to URL paths (only for actual tools) - if (toolKey) { - const pathMappings: Record = { - 'split': '/split-pdf', - 'merge': '/merge-pdf', - 'compress': '/compress-pdf', - 'convert': '/convert-pdf', - 'addPassword': '/add-password-pdf', - 'changePermissions': '/change-permissions-pdf', - 'sanitize': '/sanitize-pdf', - 'ocr': '/ocr-pdf' - }; - - newPath = pathMappings[toolKey] || `/${toolKey}`; - } - - // Remove tool query parameter since we're using path-based routing - searchParams.delete('tool'); - - // Construct final URL const queryString = searchParams.toString(); const fullUrl = newPath + (queryString ? `?${queryString}` : ''); - - // Update URL without triggering page reload + + // Only update URL and fire pixel if something actually changed if (currentPath !== newPath || window.location.search !== (queryString ? `?${queryString}` : '')) { - window.history.replaceState(null, '', fullUrl); + if (replace) { + window.history.replaceState(null, '', fullUrl); + } else { + window.history.pushState(null, '', fullUrl); + } + firePixel(newPath); } } +/** + * Update the URL to reflect the current tool selection + */ +export function updateToolRoute(toolId: ToolId, registry: ToolRegistry, replace: boolean = false): void { + const tool = registry[toolId]; + if (!tool) { + console.warn(`Tool ${toolId} not found in registry`); + return; + } + + const newPath = getToolUrlPath(toolId, tool); + const searchParams = new URLSearchParams(window.location.search); + + // Remove tool query parameter since we're using path-based routing + searchParams.delete('tool'); + + updateUrl(newPath, searchParams, replace); +} + /** * Clear tool routing and return to home page */ -export function clearToolRoute(): void { +export function clearToolRoute(replace: boolean = false): void { const searchParams = new URLSearchParams(window.location.search); searchParams.delete('tool'); - - const queryString = searchParams.toString(); - const url = '/' + (queryString ? `?${queryString}` : ''); - - window.history.replaceState(null, '', url); + + updateUrl('/', searchParams, replace); } /** - * Get clean tool name for display purposes + * Get clean tool name for display purposes using registry */ -export function getToolDisplayName(toolKey: string): string { - const displayNames: Record = { - 'split': 'Split PDF', - 'merge': 'Merge PDF', - 'compress': 'Compress PDF', - 'convert': 'Convert PDF', - 'addPassword': 'Add Password', - 'changePermissions': 'Change Permissions', - 'sanitize': 'Sanitize PDF', - 'ocr': 'OCR PDF' - }; - - return displayNames[toolKey] || toolKey; +export function getToolDisplayName(toolId: ToolId, registry: ToolRegistry): string { + const tool = registry[toolId]; + return tool ? tool.name : toolId; } -// Note: isValidMode is now imported from types/navigation.ts - /** - * Generate shareable URL for current tool state - * Only generates URLs for actual tools, not internal UI modes + * Generate shareable URL for current tool state using registry */ -export function generateShareableUrl(mode: ModeType, toolKey?: string): string { +export function generateShareableUrl(toolId: ToolId | null, registry: ToolRegistry): string { const baseUrl = window.location.origin; - - // Don't generate URLs for internal UI modes - if (mode === 'viewer' || mode === 'fileEditor' || mode === 'pageEditor') { + + if (!toolId || !registry[toolId]) { return baseUrl; } - - if (toolKey) { - const pathMappings: Record = { - 'split': '/split-pdf', - 'merge': '/merge-pdf', - 'compress': '/compress-pdf', - 'convert': '/convert-pdf', - 'addPassword': '/add-password-pdf', - 'changePermissions': '/change-permissions-pdf', - 'sanitize': '/sanitize-pdf', - 'ocr': '/ocr-pdf' - }; - - const path = pathMappings[toolKey] || `/${toolKey}`; - return `${baseUrl}${path}`; - } - - return baseUrl; -} \ No newline at end of file + + const tool = registry[toolId]; + + const path = getToolUrlPath(toolId, tool); + return `${baseUrl}${path}`; +} diff --git a/frontend/vite-env.d.ts b/frontend/vite-env.d.ts new file mode 100644 index 000000000..ca36e9027 --- /dev/null +++ b/frontend/vite-env.d.ts @@ -0,0 +1,10 @@ +/// + +interface ImportMetaEnv { + readonly VITE_PUBLIC_POSTHOG_KEY: string; + readonly VITE_PUBLIC_POSTHOG_HOST: string; +} + +interface ImportMeta { + readonly env: ImportMetaEnv; +}