Refactor InsertTitles component for better maintainability and performance

This commit is contained in:
2025-03-22 16:01:10 +01:00
parent ba304f0047
commit 5e02bfc81e
+241 -138
View File
@@ -1,169 +1,272 @@
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<PowerPoint.Shape | null> => {
// 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<TitleCollectionResult> => {
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");
// 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;
// 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();
// Check if any shape is selected
if (selectedShapes.items.length === 0) {
setStatusMessage("Please select a text box to insert slide titles.");
setStatusType("warning");
return;
}
if (shape.textFrame.textRange) {
shape.textFrame.textRange.load("text");
await context.sync();
// Get the first selected shape
const selectedShape = selectedShapes.items[0];
const shapeText = shape.textFrame.textRange.text;
// 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;
}
// 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");
// 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<boolean> => {
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<void> => {
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 (
<div className={styles.container}>
<div className={styles.buttonGroup}>
<Button
appearance="primary"
className={styles.actionButton}
onClick={collectAndInsertTitles}
<ActionButton
title="Insert Titles"
icon={<TextBulletListRegular />}
disabled={isProcessing}
>
Insert Titles
</Button>
onClick={collectAndInsertTitles}
/>
</div>
</div>
);