From 5e02bfc81e6d1fe9236ea14f2d6f74fc0cc7717e Mon Sep 17 00:00:00 2001 From: Heiko Joerg Schick Date: Sat, 22 Mar 2025 16:01:10 +0100 Subject: [PATCH] Refactor InsertTitles component for better maintainability and performance --- src/taskpane/components/InsertTitles.tsx | 385 ++++++++++++++--------- 1 file changed, 244 insertions(+), 141 deletions(-) diff --git a/src/taskpane/components/InsertTitles.tsx b/src/taskpane/components/InsertTitles.tsx index d8272f27..567930ed 100644 --- a/src/taskpane/components/InsertTitles.tsx +++ b/src/taskpane/components/InsertTitles.tsx @@ -1,172 +1,275 @@ import * as React from "react"; -import { - Button -} from "@fluentui/react-components"; -import { - TextBulletListRegular -} from "@fluentui/react-icons"; +import { TextBulletListRegular } from "@fluentui/react-icons"; import { useStatusContext } from "./App"; import { useCommonStyles } from "./commonStyles"; +import ActionButton from "./ActionButton"; +import { getErrorMessage } from "../types/office-types"; + +// Configuration constants +const TITLE_POSITION_THRESHOLD = 150; // Pixels from top of slide to consider a shape as a title +const INCLUDE_SLIDE_NUMBERS = false; // Whether to include slide numbers in the output + +// Title collection options +interface TitleCollectionOptions { + includeSlideNumbers: boolean; +} + +// Title collection result +interface TitleCollectionResult { + titleText: string; + titlesCollected: number; +} export const InsertTitles: React.FC = () => { const styles = useCommonStyles(); - const { - statusMessage, setStatusMessage, - statusType, setStatusType, - isProcessing, setIsProcessing - } = useStatusContext(); + const { setStatusMessage, setStatusType } = useStatusContext(); + + /** + * Validates that a text box is selected + * @param shapes The collection of selected shapes + * @param context The PowerPoint request context + * @returns The selected text box shape or null if validation fails + */ + const validateSelection = async ( + shapes: PowerPoint.ShapeScopedCollection, + context: PowerPoint.RequestContext + ): Promise => { + // Check if any shape is selected + if (shapes.items.length === 0) { + setStatusMessage("Please select a text box to insert slide titles."); + setStatusType("warning"); + return null; + } + + // Get the first selected shape + const selectedShape = shapes.items[0]; + + // Check if the selected shape has a text frame + selectedShape.load("textFrame"); + await context.sync(); + + if (!selectedShape.textFrame) { + setStatusMessage("Please select a text box to insert slide titles."); + setStatusType("warning"); + return null; + } + + return selectedShape; + }; + + /** + * Collects titles from all slides in the presentation + * @param slides The collection of slides + * @param context The PowerPoint request context + * @param options Options for title collection + * @returns The collected titles and count + */ + const collectSlideTitles = async ( + slides: PowerPoint.SlideCollection, + context: PowerPoint.RequestContext, + options: TitleCollectionOptions = { includeSlideNumbers: INCLUDE_SLIDE_NUMBERS } + ): Promise => { + let titleText = ""; + let titlesCollected = 0; - const collectAndInsertTitles = async () => { - setIsProcessing(true); try { - await PowerPoint.run(async (context) => { - try { - // Get the selected shape - const selectedShapes = context.presentation.getSelectedShapes(); - selectedShapes.load("items"); - await context.sync(); - - // Check if any shape is selected - if (selectedShapes.items.length === 0) { - setStatusMessage("Please select a text box to insert slide titles."); - setStatusType("warning"); - return; - } - - // Get the first selected shape - const selectedShape = selectedShapes.items[0]; + // Process each slide + for (let i = 0; i < slides.items.length; i++) { + const slide = slides.items[i]; + + // Load shapes collection + slide.load("shapes"); + await context.sync(); + + const shapes = slide.shapes; + shapes.load("items"); + await context.sync(); + + // Find the title shape on this slide + const titleShape = await findTitleShape(shapes, context, i + 1); + + if (titleShape) { + // Format the title text based on options + const slideTitle = titleShape.text.trim(); + const formattedTitle = options.includeSlideNumbers + ? `Slide ${i + 1}: ${slideTitle}` + : slideTitle; - // Check if the selected shape has a text frame - selectedShape.load("textFrame"); + // Add to our collection + titleText += `${formattedTitle}\n`; + titlesCollected++; + } + } + } catch (error) { + console.error("Error collecting slide titles:", getErrorMessage(error)); + } + + return { titleText, titlesCollected }; + }; + + /** + * Finds the title shape on a slide + * @param shapes The collection of shapes on a slide + * @param context The PowerPoint request context + * @param slideNumber The slide number (for logging) + * @returns The title shape text or null if not found + */ + const findTitleShape = async ( + shapes: PowerPoint.ShapeCollection, + context: PowerPoint.RequestContext, + slideNumber: number + ): Promise<{ text: string } | null> => { + try { + // Batch load textFrame for all shapes + for (let j = 0; j < shapes.items.length; j++) { + shapes.items[j].load("textFrame"); + } + await context.sync(); + + // Process shapes with text frames + for (let j = 0; j < shapes.items.length; j++) { + const shape = shapes.items[j]; + + if (shape.textFrame) { + // Load text range for shapes with text frames + shape.textFrame.load("textRange"); await context.sync(); - - if (!selectedShape.textFrame) { - setStatusMessage("Please select a text box to insert slide titles."); - setStatusType("warning"); - return; - } - - // Get all slides in the presentation - const slides = context.presentation.slides; - slides.load("items"); - await context.sync(); - - // Collect all slide titles using a simple approach - let titleText = ""; - let titlesCollected = 0; - - // Process each slide - for (let i = 0; i < slides.items.length; i++) { - try { - const slide = slides.items[i]; - - // Load only necessary shape properties - slide.load("shapes"); + + if (shape.textFrame.textRange) { + shape.textFrame.textRange.load("text"); + await context.sync(); + + const shapeText = shape.textFrame.textRange.text; + + // Check if the shape has text + if (shapeText && shapeText.trim() !== "") { + // Load position to check if it's at the top + shape.load("top"); await context.sync(); - // Get title text using a simplified approach - // This gets all the shapes and looks for one with text content positioned at the top - const shapes = slide.shapes; - shapes.load("items"); - await context.sync(); - - // Look for shapes with text content - for (let j = 0; j < shapes.items.length; j++) { - try { - const shape = shapes.items[j]; - - // Only load textFrame property initially - shape.load("textFrame"); - await context.sync(); - - // Only proceed with shapes that have text frames - if (shape.textFrame) { - // Load text range to see if there's content - shape.textFrame.load("textRange"); - await context.sync(); - - if (shape.textFrame.textRange) { - shape.textFrame.textRange.load("text"); - await context.sync(); - - const shapeText = shape.textFrame.textRange.text; - - // Check if the shape has text and might be a title - if (shapeText && shapeText.trim() !== "") { - // Load position to see if it's at the top - shape.load("top"); - await context.sync(); - - // Titles are usually at the top of the slide - // Since we can't directly identify a title placeholder, - // we're using position as a heuristic - if (shape.top < 150) { - // Add the title to our collection - // titleText += `Slide ${i+1}: ${shapeText}\n`; - titleText += `${shapeText}\n`; - titlesCollected++; - break; // Only use the first potential title shape on each slide - } - } - } - } - } catch (shapeError) { - console.error(`Error processing shape on slide ${i+1}:`, shapeError); - // Continue to the next shape - continue; - } + // Use position as a heuristic for identifying titles + if (shape.top < TITLE_POSITION_THRESHOLD) { + return { text: shapeText }; } - } catch (slideError) { - console.error(`Error processing slide ${i+1}:`, slideError); - // Continue to the next slide - continue; } } - - // Insert the collected titles into the selected text frame - if (titleText) { - // Make sure we have a textRange on the selected shape - selectedShape.textFrame.load("textRange"); - await context.sync(); - - selectedShape.textFrame.textRange.text = titleText; - await context.sync(); - - setStatusMessage(`Collected and inserted ${titlesCollected} slide titles.`); - setStatusType("success"); - } else { - setStatusMessage("No slide titles found to insert."); - setStatusType("warning"); - } - } catch (innerError) { - console.error("Inner error:", innerError); - throw innerError; // Re-throw to outer catch } + } + } catch (error) { + console.error(`Error finding title on slide ${slideNumber}:`, getErrorMessage(error)); + } + + return null; + }; + + /** + * Inserts collected titles into a shape + * @param shape The shape to insert titles into + * @param titleText The collected title text + * @param context The PowerPoint request context + * @returns True if insertion was successful + */ + const insertTitlesIntoShape = async ( + shape: PowerPoint.Shape, + titleText: string, + context: PowerPoint.RequestContext + ): Promise => { + try { + // Make sure we have a textRange on the selected shape + shape.textFrame.load("textRange"); + await context.sync(); + + shape.textFrame.textRange.text = titleText; + await context.sync(); + return true; + } catch (error) { + console.error("Error inserting titles:", getErrorMessage(error)); + return false; + } + }; + + /** + * Generates an appropriate status message based on the results + * @param titlesCollected The number of titles collected + * @param insertSuccess Whether insertion was successful + * @returns The formatted status message + */ + const generateStatusMessage = ( + titlesCollected: number, + insertSuccess: boolean + ): string => { + if (titlesCollected === 0) { + return "No slide titles found to insert."; + } + + if (!insertSuccess) { + return "Error inserting titles into the selected shape."; + } + + return `Collected and inserted ${titlesCollected} slide titles.`; + }; + + /** + * Main function to collect and insert slide titles + */ + const collectAndInsertTitles = async (): Promise => { + try { + await PowerPoint.run(async (context) => { + // Get the selected shapes + const selectedShapes = context.presentation.getSelectedShapes(); + selectedShapes.load("items"); + await context.sync(); + + // Validate selection and get the target shape + const targetShape = await validateSelection(selectedShapes, context); + if (!targetShape) { + return; + } + + // Get all slides in the presentation + const slides = context.presentation.slides; + slides.load("items"); + await context.sync(); + + // Collect titles from all slides + const { titleText, titlesCollected } = await collectSlideTitles( + slides, + context, + { includeSlideNumbers: INCLUDE_SLIDE_NUMBERS } + ); + + // Insert titles if any were found + let insertSuccess = false; + if (titleText) { + insertSuccess = await insertTitlesIntoShape(targetShape, titleText, context); + } + + // Update status message + const statusMessage = generateStatusMessage(titlesCollected, insertSuccess); + setStatusMessage(statusMessage); + setStatusType( + titlesCollected === 0 ? "warning" : + insertSuccess ? "success" : "error" + ); }); } catch (error) { - setStatusMessage(`Error: ${error.message}`); + console.error("Error in collectAndInsertTitles:", getErrorMessage(error)); + setStatusMessage(`Error: ${getErrorMessage(error)}`); setStatusType("error"); - console.error("Collect titles error:", error); - } finally { - setIsProcessing(false); } }; return (
- + onClick={collectAndInsertTitles} + />
); }; -export default InsertTitles; \ No newline at end of file +export default InsertTitles;