mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2025-09-18 17:39:24 +00:00

# Description of Changes This pull request introduces the new "Auto Rename PDF" tool to the frontend, enabling users to automatically rename PDF files based on their content. The implementation includes UI components, parameter handling, operation logic, localization, and enhancements to the file response utilities to support backend-provided filenames. Below are the most important changes grouped by theme: **Feature: Auto Rename PDF Tool** - Added the main `AutoRename` tool component (`AutoRename.tsx`) and registered it in the tool registry, enabling selection and execution of the auto-rename operation in the UI. [[1]](diffhunk://#diff-3647ca39d46d109d122d4cd6cbfe981beb4189d05b1b446e5c46824eb98a4a88R1-R80) [[2]](diffhunk://#diff-0a3e636736c137356dd9354ff3cacbd302ebda40147545e13c62d073525d1969R17) [[3]](diffhunk://#diff-0a3e636736c137356dd9354ff3cacbd302ebda40147545e13c62d073525d1969L359-R366) [[4]](diffhunk://#diff-29427b8d06a23772c56645fc4b72af2980c813605abc162e3d47c2e39d026d06L25-R26) - Implemented the settings panel (`AutoRenameSettings.tsx`) and parameter management hook (`useAutoRenameParameters.ts`), allowing users to configure options such as using the first text as a fallback for the filename. [[1]](diffhunk://#diff-b2f9474c8e5a7a42df00a12ffd2d31a785895fe1096e8ca515e6af5633a4d648R1-R27) [[2]](diffhunk://#diff-8798a1ef451233bf3a1bf8825c12c5b434ad1a17a1beb1ca21fd972fdaceb50cR1-R19) - Created the operation hook (`useAutoRenameOperation.ts`) to handle API requests, error handling, and result processing for the auto-rename feature. **Localization** - Added English (US and GB) translations for the new tool, including UI labels, descriptions, error messages, and settings. [[1]](diffhunk://#diff-e4d543afa388d9eb8a423e45dfebb91641e3558d00848d70b285ebb91c40b249R1048-R1066) [[2]](diffhunk://#diff-14c707e28788a3a84ed5293ff6689be73d4bca00e155beaf090f9b37c978babbR1321-R1339) **File Response Handling Enhancements** - Updated the file response processor and related hooks to support preserving backend-provided filenames via the `Content-Disposition` header, ensuring files are renamed according to backend results. [[1]](diffhunk://#diff-97ea1c842d4b269c566a3085d8555ded7f9b462d9ce8dc73706bec79fe3973e0R11) [[2]](diffhunk://#diff-97ea1c842d4b269c566a3085d8555ded7f9b462d9ce8dc73706bec79fe3973e0L49-R51) [[3]](diffhunk://#diff-d44da7f96721d9829f3c20bf9c7ac5b9e156b647d2c75d76e861c8c09abc5191R52-R58) [[4]](diffhunk://#diff-d44da7f96721d9829f3c20bf9c7ac5b9e156b647d2c75d76e861c8c09abc5191L175-R183) [[5]](diffhunk://#diff-fa8af80f4d87370d58e3a5b79df675d201f0c3aa753eda89cec03ff027c4213dL13-R21) [[6]](diffhunk://#diff-efa525dbdeceaeb5701aa3d2303bf1d533541f65a92d985f94f33b8e87b036d1R2-R37) These changes collectively deliver a new advanced tool for users to automatically rename PDFs, with robust parameter handling, user interface integration, and proper handling of filenames as determined by backend logic. --- ## Checklist ### General - [ ] I have read the [Contribution Guidelines](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/CONTRIBUTING.md) - [ ] I have read the [Stirling-PDF Developer Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md) (if applicable) - [ ] I have read the [How to add new languages to Stirling-PDF](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md) (if applicable) - [ ] I have performed a self-review of my own code - [ ] My changes generate no new warnings ### Documentation - [ ] I have updated relevant docs on [Stirling-PDF's doc repo](https://github.com/Stirling-Tools/Stirling-Tools.github.io/blob/main/docs/) (if functionality has heavily changed) - [ ] I have read the section [Add New Translation Tags](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/HowToAddNewLanguage.md#add-new-translation-tags) (for new translation tags only) ### UI Changes (if applicable) - [ ] Screenshots or videos demonstrating the UI changes are attached (e.g., as comments or direct attachments in the PR) ### Testing (if applicable) - [ ] I have tested my changes locally. Refer to the [Testing Guide](https://github.com/Stirling-Tools/Stirling-PDF/blob/main/devGuide/DeveloperGuide.md#6-testing) for more details. --------- Co-authored-by: Connor Yoh <connor@stirlingpdf.com> Co-authored-by: ConnorYoh <40631091+ConnorYoh@users.noreply.github.com> Co-authored-by: Reece Browne <74901996+reecebrowne@users.noreply.github.com>
205 lines
6.7 KiB
TypeScript
205 lines
6.7 KiB
TypeScript
import { useState, useMemo, useCallback, useRef, useEffect } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { Stack, Text, ScrollArea } from '@mantine/core';
|
|
import { ToolRegistryEntry } from '../../../data/toolsTaxonomy';
|
|
import { useToolSections } from '../../../hooks/useToolSections';
|
|
import { renderToolButtons } from '../shared/renderToolButtons';
|
|
import ToolSearch from '../toolPicker/ToolSearch';
|
|
import ToolButton from '../toolPicker/ToolButton';
|
|
|
|
interface ToolSelectorProps {
|
|
onSelect: (toolKey: string) => void;
|
|
excludeTools?: string[];
|
|
toolRegistry: Record<string, ToolRegistryEntry>; // Pass registry as prop to break circular dependency
|
|
selectedValue?: string; // For showing current selection when editing existing tool
|
|
placeholder?: string; // Custom placeholder text
|
|
}
|
|
|
|
export default function ToolSelector({
|
|
onSelect,
|
|
excludeTools = [],
|
|
toolRegistry,
|
|
selectedValue,
|
|
placeholder
|
|
}: ToolSelectorProps) {
|
|
const { t } = useTranslation();
|
|
const [opened, setOpened] = useState(false);
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [shouldAutoFocus, setShouldAutoFocus] = useState(false);
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
|
|
// Filter out excluded tools (like 'automate' itself)
|
|
const baseFilteredTools = useMemo(() => {
|
|
return Object.entries(toolRegistry).filter(([key]) => !excludeTools.includes(key));
|
|
}, [toolRegistry, excludeTools]);
|
|
|
|
// Apply search filter
|
|
const filteredTools = useMemo(() => {
|
|
if (!searchTerm.trim()) {
|
|
return baseFilteredTools;
|
|
}
|
|
|
|
const lowercaseSearch = searchTerm.toLowerCase();
|
|
return baseFilteredTools.filter(([key, tool]) => {
|
|
return (
|
|
tool.name.toLowerCase().includes(lowercaseSearch) ||
|
|
tool.description?.toLowerCase().includes(lowercaseSearch) ||
|
|
key.toLowerCase().includes(lowercaseSearch)
|
|
);
|
|
});
|
|
}, [baseFilteredTools, searchTerm]);
|
|
|
|
// Create filtered tool registry for ToolSearch
|
|
const filteredToolRegistry = useMemo(() => {
|
|
const registry: Record<string, ToolRegistryEntry> = {};
|
|
baseFilteredTools.forEach(([key, tool]) => {
|
|
registry[key] = tool;
|
|
});
|
|
return registry;
|
|
}, [baseFilteredTools]);
|
|
|
|
// Use the same tool sections logic as the main ToolPicker
|
|
const { sections, searchGroups } = useToolSections(filteredTools);
|
|
|
|
// Determine what to display: search results or organized sections
|
|
const isSearching = searchTerm.trim().length > 0;
|
|
const displayGroups = useMemo(() => {
|
|
if (isSearching) {
|
|
return searchGroups || [];
|
|
}
|
|
|
|
if (!sections || sections.length === 0) {
|
|
// If no sections, create a simple group from filtered tools
|
|
if (baseFilteredTools.length > 0) {
|
|
return [{
|
|
name: 'All Tools',
|
|
subcategoryId: 'all' as any,
|
|
tools: baseFilteredTools.map(([key, tool]) => ({ id: key, tool }))
|
|
}];
|
|
}
|
|
return [];
|
|
}
|
|
|
|
// Find the "all" section which contains all tools without duplicates
|
|
const allSection = sections.find(s => (s as any).key === 'all');
|
|
return allSection?.subcategories || [];
|
|
}, [isSearching, searchGroups, sections, baseFilteredTools]);
|
|
|
|
const handleToolSelect = useCallback((toolKey: string) => {
|
|
onSelect(toolKey);
|
|
setOpened(false);
|
|
setSearchTerm(''); // Clear search to show the selected tool display
|
|
}, [onSelect]);
|
|
|
|
const renderedTools = useMemo(() =>
|
|
displayGroups.map((subcategory) =>
|
|
renderToolButtons(t, subcategory, null, handleToolSelect, !isSearching, true)
|
|
), [displayGroups, handleToolSelect, isSearching, t]
|
|
);
|
|
|
|
const handleSearchFocus = () => {
|
|
setOpened(true);
|
|
setShouldAutoFocus(true); // Request auto-focus for the input
|
|
};
|
|
|
|
// Handle click outside to close dropdown
|
|
useEffect(() => {
|
|
const handleClickOutside = (event: MouseEvent) => {
|
|
if (containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
|
setOpened(false);
|
|
setSearchTerm('');
|
|
}
|
|
};
|
|
|
|
if (opened) {
|
|
document.addEventListener('mousedown', handleClickOutside);
|
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
|
}
|
|
}, [opened]);
|
|
|
|
|
|
const handleSearchChange = (value: string) => {
|
|
setSearchTerm(value);
|
|
if (!opened) {
|
|
setOpened(true);
|
|
}
|
|
};
|
|
|
|
const handleInputFocus = () => {
|
|
if (!opened) {
|
|
setOpened(true);
|
|
}
|
|
// Clear auto-focus flag since input is now focused
|
|
setShouldAutoFocus(false);
|
|
};
|
|
|
|
// Get display value for selected tool
|
|
const getDisplayValue = () => {
|
|
if (selectedValue && toolRegistry[selectedValue]) {
|
|
return toolRegistry[selectedValue].name;
|
|
}
|
|
return placeholder || t('automate.creation.tools.add', 'Add a tool...');
|
|
};
|
|
|
|
return (
|
|
<div ref={containerRef} className='rounded-xl'>
|
|
{/* Always show the target - either selected tool or search input */}
|
|
|
|
{selectedValue && toolRegistry[selectedValue] && !opened ? (
|
|
// Show selected tool in AutomationEntry style when tool is selected and dropdown closed
|
|
<div onClick={handleSearchFocus} style={{ cursor: 'pointer',
|
|
borderRadius: "var(--mantine-radius-lg)" }}>
|
|
<ToolButton id='tool' tool={toolRegistry[selectedValue]} isSelected={false}
|
|
onSelect={()=>{}} rounded={true} disableNavigation={true}></ToolButton>
|
|
</div>
|
|
) : (
|
|
// Show search input when no tool selected OR when dropdown is opened
|
|
<ToolSearch
|
|
value={searchTerm}
|
|
onChange={handleSearchChange}
|
|
toolRegistry={filteredToolRegistry}
|
|
mode="unstyled"
|
|
placeholder={getDisplayValue()}
|
|
hideIcon={true}
|
|
onFocus={handleInputFocus}
|
|
autoFocus={shouldAutoFocus}
|
|
/>
|
|
)}
|
|
|
|
{/* Custom dropdown */}
|
|
{opened && (
|
|
<div
|
|
style={{
|
|
position: 'absolute',
|
|
top: '100%',
|
|
left: 0,
|
|
right: 0,
|
|
zIndex: 1000,
|
|
backgroundColor: 'var(--mantine-color-body)',
|
|
border: '1px solid var(--mantine-color-gray-3)',
|
|
borderRadius: 'var(--mantine-radius-sm)',
|
|
boxShadow: 'var(--mantine-shadow-sm)',
|
|
marginTop: '4px',
|
|
minWidth: '16rem'
|
|
}}
|
|
>
|
|
<ScrollArea h={350}>
|
|
<Stack gap="sm" p="sm">
|
|
{displayGroups.length === 0 ? (
|
|
<Text size="sm" c="dimmed" ta="center" p="md">
|
|
{isSearching
|
|
? t('tools.noSearchResults', 'No tools found')
|
|
: t('tools.noTools', 'No tools available')
|
|
}
|
|
</Text>
|
|
) : (
|
|
renderedTools
|
|
)}
|
|
</Stack>
|
|
</ScrollArea>
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|